From dbdf760d4da014a5ebb0186aa31c934302945ec7 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 11 Nov 2025 00:09:15 -0500 Subject: [PATCH] =?UTF-8?q?release(v1.9.3):=20unify=20folder=20icons=20acr?= =?UTF-8?q?oss=20tree=20&=20strip,=20add=20=E2=80=9Cpaper=E2=80=9D=20lines?= =?UTF-8?q?,=20live=20color=20sync,=20and=20vendor-aware=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-on-version.yml | 61 +- CHANGELOG.md | 37 + public/css/styles.css | 533 +++-- public/js/appCore.js | 2 +- public/js/domUtils.js | 6 +- public/js/fileListView.js | 2372 ++++++++++++---------- public/js/folderManager.js | 117 +- 7 files changed, 1817 insertions(+), 1311 deletions(-) diff --git a/.github/workflows/release-on-version.yml b/.github/workflows/release-on-version.yml index e58c01c..0fdfb53 100644 --- a/.github/workflows/release-on-version.yml +++ b/.github/workflows/release-on-version.yml @@ -21,15 +21,10 @@ permissions: jobs: release: runs-on: ubuntu-latest - - # Only run on: - # - push (master + version.js path filter already enforces that) - # - manual dispatch if: | github.event_name == 'push' || github.event_name == 'workflow_dispatch' - # Duplicate safety; also step "Skip if tag exists" will no-op if already released. concurrency: group: release-${{ github.event_name }}-${{ github.run_id }} cancel-in-progress: false @@ -46,17 +41,14 @@ jobs: else REF_IN="master" fi - # Resolve to a commit sha (allow branches or shas) if git ls-remote --exit-code --heads https://github.com/${{ github.repository }}.git "$REF_IN" >/dev/null 2>&1; then REF="$REF_IN" else - # Accept SHAs too; we’ll let checkout validate REF="$REF_IN" fi else REF="${{ github.sha }}" fi - echo "ref=$REF" >> "$GITHUB_OUTPUT" echo "Using ref=$REF" @@ -75,7 +67,6 @@ jobs: if [[ -n "${{ github.event.inputs.version || '' }}" ]]; then VER="${{ github.event.inputs.version }}" else - # Parse APP_VERSION from public/js/version.js (expects vX.Y.Z) if [[ ! -f public/js/version.js ]]; then echo "public/js/version.js not found; cannot auto-detect version." >&2 exit 1 @@ -86,7 +77,6 @@ jobs: exit 1 fi fi - echo "version=$VER" >> "$GITHUB_OUTPUT" echo "Detected version: $VER" @@ -124,20 +114,67 @@ jobs: ./ staging/ bash ./scripts/stamp-assets.sh "${VER}" "$(pwd)/staging" - - name: Verify placeholders removed + # --- PHP + Composer for vendor/ (production) --- + - name: Setup PHP + if: steps.tagcheck.outputs.exists == 'false' + id: php + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer:v2 + extensions: mbstring, json, curl, dom, fileinfo, openssl, zip + coverage: none + ini-values: memory_limit=-1 + + - name: Cache Composer downloads + if: steps.tagcheck.outputs.exists == 'false' + uses: actions/cache@v4 + with: + path: | + ~/.composer/cache + ~/.cache/composer + key: composer-${{ runner.os }}-php-${{ steps.php.outputs.php-version }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + composer-${{ runner.os }}-php-${{ steps.php.outputs.php-version }}- + + - name: Install PHP dependencies into staging + if: steps.tagcheck.outputs.exists == 'false' + env: + COMPOSER_MEMORY_LIMIT: -1 + shell: bash + run: | + set -euo pipefail + pushd staging >/dev/null + if [[ -f composer.json ]]; then + composer install \ + --no-dev \ + --prefer-dist \ + --no-interaction \ + --no-progress \ + --optimize-autoloader \ + --classmap-authoritative + test -f vendor/autoload.php || (echo "Composer install did not produce vendor/autoload.php" >&2; exit 1) + else + echo "No composer.json in staging; skipping vendor install." + fi + popd >/dev/null + # --- end Composer --- + + - name: Verify placeholders removed (skip vendor/) if: steps.tagcheck.outputs.exists == 'false' shell: bash run: | set -euo pipefail ROOT="$(pwd)/staging" if grep -R -n -E "{{APP_QVER}}|{{APP_VER}}" "$ROOT" \ + --exclude-dir=vendor --exclude-dir=vendor-bin \ --include='*.html' --include='*.php' --include='*.css' --include='*.js' 2>/dev/null; then echo "Unreplaced placeholders found in staging." >&2 exit 1 fi echo "OK: No unreplaced placeholders." - - name: Zip artifact + - name: Zip artifact (includes vendor/) if: steps.tagcheck.outputs.exists == 'false' shell: bash run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 513cba1..0e8a884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## Changes 11/11/2025 (v1.9.3) + +release(v1.9.3): unify folder icons across tree & strip, add “paper” lines, live color sync, and vendor-aware release + +- UI / Icons + - Replace Material icon in folder strip with shared `folderSVG()` and export it for reuse. Adds clipPaths, subtle gradients, and `shape-rendering: geometricPrecision` to eliminate the tiny seam. + - Add ruled “paper” lines and blue handwriting dashes; CSS for `.paper-line` and `.paper-ink` included. + - Match strokes between tree (24px) and strip (48px) so both look identical; round joins/caps to avoid nicks. + - Polish folder strip layout & hover: tighter spacing, centered icon+label, improved wrapping. + +- Folder color & non-empty detection + - Live color sync: after saving a color we dispatch `folderColorChanged`; strip repaints and tree refreshes. + - Async strip icon: paint immediately, then flip to “paper” if the folder has contents. HSL helpers compute front/back/stroke shades. + +- FileList strip + - Render subfolders with `` + name, wire context menu actions (move, color, share, etc.), and attach icons for each tile. + +- Exports & helpers + - Export `openColorFolderModal(...)` and `openMoveFolderUI(...)` for the strip and toolbar; use `refreshFolderIcon(...)` after ops to keep icons current. + +- AppCore + - Update file upload DnD relay hook to `#fileList` (id rename). + +- CSS tweaks + - Bring tree icon stroke/paint rules in line with the strip, add scribble styles, and adjust margins/spacing. + +- CI/CD (release) + - Build PHP dependencies during release: setup PHP 8.3 + Composer, cache downloads, install into `staging/vendor/`, exclude `vendor/` from placeholder checks, and ship artifact including `vendor/`. + +- Changelog highlights + - Sharper, seam-free folder SVGs shared across tree & strip, with paper lines + handwriting accents. + - Real-time folder color propagation between views. + - Folder strip switched to SVG tiles with better layout + context actions. + - Release pipeline now produces a ready-to-run zip that includes `vendor/`. + +--- + ## Changes 11/10/2025 (v1.9.2) release(v1.9.2): Upload modal + DnD relay from file list (with robust synthetic-drop fallback) diff --git a/public/css/styles.css b/public/css/styles.css index af86133..100afaf 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -739,11 +739,167 @@ body { width: 100% !important; }#fileList table tr:nth-child(even) { background-color: transparent; - }#fileList table tr:hover { - background-color: #e0e0e0; - }.dark-mode #fileList table tr:hover { - background-color: #444; - }#fileListTitle { + } +/* --- File list rows: match folder-tree hover/selected --- */ +:root { + --filr-row-hover-bg: rgba(122,179,255,.14); + --filr-row-selected-bg: rgba(122,179,255,.24); +} + +/* Let cell corners round like a pill */ +#fileList table { + border-collapse: separate; + border-spacing: 0; +} + +/* ===== Reset any conflicting backgrounds (Bootstrap etc.) inside #fileList only ===== */ +#fileList table tbody tr, +#fileList table tbody tr > td { + background-color: transparent !important; +} + +/* Kill Bootstrap hover/zebra just for this table */ +#fileList table.table-hover tbody tr:hover > * { background-color: transparent !important; } +#fileList table.table-striped > tbody > tr:nth-of-type(odd) > * { background-color: transparent !important; } + +/* ===== Our look, scoped to the table we tagged in JS ===== */ +#fileList table.filr-table tbody tr, +#fileList table.filr-table tbody td { + transition: background-color .12s ease; +} + +/* Hover (when not selected) */ +#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td { + background: var(--filr-row-hover-bg) !important; +} + +/* Selected (support a few legacy class names just in case) */ +#fileList table.filr-table tbody tr.selected > td, +#fileList table.filr-table tbody tr.row-selected > td, +#fileList table.filr-table tbody tr.selected-row > td, +#fileList table.filr-table tbody tr.is-selected > td { + background: var(--filr-row-selected-bg) !important; + box-shadow: inset 0 0 0 1px rgba(122,179,255,.45); +} + +/* Rounded “pill” ends on hover/selected */ +#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td:first-child, +#fileList table.filr-table tbody tr.selected > td:first-child, +#fileList table.filr-table tbody tr.row-selected > td:first-child, +#fileList table.filr-table tbody tr.selected-row > td:first-child, +#fileList table.filr-table tbody tr.is-selected > td:first-child { + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; +} +#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td:last-child, +#fileList table.filr-table tbody tr.selected > td:last-child, +#fileList table.filr-table tbody tr.row-selected > td:last-child, +#fileList table.filr-table tbody tr.selected-row > td:last-child, +#fileList table.filr-table tbody tr.is-selected > td:last-child { + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; +} + +/* Keyboard focus visibility */ +#fileList table.filr-table tbody tr:focus-within > td { + outline: 2px solid #8ab4f8; + outline-offset: -2px; + border-top-left-radius: 8px; border-bottom-left-radius: 8px; + border-top-right-radius: 8px; border-bottom-right-radius: 8px; +} + +.dark-mode #fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td { + background: var(--filr-row-hover-bg) !important; +} +.dark-mode #fileList table.filr-table tbody tr.selected > td, +.dark-mode #fileList table.filr-table tbody tr.row-selected > td, +.dark-mode #fileList table.filr-table tbody tr.selected-row > td, +.dark-mode #fileList table.filr-table tbody tr.is-selected > td { + background: var(--filr-row-selected-bg) !important; +} + +#fileList table.filr-table { + --bs-table-hover-color: inherit; + --bs-table-striped-color: inherit; +} + +#fileList table.table-hover tbody tr:hover, +#fileList table.table-hover tbody tr:hover > td { + color: inherit !important; +} + +.dark-mode #fileList table.filr-table tbody td a, +.dark-mode #fileList table.filr-table tbody td a:hover { + color: inherit !important; +} +:root{ + --filr-row-outline: rgba(122,179,255,.45); + --filr-row-outline-hover: rgba(122,179,255,.35); +} + +#fileList table.filr-table > :not(caption) > * > * { border: 0 !important; } +#fileList table.filr-table td, +#fileList table.filr-table th { box-shadow: none !important; } + +#fileList table.filr-table tbody tr.selected > td, +#fileList table.filr-table tbody tr.row-selected > td, +#fileList table.filr-table tbody tr.selected-row > td, +#fileList table.filr-table tbody tr.is-selected > td { + background: var(--filr-row-selected-bg) !important; + box-shadow: + inset 0 1px 0 var(--filr-row-outline), + inset 0 -1px 0 var(--filr-row-outline); +} +#fileList table.filr-table tbody tr.selected > td:first-child, +#fileList table.filr-table tbody tr.row-selected > td:first-child, +#fileList table.filr-table tbody tr.selected-row > td:first-child, +#fileList table.filr-table tbody tr.is-selected > td:first-child { + box-shadow: + inset 1px 0 0 var(--filr-row-outline), + inset 0 1px 0 var(--filr-row-outline), + inset 0 -1px 0 var(--filr-row-outline); + border-top-left-radius: 8px; border-bottom-left-radius: 8px; +} +#fileList table.filr-table tbody tr.selected > td:last-child, +#fileList table.filr-table tbody tr.row-selected > td:last-child, +#fileList table.filr-table tbody tr.selected-row > td:last-child, +#fileList table.filr-table tbody tr.is-selected > td:last-child { + box-shadow: + inset -1px 0 0 var(--filr-row-outline), + inset 0 1px 0 var(--filr-row-outline), + inset 0 -1px 0 var(--filr-row-outline); + border-top-right-radius: 8px; border-bottom-right-radius: 8px; + +} + +#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td { + background: var(--filr-row-hover-bg) !important; + box-shadow: + inset 0 1px 0 var(--filr-row-outline-hover), + inset 0 -1px 0 var(--filr-row-outline-hover); +} +#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td:first-child { + box-shadow: + inset 1px 0 0 var(--filr-row-outline-hover), + inset 0 1px 0 var(--filr-row-outline-hover), + inset 0 -1px 0 var(--filr-row-outline-hover); + border-top-left-radius: 8px; border-bottom-left-radius: 8px; +} +#fileList table.filr-table tbody tr:hover:not(.selected, .row-selected, .selected-row, .is-selected) > td:last-child { + box-shadow: + inset -1px 0 0 var(--filr-row-outline-hover), + inset 0 1px 0 var(--filr-row-outline-hover), + inset 0 -1px 0 var(--filr-row-outline-hover); + border-top-right-radius: 8px; border-bottom-right-radius: 8px; +} + +#fileList table.filr-table tbody tr:focus-within > td { outline: none; } +#fileList table.filr-table tbody tr:focus-within > td:first-child, +#fileList table.filr-table tbody tr:focus-within > td:last-child { + outline: 2px solid #8ab4f8; outline-offset: -2px; +} + + #fileListTitle { white-space: normal !important; word-wrap: break-word !important; overflow-wrap: break-word !important; @@ -1000,7 +1156,7 @@ body { #fileListTitle { font-size: 1.8em; margin-top: 10px; - margin-bottom: 15px; + margin-bottom: 10px; }.file-list-actions { display: flex; flex-wrap: wrap; @@ -1185,7 +1341,7 @@ body { }.folder-tree.expanded { display: block; }.folder-item { - margin: 4px 0; + margin: 2px 0; display: block; }.folder-toggle { cursor: pointer; @@ -1432,8 +1588,6 @@ body { }.dark-mode table { background-color: #2c2c2c; color: #e0e0e0; - }.dark-mode table tr:hover { - background-color: #444; }.dark-mode #uploadProgressContainer .progress { background-color: #333; }.dark-mode #uploadProgressContainer .progress-bar { @@ -1876,34 +2030,74 @@ body { font-weight: 500; vertical-align: middle; white-space: nowrap; - }.folder-strip-container { - display: flex; + } + +.folder-strip-container { + display: flex; + padding-top: 0px !important; flex-wrap: wrap; - gap: 12px; - padding: 8px 0; - }.folder-strip-container .folder-item { - display: flex; + gap: 10px 14px; + align-content: flex-start; /* multi-line wrap stays top-aligned */ + padding: 6px 4px; + } + + .folder-strip-container .folder-item { + display: flex; + padding-top: 0px !important; flex-direction: column; - align-items: center; - cursor: pointer; - width: 80px; + align-items: center; /* horizontal (cross-axis) center */ + justify-content: center; /* vertical (main-axis) center */ + min-width: 0; + gap: 2px !important; + padding: 6px 8px; + box-sizing: border-box; + border-radius: 10px; color: inherit; font-size: 0.85em; - }.folder-strip-container .folder-item i.material-icons { - font-size: 28px; - margin-bottom: 4px; - }.folder-strip-container .folder-name { + transition: transform .12s ease, box-shadow .12s ease, background-color .12s ease; + } +.folder-strip-container .folder-item .folder-svg { + line-height: 0; + margin-bottom: 0px; +} +.folder-strip-container .folder-item .folder-svg svg { + width: 48px; + height: 48px; + display: block; +} +.folder-strip-container .folder-name { + display: block; + width: 100%; + max-width: 100%; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; + hyphens: auto; text-align: center; - white-space: normal; - word-break: break-word; - max-width: 80px; - margin-top: 4px; - }.folder-strip-container .folder-item i.material-icons { - color: currentColor; - }.folder-strip-container .folder-item:hover { - background-color: rgba(255, 255, 255, 0.2); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); - }:root { + overflow: visible; + text-overflow: clip; + line-height: 1.2; +} + +.folder-strip-container .folder-item:hover { + transform: translateY(-1px) scale(1.04); + background-color: rgba(0, 0, 0, 0.04); /* light mode */ + box-shadow: 0 4px 10px rgba(0, 0, 0, .15); +} + +/* Dark mode hover */ +body.dark-mode .folder-strip-container .folder-item:hover { + background-color: rgba(255, 255, 255, 0.06); + box-shadow: 0 6px 16px rgba(0, 0, 0, .45); +} + +/* Optional: keyboard focus */ +.folder-strip-container .folder-item:focus-visible { + outline: 2px solid #8ab4f8; + outline-offset: 2px; +} + + :root { --perm-caret: #444; }/* light */ .dark-mode { @@ -2005,170 +2199,217 @@ body { #downloadProgressBarOuter { height: 10px; } -/* ===== FileRise Folder Tree: unified, crisp, aligned ===== */ - -/* Knobs (size, spacing, colors) */ +/* ===== Folder Tree – theme + structure ===== */ #folderTreeContainer { - /* Colors (used in BOTH themes) */ - --filr-folder-front: #f6b84e; /* front/lip */ - --filr-folder-back: #ffd36e; /* back body */ - --filr-folder-stroke:#a87312; /* outline */ - --filr-paper-fill: #ffffff; /* paper */ - --filr-paper-stroke: #b2c2db; /* paper edges/lines */ - - /* Size & spacing */ - --row-h: 28px; /* row height */ - --twisty: 24px; /* chevron hit-area size */ - --twisty-gap: -5px; /* gap between chevron and row content */ - --icon-size: 24px; /* 22–26 look good */ - --icon-gap: 6px; /* space between icon and label */ - --indent: 10px; /* subtree indent */ -} - -/* Keep the same yellow/orange in dark mode; boost paper contrast a touch */ -.dark-mode #folderTreeContainer { + /* Theme vars (light mode defaults) */ --filr-folder-front: #f6b84e; --filr-folder-back: #ffd36e; --filr-folder-stroke:#a87312; --filr-paper-fill: #ffffff; - --filr-paper-stroke: #d0def7; /* brighter so it pops on dark */ + --filr-paper-stroke: #9fb3d6; /* slightly darker for sharper paper */ + + /* Sizes */ + --row-h: 28px; + --twisty: 24px; + --twisty-gap: -5px; + --icon-size: 24px; + --icon-gap: 6px; + --indent: 10px; } -#folderTreeContainer .folder-item { position: static; padding-left: 0; } -/* visible “row” for each node */ +#folderTreeContainer .folder-item { position: static; padding-left: 0; } +#folderTreeContainer .folder-item > .folder-tree { margin-left: var(--indent); } + #folderTreeContainer .folder-row { position: relative; display: flex; align-items: center; - height: var(--row-h); + min-height: var(--row-h); + height: auto; padding-left: calc(var(--twisty) + var(--twisty-gap)); } -/* children indent */ -#folderTreeContainer .folder-item > .folder-tree { margin-left: var(--indent); } - -/* ---------- Chevron toggle (twisty) ---------- */ - -#folderTreeContainer .folder-row > button.folder-toggle { +/* Chevron + spacer (centered vertically) */ +#folderTreeContainer .folder-row > button.folder-toggle, +#folderTreeContainer .folder-row > .folder-spacer { position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: var(--twisty); height: var(--twisty); display: inline-flex; align-items: center; justify-content: center; border: 1px solid transparent; border-radius: 6px; background: transparent; cursor: pointer; } - #folderTreeContainer .folder-row > button.folder-toggle::before { - content: "▸"; /* closed */ - font-size: calc(var(--twisty) * 0.8); - line-height: 1; + content: "▸"; font-size: calc(var(--twisty) * 0.8); line-height: 1; } - #folderTreeContainer li[role="treeitem"][aria-expanded="true"] - > .folder-row > button.folder-toggle::before { content: "▾"; } - -/* root row (it's a
) */ + > .folder-row > button.folder-toggle::before, #rootRow[aria-expanded="true"] > button.folder-toggle::before { content: "▾"; } - #folderTreeContainer .folder-row > button.folder-toggle:hover { border-color: color-mix(in srgb, #7ab3ff 35%, transparent); } -/* spacer for leaves so labels align with parents that have a button */ -#folderTreeContainer .folder-row > .folder-spacer { - position: absolute; left: 0; top: 50%; transform: translateY(-50%); - width: var(--twisty); height: var(--twisty); display: inline-block; -} - +/* Row "pill" that hugs content and wraps */ #folderTreeContainer .folder-option { display: inline-flex; + flex: 0 1 auto; /* don't stretch full width */ align-items: center; - height: var(--row-h); - line-height: 1.2; /* avoids baseline weirdness */ + gap: var(--icon-gap); + height: auto; + min-height: var(--row-h); padding: 0 8px; border-radius: 8px; + line-height: 1.2; user-select: none; - white-space: nowrap; max-width: 100%; - gap: var(--icon-gap); + white-space: normal; /* allow wrapping */ } - -#folderTreeContainer .folder-label { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transform: translateY(0.5px); /* tiny optical nudge for text */ -} - -/* ---------- Icon box (size & alignment) ---------- */ - -#folderTreeContainer .folder-icon { - flex: 0 0 var(--icon-size); - width: var(--icon-size); - height: var(--icon-size); - display: inline-flex; - align-items: center; - justify-content: center; - transform: translateY(0.5px); /* tiny optical nudge for SVG */ -} - -#folderTreeContainer .folder-icon svg { - width: 100%; - height: 100%; - display: block; - shape-rendering: geometricPrecision; -} - -/* ---------- Crisp colors & strokes for the SVG parts ---------- */ - - -#folderTreeContainer .folder-icon .paper { - fill: var(--filr-paper-fill); - stroke: var(--filr-paper-stroke); - stroke-width: 1.5; /* thick so it reads at 24px */ - paint-order: stroke fill; -} - -#folderTreeContainer .folder-icon .paper-fold { - fill: var(--filr-paper-stroke); -} - -#folderTreeContainer .folder-icon .paper-line { - stroke: var(--filr-paper-stroke); - stroke-width: 1.5; - stroke-linecap: round; - fill: none; - opacity: 0.95; -} - -/* subtle highlight along lip to add depth */ -#folderTreeContainer .folder-icon .lip-highlight { - stroke: #ffffff; - stroke-opacity: .35; - stroke-width: 0.9; - fill: none; - vector-effect: non-scaling-stroke; -} - -/* ---------- Hover / Selected ---------- */ - #folderTreeContainer .folder-option:hover { background: rgba(122,179,255,.14); } - #folderTreeContainer .folder-option.selected { background: rgba(122,179,255,.24); box-shadow: inset 0 0 0 1px rgba(122,179,255,.45); } -/* variables will be set inline per .folder-option when user colors a folder */ +/* Label must be shrinkable so wrapping works */ +#folderTreeContainer .folder-label { + flex: 1 1 120px; + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; + line-height: 1.2; +} + +/* Icon box */ +#folderTreeContainer .folder-icon { + flex: 0 0 var(--icon-size); + width: var(--icon-size); height: var(--icon-size); + display: inline-flex; align-items: center; justify-content: center; +} +#folderTreeContainer .folder-icon svg { + width: 100%; height: 100%; display: block; + shape-rendering: geometricPrecision; +} + + +/* Make folder tree outline match folder strip */ #folderTreeContainer .folder-icon .folder-front, #folderTreeContainer .folder-icon .folder-back { fill: currentColor; stroke: var(--filr-folder-stroke); - stroke-width: 1.1; + stroke-width: .5; + paint-order: fill stroke; + stroke-linejoin: round; + stroke-linecap: round; vector-effect: non-scaling-stroke; - paint-order: stroke fill; } #folderTreeContainer .folder-icon .folder-front { color: var(--filr-folder-front); } #folderTreeContainer .folder-icon .folder-back { color: var(--filr-folder-back); } + +#folderTreeContainer .folder-icon .paper { + fill: var(--filr-paper-fill); + stroke: var(--filr-paper-stroke); + stroke-width: 1.5; + paint-order: stroke fill; +} +#folderTreeContainer .folder-icon .paper-fold { fill: var(--filr-paper-stroke); } +#folderTreeContainer .folder-icon .paper-line { + stroke: var(--filr-paper-stroke); stroke-width: 1.5; + stroke-linecap: round; fill: none; opacity: .95; +} +#folderTreeContainer .folder-icon .lip-highlight { + stroke: #ffffff; stroke-opacity: .35; stroke-width: .9; + fill: none; vector-effect: non-scaling-stroke; +} + +#folderTreeContainer .folder-icon, +#folderTreeContainer .folder-label { transform: none !important; } + +/* ===== File List Strip – color the shared folderSVG() ===== */ +.folder-strip-container .folder-svg svg { + display: block; + shape-rendering: crispEdges; +} +.folder-strip-container .folder-item { + /* defaults — overridden per-tile via inline CSS vars set in JS */ + --filr-folder-front: #f6b84e; + --filr-folder-back: #ffd36e; + --filr-folder-stroke: #a87312; +} +.folder-strip-container .folder-svg .folder-front, +.folder-strip-container .folder-svg .folder-back { + fill: currentColor; + stroke: var(--filr-folder-stroke); + stroke-width: .5; + paint-order: fill stroke; + stroke-linejoin: round; + stroke-linecap: round; +} + +.folder-strip-container .folder-svg .folder-front { color: var(--filr-folder-front); } +.folder-strip-container .folder-svg .folder-back { color: var(--filr-folder-back); } + +.folder-strip-container .folder-svg .paper { + fill: #fff; + stroke: #b2c2db; /* light mode */ + stroke-width: 1; + vector-effect: non-scaling-stroke; +} +.folder-strip-container .folder-svg .paper-fold { fill: #b2c2db; } +.folder-strip-container .folder-svg .paper-line { + stroke: #b2c2db; stroke-width: 1; stroke-linecap: round; fill: none; + vector-effect: non-scaling-stroke; +} +.folder-strip-container .folder-svg .lip-highlight { + stroke: rgba(255,255,255,.45); stroke-width: .8; fill: none; + vector-effect: non-scaling-stroke; +} + +#folderTreeContainer .folder-icon .folder-front, +#folderTreeContainer .folder-icon .folder-back, +.folder-strip-container .folder-svg .folder-front, +.folder-strip-container .folder-svg .folder-back, +#folderTreeContainer .folder-icon .lip-highlight, +.folder-strip-container .folder-svg .lip-highlight { + stroke-linejoin: round; + stroke-linecap: round; +} + +/* Make sure we’re not forcing crispEdges anywhere */ +.folder-strip-container .folder-svg svg, +#folderTreeContainer .folder-icon svg { shape-rendering: geometricPrecision !important; } + +@media (max-resolution: 1.5dppx) { + #folderTreeContainer .folder-icon .folder-front, + #folderTreeContainer .folder-icon .folder-back { stroke-width: .6; } +} + + +/* Scribble (the handwriting line) */ +#folderTreeContainer .folder-icon .paper-ink, +.folder-strip-container .folder-svg .paper-ink { + stroke: #4da3ff; + stroke-width: .9; + stroke-linecap: round; + stroke-linejoin: round; + fill: none; + opacity: .85; + paint-order: normal; + +} + +/* tree @ 24px icon */ +#folderTreeContainer .folder-icon .folder-front, +#folderTreeContainer .folder-icon .folder-back, +#folderTreeContainer .folder-icon .paper-line, +#folderTreeContainer .folder-icon .paper-ink, +#folderTreeContainer .folder-icon .lip-highlight { stroke-width: .6px; } + +/* strip @ 48px icon (2× bigger), halve stroke width to look the same */ +.folder-strip-container .folder-svg .folder-front, +.folder-strip-container .folder-svg .folder-back, +.folder-strip-container .folder-svg .paper-line, +.folder-strip-container .folder-svg .paper-ink, +.folder-strip-container .folder-svg .lip-highlight { stroke-width: 1.1px; } diff --git a/public/js/appCore.js b/public/js/appCore.js index 744fc6d..905b1c3 100644 --- a/public/js/appCore.js +++ b/public/js/appCore.js @@ -100,7 +100,7 @@ export function initializeApp() { // Hook DnD relay from fileList area into upload area - const fileListArea = document.getElementById('fileListContainer'); + const fileListArea = document.getElementById('fileList'); if (fileListArea) { let hoverTimer = null; diff --git a/public/js/domUtils.js b/public/js/domUtils.js index 4636655..700856f 100644 --- a/public/js/domUtils.js +++ b/public/js/domUtils.js @@ -156,7 +156,7 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear export function buildFileTableHeader(sortOrder) { return ` - +
@@ -283,9 +283,9 @@ export function updateRowHighlight(checkbox) { const row = checkbox.closest('tr'); if (!row) return; if (checkbox.checked) { - row.classList.add('row-selected'); + row.classList.add('row-selected', 'selected'); } else { - row.classList.remove('row-selected'); + row.classList.remove('row-selected', 'selected'); } } diff --git a/public/js/fileListView.js b/public/js/fileListView.js index 123198e..9b0cc87 100644 --- a/public/js/fileListView.js +++ b/public/js/fileListView.js @@ -1,43 +1,47 @@ // fileListView.js import { - escapeHTML, - debounce, - buildSearchAndPaginationControls, - buildFileTableHeader, - buildFileTableRow, - buildBottomControls, - updateFileActionButtons, - showToast, - updateRowHighlight, - toggleRowSelection, - attachEnterKeyListener - } from './domUtils.js?v={{APP_QVER}}'; - import { t } from './i18n.js?v={{APP_QVER}}'; - import { bindFileListContextMenu } from './fileMenu.js?v={{APP_QVER}}'; - import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}'; - import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}'; - import { - getParentFolder, - updateBreadcrumbTitle, - setupBreadcrumbDelegation, - showFolderManagerContextMenu, - hideFolderManagerContextMenu, - openRenameFolderModal, - openDeleteFolderModal - } from './folderManager.js?v={{APP_QVER}}'; - import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}'; - import { - folderDragOverHandler, - folderDragLeaveHandler, - folderDropHandler - } from './fileDragDrop.js?v={{APP_QVER}}'; - - export let fileData = []; - export let sortOrder = { column: "uploaded", ascending: true }; + escapeHTML, + debounce, + buildSearchAndPaginationControls, + buildFileTableHeader, + buildFileTableRow, + buildBottomControls, + updateFileActionButtons, + showToast, + updateRowHighlight, + toggleRowSelection, + attachEnterKeyListener +} from './domUtils.js?v={{APP_QVER}}'; +import { t } from './i18n.js?v={{APP_QVER}}'; +import { bindFileListContextMenu } from './fileMenu.js?v={{APP_QVER}}'; +import { openDownloadModal } from './fileActions.js?v={{APP_QVER}}'; +import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}'; +import { + getParentFolder, + updateBreadcrumbTitle, + setupBreadcrumbDelegation, + showFolderManagerContextMenu, + hideFolderManagerContextMenu, + openRenameFolderModal, + openDeleteFolderModal, + refreshFolderIcon, + openColorFolderModal, + openMoveFolderUI, + folderSVG +} from './folderManager.js?v={{APP_QVER}}'; +import { openFolderShareModal } from './folderShareModal.js?v={{APP_QVER}}'; +import { + folderDragOverHandler, + folderDragLeaveHandler, + folderDropHandler +} from './fileDragDrop.js?v={{APP_QVER}}'; + +export let fileData = []; +export let sortOrder = { column: "uploaded", ascending: true }; - // onnlyoffice +// onnlyoffice let OO_ENABLED = false; let OO_EXTS = new Set(); @@ -53,38 +57,70 @@ export async function initOnlyOfficeCaps() { OO_EXTS = new Set(); } } - - // Hide "Edit" for files >10 MiB - const MAX_EDIT_BYTES = 10 * 1024 * 1024; - - // Latest-response-wins guard (prevents double render/flicker if loadFileList gets called twice) - let __fileListReqSeq = 0; - - window.itemsPerPage = parseInt( - localStorage.getItem('itemsPerPage') || window.itemsPerPage || '10', - 10 - ); - window.currentPage = window.currentPage || 1; - window.viewMode = localStorage.getItem("viewMode") || "table"; - - // Global flag for advanced search mode. - window.advancedSearchEnabled = false; - - /* =========================================================== - SECURITY: build file URLs only via the API (no /uploads) - =========================================================== */ - function apiFileUrl(folder, name, inline = false) { - const f = folder && folder !== "root" ? folder : "root"; - const q = new URLSearchParams({ - folder: f, - file: name, - inline: inline ? "1" : "0", - t: String(Date.now()) // cache-bust - }); - return `/api/file/download.php?${q.toString()}`; - } - - // Wire "select all" header checkbox for the current table render + + +// helper to repaint one strip item quickly +function repaintStripIcon(folder) { + const el = document.querySelector(`#folderStripContainer .folder-item[data-folder="${CSS.escape(folder)}"]`); + if (!el) return; + const iconSpan = el.querySelector('.folder-svg'); + if (!iconSpan) return; + + const hex = (window.folderColorMap && window.folderColorMap[folder]) || '#f6b84e'; + const front = hex; + const back = _lighten(hex, 14); + const stroke = _darken(hex, 22); + el.style.setProperty('--filr-folder-front', front); + el.style.setProperty('--filr-folder-back', back); + el.style.setProperty('--filr-folder-stroke', stroke); + + const kind = iconSpan.dataset.kind || 'empty'; + iconSpan.innerHTML = folderSVG(kind); +} + +// Listen once: update strip + tree when folder color changes +window.addEventListener('folderColorChanged', (e) => { + const { folder } = e.detail || {}; + if (!folder) return; + + // Update the strip (if that folder is currently shown) + repaintStripIcon(folder); + + // And refresh the tree icon too (existing function) + try { refreshFolderIcon(folder); } catch { } +}); + +// Hide "Edit" for files >10 MiB +const MAX_EDIT_BYTES = 10 * 1024 * 1024; + +// Latest-response-wins guard (prevents double render/flicker if loadFileList gets called twice) +let __fileListReqSeq = 0; + +window.itemsPerPage = parseInt( + localStorage.getItem('itemsPerPage') || window.itemsPerPage || '10', + 10 +); +window.currentPage = window.currentPage || 1; +window.viewMode = localStorage.getItem("viewMode") || "table"; + +// Global flag for advanced search mode. +window.advancedSearchEnabled = false; + +/* =========================================================== + SECURITY: build file URLs only via the API (no /uploads) + =========================================================== */ +function apiFileUrl(folder, name, inline = false) { + const f = folder && folder !== "root" ? folder : "root"; + const q = new URLSearchParams({ + folder: f, + file: name, + inline: inline ? "1" : "0", + t: String(Date.now()) // cache-bust + }); + return `/api/file/download.php?${q.toString()}`; +} + +// Wire "select all" header checkbox for the current table render function wireSelectAll(fileListContent) { // Be flexible about how the header checkbox is identified const selectAll = fileListContent.querySelector( @@ -137,27 +173,98 @@ function wireSelectAll(fileListContent) { syncHeader(); } - /* ----------------------------- - Helper: robust JSON handling - ----------------------------- */ - // Parse JSON if possible; throw on non-2xx with useful message & status - async function safeJson(res) { - const text = await res.text(); - let body = null; - try { body = text ? JSON.parse(text) : null; } catch { /* ignore */ } - - if (!res.ok) { - const msg = - (body && (body.error || body.message)) || - (text && text.trim()) || - `HTTP ${res.status}`; - const err = new Error(msg); - err.status = res.status; - throw err; +// ---- Folder-strip icon helpers (same geometry as tree, but colored inline) ---- +function _hexToHsl(hex) { + hex = String(hex || '').replace('#', ''); + if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); + const r = parseInt(hex.slice(0, 2), 16) / 255; + const g = parseInt(hex.slice(2, 4), 16) / 255; + const b = parseInt(hex.slice(4, 6), 16) / 255; + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + if (max === min) { h = s = 0; } + else { + const d = max - min; + s = l > .5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + default: h = (r - g) / d + 4; } - return body ?? {}; + h /= 6; } - // ---- Viewed badges (table + gallery) ---- + return { h: h * 360, s: s * 100, l: l * 100 }; +} +function _hslToHex(h, s, l) { + h /= 360; s /= 100; l /= 100; + const f = n => { + const k = (n + h * 12) % 12, a = s * Math.min(l, 1 - l); + const c = l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1))); + return Math.round(255 * c).toString(16).padStart(2, '0'); + }; + return '#' + f(0) + f(8) + f(4); +} +function _lighten(hex, amt = 14) { const { h, s, l } = _hexToHsl(hex); return _hslToHex(h, s, Math.min(100, l + amt)); } +function _darken(hex, amt = 22) { const { h, s, l } = _hexToHsl(hex); return _hslToHex(h, s, Math.max(0, l - amt)); } + + +// tiny fetch helper with timeout for folder counts +function _fetchJSONWithTimeout(url, ms = 2500) { + const ctrl = new AbortController(); + const tid = setTimeout(() => ctrl.abort(), ms); + return fetch(url, { credentials: 'include', signal: ctrl.signal }) + .then(r => r.ok ? r.json() : { folders: 0, files: 0 }) + .catch(() => ({ folders: 0, files: 0 })) + .finally(() => clearTimeout(tid)); +} + +// Paint initial icon, then flip to "paper" if non-empty +function attachStripIconAsync(hostEl, fullPath, size = 28) { + const hex = (window.folderColorMap && window.folderColorMap[fullPath]) || '#f6b84e'; + const front = hex; + 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); + + const iconSpan = hostEl.querySelector('.folder-svg'); + if (!iconSpan) return; + + iconSpan.dataset.kind = 'empty'; + iconSpan.innerHTML = folderSVG('empty'); // size is baked into viewBox; add a size arg if you prefer + 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(() => { }); +} + +/* ----------------------------- + Helper: robust JSON handling + ----------------------------- */ +// Parse JSON if possible; throw on non-2xx with useful message & status +async function safeJson(res) { + const text = await res.text(); + let body = null; + try { body = text ? JSON.parse(text) : null; } catch { /* ignore */ } + + if (!res.ok) { + const msg = + (body && (body.error || body.message)) || + (text && text.trim()) || + `HTTP ${res.status}`; + const err = new Error(msg); + err.status = res.status; + throw err; + } + return body ?? {}; +} +// ---- Viewed badges (table + gallery) ---- // ---------- Badge factory (center text vertically) ---------- function makeBadge(state) { if (!state) return null; @@ -182,8 +289,8 @@ function makeBadge(state) { el.classList.add('watched'); el.textContent = (t('watched') || t('viewed') || 'Watched'); el.style.borderColor = 'rgba(34,197,94,.45)'; - el.style.background = 'rgba(34,197,94,.15)'; - el.style.color = '#22c55e'; + el.style.background = 'rgba(34,197,94,.15)'; + el.style.color = '#22c55e'; return el; } @@ -192,8 +299,8 @@ function makeBadge(state) { el.classList.add('progress'); el.textContent = `${pct}%`; el.style.borderColor = 'rgba(234,88,12,.55)'; - el.style.background = 'rgba(234,88,12,.18)'; - el.style.color = '#ea580c'; + el.style.background = 'rgba(234,88,12,.18)'; + el.style.color = '#ea580c'; return el; } @@ -272,73 +379,73 @@ export async function refreshViewedBadges(folder) { if (badge) title.appendChild(badge); }); } - /** - * Convert a file size string (e.g. "456.9KB", "1.2 MB", "1024") into bytes. - */ - function parseSizeToBytes(sizeStr) { - if (!sizeStr) return 0; - let s = sizeStr.trim(); - let value = parseFloat(s); - let upper = s.toUpperCase(); - if (upper.includes("KB")) { - value *= 1024; - } else if (upper.includes("MB")) { - value *= 1024 * 1024; - } else if (upper.includes("GB")) { - value *= 1024 * 1024 * 1024; - } - return value; +/** + * Convert a file size string (e.g. "456.9KB", "1.2 MB", "1024") into bytes. + */ +function parseSizeToBytes(sizeStr) { + if (!sizeStr) return 0; + let s = sizeStr.trim(); + let value = parseFloat(s); + let upper = s.toUpperCase(); + if (upper.includes("KB")) { + value *= 1024; + } else if (upper.includes("MB")) { + value *= 1024 * 1024; + } else if (upper.includes("GB")) { + value *= 1024 * 1024 * 1024; } - - /** - * Format the total bytes as a human-readable string. - */ - function formatSize(totalBytes) { - if (totalBytes < 1024) { - return totalBytes + " Bytes"; - } else if (totalBytes < 1024 * 1024) { - return (totalBytes / 1024).toFixed(2) + " KB"; - } else if (totalBytes < 1024 * 1024 * 1024) { - return (totalBytes / (1024 * 1024)).toFixed(2) + " MB"; - } else { - return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"; - } + return value; +} + +/** + * Format the total bytes as a human-readable string. + */ +function formatSize(totalBytes) { + if (totalBytes < 1024) { + return totalBytes + " Bytes"; + } else if (totalBytes < 1024 * 1024) { + return (totalBytes / 1024).toFixed(2) + " KB"; + } else if (totalBytes < 1024 * 1024 * 1024) { + return (totalBytes / (1024 * 1024)).toFixed(2) + " MB"; + } else { + return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"; } - - /** - * Build the folder summary HTML using the filtered file list. - */ - function buildFolderSummary(filteredFiles) { - const totalFiles = filteredFiles.length; - const totalBytes = filteredFiles.reduce((sum, file) => { - return sum + parseSizeToBytes(file.size); - }, 0); - const sizeStr = formatSize(totalBytes); - return `${t('total_files')}: ${totalFiles}  |  ${t('total_size')}: ${sizeStr}`; +} + +/** + * Build the folder summary HTML using the filtered file list. + */ +function buildFolderSummary(filteredFiles) { + const totalFiles = filteredFiles.length; + const totalBytes = filteredFiles.reduce((sum, file) => { + return sum + parseSizeToBytes(file.size); + }, 0); + const sizeStr = formatSize(totalBytes); + return `${t('total_files')}: ${totalFiles}  |  ${t('total_size')}: ${sizeStr}`; +} + +/** + * Advanced Search toggle + */ +function toggleAdvancedSearch() { + window.advancedSearchEnabled = !window.advancedSearchEnabled; + const advancedBtn = document.getElementById("advancedSearchToggle"); + if (advancedBtn) { + advancedBtn.textContent = window.advancedSearchEnabled ? "Basic Search" : "Advanced Search"; } - - /** - * Advanced Search toggle - */ - function toggleAdvancedSearch() { - window.advancedSearchEnabled = !window.advancedSearchEnabled; - const advancedBtn = document.getElementById("advancedSearchToggle"); - if (advancedBtn) { - advancedBtn.textContent = window.advancedSearchEnabled ? "Basic Search" : "Advanced Search"; - } - renderFileTable(window.currentFolder); - } - - window.imageCache = window.imageCache || {}; - function cacheImage(imgElem, key) { - window.imageCache[key] = imgElem.src; - } - window.cacheImage = cacheImage; - - /** - * Fuse.js fuzzy search helper - */ - // --- Lazy Fuse loader (drop-in, CSP-safe, no inline) --- + renderFileTable(window.currentFolder); +} + +window.imageCache = window.imageCache || {}; +function cacheImage(imgElem, key) { + window.imageCache[key] = imgElem.src; +} +window.cacheImage = cacheImage; + +/** + * Fuse.js fuzzy search helper + */ +// --- Lazy Fuse loader (drop-in, CSP-safe, no inline) --- const FUSE_SRC = '/vendor/fuse/6.6.2/fuse.min.js?v={{APP_QVER}}'; let _fuseLoadingPromise = null; @@ -371,7 +478,7 @@ function lazyLoadFuse() { // warmUpSearch(); // This just starts fetching Fuse in the background. export function warmUpSearch() { - lazyLoadFuse().catch(() => {/* ignore; we’ll fall back */}); + lazyLoadFuse().catch(() => {/* ignore; we’ll fall back */ }); } // Lazy + backward-compatible search @@ -383,8 +490,8 @@ function searchFiles(searchTerm) { // keys config (matches your original) const fuseKeys = [ - { name: 'name', weight: 0.1 }, - { name: 'uploader', weight: 0.1 }, + { name: 'name', weight: 0.1 }, + { name: 'uploader', weight: 0.1 }, { name: 'tags.name', weight: 0.1 } ]; if (window.advancedSearchEnabled) { @@ -416,200 +523,200 @@ function searchFiles(searchTerm) { return false; }); } - - /** - * View mode toggle - */ - export function createViewToggleButton() { - let toggleBtn = document.getElementById("toggleViewBtn"); - if (!toggleBtn) { - toggleBtn = document.createElement("button"); - toggleBtn.id = "toggleViewBtn"; - toggleBtn.classList.add("btn", "btn-toggleview"); - - if (window.viewMode === "gallery") { - toggleBtn.innerHTML = 'view_list'; - toggleBtn.title = t("switch_to_table_view"); - } else { - toggleBtn.innerHTML = 'view_module'; - toggleBtn.title = t("switch_to_gallery_view"); - } - - const headerButtons = document.querySelector(".header-buttons"); - if (headerButtons && headerButtons.lastElementChild) { - headerButtons.insertBefore(toggleBtn, headerButtons.lastElementChild); - } else if (headerButtons) { - headerButtons.appendChild(toggleBtn); - } + +/** + * View mode toggle + */ +export function createViewToggleButton() { + let toggleBtn = document.getElementById("toggleViewBtn"); + if (!toggleBtn) { + toggleBtn = document.createElement("button"); + toggleBtn.id = "toggleViewBtn"; + toggleBtn.classList.add("btn", "btn-toggleview"); + + if (window.viewMode === "gallery") { + toggleBtn.innerHTML = 'view_list'; + toggleBtn.title = t("switch_to_table_view"); + } else { + toggleBtn.innerHTML = 'view_module'; + toggleBtn.title = t("switch_to_gallery_view"); + } + + const headerButtons = document.querySelector(".header-buttons"); + if (headerButtons && headerButtons.lastElementChild) { + headerButtons.insertBefore(toggleBtn, headerButtons.lastElementChild); + } else if (headerButtons) { + headerButtons.appendChild(toggleBtn); } - - toggleBtn.onclick = () => { - window.viewMode = window.viewMode === "gallery" ? "table" : "gallery"; - localStorage.setItem("viewMode", window.viewMode); - loadFileList(window.currentFolder); - if (window.viewMode === "gallery") { - toggleBtn.innerHTML = 'view_list'; - toggleBtn.title = t("switch_to_table_view"); - } else { - toggleBtn.innerHTML = 'view_module'; - toggleBtn.title = t("switch_to_gallery_view"); - } - }; - - return toggleBtn; } - - export function formatFolderName(folder) { - if (folder === "root") return "(Root)"; - return folder - .replace(/[_-]+/g, " ") - .replace(/\b\w/g, char => char.toUpperCase()); - } - - // Expose inline DOM helpers. - window.toggleRowSelection = toggleRowSelection; - window.updateRowHighlight = updateRowHighlight; - - export async function loadFileList(folderParam) { - await initOnlyOfficeCaps(); - const reqId = ++__fileListReqSeq; // latest call wins - const folder = folderParam || "root"; - const fileListContainer = document.getElementById("fileList"); - const actionsContainer = document.getElementById("fileListActions"); - - // 1) show loader (only this request is allowed to render) - fileListContainer.style.visibility = "hidden"; - fileListContainer.innerHTML = "
Loading files...
"; - - try { - // Kick off both in parallel, but render as soon as FILES are ready - const filesPromise = fetch( - `/api/file/getFileList.php?folder=${encodeURIComponent(folder)}&recursive=1&t=${Date.now()}`, - { credentials: 'include' } - ); - const foldersPromise = fetch( - `/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}`, - { credentials: 'include' } - ); - - // ----- FILES FIRST ----- - const filesRes = await filesPromise; - - if (filesRes.status === 401) { - // session expired — bounce to logout - window.location.href = "/api/auth/logout.php"; - throw new Error("Unauthorized"); - } - if (filesRes.status === 403) { - // forbidden — friendly message, keep UI responsive - fileListContainer.innerHTML = ` + + toggleBtn.onclick = () => { + window.viewMode = window.viewMode === "gallery" ? "table" : "gallery"; + localStorage.setItem("viewMode", window.viewMode); + loadFileList(window.currentFolder); + if (window.viewMode === "gallery") { + toggleBtn.innerHTML = 'view_list'; + toggleBtn.title = t("switch_to_table_view"); + } else { + toggleBtn.innerHTML = 'view_module'; + toggleBtn.title = t("switch_to_gallery_view"); + } + }; + + return toggleBtn; +} + +export function formatFolderName(folder) { + if (folder === "root") return "(Root)"; + return folder + .replace(/[_-]+/g, " ") + .replace(/\b\w/g, char => char.toUpperCase()); +} + +// Expose inline DOM helpers. +window.toggleRowSelection = toggleRowSelection; +window.updateRowHighlight = updateRowHighlight; + +export async function loadFileList(folderParam) { + await initOnlyOfficeCaps(); + const reqId = ++__fileListReqSeq; // latest call wins + const folder = folderParam || "root"; + const fileListContainer = document.getElementById("fileList"); + const actionsContainer = document.getElementById("fileListActions"); + + // 1) show loader (only this request is allowed to render) + fileListContainer.style.visibility = "hidden"; + fileListContainer.innerHTML = "
Loading files...
"; + + try { + // Kick off both in parallel, but render as soon as FILES are ready + const filesPromise = fetch( + `/api/file/getFileList.php?folder=${encodeURIComponent(folder)}&recursive=1&t=${Date.now()}`, + { credentials: 'include' } + ); + const foldersPromise = fetch( + `/api/folder/getFolderList.php?folder=${encodeURIComponent(folder)}`, + { credentials: 'include' } + ); + + // ----- FILES FIRST ----- + const filesRes = await filesPromise; + + if (filesRes.status === 401) { + // session expired — bounce to logout + window.location.href = "/api/auth/logout.php"; + throw new Error("Unauthorized"); + } + if (filesRes.status === 403) { + // forbidden — friendly message, keep UI responsive + fileListContainer.innerHTML = `
${t("no_access_to_resource") || "You don't have access to this folder."}
`; - showToast(t("no_access_to_resource") || "You don't have access to this folder.", "error"); - return []; - } - - const data = await safeJson(filesRes); - if (data.error) { - throw new Error(typeof data.error === 'string' ? data.error : 'Server returned an error.'); - } - - // If another loadFileList ran after this one, bail before touching the DOM + showToast(t("no_access_to_resource") || "You don't have access to this folder.", "error"); + return []; + } + + const data = await safeJson(filesRes); + if (data.error) { + throw new Error(typeof data.error === 'string' ? data.error : 'Server returned an error.'); + } + + // If another loadFileList ran after this one, bail before touching the DOM + if (reqId !== __fileListReqSeq) return []; + + // 3) clear loader + fileListContainer.innerHTML = ""; + + // 4) handle “no files” case + if (!data.files || Object.keys(data.files).length === 0) { if (reqId !== __fileListReqSeq) return []; - - // 3) clear loader - fileListContainer.innerHTML = ""; - - // 4) handle “no files” case - if (!data.files || Object.keys(data.files).length === 0) { - if (reqId !== __fileListReqSeq) return []; - fileListContainer.innerHTML = ` + fileListContainer.innerHTML = `
${t("no_files_found")}
${t("no_folder_access_yet") || "No folder access has been assigned to your account yet."}
`; - - const summaryElem = document.getElementById("fileSummary"); - if (summaryElem) summaryElem.style.display = "none"; - const sliderContainer = document.getElementById("viewSliderContainer"); - if (sliderContainer) sliderContainer.style.display = "none"; - - const strip = document.getElementById("folderStripContainer"); - if (strip) strip.style.display = "none"; - - updateFileActionButtons(); - fileListContainer.style.visibility = "visible"; - // We still try to populate the folder strip below - } - - // 5) normalize files array - if (!Array.isArray(data.files)) { - data.files = Object.entries(data.files).map(([name, meta]) => { - meta.name = name; - return meta; - }); - } - - data.files = data.files.map(f => { - f.fullName = (f.path || f.name).trim().toLowerCase(); - - // Prefer numeric size if API provides it; otherwise parse the "1.2 MB" string - let bytes = Number.isFinite(f.sizeBytes) - ? f.sizeBytes - : parseSizeToBytes(String(f.size || "")); - - if (!Number.isFinite(bytes)) bytes = Infinity; - - f.editable = canEditFile(f.name) && (bytes <= MAX_EDIT_BYTES); - f.folder = folder; - return f; + + const summaryElem = document.getElementById("fileSummary"); + if (summaryElem) summaryElem.style.display = "none"; + const sliderContainer = document.getElementById("viewSliderContainer"); + if (sliderContainer) sliderContainer.style.display = "none"; + + const strip = document.getElementById("folderStripContainer"); + if (strip) strip.style.display = "none"; + + updateFileActionButtons(); + fileListContainer.style.visibility = "visible"; + // We still try to populate the folder strip below + } + + // 5) normalize files array + if (!Array.isArray(data.files)) { + data.files = Object.entries(data.files).map(([name, meta]) => { + meta.name = name; + return meta; }); - fileData = data.files; - - if (reqId !== __fileListReqSeq) return []; - - // 6) inject summary + slider - if (actionsContainer) { - // a) summary - let summaryElem = document.getElementById("fileSummary"); - if (!summaryElem) { - summaryElem = document.createElement("div"); - summaryElem.id = "fileSummary"; - summaryElem.style.cssText = "float:right; margin:0 60px 0 auto; font-size:0.9em;"; - actionsContainer.appendChild(summaryElem); - } - summaryElem.style.display = "block"; - summaryElem.innerHTML = buildFolderSummary(fileData); - - // b) slider - const viewMode = window.viewMode || "table"; - let sliderContainer = document.getElementById("viewSliderContainer"); - if (!sliderContainer) { - sliderContainer = document.createElement("div"); - sliderContainer.id = "viewSliderContainer"; - sliderContainer.style.cssText = "display:inline-flex; align-items:center; margin-right:auto; font-size:0.9em;"; - actionsContainer.insertBefore(sliderContainer, summaryElem); - } else { - sliderContainer.style.display = "inline-flex"; - } - - if (viewMode === "gallery") { - const w = window.innerWidth; - let maxCols; - if (w < 600) maxCols = 1; - else if (w < 900) maxCols = 2; - else if (w < 1200) maxCols = 4; - else maxCols = 6; - - const currentCols = Math.min( - parseInt(localStorage.getItem("galleryColumns") || "3", 10), - maxCols - ); - - sliderContainer.innerHTML = ` + } + + data.files = data.files.map(f => { + f.fullName = (f.path || f.name).trim().toLowerCase(); + + // Prefer numeric size if API provides it; otherwise parse the "1.2 MB" string + let bytes = Number.isFinite(f.sizeBytes) + ? f.sizeBytes + : parseSizeToBytes(String(f.size || "")); + + if (!Number.isFinite(bytes)) bytes = Infinity; + + f.editable = canEditFile(f.name) && (bytes <= MAX_EDIT_BYTES); + f.folder = folder; + return f; + }); + fileData = data.files; + + if (reqId !== __fileListReqSeq) return []; + + // 6) inject summary + slider + if (actionsContainer) { + // a) summary + let summaryElem = document.getElementById("fileSummary"); + if (!summaryElem) { + summaryElem = document.createElement("div"); + summaryElem.id = "fileSummary"; + summaryElem.style.cssText = "float:right; margin:0 60px 0 auto; font-size:0.9em;"; + actionsContainer.appendChild(summaryElem); + } + summaryElem.style.display = "block"; + summaryElem.innerHTML = buildFolderSummary(fileData); + + // b) slider + const viewMode = window.viewMode || "table"; + let sliderContainer = document.getElementById("viewSliderContainer"); + if (!sliderContainer) { + sliderContainer = document.createElement("div"); + sliderContainer.id = "viewSliderContainer"; + sliderContainer.style.cssText = "display:inline-flex; align-items:center; margin-right:auto; font-size:0.9em;"; + actionsContainer.insertBefore(sliderContainer, summaryElem); + } else { + sliderContainer.style.display = "inline-flex"; + } + + if (viewMode === "gallery") { + const w = window.innerWidth; + let maxCols; + if (w < 600) maxCols = 1; + else if (w < 900) maxCols = 2; + else if (w < 1200) maxCols = 4; + else maxCols = 6; + + const currentCols = Math.min( + parseInt(localStorage.getItem("galleryColumns") || "3", 10), + maxCols + ); + + sliderContainer.innerHTML = ` @@ -623,535 +730,566 @@ function searchFiles(searchTerm) { > ${currentCols} `; - const gallerySlider = document.getElementById("galleryColumnsSlider"); - const galleryValue = document.getElementById("galleryColumnsValue"); - gallerySlider.oninput = e => { - const v = +e.target.value; - localStorage.setItem("galleryColumns", v); - galleryValue.textContent = v; - document.querySelector(".gallery-container") - ?.style.setProperty("grid-template-columns", `repeat(${v},1fr)`); - }; - } else { - const currentHeight = parseInt(localStorage.getItem("rowHeight") || "48", 10); - sliderContainer.innerHTML = ` + const gallerySlider = document.getElementById("galleryColumnsSlider"); + const galleryValue = document.getElementById("galleryColumnsValue"); + gallerySlider.oninput = e => { + const v = +e.target.value; + localStorage.setItem("galleryColumns", v); + galleryValue.textContent = v; + document.querySelector(".gallery-container") + ?.style.setProperty("grid-template-columns", `repeat(${v},1fr)`); + }; + } else { + const currentHeight = parseInt(localStorage.getItem("rowHeight") || "48", 10); + sliderContainer.innerHTML = ` ${currentHeight}px `; - const rowSlider = document.getElementById("rowHeightSlider"); - const rowValue = document.getElementById("rowHeightValue"); - rowSlider.oninput = e => { - const v = e.target.value; - document.documentElement.style.setProperty("--file-row-height", v + "px"); - localStorage.setItem("rowHeight", v); - rowValue.textContent = v + "px"; - }; - } + const rowSlider = document.getElementById("rowHeightSlider"); + const rowValue = document.getElementById("rowHeightValue"); + rowSlider.oninput = e => { + const v = e.target.value; + document.documentElement.style.setProperty("--file-row-height", v + "px"); + localStorage.setItem("rowHeight", v); + rowValue.textContent = v + "px"; + }; } - - // 7) render files - if (reqId !== __fileListReqSeq) return []; - - if (window.viewMode === "gallery") { - renderGalleryView(folder); - } else { - renderFileTable(folder); - } - updateFileActionButtons(); - fileListContainer.style.visibility = "visible"; + } - - // ----- FOLDERS NEXT (populate strip when ready; doesn't block rows) ----- - try { - const foldersRes = await foldersPromise; - // If folders API forbids, just skip the strip; keep file rows rendered - if (foldersRes.status === 403) { - const strip = document.getElementById("folderStripContainer"); - if (strip) strip.style.display = "none"; - return data.files; - } - - const folderRaw = await safeJson(foldersRes).catch(() => []); // don't block file render on strip issues - if (reqId !== __fileListReqSeq) return data.files; - - // --- build ONLY the *direct* children of current folder --- - let subfolders = []; - const hidden = new Set(["profile_pics", "trash"]); - if (Array.isArray(folderRaw)) { - const allPaths = folderRaw.map(item => item.folder ?? item); - const depth = folder === "root" ? 1 : folder.split("/").length + 1; - subfolders = allPaths - .filter(p => { - if (folder === "root") return p.indexOf("/") === -1; - if (!p.startsWith(folder + "/")) return false; - return p.split("/").length === depth; - }) - .map(p => ({ name: p.split("/").pop(), full: p })); - } - subfolders = subfolders.filter(sf => !hidden.has(sf.name)); - - let strip = document.getElementById("folderStripContainer"); - if (!strip) { - strip = document.createElement("div"); - strip.id = "folderStripContainer"; - strip.className = "folder-strip-container"; - actionsContainer.parentNode.insertBefore(strip, actionsContainer); - } - - if (window.showFoldersInList && subfolders.length) { - strip.innerHTML = subfolders.map(sf => ` -
- folder -
${escapeHTML(sf.name)}
-
- `).join(""); - strip.style.display = "flex"; - - strip.querySelectorAll(".folder-item").forEach(el => { - // 1) click to navigate - el.addEventListener("click", () => { - const dest = el.dataset.folder; - window.currentFolder = dest; - localStorage.setItem("lastOpenedFolder", dest); - updateBreadcrumbTitle(dest); - document.querySelectorAll(".folder-option.selected").forEach(o => o.classList.remove("selected")); - document.querySelector(`.folder-option[data-folder="${dest}"]`)?.classList.add("selected"); - loadFileList(dest); - }); - - // 2) drag & drop - el.addEventListener("dragover", folderDragOverHandler); - el.addEventListener("dragleave", folderDragLeaveHandler); - el.addEventListener("drop", folderDropHandler); - - // 3) right-click context menu - el.addEventListener("contextmenu", e => { - e.preventDefault(); - e.stopPropagation(); - - const dest = el.dataset.folder; - window.currentFolder = dest; - localStorage.setItem("lastOpenedFolder", dest); - - strip.querySelectorAll(".folder-item.selected").forEach(i => i.classList.remove("selected")); - el.classList.add("selected"); - - const menuItems = [ - { - label: t("create_folder"), - action: () => document.getElementById("createFolderModal").style.display = "block" - }, - { - label: t("rename_folder"), - action: () => openRenameFolderModal() - }, - { - label: t("folder_share"), - action: () => openFolderShareModal(dest) - }, - { - label: t("delete_folder"), - action: () => openDeleteFolderModal() - } - ]; - showFolderManagerContextMenu(e.pageX, e.pageY, menuItems); - }); + // 7) render files + if (reqId !== __fileListReqSeq) return []; + + if (window.viewMode === "gallery") { + renderGalleryView(folder); + } else { + renderFileTable(folder); + } + updateFileActionButtons(); + fileListContainer.style.visibility = "visible"; + + + // ----- FOLDERS NEXT (populate strip when ready; doesn't block rows) ----- + try { + const foldersRes = await foldersPromise; + // If folders API forbids, just skip the strip; keep file rows rendered + if (foldersRes.status === 403) { + const strip = document.getElementById("folderStripContainer"); + if (strip) strip.style.display = "none"; + return data.files; + } + + const folderRaw = await safeJson(foldersRes).catch(() => []); // don't block file render on strip issues + if (reqId !== __fileListReqSeq) return data.files; + + // --- build ONLY the *direct* children of current folder --- + let subfolders = []; + const hidden = new Set(["profile_pics", "trash"]); + if (Array.isArray(folderRaw)) { + const allPaths = folderRaw.map(item => item.folder ?? item); + const depth = folder === "root" ? 1 : folder.split("/").length + 1; + subfolders = allPaths + .filter(p => { + if (folder === "root") return p.indexOf("/") === -1; + if (!p.startsWith(folder + "/")) return false; + return p.split("/").length === depth; + }) + .map(p => ({ name: p.split("/").pop(), full: p })); + } + subfolders = subfolders.filter(sf => !hidden.has(sf.name)); + + let strip = document.getElementById("folderStripContainer"); + if (!strip) { + strip = document.createElement("div"); + strip.id = "folderStripContainer"; + strip.className = "folder-strip-container"; + actionsContainer.parentNode.insertBefore(strip, actionsContainer); + } + + if (window.showFoldersInList && subfolders.length) { + strip.innerHTML = subfolders.map(sf => { + return ` +
+ +
+ ${escapeHTML(sf.name)} +
+
+ `; + }).join(""); + strip.style.display = "flex"; + + strip.querySelectorAll(".folder-item").forEach(el => { + // 1) click to navigate + el.addEventListener("click", () => { + const dest = el.dataset.folder; + window.currentFolder = dest; + localStorage.setItem("lastOpenedFolder", dest); + updateBreadcrumbTitle(dest); + document.querySelectorAll(".folder-option.selected").forEach(o => o.classList.remove("selected")); + document.querySelector(`.folder-option[data-folder="${dest}"]`)?.classList.add("selected"); + loadFileList(dest); }); - - document.addEventListener("click", hideFolderManagerContextMenu); - - } else { - strip.style.display = "none"; - } - } catch { - // ignore folder errors; rows already rendered - } - - return data.files; - - } catch (err) { - console.error("Error loading file list:", err); - if (err.status === 403) { - showToast(t("no_access_to_resource") || "You don't have access to this folder.", "error"); - const fileListContainer = document.getElementById("fileList"); - if (fileListContainer) fileListContainer.textContent = t("no_access_to_resource") || "You don't have access to this folder."; - } else if (err.message !== "Unauthorized") { - const fileListContainer = document.getElementById("fileList"); - if (fileListContainer) fileListContainer.textContent = "Error loading files."; - } - return []; - } finally { - if (reqId === __fileListReqSeq) { - fileListContainer.style.visibility = "visible"; + + // 2) drag & drop + el.addEventListener("dragover", folderDragOverHandler); + el.addEventListener("dragleave", folderDragLeaveHandler); + el.addEventListener("drop", folderDropHandler); + + // 3) right-click context menu + el.addEventListener("contextmenu", e => { + e.preventDefault(); + e.stopPropagation(); + + const dest = el.dataset.folder; + window.currentFolder = dest; + localStorage.setItem("lastOpenedFolder", dest); + + strip.querySelectorAll(".folder-item.selected").forEach(i => i.classList.remove("selected")); + el.classList.add("selected"); + + 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); + }); + }); + + document.addEventListener("click", hideFolderManagerContextMenu); + + // After wiring events for each .folder-item: + strip.querySelectorAll(".folder-item").forEach(el => { + const full = el.getAttribute('data-folder'); + attachStripIconAsync(el, full, 48); + }); + + } else { + strip.style.display = "none"; } + } catch { + // ignore folder errors; rows already rendered + } + + return data.files; + + } catch (err) { + console.error("Error loading file list:", err); + if (err.status === 403) { + showToast(t("no_access_to_resource") || "You don't have access to this folder.", "error"); + const fileListContainer = document.getElementById("fileList"); + if (fileListContainer) fileListContainer.textContent = t("no_access_to_resource") || "You don't have access to this folder."; + } else if (err.message !== "Unauthorized") { + const fileListContainer = document.getElementById("fileList"); + if (fileListContainer) fileListContainer.textContent = "Error loading files."; + } + return []; + } finally { + if (reqId === __fileListReqSeq) { + fileListContainer.style.visibility = "visible"; } } - - /** - * 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); - let currentPage = window.currentPage || 1; - - const filteredFiles = searchFiles(searchTerm); - - const totalFiles = filteredFiles.length; - const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); - if (currentPage > totalPages) { - currentPage = totalPages > 0 ? totalPages : 1; - window.currentPage = currentPage; - } - - // We pass a harmless "base" string to keep buildFileTableRow happy, - // then we will FIX the preview/thumbnail URLs to the API below. - const fakeBase = "#/"; - - const topControlsHTML = buildSearchAndPaginationControls({ - currentPage, - totalPages, - searchTerm: window.currentSearchTerm || "" - }); - - const combinedTopHTML = topControlsHTML; - - let headerHTML = buildFileTableHeader(sortOrder); - 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 rowHTML = buildFileTableRow(file, fakeBase); - - // add row id + data-file-name, and ensure the name cell also has "name-cell" - rowHTML = rowHTML - .replace(" 0) { - tagBadgesHTML = '
'; - file.tags.forEach(tag => { - tagBadgesHTML += `${escapeHTML(tag.name)}`; - }); - tagBadgesHTML += "
"; - } - rowsHTML += 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 += `
`; - } - rowsHTML += "
No files found.
"; - const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); - - fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML; +} - 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; - }); - }); +/** + * 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); + let currentPage = window.currentPage || 1; + + const filteredFiles = searchFiles(searchTerm); + + const totalFiles = filteredFiles.length; + const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); + if (currentPage > totalPages) { + currentPage = totalPages > 0 ? totalPages : 1; + window.currentPage = currentPage; + } + + // We pass a harmless "base" string to keep buildFileTableRow happy, + // then we will FIX the preview/thumbnail URLs to the API below. + const fakeBase = "#/"; + + const topControlsHTML = buildSearchAndPaginationControls({ + currentPage, + totalPages, + searchTerm: window.currentSearchTerm || "" + }); + + const combinedTopHTML = topControlsHTML; + + let headerHTML = buildFileTableHeader(sortOrder); + + headerHTML = headerHTML.replace(/]*)>/i, (full, attrs) => { + // If table already has class="", append filr-table. Otherwise add class attribute. + if (/class\s*=\s*"/i.test(attrs)) { + return full.replace(/class="([^"]*)"/i, (m, cls) => `class="${cls} filr-table"`); } + return ``; + }); - fileListContent.querySelectorAll('.folder-item').forEach(el => { - el.addEventListener('click', () => loadFileList(el.dataset.folder)); - }); - - // pagination clicks - const prevBtn = document.getElementById("prevPageBtn"); - if (prevBtn) prevBtn.addEventListener("click", () => { - if (window.currentPage > 1) { - window.currentPage--; - renderFileTable(folder, container); + 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 rowHTML = buildFileTableRow(file, fakeBase); + + // add row id + data-file-name, and ensure the name cell also has "name-cell" + rowHTML = rowHTML + .replace(" 0) { + tagBadgesHTML = '
'; + file.tags.forEach(tag => { + tagBadgesHTML += `${escapeHTML(tag.name)}`; + }); + tagBadgesHTML += "
"; } + rowsHTML += 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}`; + } + ); }); - const nextBtn = document.getElementById("nextPageBtn"); - if (nextBtn) nextBtn.addEventListener("click", () => { - if (window.currentPage < totalPages) { - window.currentPage++; - renderFileTable(folder, container); + } else { + rowsHTML += `
`; + } + rowsHTML += "
No files found.
"; + const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); + + fileListContent.innerHTML = combinedTopHTML + headerHTML + rowsHTML + bottomControlsHTML; + + 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; + }); }); - - // advanced search toggle - const advToggle = document.getElementById("advancedSearchToggle"); - if (advToggle) advToggle.addEventListener("click", () => { - toggleAdvancedSearch(); + } + + fileListContent.querySelectorAll('.folder-item').forEach(el => { + el.addEventListener('click', () => loadFileList(el.dataset.folder)); + }); + + // pagination clicks + const prevBtn = document.getElementById("prevPageBtn"); + if (prevBtn) prevBtn.addEventListener("click", () => { + if (window.currentPage > 1) { + window.currentPage--; + renderFileTable(folder, container); + } + }); + const nextBtn = document.getElementById("nextPageBtn"); + if (nextBtn) nextBtn.addEventListener("click", () => { + if (window.currentPage < totalPages) { + window.currentPage++; + renderFileTable(folder, container); + } + }); + + // advanced search toggle + const advToggle = document.getElementById("advancedSearchToggle"); + if (advToggle) advToggle.addEventListener("click", () => { + toggleAdvancedSearch(); + }); + + // items-per-page selector + const itemsSelect = document.getElementById("itemsPerPageSelect"); + if (itemsSelect) itemsSelect.addEventListener("change", e => { + window.itemsPerPage = parseInt(e.target.value, 10); + localStorage.setItem("itemsPerPage", window.itemsPerPage); + window.currentPage = 1; + renderFileTable(folder, container); + }); + + // Row-select + fileListContent.querySelectorAll("tbody tr").forEach(row => { + row.addEventListener("click", e => { + const cb = row.querySelector(".file-checkbox"); + if (!cb) return; + toggleRowSelection(e, cb.value); }); - - // items-per-page selector - const itemsSelect = document.getElementById("itemsPerPageSelect"); - if (itemsSelect) itemsSelect.addEventListener("change", e => { - window.itemsPerPage = parseInt(e.target.value, 10); - localStorage.setItem("itemsPerPage", window.itemsPerPage); + }); + + // Download buttons + fileListContent.querySelectorAll(".download-btn").forEach(btn => { + btn.addEventListener("click", e => { + e.stopPropagation(); + openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder); + }); + }); + + // Edit buttons + fileListContent.querySelectorAll(".edit-btn").forEach(btn => { + btn.addEventListener("click", async e => { + e.stopPropagation(); + const m = await import('./fileEditor.js?v={{APP_QVER}}'); + m.editFile(btn.dataset.editName, btn.dataset.editFolder); + }); + }); + + // Rename buttons + fileListContent.querySelectorAll(".rename-btn").forEach(btn => { + btn.addEventListener("click", async e => { + e.stopPropagation(); + const m = await import('./fileActions.js?v={{APP_QVER}}'); + m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder); + }); + }); + + // Preview buttons + fileListContent.querySelectorAll(".preview-btn").forEach(btn => { + btn.addEventListener("click", async e => { + e.stopPropagation(); + const m = await import('./filePreview.js?v={{APP_QVER}}'); + m.previewFile(btn.dataset.previewUrl, btn.dataset.previewName); + }); + }); + + createViewToggleButton(); + + // search input + const newSearchInput = document.getElementById("searchInput"); + if (newSearchInput) { + newSearchInput.addEventListener("input", debounce(function () { + window.currentSearchTerm = newSearchInput.value; window.currentPage = 1; renderFileTable(folder, container); + setTimeout(() => { + const freshInput = document.getElementById("searchInput"); + if (freshInput) { + freshInput.focus(); + const len = freshInput.value.length; + freshInput.setSelectionRange(len, len); + } + }, 0); + }, 300)); + } + + const slider = document.getElementById('rowHeightSlider'); + const valueDisplay = document.getElementById('rowHeightValue'); + if (slider) { + slider.addEventListener('input', e => { + const v = +e.target.value; // slider value in px + document.documentElement.style.setProperty('--file-row-height', v + 'px'); + localStorage.setItem('rowHeight', v); + valueDisplay.textContent = v + 'px'; }); - - // Row-select - fileListContent.querySelectorAll("tbody tr").forEach(row => { - row.addEventListener("click", e => { - const cb = row.querySelector(".file-checkbox"); - if (!cb) return; - toggleRowSelection(e, cb.value); - }); + } + + document.querySelectorAll("#fileList table.filr-table thead th[data-column]").forEach(cell => { + cell.addEventListener("click", function () { + const column = this.getAttribute("data-column"); + sortFiles(column, folder); }); - - // Download buttons - fileListContent.querySelectorAll(".download-btn").forEach(btn => { - btn.addEventListener("click", e => { - e.stopPropagation(); - openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder); - }); + }); + document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => { + checkbox.addEventListener("change", function (e) { + updateRowHighlight(e.target); + updateFileActionButtons(); }); - - // Edit buttons - fileListContent.querySelectorAll(".edit-btn").forEach(btn => { - btn.addEventListener("click", async e => { - e.stopPropagation(); - const m = await import('./fileEditor.js?v={{APP_QVER}}'); - m.editFile(btn.dataset.editName, btn.dataset.editFolder); - }); + }); + document.querySelectorAll(".share-btn").forEach(btn => { + btn.addEventListener("click", function (e) { + e.stopPropagation(); + const fileName = this.getAttribute("data-file"); + const file = fileData.find(f => f.name === fileName); + if (file) { + import('./filePreview.js?v={{APP_QVER}}').then(module => { + module.openShareModal(file, folder); + }); + } }); - - // Rename buttons - fileListContent.querySelectorAll(".rename-btn").forEach(btn => { - btn.addEventListener("click", async e => { - e.stopPropagation(); - const m = await import('./fileActions.js?v={{APP_QVER}}'); - m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder); - }); + }); + updateFileActionButtons(); + + document.querySelectorAll("#fileList tbody tr").forEach(row => { + row.setAttribute("draggable", "true"); + import('./fileDragDrop.js?v={{APP_QVER}}').then(module => { + row.addEventListener("dragstart", module.fileDragStartHandler); }); - - // Preview buttons - fileListContent.querySelectorAll(".preview-btn").forEach(btn => { - btn.addEventListener("click", async e => { - e.stopPropagation(); - const m = await import('./filePreview.js?v={{APP_QVER}}'); - m.previewFile(btn.dataset.previewUrl, btn.dataset.previewName); - }); - }); - - createViewToggleButton(); - - // search input - const newSearchInput = document.getElementById("searchInput"); - if (newSearchInput) { - newSearchInput.addEventListener("input", debounce(function () { - window.currentSearchTerm = newSearchInput.value; + }); + document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => { + btn.addEventListener("click", e => e.stopPropagation()); + }); + bindFileListContextMenu(); + refreshViewedBadges(folder).catch(() => { }); +} + +// A helper to compute the max image height based on the current column count. +function getMaxImageHeight() { + const columns = parseInt(window.galleryColumns || 3, 10); + return 150 * (7 - columns); +} + +export function renderGalleryView(folder, container) { + const fileListContent = container || document.getElementById("fileList"); + const searchTerm = (window.currentSearchTerm || "").toLowerCase(); + const filteredFiles = searchFiles(searchTerm); + + // API preview base (we’ll build per-file URLs) + const apiBase = `/api/file/download.php?folder=${encodeURIComponent(folder)}&file=`; + + // pagination settings + const itemsPerPage = window.itemsPerPage; + let currentPage = window.currentPage || 1; + const totalFiles = filteredFiles.length; + const totalPages = Math.ceil(totalFiles / itemsPerPage); + if (currentPage > totalPages) { + currentPage = totalPages || 1; + window.currentPage = currentPage; + } + + // --- Top controls: search + pagination + items-per-page --- + let galleryHTML = buildSearchAndPaginationControls({ + currentPage, + totalPages, + searchTerm: window.currentSearchTerm || "" + }); + + // wire up search input just like table view + setTimeout(() => { + const searchInput = document.getElementById("searchInput"); + if (searchInput) { + searchInput.addEventListener("input", debounce(() => { + window.currentSearchTerm = searchInput.value; window.currentPage = 1; - renderFileTable(folder, container); + renderGalleryView(folder); setTimeout(() => { - const freshInput = document.getElementById("searchInput"); - if (freshInput) { - freshInput.focus(); - const len = freshInput.value.length; - freshInput.setSelectionRange(len, len); + const f = document.getElementById("searchInput"); + if (f) { + f.focus(); + const len = f.value.length; + f.setSelectionRange(len, len); } }, 0); }, 300)); } - - const slider = document.getElementById('rowHeightSlider'); - const valueDisplay = document.getElementById('rowHeightValue'); - if (slider) { - slider.addEventListener('input', e => { - const v = +e.target.value; // slider value in px - document.documentElement.style.setProperty('--file-row-height', v + 'px'); - localStorage.setItem('rowHeight', v); - valueDisplay.textContent = v + 'px'; - }); - } - - document.querySelectorAll("table.table thead th[data-column]").forEach(cell => { - cell.addEventListener("click", function () { - const column = this.getAttribute("data-column"); - sortFiles(column, folder); - }); - }); - document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => { - checkbox.addEventListener("change", function (e) { - updateRowHighlight(e.target); - updateFileActionButtons(); - }); - }); - document.querySelectorAll(".share-btn").forEach(btn => { - btn.addEventListener("click", function (e) { - e.stopPropagation(); - const fileName = this.getAttribute("data-file"); - const file = fileData.find(f => f.name === fileName); - if (file) { - import('./filePreview.js?v={{APP_QVER}}').then(module => { - module.openShareModal(file, folder); - }); - } - }); - }); - updateFileActionButtons(); - - document.querySelectorAll("#fileList tbody tr").forEach(row => { - 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()); - }); - bindFileListContextMenu(); - refreshViewedBadges(folder).catch(() => {}); - } - - // A helper to compute the max image height based on the current column count. - function getMaxImageHeight() { - const columns = parseInt(window.galleryColumns || 3, 10); - return 150 * (7 - columns); - } - - export function renderGalleryView(folder, container) { - const fileListContent = container || document.getElementById("fileList"); - const searchTerm = (window.currentSearchTerm || "").toLowerCase(); - const filteredFiles = searchFiles(searchTerm); - - // API preview base (we’ll build per-file URLs) - const apiBase = `/api/file/download.php?folder=${encodeURIComponent(folder)}&file=`; - - // pagination settings - const itemsPerPage = window.itemsPerPage; - let currentPage = window.currentPage || 1; - const totalFiles = filteredFiles.length; - const totalPages = Math.ceil(totalFiles / itemsPerPage); - if (currentPage > totalPages) { - currentPage = totalPages || 1; - window.currentPage = currentPage; - } - - // --- Top controls: search + pagination + items-per-page --- - let galleryHTML = buildSearchAndPaginationControls({ - currentPage, - totalPages, - searchTerm: window.currentSearchTerm || "" - }); - - // wire up search input just like table view - setTimeout(() => { - const searchInput = document.getElementById("searchInput"); - if (searchInput) { - searchInput.addEventListener("input", debounce(() => { - window.currentSearchTerm = searchInput.value; - window.currentPage = 1; - renderGalleryView(folder); - setTimeout(() => { - const f = document.getElementById("searchInput"); - if (f) { - f.focus(); - const len = f.value.length; - f.setSelectionRange(len, len); - } - }, 0); - }, 300)); - } - }, 0); - - // determine column max by screen size - const numColumns = window.galleryColumns || 3; - const w = window.innerWidth; - let maxCols = 6; - if (w < 600) maxCols = 1; - else if (w < 900) maxCols = 2; - const startCols = Math.min(numColumns, maxCols); - window.galleryColumns = startCols; - - // --- Start gallery grid --- - galleryHTML += ` + }, 0); + + // determine column max by screen size + const numColumns = window.galleryColumns || 3; + const w = window.innerWidth; + let maxCols = 6; + if (w < 600) maxCols = 1; + else if (w < 900) maxCols = 2; + const startCols = Math.min(numColumns, maxCols); + window.galleryColumns = startCols; + + // --- Start gallery grid --- + galleryHTML += ` `; - }); - - galleryHTML += `
`; // end gallery-container - - // bottom controls - galleryHTML += buildBottomControls(itemsPerPage); - - // render - fileListContent.innerHTML = galleryHTML; - - - // pagination buttons for gallery - const prevBtn = document.getElementById("prevPageBtn"); - if (prevBtn) prevBtn.addEventListener("click", () => { - if (window.currentPage > 1) { - window.currentPage--; - renderGalleryView(folder, container); - } - }); - const nextBtn = document.getElementById("nextPageBtn"); - if (nextBtn) nextBtn.addEventListener("click", () => { - if (window.currentPage < totalPages) { - window.currentPage++; - renderGalleryView(folder, container); - } - }); - - // advanced search toggle - const advToggle = document.getElementById("advancedSearchToggle"); - if (advToggle) advToggle.addEventListener("click", () => { - toggleAdvancedSearch(); - }); - - // context menu in gallery - bindFileListContextMenu(); - - // items-per-page selector for gallery - const itemsSelect = document.getElementById("itemsPerPageSelect"); - if (itemsSelect) itemsSelect.addEventListener("change", e => { - window.itemsPerPage = parseInt(e.target.value, 10); - localStorage.setItem("itemsPerPage", window.itemsPerPage); - window.currentPage = 1; + }); + + galleryHTML += ``; // end gallery-container + + // bottom controls + galleryHTML += buildBottomControls(itemsPerPage); + + // render + fileListContent.innerHTML = galleryHTML; + + + // pagination buttons for gallery + const prevBtn = document.getElementById("prevPageBtn"); + if (prevBtn) prevBtn.addEventListener("click", () => { + if (window.currentPage > 1) { + window.currentPage--; renderGalleryView(folder, container); - }); - - // cache images on load - fileListContent.querySelectorAll('.gallery-thumbnail').forEach(img => { - const key = img.dataset.cacheKey; - img.addEventListener('load', () => cacheImage(img, key)); - }); - - // preview clicks (dynamic import to avoid global dependency) - fileListContent.querySelectorAll(".gallery-preview").forEach(el => { - el.addEventListener("click", async () => { - const m = await import('./filePreview.js?v={{APP_QVER}}'); - m.previewFile(el.dataset.previewUrl, el.dataset.previewName); - }); - }); - - // download clicks - fileListContent.querySelectorAll(".download-btn").forEach(btn => { - btn.addEventListener("click", e => { - e.stopPropagation(); - openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder); - }); - }); - - // edit clicks - fileListContent.querySelectorAll(".edit-btn").forEach(btn => { - btn.addEventListener("click", async e => { - e.stopPropagation(); - const m = await import('./fileEditor.js?v={{APP_QVER}}'); - m.editFile(btn.dataset.editName, btn.dataset.editFolder); - }); - }); - - // rename clicks - fileListContent.querySelectorAll(".rename-btn").forEach(btn => { - btn.addEventListener("click", async e => { - e.stopPropagation(); - const m = await import('./fileActions.js?v={{APP_QVER}}'); - m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder); - }); - }); - - // share clicks - fileListContent.querySelectorAll(".share-btn").forEach(btn => { - btn.addEventListener("click", e => { - e.stopPropagation(); - const fileName = btn.dataset.file; - const fileObj = fileData.find(f => f.name === fileName); - if (fileObj) { - import('./filePreview.js?v={{APP_QVER}}').then(m => m.openShareModal(fileObj, folder)); - } - }); - }); - - // checkboxes - fileListContent.querySelectorAll(".file-checkbox").forEach(cb => { - cb.addEventListener("change", () => updateFileActionButtons()); - }); - - // slider - const slider = document.getElementById("galleryColumnsSlider"); - if (slider) { - slider.addEventListener("input", () => { - const v = +slider.value; - document.getElementById("galleryColumnsValue").textContent = v; - window.galleryColumns = v; - document.querySelector(".gallery-container") - .style.gridTemplateColumns = `repeat(${v},1fr)`; - document.querySelectorAll(".gallery-thumbnail") - .forEach(img => img.style.maxHeight = getMaxImageHeight() + "px"); - }); } - - // pagination helpers - window.changePage = newPage => { - window.currentPage = newPage; - if (window.viewMode === "gallery") renderGalleryView(folder); - else renderFileTable(folder); - }; - - window.changeItemsPerPage = cnt => { - window.itemsPerPage = +cnt; - localStorage.setItem("itemsPerPage", cnt); - window.currentPage = 1; - if (window.viewMode === "gallery") renderGalleryView(folder); - else renderFileTable(folder); - }; - refreshViewedBadges(folder).catch(() => {}); - updateFileActionButtons(); - createViewToggleButton(); - } - - // Responsive slider constraints based on screen size. - function updateSliderConstraints() { - const slider = document.getElementById("galleryColumnsSlider"); - if (!slider) return; - - const width = window.innerWidth; - let min = 1; - let max; - - if (width < 600) { - max = 1; - } else if (width < 1024) { - max = 3; - } else if (width < 1440) { - max = 4; - } else { - max = 6; + }); + const nextBtn = document.getElementById("nextPageBtn"); + if (nextBtn) nextBtn.addEventListener("click", () => { + if (window.currentPage < totalPages) { + window.currentPage++; + renderGalleryView(folder, container); } - - let currentVal = parseInt(slider.value, 10); - if (currentVal > max) { - currentVal = max; - slider.value = max; - } - - slider.min = min; - slider.max = max; - document.getElementById("galleryColumnsValue").textContent = currentVal; - - const galleryContainer = document.querySelector(".gallery-container"); - if (galleryContainer) { - galleryContainer.style.gridTemplateColumns = `repeat(${currentVal}, 1fr)`; - } - } - - window.addEventListener('load', updateSliderConstraints); - window.addEventListener('resize', updateSliderConstraints); - - export function sortFiles(column, folder) { - if (sortOrder.column === column) { - sortOrder.ascending = !sortOrder.ascending; - } else { - sortOrder.column = column; - sortOrder.ascending = true; - } - fileData.sort((a, b) => { - let valA = a[column] || ""; - let valB = b[column] || ""; - if (column === "modified" || column === "uploaded") { - const parsedA = parseCustomDate(valA); - const parsedB = parseCustomDate(valB); - valA = parsedA; - valB = parsedB; - } else if (typeof valA === "string") { - valA = valA.toLowerCase(); - valB = valB.toLowerCase(); - } - if (valA < valB) return sortOrder.ascending ? -1 : 1; - if (valA > valB) return sortOrder.ascending ? 1 : -1; - return 0; - }); - if (window.viewMode === "gallery") { - renderGalleryView(folder); - } else { - renderFileTable(folder); - } - } - - function parseCustomDate(dateStr) { - dateStr = dateStr.replace(/\s+/g, " ").trim(); - const parts = dateStr.split(" "); - if (parts.length !== 2) { - return new Date(dateStr).getTime(); - } - const datePart = parts[0]; - const timePart = parts[1]; - const dateComponents = datePart.split("/"); - if (dateComponents.length !== 3) { - return new Date(dateStr).getTime(); - } - let month = parseInt(dateComponents[0], 10); - let day = parseInt(dateComponents[1], 10); - let year = parseInt(dateComponents[2], 10); - if (year < 100) { - year += 2000; - } - const timeRegex = /^(\d{1,2}):(\d{2})(AM|PM)$/i; - const match = timePart.match(timeRegex); - if (!match) { - return new Date(dateStr).getTime(); - } - let hour = parseInt(match[1], 10); - const minute = parseInt(match[2], 10); - const period = match[3].toUpperCase(); - if (period === "PM" && hour !== 12) { - hour += 12; - } - if (period === "AM" && hour === 12) { - hour = 0; - } - return new Date(year, month - 1, day, hour, minute).getTime(); - } - - export function canEditFile(fileName) { - if (!fileName || typeof fileName !== "string") return false; - const dot = fileName.lastIndexOf("."); - if (dot < 0) return false; - const ext = fileName.slice(dot + 1).toLowerCase(); - - // Your CodeMirror text-based types - const textEditExts = new Set([ - "txt","text","md","markdown","rst", - "html","htm","xhtml","shtml", - "css","scss","sass","less", - "js","mjs","cjs","jsx", - "ts","tsx", - "json","jsonc","ndjson", - "yml","yaml","toml","xml","plist", - "ini","conf","config","cfg","cnf","properties","props","rc", - "env","dotenv", - "csv","tsv","tab", - "log", - "sh","bash","zsh","ksh","fish", - "bat","cmd", - "ps1","psm1","psd1", - "py","pyw","rb","pl","pm","go","rs","java","kt","kts", - "scala","sc","groovy","gradle", - "c","h","cpp","cxx","cc","hpp","hh","hxx", - "m","mm","swift","cs","fs","fsx","dart","lua","r","rmd", - "sql","vue","svelte","twig","mustache","hbs","handlebars","ejs","pug","jade" - ]); - - if (textEditExts.has(ext)) return true; // CodeMirror - if (OO_ENABLED && OO_EXTS.has(ext)) return true; // ONLYOFFICE types if enabled - return false; - } - - // Expose global functions for pagination and preview. - window.changePage = function (newPage) { - window.currentPage = newPage; - if (window.viewMode === 'gallery') { - renderGalleryView(window.currentFolder); - } else { - renderFileTable(window.currentFolder); - } - }; - - window.changeItemsPerPage = function (newCount) { - window.itemsPerPage = parseInt(newCount, 10); - localStorage.setItem('itemsPerPage', newCount); + }); + + // advanced search toggle + const advToggle = document.getElementById("advancedSearchToggle"); + if (advToggle) advToggle.addEventListener("click", () => { + toggleAdvancedSearch(); + }); + + // context menu in gallery + bindFileListContextMenu(); + + // items-per-page selector for gallery + const itemsSelect = document.getElementById("itemsPerPageSelect"); + if (itemsSelect) itemsSelect.addEventListener("change", e => { + window.itemsPerPage = parseInt(e.target.value, 10); + localStorage.setItem("itemsPerPage", window.itemsPerPage); window.currentPage = 1; - if (window.viewMode === 'gallery') { - renderGalleryView(window.currentFolder); - } else { - renderFileTable(window.currentFolder); - } + renderGalleryView(folder, container); + }); + + // cache images on load + fileListContent.querySelectorAll('.gallery-thumbnail').forEach(img => { + const key = img.dataset.cacheKey; + img.addEventListener('load', () => cacheImage(img, key)); + }); + + // preview clicks (dynamic import to avoid global dependency) + fileListContent.querySelectorAll(".gallery-preview").forEach(el => { + el.addEventListener("click", async () => { + const m = await import('./filePreview.js?v={{APP_QVER}}'); + m.previewFile(el.dataset.previewUrl, el.dataset.previewName); + }); + }); + + // download clicks + fileListContent.querySelectorAll(".download-btn").forEach(btn => { + btn.addEventListener("click", e => { + e.stopPropagation(); + openDownloadModal(btn.dataset.downloadName, btn.dataset.downloadFolder); + }); + }); + + // edit clicks + fileListContent.querySelectorAll(".edit-btn").forEach(btn => { + btn.addEventListener("click", async e => { + e.stopPropagation(); + const m = await import('./fileEditor.js?v={{APP_QVER}}'); + m.editFile(btn.dataset.editName, btn.dataset.editFolder); + }); + }); + + // rename clicks + fileListContent.querySelectorAll(".rename-btn").forEach(btn => { + btn.addEventListener("click", async e => { + e.stopPropagation(); + const m = await import('./fileActions.js?v={{APP_QVER}}'); + m.renameFile(btn.dataset.renameName, btn.dataset.renameFolder); + }); + }); + + // share clicks + fileListContent.querySelectorAll(".share-btn").forEach(btn => { + btn.addEventListener("click", e => { + e.stopPropagation(); + const fileName = btn.dataset.file; + const fileObj = fileData.find(f => f.name === fileName); + if (fileObj) { + import('./filePreview.js?v={{APP_QVER}}').then(m => m.openShareModal(fileObj, folder)); + } + }); + }); + + // checkboxes + fileListContent.querySelectorAll(".file-checkbox").forEach(cb => { + cb.addEventListener("change", () => updateFileActionButtons()); + }); + + // slider + const slider = document.getElementById("galleryColumnsSlider"); + if (slider) { + slider.addEventListener("input", () => { + const v = +slider.value; + document.getElementById("galleryColumnsValue").textContent = v; + window.galleryColumns = v; + document.querySelector(".gallery-container") + .style.gridTemplateColumns = `repeat(${v},1fr)`; + document.querySelectorAll(".gallery-thumbnail") + .forEach(img => img.style.maxHeight = getMaxImageHeight() + "px"); + }); + } + + // pagination helpers + window.changePage = newPage => { + window.currentPage = newPage; + if (window.viewMode === "gallery") renderGalleryView(folder); + else renderFileTable(folder); }; - - // fileListView.js (bottom) - window.loadFileList = loadFileList; - window.renderFileTable = renderFileTable; - window.renderGalleryView = renderGalleryView; - window.sortFiles = sortFiles; - window.toggleAdvancedSearch = toggleAdvancedSearch; \ No newline at end of file + + window.changeItemsPerPage = cnt => { + window.itemsPerPage = +cnt; + localStorage.setItem("itemsPerPage", cnt); + window.currentPage = 1; + if (window.viewMode === "gallery") renderGalleryView(folder); + else renderFileTable(folder); + }; + refreshViewedBadges(folder).catch(() => { }); + updateFileActionButtons(); + createViewToggleButton(); +} + +// Responsive slider constraints based on screen size. +function updateSliderConstraints() { + const slider = document.getElementById("galleryColumnsSlider"); + if (!slider) return; + + const width = window.innerWidth; + let min = 1; + let max; + + if (width < 600) { + max = 1; + } else if (width < 1024) { + max = 3; + } else if (width < 1440) { + max = 4; + } else { + max = 6; + } + + let currentVal = parseInt(slider.value, 10); + if (currentVal > max) { + currentVal = max; + slider.value = max; + } + + slider.min = min; + slider.max = max; + document.getElementById("galleryColumnsValue").textContent = currentVal; + + const galleryContainer = document.querySelector(".gallery-container"); + if (galleryContainer) { + galleryContainer.style.gridTemplateColumns = `repeat(${currentVal}, 1fr)`; + } +} + +window.addEventListener('load', updateSliderConstraints); +window.addEventListener('resize', updateSliderConstraints); + +export function sortFiles(column, folder) { + if (sortOrder.column === column) { + sortOrder.ascending = !sortOrder.ascending; + } else { + sortOrder.column = column; + sortOrder.ascending = true; + } + fileData.sort((a, b) => { + let valA = a[column] || ""; + let valB = b[column] || ""; + if (column === "modified" || column === "uploaded") { + const parsedA = parseCustomDate(valA); + const parsedB = parseCustomDate(valB); + valA = parsedA; + valB = parsedB; + } else if (typeof valA === "string") { + valA = valA.toLowerCase(); + valB = valB.toLowerCase(); + } + if (valA < valB) return sortOrder.ascending ? -1 : 1; + if (valA > valB) return sortOrder.ascending ? 1 : -1; + return 0; + }); + if (window.viewMode === "gallery") { + renderGalleryView(folder); + } else { + renderFileTable(folder); + } +} + +function parseCustomDate(dateStr) { + dateStr = dateStr.replace(/\s+/g, " ").trim(); + const parts = dateStr.split(" "); + if (parts.length !== 2) { + return new Date(dateStr).getTime(); + } + const datePart = parts[0]; + const timePart = parts[1]; + const dateComponents = datePart.split("/"); + if (dateComponents.length !== 3) { + return new Date(dateStr).getTime(); + } + let month = parseInt(dateComponents[0], 10); + let day = parseInt(dateComponents[1], 10); + let year = parseInt(dateComponents[2], 10); + if (year < 100) { + year += 2000; + } + const timeRegex = /^(\d{1,2}):(\d{2})(AM|PM)$/i; + const match = timePart.match(timeRegex); + if (!match) { + return new Date(dateStr).getTime(); + } + let hour = parseInt(match[1], 10); + const minute = parseInt(match[2], 10); + const period = match[3].toUpperCase(); + if (period === "PM" && hour !== 12) { + hour += 12; + } + if (period === "AM" && hour === 12) { + hour = 0; + } + return new Date(year, month - 1, day, hour, minute).getTime(); +} + +export function canEditFile(fileName) { + if (!fileName || typeof fileName !== "string") return false; + const dot = fileName.lastIndexOf("."); + if (dot < 0) return false; + const ext = fileName.slice(dot + 1).toLowerCase(); + + // Your CodeMirror text-based types + const textEditExts = new Set([ + "txt", "text", "md", "markdown", "rst", + "html", "htm", "xhtml", "shtml", + "css", "scss", "sass", "less", + "js", "mjs", "cjs", "jsx", + "ts", "tsx", + "json", "jsonc", "ndjson", + "yml", "yaml", "toml", "xml", "plist", + "ini", "conf", "config", "cfg", "cnf", "properties", "props", "rc", + "env", "dotenv", + "csv", "tsv", "tab", + "log", + "sh", "bash", "zsh", "ksh", "fish", + "bat", "cmd", + "ps1", "psm1", "psd1", + "py", "pyw", "rb", "pl", "pm", "go", "rs", "java", "kt", "kts", + "scala", "sc", "groovy", "gradle", + "c", "h", "cpp", "cxx", "cc", "hpp", "hh", "hxx", + "m", "mm", "swift", "cs", "fs", "fsx", "dart", "lua", "r", "rmd", + "sql", "vue", "svelte", "twig", "mustache", "hbs", "handlebars", "ejs", "pug", "jade" + ]); + + if (textEditExts.has(ext)) return true; // CodeMirror + if (OO_ENABLED && OO_EXTS.has(ext)) return true; // ONLYOFFICE types if enabled + return false; +} + +// Expose global functions for pagination and preview. +window.changePage = function (newPage) { + window.currentPage = newPage; + if (window.viewMode === 'gallery') { + renderGalleryView(window.currentFolder); + } else { + renderFileTable(window.currentFolder); + } +}; + +window.changeItemsPerPage = function (newCount) { + window.itemsPerPage = parseInt(newCount, 10); + localStorage.setItem('itemsPerPage', newCount); + window.currentPage = 1; + if (window.viewMode === 'gallery') { + renderGalleryView(window.currentFolder); + } else { + renderFileTable(window.currentFolder); + } +}; + +// fileListView.js (bottom) +window.loadFileList = loadFileList; +window.renderFileTable = renderFileTable; +window.renderGalleryView = renderGalleryView; +window.sortFiles = sortFiles; +window.toggleAdvancedSearch = toggleAdvancedSearch; \ No newline at end of file diff --git a/public/js/folderManager.js b/public/js/folderManager.js index ab3b9e7..a1d356a 100644 --- a/public/js/folderManager.js +++ b/public/js/folderManager.js @@ -367,12 +367,18 @@ async function saveFolderColor(folder, colorHexOrEmpty) { if (!res.ok || data.error) throw new Error(data.error || `HTTP ${res.status}`); // update local map & apply if (data.color) window.folderColorMap[folder] = data.color; - else delete window.folderColorMap[folder]; - applyFolderColorToOption(folder, data.color || ''); - return data; +else delete window.folderColorMap[folder]; +applyFolderColorToOption(folder, data.color || ''); + +// notify other views (fileListView's strip) +window.dispatchEvent(new CustomEvent('folderColorChanged', { + detail: { folder, color: data.color || '' } +})); + +return data; } -function openColorFolderModal(folder) { +export function openColorFolderModal(folder) { const existing = window.folderColorMap[folder] || ''; const defaultHex = existing || '#f6b84e'; @@ -559,39 +565,86 @@ const _nonEmptyCache = new Map(); Folder icon (SVG + fetch + cache) ----------------------*/ -// Crisp emoji-like folder (empty / with paper) -function folderSVG(kind = 'empty') { +// shared by folder tree + folder strip +export function folderSVG(kind = 'empty') { + const gid = 'g' + Math.random().toString(36).slice(2, 8); + + // tweak these + const PAPER_SHIFT_Y = -1.2; // move paper up (negative = up) + const INK_SHIFT_Y = -0.8; // extra lift for the blue lines + return ` - `; } const _folderCountCache = new Map(); @@ -1253,7 +1306,7 @@ if (submitRename) { } // === Move Folder Modal helper (shared by button + context menu) === -function openMoveFolderUI(sourceFolder) { +export function openMoveFolderUI(sourceFolder) { const modal = document.getElementById('moveFolderModal'); const targetSel = document.getElementById('moveFolderTarget');