release(v1.9.3): unify folder icons across tree & strip, add “paper” lines, live color sync, and vendor-aware release

This commit is contained in:
Ryan
2025-11-11 00:09:15 -05:00
committed by GitHub
parent a031fc99c2
commit dbdf760d4d
7 changed files with 1817 additions and 1311 deletions

View File

@@ -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; well 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: |

View File

@@ -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 `<span class="folder-svg">` + 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)

View File

@@ -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 {
}
.folder-strip-container {
display: flex;
padding-top: 0px !important;
flex-wrap: wrap;
gap: 12px;
padding: 8px 0;
}.folder-strip-container .folder-item {
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 {
text-align: center;
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;
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 {
hyphens: auto;
text-align: center;
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; /* 2226 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 <div>) */
> .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 were 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; }

View File

@@ -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;

View File

@@ -156,7 +156,7 @@ export function buildSearchAndPaginationControls({ currentPage, totalPages, sear
export function buildFileTableHeader(sortOrder) {
return `
<table class="table">
<table class="table filr-table table-hover table-striped">
<thead>
<tr>
<th class="checkbox-col"><input type="checkbox" id="selectAll"></th>
@@ -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');
}
}

View File

@@ -11,33 +11,37 @@ import {
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 {
} 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 {
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}}';
} from './fileDragDrop.js?v={{APP_QVER}}';
export let fileData = [];
export let sortOrder = { column: "uploaded", ascending: true };
export let fileData = [];
export let sortOrder = { column: "uploaded", ascending: true };
// onnlyoffice
// onnlyoffice
let OO_ENABLED = false;
let OO_EXTS = new Set();
@@ -54,26 +58,58 @@ export async function initOnlyOfficeCaps() {
}
}
// 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;
// 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;
window.itemsPerPage = parseInt(
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";
);
window.currentPage = window.currentPage || 1;
window.viewMode = localStorage.getItem("viewMode") || "table";
// Global flag for advanced search mode.
window.advancedSearchEnabled = false;
// 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) {
function apiFileUrl(folder, name, inline = false) {
const f = folder && folder !== "root" ? folder : "root";
const q = new URLSearchParams({
folder: f,
@@ -82,9 +118,9 @@ export async function initOnlyOfficeCaps() {
t: String(Date.now()) // cache-bust
});
return `/api/file/download.php?${q.toString()}`;
}
}
// Wire "select all" header checkbox for the current table render
// 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,11 +173,82 @@ function wireSelectAll(fileListContent) {
syncHeader();
}
/* -----------------------------
// ---- 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;
}
h /= 6;
}
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) {
// 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 */ }
@@ -156,8 +263,8 @@ function wireSelectAll(fileListContent) {
throw err;
}
return body ?? {};
}
// ---- Viewed badges (table + gallery) ----
}
// ---- Viewed badges (table + gallery) ----
// ---------- Badge factory (center text vertically) ----------
function makeBadge(state) {
if (!state) return null;
@@ -272,10 +379,10 @@ 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) {
function parseSizeToBytes(sizeStr) {
if (!sizeStr) return 0;
let s = sizeStr.trim();
let value = parseFloat(s);
@@ -288,12 +395,12 @@ export async function refreshViewedBadges(folder) {
value *= 1024 * 1024 * 1024;
}
return value;
}
}
/**
/**
* Format the total bytes as a human-readable string.
*/
function formatSize(totalBytes) {
function formatSize(totalBytes) {
if (totalBytes < 1024) {
return totalBytes + " Bytes";
} else if (totalBytes < 1024 * 1024) {
@@ -303,42 +410,42 @@ export async function refreshViewedBadges(folder) {
} else {
return (totalBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB";
}
}
}
/**
/**
* Build the folder summary HTML using the filtered file list.
*/
function buildFolderSummary(filteredFiles) {
function buildFolderSummary(filteredFiles) {
const totalFiles = filteredFiles.length;
const totalBytes = filteredFiles.reduce((sum, file) => {
return sum + parseSizeToBytes(file.size);
}, 0);
const sizeStr = formatSize(totalBytes);
return `<strong>${t('total_files')}:</strong> ${totalFiles} &nbsp;|&nbsp; <strong>${t('total_size')}:</strong> ${sizeStr}`;
}
}
/**
/**
* Advanced Search toggle
*/
function toggleAdvancedSearch() {
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 = window.imageCache || {};
function cacheImage(imgElem, key) {
window.imageCache[key] = imgElem.src;
}
window.cacheImage = cacheImage;
}
window.cacheImage = cacheImage;
/**
/**
* Fuse.js fuzzy search helper
*/
// --- Lazy Fuse loader (drop-in, CSP-safe, no inline) ---
// --- 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; well fall back */});
lazyLoadFuse().catch(() => {/* ignore; well fall back */ });
}
// Lazy + backward-compatible search
@@ -417,10 +524,10 @@ function searchFiles(searchTerm) {
});
}
/**
/**
* View mode toggle
*/
export function createViewToggleButton() {
export function createViewToggleButton() {
let toggleBtn = document.getElementById("toggleViewBtn");
if (!toggleBtn) {
toggleBtn = document.createElement("button");
@@ -457,20 +564,20 @@ function searchFiles(searchTerm) {
};
return toggleBtn;
}
}
export function formatFolderName(folder) {
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;
// Expose inline DOM helpers.
window.toggleRowSelection = toggleRowSelection;
window.updateRowHighlight = updateRowHighlight;
export async function loadFileList(folderParam) {
export async function loadFileList(folderParam) {
await initOnlyOfficeCaps();
const reqId = ++__fileListReqSeq; // latest call wins
const folder = folderParam || "root";
@@ -702,12 +809,20 @@ function searchFiles(searchTerm) {
}
if (window.showFoldersInList && subfolders.length) {
strip.innerHTML = subfolders.map(sf => `
<div class="folder-item" data-folder="${sf.full}" draggable="true">
<i class="material-icons">folder</i>
<div class="folder-name">${escapeHTML(sf.name)}</div>
strip.innerHTML = subfolders.map(sf => {
return `
<div class="folder-item"
data-folder="${sf.full}"
draggable="true"
style="display:flex;align-items:center;gap:10px;min-width:0;">
<span class="folder-svg" style="flex:0 0 auto;line-height:0;"></span>
<div class="folder-name"
style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
${escapeHTML(sf.name)}
</div>
`).join("");
</div>
`;
}).join("");
strip.style.display = "flex";
strip.querySelectorAll(".folder-item").forEach(el => {
@@ -744,10 +859,18 @@ function searchFiles(searchTerm) {
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)
@@ -763,6 +886,12 @@ function searchFiles(searchTerm) {
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";
}
@@ -788,12 +917,12 @@ function searchFiles(searchTerm) {
fileListContainer.style.visibility = "visible";
}
}
}
}
/**
/**
* Render table view
*/
export function renderFileTable(folder, container, subfolders) {
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);
@@ -821,6 +950,15 @@ function searchFiles(searchTerm) {
const combinedTopHTML = topControlsHTML;
let headerHTML = buildFileTableHeader(sortOrder);
headerHTML = headerHTML.replace(/<table([^>]*)>/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 `<table class="filr-table"${attrs}>`;
});
const startIndex = (currentPage - 1) * itemsPerPageSetting;
const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles);
let rowsHTML = "<tbody>";
@@ -1003,7 +1141,7 @@ function searchFiles(searchTerm) {
});
}
document.querySelectorAll("table.table thead th[data-column]").forEach(cell => {
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);
@@ -1039,16 +1177,16 @@ function searchFiles(searchTerm) {
btn.addEventListener("click", e => e.stopPropagation());
});
bindFileListContextMenu();
refreshViewedBadges(folder).catch(() => {});
}
refreshViewedBadges(folder).catch(() => { });
}
// A helper to compute the max image height based on the current column count.
function getMaxImageHeight() {
// 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) {
export function renderGalleryView(folder, container) {
const fileListContent = container || document.getElementById("fileList");
const searchTerm = (window.currentSearchTerm || "").toLowerCase();
const filteredFiles = searchFiles(searchTerm);
@@ -1363,13 +1501,13 @@ function searchFiles(searchTerm) {
if (window.viewMode === "gallery") renderGalleryView(folder);
else renderFileTable(folder);
};
refreshViewedBadges(folder).catch(() => {});
refreshViewedBadges(folder).catch(() => { });
updateFileActionButtons();
createViewToggleButton();
}
}
// Responsive slider constraints based on screen size.
function updateSliderConstraints() {
// Responsive slider constraints based on screen size.
function updateSliderConstraints() {
const slider = document.getElementById("galleryColumnsSlider");
if (!slider) return;
@@ -1401,12 +1539,12 @@ function searchFiles(searchTerm) {
if (galleryContainer) {
galleryContainer.style.gridTemplateColumns = `repeat(${currentVal}, 1fr)`;
}
}
}
window.addEventListener('load', updateSliderConstraints);
window.addEventListener('resize', updateSliderConstraints);
window.addEventListener('load', updateSliderConstraints);
window.addEventListener('resize', updateSliderConstraints);
export function sortFiles(column, folder) {
export function sortFiles(column, folder) {
if (sortOrder.column === column) {
sortOrder.ascending = !sortOrder.ascending;
} else {
@@ -1434,9 +1572,9 @@ function searchFiles(searchTerm) {
} else {
renderFileTable(folder);
}
}
}
function parseCustomDate(dateStr) {
function parseCustomDate(dateStr) {
dateStr = dateStr.replace(/\s+/g, " ").trim();
const parts = dateStr.split(" ");
if (parts.length !== 2) {
@@ -1469,9 +1607,9 @@ function searchFiles(searchTerm) {
hour = 0;
}
return new Date(year, month - 1, day, hour, minute).getTime();
}
}
export function canEditFile(fileName) {
export function canEditFile(fileName) {
if (!fileName || typeof fileName !== "string") return false;
const dot = fileName.lastIndexOf(".");
if (dot < 0) return false;
@@ -1479,43 +1617,43 @@ function searchFiles(searchTerm) {
// 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",
"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"
"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) {
// 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.changeItemsPerPage = function (newCount) {
window.itemsPerPage = parseInt(newCount, 10);
localStorage.setItem('itemsPerPage', newCount);
window.currentPage = 1;
@@ -1524,11 +1662,11 @@ function searchFiles(searchTerm) {
} else {
renderFileTable(window.currentFolder);
}
};
};
// fileListView.js (bottom)
window.loadFileList = loadFileList;
window.renderFileTable = renderFileTable;
window.renderGalleryView = renderGalleryView;
window.sortFiles = sortFiles;
window.toggleAdvancedSearch = toggleAdvancedSearch;
// fileListView.js (bottom)
window.loadFileList = loadFileList;
window.renderFileTable = renderFileTable;
window.renderGalleryView = renderGalleryView;
window.sortFiles = sortFiles;
window.toggleAdvancedSearch = toggleAdvancedSearch;

View File

@@ -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 `
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<!-- Angled back body -->
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"
style="display:block;shape-rendering:geometricPrecision">
<defs>
<clipPath id="${gid}-clipBack"><path d="M3.5 7.5 H10.5 L12.5 9.5 H20.5
C21.6 9.5 22.5 10.4 22.5 11.5 V19.5
C22.5 20.6 21.6 21.5 20.5 21.5 H5.5
C4.4 21.5 3.5 20.6 3.5 19.5 V9.5
C3.5 8.4 4.4 7.5 5.5 7.5 Z"/></clipPath>
<clipPath id="${gid}-clipFront"><path d="M2.5 10.5 H11.5 L13.5 8.5 H20.5
C21.6 8.5 22.5 9.4 22.5 10.5 V17.5
C22.5 18.6 21.6 19.5 20.5 19.5 H4.5
C3.4 19.5 2.5 18.6 2.5 17.5 V10.5 Z"/></clipPath>
<linearGradient id="${gid}-back" x1="4" y1="20" x2="20" y2="4" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff" stop-opacity="0"/>
<stop offset=".55" stop-color="#fff" stop-opacity=".10"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="${gid}-front" x1="6" y1="19" x2="19" y2="7" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#000" stop-opacity="0"/>
<stop offset="1" stop-color="#000" stop-opacity=".06"/>
</linearGradient>
</defs>
<!-- BACK -->
<g class="back-group" clip-path="url(#${gid}-clipBack)">
<path class="folder-back"
d="M3 7.4h7.6l1.6 1.8H20.3c1.1 0 2 .9 2 2v7.6c0 1.1-.9 2-2 2H5
c-1.1 0-2-.9-2-2V9.4c0-1.1.9-2 2-2z"/>
d="M3.5 7.5 H10.5 L12.5 9.5 H20.5
C21.6 9.5 22.5 10.4 22.5 11.5 V19.5
C22.5 20.6 21.6 21.5 20.5 21.5 H5.5
C4.4 21.5 3.5 20.6 3.5 19.5 V9.5
C3.5 8.4 4.4 7.5 5.5 7.5 Z"/>
<path d="M3.5 7.5 H10.5 L12.5 9.5 H20.5 V21.5 H3.5 Z"
fill="url(#${gid}-back)" pointer-events="none"/>
</g>
${kind === 'paper'
? `
<!-- Paper raised so it peeks above the lip -->
<rect class="paper" x="6.1" y="5.7" width="11.8" height="10.8" rx="1.2"/>
<!-- Bigger fold -->
<path class="paper-fold" d="M18.0 5.7h-3.2l3.2 3.2z"/>
<!-- Content lines -->
<path class="paper-line" d="M7.7 8.2h8.3"/>
<path class="paper-line" d="M7.7 9.8h7.2"/>
<path class="paper-line" d="M7.7 11.3h6.0"/>
`
: ''
}
${kind === 'paper' ? `
<!-- Move the entire paper block up (keep your existing shift if you use it) -->
<g class="paper-group" transform="translate(0, -1.2)">
<rect class="paper" x="6.5" y="6.5" width="11" height="10" rx="1"/>
<!-- Front lip (angled) -->
<!-- Fold aligned to the paper's top-right corner (right edge = 17.5) -->
<path class="paper-fold" d="M17.5 6.5 H15.2 L17.5 9.0 Z"/>
<!-- handwriting dashes -->
<g transform="translate(0, -2.4)">
<path class="paper-ink" d="M9 11.3 H14.2"
stroke="#4da3ff" stroke-width=".9" fill="none"
stroke-linecap="round" stroke-linejoin="round"
paint-order="normal" vector-effect="non-scaling-stroke"/>
<path class="paper-ink" d="M9 12.8 H16.4"
stroke="#4da3ff" stroke-width=".9" fill="none"
stroke-linecap="round" stroke-linejoin="round"
paint-order="normal" vector-effect="non-scaling-stroke"/>
</g>
</g>
` : ``}
<!-- FRONT -->
<g class="front-group" clip-path="url(#${gid}-clipFront)">
<path class="folder-front"
d="M2.3 10.1H10.9l2.0-2.1h7.4c.94 0 1.7.76 1.7 1.7v7.3c0 .94-.76 1.7-1.7 1.7H4
c-.94 0-1.7-.76-1.7-1.7v-6.9z"/>
d="M2.5 10.5 H11.5 L13.5 8.5 H20.5
C21.6 8.5 22.5 9.4 22.5 10.5 V17.5
C22.5 18.6 21.6 19.5 20.5 19.5 H4.5
C3.4 19.5 2.5 18.6 2.5 17.5 V10.5 Z"/>
<path d="M2.5 10.5 H11.5 L13.5 8.5 H20.5 V19.5 H2.5 Z"
fill="url(#${gid}-front)" pointer-events="none"/>
</g>
<!-- Subtle highlight along the lip to add depth -->
<path class="lip-highlight"
d="M3.3 10.2H11.2l1.7-1.8h7.0"
/>
</svg>`;
<!-- Lip highlight -->
<path class="lip-highlight" d="M3 10.5 H11.5 L13.5 8.5 H20.3"/>
</svg>`;
}
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');