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:
61
.github/workflows/release-on-version.yml
vendored
61
.github/workflows/release-on-version.yml
vendored
@@ -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: |
|
||||
|
||||
37
CHANGELOG.md
37
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 `<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)
|
||||
|
||||
@@ -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; /* 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 <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 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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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} | <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; we’ll fall back */});
|
||||
lazyLoadFuse().catch(() => {/* ignore; we’ll 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;
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user