diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-10 15:50:39 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-10 15:50:39 +0200 |
| commit | 1c97ffb91bb4928022e995709d7e55b19cfd18eb (patch) | |
| tree | aaa05d17adcd6db4256c01ba79b27dfc7ec96d70 /packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js | |
| parent | 1521cc1ac0cca7ee8786056f84fa399d5cc268f9 (diff) | |
| download | meshbay-1c97ffb91bb4928022e995709d7e55b19cfd18eb.tar.gz | |
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 <folder>/<account>-drive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/drive-sync-settings.js | 122 |
1 files changed, 122 insertions, 0 deletions
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` + <div class="settings-section"> + <h3 class="settings-heading">${t('drivesync.heading')}</h3> + ${!status.on && html`<p class="settings-hint">${t('drivesync.intro')}</p>`} + ${!destination && html`<p class="settings-hint">${t('androidsync.dest_needed')}</p>`} + ${destination && html` + <p class="settings-label" style="margin-top:6px">${t('drivesync.folders')}</p> + ${!folders.length && html`<p class="settings-hint">${t('drivesync.no_folders')}</p>`} + ${folders.map((f) => html` + <div class="settings-row" key=${f.id}> + <span class="settings-label settings-name">${f.name}</span> + <button class="btn-secondary" disabled=${busy} + onClick=${() => act(() => sync.removeFolder(f.id))}>${t('drivesync.remove')}</button> + </div> + `)} + <p class="settings-hint">${t('drivesync.folders_hint')}</p> + <button class="btn-secondary" disabled=${busy} + onClick=${() => act(() => sync.addFolder())}>${t('drivesync.add_folder')}</button> + `} + ${destination && !status.on && html` + <div class="settings-row" style="gap:8px; justify-content:flex-start; margin-top:8px"> + <button class="admin-btn" disabled=${busy || !folders.length} + onClick=${() => act(async () => { + const next = await sync.configure({}); + runDriveSync(); + return next; + })}>${t('phonesync.turn_on')}</button> + </div> + `} + ${destination && status.on && html` + <div class="settings-row" style="margin-top:8px"> + <span class="settings-label settings-name"> + ${t('phonesync.summary', { group: destination.groupName, dir: status.dir })} + </span> + </div> + <p class="settings-hint"> + ${t('drivesync.sent_total', { n: status.sent })} + ${' · '} + ${status.lastCompleted + ? t('photosync.last_run', { when: new Date(status.lastCompleted).toLocaleString() }) + : t('photosync.never')} + </p> + ${status.failure + ? html`<p class="error-msg">${failureText(status.failure)}</p>` + : stateLine(run, status) && html`<p class="settings-hint">${stateLine(run, status)}</p>`} + <div class="settings-row" style="gap:8px; justify-content:flex-start; flex-wrap:wrap"> + <button class="admin-btn" disabled=${running || busy || !folders.length} + onClick=${backUpNow}>${t('photosync.now')}</button> + <button class="btn-secondary" disabled=${busy} + onClick=${() => act(() => sync.configure(null))}>${t('photosync.turn_off')}</button> + </div> + `} + ${error && html`<p class="error-msg">${error}</p>`} + </div> + `; +} |