From 402f590163fa3f2fc6aa028dd831bb6c3057a78a Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 14 Nov 2025 04:59:58 -0500 Subject: [PATCH] release(v1.9.6): hardened resumable uploads, menu/tag UI polish and hidden temp folders (closes #67) --- CHANGELOG.md | 50 +++ public/css/styles.css | 248 ++++++++++-- public/index.html | 20 + public/js/fileListView.js | 8 +- public/js/fileMenu.js | 338 ++++++++++------ public/js/filePreview.js | 2 +- public/js/fileTags.js | 582 +++++++++++++-------------- public/js/folderManager.js | 213 ++++++---- public/js/upload.js | 373 ++++++++++++++--- src/controllers/UploadController.php | 199 +++++---- src/models/UploadModel.php | 161 +++++--- 11 files changed, 1457 insertions(+), 737 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a793b..4cc95e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## Changes 11/14/2025 (v1.9.6) + +release(v1.9.6): hardened resumable uploads, menu/tag UI polish and hidden temp folders (closes #67) + +- Resumable uploads + - Normalize resumable GET “test chunk” handling in `UploadModel` using `resumableChunkNumber` + `resumableIdentifier`, returning explicit `status: "found"|"not found"`. + - Skip CSRF checks for resumable GET tests in `UploadController`, but keep strict CSRF validation for real POST uploads with soft-fail `csrf_expired` responses. + - Refactor `UploadModel::handleUpload()` for chunked uploads: strict filename validation, safe folder normalization, reliable temp chunk directory creation, and robust merge with clear errors if any chunk is missing. + - Add `UploadModel::removeChunks()` + internal `rrmdir()` to safely clean up `resumable_…` temp folders via a dedicated controller endpoint. + +- Frontend resumable UX & persistence + - Enable `testChunks: true` for Resumable.js and wire GET checks to the new backend status logic. + - Track in-progress resumable files per user in `localStorage` (identifier, filename, folder, size, lastPercent, updatedAt) and show a resumable hint banner inside the Upload card with a dismiss button that clears the hints for that folder. + - Clamp client-side progress to max `99%` until the server confirms success, so aborted tabs still show resumable state instead of “100% done”. + - Improve progress UI: show upload speed, spinner while finalizing, and ensure progress elements exist even for non-standard flows (e.g., submit without prior list build). + - On complete success, clear the progress UI, reset the file input, cancel Resumable’s internal queue, clear draft records for the folder, and re-show the resumable banner only when appropriate. + +- Hiding resumable temp folders + - Hide `resumable_…` folders alongside `trash` and `profile_pics` in: + - Folder tree BFS traversal (child discovery / recursion). + - `listChildren.php` results and child-cache hydration. + - The inline folder strip above the file list (also filtered in `fileListView.js`). + +- Folder manager context menu upgrade + - Replace the old ad-hoc folder context menu with a unified `filr-menu` implementation that mirrors the file context menu styling. + - Add Material icon mapping per action (`create_folder`, `move_folder`, `rename_folder`, `color_folder`, `folder_share`, `delete_folder`) and clamp the menu to viewport with escape/outside-click close behavior. + - Wire the new menu from both tree nodes and breadcrumb links, respecting locked folders and current folder capabilities. + +- File context menu & selection logic + - Define a semantic file context menu in `index.html` (`#fileContextMenu` with `.filr-menu` buttons, icons, `data-action`, and `data-when` visibility flags). + - Rebuild `fileMenu.js` to: + - Derive the current selection from file checkboxes and map back to real `fileData` entries, handling the encoded row IDs. + - Toggle menu items based on selection state (`any`, `one`, `many`, `zip`, `can-edit`) and hide redundant separators. + - Position the menu within the viewport, add ESC/outside-click dismissal, and delegate click handling to call the existing file actions (preview, edit, rename, copy/move/delete/download/extract, tag single/multiple). + +- Tagging system robustness + - Refactor `fileTags.js` to enforce single-instance modals for both single-file and multi-file tagging, preventing duplicate DOM nodes and double bindings. + - Centralize global tag storage (`window.globalTags` + `localStorage`) with shared dropdowns for both modals, including “×” removal for global tags that syncs back to the server. + - Make the tag modals safer and more idempotent (re-usable DOM, Esc and backdrop-to-close, defensive checks on elements) while keeping the existing file row badge rendering and tag-based filtering behavior. + - Localize various tag-related strings where possible and ensure gallery + table views stay in sync after tag changes. + +- Visual polish & theming + - Introduce a shared `--menu-radius` token and apply it across login form, file list container, restore modal, preview modals, OnlyOffice modal, user dropdown menus, and the Upload / Folder Management cards for consistent rounded corners. + - Update header button hover to use the same soft blue hover as other interactive elements and tune card shadows for light vs dark mode. + - Adjust media preview modal background to a darker neutral and tweak `filePreview` panel background fallback (`--panel-bg` / `--bg-color`) for better dark mode contrast. + - Style `.filr-menu` for both file + folder menus with max-height, scrolling, proper separators, and Material icons inheriting text color in light and dark themes. + - Align the user dropdown menu hover/active styles with the new menu hover tokens (`--filr-row-hover-bg`, `--filr-row-outline-hover`) for a consistent interaction feel. + +--- + ## Changes 11/13/2025 (v1.9.5) release(v1.9.5): harden folder tree DOM, add a11y to “Load more”, and guard folder paths diff --git a/public/css/styles.css b/public/css/styles.css index 401f82e..89979be 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -20,7 +20,7 @@ img.logo{ width:50px; height:50px; display:block; } /* belt & suspenders for lo min-height: 40px; /* reserve space */ max-width: 520px; margin: 8px auto 0; - border-radius: 8px; + border-radius: var(--menu-radius); padding: 10px 12px; text-align: left; margin-bottom: 10px; @@ -195,7 +195,7 @@ body { min-height: 40px; /* so the label has room */ } .header-buttons button:hover { - background-color: rgba(255, 255, 255, 0.2); + background-color: rgba(122,179,255,.14); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); color: #fff; }@media (max-width: 600px) { @@ -332,12 +332,12 @@ body { background: white; padding: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border-radius: 4px; + border-radius: var(--menu-radius); }.dark-mode #loginForm { background-color: #2c2c2c; color: #e0e0e0; padding: 20px; - border-radius: 8px; + border-radius: var(--menu-radius); box-shadow: 0 2px 4px rgba(255, 255, 255, 0.2); }.dark-mode #loginForm input { background-color: #333; @@ -370,7 +370,7 @@ body { background: #fff !important; padding: 20px; border: 1px solid #ccc; - border-radius: 4px; + border-radius: var(--menu-radius); }/* Override modal content for dark mode */ .dark-mode #restoreFilesModal .modal-content { background: #2c2c2c !important; @@ -441,7 +441,7 @@ body { background: #fff; padding: 20px; border: 1px solid #ccc; - border-radius: 4px; + border-radius: var(--menu-radius); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); max-width: 400px; width: 90%; @@ -501,7 +501,7 @@ body { background-color: #fff; padding: 10px 20px 20px 20px; border: 1px solid #ccc; - border-radius: 4px !important; + border-radius: var(--menu-radius); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important; z-index: 1100 !important; display: flex !important; @@ -1119,7 +1119,7 @@ body { border: 1px solid #e0e0e0; background: white; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border-radius: 8px; + border-radius: var(--menu-radius); max-width: 100%; padding-bottom: 10px !important; padding-left: 5px !important; @@ -1134,7 +1134,7 @@ body { background-color: #2c2c2c; color: #e0e0e0; border: 1px solid #444; - border-radius: 8px; + border-radius: var(--menu-radius); }#fileListContainer>h2, #fileListContainer>.file-list-actions, #fileListContainer>#fileList { @@ -1393,7 +1393,7 @@ body { max-height: 90vh; background: #fff; padding: 20px !important; - border-radius: 4px; + border-radius: var(--menu-radius); overflow: hidden !important; margin: auto; position: relative; @@ -1706,20 +1706,94 @@ body { transform: translateY(-3px) !important; }#restoreFilesList li label { margin-left: 8px !important; - }.dark-mode #fileContextMenu { - background-color: #2c2c2c !important; - border: 1px solid #555 !important; - color: #e0e0e0 !important; - }.dark-mode #fileContextMenu div { - color: #e0e0e0 !important; - }#folderContextMenu { - font-family: Arial, sans-serif; - font-size: 14px; - }.dark-mode #folderContextMenu { - background-color: #2c2c2c; - border-color: #555; - color: #e0e0e0; - }.drop-target-sidebar { + } + /* ===== File context menu (CSS-only visuals) ===== */ +/* Context menu visual design */ +.filr-menu{ + position: fixed; + z-index: 9999; + min-width: 220px; + max-width: min(320px, 90vw); + height: auto; /* don't stretch */ + max-height: calc(100vh - 16px);/* never exceed viewport; adds scroll if needed */ + overflow: auto; + padding: 6px; + border-radius: 10px; + border: 1px solid var(--ctx-sep, rgba(0,0,0,.08)); + background: var(--ctx-bg, #fff); + color: var(--ctx-fg, #1a1a1a); + box-shadow: + 0 8px 24px rgba(0,0,0,.18), + 0 2px 6px rgba(0,0,0,.10); +} + +/* Light/Dark tokens (inherit from body.dark-mode you already use) */ +.filr-menu { --ctx-bg:#fff; --ctx-fg:#1a1a1a; --ctx-sep:rgba(0,0,0,.08); } +.dark-mode .filr-menu { --ctx-bg:#2c2c2c; --ctx-fg:#eaeaea; --ctx-sep:rgba(255,255,255,.12); } + +/* Items */ +.filr-menu .mi{ + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 10px; + border: 0; + background: transparent; + color: inherit; /* text color follows theme */ + text-align: left; + cursor: pointer; + border-radius: 8px; + user-select: none; +} +.filr-menu .mi:focus{ outline: none; } +.filr-menu .mi:hover, +.filr-menu .mi:focus-visible{ + background: var(--filr-row-hover-bg, rgba(122,179,255,.14)); + box-shadow: inset 0 0 0 1px var(--filr-row-outline-hover, rgba(122,179,255,.35)); +} + +/* Icons = Material Icons font; inherit color so light mode isn't white */ +.filr-menu .mi .material-icons{ + font-size: 18px; + line-height: 1; + color: currentColor; /* critical: icon color matches text (light/dark) */ +} + +/* Labels */ +.filr-menu .mi span{ flex: 1 1 auto; } + +/* Separators */ +.filr-menu .sep{ + height: 1px; + margin: 6px 4px; + background: var(--ctx-sep); +} + +/* Ensure no weird default colors on hover from BS inside the menu */ +.filr-menu, .filr-menu *{ + --bs-body-color: inherit; + --bs-dropdown-link-hover-color: inherit; +} + +.dark-mode #fileContextMenu { + background: #2c2c2c; + border-color: #555; + box-shadow: 0 8px 24px rgba(0,0,0,.45); +} + +#fileContextMenu { z-index: 1039; } /* below typical modal stacks */ +#fileContextMenu[hidden] { display:none !important; pointer-events:none !important; } + +/* Share file-menu visuals with folder menu */ +#folderContextMenu.filr-menu { + max-height: min(calc(100vh - 16px), 420px); + overflow-y: auto; +} + +/* Ensure icons adapt to theme */ +#folderContextMenu .material-icons { color: currentColor; opacity: .9; } + .drop-target-sidebar { display: none; background-color: #f8f9fa; border-right: 2px dashed #1565C0; @@ -1798,13 +1872,35 @@ body { box-shadow: 0 20px 30px rgba(0, 0, 0, 0.3); transition: transform 0.2s ease, box-shadow 0.2s ease; z-index: 10000; - }#uploadCard, - #folderManagementCard { + } + :root { --menu-radius: 12px; } + +.filr-menu { border-radius: var(--menu-radius); } + +/* Cards: match the menu rounding */ +#uploadCard, +#folderManagementCard { transition: transform 0.3s ease, opacity 0.3s ease; - width: 100%; - margin-bottom: 20px; - min-height: 320px; - }#uploadFolderRow.highlight { + width: 100%; + margin-bottom: 20px; + min-height: 320px; + + border-radius: var(--menu-radius); + overflow: hidden; /* ensures children don’t poke past rounded edges */ + border: 1px solid var(--card-border, #e5e7eb); + background: var(--card-bg, #fff); + box-shadow: 0 8px 24px rgba(0,0,0,.08); +} + +/* Dark mode polish */ +body.dark-mode #uploadCard, +body.dark-mode #folderManagementCard { + border-color: var(--card-border-dark, #3a3a3a); + background: var(--card-bg-dark, #2c2c2c); + box-shadow: 0 12px 28px rgba(0,0,0,.35); +} + + #uploadFolderRow.highlight { min-height: 320px; margin-bottom: 20px; }#sidebarDropArea, @@ -1981,8 +2077,11 @@ body { color: #333; }.btn-icon:hover, .btn-icon:focus { - background: rgba(0, 0, 0, 0.1); + background: var(--filr-row-hover-bg) !important; outline: none; + box-shadow: + inset 0 1px 0 var(--filr-row-outline-hover), + inset 0 -1px 0 var(--filr-row-outline-hover); }.dark-mode .btn-icon .material-icons, .dark-mode #searchIcon .material-icons { color: #fff; @@ -1999,7 +2098,7 @@ body { margin-top: 0.25rem; background: var(--bs-body-bg, #fff); border: 1px solid #ccc; - border-radius: 4px; + border-radius: var(--menu-radius); min-width: 150px; box-shadow: 0 2px 6px rgba(0,0,0,0.2); z-index: 1000; @@ -2009,8 +2108,6 @@ body { padding: 0.5rem 0.75rem; cursor: pointer; white-space: nowrap; - }.user-dropdown .user-menu .item:hover { - background: #f5f5f5; }.user-dropdown .dropdown-caret { border-top: 5px solid currentColor; border-left: 5px solid transparent; @@ -2023,8 +2120,6 @@ body { border-color: #444; }.dark-mode .user-dropdown .user-menu .item { color: #e0e0e0; - }.dark-mode .user-dropdown .user-menu .item:hover { - background: rgba(255,255,255,0.1); }.user-dropdown .dropdown-username { margin: 0 8px; font-weight: 500; @@ -2032,6 +2127,46 @@ body { white-space: nowrap; } + +/* container polish to match menus */ +.user-dropdown .user-menu { + border-radius: var(--menu-radius); + overflow: hidden; /* keep hover corners crisp */ + backdrop-filter: saturate(140%) blur(2px); +} + +/* items: same hover treatment as context menu */ +.user-dropdown .user-menu .item { + padding: 0.5rem 0.75rem; + cursor: pointer; + white-space: nowrap; + transition: background-color .12s ease, box-shadow .12s ease; +} + +/* blue hover + inset hairline outline */ +.user-dropdown .user-menu .item:hover { + 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); +} + +/* optional: round the first/last hover edges like the menu */ +.user-dropdown .user-menu .item:first-child:hover { + border-top-left-radius: var(--menu-radius); + border-top-right-radius: var(--menu-radius); +} +.user-dropdown .user-menu .item:last-child:hover { + border-bottom-left-radius: var(--menu-radius); + border-bottom-right-radius: var(--menu-radius); +} + +/* dark mode keeps the same hue; base surface stays dark */ +.dark-mode .user-dropdown .user-menu { + background: #2c2c2c; + border-color: #444; +} + .folder-strip-container { display: flex; padding-top: 0px !important; @@ -2159,7 +2294,7 @@ body.dark-mode .folder-strip-container .folder-item:hover { border-color: #e2e2e2; } /* media modal polish */ -.media-modal { background: var(--panel-bg, #121212); } +.media-modal { background: #2c2c2c; } .media-header-bar .btn { padding: 6px 10px; } .gallery-nav-btn { color: #fff; opacity: 0.85; } .gallery-nav-btn:hover { opacity: 1; transform: scale(1.05); } @@ -2476,4 +2611,41 @@ body.dark-mode #folderTreeContainer .folder-icon .lock-keyhole { display: inline-block; animation: filr-spin .8s linear infinite; } -@keyframes filr-spin { to { transform: rotate(360deg); } } \ No newline at end of file +@keyframes filr-spin { to { transform: rotate(360deg); } } + +/* Resumable upload resume hint banner */ +#resumableDraftBanner.upload-resume-banner { + margin: 8px 12px 12px; /* space from card edges & content below */ +} + +.upload-resume-banner-inner { + display: flex; + align-items: center; + gap: 8px; + + padding: 8px 12px; + border-radius: 10px; + + /* Soft background that should work in light & dark */ + background: rgba(255, 152, 0, 0.06); + border: 1px solid rgba(255, 152, 0, 0.55); + + font-size: 0.9rem; +} + +/* Icon spacing + base style */ +.upload-resume-banner-inner .material-icons { + font-size: 20px; + margin-right: 6px; + vertical-align: middle; + + /* Make sure the icon itself is transparent */ + background-color: transparent; + color: #111; /* dark icon for light mode */ +} + +/* Dark mode: just change the icon color */ +.dark-mode .upload-resume-banner-inner .material-icons { + background-color: transparent; /* stay transparent */ + color: #f5f5f5; /* light icon for dark mode */ +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index a1ac6b0..d584184 100644 --- a/public/index.html +++ b/public/index.html @@ -477,6 +477,26 @@ + +