From 1c97ffb91bb4928022e995709d7e55b19cfd18eb Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 10 Oct 2026 15:50:39 +0200 Subject: feat(android): back up the files of folders the person chooses A Files section takes folders through the system picker (no storage permission), refuses DCIM, Pictures and Movies, skips what the photo backup sends, and runs on the photo backup's own runner into /-drive. Co-Authored-By: Claude Opus 5.5 --- .../src/meshbay_hub/static/drive-sync-settings.js | 122 +++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js (limited to 'packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js new file mode 100644 index 0000000..3fbc65e --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js @@ -0,0 +1,122 @@ +// The files section of the Android Sync page, on the Android application only +// (docs/MESHBAY_DESIGN.md §9.13). Loaded on demand: no other client has it. +// Where the files go is the page's destination section; this one chooses the +// folders, through the system's own picker. + +import { html, useState, useEffect, useCallback } from './vendor/htm-preact.js'; +import { t } from './i18n.js'; +import { ask } from './ask.js'; +import * as platform from './platform.js'; +import { subscribe, runDriveSync } from './drive-sync.js'; +import { failureText } from './phone-sync.js'; + +// What the phone says when it refuses a folder, in the person's words. +const REFUSED = { + media_folder: 'drivesync.refused_media', + whole_storage: 'drivesync.refused_root', +}; + +function refusal(message) { + const code = /Refused: (\w+)/.exec(message || ''); + return code && REFUSED[code[1]] ? t(REFUSED[code[1]]) : message; +} + +function stateLine(st, status) { + switch (st.phase) { + case 'waiting_wifi': return t('photosync.state_waiting_wifi'); + case 'connecting': return t('photosync.state_connecting'); + case 'sending': return t('photosync.state_sending', { done: st.done, total: st.total }); + case 'offline': return t('photosync.state_offline'); + case 'interrupted': return t('photosync.state_interrupted'); + case 'up_to_date': return t('photosync.state_up_to_date'); + default: return status && status.lastCompleted ? t('photosync.state_up_to_date') : ''; + } +} + +export function DriveSyncSection({ destination }) { + const sync = platform.driveSync; + const [status, setStatus] = useState(null); + const [run, setRun] = useState({ phase: 'idle' }); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + + const refresh = useCallback(() => sync.status().then(setStatus).catch(() => {}), [sync]); + useEffect(() => { refresh(); }, [refresh]); + useEffect(() => subscribe((st) => { + setRun(st); + if (st.phase !== 'sending' && st.phase !== 'connecting') refresh(); + }), [refresh]); + + if (!status) return null; + const folders = status.folders || []; + const running = run.phase === 'sending' || run.phase === 'connecting'; + + const act = async (fn) => { + setBusy(true); + setError(''); + try { const next = await fn(); if (next) setStatus(next); } catch (e) { setError(refusal(e.message)); } + finally { setBusy(false); } + }; + + const backUpNow = () => act(async () => { + if (!status.unmetered && !await ask(t('drivesync.metered_confirm'))) return null; + await runDriveSync({ force: true, metered: !status.unmetered }); + return null; + }); + + return html` +
+

${t('drivesync.heading')}

+ ${!status.on && html`

${t('drivesync.intro')}

`} + ${!destination && html`

${t('androidsync.dest_needed')}

`} + ${destination && html` +

${t('drivesync.folders')}

+ ${!folders.length && html`

${t('drivesync.no_folders')}

`} + ${folders.map((f) => html` +
+ ${f.name} + +
+ `)} +

${t('drivesync.folders_hint')}

+ + `} + ${destination && !status.on && html` +
+ +
+ `} + ${destination && status.on && html` +
+ + ${t('phonesync.summary', { group: destination.groupName, dir: status.dir })} + +
+

+ ${t('drivesync.sent_total', { n: status.sent })} + ${' · '} + ${status.lastCompleted + ? t('photosync.last_run', { when: new Date(status.lastCompleted).toLocaleString() }) + : t('photosync.never')} +

+ ${status.failure + ? html`

${failureText(status.failure)}

` + : stateLine(run, status) && html`

${stateLine(run, status)}

`} +
+ + +
+ `} + ${error && html`

${error}

`} +
+ `; +} -- cgit v1.2.3