From 1b4a93b0608b5481d18089c2600f04adf91e9c79 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 21 Nov 2025 02:12:17 -0500 Subject: [PATCH] release(v1.9.14): inline folder rows, synced folder icons, and compact theme polish --- CHANGELOG.md | 18 + public/css/styles.css | 785 ++++++------------------------------- public/js/appCore.js | 6 +- public/js/authModals.js | 89 ++++- public/js/domUtils.js | 10 +- public/js/fileListView.js | 659 ++++++++++++++++++++++++++++--- public/js/folderManager.js | 1 - public/js/i18n.js | 10 +- src/models/FolderModel.php | 186 +++++---- 9 files changed, 942 insertions(+), 822 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5833d28..a0542fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## Changes 11/21/2025 (v1.9.14) + +release(v1.9.14): inline folder rows, synced folder icons, and compact theme polish + +- Add ACL-aware folder stats and byte counts in FolderModel::countVisible() +- Show subfolders inline as rows above files in table view (Explorer-style) +- Page folders + files together and wire folder rows into existing DnD and context menu flows +- Add folder action buttons (move/rename/color/share) with capability checks from /api/folder/capabilities.php +- Cache folder capabilities and owners to avoid repeat calls per row +- Add user settings to toggle folder strip and inline folder rows (stored in localStorage) +- Default itemsPerPage to 50 and remember current page across renders +- Sync inline folder icon size to file row height and tweak vertical alignment for different row heights +- Update table headers + i18n keys to use Name / Size / Modified / Created / Owner labels +- Compact and consolidate light/dark theme CSS, search pill, pagination, and font-size controls +- Tighten file action button hit areas and add specific styles for folder move/rename buttons + +--- + ## Changes 11/20/2025 (v1.9.13) release(v1.9.13): style(ui): compact dual-theme polish for lists, inputs, search & modals diff --git a/public/css/styles.css b/public/css/styles.css index 8945feb..ad7ff6d 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -614,7 +614,8 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover{background-color: rgba #fileList button.edit-btn{background-color: #007bff; color: white;} .rename-btn .material-icons, - #renameFolderBtn .material-icons{color: black !important;} + #renameFolderBtn .material-icons, + .folder-rename-btn .material-icons{color: black !important;} #fileList table{background-color: transparent; border-collapse: collapse !important; border-spacing: 0 !important; @@ -818,9 +819,34 @@ label{font-size: 0.9rem;} .folder-actions .btn, .folder-actions .material-icons{transition: none;} } -#moveFolderBtn{background-color: #ff9800; +#moveFolderBtn, +.folder-move-btn{background-color: #ff9800; border-color: #ff9800; - color: #fff;} + color: #fff; + } + #moveFolderBtn:hover:not(:disabled):not(.disabled), +.folder-move-btn:hover:not(:disabled):not(.disabled) { + background-color: #fb8c00; /* slightly darker */ + border-color: #fb8c00; +} + +/* Active/pressed (only when enabled) */ +#moveFolderBtn:active:not(:disabled):not(.disabled), +.folder-move-btn:active:not(:disabled):not(.disabled) { + background-color: #f57c00; + border-color: #f57c00; +} + +/* Disabled state (both attribute + .disabled class) */ +#moveFolderBtn:disabled, +#moveFolderBtn.disabled, +.folder-move-btn:disabled, +.folder-move-btn.disabled { + background-color: #ffb74d; + border-color: #ffb74d; + color: #fff; + opacity: 0.55; +} .row-selected{background-color: #f2f2f2 !important;} .dark-mode .row-selected{background-color: #444 !important; color: #fff !important;} @@ -947,7 +973,8 @@ label{font-size: 0.9rem;} transform: none !important; box-shadow: none !important;} } -.btn-group.btn-group-sm[aria-label="File actions"] .btn{padding: .2rem !important; + +.btn-group.btn-group-sm[aria-label="File actions"] .btn{padding: .8rem !important; width: 32px; height: 32px; line-height: 1 !important; @@ -978,6 +1005,7 @@ label{font-size: 0.9rem;} .btn-group.btn-group-sm[aria-label="File actions"] .btn .material-symbols-rounded{transition: none !important; transform: none !important;} } + .breadcrumb-link{cursor: pointer; color: #007bff; text-decoration: underline;} @@ -1693,8 +1721,6 @@ body.dark-mode .folder-strip-container .folder-item:hover{background-color: rgba --filr-folder-stroke:#a87312; --filr-paper-fill: #ffffff; --filr-paper-stroke: #9fb3d6; - - --row-h: 28px; --twisty: 24px; --twisty-gap: -5px; @@ -1841,7 +1867,6 @@ body.dark-mode #folderTreeContainer .folder-icon .lock-keyhole{fill: rgba(255,25 align-items: center; gap: 8px; justify-content: center; - border-radius: 10px; border: 1px solid var(--tree-ghost-border); background: var(--tree-ghost-bg); color: var(--tree-ghost-fg); @@ -1971,653 +1996,99 @@ body.dark-mode #folderTreeContainer .folder-icon .lock-keyhole{fill: rgba(255,25 /* ============================================ FileRise polish – compact theme layer ============================================ */ - -/* Tokens */ -:root { - --filr-radius-lg: 14px; - --filr-radius-xl: 18px; - --filr-shadow-soft: 0 12px 35px rgba(15,23,42,.14); - --filr-shadow-subtle: 0 8px 20px rgba(15,23,42,.10); - --filr-header-blur: 18px; - --filr-transition-fast: 150ms ease-out; - --filr-transition-med: 220ms cubic-bezier(.22,.61,.36,1); - - /* Dark theme */ - --fr-bg-dark: #0f0f0f; - --fr-surface-dark: #212121; - --fr-surface-dark-2: #181818; - --fr-border-dark: #303030; - --fr-muted-dark: #aaaaaa; - - /* Light theme */ - --fr-bg-light: #f9f9f9; - --fr-surface-light: #ffffff; - --fr-surface-light-2: #f1f1f1; - --fr-border-light: #e5e5e5; - --fr-muted-light: #606060; -} - -/* Pro badge */ -.btn-pro-admin { - background: linear-gradient(135deg, #ff9800, #ff5722); - border-color: #ff9800; - color: #1b0f00 !important; - font-weight: 600; - box-shadow: 0 0 10px rgba(255,152,0,.4); -} - -/* Toast base shape (colors themed below) */ -#customToast { - border-radius: 999px; -} - -/* Folder tree row rounding */ -#folderTreeContainer .folder-row { border-radius: 8px; } - -/* Buttons – pill style + hover lift */ -.btn, -#customChooseBtn { - border-radius: 999px; - font-weight: 500; - border: 1px solid transparent; - transition: - background-color var(--filr-transition-fast), - box-shadow var(--filr-transition-fast), - transform var(--filr-transition-fast), - border-color var(--filr-transition-fast); -} - -/* Upload / create / primary: shared shadow in light + dark */ -.btn-primary, -#createBtn, -#uploadBtn { - box-shadow: 0 2px 4px rgba(0,0,0,.6); -} - -.btn-primary:hover, -#createBtn:hover, -#uploadBtn:hover { - filter: brightness(1.04); - transform: translateY(-1px); - box-shadow: 0 10px 22px rgba(0,140,180,.28); -} - -/* Destructive buttons */ -#deleteSelectedBtn, -#deleteAllBtn, -#deleteTrashSelectedBtn { - border-color: rgba(248,113,113,.9); - box-shadow: 0 8px 18px rgba(248,113,113,.35); -} - -/* Forms & inputs – base */ -input[type="text"], -input[type="password"], -input[type="email"], -input[type="url"], -select, -textarea { - border-radius: 10px; - padding: 8px 10px; - font-size: .92rem; - transition: - border-color var(--filr-transition-fast), - box-shadow var(--filr-transition-fast), - background-color var(--filr-transition-fast); -} - -input:focus, -select:focus, -textarea:focus { - outline: none; - border-color: var(--filr-accent-500); - box-shadow: 0 0 0 1px var(--filr-accent-ring); -} - -/* Modals – subtle blur baseline (overridden per theme) */ -.modal { - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); -} - -/* Core elevated surfaces */ -#fileListContainer, -#uploadCard, -#folderManagementCard, -.card, -.admin-panel-content { - border-radius: var(--filr-radius-xl); - border: 1px solid rgba(15,23,42,.06); - background: #ffffff; - box-shadow: var(--filr-shadow-subtle); -} - -/* Full-height body */ -body { min-height: 100vh; } - -/* ============================================ - Dark theme -============================================ */ - -body.dark-mode { - background: var(--fr-bg-dark) !important; - color: #f1f1f1 !important; - background-image: none !important; -} - -/* Main surfaces */ -body.dark-mode #fileListContainer, -body.dark-mode #uploadCard, -body.dark-mode #folderManagementCard, -body.dark-mode .card, -body.dark-mode .admin-panel-content, -body.dark-mode .media-topbar { - background: var(--fr-surface-dark) !important; - border-color: var(--fr-border-dark) !important; - box-shadow: 0 1px 4px rgba(0,0,0,.9) !important; - backdrop-filter: none !important; - -webkit-backdrop-filter: none !important; -} - -/* Remove inner “glass” highlight if present */ -body.dark-mode #fileListContainer::before, -body.dark-mode #uploadCard::before, -body.dark-mode #folderManagementCard::before, -body.dark-mode .card::before, -body.dark-mode .admin-panel-content::before { - box-shadow: none !important; -} - -/* Section headers */ -body.dark-mode .card-header, -body.dark-mode .custom-folder-card-body .drag-header { - background: var(--fr-surface-dark-2) !important; - border-bottom: 1px solid var(--fr-border-dark) !important; -} - -/* File list header / rows */ -body.dark-mode #fileList table thead th { - background: var(--fr-surface-dark-2) !important; - border-bottom: 1px solid var(--fr-border-dark) !important; -} - -body.dark-mode #fileList table.filr-table tbody tr:hover:not(.selected,.row-selected,.selected-row,.is-selected)>td { - background: rgba(255,255,255,.04) !important; - box-shadow: none !important; -} - -body.dark-mode #fileList table.filr-table tbody tr.selected>td, -body.dark-mode #fileList table.filr-table tbody tr.row-selected>td, -body.dark-mode #fileList table.filr-table tbody tr.selected-row>td, -body.dark-mode #fileList table.filr-table tbody tr.is-selected>td { - background: rgba(62,166,255,.16) !important; - box-shadow: none !important; -} - -/* Dark modals */ -body.dark-mode .modal { - background-color: rgba(0,0,0,.65) !important; - backdrop-filter: none !important; - -webkit-backdrop-filter: none !important; -} - -body.dark-mode .modal .modal-content, -body.dark-mode .editor-modal, -body.dark-mode .image-preview-modal-content, -body.dark-mode #restoreFilesModal .modal-content, -body.dark-mode #downloadProgressModal .modal-content { - background: var(--fr-surface-dark) !important; - border-radius: 12px !important; - border: 1px solid var(--fr-border-dark) !important; - box-shadow: 0 8px 24px rgba(0,0,0,.9) !important; -} - -body.dark-mode .modal .modal-content::before, -body.dark-mode .editor-modal::before, -body.dark-mode .image-preview-modal-content::before, -body.dark-mode #restoreFilesModal .modal-content::before, -body.dark-mode #downloadProgressModal .modal-content::before { - box-shadow: none !important; -} - -/* Dark inputs */ -body.dark-mode input[type="text"], -body.dark-mode input[type="password"], -body.dark-mode input[type="email"], -body.dark-mode input[type="url"], -body.dark-mode select, -body.dark-mode textarea { - background: #121212 !important; - border-color: #3d3d3d !important; - color: #f1f1f1 !important; -} - -body.dark-mode input::placeholder, -body.dark-mode textarea::placeholder { color: #777 !important; } - -body.dark-mode input:focus, -body.dark-mode select:focus, -body.dark-mode textarea:focus { - border-color: #3ea6ff !important; - box-shadow: 0 0 0 1px rgba(62,166,255,.7) !important; -} - -/* Dark destructive buttons */ -body.dark-mode #deleteSelectedBtn, -body.dark-mode #deleteAllBtn, -body.dark-mode #deleteTrashSelectedBtn { - background-color: #b3261e !important; - border-color: #b3261e !important; - box-shadow: 0 4px 10px rgba(0,0,0,.7) !important; -} - -/* Dark folder strip */ -body.dark-mode .folder-strip-container.folder-strip-mobile { - background: var(--fr-surface-dark-2) !important; - border: 1px solid var(--fr-border-dark) !important; -} - -/* Dark toast */ -body.dark-mode #customToast { - background: #212121 !important; - border: 1px solid var(--fr-border-dark) !important; - box-shadow: 0 8px 20px rgba(0,0,0,.9) !important; -} - -/* Dark meta text */ -body.dark-mode #fileSummary { color: var(--fr-muted-dark) !important; } - -/* Menus & panels (dark) */ -body.dark-mode #createMenu, -body.dark-mode .user-dropdown .user-menu, -body.dark-mode #fileContextMenu, -body.dark-mode #folderContextMenu, -body.dark-mode #folderManagerContextMenu, -body.dark-mode #adminPanelModal .modal-content, -body.dark-mode #userPermissionsModal .modal-content, -body.dark-mode #userFlagsModal .modal-content, -body.dark-mode #userGroupsModal .modal-content, -body.dark-mode #userPanelModal .modal-content, -body.dark-mode #groupAclModal .modal-content, -body.dark-mode .editor-modal, -body.dark-mode #filePreviewModal .modal-content, -body.dark-mode #loginForm, -body.dark-mode .editor-header { - background: var(--fr-surface-dark) !important; - border: 1px solid var(--fr-border-dark) !important; - color: #f1f1f1 !important; - border-radius: 12px !important; - box-shadow: 0 8px 24px rgba(0,0,0,.9) !important; -} - -body.dark-mode .user-dropdown .user-menu, -body.dark-mode #createMenu, -body.dark-mode #fileContextMenu, -body.dark-mode #folderContextMenu, -body.dark-mode #folderManagerContextMenu { - background-clip: padding-box; -} - -/* ============================================ - Light theme -============================================ */ - -body:not(.dark-mode) { - background: var(--fr-bg-light) !important; - color: #111 !important; - background-image: none !important; -} - -/* Light surfaces */ -body:not(.dark-mode) #fileListContainer, -body:not(.dark-mode) #uploadCard, -body:not(.dark-mode) #folderManagementCard, -body:not(.dark-mode) .card, -body:not(.dark-mode) .admin-panel-content { - background: var(--fr-surface-light) !important; - border-color: var(--fr-border-light) !important; - box-shadow: 0 3px 8px rgba(0,0,0,.04) !important; - backdrop-filter: none !important; - -webkit-backdrop-filter: none !important; -} - -/* Remove inner highlight */ -body:not(.dark-mode) #fileListContainer::before, -body:not(.dark-mode) #uploadCard::before, -body:not(.dark-mode) #folderManagementCard::before, -body:not(.dark-mode) .card::before, -body:not(.dark-mode) .admin-panel-content::before { - box-shadow: none !important; -} - -/* Light section headers */ -body:not(.dark-mode) .card-header, -body:not(.dark-mode) .custom-folder-card-body .drag-header { - background: var(--fr-surface-light-2) !important; - border-bottom: 1px solid var(--fr-border-light) !important; -} - -/* Light file list */ -body:not(.dark-mode) #fileList table thead th { - background: var(--fr-surface-light-2) !important; - border-bottom: 1px solid var(--fr-border-light) !important; -} - -body:not(.dark-mode) #fileList table.filr-table tbody tr:hover:not(.selected,.row-selected,.selected-row,.is-selected)>td { - background: rgba(0,0,0,.02) !important; - box-shadow: none !important; -} - -body:not(.dark-mode) #fileList table.filr-table tbody tr.selected>td, -body:not(.dark-mode) #fileList table.filr-table tbody tr.row-selected>td, -body:not(.dark-mode) #fileList table.filr-table tbody tr.selected-row>td, -body:not(.dark-mode) #fileList table.filr-table tbody tr.is-selected>td { - background: rgba(33,150,243,.12) !important; - box-shadow: none !important; -} - -/* Light modals */ -body:not(.dark-mode) .modal { - background-color: rgba(0,0,0,.4) !important; - backdrop-filter: none !important; - -webkit-backdrop-filter: none !important; -} - -body:not(.dark-mode) .modal .modal-content, -body:not(.dark-mode) .editor-modal, -body:not(.dark-mode) .image-preview-modal-content, -body:not(.dark-mode) #restoreFilesModal .modal-content, -body:not(.dark-mode) #downloadProgressModal .modal-content { - background: var(--fr-surface-light) !important; - border-radius: 12px !important; - border: 1px solid var(--fr-border-light) !important; - box-shadow: 0 8px 24px rgba(0,0,0,.18) !important; -} - -body:not(.dark-mode) .modal .modal-content::before, -body:not(.dark-mode) .editor-modal::before, -body:not(.dark-mode) .image-preview-modal-content::before, -body:not(.dark-mode) #restoreFilesModal .modal-content::before, -body:not(.dark-mode) #downloadProgressModal .modal-content::before { - box-shadow: none !important; -} - -/* Light inputs */ -body:not(.dark-mode) input[type="text"], -body:not(.dark-mode) input[type="password"], -body:not(.dark-mode) input[type="email"], -body:not(.dark-mode) input[type="url"], -body:not(.dark-mode) select, -body:not(.dark-mode) textarea { - background: #fff !important; - border-color: #d0d0d0 !important; - color: #111 !important; -} - -body:not(.dark-mode) input::placeholder, -body:not(.dark-mode) textarea::placeholder { - color: #9e9e9e !important; -} - -body:not(.dark-mode) input:focus, -body:not(.dark-mode) select:focus, -body:not(.dark-mode) textarea:focus { - border-color: #2196f3 !important; - box-shadow: 0 0 0 1px rgba(33,150,243,.55) !important; -} - -/* Light destructive buttons */ -body:not(.dark-mode) #deleteSelectedBtn, -body:not(.dark-mode) #deleteAllBtn, -body:not(.dark-mode) #deleteTrashSelectedBtn { - box-shadow: 0 2px 6px rgba(244,67,54,.3) !important; -} - -/* Light folder strip */ -body:not(.dark-mode) .folder-strip-container.folder-strip-mobile { - background: #f1f1f1 !important; - border: 1px solid var(--fr-border-light) !important; -} - -/* Light toast */ -body:not(.dark-mode) #customToast { - background: #212121 !important; - color: #fff !important; - border: 1px solid #000 !important; - box-shadow: 0 8px 18px rgba(0,0,0,.45) !important; -} - -/* Light meta text */ -body:not(.dark-mode) #fileSummary { color: var(--fr-muted-light) !important; } - -/* Menus & panels (light) */ -body:not(.dark-mode) #createMenu, -body:not(.dark-mode) .user-dropdown .user-menu, -body:not(.dark-mode) #fileContextMenu, -body:not(.dark-mode) #folderContextMenu, -body:not(.dark-mode) #folderManagerContextMenu, -body:not(.dark-mode) #adminPanelModal .modal-content, -body:not(.dark-mode) #userPermissionsModal .modal-content, -body:not(.dark-mode) #userFlagsModal .modal-content, -body:not(.dark-mode) #userGroupsModal .modal-content, -body:not(.dark-mode) #userPanelModal .modal-content, -body:not(.dark-mode) #groupAclModal .modal-content, -body:not(.dark-mode) .editor-modal, -body:not(.dark-mode) #filePreviewModal .modal-content, -body:not(.dark-mode) #loginForm -body:not(.dark-mode) .editor-header{ - background: var(--fr-surface-light) !important; - border: 1px solid var(--fr-border-light) !important; - color: #111 !important; - border-radius: 12px !important; - box-shadow: 0 4px 12px rgba(0,0,0,.12) !important; -} - -/* ============================================ - Search group / advanced search / pagination -============================================ */ - -/* Search icon + input */ -#searchIcon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 38px; - height: 36px; - padding: 0; - border-radius: 999px 0 0 999px; - border: 1px solid rgba(0,0,0,.18); - border-right: none; - background: #fff; - cursor: pointer; - box-shadow: none; - transform: none; -} - -#searchIcon .material-icons { - font-size: 20px; - line-height: 1; - color: #555; -} - -#searchIcon:hover { background: #f5f5f5; } - -#searchIcon + #searchInput { - height: 36px; - border-radius: 0 999px 999px 0; - border-left: none; - padding-top: 6px; - padding-bottom: 6px; -} - -/* Dark search */ -body.dark-mode #searchIcon { - background: #212121; - border-color: #3d3d3d; -} -body.dark-mode #searchIcon .material-icons { color: #f1f1f1; } -body.dark-mode #searchIcon:hover { background: #303030; } -body.dark-mode #searchIcon + #searchInput { border-left: none; } - -/* Advanced search toggle */ -#advancedSearchToggle { - border-radius: 999px; - border: 1px solid #d0d0d0; - padding: 6px 12px; - font-size: .9rem; - background: #f5f5f5; - color: #333; - cursor: pointer; - display: inline-flex; - align-items: center; - gap: 4px; - margin-right: 8px; - transition: background .15s ease, box-shadow .15s ease, transform .1s ease; -} -#advancedSearchToggle:hover, -#advancedSearchToggle:focus-visible { - background: #e8e8e8; - box-shadow: 0 1px 4px rgba(0,0,0,.16); - outline: none; - transform: translateY(-1px); -} -.dark-mode #advancedSearchToggle { - background: #2a2a2a; - border-color: #444; - color: #f1f1f1; -} -.dark-mode #advancedSearchToggle:hover, -.dark-mode #advancedSearchToggle:focus-visible { - background: #333; - box-shadow: 0 1px 4px rgba(0,0,0,.5); -} - -/* Prev / Next pagination */ -.custom-prev-next-btn { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 64px; - padding: 6px 14px; - font-size: 13px; - font-weight: 500; - border-radius: 999px; - border: 1px solid rgba(0,0,0,.14); - background: #f1f1f1; - color: #111; - cursor: pointer; - transition: - background-color 140ms ease-out, - border-color 140ms ease-out, - box-shadow 140ms ease-out, - transform 120ms ease-out; -} -.custom-prev-next-btn:not(:disabled):hover { - background: #e5e5e5; - border-color: rgba(0,0,0,.22); - box-shadow: 0 2px 6px rgba(0,0,0,.18); - transform: translateY(-1px); -} -.custom-prev-next-btn:not(:disabled):active { - transform: translateY(0); - box-shadow: 0 1px 3px rgba(0,0,0,.25); -} -.custom-prev-next-btn:disabled { - opacity: .5; - cursor: default; - box-shadow: none; -} -body.dark-mode .custom-prev-next-btn { - background: #212121; - border-color: #3d3d3d; - color: #f1f1f1; -} -body.dark-mode .custom-prev-next-btn:not(:disabled):hover { - background: #2a2a2a; - border-color: #4a4a4a; - box-shadow: 0 2px 6px rgba(0,0,0,.7); -} - -/* Normalize normal inputs (everything except the search pill) */ -input[type="text"]:not(#searchInput), -input[type="password"], -input[type="email"], -input[type="url"], -input[type="number"], -textarea, -select { - border: 1px solid rgba(148,163,184,.6) !important; - border-radius: 10px; - background: #ffffff; - box-sizing: border-box; -} -/* Compact font-size controls (A- / A+) */ -#decreaseFont, -#increaseFont { - display: inline-flex; - align-items: center; - justify-content: center; - margin-top: 5px; - height: 24px; - min-width: 30px; - padding: 2px 8px; - - font-size: 11px; - font-weight: 500; - line-height: 1; - border-radius: 999px; - - border: 1px solid rgba(0, 0, 0, 0.16); - background: #f5f5f5; - color: #222; - - cursor: pointer; - margin-left: 4px; - - transition: - background-color 140ms ease-out, - border-color 140ms ease-out, - box-shadow 140ms ease-out, - transform 120ms ease-out; -} - -/* Hover / active */ -#decreaseFont:not(:disabled):hover, -#increaseFont:not(:disabled):hover { - background: #e8e8e8; - border-color: rgba(0, 0, 0, 0.24); - box-shadow: 0 1px 4px rgba(0,0,0,0.18); - transform: translateY(-1px); -} - -#decreaseFont:not(:disabled):active, -#increaseFont:not(:disabled):active { - transform: translateY(5); - box-shadow: 0 1px 2px rgba(0,0,0,0.25); -} - -/* Disabled */ -#decreaseFont:disabled, -#increaseFont:disabled { - opacity: 0.5; - cursor: default; - box-shadow: none; -} - -/* Dark mode tweaks */ -body.dark-mode #decreaseFont, -body.dark-mode #increaseFont { - background: #212121; - border-color: #3d3d3d; - color: #f1f1f1; -} - -body.dark-mode #decreaseFont:not(:disabled):hover, -body.dark-mode #increaseFont:not(:disabled):hover { - background: #2a2a2a; - border-color: #4a4a4a; - box-shadow: 0 1px 4px rgba(0,0,0,0.7); -} -#closeEditorX { -margin-right: 10px; -} \ No newline at end of file +:root{--filr-radius-lg:14px;--filr-radius-xl:18px;--filr-shadow-soft:0 12px 35px rgba(15,23,42,.14);--filr-shadow-subtle:0 8px 20px rgba(15,23,42,.10);--filr-header-blur:18px;--filr-transition-fast:150ms ease-out;--filr-transition-med:220ms cubic-bezier(.22,.61,.36,1);--fr-bg-dark:#0f0f0f;--fr-surface-dark:#212121;--fr-surface-dark-2:#181818;--fr-border-dark:#303030;--fr-muted-dark:#aaaaaa;--fr-bg-light:#f9f9f9;--fr-surface-light:#ffffff;--fr-surface-light-2:#f1f1f1;--fr-border-light:#e5e5e5;--fr-muted-light:#606060} +.btn-pro-admin{background:linear-gradient(135deg,#ff9800,#ff5722);border-color:#ff9800;color:#1b0f00!important;font-weight:600;box-shadow:0 0 10px rgba(255,152,0,.4)} +#customToast{border-radius:999px} +#folderTreeContainer .folder-row{border-radius:8px} +.btn,#customChooseBtn, #colorFolderModal .btn-ghost, #cancelMoveFolder, #confirmMoveFolder, #cancelRenameFolder, #submitRenameFolder, #cancelDeleteFolder, #confirmDeleteFolder, #cancelCreateFolder, #submitCreateFolder{border-radius:999px;font-weight:500;border:1px solid transparent;transition:background-color var(--filr-transition-fast),box-shadow var(--filr-transition-fast),transform var(--filr-transition-fast),border-color var(--filr-transition-fast)} +.btn-primary,#createBtn,#uploadBtn,#submitCreateFolder,#submitRenameFolder,#confirmMoveFolder{box-shadow:0 2px 4px rgba(0,0,0,.6)} +.btn-primary:hover,#createBtn:hover,#uploadBtn:hover,#submitCreateFolder:hover,#submitRenameFolder:hover,#confirmMoveFolder:hover{filter:brightness(1.04);transform:translateY(-1px);box-shadow:0 10px 22px rgba(0,140,180,.28)} +#deleteSelectedBtn,#deleteAllBtn,#deleteTrashSelectedBtn,#deleteFolderBtn,#confirmDeleteFolder{border-color:rgba(248,113,113,.9);box-shadow:0 8px 18px rgba(248,113,113,.35)} +input[type=text],input[type=password],input[type=email],input[type=url],select,textarea{border-radius:10px;padding:8px 10px;font-size:.92rem;transition:border-color var(--filr-transition-fast),box-shadow var(--filr-transition-fast),background-color var(--filr-transition-fast)} +input:focus,select:focus,textarea:focus{outline:none;border-color:var(--filr-accent-500);box-shadow:0 0 0 1px var(--filr-accent-ring)} +.modal{backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)} +#fileListContainer,#uploadCard,#folderManagementCard,.card,.admin-panel-content{border-radius:var(--filr-radius-xl);border:1px solid rgba(15,23,42,.06);background:#ffffff;box-shadow:var(--filr-shadow-subtle)} +body{min-height:100vh} +body.dark-mode{background:var(--fr-bg-dark)!important;color:#f1f1f1!important;background-image:none!important} +body.dark-mode #fileListContainer,body.dark-mode #uploadCard,body.dark-mode #folderManagementCard,body.dark-mode .card,body.dark-mode .admin-panel-content,body.dark-mode .media-topbar{background:var(--fr-surface-dark)!important;border-color:var(--fr-border-dark)!important;box-shadow:0 1px 4px rgba(0,0,0,.9)!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important} +body.dark-mode #fileListContainer::before,body.dark-mode #uploadCard::before,body.dark-mode #folderManagementCard::before,body.dark-mode .card::before,body.dark-mode .admin-panel-content::before{box-shadow:none!important} +body.dark-mode .card-header,body.dark-mode .custom-folder-card-body .drag-header{background:var(--fr-surface-dark-2)!important;border-bottom:1px solid var(--fr-border-dark)!important} +body.dark-mode #fileList table thead th{background:var(--fr-surface-dark-2)!important;border-bottom:1px solid var(--fr-border-dark)!important} +body.dark-mode #fileList table.filr-table tbody tr.selected>td,body.dark-mode #fileList table.filr-table tbody tr.row-selected>td,body.dark-mode #fileList table.filr-table tbody tr.selected-row>td,body.dark-mode #fileList table.filr-table tbody tr.is-selected>td{background:rgba(62,166,255,.16)!important;box-shadow:none!important} +body.dark-mode .modal{background-color:rgba(0,0,0,.65)!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important} +body.dark-mode .modal .modal-content,body.dark-mode .editor-modal,body.dark-mode .image-preview-modal-content,body.dark-mode #restoreFilesModal .modal-content,body.dark-mode #downloadProgressModal .modal-content{background:var(--fr-surface-dark)!important;border-radius:12px!important;border:1px solid var(--fr-border-dark)!important;box-shadow:0 8px 24px rgba(0,0,0,.9)!important} +body.dark-mode .modal .modal-content::before,body.dark-mode .editor-modal::before,body.dark-mode .image-preview-modal-content::before,body.dark-mode #restoreFilesModal .modal-content::before,body.dark-mode #downloadProgressModal .modal-content::before{box-shadow:none!important} +body.dark-mode input[type=text],body.dark-mode input[type=password],body.dark-mode input[type=email],body.dark-mode input[type=url],body.dark-mode select,body.dark-mode textarea{background:#121212!important;border-color:#3d3d3d!important;color:#f1f1f1!important} +body.dark-mode input::placeholder,body.dark-mode textarea::placeholder{color:#777!important} +body.dark-mode input:focus,body.dark-mode select:focus,body.dark-mode textarea:focus{border-color:#3ea6ff!important;box-shadow:0 0 0 1px rgba(62,166,255,.7)!important} +body.dark-mode #deleteSelectedBtn,body.dark-mode #deleteAllBtn,body.dark-mode #deleteTrashSelectedBtn,#deleteFolderBtn,#confirmDeleteFolder{background-color:#b3261e!important;border-color:#b3261e!important;box-shadow:0 4px 10px rgba(0,0,0,.7)!important} +body.dark-mode .folder-strip-container.folder-strip-mobile{background:var(--fr-surface-dark-2)!important;border:1px solid var(--fr-border-dark)!important} +body.dark-mode #customToast{background:#212121!important;border:1px solid var(--fr-border-dark)!important;box-shadow:0 8px 20px rgba(0,0,0,.9)!important} +body.dark-mode #fileSummary{color:var(--fr-muted-dark)!important} +body.dark-mode #createMenu,body.dark-mode .user-dropdown .user-menu,body.dark-mode #fileContextMenu,body.dark-mode #folderContextMenu,body.dark-mode #folderManagerContextMenu,body.dark-mode #adminPanelModal .modal-content,body.dark-mode #userPermissionsModal .modal-content,body.dark-mode #userFlagsModal .modal-content,body.dark-mode #userGroupsModal .modal-content,body.dark-mode #userPanelModal .modal-content,body.dark-mode #groupAclModal .modal-content,body.dark-mode .editor-modal,body.dark-mode #filePreviewModal .modal-content,body.dark-mode #loginForm,body.dark-mode .editor-header{background:var(--fr-surface-dark)!important;border:1px solid var(--fr-border-dark)!important;color:#f1f1f1!important;border-radius:12px!important;box-shadow:0 8px 24px rgba(0,0,0,.9)!important} +body.dark-mode .user-dropdown .user-menu,body.dark-mode #createMenu,body.dark-mode #fileContextMenu,body.dark-mode #folderContextMenu,body.dark-mode #folderManagerContextMenu{background-clip:padding-box} +body:not(.dark-mode){background:var(--fr-bg-light)!important;color:#111!important;background-image:none!important} +body:not(.dark-mode) #fileListContainer,body:not(.dark-mode) #uploadCard,body:not(.dark-mode) #folderManagementCard,body:not(.dark-mode) .card,body:not(.dark-mode) .admin-panel-content{background:var(--fr-surface-light)!important;border-color:var(--fr-border-light)!important;box-shadow:0 3px 8px rgba(0,0,0,.04)!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important} +body:not(.dark-mode) #fileListContainer::before,body:not(.dark-mode) #uploadCard::before,body:not(.dark-mode) #folderManagementCard::before,body:not(.dark-mode) .card::before,body:not(.dark-mode) .admin-panel-content::before{box-shadow:none!important} +body:not(.dark-mode) .card-header,body:not(.dark-mode) .custom-folder-card-body .drag-header{background:var(--fr-surface-light-2)!important;border-bottom:1px solid var(--fr-border-light)!important} +body:not(.dark-mode) #fileList table thead th{background:var(--fr-surface-light-2)!important;border-bottom:1px solid var(--fr-border-light)!important} +body:not(.dark-mode) #fileList table.filr-table tbody tr.selected>td,body:not(.dark-mode) #fileList table.filr-table tbody tr.row-selected>td,body:not(.dark-mode) #fileList table.filr-table tbody tr.selected-row>td,body:not(.dark-mode) #fileList table.filr-table tbody tr.is-selected>td{background:rgba(33,150,243,.12)!important;box-shadow:none!important} +body:not(.dark-mode) .modal{background-color:rgba(0,0,0,.4)!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important} +body:not(.dark-mode) .modal .modal-content,body:not(.dark-mode) .editor-modal,body:not(.dark-mode) .image-preview-modal-content,body:not(.dark-mode) #restoreFilesModal .modal-content,body:not(.dark-mode) #downloadProgressModal .modal-content{background:var(--fr-surface-light)!important;border-radius:12px!important;border:1px solid var(--fr-border-light)!important;box-shadow:0 8px 24px rgba(0,0,0,.18)!important} +body:not(.dark-mode) .modal .modal-content::before,body:not(.dark-mode) .editor-modal::before,body:not(.dark-mode) .image-preview-modal-content::before,body:not(.dark-mode) #restoreFilesModal .modal-content::before,body:not(.dark-mode) #downloadProgressModal .modal-content::before{box-shadow:none!important} +body:not(.dark-mode) input[type=text],body:not(.dark-mode) input[type=password],body:not(.dark-mode) input[type=email],body:not(.dark-mode) input[type=url],body:not(.dark-mode) select,body:not(.dark-mode) textarea{background:#fff!important;border-color:#d0d0d0!important;color:#111!important} +body:not(.dark-mode) input::placeholder,body:not(.dark-mode) textarea::placeholder{color:#9e9e9e!important} +body:not(.dark-mode) input:focus,body:not(.dark-mode) select:focus,body:not(.dark-mode) textarea:focus{border-color:#2196f3!important;box-shadow:0 0 0 1px rgba(33,150,243,.55)!important} +body:not(.dark-mode) #deleteSelectedBtn,body:not(.dark-mode) #deleteAllBtn,body:not(.dark-mode) #deleteTrashSelectedBtn{box-shadow:0 2px 6px rgba(244,67,54,.3)!important} +body:not(.dark-mode) .folder-strip-container.folder-strip-mobile{background:#f1f1f1!important;border:1px solid var(--fr-border-light)!important} +body:not(.dark-mode) #customToast{background:#212121!important;color:#fff!important;border:1px solid #000!important;box-shadow:0 8px 18px rgba(0,0,0,.45)!important} +body:not(.dark-mode) #fileSummary{color:var(--fr-muted-light)!important} +body:not(.dark-mode) #createMenu,body:not(.dark-mode) .user-dropdown .user-menu,body:not(.dark-mode) #fileContextMenu,body:not(.dark-mode) #folderContextMenu,body:not(.dark-mode) #folderManagerContextMenu,body:not(.dark-mode) #adminPanelModal .modal-content,body:not(.dark-mode) #userPermissionsModal .modal-content,body:not(.dark-mode) #userFlagsModal .modal-content,body:not(.dark-mode) #userGroupsModal .modal-content,body:not(.dark-mode) #userPanelModal .modal-content,body:not(.dark-mode) #groupAclModal .modal-content,body:not(.dark-mode) .editor-modal,body:not(.dark-mode) #filePreviewModal .modal-content,body:not(.dark-mode) #loginForm,body:not(.dark-mode) .editor-header{background:var(--fr-surface-light)!important;border:1px solid var(--fr-border-light)!important;color:#111!important;border-radius:12px!important;box-shadow:0 4px 12px rgba(0,0,0,.12)!important} +#searchIcon{display:inline-flex;align-items:center;justify-content:center;width:38px;height:36px;padding:0;border-radius:999px 0 0 999px;border:1px solid rgba(0,0,0,.18);border-right:none;background:#fff;cursor:pointer;box-shadow:none;transform:none} +#searchIcon .material-icons{font-size:20px;line-height:1;color:#555} +#searchIcon:hover{background:#f5f5f5} +#searchIcon+#searchInput{height:36px;border-radius:0 999px 999px 0;border-left:none;padding-top:6px;padding-bottom:6px} +body.dark-mode #searchIcon{background:#212121;border-color:#3d3d3d} +body.dark-mode #searchIcon .material-icons{color:#f1f1f1} +body.dark-mode #searchIcon:hover{background:#303030} +body.dark-mode #searchIcon+#searchInput{border-left:none} +#advancedSearchToggle{border-radius:999px;border:1px solid #d0d0d0;padding:6px 12px;font-size:.9rem;background:#f5f5f5;color:#333;cursor:pointer;display:inline-flex;align-items:center;gap:4px;margin-right:8px;transition:background .15s ease,box-shadow .15s ease,transform .1s ease} +#advancedSearchToggle:hover,#advancedSearchToggle:focus-visible{background:#e8e8e8;box-shadow:0 1px 4px rgba(0,0,0,.16);outline:none;transform:translateY(-1px)} +.dark-mode #advancedSearchToggle{background:#2a2a2a;border-color:#444;color:#f1f1f1} +.dark-mode #advancedSearchToggle:hover,.dark-mode #advancedSearchToggle:focus-visible{background:#333;box-shadow:0 1px 4px rgba(0,0,0,.5)} +.custom-prev-next-btn{display:inline-flex;align-items:center;justify-content:center;min-width:64px;padding:6px 14px;font-size:13px;font-weight:500;border-radius:999px;border:1px solid rgba(0,0,0,.14);background:#f1f1f1;color:#111;cursor:pointer;transition:background-color 140ms ease-out,border-color 140ms ease-out,box-shadow 140ms ease-out,transform 120ms ease-out} +.custom-prev-next-btn:not(:disabled):hover{background:#e5e5e5;border-color:rgba(0,0,0,.22);box-shadow:0 2px 6px rgba(0,0,0,.18);transform:translateY(-1px)} +.custom-prev-next-btn:not(:disabled):active{transform:translateY(0);box-shadow:0 1px 3px rgba(0,0,0,.25)} +.custom-prev-next-btn:disabled{opacity:.5;cursor:default;box-shadow:none} +body.dark-mode .custom-prev-next-btn{background:#212121;border-color:#3d3d3d;color:#f1f1f1} +body.dark-mode .custom-prev-next-btn:not(:disabled):hover{background:#2a2a2a;border-color:#4a4a4a;box-shadow:0 2px 6px rgba(0,0,0,.7)} +input[type=text]:not(#searchInput),input[type=password],input[type=email],input[type=url],input[type=number],textarea,select{border:1px solid rgba(148,163,184,.6)!important;border-radius:10px;background:#ffffff;box-sizing:border-box} +#decreaseFont,#increaseFont{display:inline-flex;align-items:center;justify-content:center;margin-top:5px;height:24px;min-width:30px;padding:2px 8px;font-size:11px;font-weight:500;line-height:1;border-radius:999px;border:1px solid rgba(0,0,0,.16);background:#f5f5f5;color:#222;cursor:pointer;margin-left:4px;transition:background-color 140ms ease-out,border-color 140ms ease-out,box-shadow 140ms ease-out,transform 120ms ease-out} +#decreaseFont:not(:disabled):hover,#increaseFont:not(:disabled):hover{background:#e8e8e8;border-color:rgba(0,0,0,.24);box-shadow:0 1px 4px rgba(0,0,0,.18);transform:translateY(-1px)} +#decreaseFont:not(:disabled):active,#increaseFont:not(:disabled):active{transform:translateY(5px);box-shadow:0 1px 2px rgba(0,0,0,.25)} +#decreaseFont:disabled,#increaseFont:disabled{opacity:.5;cursor:default;box-shadow:none} +body.dark-mode #decreaseFont,body.dark-mode #increaseFont{background:#212121;border-color:#3d3d3d;color:#f1f1f1} +body.dark-mode #decreaseFont:not(:disabled):hover,body.dark-mode #increaseFont:not(:disabled):hover{background:#2a2a2a;border-color:#4a4a4a;box-shadow:0 1px 4px rgba(0,0,0,.7)} +#closeEditorX{margin-right:10px} +#fileList .folder-row-icon .folder-front{fill:var(--filr-folder-front,#f6b84e);stroke:var(--filr-folder-stroke,#a87312);stroke-width:.5;stroke-linejoin:round;stroke-linecap:round} +#fileList .folder-row-icon .folder-back{fill:var(--filr-folder-back,#fcd68a);stroke:var(--filr-folder-stroke,#a87312);stroke-width:.5;stroke-linejoin:round;stroke-linecap:round} +#fileList .folder-row-icon .paper{fill:#fff;stroke:#b2c2db;stroke-width:1;vector-effect:non-scaling-stroke} +#fileList .folder-row-icon .paper-fold{fill:#b2c2db} +#fileList .folder-row-icon .paper-line{stroke:#b2c2db;stroke-width:1;stroke-linecap:round;fill:none;vector-effect:non-scaling-stroke} +#fileList .folder-row-icon .paper-ink{stroke:#4da3ff;stroke-width:.9;stroke-linecap:round;stroke-linejoin:round;fill:none;opacity:.85} +#fileList .folder-row-icon .lip-highlight{fill:none;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round} +#fileList .folder-row-name{font-weight:500;margin-right:4px} +#fileList .folder-row-meta{margin-left:4px;opacity:.75;font-size:.86em} +#fileList tbody tr.folder-row{height:var(--file-row-height,44px);cursor:pointer} +#fileList tbody tr.folder-row .folder-name-cell{padding-top:0;padding-bottom:0} +#fileList tbody tr.folder-row .folder-row-inner{cursor:inherit} +#fileList tbody tr.folder-row .folder-icon-cell{text-align:left;vertical-align:middle} +#fileList tbody tr.folder-row .folder-row-icon svg{display:block} +.folder-row-icon{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin-right:8px;position:relative;left:-8px;top:5px} +.folder-row-inner{display:flex;align-items:center} +#fileList table.filr-table th.checkbox-col,#fileList table.filr-table td.checkbox-col,#fileList table.filr-table td.folder-icon-cell{width:30px!important;max-width:30px!important} +#fileList tr.folder-row.folder-row-droptarget{background:var(--filr-accent-50,rgba(250,204,21,.12));box-shadow:inset 0 0 0 1px var(--filr-accent-400,rgba(250,204,21,.6))} +#fileList tr.folder-row.folder-row-droptarget .folder-row-name{font-weight:600} +#fileList table.filr-table tbody tr.folder-row>td{padding-top:0!important;padding-bottom:0!important} +#fileList table.filr-table tbody tr.folder-row>td.folder-icon-cell{overflow:visible} +#fileList tr.folder-row .folder-row-inner,#fileList tr.folder-row .folder-row-name{cursor:inherit} \ No newline at end of file diff --git a/public/js/appCore.js b/public/js/appCore.js index 905b1c3..a373b20 100644 --- a/public/js/appCore.js +++ b/public/js/appCore.js @@ -90,7 +90,8 @@ export function initializeApp() { window.currentFolder = last ? last : "root"; const stored = localStorage.getItem('showFoldersInList'); - window.showFoldersInList = stored === null ? true : stored === 'true'; + // default: false (unchecked) + window.showFoldersInList = stored === 'true'; // Load public site config early (safe subset) loadAdminConfigFunc(); @@ -99,6 +100,7 @@ export function initializeApp() { initTagSearch(); + /* // Hook DnD relay from fileList area into upload area const fileListArea = document.getElementById('fileList'); @@ -146,7 +148,7 @@ export function initializeApp() { uploadArea.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true })); } }); - } + }*/ // App subsystems initDragAndDrop(); diff --git a/public/js/authModals.js b/public/js/authModals.js index 1cc8868..1eebfbe 100644 --- a/public/js/authModals.js +++ b/public/js/authModals.js @@ -351,30 +351,73 @@ export async function openUserPanel() { langFs.appendChild(langSel); content.appendChild(langFs); - // --- Display fieldset: “Show folders above files” --- + // --- Display fieldset: strip + inline folder rows --- const dispFs = document.createElement('fieldset'); dispFs.style.marginBottom = '15px'; + const dispLegend = document.createElement('legend'); dispLegend.textContent = t('display'); dispFs.appendChild(dispLegend); - const dispLabel = document.createElement('label'); - dispLabel.style.cursor = 'pointer'; - const dispCb = document.createElement('input'); - dispCb.type = 'checkbox'; - dispCb.id = 'showFoldersInList'; - dispCb.style.verticalAlign = 'middle'; - const stored = localStorage.getItem('showFoldersInList'); - dispCb.checked = stored === null ? true : stored === 'true'; - dispLabel.appendChild(dispCb); - dispLabel.append(` ${t('show_folders_above_files')}`); - dispFs.appendChild(dispLabel); + + // 1) Show folder strip above list + const stripLabel = document.createElement('label'); + stripLabel.style.cursor = 'pointer'; + stripLabel.style.display = 'block'; + stripLabel.style.marginBottom = '4px'; + + const stripCb = document.createElement('input'); + stripCb.type = 'checkbox'; + stripCb.id = 'showFoldersInList'; + stripCb.style.verticalAlign = 'middle'; + + { + const storedStrip = localStorage.getItem('showFoldersInList'); + // default: unchecked + stripCb.checked = storedStrip === null ? false : storedStrip === 'true'; + } + + stripLabel.appendChild(stripCb); + stripLabel.append(` ${t('show_folders_above_files')}`); + dispFs.appendChild(stripLabel); + + // 2) Show inline folder rows above files in table view + const inlineLabel = document.createElement('label'); + inlineLabel.style.cursor = 'pointer'; + inlineLabel.style.display = 'block'; + + const inlineCb = document.createElement('input'); + inlineCb.type = 'checkbox'; + inlineCb.id = 'showInlineFolders'; + inlineCb.style.verticalAlign = 'middle'; + + { + const storedInline = localStorage.getItem('showInlineFolders'); + inlineCb.checked = storedInline === null ? true : storedInline === 'true'; + } + + inlineLabel.appendChild(inlineCb); + // you’ll want a string like this in i18n: + // "show_inline_folders": "Show folders inline (above files)" + inlineLabel.append(` ${t('show_inline_folders') || 'Show folders inline (above files)'}`); + dispFs.appendChild(inlineLabel); + content.appendChild(dispFs); - dispCb.addEventListener('change', () => { - window.showFoldersInList = dispCb.checked; - localStorage.setItem('showFoldersInList', dispCb.checked); - // re‐load the entire file list (and strip) in one go: - loadFileList(window.currentFolder); + // Handlers: toggle + refresh list + stripCb.addEventListener('change', () => { + window.showFoldersInList = stripCb.checked; + localStorage.setItem('showFoldersInList', stripCb.checked); + if (typeof window.loadFileList === 'function') { + window.loadFileList(window.currentFolder || 'root'); + } + }); + + inlineCb.addEventListener('change', () => { + window.showInlineFolders = inlineCb.checked; + localStorage.setItem('showInlineFolders', inlineCb.checked); + if (typeof window.loadFileList === 'function') { + window.loadFileList(window.currentFolder || 'root'); + } }); // wire up image‐input change @@ -425,6 +468,18 @@ export async function openUserPanel() { modal.querySelector('#userTOTPEnabled').checked = totp_enabled; modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en'; modal.querySelector('h3').textContent = `${t('user_panel')} (${username})`; + + // sync display toggles from localStorage + const stripCb = modal.querySelector('#showFoldersInList'); + const inlineCb = modal.querySelector('#showInlineFolders'); + if (stripCb) { + const storedStrip = localStorage.getItem('showFoldersInList'); + stripCb.checked = storedStrip === null ? false : storedStrip === 'true'; + } + if (inlineCb) { + const storedInline = localStorage.getItem('showInlineFolders'); + inlineCb.checked = storedInline === null ? true : storedInline === 'true'; + } } // show diff --git a/public/js/domUtils.js b/public/js/domUtils.js index 700856f..d8749a6 100644 --- a/public/js/domUtils.js +++ b/public/js/domUtils.js @@ -160,11 +160,11 @@ export function buildFileTableHeader(sortOrder) { - ${t("file_name")} ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""} - ${t("date_modified")} ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""} - ${t("upload_date")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""} - ${t("file_size")} ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""} - ${t("uploader")} ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""} + ${t("name")} ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""} + ${t("modified")} ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""} + ${t("created")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""} + ${t("size")} ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""} + ${t("owner")} ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""} ${t("actions")} diff --git a/public/js/fileListView.js b/public/js/fileListView.js index ee9aeed..4c1bb62 100644 --- a/public/js/fileListView.js +++ b/public/js/fileListView.js @@ -240,16 +240,29 @@ window.addEventListener('resize', () => { if (strip) applyFolderStripLayout(strip); }); -// Listen once: update strip + tree when folder color changes +// Listen once: update strip + tree + inline rows when folder color changes window.addEventListener('folderColorChanged', (e) => { const { folder } = e.detail || {}; if (!folder) return; - // Update the strip (if that folder is currently shown) + // 1) Update the strip (if that folder is currently shown) repaintStripIcon(folder); - // And refresh the tree icon too (existing function) + // 2) Refresh the tree icon (existing function) try { refreshFolderIcon(folder); } catch { } + + // 3) Repaint any inline folder rows in the file table + try { + const safeFolder = CSS.escape(folder); + document + .querySelectorAll(`#fileList tr.folder-row[data-folder="${safeFolder}"]`) + .forEach(row => { + // reuse the same helper we used when injecting inline rows + attachStripIconAsync(row, folder, 28); + }); + } catch { + // CSS.escape might not exist on very old browsers; fail silently + } }); // Hide "Edit" for files >10 MiB @@ -259,11 +272,25 @@ const MAX_EDIT_BYTES = 10 * 1024 * 1024; let __fileListReqSeq = 0; window.itemsPerPage = parseInt( - localStorage.getItem('itemsPerPage') || window.itemsPerPage || '10', + localStorage.getItem('itemsPerPage') || window.itemsPerPage || '50', 10 ); window.currentPage = window.currentPage || 1; window.viewMode = localStorage.getItem("viewMode") || "table"; +window.currentSubfolders = window.currentSubfolders || []; + +// Default folder display settings from localStorage +try { + const storedStrip = localStorage.getItem('showFoldersInList'); + const storedInline = localStorage.getItem('showInlineFolders'); + + window.showFoldersInList = storedStrip === null ? true : storedStrip === 'true'; + window.showInlineFolders = storedInline === null ? true : storedInline === 'true'; +} catch { + // if localStorage blows up, fall back to both enabled + window.showFoldersInList = true; + window.showInlineFolders = true; +} // Global flag for advanced search mode. window.advancedSearchEnabled = false; @@ -387,7 +414,6 @@ function attachStripIconAsync(hostEl, fullPath, size = 28) { const back = _lighten(hex, 14); const stroke = _darken(hex, 22); - // apply vars on the tile (or icon span) hostEl.style.setProperty('--filr-folder-front', front); hostEl.style.setProperty('--filr-folder-back', back); hostEl.style.setProperty('--filr-folder-stroke', stroke); @@ -395,15 +421,26 @@ function attachStripIconAsync(hostEl, fullPath, size = 28) { const iconSpan = hostEl.querySelector('.folder-svg'); if (!iconSpan) return; + // 1) initial "empty" icon iconSpan.dataset.kind = 'empty'; - iconSpan.innerHTML = folderSVG('empty'); // size is baked into viewBox; add a size arg if you prefer + iconSpan.innerHTML = folderSVG('empty'); + + // make sure this brand-new SVG is sized correctly + try { syncFolderIconSizeToRowHeight(); } catch {} + const url = `/api/folder/isEmpty.php?folder=${encodeURIComponent(fullPath)}&t=${Date.now()}`; - _fetchJSONWithTimeout(url, 2500).then(({ folders = 0, files = 0 }) => { - if ((folders + files) > 0 && iconSpan.dataset.kind !== 'paper') { - iconSpan.dataset.kind = 'paper'; - iconSpan.innerHTML = folderSVG('paper'); - } - }).catch(() => { }); + _fetchJSONWithTimeout(url, 2500) + .then(({ folders = 0, files = 0 }) => { + if ((folders + files) > 0 && iconSpan.dataset.kind !== 'paper') { + // 2) swap to "paper" icon + iconSpan.dataset.kind = 'paper'; + iconSpan.innerHTML = folderSVG('paper'); + + // re-apply sizing to this new SVG too + try { syncFolderIconSizeToRowHeight(); } catch {} + } + }) + .catch(() => { /* ignore */ }); } /* ----------------------------- @@ -426,6 +463,56 @@ async function safeJson(res) { } return body ?? {}; } + +// --- Folder capabilities + owner cache ---------------------- +const _folderCapsCache = new Map(); + +async function fetchFolderCaps(folder) { + if (!folder) return null; + if (_folderCapsCache.has(folder)) { + return _folderCapsCache.get(folder); + } + try { + const res = await fetch( + `/api/folder/capabilities.php?folder=${encodeURIComponent(folder)}`, + { credentials: 'include' } + ); + const data = await safeJson(res); + _folderCapsCache.set(folder, data || null); + + if (data && (data.owner || data.user)) { + _folderOwnerCache.set(folder, data.owner || data.user || ""); + } + return data || null; + } catch { + _folderCapsCache.set(folder, null); + return null; + } +} + +// --- Folder owner cache + helper ---------------------- +const _folderOwnerCache = new Map(); + +async function fetchFolderOwner(folder) { + if (!folder) return ""; + if (_folderOwnerCache.has(folder)) { + return _folderOwnerCache.get(folder); + } + + try { + const res = await fetch( + `/api/folder/capabilities.php?folder=${encodeURIComponent(folder)}`, + { credentials: 'include' } + ); + const data = await safeJson(res); + const owner = data && (data.owner || data.user || ""); + _folderOwnerCache.set(folder, owner || ""); + return owner || ""; + } catch { + _folderOwnerCache.set(folder, ""); + return ""; + } +} // ---- Viewed badges (table + gallery) ---- // ---------- Badge factory (center text vertically) ---------- function makeBadge(state) { @@ -917,6 +1004,13 @@ export async function loadFileList(folderParam) { document.documentElement.style.setProperty("--file-row-height", v + "px"); localStorage.setItem("rowHeight", v); rowValue.textContent = v + "px"; + // mark compact mode for very low heights + if (v <= 32) { + document.documentElement.setAttribute('data-row-compact', '1'); + } else { + document.documentElement.removeAttribute('data-row-compact'); + } + syncFolderIconSizeToRowHeight(); }; } } @@ -966,6 +1060,9 @@ export async function loadFileList(folderParam) { return !hidden.has(lower) && !lower.startsWith("resumable_"); }); + // Expose for inline folder rows in table view + window.currentSubfolders = subfolders; + let strip = document.getElementById("folderStripContainer"); if (!strip) { strip = document.createElement("div"); @@ -976,6 +1073,11 @@ export async function loadFileList(folderParam) { // NEW: paged + responsive strip renderFolderStripPaged(strip, subfolders); + + // Re-render table view once folders are known so they appear inline above files + if (window.viewMode === "table" && reqId === __fileListReqSeq) { + renderFileTable(folder); + } } catch { // ignore folder errors; rows already rendered } @@ -1000,24 +1102,456 @@ export async function loadFileList(folderParam) { } } + +function injectInlineFolderRows(fileListContent, folder, pageSubfolders) { + const table = fileListContent.querySelector('table.filr-table'); + + // Use the paged subfolders if provided, otherwise fall back to all + const subfolders = Array.isArray(pageSubfolders) && pageSubfolders.length + ? pageSubfolders + : (Array.isArray(window.currentSubfolders) ? window.currentSubfolders : []); + + if (!table || !subfolders.length) return; + + const thead = table.tHead; + const tbody = table.tBodies && table.tBodies[0]; + if (!thead || !tbody) return; + + const headerRow = thead.rows[0]; + if (!headerRow) return; + + const headerCells = Array.from(headerRow.cells); + const colCount = headerCells.length; + + // --- Column indices ------------------------------------------------------- + let checkboxIdx = headerCells.findIndex(th => + th.classList.contains("checkbox-col") || + th.querySelector('input[type="checkbox"]') + ); + + let nameIdx = headerCells.findIndex(th => + (th.dataset && th.dataset.column === "name") || + /\bname\b/i.test((th.textContent || "").trim()) + ); + if (nameIdx < 0) { + nameIdx = Math.min(1, colCount - 1); // fallback to 2nd col + } + + let sizeIdx = headerCells.findIndex(th => + (th.dataset && (th.dataset.column === "size" || th.dataset.column === "filesize")) || + /\bsize\b/i.test((th.textContent || "").trim()) + ); + if (sizeIdx < 0) sizeIdx = -1; + + let uploaderIdx = headerCells.findIndex(th => + (th.dataset && th.dataset.column === "uploader") || + /\buploader\b/i.test((th.textContent || "").trim()) + ); + if (uploaderIdx < 0) uploaderIdx = -1; + + let actionsIdx = headerCells.findIndex(th => + (th.dataset && th.dataset.column === "actions") || + /\bactions?\b/i.test((th.textContent || "").trim()) || + /\bactions?-col\b/i.test(th.className || "") + ); + if (actionsIdx < 0) actionsIdx = -1; + + // Remove any previous folder rows + tbody.querySelectorAll("tr.folder-row").forEach(tr => tr.remove()); + + + + const firstDataRow = tbody.firstElementChild; + + subfolders.forEach(sf => { + const tr = document.createElement("tr"); + tr.classList.add("folder-row"); + tr.dataset.folder = sf.full; + + for (let i = 0; i < colCount; i++) { + const td = document.createElement("td"); + +// *** copy header classes so responsive breakpoints match file rows *** +// but strip Bootstrap margin helpers (ml-2 / mx-2) so we don't get a big gap +const headerClass = headerCells[i] && headerCells[i].className; +if (headerClass) { + td.className = headerClass; + td.classList.remove("ml-2", "mx-2"); +} + + // 1) icon / checkbox column + if (i === checkboxIdx) { + td.classList.add("folder-icon-cell"); + td.style.textAlign = "left"; + td.style.verticalAlign = "middle"; + + const iconSpan = document.createElement("span"); + iconSpan.className = "folder-svg folder-row-icon"; + td.appendChild(iconSpan); + + // 2) name column + } else if (i === nameIdx) { + td.classList.add("name-cell", "file-name-cell", "folder-name-cell"); + + const wrap = document.createElement("div"); + wrap.className = "folder-row-inner"; + + const nameSpan = document.createElement("span"); + nameSpan.className = "folder-row-name"; + nameSpan.textContent = sf.name || sf.full; + + const metaSpan = document.createElement("span"); + metaSpan.className = "folder-row-meta"; + metaSpan.textContent = ""; // "(15 folders, 19 files)" later + + wrap.appendChild(nameSpan); + wrap.appendChild(metaSpan); + td.appendChild(wrap); + + // 3) size column + } else if (i === sizeIdx) { + td.classList.add("folder-size-cell"); + td.textContent = "…"; // placeholder until we load stats + + // 4) uploader / owner column + } else if (i === uploaderIdx) { + td.classList.add("uploader-cell", "folder-uploader-cell"); + td.textContent = ""; // filled asynchronously with owner + + // 5) actions column + } else if (i === actionsIdx) { + td.classList.add("folder-actions-cell"); + + const group = document.createElement("div"); + group.className = "btn-group btn-group-sm folder-actions-group"; + group.setAttribute("role", "group"); +group.setAttribute("aria-label", "File actions"); + +const makeActionBtn = (iconName, titleKey, btnClass, actionKey, handler) => { + const btn = document.createElement("button"); + btn.type = "button"; + + // base classes – same size as file actions + btn.className = `btn ${btnClass} py-1`; + + // kill any Bootstrap margin helpers that got passed in + btn.classList.remove("ml-2", "mx-2"); + + btn.setAttribute("data-folder-action", actionKey); + btn.setAttribute("data-i18n-title", titleKey); + btn.title = t(titleKey); + + const icon = document.createElement("i"); + icon.className = "material-icons"; + icon.textContent = iconName; + btn.appendChild(icon); + + btn.addEventListener("click", e => { + e.stopPropagation(); + window.currentFolder = sf.full; + try { localStorage.setItem("lastOpenedFolder", sf.full); } catch {} + handler(); + }); + + // start disabled; caps logic will enable + btn.disabled = true; + btn.style.pointerEvents = "none"; + btn.style.opacity = "0.5"; + + group.appendChild(btn); +}; + +makeActionBtn("drive_file_move", "move_folder", "btn-warning folder-move-btn", "move", () => openMoveFolderUI()); +makeActionBtn("palette", "color_folder", "btn-color-folder","color", () => openColorFolderModal(sf.full)); +makeActionBtn("drive_file_rename_outline", "rename_folder", "btn-warning folder-rename-btn", "rename", () => openRenameFolderModal()); +makeActionBtn("share", "share_folder", "btn-secondary", "share", () => openFolderShareModal(sf.full)); + + td.appendChild(group); + } + + // IMPORTANT: always append the cell, no matter which column we're in + tr.appendChild(td); + } + + // click → navigate, same as before + tr.addEventListener("click", e => { + if (e.button !== 0) return; + const dest = sf.full; + if (!dest) return; + + window.currentFolder = dest; + try { localStorage.setItem("lastOpenedFolder", dest); } catch { } + + updateBreadcrumbTitle(dest); + + document.querySelectorAll(".folder-option.selected") + .forEach(o => o.classList.remove("selected")); + const treeNode = document.querySelector( + `.folder-option[data-folder="${CSS.escape(dest)}"]` + ); + if (treeNode) treeNode.classList.add("selected"); + + const strip = document.getElementById("folderStripContainer"); + if (strip) { + strip.querySelectorAll(".folder-item.selected") + .forEach(i => i.classList.remove("selected")); + const stripItem = strip.querySelector( + `.folder-item[data-folder="${CSS.escape(dest)}"]` + ); + if (stripItem) stripItem.classList.add("selected"); + } + + loadFileList(dest); + }); + + + // DnD + context menu – keep existing logic, but also add a visual highlight + tr.addEventListener("dragover", e => { + folderDragOverHandler(e); + tr.classList.add("folder-row-droptarget"); + }); + + tr.addEventListener("dragleave", e => { + folderDragLeaveHandler(e); + tr.classList.remove("folder-row-droptarget"); + }); + + tr.addEventListener("drop", e => { + folderDropHandler(e); + tr.classList.remove("folder-row-droptarget"); + }); + + tr.addEventListener("contextmenu", e => { + e.preventDefault(); + e.stopPropagation(); + + const dest = sf.full; + if (!dest) return; + + window.currentFolder = dest; + try { localStorage.setItem("lastOpenedFolder", dest); } catch { } + + const menuItems = [ + { label: t("create_folder"), action: () => document.getElementById("createFolderModal").style.display = "block" }, + { label: t("move_folder"), action: () => openMoveFolderUI() }, + { label: t("rename_folder"), action: () => openRenameFolderModal() }, + { label: t("color_folder"), action: () => openColorFolderModal(dest) }, + { label: t("folder_share"), action: () => openFolderShareModal(dest) }, + { label: t("delete_folder"), action: () => openDeleteFolderModal() } + ]; + showFolderManagerContextMenu(e.pageX, e.pageY, menuItems); + }); + + // insert row above first file row + tbody.insertBefore(tr, firstDataRow || null); + + // ----- ICON: color + alignment (size is driven by row height) ----- +attachStripIconAsync(tr, sf.full); +const iconSpan = tr.querySelector(".folder-row-icon"); +if (iconSpan) { + iconSpan.style.display = "inline-flex"; + iconSpan.style.alignItems = "center"; + iconSpan.style.justifyContent = "flex-start"; + iconSpan.style.marginLeft = "0px"; // small left nudge + iconSpan.style.marginTop = "0px"; // small down nudge +} + + // ----- FOLDER STATS + OWNER + CAPS (keep your existing code below here) ----- + const sizeCellIndex = (sizeIdx >= 0 && sizeIdx < tr.cells.length) ? sizeIdx : -1; + const nameCellIndex = (nameIdx >= 0 && nameIdx < tr.cells.length) ? nameIdx : -1; + + const url = `/api/folder/isEmpty.php?folder=${encodeURIComponent(sf.full)}&t=${Date.now()}`; + _fetchJSONWithTimeout(url, 2500).then(stats => { + if (!stats) return; + + const foldersCount = Number.isFinite(stats.folders) ? stats.folders : 0; + const filesCount = Number.isFinite(stats.files) ? stats.files : 0; + const bytes = Number.isFinite(stats.bytes) + ? stats.bytes + : (Number.isFinite(stats.sizeBytes) ? stats.sizeBytes : null); + + let pieces = []; + if (foldersCount) pieces.push(`${foldersCount} folder${foldersCount === 1 ? "" : "s"}`); + if (filesCount) pieces.push(`${filesCount} file${filesCount === 1 ? "" : "s"}`); + if (!pieces.length) pieces.push("0 items"); + const countLabel = pieces.join(", "); + + if (nameCellIndex >= 0) { + const nameCell = tr.cells[nameCellIndex]; + if (nameCell) { + const metaSpan = nameCell.querySelector(".folder-row-meta"); + if (metaSpan) metaSpan.textContent = ` (${countLabel})`; + } + } + + if (sizeCellIndex >= 0) { + const sizeCell = tr.cells[sizeCellIndex]; + if (sizeCell) { + let sizeLabel = "—"; + if (bytes != null && bytes >= 0) { + sizeLabel = formatSize(bytes); + } + sizeCell.textContent = sizeLabel; + sizeCell.title = `${countLabel}${bytes != null && bytes >= 0 ? " • " + sizeLabel : ""}`; + } + } + }).catch(() => { + if (sizeCellIndex >= 0) { + const sizeCell = tr.cells[sizeCellIndex]; + if (sizeCell && !sizeCell.textContent) sizeCell.textContent = "—"; + } + }); + + // OWNER + action permissions + if (uploaderIdx >= 0 || actionsIdx >= 0) { + fetchFolderCaps(sf.full).then(caps => { + if (!caps || !document.body.contains(tr)) return; + + if (uploaderIdx >= 0 && uploaderIdx < tr.cells.length) { + const uploaderCell = tr.cells[uploaderIdx]; + if (uploaderCell) { + const owner = caps.owner || caps.user || ""; + uploaderCell.textContent = owner || ""; + } + } + + if (actionsIdx >= 0 && actionsIdx < tr.cells.length) { + const actCell = tr.cells[actionsIdx]; + if (!actCell) return; + + actCell.querySelectorAll('button[data-folder-action]').forEach(btn => { + const action = btn.getAttribute('data-folder-action'); + let enabled = false; + switch (action) { + case "move": + enabled = !!caps.canMoveFolder; + break; + case "color": + enabled = !!caps.canRename; // same gate as tree “color” button + break; + case "rename": + enabled = !!caps.canRename; + break; + case "share": + enabled = !!caps.canShareFolder; + break; + } + if (enabled === undefined) { + enabled = true; // fallback so admin still gets buttons even if a flag is missing + } + if (enabled) { + btn.disabled = false; + btn.style.pointerEvents = ""; + btn.style.opacity = ""; + } else { + btn.disabled = true; + btn.style.pointerEvents = "none"; + btn.style.opacity = "0.5"; + } + }); + } + }).catch(() => { /* ignore */ }); + } + }); + syncFolderIconSizeToRowHeight(); +} +function syncFolderIconSizeToRowHeight() { + const cs = getComputedStyle(document.documentElement); + const raw = cs.getPropertyValue('--file-row-height') || '48px'; + const rowH = parseInt(raw, 10) || 60; + + const FUDGE = 5; + const MAX_GROWTH_ROW = 44; // after this, stop growing the icon + + const BASE_ROW_FOR_OFFSET = 40; // where icon looks centered + const OFFSET_FACTOR = 0.25; + + // cap growth for size, like you already do + const effectiveRow = Math.min(rowH, MAX_GROWTH_ROW); + + const boxSize = Math.max(25, Math.min(35, effectiveRow - 20 + FUDGE)); + const scale = 1.20; + + // use your existing offset curve + const clampedForOffset = Math.max(30, Math.min(60, rowH)); + let offsetY = (clampedForOffset - BASE_ROW_FOR_OFFSET) * OFFSET_FACTOR; + + // 30–44: untouched (you said this range is perfect) + // 45–60: same curve, but shifted up slightly + if (rowH > 53) { + offsetY -= 3; + } + + document.querySelectorAll('#fileList .folder-row-icon').forEach(iconSpan => { + iconSpan.style.width = boxSize + 'px'; + iconSpan.style.height = boxSize + 'px'; + iconSpan.style.overflow = 'visible'; + + const svg = iconSpan.querySelector('svg'); + if (!svg) return; + + svg.setAttribute('width', String(boxSize)); + svg.setAttribute('height', String(boxSize)); + svg.style.transformOrigin = 'left center'; + svg.style.transform = `translateY(${offsetY}px) scale(${scale})`; + }); +} /** * Render table view */ export function renderFileTable(folder, container, subfolders) { const fileListContent = container || document.getElementById("fileList"); const searchTerm = (window.currentSearchTerm || "").toLowerCase(); - const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10); + const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "50", 10); let currentPage = window.currentPage || 1; + // Files (filtered by search) const filteredFiles = searchFiles(searchTerm); - const totalFiles = filteredFiles.length; - const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); + // Inline folders: sort once (Explorer-style A→Z) + const allSubfolders = Array.isArray(window.currentSubfolders) + ? window.currentSubfolders + : []; + const subfoldersSorted = [...allSubfolders].sort((a, b) => + (a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" }) + ); + + const totalFiles = filteredFiles.length; + const totalFolders = subfoldersSorted.length; + const totalRows = totalFiles + totalFolders; + const hasFolders = totalFolders > 0; + + // Pagination is now over (folders + files) + const totalPages = totalRows > 0 + ? Math.ceil(totalRows / itemsPerPageSetting) + : 1; + if (currentPage > totalPages) { - currentPage = totalPages > 0 ? totalPages : 1; + currentPage = totalPages; window.currentPage = currentPage; } + const startRow = (currentPage - 1) * itemsPerPageSetting; + const endRow = Math.min(startRow + itemsPerPageSetting, totalRows); + + // Figure out which folders + files belong to THIS page + const pageFolders = []; + const pageFiles = []; + + for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { + if (rowIndex < totalFolders) { + pageFolders.push(subfoldersSorted[rowIndex]); + } else { + const fileIdx = rowIndex - totalFolders; + const file = filteredFiles[fileIdx]; + if (file) pageFiles.push(file); + } + } + + // Stable id per file row on this page + const rowIdFor = (file, idx) => + `${encodeURIComponent(file.name)}-p${currentPage}-${idx}`; + // We pass a harmless "base" string to keep buildFileTableRow happy, // then we will FIX the preview/thumbnail URLs to the API below. const fakeBase = "#/"; @@ -1040,19 +1574,16 @@ export function renderFileTable(folder, container, subfolders) { return ``; }); - const startIndex = (currentPage - 1) * itemsPerPageSetting; - const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles); - let rowsHTML = ""; - if (totalFiles > 0) { - filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { - // Build row with a neutral base, then correct the links/preview below. - // Give the row an ID so we can patch attributes safely - const idSafe = encodeURIComponent(file.name) + "-" + (startIndex + idx); + let rowsHTML = ""; + + if (pageFiles.length > 0) { + pageFiles.forEach((file, idx) => { + const rowKey = rowIdFor(file, idx); let rowHTML = buildFileTableRow(file, fakeBase); // add row id + data-file-name, and ensure the name cell also has "name-cell" rowHTML = rowHTML - .replace(")([\s\S]*?)(<\/td>)/, (m, open, inner, close) => { - // keep the original filename content, then add your tag badges, then close return `${open}${inner}${tagBadgesHTML}${close}`; } ); }); - } else { - rowsHTML += ``; + } else if (!hasFolders && totalFiles === 0) { + // Only show "No files found" if there are no folders either + rowsHTML += ``; } + rowsHTML += "
No files found.
${t("no_files_found") || "No files found."}
"; + const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML; + // Inject inline folder rows for THIS page (Explorer-style) + if (window.showInlineFolders !== false && pageFolders.length) { + injectInlineFolderRows(fileListContent, folder, pageFolders); + } wireSelectAll(fileListContent); // PATCH each row's preview/thumb to use the secure API URLs - if (totalFiles > 0) { - filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { - const rowEl = document.getElementById(`file-row-${encodeURIComponent(file.name)}-${startIndex + idx}`); - if (!rowEl) return; - - const previewUrl = apiFileUrl(file.folder || folder, file.name, true); - - // Preview button dataset - const previewBtn = rowEl.querySelector(".preview-btn"); - if (previewBtn) { - previewBtn.dataset.previewUrl = previewUrl; - previewBtn.dataset.previewName = file.name; - } - - // Thumbnail (if present) - const thumbImg = rowEl.querySelector("img"); - if (thumbImg) { - thumbImg.src = previewUrl; - thumbImg.setAttribute("data-cache-key", previewUrl); - } - - // Any anchor that might have been built to point at a file path - rowEl.querySelectorAll('a[href]').forEach(a => { - // Only rewrite obvious file anchors (ignore actions with '#', 'javascript:', etc.) - if (/^#|^javascript:/i.test(a.getAttribute('href') || '')) return; - a.href = previewUrl; + // PATCH each row's preview/thumb to use the secure API URLs + if (pageFiles.length > 0) { + pageFiles.forEach((file, idx) => { + const rowKey = rowIdFor(file, idx); + const rowEl = document.getElementById(`file-row-${rowKey}`); + if (!rowEl) return; + + const previewUrl = apiFileUrl(file.folder || folder, file.name, true); + + // Preview button dataset + const previewBtn = rowEl.querySelector(".preview-btn"); + if (previewBtn) { + previewBtn.dataset.previewUrl = previewUrl; + previewBtn.dataset.previewName = file.name; + } + + // Thumbnail (if present) + const thumbImg = rowEl.querySelector("img"); + if (thumbImg) { + thumbImg.src = previewUrl; + thumbImg.setAttribute("data-cache-key", previewUrl); + } + + // Any anchor that might have been built to point at a file path + rowEl.querySelectorAll('a[href]').forEach(a => { + // Only rewrite obvious file anchors (ignore actions with '#', 'javascript:', etc.) + if (/^#|^javascript:/i.test(a.getAttribute('href') || '')) return; + a.href = previewUrl; + }); }); - }); - } + } fileListContent.querySelectorAll('.folder-item').forEach(el => { el.addEventListener('click', () => loadFileList(el.dataset.folder)); @@ -1147,7 +1687,7 @@ export function renderFileTable(folder, container, subfolders) { renderFileTable(folder, container); }); - // Row-select + // Row-select (only file rows have checkboxes; folder rows are ignored here) fileListContent.querySelectorAll("tbody tr").forEach(row => { row.addEventListener("click", e => { const cb = row.querySelector(".file-checkbox"); @@ -1156,6 +1696,8 @@ export function renderFileTable(folder, container, subfolders) { }); }); + + // Download buttons fileListContent.querySelectorAll(".download-btn").forEach(btn => { btn.addEventListener("click", e => { @@ -1248,12 +1790,15 @@ export function renderFileTable(folder, container, subfolders) { }); updateFileActionButtons(); + // Dragstart only for file rows (skip folder rows) document.querySelectorAll("#fileList tbody tr").forEach(row => { + if (row.classList.contains("folder-row")) return; row.setAttribute("draggable", "true"); import('./fileDragDrop.js?v={{APP_QVER}}').then(module => { row.addEventListener("dragstart", module.fileDragStartHandler); }); }); + document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => { btn.addEventListener("click", e => e.stopPropagation()); }); diff --git a/public/js/folderManager.js b/public/js/folderManager.js index 70195dd..508542b 100644 --- a/public/js/folderManager.js +++ b/public/js/folderManager.js @@ -928,7 +928,6 @@ export function openColorFolderModal(folder) { border: 1px solid var(--ghost-border, #cfcfcf); color: var(--ghost-fg, #222); padding: 6px 12px; - border-radius: 8px; } #colorFolderModal .btn-ghost:hover { background: var(--ghost-hover-bg, #f5f5f5); diff --git a/public/js/i18n.js b/public/js/i18n.js index a3daa7e..711096c 100644 --- a/public/js/i18n.js +++ b/public/js/i18n.js @@ -268,7 +268,7 @@ const translations = { "columns": "Columns", "row_height": "Row Height", "api_docs": "API Docs", - "show_folders_above_files": "Show folders above files", + "show_folders_above_files": "Show folder strip above list", "display": "Display", "create_file": "Create File", "create_new_file": "Create New File", @@ -331,7 +331,13 @@ const translations = { "folder_help_last_folder": "Your last opened folder is remembered. If you lose access, we pick the first allowed folder automatically.", "folder_help_breadcrumbs": "Use the breadcrumb to jump up the path. You can also drop onto a breadcrumb.", "folder_help_permissions": "Buttons enable/disable based on your permissions for the selected folder.", - "load_more_folders": "Load More Folders" + "load_more_folders": "Load More Folders", + "show_inline_folders": "Show folders as rows above files", + "name": "Name", + "size": "Size", + "modified": "Modified", + "created": "Created", + "owner": "Owner" }, es: { "please_log_in_to_continue": "Por favor, inicie sesión para continuar.", diff --git a/src/models/FolderModel.php b/src/models/FolderModel.php index e920db8..8255765 100644 --- a/src/models/FolderModel.php +++ b/src/models/FolderModel.php @@ -11,87 +11,111 @@ class FolderModel * Ownership mapping helpers (stored in META_DIR/folder_owners.json) * ============================================================ */ - public static function countVisible(string $folder, string $user, array $perms): array - { - $folder = ACL::normalizeFolder($folder); - - // If the user can't view this folder at all, short-circuit (admin/read/read_own) - $canViewFolder = ACL::isAdmin($perms) - || ACL::canRead($user, $perms, $folder) - || ACL::canReadOwn($user, $perms, $folder); - if (!$canViewFolder) return ['folders' => 0, 'files' => 0]; - - $base = realpath((string)UPLOAD_DIR); - if ($base === false) return ['folders' => 0, 'files' => 0]; - - // Resolve target dir + ACL-relative prefix - if ($folder === 'root') { - $dir = $base; - $relPrefix = ''; - } else { - $parts = array_filter(explode('/', $folder), fn($p) => $p !== ''); - foreach ($parts as $seg) { - if (!self::isSafeSegment($seg)) return ['folders' => 0, 'files' => 0]; - } - $guess = $base . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts); - $dir = self::safeReal($base, $guess); - if ($dir === null || !is_dir($dir)) return ['folders' => 0, 'files' => 0]; - $relPrefix = implode('/', $parts); - } - - // Ignore lists (expandable) - $IGNORE = ['@eaDir', '#recycle', '.DS_Store', 'Thumbs.db']; - $SKIP = ['trash', 'profile_pics']; - - $entries = @scandir($dir); - if ($entries === false) return ['folders' => 0, 'files' => 0]; - - $hasChildFolder = false; - $hasFile = false; - - // Cap scanning to avoid pathological dirs - $MAX_SCAN = 4000; - $scanned = 0; - - foreach ($entries as $name) { - if (++$scanned > $MAX_SCAN) break; - - if ($name === '.' || $name === '..') continue; - if ($name[0] === '.') continue; - if (in_array($name, $IGNORE, true)) continue; - if (in_array(strtolower($name), $SKIP, true)) continue; - if (!self::isSafeSegment($name)) continue; - - $abs = $dir . DIRECTORY_SEPARATOR . $name; - - if (@is_dir($abs)) { - // Symlink defense on children - if (@is_link($abs)) { - $safe = self::safeReal($base, $abs); - if ($safe === null || !is_dir($safe)) continue; - } - // Only count child dirs the user can view (admin/read/read_own) - $childRel = ($relPrefix === '' ? $name : $relPrefix . '/' . $name); - if ( - ACL::isAdmin($perms) - || ACL::canRead($user, $perms, $childRel) - || ACL::canReadOwn($user, $perms, $childRel) - ) { - $hasChildFolder = true; - } - } elseif (@is_file($abs)) { - // Any file present is enough for the "files" flag once the folder itself is viewable - $hasFile = true; - } - - if ($hasChildFolder && $hasFile) break; // early exit - } - - return [ - 'folders' => $hasChildFolder ? 1 : 0, - 'files' => $hasFile ? 1 : 0, - ]; - } + public static function countVisible(string $folder, string $user, array $perms): array + { + $folder = ACL::normalizeFolder($folder); + + // If the user can't view this folder at all, short-circuit (admin/read/read_own) + $canViewFolder = ACL::isAdmin($perms) + || ACL::canRead($user, $perms, $folder) + || ACL::canReadOwn($user, $perms, $folder); + if (!$canViewFolder) { + return ['folders' => 0, 'files' => 0, 'bytes' => 0]; + } + + // NEW: distinguish full read vs own-only for this folder + $hasFullRead = ACL::isAdmin($perms) || ACL::canRead($user, $perms, $folder); + // if !$hasFullRead but $canViewFolder is true, they’re effectively "view own" only + + $base = realpath((string)UPLOAD_DIR); + if ($base === false) { + return ['folders' => 0, 'files' => 0, 'bytes' => 0]; + } + + // Resolve target dir + ACL-relative prefix + if ($folder === 'root') { + $dir = $base; + $relPrefix = ''; + } else { + $parts = array_filter(explode('/', $folder), fn($p) => $p !== ''); + foreach ($parts as $seg) { + if (!self::isSafeSegment($seg)) { + return ['folders' => 0, 'files' => 0, 'bytes' => 0]; + } + } + $guess = $base . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts); + $dir = self::safeReal($base, $guess); + if ($dir === null || !is_dir($dir)) { + return ['folders' => 0, 'files' => 0, 'bytes' => 0]; + } + $relPrefix = implode('/', $parts); + } + + $IGNORE = ['@eaDir', '#recycle', '.DS_Store', 'Thumbs.db']; + $SKIP = ['trash', 'profile_pics']; + + $entries = @scandir($dir); + if ($entries === false) { + return ['folders' => 0, 'files' => 0, 'bytes' => 0]; + } + + $folderCount = 0; + $fileCount = 0; + $totalBytes = 0; + + $MAX_SCAN = 4000; + $scanned = 0; + + foreach ($entries as $name) { + if (++$scanned > $MAX_SCAN) { + break; + } + + if ($name === '.' || $name === '..') continue; + if ($name[0] === '.') continue; + if (in_array($name, $IGNORE, true)) continue; + if (in_array(strtolower($name), $SKIP, true)) continue; + if (!self::isSafeSegment($name)) continue; + + $abs = $dir . DIRECTORY_SEPARATOR . $name; + + if (@is_dir($abs)) { + if (@is_link($abs)) { + $safe = self::safeReal($base, $abs); + if ($safe === null || !is_dir($safe)) { + continue; + } + } + + $childRel = ($relPrefix === '' ? $name : $relPrefix . '/' . $name); + if ( + ACL::isAdmin($perms) + || ACL::canRead($user, $perms, $childRel) + || ACL::canReadOwn($user, $perms, $childRel) + ) { + $folderCount++; + } + } elseif (@is_file($abs)) { + // Only count files if the user has full read on *this* folder. + // If they’re view_own-only here, don’t leak or mis-report counts. + if (!$hasFullRead) { + continue; + } + + $fileCount++; + $sz = @filesize($abs); + if (is_int($sz) && $sz > 0) { + $totalBytes += $sz; + } + } + } + + return [ + 'folders' => $folderCount, + 'files' => $fileCount, + 'bytes' => $totalBytes, + ]; + } /* Helpers (private) */ private static function isSafeSegment(string $name): bool