release(v1.9.14): inline folder rows, synced folder icons, and compact theme polish

This commit is contained in:
Ryan
2025-11-21 02:12:17 -05:00
committed by GitHub
parent bf077b142b
commit 1b4a93b060
9 changed files with 942 additions and 822 deletions

View File

@@ -1,5 +1,23 @@
# Changelog # 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) ## Changes 11/20/2025 (v1.9.13)
release(v1.9.13): style(ui): compact dual-theme polish for lists, inputs, search & modals release(v1.9.13): style(ui): compact dual-theme polish for lists, inputs, search & modals

View File

@@ -614,7 +614,8 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover{background-color: rgba
#fileList button.edit-btn{background-color: #007bff; #fileList button.edit-btn{background-color: #007bff;
color: white;} color: white;}
.rename-btn .material-icons, .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; #fileList table{background-color: transparent;
border-collapse: collapse !important; border-collapse: collapse !important;
border-spacing: 0 !important; border-spacing: 0 !important;
@@ -818,9 +819,34 @@ label{font-size: 0.9rem;}
.folder-actions .btn, .folder-actions .btn,
.folder-actions .material-icons{transition: none;} .folder-actions .material-icons{transition: none;}
} }
#moveFolderBtn{background-color: #ff9800; #moveFolderBtn,
.folder-move-btn{background-color: #ff9800;
border-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;} .row-selected{background-color: #f2f2f2 !important;}
.dark-mode .row-selected{background-color: #444 !important; .dark-mode .row-selected{background-color: #444 !important;
color: #fff !important;} color: #fff !important;}
@@ -947,7 +973,8 @@ label{font-size: 0.9rem;}
transform: none !important; transform: none !important;
box-shadow: 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; width: 32px;
height: 32px; height: 32px;
line-height: 1 !important; 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; .btn-group.btn-group-sm[aria-label="File actions"] .btn .material-symbols-rounded{transition: none !important;
transform: none !important;} transform: none !important;}
} }
.breadcrumb-link{cursor: pointer; .breadcrumb-link{cursor: pointer;
color: #007bff; color: #007bff;
text-decoration: underline;} text-decoration: underline;}
@@ -1693,8 +1721,6 @@ body.dark-mode .folder-strip-container .folder-item:hover{background-color: rgba
--filr-folder-stroke:#a87312; --filr-folder-stroke:#a87312;
--filr-paper-fill: #ffffff; --filr-paper-fill: #ffffff;
--filr-paper-stroke: #9fb3d6; --filr-paper-stroke: #9fb3d6;
--row-h: 28px; --row-h: 28px;
--twisty: 24px; --twisty: 24px;
--twisty-gap: -5px; --twisty-gap: -5px;
@@ -1841,7 +1867,6 @@ body.dark-mode #folderTreeContainer .folder-icon .lock-keyhole{fill: rgba(255,25
align-items: center; align-items: center;
gap: 8px; gap: 8px;
justify-content: center; justify-content: center;
border-radius: 10px;
border: 1px solid var(--tree-ghost-border); border: 1px solid var(--tree-ghost-border);
background: var(--tree-ghost-bg); background: var(--tree-ghost-bg);
color: var(--tree-ghost-fg); 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 FileRise polish compact theme layer
============================================ */ ============================================ */
: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}
/* Tokens */ .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)}
:root { #customToast{border-radius:999px}
--filr-radius-lg: 14px; #folderTreeContainer .folder-row{border-radius:8px}
--filr-radius-xl: 18px; .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)}
--filr-shadow-soft: 0 12px 35px rgba(15,23,42,.14); .btn-primary,#createBtn,#uploadBtn,#submitCreateFolder,#submitRenameFolder,#confirmMoveFolder{box-shadow:0 2px 4px rgba(0,0,0,.6)}
--filr-shadow-subtle: 0 8px 20px rgba(15,23,42,.10); .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)}
--filr-header-blur: 18px; #deleteSelectedBtn,#deleteAllBtn,#deleteTrashSelectedBtn,#deleteFolderBtn,#confirmDeleteFolder{border-color:rgba(248,113,113,.9);box-shadow:0 8px 18px rgba(248,113,113,.35)}
--filr-transition-fast: 150ms ease-out; 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)}
--filr-transition-med: 220ms cubic-bezier(.22,.61,.36,1); 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)}
/* Dark theme */ #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)}
--fr-bg-dark: #0f0f0f; body{min-height:100vh}
--fr-surface-dark: #212121; body.dark-mode{background:var(--fr-bg-dark)!important;color:#f1f1f1!important;background-image:none!important}
--fr-surface-dark-2: #181818; 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}
--fr-border-dark: #303030; 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}
--fr-muted-dark: #aaaaaa; 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}
/* Light theme */ 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}
--fr-bg-light: #f9f9f9; body.dark-mode .modal{background-color:rgba(0,0,0,.65)!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important}
--fr-surface-light: #ffffff; 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}
--fr-surface-light-2: #f1f1f1; 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}
--fr-border-light: #e5e5e5; 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}
--fr-muted-light: #606060; 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}
/* Pro badge */ body.dark-mode .folder-strip-container.folder-strip-mobile{background:var(--fr-surface-dark-2)!important;border:1px solid var(--fr-border-dark)!important}
.btn-pro-admin { 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}
background: linear-gradient(135deg, #ff9800, #ff5722); body.dark-mode #fileSummary{color:var(--fr-muted-dark)!important}
border-color: #ff9800; 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}
color: #1b0f00 !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}
font-weight: 600; body:not(.dark-mode){background:var(--fr-bg-light)!important;color:#111!important;background-image:none!important}
box-shadow: 0 0 10px rgba(255,152,0,.4); 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}
/* Toast base shape (colors themed below) */ body:not(.dark-mode) #fileList table thead th{background:var(--fr-surface-light-2)!important;border-bottom:1px solid var(--fr-border-light)!important}
#customToast { 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}
border-radius: 999px; 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}
/* Folder tree row rounding */ 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}
#folderTreeContainer .folder-row { border-radius: 8px; } 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}
/* Buttons pill style + hover lift */ 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}
.btn, body:not(.dark-mode) .folder-strip-container.folder-strip-mobile{background:#f1f1f1!important;border:1px solid var(--fr-border-light)!important}
#customChooseBtn { 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}
border-radius: 999px; body:not(.dark-mode) #fileSummary{color:var(--fr-muted-light)!important}
font-weight: 500; 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}
border: 1px solid transparent; #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}
transition: #searchIcon .material-icons{font-size:20px;line-height:1;color:#555}
background-color var(--filr-transition-fast), #searchIcon:hover{background:#f5f5f5}
box-shadow var(--filr-transition-fast), #searchIcon+#searchInput{height:36px;border-radius:0 999px 999px 0;border-left:none;padding-top:6px;padding-bottom:6px}
transform var(--filr-transition-fast), body.dark-mode #searchIcon{background:#212121;border-color:#3d3d3d}
border-color var(--filr-transition-fast); body.dark-mode #searchIcon .material-icons{color:#f1f1f1}
} body.dark-mode #searchIcon:hover{background:#303030}
body.dark-mode #searchIcon+#searchInput{border-left:none}
/* Upload / create / primary: shared shadow in light + dark */ #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}
.btn-primary, #advancedSearchToggle:hover,#advancedSearchToggle:focus-visible{background:#e8e8e8;box-shadow:0 1px 4px rgba(0,0,0,.16);outline:none;transform:translateY(-1px)}
#createBtn, .dark-mode #advancedSearchToggle{background:#2a2a2a;border-color:#444;color:#f1f1f1}
#uploadBtn { .dark-mode #advancedSearchToggle:hover,.dark-mode #advancedSearchToggle:focus-visible{background:#333;box-shadow:0 1px 4px rgba(0,0,0,.5)}
box-shadow: 0 2px 4px rgba(0,0,0,.6); .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)}
.btn-primary:hover, .custom-prev-next-btn:disabled{opacity:.5;cursor:default;box-shadow:none}
#createBtn:hover, body.dark-mode .custom-prev-next-btn{background:#212121;border-color:#3d3d3d;color:#f1f1f1}
#uploadBtn:hover { 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)}
filter: brightness(1.04); 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}
transform: translateY(-1px); #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}
box-shadow: 0 10px 22px rgba(0,140,180,.28); #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}
/* Destructive buttons */ body.dark-mode #decreaseFont,body.dark-mode #increaseFont{background:#212121;border-color:#3d3d3d;color:#f1f1f1}
#deleteSelectedBtn, 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)}
#deleteAllBtn, #closeEditorX{margin-right:10px}
#deleteTrashSelectedBtn { #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}
border-color: rgba(248,113,113,.9); #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}
box-shadow: 0 8px 18px rgba(248,113,113,.35); #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}
/* Forms & inputs base */ #fileList .folder-row-icon .paper-ink{stroke:#4da3ff;stroke-width:.9;stroke-linecap:round;stroke-linejoin:round;fill:none;opacity:.85}
input[type="text"], #fileList .folder-row-icon .lip-highlight{fill:none;vector-effect:non-scaling-stroke;stroke-linecap:round;stroke-linejoin:round}
input[type="password"], #fileList .folder-row-name{font-weight:500;margin-right:4px}
input[type="email"], #fileList .folder-row-meta{margin-left:4px;opacity:.75;font-size:.86em}
input[type="url"], #fileList tbody tr.folder-row{height:var(--file-row-height,44px);cursor:pointer}
select, #fileList tbody tr.folder-row .folder-name-cell{padding-top:0;padding-bottom:0}
textarea { #fileList tbody tr.folder-row .folder-row-inner{cursor:inherit}
border-radius: 10px; #fileList tbody tr.folder-row .folder-icon-cell{text-align:left;vertical-align:middle}
padding: 8px 10px; #fileList tbody tr.folder-row .folder-row-icon svg{display:block}
font-size: .92rem; .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}
transition: .folder-row-inner{display:flex;align-items:center}
border-color var(--filr-transition-fast), #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}
box-shadow var(--filr-transition-fast), #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))}
background-color var(--filr-transition-fast); #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}
input:focus, #fileList tr.folder-row .folder-row-inner,#fileList tr.folder-row .folder-row-name{cursor:inherit}
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;
}

