diff options
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> + `; +} |