1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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({ force: true });
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>
`;
}
|