diff --git a/README.md b/README.md index b24f5f2..e8ab4b1 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,19 @@ The app runs entirely on your local machine. Open it via a start script, a brows | Connect | Setup | |---------|-------| -| ![Connect screen](docs/screenshots/connect.png) | ![Setup screen](docs/screenshots/setup.png) | +| ![Connect screen](docs/screenshots/connect.png) | ![Setup screen – file type and filter options](docs/screenshots/setup%201.png) | -| Transfer | Summary | -|----------|---------| -| ![Transfer screen](docs/screenshots/transfer.png) | ![Summary screen](docs/screenshots/summary.png) | +| Setup (date range active) | Transfer | +|--------------------------|----------| +| ![Setup screen – date range selected with folders chosen](docs/screenshots/setup%202.png) | ![Transfer screen](docs/screenshots/transfer.png) | + +| Summary | Summary – files copied detail | +|---------|-------------------------------| +| ![Summary screen](docs/screenshots/summary%201.png) | ![Summary – copied breakdown by type](docs/screenshots/summary%202.png) | + +| Summary – files skipped detail | Summary – errors detail | +|--------------------------------|-------------------------| +| ![Summary – skipped file list](docs/screenshots/summary%203.png) | ![Summary – error details](docs/screenshots/summary%204.png) | --- diff --git a/docs/screenshots/setup.png b/docs/screenshots/setup 1.png similarity index 100% rename from docs/screenshots/setup.png rename to docs/screenshots/setup 1.png diff --git a/docs/screenshots/setup 2.png b/docs/screenshots/setup 2.png new file mode 100644 index 0000000..1579029 Binary files /dev/null and b/docs/screenshots/setup 2.png differ diff --git a/docs/screenshots/summary 1.png b/docs/screenshots/summary 1.png new file mode 100644 index 0000000..91b7571 Binary files /dev/null and b/docs/screenshots/summary 1.png differ diff --git a/docs/screenshots/summary 2.png b/docs/screenshots/summary 2.png new file mode 100644 index 0000000..1f11289 Binary files /dev/null and b/docs/screenshots/summary 2.png differ diff --git a/docs/screenshots/summary 3.png b/docs/screenshots/summary 3.png new file mode 100644 index 0000000..ebec7d3 Binary files /dev/null and b/docs/screenshots/summary 3.png differ diff --git a/docs/screenshots/summary 4.png b/docs/screenshots/summary 4.png new file mode 100644 index 0000000..319fc0d Binary files /dev/null and b/docs/screenshots/summary 4.png differ diff --git a/docs/screenshots/summary.png b/docs/screenshots/summary.png deleted file mode 100644 index 8bed96b..0000000 Binary files a/docs/screenshots/summary.png and /dev/null differ diff --git a/docs/screenshots/transfer.png b/docs/screenshots/transfer.png index 9e74200..54725e9 100644 Binary files a/docs/screenshots/transfer.png and b/docs/screenshots/transfer.png differ diff --git a/public/app.js b/public/app.js index daf9511..2c85b1e 100644 --- a/public/app.js +++ b/public/app.js @@ -13,6 +13,7 @@ const state = { browserWarningShown: false, homeDir: null, // local user home directory (fetched from API) ws: null, // active WebSocket during transfer + lastSummary: null, // summary payload for detail panel }; const PHONE_DEFAULT_PATH = { @@ -139,6 +140,11 @@ document.addEventListener('DOMContentLoaded', async () => { // Summary OK button document.getElementById('btn-ok').addEventListener('click', resetToStart); + // Summary detail tiles + document.querySelectorAll('[data-detail]').forEach(tile => { + tile.addEventListener('click', () => toggleDetailPanel(tile.dataset.detail)); + }); + showScreen('connect'); startDevicePolling(); }); @@ -592,19 +598,86 @@ async function resolveConflict(action) { } // ─── Summary ─────────────────────────────────────────────────────────────────── -function showSummary({ copied, skipped, errors }) { +function showSummary(msg) { + const { copied, skipped, errors } = msg; document.getElementById('sum-copied').textContent = copied; document.getElementById('sum-skipped').textContent = skipped; document.getElementById('sum-errors').textContent = errors; + state.lastSummary = msg; + stopDeviceWatch(); if (state.ws) { state.ws.close(); state.ws = null; } showScreen('summary'); } +function toggleDetailPanel(type) { + const panel = document.getElementById('summary-detail-panel'); + const content = document.getElementById('summary-detail-content'); + const tiles = document.querySelectorAll('[data-detail]'); + + const alreadyOpen = !panel.hidden && panel.dataset.activeType === type; + + tiles.forEach(tile => tile.classList.remove('active')); + if (alreadyOpen) { + panel.hidden = true; + panel.dataset.activeType = ''; + return; + } + + panel.dataset.activeType = type; + panel.hidden = false; + document.querySelector(`[data-detail="${type}"]`).classList.add('active'); + content.innerHTML = renderDetail(type, state.lastSummary); +} + +function renderDetail(type, s) { + if (type === 'copied') { + const rows = [ + ['images', t('summary.images')], + ['videos', t('summary.videos')], + ['music', t('summary.music')], + ['files', t('summary.files')], + ].filter(([k]) => s.copiedByType[k] > 0) + .map(([k, label]) => + `
${esc(label)}${s.copiedByType[k]}
` + ).join(''); + return rows || `
${esc(t('summary.noDetails'))}
`; + } + if (type === 'skipped') { + if (!s.skippedFiles.length) + return `
${esc(t('summary.noDetails'))}
`; + return s.skippedFiles + .map(f => `
${esc(f)}
`) + .join(''); + } + if (type === 'errors') { + if (!s.errorDetails.length) + return `
${esc(t('summary.noDetails'))}
`; + return s.errorDetails + .map(e => `
${esc(e.file)}${esc(e.reason)}
`) + .join(''); + } + return ''; +} + +function esc(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + // ─── Reset ───────────────────────────────────────────────────────────────────── function resetToStart() { + const detailPanel = document.getElementById('summary-detail-panel'); + detailPanel.hidden = true; + detailPanel.dataset.activeType = ''; + document.querySelectorAll('[data-detail]').forEach(tile => tile.classList.remove('active')); + state.lastSummary = null; + state.sourcePath = null; state.destPath = null; state.fileType = 'allTypes'; diff --git a/public/i18n/da.json b/public/i18n/da.json index a2bcc1d..48bbb5e 100644 --- a/public/i18n/da.json +++ b/public/i18n/da.json @@ -89,7 +89,12 @@ "copied": "Filer kopieret", "skipped": "Filer sprunget over", "errors": "Fejl", - "ok": "Tilbage til start" + "ok": "Tilbage til start", + "images": "Fotos", + "videos": "Videoer", + "music": "Musik", + "files": "Filer", + "noDetails": "Ingen detaljer" }, "browser": { "titlePhone": "Vælg kildemappe på telefonen", diff --git a/public/i18n/en.json b/public/i18n/en.json index 7a800f8..d2498dd 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -89,7 +89,12 @@ "copied": "Files copied", "skipped": "Files skipped", "errors": "Errors", - "ok": "Back to Start" + "ok": "Back to Start", + "images": "Photos", + "videos": "Videos", + "music": "Music", + "files": "Files", + "noDetails": "No details" }, "browser": { "titlePhone": "Select source folder on phone", diff --git a/public/i18n/es.json b/public/i18n/es.json index 008f6bd..5fc2045 100644 --- a/public/i18n/es.json +++ b/public/i18n/es.json @@ -89,7 +89,12 @@ "copied": "Archivos copiados", "skipped": "Archivos omitidos", "errors": "Errores", - "ok": "Volver al inicio" + "ok": "Volver al inicio", + "images": "Fotos", + "videos": "Videos", + "music": "Música", + "files": "Archivos", + "noDetails": "Sin detalles" }, "browser": { "titlePhone": "Seleccionar carpeta de origen en el teléfono", diff --git a/public/index.html b/public/index.html index 812013e..1070873 100644 --- a/public/index.html +++ b/public/index.html @@ -194,19 +194,22 @@

Scanning phone for files..

Transfer Complete

-
+
0 Files copied
-
+
0 Files skipped
-
+
0 Errors
+
diff --git a/public/style.css b/public/style.css index b224e1b..07c1f7a 100644 --- a/public/style.css +++ b/public/style.css @@ -467,7 +467,12 @@ main { font-size: 0.78rem; color: var(--muted); text-align: center; + cursor: pointer; + outline: 2px solid transparent; + transition: outline-color 0.15s, opacity 0.15s; } +.summary-item:hover { opacity: 0.8; } +.summary-item.active { outline-color: var(--accent); } .summary-num { font-size: 2rem; @@ -478,6 +483,34 @@ main { .summary-item.success .summary-num { color: var(--success); } .summary-item.danger .summary-num { color: var(--error); } +.summary-detail-panel { + margin-top: 12px; + padding: 10px 12px; + border-radius: var(--radius-sm); + background: var(--bg); + max-height: 200px; + overflow-y: auto; + font-size: 0.83rem; +} + +.detail-row { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 5px 0; + border-bottom: 1px solid var(--border); + word-break: break-all; +} +.detail-row:last-child { border-bottom: none; } +.detail-row .detail-reason { + color: var(--muted); + word-break: break-word; +} +.detail-row--error { + flex-direction: column; + gap: 2px; +} + /* ─── Modal overlay ──────────────────────────────────────────────────────── */ .modal-overlay { position: fixed; diff --git a/src/services/TransferService.ts b/src/services/TransferService.ts index 58e47d8..c99dd94 100644 --- a/src/services/TransferService.ts +++ b/src/services/TransferService.ts @@ -34,9 +34,12 @@ export interface ScanEvent { } export interface SummaryEvent { - copied: number; - skipped: number; - errors: number; + copied: number; + skipped: number; + errors: number; + copiedByType: { images: number; videos: number; music: number; files: number }; + skippedFiles: string[]; + errorDetails: { file: string; reason: string }[]; } const IMAGE_EXTENSIONS = new Set([ @@ -125,7 +128,12 @@ export class TransferService extends EventEmitter { // Tell the frontend how many files were found before copying begins this.emit('scan', { total: filtered.length }); - const summary: SummaryEvent = { copied: 0, skipped: 0, errors: 0 }; + const summary: SummaryEvent = { + copied: 0, skipped: 0, errors: 0, + copiedByType: { images: 0, videos: 0, music: 0, files: 0 }, + skippedFiles: [], + errorDetails: [], + }; // 'skip-all' or 'overwrite-all' applied to all remaining conflicts let globalAction: 'skip-all' | 'overwrite-all' | null = null; @@ -189,6 +197,7 @@ export class TransferService extends EventEmitter { if (action === 'skip' || action === 'skip-all') { summary.skipped++; + summary.skippedFiles.push(filename); await this.files.deleteFile(tempPath); continue; } @@ -196,6 +205,11 @@ export class TransferService extends EventEmitter { await this.moveToDest(tempPath, destPath); summary.copied++; + const category = + FILE_EXTENSIONS.has(ext) ? 'files' : + MUSIC_EXTENSIONS.has(ext) ? 'music' : + VIDEO_EXTENSIONS.has(ext) ? 'videos' : 'images'; + summary.copiedByType[category]++; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); const deviceGone = @@ -211,6 +225,7 @@ export class TransferService extends EventEmitter { } summary.errors++; + summary.errorDetails.push({ file: filename, reason: msg }); await this.files.deleteFile(tempPath); } }