📜
files_copy.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// files.js - File browser module for tile images // Persistent selection across folders using a shared SelectionStore on window. (function(){ // --- Shared selection store (created once) --- (function initSelectionStore(){ if (window.SelectionStore) return; const KEY = 'tile_selection_v1'; const listeners = new Set(); let items = []; try { const raw = localStorage.getItem(KEY); if (raw) items = JSON.parse(raw) || []; } catch {} function save(){ try { localStorage.setItem(KEY, JSON.stringify(items)); } catch {} } function notify(){ listeners.forEach(fn => { try { fn(getAll()); } catch {} }); } function getAll(){ return items.slice(); } function indexOf(url){ return items.findIndex(i => i.url === url); } function has(url){ return indexOf(url) !== -1; } function add(entry){ if (!entry || !entry.url) return false; if (has(entry.url)) return false; items.push({ url: entry.url, name: entry.name || 'image' }); save(); notify(); return true; } function remove(url){ const i = indexOf(url); if (i === -1) return false; items.splice(i, 1); save(); notify(); return true; } function clear(){ if (!items.length) return false; items = []; save(); notify(); return true; } function subscribe(fn){ listeners.add(fn); return ()=>listeners.delete(fn); } window.SelectionStore = { getAll, add, remove, clear, has, indexOf, subscribe }; })(); let currentSub = ''; let currentFolder = ''; // --- Data loading --- async function loadMedia(sub = '') { try { const url = sub ? `media.php?sub=${encodeURIComponent(sub)}` : 'media.php'; const res = await fetch(url, { cache: 'no-store' }); if (!res.ok) throw new Error('Failed to load media'); return await res.json(); } catch (err) { console.error('Media load error:', err); return { breadcrumb: [], folders: [], images: [], error: err.message }; } } // --- Render helpers --- function renderBreadcrumb(crumbs) { if (!crumbs || crumbs.length === 0) return ''; return ` <nav id="files-breadcrumbs" style="display:flex;gap:.5rem;align-items:center;margin-bottom:1rem;flex-wrap:wrap;"> ${crumbs.map((c, i) => ` <button class="breadcrumb-btn" data-sub="${c.sub || ''}" style="background:rgba(30,41,59,0.6);border:1px solid rgba(71,85,105,0.4);color:#f1f5f9; padding:.375rem .75rem;border-radius:.5rem;cursor:pointer;font-size:.875rem;font-weight:500;"> ${c.label} </button> ${i < crumbs.length - 1 ? '<span style="color:#94a3b8;">›</span>' : ''} `).join('')} </nav> `; } function renderSelectionTank(){ return ` <div id="selection-wrap" style="margin-bottom:.75rem;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin-bottom:.5rem;"> <h3 style="color:#94a3b8;font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin:0;">Selected</h3> <div style="display:flex;align-items:center;gap:.5rem;"> <span id="files-selection-count" style="display:none;background:rgba(59,130,246,0.15);border:1px solid rgba(59,130,246,0.35);color:#dbeafe;font-size:.75rem;padding:.35rem .6rem;border-radius:.5rem;"> 0 selected </span> <button id="files-clear-selection" type="button" style="display:none;background:rgba(148,163,184,0.12);border:1px solid rgba(148,163,184,0.25);color:#e2e8f0;font-size:.75rem;padding:.35rem .6rem;border-radius:.5rem;cursor:pointer;"> Clear all </button> </div> </div> <div id="selection-tank" style="display:flex;gap:.5rem;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch; padding:.35rem;border:1px solid rgba(71,85,105,0.35);border-radius:.75rem; background:linear-gradient(135deg, rgba(30,41,59,0.4), rgba(15,23,42,0.4));scrollbar-width:thin;" aria-label="Selected images"></div> </div> `; } function chipEl({url, name}){ const chip = document.createElement('div'); chip.className = 'tank-chip'; chip.dataset.url = url; chip.title = (name || 'image'); chip.style.cssText = ` flex:0 0 auto; position:relative; display:flex; align-items:center; gap:.25rem; padding:.2rem; border:1px solid rgba(148,163,184,0.25); border-radius:.5rem; background:rgba(30,41,59,0.6); `; const thumbWrap = document.createElement('div'); thumbWrap.style.cssText = ` width:36px; height:36px; border-radius:.35rem; overflow:hidden; background:#0a0f1c; display:flex; align-items:center; justify-content:center; `; const img = document.createElement('img'); img.src = url; img.alt = name || 'image'; img.decoding = 'async'; img.loading = 'lazy'; img.referrerPolicy = 'no-referrer'; img.width = 36; img.height = 36; img.style.cssText = 'width:100%; height:100%; object-fit:cover;'; thumbWrap.appendChild(img); const x = document.createElement('button'); x.type = 'button'; x.className = 'tank-remove'; x.setAttribute('aria-label','Remove'); x.textContent = '✕'; x.style.cssText = 'border:none;background:transparent;color:#94a3b8;cursor:pointer;font-size:1rem;line-height:1;'; chip.appendChild(thumbWrap); chip.appendChild(x); return chip; } function rebuildTank(){ const tank = document.getElementById('selection-tank'); if (!tank) return; tank.textContent = ''; window.SelectionStore.getAll().forEach(item => tank.appendChild(chipEl(item))); updateSelectionCount(); } function updateSelectionCount(){ const countEl = document.getElementById('files-selection-count'); const clearBtn = document.getElementById('files-clear-selection'); const len = window.SelectionStore.getAll().length; if (countEl) { countEl.textContent = `${len} selected`; countEl.style.display = len ? 'inline-block' : 'none'; } if (clearBtn) clearBtn.style.display = len ? 'inline-flex' : 'none'; } function renderFolders(folders) { if (!folders || folders.length === 0) return ''; return ` <div style="margin-bottom: .75rem;"> <h3 style="color:#94a3b8;font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin:0 0 .5rem 0;">Folders</h3> <div id="files-folders" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.75rem;"> ${folders.map(f => ` <button class="folder-btn" data-sub="${f.sub}" style="background:linear-gradient(135deg, rgba(30,41,59,0.8), rgba(51,65,85,0.8)); border:1px solid rgba(71,85,105,0.4);border-radius:.75rem;padding:1rem;cursor:pointer;transition:.2s;text-align:center;"> <div style="font-size:2rem;margin-bottom:.5rem;">📁</div> <div style="color:#f1f5f9;font-size:.875rem;font-weight:500;word-break:break-word;">${f.name}</div> </button> `).join('')} </div> </div> `; } function renderImages(images) { if (!images || images.length === 0) { return ` <div style="display:flex; align-items:center; justify-content:space-between; margin-top:1rem;"> <h3 style="color:#94a3b8;font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;">Images</h3> </div> <p style="color:#94a3b8;text-align:center;padding:2rem;">No images found in this folder</p>`; } return ` <div style="display:flex;align-items:center;justify-content:space-between;margin-top:1rem;"> <h3 style="color:#94a3b8;font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;">Images</h3> </div> <div id="files-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:1rem;"> ${images.map(img => ` <div class="image-card" data-url="${img.url}" data-name="${img.name || ''}" style="background:rgba(30,41,59,0.6);border:1px solid rgba(71,85,105,0.4);border-radius:.75rem;overflow:hidden;transition:.2s;cursor:pointer;user-select:none;"> <div style="aspect-ratio:1;background:#0a0f1c;display:flex;align-items:center;justify-content:center;overflow:hidden;"> <img src="${img.url}" alt="${img.name}" loading="lazy" decoding="async" referrerpolicy="no-referrer" style="width:100%;height:100%;object-fit:contain;"> </div> <div style="padding:.75rem;display:flex;align-items:center;justify-content:space-between;gap:.5rem;"> <div style="color:#f1f5f9;font-size:.75rem;font-weight:500;word-break:break-word;">${img.name}</div> <div class="check" aria-hidden="true" style="display:none;font-size:.9rem;">✔︎</div> </div> </div> `).join('')} </div> `; } async function renderContent(sub = '') { const data = await loadMedia(sub); currentSub = sub; // DO NOT clear selection on folder change (persist across folders) // Current folder name (last path segment) const parts = sub.split('/').filter(Boolean); currentFolder = parts.length ? parts[parts.length - 1] : ''; if (data.error) { return `<div style="color:#ef4444;padding:1rem;text-align:center;">Error: ${data.error}</div>`; } return ` <div id="files-content" style="height:100%;overflow-y:auto;padding:.5rem;"> ${renderBreadcrumb(data.breadcrumb)} ${renderSelectionTank()} ${renderFolders(data.folders)} ${renderImages(data.images)} </div> `; } function openCutWithSelection(list, index){ window.dispatchEvent(new CustomEvent('imageSelected', { detail: { images: list, index, folder: currentFolder } })); if (window.AppOverlay) { AppOverlay.close(); setTimeout(() => { const cutBtn = Array.from(document.querySelectorAll('.chip')).find(b => b.textContent.includes('✂️')); if (cutBtn) cutBtn.click(); }, 200); } } // --- Event delegation (bind once) --- function bindDelegates(){ const root = document.body; // React to store changes globally (tank + badges + grid highlights) window.SelectionStore.subscribe(() => { rebuildTank(); // Refresh grid highlights (efficiently) document.querySelectorAll('.image-card').forEach(card=>{ const sel = window.SelectionStore.has(card.dataset.url); if (sel) { card.classList.add('is-selected'); card.style.borderColor = 'rgba(59,130,246,0.9)'; card.style.boxShadow = '0 0 0 2px rgba(59,130,246,0.35) inset'; } else { card.classList.remove('is-selected'); card.style.borderColor = 'rgba(71,85,105,0.4)'; card.style.boxShadow = 'none'; } }); }); // Clear selection root.addEventListener('click', (e)=>{ const btn = e.target.closest('#files-clear-selection'); if (!btn) return; window.SelectionStore.clear(); }); // Breadcrumb nav root.addEventListener('click', async (e)=>{ const btn = e.target.closest('.breadcrumb-btn'); if (!btn) return; const sub = btn.dataset.sub || ''; const container = document.getElementById('files-content'); if (!container) return; container.innerHTML = '<div style="padding:2rem;color:#94a3b8;text-align:center;">Loading…</div>'; const newHtml = await renderContent(sub); container.outerHTML = newHtml; // rebuild tank for this view rebuildTank(); }); // Folder nav root.addEventListener('click', async (e)=>{ const btn = e.target.closest('.folder-btn'); if (!btn) return; const sub = btn.dataset.sub || ''; const container = document.getElementById('files-content'); if (!container) return; container.innerHTML = '<div style="padding:2rem;color:#94a3b8;text-align:center;">Loading…</div>'; const newHtml = await renderContent(sub); container.outerHTML = newHtml; rebuildTank(); }); // Tank: remove / dblclick open / click = show name root.addEventListener('click', (e)=>{ const x = e.target.closest('.tank-remove'); if (!x) return; const chip = x.closest('.tank-chip'); const url = chip?.dataset.url; if (!url) return; window.SelectionStore.remove(url); }); root.addEventListener('dblclick', (e)=>{ const chip = e.target.closest('.tank-chip'); if (!chip) return; const url = chip.dataset.url; const list = window.SelectionStore.getAll(); const idx = Math.max(0, list.findIndex(i => i.url === url)); openCutWithSelection(list, idx); }); root.addEventListener('click', (e)=>{ const chip = e.target.closest('.tank-chip'); if (!chip || e.target.closest('.tank-remove')) return; showMiniToast(chip.title || 'image'); }); // Grid: select / dblclick root.addEventListener('click', (e)=>{ const card = e.target.closest('.image-card'); if (!card) return; const grid = document.getElementById('files-grid'); if (!grid || !grid.contains(card)) return; const url = card.dataset.url; const name = card.dataset.name || 'image'; if (!window.SelectionStore.has(url)) { window.SelectionStore.add({ url, name }); } else { window.SelectionStore.remove(url); } }); root.addEventListener('dblclick', (e)=>{ const card = e.target.closest('.image-card'); if (!card) return; const grid = document.getElementById('files-grid'); if (!grid || !grid.contains(card)) return; const url = card.dataset.url; const name = card.dataset.name || 'image'; if (!window.SelectionStore.has(url)) { window.SelectionStore.add({ url, name }); } const list = window.SelectionStore.getAll(); const index = Math.max(0, list.findIndex(i => i.url === url)); openCutWithSelection(list, index); }); // Keyboard: Enter toggles, Shift+Enter opens Cut root.addEventListener('keydown', (e)=>{ const card = e.target.closest('.image-card'); if (!card) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); card.click(); } else if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); const evt = new MouseEvent('dblclick', { bubbles: true }); card.dispatchEvent(evt); } }); } // Simple toast for names function showMiniToast(text){ let toast = document.getElementById('files-mini-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'files-mini-toast'; toast.style.position = 'fixed'; toast.style.left = '50%'; toast.style.bottom = '16px'; toast.style.transform = 'translateX(-50%)'; toast.style.background = 'rgba(15,23,42,0.95)'; toast.style.border = '1px solid rgba(148,163,184,0.3)'; toast.style.color = '#e2e8f0'; toast.style.padding = '.5rem .75rem'; toast.style.borderRadius = '.5rem'; toast.style.fontSize = '.85rem'; toast.style.zIndex = '99999'; toast.style.boxShadow = '0 10px 20px rgba(0,0,0,.25)'; document.body.appendChild(toast); } toast.textContent = text; toast.style.opacity = '1'; clearTimeout(showMiniToast._t); showMiniToast._t = setTimeout(()=> toast.style.opacity = '0', 900); } // --- Boot --- window.AppItems = window.AppItems || []; const filesIndex = window.AppItems.length; window.AppItems.push({ title: '📁 Files', html: '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#94a3b8;">Loading files...</div>' }); (async function init() { const initialHtml = await renderContent(''); window.AppItems[filesIndex].html = initialHtml; // Bind delegates ONCE bindDelegates(); // Build tank from any saved selection rebuildTank(); })(); })();