import { fsService } from '../../services/file-system.js'; import { db } from '../../db/database.js'; import { getIconSvg, ICON_NAMES } from '../../utils/icons.js?v=2'; export class DocumentsView { constructor(containerId) { this.container = document.getElementById(containerId); this.currentPath = ''; // Root is empty string this.viewState = 'grid'; // 'grid' or 'detail' this._longPressTimer = null; this._longPressTarget = null; this._isLongPress = false; this._cachedFiles = []; // cached file list for current path this._filePreviewCache = new Map(); // path → preview HTML } async init() { await db.initPromise; await fsService.init(); await this.render(); } // ─── Ambient title text ───────────────────────────────── _getAmbientTitle() { if (this.currentPath) { return this.currentPath.split('/').pop(); } const greetings = [ 'Was möchtest du\nlernen?', 'Dein Wissen,\norganisiert.', 'Bereit zum\nEntdecken?', 'Deine Dateien,\nimmer dabei.' ]; return greetings[Math.floor(Math.random() * greetings.length)]; } // ─── Main Render ───────────────────────────────── async render() { this.viewState = 'grid'; this.container.innerHTML = `

${this.currentPath ? ` ` : ''} Mein Vault

${this.currentPath ? (() => { const meta = db.getFolderMetadata(this.currentPath) || { emoji: 'folder', color: '#4285f4' }; const iconName = ICON_NAMES.includes(meta.emoji) ? meta.emoji : 'folder'; const color = meta.color || '#4285f4'; const svgIconHtml = getIconSvg(iconName, { class: 'folder-icon-engraved', width: '100%', height: '100%' }); return `
${svgIconHtml}
${this.currentPath.split('/').pop()}
...
`; })() : `
${this._getAmbientTitle()}
`}
${Array(6).fill(0).map(() => `
`).join('')}
`; await this.loadFiles(); this.attachEvents(); } // ─── Load & Render Files ───────────────────────────────── async loadFiles() { const files = await fsService.readDir(this.currentPath); const gridEl = document.getElementById('vault-grid'); if (!gridEl) return; const countEl = document.getElementById('vault-folder-item-count'); if (countEl) { countEl.textContent = `${files.length} Dokument${files.length !== 1 ? 'e' : ''}`; } if (files.length === 0) { gridEl.innerHTML = `
📂
Ordner ist leer
Die KI kann hier automatisch Berichte ablegen.
`; return; } // Sort: Folders first, then alphabetically files.sort((a, b) => { if (a.type === 'directory' && b.type !== 'directory') return -1; if (a.type !== 'directory' && b.type === 'directory') return 1; return a.name.localeCompare(b.name); }); this._cachedFiles = files; // Separate folders and files const folders = files.filter(f => f.type === 'directory'); const documents = files.filter(f => f.type !== 'directory'); // Group documents by date (simulated with random dates for demo if mtime isn't easily formatted, but we'll try to use a static nice date for now like the reference) // To match the reference closely, let's group them by a realistic date string. // In a real app we'd use: new Intl.DateTimeFormat('de-DE', { weekday: 'long', month: 'long', day: 'numeric' }).format(new Date(f.mtime || Date.now())) const groups = {}; documents.forEach((f, i) => { // Create a nice date string. const date = new Date(f.mtime || Date.now()); // Formatting like "Montag, 23. September" (German format) const dateString = new Intl.DateTimeFormat('de-DE', { weekday: 'long', day: 'numeric', month: 'long' }).format(date); if (!groups[dateString]) groups[dateString] = []; groups[dateString].push(f); }); let html = ''; if (folders.length > 0) { html += `
`; html += folders.map((f, idx) => { const path = this.currentPath ? `${this.currentPath}/${f.name}` : f.name; return this._renderFolder(f, path, idx * 40); }).join(''); html += `
`; } if (documents.length > 0) { let docIdx = 0; for (const [dateTitle, groupFiles] of Object.entries(groups)) { html += `
${dateTitle}
${groupFiles.length} Dokument${groupFiles.length !== 1 ? 'e' : ''}
${groupFiles.map(f => { const path = this.currentPath ? `${this.currentPath}/${f.name}` : f.name; const itemHtml = this._renderFile(f, path, docIdx * 40); docIdx++; return itemHtml; }).join('')}
`; } } // Override the #vault-grid CSS to be a block container so our inner grid/timeline works gridEl.style.display = 'block'; gridEl.innerHTML = html; // Load async file previews (text/image) this._loadFilePreviews(files); // Hover shows delete btn gridEl.querySelectorAll('.vault-folder, .vault-file').forEach(item => { item.addEventListener('mouseenter', () => { const btn = item.querySelector('.vault-item-delete'); if (btn) btn.style.opacity = '1'; }); item.addEventListener('mouseleave', () => { const btn = item.querySelector('.vault-item-delete'); if (btn) btn.style.opacity = '0'; }); }); } // ─── Render a Glass Folder ───────────────────────────────── _renderFolder(f, path, delay) { const meta = db.getFolderMetadata(path) || { emoji: 'folder', color: '#4285f4' }; const iconName = ICON_NAMES.includes(meta.emoji) ? meta.emoji : 'folder'; const color = meta.color || '#4285f4'; const svgIconHtml = getIconSvg(iconName, { class: 'folder-icon-engraved', width: '36', height: '36' }); return `
${svgIconHtml}
`; } // ─── Render a Glass File Card ───────────────────────────────── _renderFile(f, path, delay) { const ext = f.name.split('.').pop().toLowerCase(); return `
.${ext}
${f.name}
`; } // ─── Async preview loading ───────────────────────────────── async _loadFilePreviews(files) { for (const f of files) { if (f.type === 'directory') continue; const path = this.currentPath ? `${this.currentPath}/${f.name}` : f.name; const slotId = `preview-${path.replace(/[^a-zA-Z0-9]/g, '_')}`; const slot = document.getElementById(slotId); if (slot) await this._fillPreviewSlot(slot, f.name.split('.').pop().toLowerCase(), path); } } async _fillPreviewSlot(slot, ext, path) { if (!slot.isConnected) return; const isImage = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'tiff'].includes(ext); const isCodeOrText = ['txt', 'md', 'csv', 'tsv', 'json', 'js', 'html', 'css', 'xml', 'yaml', 'yml'].includes(ext); const isAudio = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'].includes(ext); const isVideo = ['mp4', 'mkv', 'avi', 'mov', 'wmv'].includes(ext); const isArchive = ['zip', 'rar', '7z', 'tar', 'gz'].includes(ext); try { if (isImage) { const base64 = await fsService.readBinaryFile(path); if (base64 && slot.isConnected) { slot.innerHTML = `
`; } } else if (isCodeOrText) { const content = await fsService.readFile(path); if (content && slot.isConnected) { const preview = content.substring(0, 300).replace(/${preview}`; } } else if (ext === 'pdf') { slot.innerHTML = `
`; } else if (ext === 'docx') { slot.innerHTML = `
`; } else if (['xls', 'xlsx'].includes(ext)) { slot.innerHTML = `
`; } else if (ext === 'pptx') { slot.innerHTML = `
`; } else if (isAudio) { slot.innerHTML = `
`; } else if (isVideo) { slot.innerHTML = `
`; } else if (isArchive) { slot.innerHTML = `
`; } else if (['exe', 'dmg'].includes(ext)) { slot.innerHTML = `
`; } } catch (e) { // Silently fail for previews } } // ─── Events ───────────────────────────────── attachEvents() { const wrapper = this.container.querySelector('#vault-glass-wrapper'); if (!wrapper) return; // Long-press handling for folders this._setupLongPress(wrapper); const fileInput = this.container.querySelector('#vault-file-upload'); if (fileInput) { fileInput.addEventListener('change', async (e) => { const files = Array.from(e.target.files); if (!files.length) return; let targetPath = this.currentPath; if (this.viewState === 'detail') { const openFolder = document.querySelector('.vault-folder-detail'); if (openFolder) targetPath = openFolder.dataset.path; } await Promise.all(files.map(file => { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = async (event) => { const base64Data = event.target.result.split(',')[1]; if (base64Data) { const savePath = targetPath ? `${targetPath}/${file.name}` : file.name; await fsService.writeBinaryFile(savePath, base64Data); } resolve(); }; reader.readAsDataURL(file); }); })); this.loadFiles(); if (this.viewState === 'detail') { const openFolder = document.querySelector('.vault-folder-detail'); const allItems = document.querySelectorAll('.vault-folder, .vault-file'); if (openFolder) this._cinematicClose(openFolder, allItems); } }); } wrapper.addEventListener('click', async (e) => { // If the long press just ended, ignore the click if (this._isLongPress) { this._isLongPress = false; return; } const folder = e.target.closest('.vault-folder'); const file = e.target.closest('.vault-file'); const deleteBtn = e.target.closest('.vault-item-delete'); const editBtn = e.target.closest('.folder-edit-btn'); const backBtn = e.target.closest('#doc-back-btn'); const newFolderBtn = e.target.closest('#new-folder-btn'); const trashBtn = e.target.closest('#trash-btn'); const uploadBtn = e.target.closest('#upload-btn'); if (uploadBtn) { e.stopPropagation(); const fileIn = this.container.querySelector('#vault-file-upload'); if (fileIn) fileIn.click(); return; } if (trashBtn) { e.stopPropagation(); this.showTrashModal(); return; } if (deleteBtn) { e.stopPropagation(); const name = deleteBtn.dataset.name; const isDir = deleteBtn.dataset.isdir === 'true'; this.showCustomConfirm( isDir ? 'In den Papierkorb verschieben' : 'Dokument in den Papierkorb', `Möchtest du "${name}" wirklich in den Papierkorb verschieben?`, async () => { const path = this.currentPath ? `${this.currentPath}/${name}` : name; await fsService.moveToTrash(path, isDir); this.loadFiles(); }, 'Verschieben' ); return; } if (editBtn) { e.stopPropagation(); this.showFolderPopover(editBtn); return; } if (folder) { e.stopPropagation(); const name = folder.dataset.name; const path = this.currentPath ? `${this.currentPath}/${name}` : name; this._navigateToFolder(path); return; } if (file) { e.stopPropagation(); const name = file.dataset.name; const path = this.currentPath ? `${this.currentPath}/${name}` : name; this._openFile(name, path); return; } if (backBtn) { const parts = this.currentPath.split('/'); parts.pop(); this.currentPath = parts.join('/'); this.render(); return; } if (newFolderBtn) { this.showCustomPrompt('Name des neuen Ordners', async (folderName) => { if (folderName && folderName.trim()) { const cleanName = folderName.trim(); const path = this.currentPath ? `${this.currentPath}/${cleanName}` : cleanName; await fsService.createFolder(path); this.loadFiles(); try { const { folderAgent } = await import('../../ai/folder-agent.js?v=2'); const visuals = await folderAgent.generateVisualsForFolder(cleanName); db.saveFolderMetadata(path, visuals.emoji, visuals.color); this.loadFiles(); } catch (err) { console.error('Background emoji assignment failed:', err); } } }); } }); } // ─── Long-press Setup ───────────────────────────────── _setupLongPress(wrapper) { let pressTimer = null; let pressTarget = null; let pressed = false; const onStart = (e) => { pressed = false; const folder = e.target.closest('.vault-folder'); if (!folder || e.target.closest('.folder-edit-btn') || e.target.closest('.vault-item-delete')) return; pressTarget = folder; pressTimer = setTimeout(() => { pressed = true; this._isLongPress = true; this._cinematicOpen(folder); }, 500); }; const onEnd = () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } if (pressed) { // Prevent the click that follows touchend this._isLongPress = true; setTimeout(() => { this._isLongPress = false; }, 500); } pressed = false; pressTarget = null; }; const onMove = (e) => { // Cancel long-press if finger moves if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } }; wrapper.addEventListener('touchstart', onStart, { passive: true }); wrapper.addEventListener('touchend', onEnd, { passive: true }); wrapper.addEventListener('touchmove', onMove, { passive: true }); wrapper.addEventListener('touchcancel', onEnd, { passive: true }); // Mouse fallback wrapper.addEventListener('mousedown', onStart); wrapper.addEventListener('mouseup', onEnd); wrapper.addEventListener('mouseleave', onEnd); } // ─── Tap Navigation (quick folder open) ───────────────── _navigateToFolder(path) { const grid = document.getElementById('vault-grid'); if (!grid) { this.currentPath = path; this.render(); return; } // Fade out all items const items = grid.querySelectorAll('.vault-folder, .vault-file'); items.forEach(item => item.classList.add('vault-item-exit')); // After fade-out, navigate setTimeout(() => { this.currentPath = path; this.render(); }, 200); } // ─── Cinematic Long-Press Open ───────────────────────────── async _cinematicOpen(folderEl) { // Haptic feedback if (navigator.vibrate) navigator.vibrate(30); const folderName = folderEl.dataset.name; const path = folderEl.dataset.path || (this.currentPath ? `${this.currentPath}/${folderName}` : folderName); const grid = document.getElementById('vault-grid'); if (!grid) return; // 1. Scale up pressed folder folderEl.classList.add('vault-folder-pressing'); folderEl.style.willChange = 'transform, opacity'; // 2. Hide all other items const allItems = grid.querySelectorAll('.vault-folder, .vault-file'); allItems.forEach(item => { if (item !== folderEl) { item.classList.add('vault-item-hide'); item.style.willChange = 'transform, opacity'; } }); // Wait for hide animation await this._wait(300); // 3. Move folder to center and scale const rect = folderEl.getBoundingClientRect(); const targetWidth = window.innerWidth * 0.5; // Calculate top so that the folder sits 90px from the bottom during animation. // .vault-folder-body has aspect-ratio: 1.3, so height is width / 1.3. Plus ~26px for the label. const targetHeight = (targetWidth / 1.3) + 26; const targetTop = window.innerHeight - targetHeight - 90; const targetLeft = window.innerWidth * 0.05; const scale = targetWidth / rect.width; const translateX = targetLeft - rect.left; const translateY = targetTop - rect.top; folderEl._originalRect = rect; // Switch to fixed positioning at current spot folderEl.style.position = 'fixed'; folderEl.style.top = rect.top + 'px'; folderEl.style.left = rect.left + 'px'; folderEl.style.width = rect.width + 'px'; folderEl.style.zIndex = '100'; // Set random rotation const randomRot = (Math.random() > 0.5 ? 1 : -1) * (8 + Math.random() * 8); folderEl.dataset.rot = randomRot; // Combine everything into a single transform for rotation ONLY folderEl.style.setProperty('--folder-rot', `${randomRot}deg`); if (randomRot < 0) folderEl.classList.add('tilt-left'); else folderEl.classList.add('tilt-right'); folderEl.classList.remove('vault-folder-pressing'); folderEl.classList.add('vault-folder-detail'); // Force reflow folderEl.offsetHeight; // Animate to detail position using top, left, width folderEl.style.transition = 'top 0.45s cubic-bezier(.22,.68,0,1.1), left 0.45s cubic-bezier(.22,.68,0,1.1), width 0.45s cubic-bezier(.22,.68,0,1.1), transform 0.45s cubic-bezier(.22,.68,0,1.1)'; folderEl.style.top = targetTop + 'px'; folderEl.style.left = targetLeft + 'px'; folderEl.style.width = targetWidth + 'px'; // After animation, pin to bottom so it resists viewport resizing (e.g. keyboard/address bar) setTimeout(() => { if (this.viewState === 'detail' && folderEl.classList.contains('vault-folder-detail')) { folderEl.style.transition = 'none'; folderEl.style.top = 'auto'; folderEl.style.bottom = '90px'; } }, 450); // 4. Update ambient title const titleEl = document.querySelector('.vault-ambient-title'); if (titleEl) titleEl.textContent = folderName; // 5. Add detail overlay const overlay = document.createElement('div'); overlay.className = 'vault-detail-overlay'; overlay.addEventListener('click', () => this._cinematicClose(folderEl, allItems)); const wrapper = this.container.querySelector('#vault-glass-wrapper'); if (wrapper) { wrapper.appendChild(overlay); } else { document.body.appendChild(overlay); } this.viewState = 'detail'; // 6. Load child items and display them flying out from the folder await this._wait(350); try { const childFiles = await fsService.readDir(path); childFiles.sort((a, b) => { const timeA = a.mtime || a.ctime || 0; const timeB = b.mtime || b.ctime || 0; if (timeA !== timeB) return timeB - timeA; if (a.type === 'directory' && b.type !== 'directory') return -1; if (a.type !== 'directory' && b.type === 'directory') return 1; return a.name.localeCompare(b.name); }); const filesToShow = childFiles.slice(0, 3); if (filesToShow.length > 0) { // Create a container for child items inside the folder body (between front and back) const childGrid = document.createElement('div'); childGrid.id = 'vault-detail-children'; childGrid.style.cssText = ` position: absolute; bottom: 10%; left: 0; width: 100%; height: 0; z-index: 2; overflow: visible; pointer-events: none; `; // Also ensure all parent containers allow overflow folderEl.style.overflow = 'visible'; const fb = folderEl.querySelector('.vault-folder-body'); if (fb) fb.style.overflow = 'visible'; childGrid.innerHTML = filesToShow.map((cf, idx) => { const childPath = `${path}/${cf.name}`; const delay = idx * 60; // Zig-zag math relative to folder const itemWidth = window.innerWidth * 0.4; const isLeft = idx % 2 === 0; // left offset relative to folder body left const leftPx = isLeft ? 10 : (window.innerWidth * 0.4); // bottom offset relative to childGrid bottom const bottomPx = 40 + idx * (itemWidth * 0.6); // Start position for fly-out animation (originating from folder bottom-center) const startX = (targetWidth / 2) - (leftPx + itemWidth / 2); const startY = -bottomPx; // Rotate alternating left/right (e.g. -8deg, 5deg, -4deg) const rotDeg = isLeft ? - (6 + Math.random() * 4) : (4 + Math.random() * 4); const styleStr = `position: absolute; width: ${itemWidth}px; bottom: ${bottomPx}px; left: ${leftPx}px; pointer-events: auto; --start-x: ${startX}px; --start-y: ${startY}px; --rot: ${rotDeg}deg; transform: rotate(${rotDeg}deg);`; let html = cf.type === 'directory' ? this._renderFolder(cf, childPath, 0) : this._renderFile(cf, childPath, 0); // Inject style and classes html = html.replace('class="vault-', `style="${styleStr}" class="vault-`); html = html.replace('vault-item-enter', `vault-child-enter vault-child-item`); html = html.replace('--delay: 0ms', `--delay: ${delay}ms`); return html; }).join(''); const folderBody = folderEl.querySelector('.vault-folder-body'); if (folderBody) { folderBody.appendChild(childGrid); } // Load previews for child files this._loadFilePreviewsInContainer(filesToShow, path, childGrid); // Attach events to child items this._attachDetailChildEvents(childGrid, path, folderEl, allItems); } } catch (err) { console.error('Error loading children in detail view:', err); } } // ─── Cinematic Close (back to grid) ───────────────────────── async _cinematicClose(folderEl, allItems) { // 1. Fly child items back const childGrid = document.getElementById('vault-detail-children'); if (childGrid) { const children = childGrid.querySelectorAll('.vault-folder, .vault-file'); children.forEach((child, idx) => { child.style.setProperty('--delay', `${(children.length - 1 - idx) * 40}ms`); child.classList.remove('vault-child-enter'); child.classList.add('vault-child-exit'); child.style.willChange = 'transform, opacity'; }); await this._wait(200); childGrid.remove(); } // 2. Remove overlay document.querySelectorAll('.vault-detail-overlay').forEach(o => o.remove()); // Record the EXACT current fixed position before doing anything const currentFixedRect = folderEl.getBoundingClientRect(); // 3. Get fresh layout position in case of scroll folderEl.classList.remove('vault-folder-detail'); folderEl.classList.remove('tilt-left', 'tilt-right'); folderEl.style.position = ''; folderEl.style.top = ''; folderEl.style.bottom = ''; // Clear bottom too! folderEl.style.left = ''; folderEl.style.width = ''; folderEl.style.transition = 'none'; const freshRect = folderEl.getBoundingClientRect(); // Put back to fixed detail state instantly folderEl.style.position = 'fixed'; // Restore exact visual position before animating folderEl.style.top = currentFixedRect.top + 'px'; folderEl.style.left = currentFixedRect.left + 'px'; folderEl.style.width = currentFixedRect.width + 'px'; folderEl.classList.add('vault-folder-detail'); const rot = folderEl.dataset.rot || -12; if (rot < 0) folderEl.classList.add('tilt-left'); else folderEl.classList.add('tilt-right'); // Force reflow folderEl.offsetHeight; // Now animate smoothly back to grid rect folderEl.style.transition = 'top 0.45s cubic-bezier(.22,.68,0,1.1), left 0.45s cubic-bezier(.22,.68,0,1.1), width 0.45s cubic-bezier(.22,.68,0,1.1), transform 0.45s cubic-bezier(.22,.68,0,1.1)'; folderEl.style.top = freshRect.top + 'px'; folderEl.style.left = freshRect.left + 'px'; folderEl.style.width = freshRect.width + 'px'; folderEl.classList.remove('vault-folder-detail'); folderEl.classList.remove('tilt-left', 'tilt-right'); await this._wait(450); // 5. Reset folder styles folderEl.style.position = ''; folderEl.style.top = ''; folderEl.style.left = ''; folderEl.style.width = ''; folderEl.style.zIndex = ''; folderEl.style.transition = ''; folderEl.style.willChange = ''; // 5. Bring back other items allItems.forEach((item, idx) => { if (item !== folderEl) { item.classList.remove('vault-item-hide'); item.style.setProperty('--delay', `${idx * 30}ms`); item.classList.add('vault-item-return'); item.style.willChange = 'transform, opacity'; } }); // 6. Restore title const titleEl = document.querySelector('.vault-ambient-title'); if (titleEl) titleEl.textContent = this._getAmbientTitle(); this.viewState = 'grid'; // Cleanup will-change and animation classes after ALL animations finish const maxDelay = allItems.length * 30; setTimeout(() => { allItems.forEach(item => { item.classList.remove('vault-item-return', 'vault-item-enter', 'vault-item-hide'); item.style.willChange = ''; }); }, 550 + maxDelay); } // ─── Attach events to detail-view children ─────────────── _attachDetailChildEvents(childGrid, parentPath, folderEl, allItems) { childGrid.addEventListener('click', async (e) => { const folder = e.target.closest('.vault-folder'); const file = e.target.closest('.vault-file'); const deleteBtn = e.target.closest('.vault-item-delete'); const editBtn = e.target.closest('.folder-edit-btn'); if (deleteBtn) { e.stopPropagation(); const name = deleteBtn.dataset.name; const isDir = deleteBtn.dataset.isdir === 'true'; this.showCustomConfirm( isDir ? 'In den Papierkorb verschieben' : 'Dokument in den Papierkorb', `Möchtest du "${name}" wirklich in den Papierkorb verschieben?`, async () => { const path = `${parentPath}/${name}`; await fsService.moveToTrash(path, isDir); // Refresh: close and reopen await this._cinematicClose(folderEl, allItems); }, 'Verschieben' ); return; } if (editBtn) { e.stopPropagation(); this.showFolderPopover(editBtn); return; } if (folder) { e.stopPropagation(); const name = folder.dataset.name; // Close detail, navigate to sub-folder await this._cinematicClose(folderEl, allItems); this.currentPath = `${parentPath}/${name}`; this.render(); return; } if (file) { e.stopPropagation(); const name = file.dataset.name; const path = `${parentPath}/${name}`; this._openFile(name, path); return; } }); // Hover shows delete btn childGrid.querySelectorAll('.vault-folder, .vault-file').forEach(item => { item.addEventListener('mouseenter', () => { const btn = item.querySelector('.vault-item-delete'); if (btn) btn.style.opacity = '1'; }); item.addEventListener('mouseleave', () => { const btn = item.querySelector('.vault-item-delete'); if (btn) btn.style.opacity = '0'; }); }); } // ─── Load previews for child items in container ───────── async _loadFilePreviewsInContainer(files, parentPath, container) { for (const f of files) { if (f.type === 'directory') continue; const path = `${parentPath}/${f.name}`; const slotId = `preview-${path.replace(/[^a-zA-Z0-9]/g, '_')}`; const slot = container.querySelector(`#${slotId}`); if (slot) await this._fillPreviewSlot(slot, f.name.split('.').pop().toLowerCase(), path); } } // ─── Open File ───────────────────────────────── async _openFile(name, path) { if (name.endsWith('.md') || name.endsWith('.txt') || name.endsWith('.csv')) { const content = await fsService.readFile(path); if (content && window.openPdfModal) { window.openPdfModal(content); } } else if (name.toLowerCase().endsWith('.pdf')) { const base64 = await fsService.readBinaryFile(path); if (base64) { this._openPdfViewer(base64); } } } // ─── PDF Viewer (extracted, unchanged logic) ───────────── _openPdfViewer(base64) { const viewerWrapper = document.createElement('div'); viewerWrapper.id = 'pdf-viewer-wrapper'; viewerWrapper.style.position = 'fixed'; viewerWrapper.style.top = '0'; viewerWrapper.style.left = '0'; viewerWrapper.style.width = '100vw'; viewerWrapper.style.height = '100vh'; viewerWrapper.style.zIndex = '999999'; viewerWrapper.style.backgroundColor = '#1a1a1a'; viewerWrapper.style.overflow = 'auto'; viewerWrapper.style.boxSizing = 'border-box'; viewerWrapper.style.padding = '80px 20px'; const textSelectStyle = document.createElement('style'); textSelectStyle.textContent = ` #pdf-viewer-wrapper, #pdf-viewer-wrapper * { -webkit-user-select: text !important; user-select: text !important; -webkit-touch-callout: default !important; } #pdf-viewer-wrapper .textLayer span { color: rgba(0,0,0,0.01) !important; pointer-events: all !important; cursor: text !important; } #pdf-viewer-wrapper ::selection { background: rgba(0, 120, 255, 0.3) !important; } `; viewerWrapper.appendChild(textSelectStyle); const pdfContainer = document.createElement('div'); pdfContainer.style.display = 'block'; pdfContainer.style.zoom = '1.0'; pdfContainer.style.transformOrigin = '0 0'; viewerWrapper.appendChild(pdfContainer); // Close Button const closeBtn = document.createElement('button'); closeBtn.innerHTML = 'Zurück'; closeBtn.style.position = 'fixed'; closeBtn.style.top = 'env(safe-area-inset-top, 20px)'; closeBtn.style.right = '20px'; closeBtn.style.padding = '8px 16px'; closeBtn.style.background = 'rgba(0,0,0,0.5)'; closeBtn.style.color = '#fff'; closeBtn.style.border = '1px solid rgba(255,255,255,0.2)'; closeBtn.style.borderRadius = '12px'; closeBtn.style.cursor = 'pointer'; closeBtn.style.backdropFilter = 'blur(10px)'; closeBtn.style.fontWeight = '600'; closeBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)'; closeBtn.style.zIndex = '1000000'; // Zoom Controls const zoomControls = document.createElement('div'); zoomControls.style.position = 'fixed'; zoomControls.style.bottom = '20px'; zoomControls.style.left = '50%'; zoomControls.style.transform = 'translateX(-50%)'; zoomControls.style.display = 'flex'; zoomControls.style.gap = '12px'; zoomControls.style.background = 'rgba(30, 30, 35, 0.9)'; zoomControls.style.padding = '8px'; zoomControls.style.borderRadius = '16px'; zoomControls.style.border = '1px solid rgba(255,255,255,0.1)'; zoomControls.style.backdropFilter = 'blur(10px)'; zoomControls.style.zIndex = '1000000'; zoomControls.style.boxShadow = '0 8px 24px rgba(0,0,0,0.5)'; let currentZoom = 1.0; const updateZoom = (newZoom) => { if (newZoom < 1.0) newZoom = 1.0; if (newZoom > 4.0) newZoom = 4.0; currentZoom = newZoom; pdfContainer.style.zoom = currentZoom; }; const zoomToPoint = (delta, centerX, centerY) => { const pointX = (centerX + viewerWrapper.scrollLeft) / currentZoom; const pointY = (centerY + viewerWrapper.scrollTop) / currentZoom; updateZoom(currentZoom + delta); viewerWrapper.scrollLeft = (pointX * currentZoom) - centerX; viewerWrapper.scrollTop = (pointY * currentZoom) - centerY; }; const zoomOutBtn = document.createElement('button'); zoomOutBtn.innerHTML = ''; zoomOutBtn.style.background = 'transparent'; zoomOutBtn.style.border = 'none'; zoomOutBtn.style.color = 'white'; zoomOutBtn.style.cursor = 'pointer'; zoomOutBtn.style.padding = '8px'; zoomOutBtn.onclick = () => zoomToPoint(-0.5, viewerWrapper.clientWidth / 2, viewerWrapper.clientHeight / 2); const zoomInBtn = document.createElement('button'); zoomInBtn.innerHTML = ''; zoomInBtn.style.background = 'transparent'; zoomInBtn.style.border = 'none'; zoomInBtn.style.color = 'white'; zoomInBtn.style.cursor = 'pointer'; zoomInBtn.style.padding = '8px'; zoomInBtn.onclick = () => zoomToPoint(0.5, viewerWrapper.clientWidth / 2, viewerWrapper.clientHeight / 2); zoomControls.appendChild(zoomOutBtn); zoomControls.appendChild(zoomInBtn); // Pinch-To-Zoom let initialDistance = 0; let initialZoom = 1.0; let pinchPoint = { x: 0, y: 0 }; viewerWrapper.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { const t1 = e.touches[0]; const t2 = e.touches[1]; const dx = t1.clientX - t2.clientX; const dy = t1.clientY - t2.clientY; initialDistance = Math.sqrt(dx * dx + dy * dy); initialZoom = currentZoom; const centerX = (t1.clientX + t2.clientX) / 2; const centerY = (t1.clientY + t2.clientY) / 2; pinchPoint.x = (centerX + viewerWrapper.scrollLeft) / currentZoom; pinchPoint.y = (centerY + viewerWrapper.scrollTop) / currentZoom; } }); viewerWrapper.addEventListener('touchmove', (e) => { if (e.touches.length === 2) { e.preventDefault(); const t1 = e.touches[0]; const t2 = e.touches[1]; const dx = t1.clientX - t2.clientX; const dy = t1.clientY - t2.clientY; const dist = Math.sqrt(dx * dx + dy * dy); const newZoom = initialZoom * (dist / initialDistance); updateZoom(newZoom); const centerX = (t1.clientX + t2.clientX) / 2; const centerY = (t1.clientY + t2.clientY) / 2; viewerWrapper.scrollLeft = (pinchPoint.x * currentZoom) - centerX; viewerWrapper.scrollTop = (pinchPoint.y * currentZoom) - centerY; } }, { passive: false }); closeBtn.onclick = () => { if (viewerWrapper.parentNode) document.body.removeChild(viewerWrapper); }; viewerWrapper.appendChild(closeBtn); viewerWrapper.appendChild(zoomControls); document.body.appendChild(viewerWrapper); const loadingText = document.createElement('div'); loadingText.innerHTML = 'Lade PDF...'; loadingText.style.color = '#fff'; loadingText.style.fontFamily = 'sans-serif'; loadingText.style.textAlign = 'center'; pdfContainer.appendChild(loadingText); const loadPdfJs = () => new Promise((resolve, reject) => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css'; document.head.appendChild(link); if (window['pdfjs-dist/build/pdf']) return resolve(window['pdfjs-dist/build/pdf']); const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'; script.onload = () => resolve(window['pdfjs-dist/build/pdf']); script.onerror = reject; document.head.appendChild(script); }); (async () => { try { const pdfjsLib = await loadPdfJs(); pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; const pdfData = atob(base64); const uint8Array = new Uint8Array(pdfData.length); for (let i = 0; i < pdfData.length; i++) { uint8Array[i] = pdfData.charCodeAt(i); } const loadingTask = pdfjsLib.getDocument({ data: uint8Array }); const pdf = await loadingTask.promise; if (loadingText && loadingText.parentNode) { loadingText.parentNode.removeChild(loadingText); } for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { const page = await pdf.getPage(pageNum); const unscaledViewport = page.getViewport({ scale: 1.0 }); const screenWidth = window.innerWidth - 40; const baseScale = screenWidth / unscaledViewport.width; const renderScale = baseScale * 4; const cssViewport = page.getViewport({ scale: baseScale }); const renderViewport = page.getViewport({ scale: renderScale }); const pageContainerDiv = document.createElement('div'); pageContainerDiv.style.position = 'relative'; pageContainerDiv.style.width = cssViewport.width + 'px'; pageContainerDiv.style.height = cssViewport.height + 'px'; pageContainerDiv.style.marginBottom = '24px'; pageContainerDiv.style.boxShadow = '0 8px 24px rgba(0,0,0,0.5)'; pageContainerDiv.style.borderRadius = '8px'; pageContainerDiv.style.backgroundColor = 'white'; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = renderViewport.width; canvas.height = renderViewport.height; canvas.style.width = cssViewport.width + 'px'; canvas.style.height = cssViewport.height + 'px'; canvas.style.display = 'block'; canvas.style.borderRadius = '8px'; pageContainerDiv.appendChild(canvas); const textLayerDiv = document.createElement('div'); textLayerDiv.setAttribute('class', 'textLayer selectable-text'); textLayerDiv.style.width = cssViewport.width + 'px'; textLayerDiv.style.height = cssViewport.height + 'px'; textLayerDiv.style.position = 'absolute'; textLayerDiv.style.top = '0'; textLayerDiv.style.left = '0'; textLayerDiv.style.setProperty('--scale-factor', cssViewport.scale); pageContainerDiv.appendChild(textLayerDiv); pdfContainer.appendChild(pageContainerDiv); const renderTask = page.render({ canvasContext: context, viewport: renderViewport }); await renderTask.promise; const textContent = await page.getTextContent(); pdfjsLib.renderTextLayer({ textContentSource: textContent, container: textLayerDiv, viewport: cssViewport, textDivs: [] }); } } catch (err) { if (loadingText) loadingText.innerHTML = 'Fehler beim Laden der PDF.'; console.error(err); } })(); } // ─── Utility ───────────────────────────────── _wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // ─── Folder Popover (unchanged) ───────────────────────── showFolderPopover(btnEl) { document.querySelectorAll('.folder-popover').forEach(p => p.remove()); const path = btnEl.dataset.path; const currentEmoji = btnEl.dataset.emoji; const currentColor = btnEl.dataset.color || '#6c8ef5'; const popover = document.createElement('div'); popover.className = 'folder-popover'; const rect = btnEl.getBoundingClientRect(); const previewSvg = getIconSvg(currentEmoji, { class: 'folder-icon-engraved', width: '32', height: '32' }); const iconGridHtml = ICON_NAMES.map(name => `
${getIconSvg(name, { width: '20', height: '20' })}
`).join(''); popover.innerHTML = `

Ordner Anpassen

${previewSvg}
Farbton
#
${iconGridHtml}
`; document.body.appendChild(popover); let selectedEmoji = currentEmoji; let selectedColor = currentColor; popover.querySelectorAll('.icon-grid-item').forEach(item => { item.addEventListener('click', () => { popover.querySelectorAll('.icon-grid-item').forEach(i => i.classList.remove('selected')); item.classList.add('selected'); selectedEmoji = item.dataset.name; popover.querySelector('#popover-emoji-preview').innerHTML = getIconSvg(selectedEmoji, { class: 'folder-icon-engraved', width: '32', height: '32' }); }); }); // --- HSV Color Picker Logic --- const _hexToRgbArr = hex => [parseInt(hex.slice(1,3),16), parseInt(hex.slice(3,5),16), parseInt(hex.slice(5,7),16)]; const _rgbToHex = (r,g,b) => '#' + [r,g,b].map(v => Math.max(0,Math.min(255,Math.round(v))).toString(16).padStart(2,'0')).join(''); const _hsvToRgb = (h,s,v) => { s/=100; v/=100; const i=Math.floor(h/60)%6, f=h/60-Math.floor(h/60); const p=v*(1-s), q=v*(1-f*s), t=v*(1-(1-f)*s); const m=[[v,t,p],[q,v,p],[p,v,t],[p,q,v],[t,p,v],[v,p,q]][i]; return m.map(c=>Math.round(c*255)); }; const _rgbToHsv = (r,g,b) => { r/=255; g/=255; b/=255; const max=Math.max(r,g,b), min=Math.min(r,g,b), d=max-min; let h=0, s=max===0?0:d/max, v=max; if(d!==0){ if(max===r) h=((g-b)/d+6)%6; else if(max===g) h=(b-r)/d+2; else h=(r-g)/d+4; h*=60; } return [Math.round(h), Math.round(s*100), Math.round(v*100)]; }; const [initR2, initG2, initB2] = _hexToRgbArr(currentColor); let [curH, curS, curV] = _rgbToHsv(initR2, initG2, initB2); const canvas = popover.querySelector('#color-square'); const hueSlider = popover.querySelector('#hue-slider'); const inputHex = popover.querySelector('#input-hex'); const btnApplyHex = popover.querySelector('#btn-apply-hex'); const preview = popover.querySelector('#popover-color-preview'); hueSlider.value = curH; const _deselectSwatches = () => { popover.querySelectorAll('.color-swatch').forEach(s => { s.style.border='2px solid rgba(255,255,255,0.15)'; s.style.transform=''; }); }; const _drawSquare = () => { const ctx = canvas.getContext('2d'); const w = canvas.width, h = canvas.height; const hGrad = ctx.createLinearGradient(0,0,w,0); hGrad.addColorStop(0, 'white'); hGrad.addColorStop(1, `hsl(${curH},100%,50%)`); ctx.fillStyle = hGrad; ctx.fillRect(0,0,w,h); const dGrad = ctx.createLinearGradient(0,0,0,h); dGrad.addColorStop(0,'transparent'); dGrad.addColorStop(1,'black'); ctx.fillStyle = dGrad; ctx.fillRect(0,0,w,h); const cx=(curS/100)*w, cy=(1-curV/100)*h; ctx.beginPath(); ctx.arc(cx,cy,9,0,Math.PI*2); ctx.strokeStyle='white'; ctx.lineWidth=2.5; ctx.stroke(); ctx.beginPath(); ctx.arc(cx,cy,9,0,Math.PI*2); ctx.strokeStyle='rgba(0,0,0,0.5)'; ctx.lineWidth=1; ctx.stroke(); }; const _updateFromHsv = () => { const [r,g,b] = _hsvToRgb(curH,curS,curV); const hex = _rgbToHex(r,g,b); selectedColor = hex; preview.style.background = hex; inputHex.value = hex.replace('#','').toUpperCase(); _drawSquare(); _deselectSwatches(); }; const _initCanvas = () => { const rect = canvas.getBoundingClientRect(); canvas.width = rect.width || 248; canvas.height = rect.height || 160; _drawSquare(); }; setTimeout(_initCanvas, 60); let _pickingSquare = false; const _pickSquare = (e) => { e.preventDefault(); const rect = canvas.getBoundingClientRect(); const cx = e.touches ? e.touches[0].clientX : e.clientX; const cy = e.touches ? e.touches[0].clientY : e.clientY; curS = Math.round(Math.max(0,Math.min(1,(cx-rect.left)/rect.width))*100); curV = Math.round(Math.max(0,Math.min(1,1-(cy-rect.top)/rect.height))*100); _updateFromHsv(); }; canvas.addEventListener('mousedown', e => { _pickingSquare=true; _pickSquare(e); }); canvas.addEventListener('touchstart', e => { _pickingSquare=true; _pickSquare(e); }, {passive:false}); document.addEventListener('mousemove', e => { if(_pickingSquare) _pickSquare(e); }); document.addEventListener('touchmove', e => { if(_pickingSquare) _pickSquare(e); }, {passive:false}); document.addEventListener('mouseup', () => { _pickingSquare=false; }); document.addEventListener('touchend', () => { _pickingSquare=false; }); hueSlider.addEventListener('input', () => { curH=parseInt(hueSlider.value); _updateFromHsv(); }); btnApplyHex.addEventListener('click', () => { const raw = inputHex.value.trim().replace('#',''); if(/^[0-9a-fA-F]{6}$/.test(raw)) { const hex = '#'+raw.toLowerCase(); const [r,g,b] = _hexToRgbArr(hex); [curH,curS,curV] = _rgbToHsv(r,g,b); hueSlider.value = curH; selectedColor = hex; preview.style.background = hex; _deselectSwatches(); _drawSquare(); } else { inputHex.style.border='1px solid #ff4444'; setTimeout(()=>inputHex.style.border='1px solid rgba(255,255,255,0.15)',1000); } }); inputHex.addEventListener('keydown', e => { if(e.key==='Enter') btnApplyHex.click(); }); popover.querySelectorAll('.color-swatch').forEach(swatch => { swatch.addEventListener('click', () => { _deselectSwatches(); swatch.style.border = '2px solid rgba(255,255,255,0.9)'; swatch.style.transform = 'scale(1.2)'; const hex = swatch.dataset.color; const [r,g,b] = _hexToRgbArr(hex); [curH,curS,curV] = _rgbToHsv(r,g,b); hueSlider.value = curH; selectedColor = hex; preview.style.background = hex; inputHex.value = hex.replace('#','').toUpperCase(); _drawSquare(); }); }); popover.style.position = 'fixed'; popover.style.zIndex = '999999'; // ensure it's above everything let top = rect.bottom + 10; // Use hardcoded popover width (280 + 32 padding) to prevent offsetWidth returning 0 during animation const pWidth = 312; let left = rect.left - (pWidth / 2) + 14; const cw = document.documentElement.clientWidth; const ch = document.documentElement.clientHeight; if (left + pWidth > cw) left = cw - pWidth - 20; if (left < 20) left = 20; // Prevent bottom overflow const pHeight = 250; // approximate height if (top + pHeight > ch) { top = rect.top - pHeight - 10; } popover.style.top = `${top}px`; popover.style.left = `${left}px`; popover.style.touchAction = 'none'; // Prevent scrolling while dragging // --- Drag Support --- let isDragging = false; let dragStartX = 0; let dragStartY = 0; let popoverStartLeft = 0; let popoverStartTop = 0; const dragStart = (e) => { // Don't drag if clicking inside input/button/grid/canvas if (e.target.closest('input, button, .icon-grid-item, canvas')) return; isDragging = true; const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; dragStartX = clientX; dragStartY = clientY; popoverStartLeft = parseFloat(popover.style.left); popoverStartTop = parseFloat(popover.style.top); }; const dragMove = (e) => { if (!isDragging) return; e.preventDefault(); // Stop scrolling const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; const dx = clientX - dragStartX; const dy = clientY - dragStartY; popover.style.left = `${popoverStartLeft + dx}px`; popover.style.top = `${popoverStartTop + dy}px`; }; const dragEnd = () => { isDragging = false; }; popover.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', dragMove); document.addEventListener('mouseup', dragEnd); popover.addEventListener('touchstart', dragStart, { passive: false }); document.addEventListener('touchmove', dragMove, { passive: false }); document.addEventListener('touchend', dragEnd); // -------------------- popover.querySelector('#popover-save').addEventListener('click', () => { const newColor = selectedColor; db.saveFolderMetadata(path, selectedEmoji, newColor); popover.remove(); // Update inline immediately without rebuilding entire grid const folderBtn = document.querySelector(`.folder-edit-btn[data-path="${path}"]`); if (folderBtn) { const folderEl = folderBtn.closest('.vault-folder'); if (folderEl) { folderEl.style.setProperty('--folder-color', newColor); folderEl.dataset.color = newColor; folderEl.dataset.emoji = selectedEmoji; folderBtn.dataset.color = newColor; folderBtn.dataset.emoji = selectedEmoji; // Update icon if in grid or detail const iconEl = folderEl.querySelector('.vault-folder-icon'); if (iconEl) { import('../../utils/icons.js?v=2').then(({ getIconSvg }) => { iconEl.innerHTML = getIconSvg(selectedEmoji, { class: 'folder-icon-engraved', width: '36', height: '36' }); }).catch(() => {}); } } } // Only rebuild if not in detail view to prevent destroying the animating elements if (this.viewState !== 'detail') { this.loadFiles(); } }); const closePopover = (e) => { // Don't close if we were just dragging if (isDragging) return; if (!popover.contains(e.target) && !btnEl.contains(e.target)) { popover.remove(); document.removeEventListener('click', closePopover); document.removeEventListener('mousemove', dragMove); document.removeEventListener('mouseup', dragEnd); document.removeEventListener('touchmove', dragMove); document.removeEventListener('touchend', dragEnd); } }; setTimeout(() => { document.addEventListener('click', closePopover); }, 10); } // ─── Modals (unchanged) ───────────────────────────────── showCustomConfirm(title, text, onConfirm, confirmText = 'Löschen') { const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay'; overlay.innerHTML = `

${title}

${text}

`; document.body.appendChild(overlay); const close = () => { overlay.classList.add('fade-out'); setTimeout(() => overlay.remove(), 200); }; overlay.querySelector('.cancel').onclick = close; overlay.querySelector('.confirm').onclick = () => { onConfirm(); close(); }; } showCustomPrompt(title, onConfirm) { const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay'; overlay.innerHTML = `

${title}

`; document.body.appendChild(overlay); const input = overlay.querySelector('#custom-prompt-input'); input.focus(); const close = () => { overlay.classList.add('fade-out'); setTimeout(() => overlay.remove(), 200); }; overlay.querySelector('.cancel').onclick = close; overlay.querySelector('.confirm').onclick = () => { onConfirm(input.value); close(); }; input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { onConfirm(input.value); close(); } if (e.key === 'Escape') close(); }); } showTrashModal() { let items = []; try { items = db.getTrashItems(); } catch (err) { console.error('Error fetching trash items:', err); alert('Error fetching trash items: ' + err.message); return; } const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay'; let listHtml = ''; if (items.length === 0) { listHtml = '
Der Papierkorb ist leer.
'; } else { listHtml = `
${items.map(item => { const dateStr = new Date(item.deleted_at).toLocaleDateString(); const icon = item.is_dir ? '📁' : '📄'; return `
${icon}
${item.original_path.split('/').pop()} Gelöscht am: ${dateStr}
`; }).join('')}
`; } overlay.innerHTML = `

🗑️ Papierkorb (30 Tage) ${items.length > 0 ? `` : ''}

Gelöschte Elemente werden nach 30 Tagen endgültig gelöscht.

${listHtml}
`; document.body.appendChild(overlay); const close = () => { overlay.classList.add('fade-out'); setTimeout(() => overlay.remove(), 200); this.loadFiles(); }; overlay.querySelector('.close-trash-btn').onclick = close; const emptyBtn = overlay.querySelector('#empty-trash-btn'); if (emptyBtn) { emptyBtn.onclick = async () => { this.showCustomConfirm('Papierkorb leeren', 'Wirklich den gesamten Papierkorb endgültig leeren?', async () => { for (const item of items) { await fsService.deletePath(`.trash/${item.id}`, item.is_dir === 1); db.removeTrashItem(item.id); if (item.is_dir === 1) db.deleteFolderMetadata(item.original_path); } close(); this.showTrashModal(); }); }; } overlay.querySelectorAll('.restore-btn').forEach(btn => { btn.onclick = async () => { const id = btn.dataset.id; btn.style.opacity = '0.5'; await fsService.restoreFromTrash(id); close(); this.showTrashModal(); }; }); overlay.querySelectorAll('.perm-delete-btn').forEach(btn => { btn.onclick = async () => { const id = btn.dataset.id; const isDir = btn.dataset.isdir === '1'; const item = items.find(i => i.id === id); this.showCustomConfirm('Endgültig löschen', 'Dieses Element unwiderruflich löschen?', async () => { btn.style.opacity = '0.5'; await fsService.deletePath(`.trash/${id}`, isDir); db.removeTrashItem(id); if (isDir && item) db.deleteFolderMetadata(item.original_path); close(); this.showTrashModal(); }); }; }); } }