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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@

## Documentation

- [Architecture](docs/architecture.md)
- [Data persistence & migration](docs/persistence.md)
- [Data storage internals](docs/data-storage.md)
- [Tile caching architecture](docs/tile-caching.md)
- [Synology NAS + Tailscale setup](docs/synology-setup.md)
- [Keyboard shortcuts](docs/keyboard-shortcuts.md)
- [Tips & features](docs/tips.md)
- [Architecture](docs/architecture.md)
- [Testing](docs/testing.md)
- [Keyboard shortcuts](docs/keyboard-shortcuts.md)
- [Synology NAS + Tailscale setup](docs/synology-setup.md)
- [Demo modes](docs/demo-mode.md)
- [Testing](docs/testing.md)
- [GitHub workflows](docs/github-workflows.md)

---
Expand Down
7 changes: 3 additions & 4 deletions css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
--accent2:#e8774a;--green:#4ec98a;
--text:#e4e7f2;--muted:#7a829c;--muted2:#555d78;
--radius:12px;--shadow:0 8px 40px rgba(0,0,0,.55);
--font:'Josefin Sans',sans-serif;--font-serif:'Josefin Sans',sans-serif;
--font:'Josefin Sans',sans-serif;
}
html{font-size:19px}
html,body{height:100%;overflow:hidden;font-family:var(--font);background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
Expand All @@ -26,7 +26,7 @@ input,textarea,select{font-family:var(--font)}
.sidebar-header{padding:14px 18px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0}
.logo-mark{width:56px;height:56px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
.logo-mark svg{width:100%;height:100%;fill:var(--accent)}
.logo-text{font-family:var(--font-serif);font-size:1.15rem;font-weight:600;letter-spacing:-.02em}
.logo-text{font-family:var(--font);font-size:1.15rem;font-weight:600;letter-spacing:-.02em}
.logo-text span{color:var(--accent)}

/* 3-tab sidebar */
Expand Down Expand Up @@ -360,13 +360,12 @@ textarea.fi{resize:vertical;min-height:60px;line-height:1.5}
#export-overlay{display:none;position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.75);backdrop-filter:blur(6px);align-items:center;justify-content:center}
#export-overlay.open{display:flex}
.export-overlay-box{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:32px 40px;text-align:center;box-shadow:0 12px 60px rgba(0,0,0,.6);min-width:320px}
.export-overlay-title{font-family:var(--font-serif);font-size:1.2rem;font-weight:600;color:var(--text);margin-bottom:8px}
.export-overlay-title{font-family:var(--font);font-size:1.2rem;font-weight:600;color:var(--text);margin-bottom:8px}
.export-overlay-progress{font-size:.78rem;color:var(--muted);margin-bottom:16px}
.export-overlay-bar-wrap{height:6px;background:var(--surface3);border-radius:3px;overflow:hidden}
.export-overlay-bar{height:100%;width:0%;background:var(--accent);border-radius:3px;transition:width .3s ease}
.export-cancel-btn{margin-top:16px;background:none;border:1px solid var(--border2);color:var(--muted);padding:6px 20px;border-radius:8px;font-size:.75rem;cursor:pointer;transition:color .15s,border-color .15s}
.export-cancel-btn:hover{color:var(--text);border-color:var(--text)}
#tb-export-video.recording{color:var(--accent2)}

/* TOAST */
#toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(60px);background:var(--surface);border:1px solid var(--border);padding:8px 16px;border-radius:28px;font-size:.74rem;box-shadow:var(--shadow);z-index:999;transition:transform .28s ease;pointer-events:none}
Expand Down
84 changes: 0 additions & 84 deletions js/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -598,93 +598,9 @@ async function init() {
}, 500);
// Proactive tile caching — start after a short delay so it doesn't compete with initial load
setTimeout(() => cacheMapTiles(), 10000);
// One-time migration: convert PNG thumbnails to JPEG (remove once both machines have run this)
setTimeout(() => _migrateThumbsToWebP(), 2000);
// One-time migration: move full-size base64 images from IndexedDB to disk
// After this, IndexedDB holds only metadata + thumbnails (~50KB per photo vs ~4MB)
setTimeout(() => _migrateImagesToDisk(), 3000);
}


