Import favorites from a Hot Tub backup
Settings gains a file picker that reads an exported Hot Tub database and merges its favorites into this client's. The file never leaves the device: sqlite.js is a small read-only reader -- header, schema, table b-trees, record decoding, and the overflow pages that real rows here spill onto -- which is all it takes to walk one table, and avoids putting a wasm SQLite behind a CDN fetch. The two sides don't agree on what identifies a video. The app keys one by a hash it computes locally (a 64-hex string); the server, and so this client, keys it as something like "reddit-1rdudss". So the merge matches on normalized URL: entries already saved here are left exactly as they are, keeping the server id that makes a listing card's heart light up, and only genuinely new videos are appended. That means an imported favorite has no server id, so hearts now also match by URL (`data-fav-url` on the card, feed slide and favorites bar). Without it an imported favorite would look unsaved on its own card, and clicking the heart would file a second copy of the same video. Only the columns a favorite needs are read. `allFormats` is deliberately left behind: it holds resolved, signed URLs, which is exactly what favorites must not store (they expire -- see App.favorites.normalize). Tests (scratchpad): the reader checked against Python's sqlite3 on a real 12MB backup -- table list, every table's row count, all 515 favorites with their fields and order, and the 25 longest records byte-for-byte, which is where a wrong overflow split shows up; and the Settings control driven end-to-end, covering the merge, an existing favorite keeping its id, a re-import adding nothing, and a listing card recognising an imported favorite and unfavoriting it cleanly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
This commit is contained in:
@@ -616,7 +616,51 @@ App.ui = App.ui || {};
|
||||
};
|
||||
|
||||
// Expose inline handlers + keyboard shortcuts.
|
||||
// Settings -> Hot Tub Backup: pick an exported database and merge the
|
||||
// favorites out of it. Bound once (unlike the controls in renderMenu, which
|
||||
// are re-assigned on every render) because a file input mid-read must not
|
||||
// have its handler swapped underneath it.
|
||||
App.ui.bindBackupImport = function() {
|
||||
const button = document.getElementById('import-favorites-btn');
|
||||
const input = document.getElementById('import-favorites-file');
|
||||
const status = document.getElementById('import-favorites-status');
|
||||
if (!button || !input) return;
|
||||
|
||||
const say = (message) => { if (status) status.textContent = message; };
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
// Cleared first so picking the same file twice still fires change.
|
||||
input.value = '';
|
||||
input.click();
|
||||
});
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
const file = input.files && input.files[0];
|
||||
if (!file) return;
|
||||
button.disabled = true;
|
||||
say('Reading backup…');
|
||||
App.hottubBackup.importFile(file).then((result) => {
|
||||
if (!result.found) {
|
||||
say('No favorites found in that backup.');
|
||||
} else if (!result.added) {
|
||||
say(`Nothing new: all ${result.found} favorites in that backup are already saved.`);
|
||||
} else {
|
||||
const plural = result.added === 1 ? 'favorite' : 'favorites';
|
||||
const already = result.skipped ? ` ${result.skipped} were already saved.` : '';
|
||||
say(`Imported ${result.added} ${plural}.${already}`);
|
||||
}
|
||||
}).catch((err) => {
|
||||
say('Could not read that file.');
|
||||
App.ui.showError((err && err.message) || 'Could not read that backup.');
|
||||
}).then(() => {
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
App.ui.bindGlobalHandlers = function() {
|
||||
App.ui.bindBackupImport();
|
||||
|
||||
window.toggleDrawer = App.ui.toggleDrawer;
|
||||
window.closeDrawers = App.ui.closeDrawers;
|
||||
window.handleSearch = App.videos.handleSearch;
|
||||
|
||||
Reference in New Issue
Block a user