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 ? (() => {
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 `
${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 += `
${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 `
`;
}
// ─── Render a Glass File Card ─────────────────────────────────
_renderFile(f, path, delay) {
const ext = f.name.split('.').pop().toLowerCase();
return `
`;
}
// ─── 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
Speichern
${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}
Abbrechen
${confirmText}
`;
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 = `
`;
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 ? `Alle leeren ` : ''}
Gelöschte Elemente werden nach 30 Tagen endgültig gelöscht.
${listHtml}
Schließen
`;
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();
});
};
});
}
}