View File

@@ -90,7 +90,8 @@ export function initializeApp() {
window.currentFolder = last ? last : "root"; window.currentFolder = last ? last : "root";
const stored = localStorage.getItem('showFoldersInList'); 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) // Load public site config early (safe subset)
loadAdminConfigFunc(); loadAdminConfigFunc();
@@ -99,6 +100,7 @@ export function initializeApp() {
initTagSearch(); initTagSearch();
/*
// Hook DnD relay from fileList area into upload area // Hook DnD relay from fileList area into upload area
const fileListArea = document.getElementById('fileList'); const fileListArea = document.getElementById('fileList');
@@ -146,7 +148,7 @@ export function initializeApp() {
uploadArea.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true })); uploadArea.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true }));
} }
}); });
} }*/
// App subsystems // App subsystems
initDragAndDrop(); initDragAndDrop();

View File

@@ -351,30 +351,73 @@ export async function openUserPanel() {
langFs.appendChild(langSel); langFs.appendChild(langSel);
content.appendChild(langFs); content.appendChild(langFs);
// --- Display fieldset: “Show folders above files” --- // --- Display fieldset: strip + inline folder rows ---
const dispFs = document.createElement('fieldset'); const dispFs = document.createElement('fieldset');
dispFs.style.marginBottom = '15px'; dispFs.style.marginBottom = '15px';
const dispLegend = document.createElement('legend'); const dispLegend = document.createElement('legend');
dispLegend.textContent = t('display'); dispLegend.textContent = t('display');
dispFs.appendChild(dispLegend); dispFs.appendChild(dispLegend);
const dispLabel = document.createElement('label');
dispLabel.style.cursor = 'pointer'; // 1) Show folder strip above list
const dispCb = document.createElement('input'); const stripLabel = document.createElement('label');
dispCb.type = 'checkbox'; stripLabel.style.cursor = 'pointer';
dispCb.id = 'showFoldersInList'; stripLabel.style.display = 'block';
dispCb.style.verticalAlign = 'middle'; stripLabel.style.marginBottom = '4px';
const stored = localStorage.getItem('showFoldersInList');
dispCb.checked = stored === null ? true : stored === 'true'; const stripCb = document.createElement('input');
dispLabel.appendChild(dispCb); stripCb.type = 'checkbox';
dispLabel.append(` ${t('show_folders_above_files')}`); stripCb.id = 'showFoldersInList';
dispFs.appendChild(dispLabel); 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);
// youll 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); content.appendChild(dispFs);
dispCb.addEventListener('change', () => { // Handlers: toggle + refresh list
window.showFoldersInList = dispCb.checked; stripCb.addEventListener('change', () => {
localStorage.setItem('showFoldersInList', dispCb.checked); window.showFoldersInList = stripCb.checked;
// reload the entire file list (and strip) in one go: localStorage.setItem('showFoldersInList', stripCb.checked);
loadFileList(window.currentFolder); 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 imageinput change // wire up imageinput change
@@ -425,6 +468,18 @@ export async function openUserPanel() {
modal.querySelector('#userTOTPEnabled').checked = totp_enabled; modal.querySelector('#userTOTPEnabled').checked = totp_enabled;
modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en'; modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en';
modal.querySelector('h3').textContent = `${t('user_panel')} (${username})`; 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 // show

View File

@@ -160,11 +160,11 @@ export function buildFileTableHeader(sortOrder) {
<thead> <thead>
<tr> <tr>
<th class="checkbox-col"><input type="checkbox" id="selectAll"></th> <th class="checkbox-col"><input type="checkbox" id="selectAll"></th>
<th data-column="name" class="sortable-col">${t("file_name")} ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="name" class="sortable-col">${t("name")} ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="modified" class="hide-small sortable-col">${t("date_modified")} ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="modified" class="hide-small sortable-col">${t("modified")} ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploaded" class="hide-small hide-medium sortable-col">${t("upload_date")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="uploaded" class="hide-small hide-medium sortable-col">${t("created")} ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="size" class="hide-small sortable-col">${t("file_size")} ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="size" class="hide-small sortable-col">${t("size")} ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th data-column="uploader" class="hide-small hide-medium sortable-col">${t("uploader")} ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th> <th data-column="uploader" class="hide-small hide-medium sortable-col">${t("owner")} ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</th>
<th>${t("actions")}</th> <th>${t("actions")}</th>
</tr> </tr>
</thead> </thead>

View File

@@ -240,16 +240,29 @@ window.addEventListener('resize', () => {
if (strip) applyFolderStripLayout(strip); 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) => { window.addEventListener('folderColorChanged', (e) => {
const { folder } = e.detail || {}; const { folder } = e.detail || {};
if (!folder) return; if (!folder) return;
// Update the strip (if that folder is currently shown) // 1) Update the strip (if that folder is currently shown)
repaintStripIcon(folder); repaintStripIcon(folder);
// And refresh the tree icon too (existing function) // 2) Refresh the tree icon (existing function)
try { refreshFolderIcon(folder); } catch { } 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 // Hide "Edit" for files >10 MiB
@@ -259,11 +272,25 @@ const MAX_EDIT_BYTES = 10 * 1024 * 1024;
let __fileListReqSeq = 0; let __fileListReqSeq = 0;
window.itemsPerPage = parseInt( window.itemsPerPage = parseInt(
localStorage.getItem('itemsPerPage') || window.itemsPerPage || '10', localStorage.getItem('itemsPerPage') || window.itemsPerPage || '50',
10 10
); );
window.currentPage = window.currentPage || 1; window.currentPage = window.currentPage || 1;
window.viewMode = localStorage.getItem("viewMode") || "table"; 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. // Global flag for advanced search mode.
window.advancedSearchEnabled = false; window.advancedSearchEnabled = false;
@@ -387,7 +414,6 @@ function attachStripIconAsync(hostEl, fullPath, size = 28) {
const back = _lighten(hex, 14); const back = _lighten(hex, 14);
const stroke = _darken(hex, 22); 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-front', front);
hostEl.style.setProperty('--filr-folder-back', back); hostEl.style.setProperty('--filr-folder-back', back);
hostEl.style.setProperty('--filr-folder-stroke', stroke); hostEl.style.setProperty('--filr-folder-stroke', stroke);
@@ -395,15 +421,26 @@ function attachStripIconAsync(hostEl, fullPath, size = 28) {
const iconSpan = hostEl.querySelector('.folder-svg'); const iconSpan = hostEl.querySelector('.folder-svg');
if (!iconSpan) return; if (!iconSpan) return;
// 1) initial "empty" icon
iconSpan.dataset.kind = 'empty'; 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()}`; const url = `/api/folder/isEmpty.php?folder=${encodeURIComponent(fullPath)}&t=${Date.now()}`;
_fetchJSONWithTimeout(url, 2500).then(({ folders = 0, files = 0 }) => { _fetchJSONWithTimeout(url, 2500)
if ((folders + files) > 0 && iconSpan.dataset.kind !== 'paper') { .then(({ folders = 0, files = 0 }) => {
iconSpan.dataset.kind = 'paper'; if ((folders + files) > 0 && iconSpan.dataset.kind !== 'paper') {
iconSpan.innerHTML = folderSVG('paper'); // 2) swap to "paper" icon
} iconSpan.dataset.kind = 'paper';
}).catch(() => { }); 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 ?? {}; 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) ---- // ---- Viewed badges (table + gallery) ----
// ---------- Badge factory (center text vertically) ---------- // ---------- Badge factory (center text vertically) ----------
function makeBadge(state) { function makeBadge(state) {
@@ -917,6 +1004,13 @@ export async function loadFileList(folderParam) {
document.documentElement.style.setProperty("--file-row-height", v + "px"); document.documentElement.style.setProperty("--file-row-height", v + "px");
localStorage.setItem("rowHeight", v); localStorage.setItem("rowHeight", v);
rowValue.textContent = v + "px"; 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_"); return !hidden.has(lower) && !lower.startsWith("resumable_");
}); });
// Expose for inline folder rows in table view
window.currentSubfolders = subfolders;
let strip = document.getElementById("folderStripContainer"); let strip = document.getElementById("folderStripContainer");
if (!strip) { if (!strip) {
strip = document.createElement("div"); strip = document.createElement("div");
@@ -976,6 +1073,11 @@ export async function loadFileList(folderParam) {
// NEW: paged + responsive strip // NEW: paged + responsive strip
renderFolderStripPaged(strip, subfolders); 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 { } catch {
// ignore folder errors; rows already rendered // 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;
// 3044: untouched (you said this range is perfect)
// 4560: 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 * Render table view
*/ */
export function renderFileTable(folder, container, subfolders) { export function renderFileTable(folder, container, subfolders) {
const fileListContent = container || document.getElementById("fileList"); const fileListContent = container || document.getElementById("fileList");
const searchTerm = (window.currentSearchTerm || "").toLowerCase(); 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; let currentPage = window.currentPage || 1;
// Files (filtered by search)
const filteredFiles = searchFiles(searchTerm); const filteredFiles = searchFiles(searchTerm);
const totalFiles = filteredFiles.length; // Inline folders: sort once (Explorer-style A→Z)
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); 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) { if (currentPage > totalPages) {
currentPage = totalPages > 0 ? totalPages : 1; currentPage = totalPages;
window.currentPage = currentPage; 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, // We pass a harmless "base" string to keep buildFileTableRow happy,
// then we will FIX the preview/thumbnail URLs to the API below. // then we will FIX the preview/thumbnail URLs to the API below.
const fakeBase = "#/"; const fakeBase = "#/";
@@ -1040,19 +1574,16 @@ export function renderFileTable(folder, container, subfolders) {
return `<table class="filr-table"${attrs}>`; return `<table class="filr-table"${attrs}>`;
}); });
const startIndex = (currentPage - 1) * itemsPerPageSetting; let rowsHTML = "<tbody>";
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
let rowsHTML = "<tbody>"; if (pageFiles.length > 0) {
if (totalFiles > 0) { pageFiles.forEach((file, idx) => {
filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { const rowKey = rowIdFor(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 rowHTML = buildFileTableRow(file, fakeBase); let rowHTML = buildFileTableRow(file, fakeBase);
// add row id + data-file-name, and ensure the name cell also has "name-cell" // add row id + data-file-name, and ensure the name cell also has "name-cell"
rowHTML = rowHTML rowHTML = rowHTML
.replace("<tr", `<tr id="file-row-${idSafe}" data-file-name="${escapeHTML(file.name)}"`) .replace("<tr", `<tr id="file-row-${rowKey}" data-file-name="${escapeHTML(file.name)}"`)
.replace('class="file-name-cell"', 'class="file-name-cell name-cell"'); .replace('class="file-name-cell"', 'class="file-name-cell name-cell"');
let tagBadgesHTML = ""; let tagBadgesHTML = "";
@@ -1063,54 +1594,63 @@ export function renderFileTable(folder, container, subfolders) {
}); });
tagBadgesHTML += "</div>"; tagBadgesHTML += "</div>";
} }
rowsHTML += rowHTML.replace( rowsHTML += rowHTML.replace(
/(<td\s+class="[^"]*\bfile-name-cell\b[^"]*">)([\s\S]*?)(<\/td>)/, /(<td\s+class="[^"]*\bfile-name-cell\b[^"]*">)([\s\S]*?)(<\/td>)/,
(m, open, inner, close) => { (m, open, inner, close) => {
// keep the original filename content, then add your tag badges, then close
return `${open}<span class="filename-text">${inner}</span>${tagBadgesHTML}${close}`; return `${open}<span class="filename-text">${inner}</span>${tagBadgesHTML}${close}`;
} }
); );
}); });
} else { } else if (!hasFolders && totalFiles === 0) {
rowsHTML += `<tr><td colspan="8">No files found.</td></tr>`; // Only show "No files found" if there are no folders either
rowsHTML += `<tr><td colspan="8">${t("no_files_found") || "No files found."}</td></tr>`;
} }
rowsHTML += "</tbody></table>"; rowsHTML += "</tbody></table>";
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML; 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); wireSelectAll(fileListContent);
// PATCH each row's preview/thumb to use the secure API URLs // PATCH each row's preview/thumb to use the secure API URLs
if (totalFiles > 0) { // PATCH each row's preview/thumb to use the secure API URLs
filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { if (pageFiles.length > 0) {
const rowEl = document.getElementById(`file-row-${encodeURIComponent(file.name)}-${startIndex + idx}`); pageFiles.forEach((file, idx) => {
if (!rowEl) return; const rowKey = rowIdFor(file, idx);
const rowEl = document.getElementById(`file-row-${rowKey}`);
if (!rowEl) return;
const previewUrl = apiFileUrl(file.folder || folder, file.name, true); const previewUrl = apiFileUrl(file.folder || folder, file.name, true);
// Preview button dataset // Preview button dataset
const previewBtn = rowEl.querySelector(".preview-btn"); const previewBtn = rowEl.querySelector(".preview-btn");
if (previewBtn) { if (previewBtn) {
previewBtn.dataset.previewUrl = previewUrl; previewBtn.dataset.previewUrl = previewUrl;
previewBtn.dataset.previewName = file.name; previewBtn.dataset.previewName = file.name;
} }
// Thumbnail (if present) // Thumbnail (if present)
const thumbImg = rowEl.querySelector("img"); const thumbImg = rowEl.querySelector("img");
if (thumbImg) { if (thumbImg) {
thumbImg.src = previewUrl; thumbImg.src = previewUrl;
thumbImg.setAttribute("data-cache-key", previewUrl); thumbImg.setAttribute("data-cache-key", previewUrl);
} }
// Any anchor that might have been built to point at a file path // Any anchor that might have been built to point at a file path
rowEl.querySelectorAll('a[href]').forEach(a => { rowEl.querySelectorAll('a[href]').forEach(a => {
// Only rewrite obvious file anchors (ignore actions with '#', 'javascript:', etc.) // Only rewrite obvious file anchors (ignore actions with '#', 'javascript:', etc.)
if (/^#|^javascript:/i.test(a.getAttribute('href') || '')) return; if (/^#|^javascript:/i.test(a.getAttribute('href') || '')) return;
a.href = previewUrl; a.href = previewUrl;
});
}); });
}); }
}
fileListContent.querySelectorAll('.folder-item').forEach(el => { fileListContent.querySelectorAll('.folder-item').forEach(el => {
el.addEventListener('click', () => loadFileList(el.dataset.folder)); el.addEventListener('click', () => loadFileList(el.dataset.folder));
@@ -1147,7 +1687,7 @@ export function renderFileTable(folder, container, subfolders) {
renderFileTable(folder, container); renderFileTable(folder, container);
}); });
// Row-select // Row-select (only file rows have checkboxes; folder rows are ignored here)
fileListContent.querySelectorAll("tbody tr").forEach(row => { fileListContent.querySelectorAll("tbody tr").forEach(row => {
row.addEventListener("click", e => { row.addEventListener("click", e => {
const cb = row.querySelector(".file-checkbox"); const cb = row.querySelector(".file-checkbox");
@@ -1156,6 +1696,8 @@ export function renderFileTable(folder, container, subfolders) {
}); });
}); });
// Download buttons // Download buttons
fileListContent.querySelectorAll(".download-btn").forEach(btn => { fileListContent.querySelectorAll(".download-btn").forEach(btn => {
btn.addEventListener("click", e => { btn.addEventListener("click", e => {
@@ -1248,12 +1790,15 @@ export function renderFileTable(folder, container, subfolders) {
}); });
updateFileActionButtons(); updateFileActionButtons();
// Dragstart only for file rows (skip folder rows)
document.querySelectorAll("#fileList tbody tr").forEach(row => { document.querySelectorAll("#fileList tbody tr").forEach(row => {
if (row.classList.contains("folder-row")) return;
row.setAttribute("draggable", "true"); row.setAttribute("draggable", "true");
import('./fileDragDrop.js?v={{APP_QVER}}').then(module => { import('./fileDragDrop.js?v={{APP_QVER}}').then(module => {
row.addEventListener("dragstart", module.fileDragStartHandler); row.addEventListener("dragstart", module.fileDragStartHandler);
}); });
}); });
document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => { document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => {
btn.addEventListener("click", e => e.stopPropagation()); btn.addEventListener("click", e => e.stopPropagation());
}); });

View File

@@ -928,7 +928,6 @@ export function openColorFolderModal(folder) {
border: 1px solid var(--ghost-border, #cfcfcf); border: 1px solid var(--ghost-border, #cfcfcf);
color: var(--ghost-fg, #222); color: var(--ghost-fg, #222);
padding: 6px 12px; padding: 6px 12px;
border-radius: 8px;
} }
#colorFolderModal .btn-ghost:hover { #colorFolderModal .btn-ghost:hover {
background: var(--ghost-hover-bg, #f5f5f5); background: var(--ghost-hover-bg, #f5f5f5);

View File

@@ -268,7 +268,7 @@ const translations = {
"columns": "Columns", "columns": "Columns",
"row_height": "Row Height", "row_height": "Row Height",
"api_docs": "API Docs", "api_docs": "API Docs",
"show_folders_above_files": "Show folders above files", "show_folders_above_files": "Show folder strip above list",
"display": "Display", "display": "Display",
"create_file": "Create File", "create_file": "Create File",
"create_new_file": "Create New 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_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_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.", "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: { es: {
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.", "please_log_in_to_continue": "Por favor, inicie sesión para continuar.",

View File

@@ -11,87 +11,111 @@ class FolderModel
* Ownership mapping helpers (stored in META_DIR/folder_owners.json) * Ownership mapping helpers (stored in META_DIR/folder_owners.json)
* ============================================================ */ * ============================================================ */
public static function countVisible(string $folder, string $user, array $perms): array public static function countVisible(string $folder, string $user, array $perms): array
{ {
$folder = ACL::normalizeFolder($folder); $folder = ACL::normalizeFolder($folder);
// If the user can't view this folder at all, short-circuit (admin/read/read_own) // If the user can't view this folder at all, short-circuit (admin/read/read_own)
$canViewFolder = ACL::isAdmin($perms) $canViewFolder = ACL::isAdmin($perms)
|| ACL::canRead($user, $perms, $folder) || ACL::canRead($user, $perms, $folder)
|| ACL::canReadOwn($user, $perms, $folder); || ACL::canReadOwn($user, $perms, $folder);
if (!$canViewFolder) return ['folders' => 0, 'files' => 0]; if (!$canViewFolder) {
return ['folders' => 0, 'files' => 0, 'bytes' => 0];
}
$base = realpath((string)UPLOAD_DIR); // NEW: distinguish full read vs own-only for this folder
if ($base === false) return ['folders' => 0, 'files' => 0]; $hasFullRead = ACL::isAdmin($perms) || ACL::canRead($user, $perms, $folder);
// if !$hasFullRead but $canViewFolder is true, theyre effectively "view own" only
// Resolve target dir + ACL-relative prefix $base = realpath((string)UPLOAD_DIR);
if ($folder === 'root') { if ($base === false) {
$dir = $base; return ['folders' => 0, 'files' => 0, 'bytes' => 0];
$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) // Resolve target dir + ACL-relative prefix
$IGNORE = ['@eaDir', '#recycle', '.DS_Store', 'Thumbs.db']; if ($folder === 'root') {
$SKIP = ['trash', 'profile_pics']; $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);
}
$entries = @scandir($dir); $IGNORE = ['@eaDir', '#recycle', '.DS_Store', 'Thumbs.db'];
if ($entries === false) return ['folders' => 0, 'files' => 0]; $SKIP = ['trash', 'profile_pics'];
$hasChildFolder = false; $entries = @scandir($dir);
$hasFile = false; if ($entries === false) {
return ['folders' => 0, 'files' => 0, 'bytes' => 0];
}
// Cap scanning to avoid pathological dirs $folderCount = 0;
$MAX_SCAN = 4000; $fileCount = 0;
$scanned = 0; $totalBytes = 0;
foreach ($entries as $name) { $MAX_SCAN = 4000;
if (++$scanned > $MAX_SCAN) break; $scanned = 0;
if ($name === '.' || $name === '..') continue; foreach ($entries as $name) {
if ($name[0] === '.') continue; if (++$scanned > $MAX_SCAN) {
if (in_array($name, $IGNORE, true)) continue; break;
if (in_array(strtolower($name), $SKIP, true)) continue; }
if (!self::isSafeSegment($name)) continue;
$abs = $dir . DIRECTORY_SEPARATOR . $name; 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;
if (@is_dir($abs)) { $abs = $dir . DIRECTORY_SEPARATOR . $name;
// 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 if (@is_dir($abs)) {
} if (@is_link($abs)) {
$safe = self::safeReal($base, $abs);
if ($safe === null || !is_dir($safe)) {
continue;
}
}
return [ $childRel = ($relPrefix === '' ? $name : $relPrefix . '/' . $name);
'folders' => $hasChildFolder ? 1 : 0, if (
'files' => $hasFile ? 1 : 0, 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 theyre view_own-only here, dont 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) */ /* Helpers (private) */
private static function isSafeSegment(string $name): bool private static function isSafeSegment(string $name): bool