Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |

---

Expand Down
File renamed without changes
Binary file added docs/screenshots/setup 2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/summary 1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/summary 2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/summary 3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/summary 4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/screenshots/summary.png
Binary file not shown.
Binary file modified docs/screenshots/transfer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 74 additions & 1 deletion public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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]) =>
`<div class="detail-row"><span>${esc(label)}</span><span>${s.copiedByType[k]}</span></div>`
).join('');
return rows || `<div class="detail-row"><span>${esc(t('summary.noDetails'))}</span></div>`;
}
if (type === 'skipped') {
if (!s.skippedFiles.length)
return `<div class="detail-row"><span>${esc(t('summary.noDetails'))}</span></div>`;
return s.skippedFiles
.map(f => `<div class="detail-row"><span>${esc(f)}</span></div>`)
.join('');
}
if (type === 'errors') {
if (!s.errorDetails.length)
return `<div class="detail-row"><span>${esc(t('summary.noDetails'))}</span></div>`;
return s.errorDetails
.map(e => `<div class="detail-row detail-row--error"><span>${esc(e.file)}</span><span class="detail-reason">${esc(e.reason)}</span></div>`)
.join('');
}
return '';
}

function esc(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}

// ─── 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';
Expand Down
7 changes: 6 additions & 1 deletion public/i18n/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion public/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion public/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 6 additions & 3 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -194,19 +194,22 @@ <h2 id="transfer-title" data-i18n="transfer.scanning">Scanning phone for files..
<div class="card">
<h2 data-i18n="summary.title">Transfer Complete</h2>
<div class="summary-grid">
<div class="summary-item success">
<div class="summary-item success" data-detail="copied" role="button" tabindex="0">
<span id="sum-copied" class="summary-num">0</span>
<span data-i18n="summary.copied">Files copied</span>
</div>
<div class="summary-item">
<div class="summary-item" data-detail="skipped" role="button" tabindex="0">
<span id="sum-skipped" class="summary-num">0</span>
<span data-i18n="summary.skipped">Files skipped</span>
</div>
<div class="summary-item danger">
<div class="summary-item danger" data-detail="errors" role="button" tabindex="0">
<span id="sum-errors" class="summary-num">0</span>
<span data-i18n="summary.errors">Errors</span>
</div>
</div>
<div id="summary-detail-panel" class="summary-detail-panel" hidden>
<div id="summary-detail-content"></div>
</div>
<button id="btn-ok" class="btn-primary" data-i18n="summary.ok">Back to Start</button>
</div>
</section>
Expand Down
33 changes: 33 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
23 changes: 19 additions & 4 deletions src/services/TransferService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -189,13 +197,19 @@ export class TransferService extends EventEmitter {

if (action === 'skip' || action === 'skip-all') {
summary.skipped++;
summary.skippedFiles.push(filename);
await this.files.deleteFile(tempPath);
continue;
}
}

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 =
Expand All @@ -211,6 +225,7 @@ export class TransferService extends EventEmitter {
}

summary.errors++;
summary.errorDetails.push({ file: filename, reason: msg });
await this.files.deleteFile(tempPath);
}
}
Expand Down
Loading