// ═══════════════════════════════════════
// ONE-TIME MIGRATION: PNG → JPEG thumbnails
// Remove this function and its setTimeout call in init() once both machines have run it.
// ═══════════════════════════════════════
async function _migrateThumbsToWebP() {
let migrated = 0;
for (const p of photos) {
if (!p.thumbUrl || p.thumbUrl.startsWith('data:image/jpeg')) continue;
try {
const img = new Image();
await new Promise((res, rej) => { img.onload = res; img.onerror = rej; img.src = p.thumbUrl; });
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
canvas.getContext('2d').drawImage(img, 0, 0);
p.thumbUrl = canvas.toDataURL('image/jpeg', 0.7);
await dbPut('photos', p);
// Upload the new WebP thumbnail to disk (bypasses _savedPhotoDisk guard)
if (_autoSaveAvailable) {
await fetch(`/api/photos/${p.id}/thumb`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dataUrl: p.thumbUrl })
});
}
migrated++;
} catch (_) { /* skip failures */ }
}
if (migrated) {
rebuildPhotoList();
buildTimeline();
scheduleAutoSave();
}
}

// ═══════════════════════════════════════
// ONE-TIME MIGRATION: Move base64 images from IndexedDB to disk
// After this migration, IndexedDB holds metadata + thumbnails only (~50KB/photo vs ~4MB).
// Full-size images live in matrix-photos/ and are loaded on-demand by the lightbox.
// ═══════════════════════════════════════
async function _migrateImagesToDisk() {
if (!_autoSaveAvailable) return; // serve.py must be running to save files
// Find photos still storing base64 full-size images in IndexedDB
const needMigration = photos.filter(p => p.dataUrl && p.dataUrl.startsWith('data:'));
if (!needMigration.length) return;

showToast(`Migrating ${needMigration.length} photo${needMigration.length !== 1 ? 's' : ''} to disk…`, 'info');
let migrated = 0;

for (const p of needMigration) {
try {
const ext = (p.dataUrl.match(/data:image\/(\w+)/) || [])[1] === 'png' ? 'png' : 'jpg';
const filePath = `matrix-photos/${p.id}.${ext}`;
// Check if the image already exists on disk (from prior auto-save)
const exists = await fetch(`/${filePath}`, { method: 'HEAD' }).then(r => r.ok).catch(() => false);
if (!exists) {
// Image not on disk yet — save it now from the base64 in IndexedDB
await fetch(`/api/photos/${p.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dataUrl: p.dataUrl })
});
}
// Replace base64 with the file path reference in memory + IndexedDB
p.dataUrl = filePath;
await dbPut('photos', p);
migrated++;
} catch (err) {
// Skip failures — photo retains its base64 and can retry next load
console.warn(`Migration failed for ${p.id}:`, err.message);
}
}

if (migrated) {
scheduleAutoSave();
showToast(`Migrated ${migrated} photo${migrated !== 1 ? 's' : ''} to disk ✓`, 'success');
}
}

// ═══════════════════════════════════════
// OFFLINE SUPPORT
// ═══════════════════════════════════════
Expand Down
20 changes: 3 additions & 17 deletions js/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,13 @@ async function exportVideo() {
if (_exporting || _playbackActive) return;
if (_mapStyle === 'globe') { showToast('Export Video is not available in Globe mode', 'error'); return; }

// Build stops (same logic as startPlayback)
const dated = photos.filter(p => p.lat !== null && p.date)
.sort((a, b) => photoSortKey(a) < photoSortKey(b) ? -1 : 1);
const stops = buildPlaybackStops();

if (dated.length < 2) {
if (stops.length < 2) {
showToast('Need at least 2 dated pinned photos to export', 'error');
return;
}

const stops = [];
const seen = new Set();
for (const p of dated) {
const k = locKey(p);
if (seen.has(k)) {
stops.find(s => s.key === k).photoIds.push(p.id);
} else {
seen.add(k);
stops.push({ key: k, lat: p.lat, lng: p.lng, photoIds: [p.id] });
}
}

// Show overlay
_exporting = true;
document.getElementById('export-overlay').classList.add('open');
Expand Down Expand Up @@ -366,7 +352,7 @@ async function exportVideo() {

// Cleanup
exportCleanup();
showToast(`Video exported (${(blob.size / 1024 / 1024).toFixed(1)} MB)`, 'success');
showToast('Video exported', 'success');
}

function exportCleanup() {
Expand Down
Loading
Loading