release(v2.1.0): add header zoom controls, preview tags & modal/dock polish
This commit is contained in:
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,5 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 11/27/2025 (v2.1.0)
|
||||
|
||||
🦃🍂 Happy Thanksgiving. 🥧🍁🍽️
|
||||
|
||||
release(v2.1.0): add header zoom controls, preview tags & modal/dock polish
|
||||
|
||||
- **feat(ux): header zoom controls with persisted app zoom**
|
||||
- Add `zoom.js` with percent-based zoom API (`window.fileriseZoom`) and `--app-zoom` CSS variable.
|
||||
- Wrap the main app in `#appZoomShell` and scale via `transform: scale(var(--app-zoom))` so the whole UI zooms uniformly.
|
||||
- Add header zoom UI (+ / − / 100% reset) and wire it via `data-zoom` buttons.
|
||||
- Persist zoom level in `localStorage` and restore on load.
|
||||
|
||||
- **feat(prefs): user toggle to hide header zoom controls**
|
||||
- Add `hide_header_zoom_controls` i18n key.
|
||||
- Extend the Settings → Display fieldset with “Hide header zoom controls”.
|
||||
- Store preference in `localStorage('hideZoomControls')` and respect it from `appCore.js` when initializing header zoom UI.
|
||||
|
||||
- **feat(preview): show file tags next to preview title**
|
||||
- Add `.title-tags` container in the media viewer header.
|
||||
- When opening a file, look up its `tags` from `fileData` and render them as pill badges beside the filename in the modal top bar.
|
||||
|
||||
- **fix(modals): folder modals always centered above header cards**
|
||||
- Introduce `detachFolderModalsToBody()` in `folderManager.js` and call it on init + before opening create/rename/move/delete modals.
|
||||
- Move those modals under `document.body` with a stable high `z-index`, so they’re not clipped/hidden when the cards live in the header dock.
|
||||
|
||||
- **fix(dnd): header dock & hidden cards container**
|
||||
- Change `#hiddenCardsContainer` from `display:none` to an off-screen absolutely positioned container so card internals (modals/layout) still work while represented as header icons.
|
||||
- Ensure sidebar is always visible as a drop target while dragging (even when panels are collapsed), plus improved highlight & placeholder behavior.
|
||||
|
||||
- **feat(ux): header dock hover/lock polish**
|
||||
- Make header icon buttons share the same hover style as other header buttons.
|
||||
- Add `.is-locked` state so a pinned header icon stays visually “pressed” while its card modal is locked open.
|
||||
|
||||
- **feat(ux): header drop zone and zoom bar layout**
|
||||
- Rework `.header-right` to neatly align zoom controls, header dock, and user buttons.
|
||||
- Add a more flexible `.header-drop-zone` with smooth width/padding transitions and a centered `"Drop Zone"` label when active and empty.
|
||||
- Adjust responsive spacing around zoom controls on smaller screens.
|
||||
|
||||
- **tweak(prefs-modal): improve settings modal sizing**
|
||||
- Increase auth/settings modal `max-height` from 500px to 600px to fit the extra display options without excessive scrolling.
|
||||
|
||||
---
|
||||
|
||||
## Changes 11/26/2025 (v2.0.4)
|
||||
|
||||
release(v2.0.4): harden sessions and align Pro paths with USERS_DIR
|
||||
|
||||
@@ -228,10 +228,7 @@ body{letter-spacing: 0.2px;
|
||||
padding: 9px;}
|
||||
#userDropdownToggle{border-radius: 4px !important;
|
||||
padding: 6px 10px !important;}
|
||||
#headerDropArea.header-drop-zone{display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
min-height: 40px;}
|
||||
|
||||
.header-buttons button:hover{background-color: rgba(122,179,255,.14);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
color: #fff;}
|
||||
@@ -254,6 +251,49 @@ body{letter-spacing: 0.2px;
|
||||
justify-content: center;}
|
||||
}
|
||||
.header-buttons button i{font-size: 24px;}
|
||||
|
||||
.header-zoom-controls .zoom-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.header-zoom-controls .zoom-btn:hover {
|
||||
background-color: rgba(122,179,255,.14);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-zoom-controls .zoom-btn .material-icons {
|
||||
font-size: 16px;
|
||||
}
|
||||
.header-buttons button,
|
||||
#headerDropArea .header-card-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.header-buttons button:not(#userDropdownToggle),
|
||||
#headerDropArea .header-card-icon {
|
||||
border-radius: 50%;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.header-buttons button:hover,
|
||||
#headerDropArea .header-card-icon:hover,
|
||||
#headerDropArea .header-card-icon.is-locked {
|
||||
background-color: rgba(122,179,255,.14) !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark-mode-toggle{background-color: #424242;
|
||||
border: 1px solid #fff;
|
||||
color: #fff;
|
||||
@@ -1384,6 +1424,7 @@ label{font-size: 0.9rem;}
|
||||
}
|
||||
#sidebarDropArea.highlight,
|
||||
#uploadFolderRow.highlight{border: 2px dashed #1565C0;
|
||||
border-radius: var(--menu-radius);
|
||||
background-color: #eef;}
|
||||
.drag-header{cursor: grab;
|
||||
user-select: none;
|
||||
@@ -1488,12 +1529,7 @@ body:not(.dark-mode){--download-spinner-color: #000;}
|
||||
.collapse-btn:hover{background: rgba(0, 0, 0, 0.1);}
|
||||
.toggle-modal-btn:focus,
|
||||
.collapse-btn:focus{outline: none;}
|
||||
.header-drop-zone{width: 66px;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
display: inline-flex;}
|
||||
|
||||
.header-drop-zone.drag-active{border: 2px dashed #1565C0;
|
||||
background-color: #eef;
|
||||
background-color: transparent;
|
||||
@@ -1502,10 +1538,23 @@ body:not(.dark-mode){--download-spinner-color: #000;}
|
||||
.dark-mode .header-drop-zone.drag-active{background-color: #333;
|
||||
border: 2px dashed #555;
|
||||
color: #fff;}
|
||||
.header-drop-zone.drag-active:empty::before{content: "Drop Zone";
|
||||
.header-drop-zone {
|
||||
position: relative; /* so ::before can absolutely position inside */
|
||||
}
|
||||
|
||||
.header-drop-zone.drag-active:empty::before {
|
||||
content: "Drop Zone";
|
||||
position: absolute;
|
||||
inset: 0; /* top/right/bottom/left: 0 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 10px;
|
||||
padding-right: 6px;
|
||||
color: #aaa;}
|
||||
padding-right: 2px;
|
||||
color: #aaa;
|
||||
pointer-events: none; /* optional, so it doesn't block drops */
|
||||
}
|
||||
#fileList tbody tr.clickable-row{-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
@@ -2093,3 +2142,117 @@ body.dark-mode #decreaseFont:not(:disabled):hover,body.dark-mode #increaseFont:n
|
||||
#fileList table.filr-table tbody tr.folder-row>td{padding-top:0!important;padding-bottom:0!important}
|
||||
#fileList table.filr-table tbody tr.folder-row>td.folder-icon-cell{overflow:visible}
|
||||
#fileList tr.folder-row .folder-row-inner,#fileList tr.folder-row .folder-row-name{cursor:inherit}
|
||||
|
||||
:root {
|
||||
--app-zoom: 1; /* 1.0 = 100% */
|
||||
}
|
||||
|
||||
#appZoomShell {
|
||||
transform-origin: top left;
|
||||
transform: scale(var(--app-zoom));
|
||||
/* compensate so scaled content still fills the viewport */
|
||||
width: calc(100% / var(--app-zoom));
|
||||
height: calc(100% / var(--app-zoom));
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
.header-zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-right: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
body:not(.dark-mode) .header-zoom-controls .zoom-vertical,
|
||||
body:not(.dark-mode) .header-zoom-controls .zoom-meta,
|
||||
body:not(.dark-mode) .header-zoom-controls .btn-icon.zoom-btn,
|
||||
body:not(.dark-mode) .header-zoom-controls .btn-icon.zoom-btn .material-icons{
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.header-zoom-controls .zoom-vertical,
|
||||
.header-zoom-controls .zoom-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.header-zoom-controls .btn-icon.zoom-btn {
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Smaller material icons */
|
||||
.header-zoom-controls .btn-icon.zoom-btn .material-icons {
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.zoom-display {
|
||||
min-width: 3ch;
|
||||
text-align: center;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.1;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-right {
|
||||
gap: 8px;
|
||||
}
|
||||
.header-zoom-controls {
|
||||
border-right: none;
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-drop-zone {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
margin-right: 0px;
|
||||
min-width: 0;
|
||||
min-height: 50px;
|
||||
flex: 0 0 auto;
|
||||
transition:
|
||||
min-width 0.15s ease,
|
||||
padding 0.15s ease,
|
||||
background-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.header-card-icon {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-card-icon .material-icons {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.header-drop-zone.drag-active {
|
||||
padding: 0 12px;
|
||||
min-width: 100px;
|
||||
border-radius: 16px;
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
@@ -61,7 +61,27 @@
|
||||
<h1>FileRise</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;">
|
||||
<!-- Zoom controls FIRST on the right -->
|
||||
<div class="header-zoom-controls">
|
||||
<!-- Left stack: + / - -->
|
||||
<div class="zoom-vertical">
|
||||
<button class="btn-icon zoom-btn" data-zoom="in" title="Zoom in">
|
||||
<span class="material-icons">add</span>
|
||||
</button>
|
||||
<button class="btn-icon zoom-btn" data-zoom="out" title="Zoom out">
|
||||
<span class="material-icons">remove</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Right stack: 100% / reset -->
|
||||
<div class="zoom-meta">
|
||||
<span id="zoomDisplay" class="zoom-display">100%</span>
|
||||
<button class="btn-icon zoom-btn" data-zoom="reset" title="Reset zoom">
|
||||
<span class="material-icons">refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-buttons-wrapper" style="display: flex; align-items: center;">
|
||||
|
||||
<div id="headerDropArea" class="header-drop-zone"></div>
|
||||
<div class="header-buttons">
|
||||
@@ -112,6 +132,7 @@
|
||||
<!-- Custom Toast Container -->
|
||||
<div id="customToast"></div>
|
||||
<div id="hiddenCardsContainer" style="display:none;"></div>
|
||||
<div id="appZoomShell">
|
||||
<main id="main" hidden>
|
||||
<div class="row mt-4" id="loginForm">
|
||||
<div class="col-12">
|
||||
@@ -401,7 +422,7 @@
|
||||
</div> <!-- end container-fluid -->
|
||||
</div> <!-- end mainColumn -->
|
||||
</div> <!-- end main-wrapper -->
|
||||
|
||||
</div>
|
||||
<!-- Download Progress Modal -->
|
||||
<div id="downloadProgressModal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="text-align: center; padding: 20px;">
|
||||
|
||||
@@ -93,6 +93,24 @@ export function initializeApp() {
|
||||
// default: false (unchecked)
|
||||
window.showFoldersInList = stored === 'true';
|
||||
|
||||
const zoomWrap = document.querySelector('.header-zoom-controls');
|
||||
if (zoomWrap) {
|
||||
const hideZoom = localStorage.getItem('hideZoomControls') === 'true';
|
||||
if (hideZoom) {
|
||||
zoomWrap.style.display = 'none';
|
||||
zoomWrap.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
zoomWrap.style.display = 'flex';
|
||||
zoomWrap.removeAttribute('aria-hidden');
|
||||
}
|
||||
|
||||
// Always load zoom.js once app is running
|
||||
const QVER = (window.APP_QVER && String(window.APP_QVER)) || '{{APP_QVER}}';
|
||||
import(`/js/zoom.js?v=${encodeURIComponent(QVER)}`).catch(err => {
|
||||
console.warn('[zoom] failed to load zoom.js', err);
|
||||
});
|
||||
}
|
||||
|
||||
// Load public site config early (safe subset)
|
||||
loadAdminConfigFunc();
|
||||
|
||||
@@ -176,6 +194,25 @@ export function initializeApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Zoom controls: load only for logged-in app ----
|
||||
(function loadZoomControls() {
|
||||
const zoomWrap = document.querySelector('.header-zoom-controls');
|
||||
if (!zoomWrap) return;
|
||||
|
||||
// show container (keep CSS default = hidden)
|
||||
zoomWrap.style.display = 'flex';
|
||||
zoomWrap.style.alignItems = 'center';
|
||||
|
||||
try {
|
||||
const QVER = (window.APP_QVER && String(window.APP_QVER)) || '{{APP_QVER}}';
|
||||
import(`/js/zoom.js?v=${encodeURIComponent(QVER)}`)
|
||||
.catch(err => console.warn('[zoom] failed to load:', err));
|
||||
} catch (e) {
|
||||
console.warn('[zoom] load error:', e);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
/* =========================
|
||||
LOGOUT (shared)
|
||||
========================= */
|
||||
|
||||
@@ -195,7 +195,7 @@ export async function openUserPanel() {
|
||||
color: ${isDark ? '#e0e0e0' : '#000'};
|
||||
padding: 20px;
|
||||
max-width: 600px; width:90%;
|
||||
overflow-y: auto; max-height: 500px;
|
||||
overflow-y: auto; max-height: 600px;
|
||||
border: ${isDark ? '1px solid #444' : '1px solid #ccc'};
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: none;
|
||||
@@ -372,7 +372,6 @@ export async function openUserPanel() {
|
||||
|
||||
{
|
||||
const storedStrip = localStorage.getItem('showFoldersInList');
|
||||
// default: unchecked
|
||||
stripCb.checked = storedStrip === null ? false : storedStrip === 'true';
|
||||
}
|
||||
|
||||
@@ -396,11 +395,29 @@ export async function openUserPanel() {
|
||||
}
|
||||
|
||||
inlineLabel.appendChild(inlineCb);
|
||||
// you’ll want a string like this in i18n:
|
||||
// "show_inline_folders": "Show folders inline (above files)"
|
||||
inlineLabel.append(` ${t('show_inline_folders') || 'Show folders inline (above files)'}`);
|
||||
dispFs.appendChild(inlineLabel);
|
||||
|
||||
// 3) Hide header zoom controls
|
||||
const zoomLabel = document.createElement('label');
|
||||
zoomLabel.style.cursor = 'pointer';
|
||||
zoomLabel.style.display = 'block';
|
||||
zoomLabel.style.marginTop = '4px';
|
||||
|
||||
const zoomCb = document.createElement('input');
|
||||
zoomCb.type = 'checkbox';
|
||||
zoomCb.id = 'hideHeaderZoomControls';
|
||||
zoomCb.style.verticalAlign = 'middle';
|
||||
|
||||
{
|
||||
const storedZoom = localStorage.getItem('hideZoomControls');
|
||||
zoomCb.checked = storedZoom === 'true';
|
||||
}
|
||||
|
||||
zoomLabel.appendChild(zoomCb);
|
||||
zoomLabel.append(` ${t('hide_header_zoom_controls') || 'Hide zoom controls in header'}`);
|
||||
dispFs.appendChild(zoomLabel);
|
||||
|
||||
content.appendChild(dispFs);
|
||||
|
||||
// Handlers: toggle + refresh list
|
||||
@@ -420,6 +437,31 @@ export async function openUserPanel() {
|
||||
}
|
||||
});
|
||||
|
||||
// NEW: zoom hide/show handler
|
||||
zoomCb.addEventListener('change', () => {
|
||||
const hideZoom = zoomCb.checked;
|
||||
localStorage.setItem('hideZoomControls', hideZoom ? 'true' : 'false');
|
||||
|
||||
const zoomWrap = document.querySelector('.header-zoom-controls');
|
||||
if (!zoomWrap) return;
|
||||
|
||||
if (hideZoom) {
|
||||
zoomWrap.style.display = 'none';
|
||||
zoomWrap.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
zoomWrap.style.display = 'flex';
|
||||
zoomWrap.removeAttribute('aria-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
inlineCb.addEventListener('change', () => {
|
||||
window.showInlineFolders = inlineCb.checked;
|
||||
localStorage.setItem('showInlineFolders', inlineCb.checked);
|
||||
if (typeof window.loadFileList === 'function') {
|
||||
window.loadFileList(window.currentFolder || 'root');
|
||||
}
|
||||
});
|
||||
|
||||
// wire up image‐input change
|
||||
fileInput.addEventListener('change', async function () {
|
||||
const f = this.files[0];
|
||||
|
||||
@@ -133,7 +133,19 @@ function insertCardInHeader(card) {
|
||||
if (!hidden) {
|
||||
hidden = document.createElement('div');
|
||||
hidden.id = 'hiddenCardsContainer';
|
||||
hidden.style.display = 'none';
|
||||
|
||||
// Park cards off–screen but keep them rendered so modals/layout still work
|
||||
Object.assign(hidden.style, {
|
||||
position: 'absolute',
|
||||
left: '-9999px',
|
||||
top: '0',
|
||||
width: '0',
|
||||
height: '0',
|
||||
overflow: 'visible',
|
||||
pointerEvents: 'none'
|
||||
// **NO** display:none here
|
||||
});
|
||||
|
||||
document.body.appendChild(hidden);
|
||||
}
|
||||
if (card.parentNode?.id !== 'hiddenCardsContainer') hidden.appendChild(card);
|
||||
@@ -212,7 +224,12 @@ function insertCardInHeader(card) {
|
||||
iconButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
isLocked = !isLocked;
|
||||
if (isLocked) showModal(); else hideModal();
|
||||
iconButton.classList.toggle('is-locked', isLocked);
|
||||
if (isLocked) {
|
||||
showModal();
|
||||
} else {
|
||||
hideModal();
|
||||
}
|
||||
});
|
||||
|
||||
host.appendChild(iconButton);
|
||||
@@ -941,7 +958,8 @@ function makeCardDraggable(card) {
|
||||
const sb = getSidebar();
|
||||
if (sb) {
|
||||
sb.classList.add('active', 'highlight');
|
||||
if (!isZonesCollapsed()) sb.style.display = 'block';
|
||||
// Always show sidebar as a drop target while dragging
|
||||
sb.style.display = 'block';
|
||||
ensureSidebarPlaceholder(); // make empty sidebar easy to drop into
|
||||
}
|
||||
|
||||
|
||||
@@ -239,7 +239,26 @@ function ensureMediaModal() {
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
// Ensure a container for tags next to the title (created once)
|
||||
(function ensureTitleTagsContainer() {
|
||||
const titleRow = overlay.querySelector('.media-title');
|
||||
if (!titleRow) return;
|
||||
|
||||
let tagsEl = overlay.querySelector('.title-tags');
|
||||
if (!tagsEl) {
|
||||
tagsEl = document.createElement('div');
|
||||
tagsEl.className = 'title-tags';
|
||||
Object.assign(tagsEl.style, {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
marginLeft: '6px',
|
||||
maxHeight: '32px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
titleRow.appendChild(tagsEl);
|
||||
}
|
||||
})();
|
||||
// theme the close “×” for visibility + hover rules that match your site:
|
||||
const closeBtn = overlay.querySelector("#closeFileModal");
|
||||
function paintCloseBase() {
|
||||
@@ -272,17 +291,46 @@ function ensureMediaModal() {
|
||||
function setTitle(overlay, name) {
|
||||
const textEl = overlay.querySelector('.title-text');
|
||||
const iconEl = overlay.querySelector('.title-icon');
|
||||
const tagsEl = overlay.querySelector('.title-tags');
|
||||
|
||||
// File name + tooltip
|
||||
if (textEl) {
|
||||
textEl.textContent = name || '';
|
||||
textEl.setAttribute('title', name || '');
|
||||
}
|
||||
|
||||
// File type icon
|
||||
if (iconEl) {
|
||||
iconEl.textContent = getIconForFile(name);
|
||||
// keep the icon legible in both themes
|
||||
const dark = document.documentElement.classList.contains('dark-mode');
|
||||
iconEl.style.color = dark ? '#f5f5f5' : '#111111';
|
||||
iconEl.style.opacity = dark ? '0.96' : '0.9';
|
||||
}
|
||||
|
||||
// Tag badges next to the title
|
||||
if (tagsEl) {
|
||||
tagsEl.innerHTML = '';
|
||||
|
||||
let fileObj = null;
|
||||
if (Array.isArray(fileData)) {
|
||||
fileObj = fileData.find(f => f.name === name);
|
||||
}
|
||||
|
||||
if (fileObj && Array.isArray(fileObj.tags) && fileObj.tags.length) {
|
||||
fileObj.tags.forEach(tag => {
|
||||
const badge = document.createElement('span');
|
||||
badge.textContent = tag.name;
|
||||
badge.style.backgroundColor = tag.color || '#444';
|
||||
badge.style.color = '#fff';
|
||||
badge.style.padding = '2px 6px';
|
||||
badge.style.borderRadius = '999px';
|
||||
badge.style.fontSize = '0.75rem';
|
||||
badge.style.lineHeight = '1.2';
|
||||
badge.style.whiteSpace = 'nowrap';
|
||||
tagsEl.appendChild(badge);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Topbar icon (theme-aware) used for image tools + video actions
|
||||
|
||||
@@ -10,6 +10,29 @@ import { fetchWithCsrf } from './auth.js?v={{APP_QVER}}';
|
||||
import { loadCsrfToken } from './appCore.js?v={{APP_QVER}}';
|
||||
|
||||
|
||||
function detachFolderModalsToBody() {
|
||||
const ids = [
|
||||
'createFolderModal',
|
||||
'deleteFolderModal',
|
||||
'moveFolderModal',
|
||||
'renameFolderModal',
|
||||
];
|
||||
|
||||
ids.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
|
||||
if (el.parentNode !== document.body) {
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
|
||||
if (!el.style.zIndex) {
|
||||
el.style.zIndex = '13000';
|
||||
}
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', detachFolderModalsToBody);
|
||||
|
||||
const PAGE_LIMIT = 100;
|
||||
|
||||
/* ----------------------
|
||||
@@ -1711,6 +1734,7 @@ function bindFolderManagerContextMenu() {
|
||||
Rename / Delete / Create hooks
|
||||
----------------------*/
|
||||
export function openRenameFolderModal() {
|
||||
detachFolderModalsToBody();
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
if (!selectedFolder || selectedFolder === "root") { showToast("Please select a valid folder to rename."); return; }
|
||||
const parts = selectedFolder.split("/");
|
||||
@@ -1781,6 +1805,7 @@ if (submitRename) submitRename.addEventListener("click", function (event) {
|
||||
});
|
||||
|
||||
export function openDeleteFolderModal() {
|
||||
detachFolderModalsToBody();
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
if (!selectedFolder || selectedFolder === "root") { showToast("Please select a valid folder to delete."); return; }
|
||||
const msgEl = document.getElementById("deleteFolderMessage");
|
||||
@@ -1823,6 +1848,7 @@ if (confirmDelete) confirmDelete.addEventListener("click", async function () {
|
||||
|
||||
const createBtn = document.getElementById("createFolderBtn");
|
||||
if (createBtn) createBtn.addEventListener("click", function () {
|
||||
detachFolderModalsToBody();
|
||||
const modal = document.getElementById("createFolderModal");
|
||||
const input = document.getElementById("newFolderName");
|
||||
if (modal) modal.style.display = "block";
|
||||
@@ -1885,6 +1911,7 @@ if (submitCreate) submitCreate.addEventListener("click", async () => {
|
||||
Move (modal) + Color carry + State migration as well
|
||||
----------------------*/
|
||||
export function openMoveFolderUI(sourceFolder) {
|
||||
detachFolderModalsToBody();
|
||||
const modal = document.getElementById('moveFolderModal');
|
||||
const targetSel = document.getElementById('moveFolderTarget');
|
||||
if (sourceFolder && sourceFolder !== 'root') window.currentFolder = sourceFolder;
|
||||
|
||||
@@ -337,7 +337,8 @@ const translations = {
|
||||
"size": "Size",
|
||||
"modified": "Modified",
|
||||
"created": "Created",
|
||||
"owner": "Owner"
|
||||
"owner": "Owner",
|
||||
"hide_header_zoom_controls": "Hide header zoom controls"
|
||||
},
|
||||
es: {
|
||||
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
|
||||
|
||||
92
public/js/zoom.js
Normal file
92
public/js/zoom.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// /js/zoom.js
|
||||
(function () {
|
||||
const MIN_PERCENT = 60; // 60%
|
||||
const MAX_PERCENT = 140; // 140%
|
||||
const STEP_PERCENT = 5; // 5%
|
||||
const STORAGE_KEY = 'filerise.appZoomPercent';
|
||||
|
||||
function clampPercent(p) {
|
||||
return Math.max(MIN_PERCENT, Math.min(MAX_PERCENT, p));
|
||||
}
|
||||
|
||||
function updateDisplay(p) {
|
||||
const el = document.getElementById('zoomDisplay');
|
||||
if (el) el.textContent = `${p}%`;
|
||||
}
|
||||
|
||||
function applyZoomPercent(p) {
|
||||
const clamped = clampPercent(p);
|
||||
const scale = clamped / 100;
|
||||
|
||||
document.documentElement.style.setProperty('--app-zoom', String(scale));
|
||||
try { localStorage.setItem(STORAGE_KEY, String(clamped)); } catch {}
|
||||
|
||||
updateDisplay(clamped);
|
||||
return clamped;
|
||||
}
|
||||
|
||||
function getCurrentPercent() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
const n = parseInt(raw, 10);
|
||||
if (Number.isFinite(n) && n > 0) return clampPercent(n);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const v = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--app-zoom')
|
||||
.trim();
|
||||
const n = parseFloat(v);
|
||||
if (Number.isFinite(n) && n > 0) {
|
||||
return clampPercent(Math.round(n * 100));
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
|
||||
// Public-ish API (percent-based)
|
||||
window.fileriseZoom = {
|
||||
in() {
|
||||
const next = getCurrentPercent() + STEP_PERCENT;
|
||||
return applyZoomPercent(next);
|
||||
},
|
||||
out() {
|
||||
const next = getCurrentPercent() - STEP_PERCENT;
|
||||
return applyZoomPercent(next);
|
||||
},
|
||||
reset() {
|
||||
return applyZoomPercent(100);
|
||||
},
|
||||
setPercent(p) {
|
||||
return applyZoomPercent(p);
|
||||
},
|
||||
currentPercent: getCurrentPercent
|
||||
};
|
||||
|
||||
function initZoomUI() {
|
||||
// bind buttons
|
||||
const btns = document.querySelectorAll('.zoom-btn[data-zoom]');
|
||||
btns.forEach(btn => {
|
||||
if (btn.__zoomBound) return;
|
||||
btn.__zoomBound = true;
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const mode = btn.dataset.zoom;
|
||||
if (mode === 'in') window.fileriseZoom.in();
|
||||
else if (mode === 'out') window.fileriseZoom.out();
|
||||
else if (mode === 'reset') window.fileriseZoom.reset();
|
||||
});
|
||||
});
|
||||
|
||||
// apply initial zoom + update display
|
||||
const initial = getCurrentPercent();
|
||||
applyZoomPercent(initial);
|
||||
}
|
||||
|
||||
// Run immediately if DOM is ready, otherwise wait
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initZoomUI, { once: true });
|
||||
} else {
|
||||
initZoomUI();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user