aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js173
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js26
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js180
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js71
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js97
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js71
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js17
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js85
18 files changed, 535 insertions, 422 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js b/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js
index 5145515..b4d85d0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/android-sync-page.js
@@ -1,21 +1,186 @@
// The Android Sync page: what this phone sends to its groups, on the Android
// application only (docs/MESHBAY_DESIGN.md §9.12, §9.13). Loaded on demand,
// like the sections it holds: no other client has it.
+//
+// Where everything goes is chosen once, at the top, and every section below
+// sends into its own folder there.
-import { html } from './vendor/htm-preact.js';
+import { html, useState, useEffect } from './vendor/htm-preact.js';
import { t } from './i18n.js';
+import { ask } from './ask.js';
import * as platform from './platform.js';
import { PhotoSyncSection } from './photo-sync-settings.js';
import { DocSyncSection } from './phone-sync-settings.js';
+import { runPhotoSync } from './photo-sync.js';
+import { runDocSync, KINDS } from './phone-sync.js';
+import {
+ destination, privateGroups, probeGroup, photosTabShows,
+} from './sync-destination.js';
+
+/** The folder each kind this phone has goes into, named as the phone names it. */
+function kindDirs(folder, account) {
+ const suffixes = [
+ ...(platform.photoSync.available ? ['photos'] : []),
+ ...Object.values(KINDS).filter((k) => k.sync.available).map((k) => k.suffix),
+ ];
+ return suffixes.map((s) => `${folder}/${account}-${s}`);
+}
export function AndroidSyncPage({ user, groups }) {
+ const [dest, setDest] = useState(undefined); // undefined while asked, null when none
+ useEffect(() => { destination.get().then(setDest).catch(() => setDest(null)); }, []);
+
+ if (dest === undefined) {
+ return html`<div><h2>${t('androidsync.title')}</h2>
+ <p class="page-message"><span class="spinner"></span></p></div>`;
+ }
+ const mine = dest && dest.account === user.username ? dest : null;
+ const place = mine ? `${mine.groupId}\n${mine.folder}` : 'none';
+
+ const moved = (next) => {
+ setDest(next);
+ // Each kind that is on starts again in the new place, now.
+ if (platform.photoSync.available) runPhotoSync();
+ for (const kind of Object.keys(KINDS)) if (KINDS[kind].sync.available) runDocSync(kind);
+ };
+
return html`
<div>
<h2>${t('androidsync.title')}</h2>
+ <${DestinationSection} user=${user} groups=${groups || []} dest=${dest} onChange=${moved} />
${platform.photoSync.available && html`
- <${PhotoSyncSection} user=${user} groups=${groups || []} />`}
- ${platform.contactSync.available && html`
- <${DocSyncSection} kind="contacts" user=${user} groups=${groups || []} />`}
+ <${PhotoSyncSection} key=${'photos' + place} destination=${mine} />`}
+ ${Object.keys(KINDS).filter((k) => KINDS[k].sync.available).map((kind) => html`
+ <${DocSyncSection} key=${kind + place} kind=${kind} destination=${mine} />`)}
+ </div>
+ `;
+}
+
+function DestinationSection({ user, groups, dest, onChange }) {
+ const [editing, setEditing] = useState(false);
+ const mine = dest && dest.account === user.username ? dest : null;
+ return html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('androidsync.dest_heading')}</h3>
+ ${!editing && mine && html`
+ <div class="settings-row">
+ <span class="settings-label settings-name">
+ ${t('androidsync.dest_summary', { group: mine.groupName, folder: mine.folder })}
+ </span>
+ </div>
+ <p class="settings-hint settings-name">
+ ${t('androidsync.dest_dirs', { dirs: kindDirs(mine.folder, user.username).join(', ') })}
+ </p>
+ <button class="btn-secondary" onClick=${() => setEditing(true)}>${t('photosync.change')}</button>
+ `}
+ ${!editing && !mine && html`
+ <p class="settings-hint">
+ ${dest ? t('photosync.other_account', { account: dest.account }) : t('androidsync.dest_intro')}
+ </p>
+ <button class="admin-btn" onClick=${() => setEditing(true)}>${t('photosync.setup')}</button>
+ `}
+ ${editing && html`
+ <${DestinationSetup} user=${user} groups=${groups} current=${mine}
+ onDone=${(next) => { setEditing(false); if (next) onChange(next); }} />`}
+ </div>
+ `;
+}
+
+function DestinationSetup({ user, groups, current, onDone }) {
+ const [candidates, setCandidates] = useState(null); // null while the hub is asked
+ const [groupId, setGroupId] = useState(current ? current.groupId : '');
+ const [found, setFound] = useState(null); // { folders, photoDirectories }
+ const [probe, setProbe] = useState(''); // '', 'checking', 'offline'
+ const [folder, setFolder] = useState(current ? current.folder : '');
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState('');
+
+ // Only groups this account owns and is alone in: what is sent there is
+ // readable by every member, and none of it is the group's.
+ useEffect(() => {
+ let gone = false;
+ privateGroups(groups).then((list) => {
+ if (gone) return;
+ setCandidates(list);
+ setGroupId((g) => (list.some((x) => x.id === g) ? g : ''));
+ });
+ return () => { gone = true; };
+ }, [groups]);
+
+ useEffect(() => {
+ setFound(null);
+ if (!groupId) return undefined;
+ let gone = false;
+ setProbe('checking');
+ probeGroup(groupId).then((r) => {
+ if (gone) return;
+ setProbe('');
+ setFound(r);
+ setFolder((f) => (r.folders.includes(f) ? f : (r.folders[0] || '')));
+ }).catch(() => { if (!gone) setProbe('offline'); });
+ return () => { gone = true; };
+ }, [groupId]);
+
+ const photosDir = folder ? `${folder}/${user.username}-photos` : '';
+ const hiddenFromPhotos = platform.photoSync.available && found && folder
+ && !photosTabShows(photosDir, found.photoDirectories);
+
+ const save = async () => {
+ const group = (candidates || []).find((g) => g.id === groupId);
+ if (!group || !folder) return;
+ setBusy(true);
+ setError('');
+ try {
+ // Moving is a fresh start for every kind: said before it happens.
+ if (current && (current.groupId !== groupId || current.folder !== folder)) {
+ const yes = await ask(t('androidsync.dest_change_confirm', { group: group.name, folder }));
+ if (!yes) { setBusy(false); return; }
+ }
+ onDone(await destination.set({
+ account: user.username, groupId, groupName: group.name, folder,
+ }));
+ } catch (e) {
+ setError(e.message);
+ setBusy(false);
+ }
+ };
+
+ return html`
+ <div>
+ ${candidates === null && html`<p class="settings-hint">${t('phonesync.checking_groups')}</p>`}
+ ${candidates && !candidates.length && html`
+ <p class="settings-hint">${t('phonesync.no_private_group')}</p>`}
+ ${candidates && candidates.length > 0 && html`
+ <div class="settings-row">
+ <span class="settings-label">${t('photosync.group')}</span>
+ <select class="settings-select" value=${groupId}
+ onChange=${(e) => setGroupId(e.target.value)}>
+ <option value="">${t('photosync.choose_group')}</option>
+ ${candidates.map((g) => html`<option key=${g.id} value=${g.id}>${g.name}</option>`)}
+ </select>
+ </div>
+ `}
+ ${probe === 'checking' && html`<p class="settings-hint">${t('photosync.checking_group')}</p>`}
+ ${probe === 'offline' && html`<p class="settings-hint">${t('photosync.group_offline')}</p>`}
+ ${found && !found.folders.length && html`<p class="settings-hint">${t('phonesync.no_folder')}</p>`}
+ ${found && found.folders.length > 0 && html`
+ <div class="settings-row">
+ <span class="settings-label">${t('photosync.folder')}</span>
+ <select class="settings-select" value=${folder} onChange=${(e) => setFolder(e.target.value)}>
+ ${found.folders.map((f) => html`<option key=${f} value=${f}>${f}</option>`)}
+ </select>
+ </div>
+ ${folder && html`<p class="settings-hint settings-name">
+ ${t('androidsync.dest_dirs', { dirs: kindDirs(folder, user.username).join(', ') })}</p>`}
+ ${hiddenFromPhotos && html`
+ <p class="settings-hint settings-name">${t('androidsync.dest_not_in_photos', { dir: photosDir })}</p>`}
+ `}
+ <div class="settings-row" style="gap:8px; justify-content:flex-start">
+ <button class="admin-btn" onClick=${save}
+ disabled=${busy || !groupId || !folder}>${t('androidsync.dest_save')}</button>
+ <button class="btn-secondary" onClick=${() => onDone(null)}>${t('dialog.cancel')}</button>
+ </div>
+ ${error && html`<p class="error-msg">${error}</p>`}
</div>
`;
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 72c52ac..12b48a7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -717,7 +717,7 @@ const SearchPage = lazy(() => import('./search-page.js'), 'SearchPage',
//
// The Android application's backups of its photos, contacts and messages
// (docs/MESHBAY_DESIGN.md §9.12, §9.13): no browser or desktop ever fetches it.
-const PHONE_SYNC = platform.photoSync.available || platform.contactSync.available;
+const PHONE_SYNC = platform.phoneSync.available;
const AndroidSyncPage = lazy(() => import('./android-sync-page.js'), 'AndroidSyncPage',
html`<div class="page-content"><p class="page-message"><span class="spinner"></span></p></div>`);
@@ -931,6 +931,7 @@ function App() {
if (gone) return;
m.startPhotoSync({
hub: HUB,
+ hubFetch,
getUser: () => userRef.current,
getBundleKey: async () => {
const key = session.bundleKey || await _loadBundleKey();
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 18cdb40..d83d0c0 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -1309,7 +1309,6 @@ export default {
'photosync.choose_group': "Gruppe wählen",
'photosync.checking_group': "Ordner der Gruppe werden gelesen…",
'photosync.group_offline': "Gerade ist kein Node online, der diese Gruppe hostet. Versuchen Sie es erneut, sobald einer online ist.",
- 'photosync.no_folder': "Diese Gruppe hat keinen Ordner, in den Sie hochladen können und den ihr Fotos-Tab anzeigt.",
'photosync.folder': "Ordner",
'photosync.albums': "Alben auf diesem Telefon",
'photosync.album_camera': "Kamera",
@@ -1317,10 +1316,9 @@ export default {
'photosync.scope_existing': "Die Fotos, die schon auf diesem Telefon sind, dann neue",
'photosync.scope_new': "Nur Fotos, die ab jetzt aufgenommen werden",
'photosync.start': "Sicherung starten",
- 'photosync.confirm': "Ihre Fotos werden an {group} (gehostet von @{owner}, {members} Mitglieder) in den Ordner {folder} gesendet. Alle Mitglieder dieser Gruppe können sie sehen und herunterladen.\n\n{count} Fotos ({size}) werden gesendet, danach neue einmal täglich.\n\nSie können später löschen, was Sie gesendet haben, aber keine Kopien, die andere bereits heruntergeladen haben.",
- 'photosync.confirm_new': "Ihre Fotos werden an {group} (gehostet von @{owner}, {members} Mitglieder) in den Ordner {folder} gesendet. Alle Mitglieder dieser Gruppe können sie sehen und herunterladen.\n\nNeue Fotos werden einmal täglich gesendet.\n\nSie können später löschen, was Sie gesendet haben, aber keine Kopien, die andere bereits heruntergeladen haben.",
+ 'photosync.confirm': "{count} Fotos ({size}) werden nach {dir} gesendet, danach einmal täglich die neuen.",
+ 'photosync.confirm_new': "Neue Fotos werden einmal täglich nach {dir} gesendet.",
'photosync.metered_confirm': "Sie sind mit mobilen Daten verbunden — {count} Fotos, {size}. Trotzdem senden?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "Eingerichtet für @{account}. Melden Sie sich als @{account} an, um sie fortzusetzen, oder richten Sie sie hier neu ein.",
'photosync.sent_total': "Gesendete Fotos: {n}",
'photosync.last_run': "Letzte Sicherung: {when}",
@@ -1340,24 +1338,32 @@ export default {
'photosync.fail_permission': "Foto-Sicherung angehalten: MeshBay hat keinen Zugriff mehr auf die Fotos.",
'photosync.fail_other': "Foto-Sicherung angehalten.",
'contactsync.heading': "Kontakte-Sicherung",
- 'contactsync.intro': "Einmal täglich, wenn sie sich geändert haben, eine Kopie der Kontakte dieses Telefons in einen Ordner einer Gruppe senden, deren einziges Mitglied du bist. Jede Kopie ist eine .vcf-Datei, die jedes Telefon importieren kann.",
+ 'contactsync.intro': "Einmal täglich, wenn sie sich geändert haben, eine Kopie der Kontakte dieses Telefons senden. Jede Kopie ist eine .vcf-Datei, die jedes Telefon importieren kann.",
'contactsync.permission_needed': "MeshBay braucht Zugriff auf die Kontakte dieses Telefons.",
- 'contactsync.confirm': "Eine Kopie deiner Kontakte wird an {group} in {dir} gesendet, danach eine neue an jedem Tag, an dem sie sich ändern.\n\nDu bist das einzige Mitglied dieser Gruppe. Tritt jemand bei, endet die Sicherung.",
'phonesync.state_sending': "Wird gesendet…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Gesendete Kopien: {n}",
'phonesync.last_file': "Letzte Kopie: {name}",
- 'phonesync.checking_groups': "Suche nach Gruppen, deren einziges Mitglied du bist…",
- 'phonesync.no_private_group': "Du hast keine eigene Gruppe. Erstelle eine Gruppe, deren einziges Mitglied du bist, und komm dann hierher zurück.",
+ 'phonesync.checking_groups': "Suche nach Gruppen, die dir gehören und deren einziges Mitglied du bist…",
+ 'phonesync.no_private_group': "Du hast keine Gruppe, die dir gehört und deren einziges Mitglied du bist. Erstelle eine nur für dich und komm dann hierher zurück.",
'phonesync.no_folder': "Diese Gruppe hat keinen Ordner, in den du hochladen kannst.",
- 'phonesync.destination': "Kopien landen in {dir}",
'phonesync.notice': "{what}. {reason}",
'phonesync.fail_disk_full': "Gestoppt: Der Datenträger des Nodes ist voll.",
'phonesync.fail_read_only': "Gestoppt: Der Ordner nimmt keine Uploads mehr an.",
'phonesync.fail_no_folder': "Gestoppt: Der Ordner existiert in der Gruppe nicht mehr.",
'phonesync.fail_unavailable': "Gestoppt: Das Laufwerk des Ordners ist nicht mit dem Node verbunden.",
'phonesync.fail_not_member': "Gestoppt: Du bist kein Mitglied dieser Gruppe mehr.",
- 'phonesync.fail_not_private': "Gestoppt: Jemand anderes ist der Gruppe beigetreten. Wähle eine Gruppe, deren einziges Mitglied du bist.",
+ 'phonesync.fail_not_private': "Gestoppt: Jemand anderes ist der Gruppe beigetreten.",
'phonesync.fail_permission': "Gestoppt: MeshBay hat keinen Zugriff mehr.",
'phonesync.fail_other': "Gestoppt.",
+ 'androidsync.dest_heading': "Ziel der Sicherungen",
+ 'androidsync.dest_intro': "Sicherungen landen in einem Ordner einer Gruppe, die dir gehört und deren einziges Mitglied du bist. Jede Art von Sicherung bekommt dort einen eigenen Ordner.",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "Verwendete Ordner: {dirs}",
+ 'androidsync.dest_change_confirm': "Alle Sicherungen beginnen neu in {folder} von {group}. Bereits Gesendetes bleibt, wo es ist.",
+ 'androidsync.dest_not_in_photos': "Der Fotos-Tab der Gruppe zeigt {dir} nicht: Fotos werden gesichert, aber dort erst angezeigt, wenn der Ordner zu den Ordnern des Tabs gehört (Gruppeneinstellungen).",
+ 'androidsync.dest_needed': "Wähle zuerst oben auf dieser Seite das Ziel der Sicherungen.",
+ 'androidsync.dest_save': "Speichern",
+ 'phonesync.turn_on': "Einschalten",
+ 'photosync.fail_not_private': "Foto-Sicherung angehalten: Jemand anderes ist der Gruppe beigetreten.",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 8f8af0d..aa25258 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -1290,7 +1290,6 @@ export default {
'photosync.choose_group': "Choose a group",
'photosync.checking_group': "Looking at the group's folders…",
'photosync.group_offline': "No node hosting this group is online right now. Try again when it is.",
- 'photosync.no_folder': "This group has no folder you can add to that its Photos tab shows.",
'photosync.folder': "Folder",
'photosync.albums': "Albums on this phone",
'photosync.album_camera': "Camera",
@@ -1298,10 +1297,9 @@ export default {
'photosync.scope_existing': "The photos already on this phone, then new ones",
'photosync.scope_new': "Only photos taken from now on",
'photosync.start': "Start backup",
- 'photosync.confirm': "Your photos will be sent to {group} (hosted by @{owner}, {members} members) into {folder}. Every member of this group will be able to see and download them.\n\n{count} photos ({size}) will be sent, then new ones once a day.\n\nYou can delete what you sent later, but not copies others have already downloaded.",
- 'photosync.confirm_new': "Your photos will be sent to {group} (hosted by @{owner}, {members} members) into {folder}. Every member of this group will be able to see and download them.\n\nNew photos will be sent once a day.\n\nYou can delete what you sent later, but not copies others have already downloaded.",
+ 'photosync.confirm': "{count} photos ({size}) will be sent to {dir}, then new ones once a day.",
+ 'photosync.confirm_new': "New photos will be sent to {dir} once a day.",
'photosync.metered_confirm': "You are on mobile data — {count} photos, {size}. Send them anyway?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "Set up for @{account}. Sign in as @{account} to continue it, or set it up again here.",
'photosync.sent_total': "Photos sent: {n}",
'photosync.last_run': "Last backup: {when}",
@@ -1321,24 +1319,32 @@ export default {
'photosync.fail_permission': "Photo backup stopped: MeshBay no longer has access to the photos.",
'photosync.fail_other': "Photo backup stopped.",
'contactsync.heading': "Contacts backup",
- 'contactsync.intro': "Send a copy of this phone's contacts to a folder of a group only you belong to, once a day when they have changed. Each copy is a .vcf file any phone can import.",
+ 'contactsync.intro': "Send a copy of this phone's contacts once a day when they have changed. Each copy is a .vcf file any phone can import.",
'contactsync.permission_needed': "MeshBay needs access to this phone's contacts.",
- 'contactsync.confirm': "A copy of your contacts will be sent to {group} into {dir}, then a new one each day they change.\n\nYou are the only member of this group. If somebody joins it, the backup stops.",
'phonesync.state_sending': "Sending…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Copies sent: {n}",
'phonesync.last_file': "Last copy: {name}",
- 'phonesync.checking_groups': "Looking for groups only you belong to…",
- 'phonesync.no_private_group': "You have no group of your own. Create a group that only you belong to, then come back here.",
+ 'phonesync.checking_groups': "Looking for groups you own and are the only member of…",
+ 'phonesync.no_private_group': "You have no group that you own and are the only member of. Create one just for yourself, then come back here.",
'phonesync.no_folder': "This group has no folder you can add to.",
- 'phonesync.destination': "Copies go into {dir}",
'phonesync.notice': "{what}. {reason}",
'phonesync.fail_disk_full': "Stopped: the node's disk is full.",
'phonesync.fail_read_only': "Stopped: the folder no longer accepts uploads.",
'phonesync.fail_no_folder': "Stopped: the folder no longer exists in the group.",
'phonesync.fail_unavailable': "Stopped: the folder's drive is not connected to the node.",
'phonesync.fail_not_member': "Stopped: you are no longer a member of this group.",
- 'phonesync.fail_not_private': "Stopped: somebody else has joined the group. Choose a group only you belong to.",
+ 'phonesync.fail_not_private': "Stopped: somebody else has joined the group.",
'phonesync.fail_permission': "Stopped: MeshBay no longer has access.",
'phonesync.fail_other': "Stopped.",
+ 'androidsync.dest_heading': "Where backups go",
+ 'androidsync.dest_intro': "Backups go into a folder of a group you own and are the only member of. Each kind of backup gets its own folder there.",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "Folders used: {dirs}",
+ 'androidsync.dest_change_confirm': "Every backup will start again in {folder} of {group}. What was already sent stays where it is.",
+ 'androidsync.dest_not_in_photos': "The group's Photos tab does not show {dir}: photos will be backed up but not shown there until it is one of the tab's folders (group settings).",
+ 'androidsync.dest_needed': "Choose where backups go first, at the top of this page.",
+ 'androidsync.dest_save': "Save",
+ 'phonesync.turn_on': "Turn on",
+ 'photosync.fail_not_private': "Photo backup stopped: somebody else has joined the group.",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 586ff59..8a483e4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -1303,7 +1303,6 @@ export default {
'photosync.choose_group': "Elegir un grupo",
'photosync.checking_group': "Leyendo las carpetas del grupo…",
'photosync.group_offline': "Ningún node que aloje este grupo está en línea ahora. Vuelve a intentarlo cuando lo esté.",
- 'photosync.no_folder': "Este grupo no tiene ninguna carpeta en la que puedas subir archivos y que muestre su pestaña Fotos.",
'photosync.folder': "Carpeta",
'photosync.albums': "Álbumes de este teléfono",
'photosync.album_camera': "Cámara",
@@ -1311,10 +1310,9 @@ export default {
'photosync.scope_existing': "Las fotos que ya están en este teléfono y luego las nuevas",
'photosync.scope_new': "Solo las fotos tomadas a partir de ahora",
'photosync.start': "Iniciar la copia",
- 'photosync.confirm': "Tus fotos se enviarán a {group} (alojado por @{owner}, {members} miembros), a la carpeta {folder}. Todos los miembros de este grupo podrán verlas y descargarlas.\n\nSe enviarán {count} fotos ({size}) y después las nuevas una vez al día.\n\nPodrás borrar lo que enviaste, pero no las copias que otros ya hayan descargado.",
- 'photosync.confirm_new': "Tus fotos se enviarán a {group} (alojado por @{owner}, {members} miembros), a la carpeta {folder}. Todos los miembros de este grupo podrán verlas y descargarlas.\n\nLas fotos nuevas se enviarán una vez al día.\n\nPodrás borrar lo que enviaste, pero no las copias que otros ya hayan descargado.",
+ 'photosync.confirm': "Se enviarán {count} fotos ({size}) a {dir} y luego las nuevas una vez al día.",
+ 'photosync.confirm_new': "Las fotos nuevas se enviarán a {dir} una vez al día.",
'photosync.metered_confirm': "Estás usando datos móviles — {count} fotos, {size}. ¿Enviarlas de todos modos?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "Configurada para @{account}. Inicia sesión como @{account} para continuarla, o vuelve a configurarla aquí.",
'photosync.sent_total': "Fotos enviadas: {n}",
'photosync.last_run': "Última copia: {when}",
@@ -1334,24 +1332,32 @@ export default {
'photosync.fail_permission': "La copia de fotos se detuvo: MeshBay ya no tiene acceso a las fotos.",
'photosync.fail_other': "La copia de fotos se detuvo.",
'contactsync.heading': "Copia de seguridad de contactos",
- 'contactsync.intro': "Enviar una copia de los contactos de este teléfono a una carpeta de un grupo del que solo tú eres miembro, una vez al día si han cambiado. Cada copia es un archivo .vcf que cualquier teléfono puede importar.",
+ 'contactsync.intro': "Enviar una copia de los contactos de este teléfono una vez al día si han cambiado. Cada copia es un archivo .vcf que cualquier teléfono puede importar.",
'contactsync.permission_needed': "MeshBay necesita acceso a los contactos de este teléfono.",
- 'contactsync.confirm': "Se enviará una copia de tus contactos a {group}, en {dir}, y luego una nueva cada día que cambien.\n\nEres el único miembro de este grupo. Si alguien se une, la copia de seguridad se detiene.",
'phonesync.state_sending': "Enviando…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Copias enviadas: {n}",
'phonesync.last_file': "Última copia: {name}",
- 'phonesync.checking_groups': "Buscando grupos de los que solo tú eres miembro…",
- 'phonesync.no_private_group': "No tienes ningún grupo propio. Crea un grupo del que solo tú seas miembro y vuelve aquí.",
+ 'phonesync.checking_groups': "Buscando grupos de los que eres propietario y único miembro…",
+ 'phonesync.no_private_group': "No tienes ningún grupo del que seas propietario y único miembro. Crea uno solo para ti y vuelve aquí.",
'phonesync.no_folder': "Este grupo no tiene ninguna carpeta en la que puedas añadir archivos.",
- 'phonesync.destination': "Las copias van a {dir}",
'phonesync.notice': "{what}. {reason}",
'phonesync.fail_disk_full': "Detenida: el disco del node está lleno.",
'phonesync.fail_read_only': "Detenida: la carpeta ya no acepta envíos.",
'phonesync.fail_no_folder': "Detenida: la carpeta ya no existe en el grupo.",
'phonesync.fail_unavailable': "Detenida: la unidad de la carpeta no está conectada al node.",
'phonesync.fail_not_member': "Detenida: ya no eres miembro de este grupo.",
- 'phonesync.fail_not_private': "Detenida: otra persona se ha unido al grupo. Elige un grupo del que solo tú seas miembro.",
+ 'phonesync.fail_not_private': "Detenida: otra persona se ha unido al grupo.",
'phonesync.fail_permission': "Detenida: MeshBay ya no tiene acceso.",
'phonesync.fail_other': "Detenida.",
+ 'androidsync.dest_heading': "Destino de las copias de seguridad",
+ 'androidsync.dest_intro': "Las copias de seguridad van a una carpeta de un grupo del que eres propietario y único miembro. Cada tipo de copia tiene allí su propia carpeta.",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "Carpetas usadas: {dirs}",
+ 'androidsync.dest_change_confirm': "Todas las copias de seguridad empezarán de nuevo en {folder} de {group}. Lo ya enviado se queda donde está.",
+ 'androidsync.dest_not_in_photos': "La pestaña Fotos del grupo no muestra {dir}: las fotos se guardarán, pero no se mostrarán allí hasta que esa carpeta sea una de las de la pestaña (ajustes del grupo).",
+ 'androidsync.dest_needed': "Elige primero el destino de las copias de seguridad, arriba en esta página.",
+ 'androidsync.dest_save': "Guardar",
+ 'phonesync.turn_on': "Activar",
+ 'photosync.fail_not_private': "Copia de seguridad de fotos detenida: otra persona se ha unido al grupo.",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index a234a45..7f15f05 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -1318,7 +1318,6 @@ export default {
'photosync.choose_group': "Choisir un groupe",
'photosync.checking_group': "Lecture des dossiers du groupe…",
'photosync.group_offline': "Aucun node hébergeant ce groupe n'est en ligne pour le moment. Réessayez quand il le sera.",
- 'photosync.no_folder': "Ce groupe n'a aucun dossier où vous pouvez déposer des fichiers et que son onglet Photos affiche.",
'photosync.folder': "Dossier",
'photosync.albums': "Albums de ce téléphone",
'photosync.album_camera': "Appareil photo",
@@ -1326,10 +1325,9 @@ export default {
'photosync.scope_existing': "Les photos déjà sur ce téléphone, puis les nouvelles",
'photosync.scope_new': "Seulement les photos prises à partir de maintenant",
'photosync.start': "Lancer la sauvegarde",
- 'photosync.confirm': "Vos photos seront envoyées dans {group} (hébergé par @{owner}, {members} membres), dans le dossier {folder}. Tous les membres de ce groupe pourront les voir et les télécharger.\n\n{count} photos ({size}) seront envoyées, puis les nouvelles une fois par jour.\n\nVous pourrez supprimer ce que vous avez envoyé, mais pas les copies que d'autres auront déjà téléchargées.",
- 'photosync.confirm_new': "Vos photos seront envoyées dans {group} (hébergé par @{owner}, {members} membres), dans le dossier {folder}. Tous les membres de ce groupe pourront les voir et les télécharger.\n\nLes nouvelles photos seront envoyées une fois par jour.\n\nVous pourrez supprimer ce que vous avez envoyé, mais pas les copies que d'autres auront déjà téléchargées.",
+ 'photosync.confirm': "{count} photos ({size}) seront envoyées dans {dir}, puis les nouvelles une fois par jour.",
+ 'photosync.confirm_new': "Les nouvelles photos seront envoyées dans {dir} une fois par jour.",
'photosync.metered_confirm': "Vous êtes en données mobiles — {count} photos, {size}. Les envoyer quand même ?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "Configurée pour @{account}. Connectez-vous en tant que @{account} pour la poursuivre, ou configurez-la à nouveau ici.",
'photosync.sent_total': "Photos envoyées : {n}",
'photosync.last_run': "Dernière sauvegarde : {when}",
@@ -1349,24 +1347,32 @@ export default {
'photosync.fail_permission': "Sauvegarde des photos arrêtée : MeshBay n'a plus accès aux photos.",
'photosync.fail_other': "Sauvegarde des photos arrêtée.",
'contactsync.heading': "Sauvegarde des contacts",
- 'contactsync.intro': "Envoyer une copie des contacts de ce téléphone dans un dossier d'un groupe dont vous êtes le seul membre, une fois par jour s'ils ont changé. Chaque copie est un fichier .vcf que tout téléphone sait importer.",
+ 'contactsync.intro': "Envoyer une copie des contacts de ce téléphone une fois par jour s'ils ont changé. Chaque copie est un fichier .vcf que tout téléphone sait importer.",
'contactsync.permission_needed': "MeshBay a besoin d'accéder aux contacts de ce téléphone.",
- 'contactsync.confirm': "Une copie de vos contacts sera envoyée dans {group}, dans {dir}, puis une nouvelle chaque jour où ils changent.\n\nVous êtes le seul membre de ce groupe. Si quelqu'un le rejoint, la sauvegarde s'arrête.",
'phonesync.state_sending': "Envoi…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Copies envoyées : {n}",
'phonesync.last_file': "Dernière copie : {name}",
- 'phonesync.checking_groups': "Recherche des groupes dont vous êtes le seul membre…",
- 'phonesync.no_private_group': "Vous n'avez aucun groupe à vous seul. Créez un groupe dont vous êtes le seul membre, puis revenez ici.",
+ 'phonesync.checking_groups': "Recherche des groupes dont vous êtes le propriétaire et le seul membre…",
+ 'phonesync.no_private_group': "Vous n'avez aucun groupe dont vous êtes le propriétaire et le seul membre. Créez-en un rien que pour vous, puis revenez ici.",
'phonesync.no_folder': "Ce groupe n'a aucun dossier où vous pouvez ajouter des fichiers.",
- 'phonesync.destination': "Les copies vont dans {dir}",
'phonesync.notice': "{what}. {reason}",
'phonesync.fail_disk_full': "Arrêtée : le disque du node est plein.",
'phonesync.fail_read_only': "Arrêtée : le dossier n'accepte plus d'envois.",
'phonesync.fail_no_folder': "Arrêtée : le dossier n'existe plus dans le groupe.",
'phonesync.fail_unavailable': "Arrêtée : le disque du dossier n'est pas branché au node.",
'phonesync.fail_not_member': "Arrêtée : vous n'êtes plus membre de ce groupe.",
- 'phonesync.fail_not_private': "Arrêtée : quelqu'un d'autre a rejoint le groupe. Choisissez un groupe dont vous êtes le seul membre.",
+ 'phonesync.fail_not_private': "Arrêtée : quelqu'un d'autre a rejoint le groupe.",
'phonesync.fail_permission': "Arrêtée : MeshBay n'y a plus accès.",
'phonesync.fail_other': "Arrêtée.",
+ 'androidsync.dest_heading': "Destination des sauvegardes",
+ 'androidsync.dest_intro': "Les sauvegardes vont dans un dossier d'un groupe dont vous êtes le propriétaire et le seul membre. Chaque type de sauvegarde y a son propre dossier.",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "Dossiers utilisés : {dirs}",
+ 'androidsync.dest_change_confirm': "Toutes les sauvegardes recommenceront dans {folder} de {group}. Ce qui a déjà été envoyé reste où il est.",
+ 'androidsync.dest_not_in_photos': "L'onglet Photos du groupe n'affiche pas {dir} : les photos seront sauvegardées mais pas affichées tant que ce dossier ne fait pas partie de ceux de l'onglet (paramètres du groupe).",
+ 'androidsync.dest_needed': "Choisissez d'abord la destination des sauvegardes, en haut de cette page.",
+ 'androidsync.dest_save': "Enregistrer",
+ 'phonesync.turn_on': "Activer",
+ 'photosync.fail_not_private': "Sauvegarde des photos arrêtée : quelqu'un d'autre a rejoint le groupe.",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 9019007..d18c373 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -1317,7 +1317,6 @@ export default {
'photosync.choose_group': "Scegli un gruppo",
'photosync.checking_group': "Lettura delle cartelle del gruppo…",
'photosync.group_offline': "Nessun node che ospita questo gruppo è online ora. Riprova quando lo sarà.",
- 'photosync.no_folder': "Questo gruppo non ha nessuna cartella in cui puoi caricare file e che la sua scheda Foto mostri.",
'photosync.folder': "Cartella",
'photosync.albums': "Album su questo telefono",
'photosync.album_camera': "Fotocamera",
@@ -1325,10 +1324,9 @@ export default {
'photosync.scope_existing': "Le foto già su questo telefono, poi quelle nuove",
'photosync.scope_new': "Solo le foto scattate da ora in poi",
'photosync.start': "Avvia il backup",
- 'photosync.confirm': "Le tue foto verranno inviate a {group} (ospitato da @{owner}, {members} membri), nella cartella {folder}. Tutti i membri di questo gruppo potranno vederle e scaricarle.\n\nVerranno inviate {count} foto ({size}), poi quelle nuove una volta al giorno.\n\nPotrai eliminare ciò che hai inviato, ma non le copie che altri hanno già scaricato.",
- 'photosync.confirm_new': "Le tue foto verranno inviate a {group} (ospitato da @{owner}, {members} membri), nella cartella {folder}. Tutti i membri di questo gruppo potranno vederle e scaricarle.\n\nLe nuove foto verranno inviate una volta al giorno.\n\nPotrai eliminare ciò che hai inviato, ma non le copie che altri hanno già scaricato.",
+ 'photosync.confirm': "{count} foto ({size}) verranno inviate in {dir}, poi quelle nuove una volta al giorno.",
+ 'photosync.confirm_new': "Le nuove foto verranno inviate in {dir} una volta al giorno.",
'photosync.metered_confirm': "Stai usando i dati mobili — {count} foto, {size}. Inviarle comunque?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "Configurato per @{account}. Accedi come @{account} per continuarlo, oppure configuralo di nuovo qui.",
'photosync.sent_total': "Foto inviate: {n}",
'photosync.last_run': "Ultimo backup: {when}",
@@ -1348,24 +1346,32 @@ export default {
'photosync.fail_permission': "Backup delle foto interrotto: MeshBay non ha più accesso alle foto.",
'photosync.fail_other': "Backup delle foto interrotto.",
'contactsync.heading': "Backup dei contatti",
- 'contactsync.intro': "Invia una copia dei contatti di questo telefono in una cartella di un gruppo di cui sei l'unico membro, una volta al giorno se sono cambiati. Ogni copia è un file .vcf che qualsiasi telefono può importare.",
+ 'contactsync.intro': "Invia una copia dei contatti di questo telefono una volta al giorno se sono cambiati. Ogni copia è un file .vcf che qualsiasi telefono può importare.",
'contactsync.permission_needed': "MeshBay ha bisogno di accedere ai contatti di questo telefono.",
- 'contactsync.confirm': "Una copia dei tuoi contatti verrà inviata a {group}, in {dir}, poi una nuova ogni giorno in cui cambiano.\n\nSei l'unico membro di questo gruppo. Se qualcuno si unisce, il backup si ferma.",
'phonesync.state_sending': "Invio…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Copie inviate: {n}",
'phonesync.last_file': "Ultima copia: {name}",
- 'phonesync.checking_groups': "Ricerca dei gruppi di cui sei l'unico membro…",
- 'phonesync.no_private_group': "Non hai un gruppo tutto tuo. Crea un gruppo di cui sei l'unico membro, poi torna qui.",
+ 'phonesync.checking_groups': "Ricerca dei gruppi di cui sei proprietario e unico membro…",
+ 'phonesync.no_private_group': "Non hai un gruppo di cui sei proprietario e unico membro. Creane uno solo per te, poi torna qui.",
'phonesync.no_folder': "Questo gruppo non ha cartelle in cui puoi aggiungere file.",
- 'phonesync.destination': "Le copie vanno in {dir}",
'phonesync.notice': "{what}. {reason}",
'phonesync.fail_disk_full': "Interrotto: il disco del node è pieno.",
'phonesync.fail_read_only': "Interrotto: la cartella non accetta più caricamenti.",
'phonesync.fail_no_folder': "Interrotto: la cartella non esiste più nel gruppo.",
'phonesync.fail_unavailable': "Interrotto: l'unità della cartella non è collegata al node.",
'phonesync.fail_not_member': "Interrotto: non sei più membro di questo gruppo.",
- 'phonesync.fail_not_private': "Interrotto: qualcun altro si è unito al gruppo. Scegli un gruppo di cui sei l'unico membro.",
+ 'phonesync.fail_not_private': "Interrotto: qualcun altro si è unito al gruppo.",
'phonesync.fail_permission': "Interrotto: MeshBay non ha più accesso.",
'phonesync.fail_other': "Interrotto.",
+ 'androidsync.dest_heading': "Destinazione dei backup",
+ 'androidsync.dest_intro': "I backup vanno in una cartella di un gruppo di cui sei proprietario e unico membro. Ogni tipo di backup vi ha la sua cartella.",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "Cartelle usate: {dirs}",
+ 'androidsync.dest_change_confirm': "Tutti i backup ricominceranno in {folder} di {group}. Ciò che è già stato inviato resta dov'è.",
+ 'androidsync.dest_not_in_photos': "La scheda Foto del gruppo non mostra {dir}: le foto verranno salvate ma non mostrate lì finché la cartella non è tra quelle della scheda (impostazioni del gruppo).",
+ 'androidsync.dest_needed': "Scegli prima la destinazione dei backup, in cima a questa pagina.",
+ 'androidsync.dest_save': "Salva",
+ 'phonesync.turn_on': "Attiva",
+ 'photosync.fail_not_private': "Backup delle foto interrotto: qualcun altro si è unito al gruppo.",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 043586f..1ee0a81 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -1301,7 +1301,6 @@ export default {
'photosync.choose_group': "グループを選択",
'photosync.checking_group': "グループのフォルダーを確認しています…",
'photosync.group_offline': "このグループをホストしている node は現在オンラインではありません。オンラインになってから再度お試しください。",
- 'photosync.no_folder': "このグループには、アップロードでき、かつ写真タブに表示されるフォルダーがありません。",
'photosync.folder': "フォルダー",
'photosync.albums': "この電話のアルバム",
'photosync.album_camera': "カメラ",
@@ -1309,10 +1308,9 @@ export default {
'photosync.scope_existing': "この電話にすでにある写真と、これからの写真",
'photosync.scope_new': "これから撮る写真のみ",
'photosync.start': "バックアップを開始",
- 'photosync.confirm': "写真は {group}(ホスト: @{owner}、メンバー {members} 人)のフォルダー {folder} に送られます。このグループのメンバー全員が写真を見てダウンロードできます。\n\n{count} 枚({size})を送信し、その後は新しい写真を 1 日 1 回送信します。\n\n送信したものは後で削除できますが、他の人がすでにダウンロードしたコピーは削除できません。",
- 'photosync.confirm_new': "写真は {group}(ホスト: @{owner}、メンバー {members} 人)のフォルダー {folder} に送られます。このグループのメンバー全員が写真を見てダウンロードできます。\n\n新しい写真を 1 日 1 回送信します。\n\n送信したものは後で削除できますが、他の人がすでにダウンロードしたコピーは削除できません。",
+ 'photosync.confirm': "{count} 枚の写真 ({size}) を {dir} に送り、その後は新しい写真を1日1回送ります。",
+ 'photosync.confirm_new': "新しい写真を1日1回 {dir} に送ります。",
'photosync.metered_confirm': "モバイルデータ通信中です — {count} 枚、{size}。それでも送信しますか?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "@{account} 用に設定されています。続けるには @{account} でサインインするか、ここで設定し直してください。",
'photosync.sent_total': "送信済みの写真: {n}",
'photosync.last_run': "前回のバックアップ: {when}",
@@ -1332,24 +1330,32 @@ export default {
'photosync.fail_permission': "写真のバックアップを停止しました: MeshBay が写真にアクセスできなくなりました。",
'photosync.fail_other': "写真のバックアップを停止しました。",
'contactsync.heading': "連絡先のバックアップ",
- 'contactsync.intro': "このスマートフォンの連絡先のコピーを、あなただけがメンバーのグループのフォルダーに、変更があった日に1日1回送ります。各コピーはどのスマートフォンでも読み込める .vcf ファイルです。",
+ 'contactsync.intro': "このスマートフォンの連絡先のコピーを、変更があった日に1日1回送ります。各コピーはどのスマートフォンでも読み込める .vcf ファイルです。",
'contactsync.permission_needed': "MeshBay にこのスマートフォンの連絡先へのアクセスを許可してください。",
- 'contactsync.confirm': "連絡先のコピーを {group} の {dir} に送り、その後は変更があった日ごとに新しいコピーを送ります。\n\nこのグループのメンバーはあなただけです。誰かが参加すると、バックアップは停止します。",
'phonesync.state_sending': "送信中…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "送信したコピー: {n}",
'phonesync.last_file': "最後のコピー: {name}",
- 'phonesync.checking_groups': "あなただけがメンバーのグループを探しています…",
- 'phonesync.no_private_group': "あなた専用のグループがありません。あなただけがメンバーのグループを作成してから、ここに戻ってください。",
+ 'phonesync.checking_groups': "あなたが所有者で唯一のメンバーのグループを探しています…",
+ 'phonesync.no_private_group': "あなたが所有者で唯一のメンバーのグループがありません。自分専用のグループを作成してから、ここに戻ってください。",
'phonesync.no_folder': "このグループには、ファイルを追加できるフォルダーがありません。",
- 'phonesync.destination': "コピーの保存先: {dir}",
'phonesync.notice': "{what}。{reason}",
'phonesync.fail_disk_full': "停止しました: node のディスクがいっぱいです。",
'phonesync.fail_read_only': "停止しました: フォルダーがアップロードを受け付けなくなりました。",
'phonesync.fail_no_folder': "停止しました: フォルダーがグループにもうありません。",
'phonesync.fail_unavailable': "停止しました: フォルダーのドライブが node に接続されていません。",
'phonesync.fail_not_member': "停止しました: このグループのメンバーではなくなりました。",
- 'phonesync.fail_not_private': "停止しました: 他の人がグループに参加しました。あなただけがメンバーのグループを選んでください。",
+ 'phonesync.fail_not_private': "停止しました: 他の人がグループに参加しました。",
'phonesync.fail_permission': "停止しました: MeshBay はアクセスできなくなりました。",
'phonesync.fail_other': "停止しました。",
+ 'androidsync.dest_heading': "バックアップの保存先",
+ 'androidsync.dest_intro': "バックアップは、あなたが所有者で唯一のメンバーであるグループのフォルダーに保存されます。バックアップの種類ごとに専用のフォルダーが作られます。",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "使用するフォルダー: {dirs}",
+ 'androidsync.dest_change_confirm': "すべてのバックアップが {group} の {folder} で最初からやり直されます。送信済みのものはそのまま残ります。",
+ 'androidsync.dest_not_in_photos': "グループの写真タブには {dir} が表示されません。写真はバックアップされますが、このフォルダーがタブのフォルダーに含まれるまで (グループ設定) そこには表示されません。",
+ 'androidsync.dest_needed': "まず、このページの上部でバックアップの保存先を選んでください。",
+ 'androidsync.dest_save': "保存",
+ 'phonesync.turn_on': "オンにする",
+ 'photosync.fail_not_private': "写真のバックアップを停止しました: 他の人がグループに参加しました。",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 5c1a0d2..ec7be69 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -1319,7 +1319,6 @@ export default {
'photosync.choose_group': "Kies een groep",
'photosync.checking_group': "Mappen van de groep worden gelezen…",
'photosync.group_offline': "Er is nu geen node online die deze groep host. Probeer het opnieuw als dat wel zo is.",
- 'photosync.no_folder': "Deze groep heeft geen map waarin je kunt uploaden en die het tabblad Foto's toont.",
'photosync.folder': "Map",
'photosync.albums': "Albums op deze telefoon",
'photosync.album_camera': "Camera",
@@ -1327,10 +1326,9 @@ export default {
'photosync.scope_existing': "De foto's die al op deze telefoon staan, daarna nieuwe",
'photosync.scope_new': "Alleen foto's die vanaf nu worden gemaakt",
'photosync.start': "Back-up starten",
- 'photosync.confirm': "Je foto's worden naar {group} (gehost door @{owner}, {members} leden) gestuurd, in de map {folder}. Alle leden van deze groep kunnen ze zien en downloaden.\n\nEr worden {count} foto's ({size}) gestuurd, daarna eenmaal per dag de nieuwe.\n\nJe kunt wat je hebt gestuurd later verwijderen, maar niet de kopieën die anderen al hebben gedownload.",
- 'photosync.confirm_new': "Je foto's worden naar {group} (gehost door @{owner}, {members} leden) gestuurd, in de map {folder}. Alle leden van deze groep kunnen ze zien en downloaden.\n\nNieuwe foto's worden eenmaal per dag gestuurd.\n\nJe kunt wat je hebt gestuurd later verwijderen, maar niet de kopieën die anderen al hebben gedownload.",
+ 'photosync.confirm': "{count} foto's ({size}) worden naar {dir} gestuurd, daarna eenmaal per dag de nieuwe.",
+ 'photosync.confirm_new': "Nieuwe foto's worden eenmaal per dag naar {dir} gestuurd.",
'photosync.metered_confirm': "Je gebruikt mobiele data — {count} foto's, {size}. Toch versturen?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "Ingesteld voor @{account}. Log in als @{account} om verder te gaan, of stel het hier opnieuw in.",
'photosync.sent_total': "Verstuurde foto's: {n}",
'photosync.last_run': "Laatste back-up: {when}",
@@ -1350,24 +1348,32 @@ export default {
'photosync.fail_permission': "Fotoback-up gestopt: MeshBay heeft geen toegang meer tot de foto's.",
'photosync.fail_other': "Fotoback-up gestopt.",
'contactsync.heading': "Back-up van contacten",
- 'contactsync.intro': "Stuur eenmaal per dag, als ze veranderd zijn, een kopie van de contacten op deze telefoon naar een map van een groep waarvan alleen jij lid bent. Elke kopie is een .vcf-bestand dat elke telefoon kan importeren.",
+ 'contactsync.intro': "Stuur eenmaal per dag, als ze veranderd zijn, een kopie van de contacten op deze telefoon. Elke kopie is een .vcf-bestand dat elke telefoon kan importeren.",
'contactsync.permission_needed': "MeshBay heeft toegang nodig tot de contacten op deze telefoon.",
- 'contactsync.confirm': "Een kopie van je contacten wordt naar {group} gestuurd, in {dir}, en daarna een nieuwe op elke dag dat ze veranderen.\n\nJij bent het enige lid van deze groep. Als iemand lid wordt, stopt de back-up.",
'phonesync.state_sending': "Bezig met verzenden…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Verzonden kopieën: {n}",
'phonesync.last_file': "Laatste kopie: {name}",
- 'phonesync.checking_groups': "Zoeken naar groepen waarvan alleen jij lid bent…",
- 'phonesync.no_private_group': "Je hebt geen eigen groep. Maak een groep waarvan alleen jij lid bent en kom dan hier terug.",
+ 'phonesync.checking_groups': "Zoeken naar groepen waarvan jij eigenaar en het enige lid bent…",
+ 'phonesync.no_private_group': "Je hebt geen groep waarvan jij eigenaar en het enige lid bent. Maak er een alleen voor jezelf en kom dan hier terug.",
'phonesync.no_folder': "Deze groep heeft geen map waaraan je bestanden kunt toevoegen.",
- 'phonesync.destination': "Kopieën gaan naar {dir}",
'phonesync.notice': "{what}. {reason}",
'phonesync.fail_disk_full': "Gestopt: de schijf van de node is vol.",
'phonesync.fail_read_only': "Gestopt: de map accepteert geen uploads meer.",
'phonesync.fail_no_folder': "Gestopt: de map bestaat niet meer in de groep.",
'phonesync.fail_unavailable': "Gestopt: de schijf van de map is niet aangesloten op de node.",
'phonesync.fail_not_member': "Gestopt: je bent geen lid meer van deze groep.",
- 'phonesync.fail_not_private': "Gestopt: iemand anders is lid geworden van de groep. Kies een groep waarvan alleen jij lid bent.",
+ 'phonesync.fail_not_private': "Gestopt: iemand anders is lid geworden van de groep.",
'phonesync.fail_permission': "Gestopt: MeshBay heeft geen toegang meer.",
'phonesync.fail_other': "Gestopt.",
+ 'androidsync.dest_heading': "Bestemming van back-ups",
+ 'androidsync.dest_intro': "Back-ups gaan naar een map van een groep waarvan jij eigenaar en het enige lid bent. Elk soort back-up krijgt daar een eigen map.",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "Gebruikte mappen: {dirs}",
+ 'androidsync.dest_change_confirm': "Alle back-ups beginnen opnieuw in {folder} van {group}. Wat al verzonden is, blijft waar het is.",
+ 'androidsync.dest_not_in_photos': "Het tabblad Foto's van de groep toont {dir} niet: foto's worden geback-upt maar daar pas getoond als de map bij de mappen van het tabblad hoort (groepsinstellingen).",
+ 'androidsync.dest_needed': "Kies eerst bovenaan deze pagina waar back-ups naartoe gaan.",
+ 'androidsync.dest_save': "Opslaan",
+ 'phonesync.turn_on': "Inschakelen",
+ 'photosync.fail_not_private': "Fotoback-up gestopt: iemand anders is lid geworden van de groep.",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 86d1d34..96da817 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -1345,7 +1345,6 @@ export default {
'photosync.choose_group': "Wybierz grupę",
'photosync.checking_group': "Odczytywanie folderów grupy…",
'photosync.group_offline': "Żaden node hostujący tę grupę nie jest teraz dostępny. Spróbuj ponownie, gdy będzie.",
- 'photosync.no_folder': "Ta grupa nie ma folderu, do którego możesz przesyłać pliki i który pokazuje jej karta Zdjęcia.",
'photosync.folder': "Folder",
'photosync.albums': "Albumy na tym telefonie",
'photosync.album_camera': "Aparat",
@@ -1353,10 +1352,9 @@ export default {
'photosync.scope_existing': "Zdjęcia, które już są na tym telefonie, a potem nowe",
'photosync.scope_new': "Tylko zdjęcia zrobione od teraz",
'photosync.start': "Rozpocznij kopię",
- 'photosync.confirm': "Twoje zdjęcia zostaną wysłane do grupy {group} (hostowanej przez @{owner}, członkowie: {members}), do folderu {folder}. Wszyscy członkowie tej grupy będą mogli je oglądać i pobierać.\n\nZdjęcia do wysłania: {count} ({size}), a potem nowe raz dziennie.\n\nMożesz później usunąć to, co wysłałeś, ale nie kopie, które inni już pobrali.",
- 'photosync.confirm_new': "Twoje zdjęcia zostaną wysłane do grupy {group} (hostowanej przez @{owner}, członkowie: {members}), do folderu {folder}. Wszyscy członkowie tej grupy będą mogli je oglądać i pobierać.\n\nNowe zdjęcia będą wysyłane raz dziennie.\n\nMożesz później usunąć to, co wysłałeś, ale nie kopie, które inni już pobrali.",
+ 'photosync.confirm': "{count} zdjęć ({size}) zostanie wysłanych do {dir}, a potem nowe raz dziennie.",
+ 'photosync.confirm_new': "Nowe zdjęcia będą wysyłane do {dir} raz dziennie.",
'photosync.metered_confirm': "Korzystasz z danych komórkowych — zdjęcia: {count}, {size}. Wysłać mimo to?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "Skonfigurowano dla @{account}. Zaloguj się jako @{account}, aby ją kontynuować, albo skonfiguruj ją tu ponownie.",
'photosync.sent_total': "Wysłane zdjęcia: {n}",
'photosync.last_run': "Ostatnia kopia: {when}",
@@ -1376,24 +1374,32 @@ export default {
'photosync.fail_permission': "Kopia zdjęć zatrzymana: MeshBay nie ma już dostępu do zdjęć.",
'photosync.fail_other': "Kopia zdjęć zatrzymana.",
'contactsync.heading': "Kopia kontaktów",
- 'contactsync.intro': "Wysyłaj kopię kontaktów z tego telefonu do folderu grupy, której jesteś jedynym członkiem, raz dziennie, jeśli się zmieniły. Każda kopia to plik .vcf, który zaimportuje każdy telefon.",
+ 'contactsync.intro': "Wysyłaj kopię kontaktów z tego telefonu raz dziennie, jeśli się zmieniły. Każda kopia to plik .vcf, który zaimportuje każdy telefon.",
'contactsync.permission_needed': "MeshBay potrzebuje dostępu do kontaktów w tym telefonie.",
- 'contactsync.confirm': "Kopia Twoich kontaktów zostanie wysłana do {group}, do {dir}, a potem nowa każdego dnia, w którym się zmienią.\n\nJesteś jedynym członkiem tej grupy. Jeśli ktoś do niej dołączy, kopia zostanie zatrzymana.",
'phonesync.state_sending': "Wysyłanie…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Wysłane kopie: {n}",
'phonesync.last_file': "Ostatnia kopia: {name}",
- 'phonesync.checking_groups': "Szukanie grup, których jesteś jedynym członkiem…",
- 'phonesync.no_private_group': "Nie masz własnej grupy. Utwórz grupę, której jesteś jedynym członkiem, i wróć tutaj.",
+ 'phonesync.checking_groups': "Szukanie grup, których jesteś właścicielem i jedynym członkiem…",
+ 'phonesync.no_private_group': "Nie masz grupy, której jesteś właścicielem i jedynym członkiem. Utwórz taką tylko dla siebie i wróć tutaj.",
'phonesync.no_folder': "Ta grupa nie ma folderu, do którego możesz dodawać pliki.",
- 'phonesync.destination': "Kopie trafiają do {dir}",
'phonesync.notice': "{what}. {reason}",
'phonesync.fail_disk_full': "Zatrzymano: dysk node'a jest pełny.",
'phonesync.fail_read_only': "Zatrzymano: folder nie przyjmuje już plików.",
'phonesync.fail_no_folder': "Zatrzymano: folder nie istnieje już w grupie.",
'phonesync.fail_unavailable': "Zatrzymano: dysk folderu nie jest podłączony do node'a.",
'phonesync.fail_not_member': "Zatrzymano: nie jesteś już członkiem tej grupy.",
- 'phonesync.fail_not_private': "Zatrzymano: ktoś inny dołączył do grupy. Wybierz grupę, której jesteś jedynym członkiem.",
+ 'phonesync.fail_not_private': "Zatrzymano: ktoś inny dołączył do grupy.",
'phonesync.fail_permission': "Zatrzymano: MeshBay nie ma już dostępu.",
'phonesync.fail_other': "Zatrzymano.",
+ 'androidsync.dest_heading': "Miejsce kopii zapasowych",
+ 'androidsync.dest_intro': "Kopie trafiają do folderu grupy, której jesteś właścicielem i jedynym członkiem. Każdy rodzaj kopii ma tam własny folder.",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "Używane foldery: {dirs}",
+ 'androidsync.dest_change_confirm': "Wszystkie kopie zaczną się od nowa w {folder} grupy {group}. To, co już wysłano, zostaje na miejscu.",
+ 'androidsync.dest_not_in_photos': "Karta Zdjęcia tej grupy nie pokazuje {dir}: zdjęcia będą kopiowane, ale nie pojawią się tam, dopóki ten folder nie będzie jednym z folderów karty (ustawienia grupy).",
+ 'androidsync.dest_needed': "Najpierw wybierz miejsce kopii zapasowych, na górze tej strony.",
+ 'androidsync.dest_save': "Zapisz",
+ 'phonesync.turn_on': "Włącz",
+ 'photosync.fail_not_private': "Kopia zdjęć zatrzymana: ktoś inny dołączył do grupy.",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index e499de2..cec9604 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -1304,7 +1304,6 @@ export default {
'photosync.choose_group': "Escolha um grupo",
'photosync.checking_group': "Lendo as pastas do grupo…",
'photosync.group_offline': "Nenhum node que hospeda este grupo está on-line agora. Tente de novo quando estiver.",
- 'photosync.no_folder': "Este grupo não tem nenhuma pasta onde você possa enviar arquivos e que a aba Fotos mostre.",
'photosync.folder': "Pasta",
'photosync.albums': "Álbuns deste celular",
'photosync.album_camera': "Câmera",
@@ -1312,10 +1311,9 @@ export default {
'photosync.scope_existing': "As fotos que já estão neste celular, depois as novas",
'photosync.scope_new': "Somente as fotos tiradas a partir de agora",
'photosync.start': "Iniciar backup",
- 'photosync.confirm': "Suas fotos serão enviadas para {group} (hospedado por @{owner}, {members} membros), na pasta {folder}. Todos os membros deste grupo poderão vê-las e baixá-las.\n\nSerão enviadas {count} fotos ({size}) e, depois, as novas uma vez por dia.\n\nVocê poderá apagar o que enviou, mas não as cópias que outros já tiverem baixado.",
- 'photosync.confirm_new': "Suas fotos serão enviadas para {group} (hospedado por @{owner}, {members} membros), na pasta {folder}. Todos os membros deste grupo poderão vê-las e baixá-las.\n\nAs novas fotos serão enviadas uma vez por dia.\n\nVocê poderá apagar o que enviou, mas não as cópias que outros já tiverem baixado.",
+ 'photosync.confirm': "{count} fotos ({size}) serão enviadas para {dir} e depois as novas uma vez por dia.",
+ 'photosync.confirm_new': "As fotos novas serão enviadas para {dir} uma vez por dia.",
'photosync.metered_confirm': "Você está usando dados móveis — {count} fotos, {size}. Enviar mesmo assim?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "Configurado para @{account}. Entre como @{account} para continuar, ou configure de novo aqui.",
'photosync.sent_total': "Fotos enviadas: {n}",
'photosync.last_run': "Último backup: {when}",
@@ -1335,24 +1333,32 @@ export default {
'photosync.fail_permission': "Backup de fotos parado: o MeshBay não tem mais acesso às fotos.",
'photosync.fail_other': "Backup de fotos parado.",
'contactsync.heading': "Backup de contatos",
- 'contactsync.intro': "Enviar uma cópia dos contatos deste celular para uma pasta de um grupo do qual só você é membro, uma vez por dia se tiverem mudado. Cada cópia é um arquivo .vcf que qualquer celular consegue importar.",
+ 'contactsync.intro': "Enviar uma cópia dos contatos deste celular uma vez por dia se tiverem mudado. Cada cópia é um arquivo .vcf que qualquer celular consegue importar.",
'contactsync.permission_needed': "O MeshBay precisa de acesso aos contatos deste celular.",
- 'contactsync.confirm': "Uma cópia dos seus contatos será enviada para {group}, em {dir}, e depois uma nova a cada dia em que mudarem.\n\nVocê é o único membro deste grupo. Se alguém entrar, o backup para.",
'phonesync.state_sending': "Enviando…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Cópias enviadas: {n}",
'phonesync.last_file': "Última cópia: {name}",
- 'phonesync.checking_groups': "Procurando grupos dos quais só você é membro…",
- 'phonesync.no_private_group': "Você não tem nenhum grupo só seu. Crie um grupo do qual só você seja membro e volte aqui.",
+ 'phonesync.checking_groups': "Procurando grupos dos quais você é dono e único membro…",
+ 'phonesync.no_private_group': "Você não tem nenhum grupo do qual seja dono e único membro. Crie um só para você e volte aqui.",
'phonesync.no_folder': "Este grupo não tem nenhuma pasta onde você possa adicionar arquivos.",
- 'phonesync.destination': "As cópias vão para {dir}",
'phonesync.notice': "{what}. {reason}",
'phonesync.fail_disk_full': "Parado: o disco do node está cheio.",
'phonesync.fail_read_only': "Parado: a pasta não aceita mais envios.",
'phonesync.fail_no_folder': "Parado: a pasta não existe mais no grupo.",
'phonesync.fail_unavailable': "Parado: o disco da pasta não está conectado ao node.",
'phonesync.fail_not_member': "Parado: você não é mais membro deste grupo.",
- 'phonesync.fail_not_private': "Parado: outra pessoa entrou no grupo. Escolha um grupo do qual só você seja membro.",
+ 'phonesync.fail_not_private': "Parado: outra pessoa entrou no grupo.",
'phonesync.fail_permission': "Parado: o MeshBay não tem mais acesso.",
'phonesync.fail_other': "Parado.",
+ 'androidsync.dest_heading': "Destino dos backups",
+ 'androidsync.dest_intro': "Os backups vão para uma pasta de um grupo do qual você é dono e único membro. Cada tipo de backup tem a sua própria pasta lá.",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "Pastas usadas: {dirs}",
+ 'androidsync.dest_change_confirm': "Todos os backups vão recomeçar em {folder} de {group}. O que já foi enviado fica onde está.",
+ 'androidsync.dest_not_in_photos': "A aba Fotos do grupo não mostra {dir}: as fotos serão salvas, mas não aparecerão lá até que essa pasta seja uma das pastas da aba (configurações do grupo).",
+ 'androidsync.dest_needed': "Escolha primeiro o destino dos backups, no topo desta página.",
+ 'androidsync.dest_save': "Salvar",
+ 'phonesync.turn_on': "Ativar",
+ 'photosync.fail_not_private': "Backup de fotos parado: outra pessoa entrou no grupo.",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 14595b1..212bc5d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -1290,7 +1290,6 @@ export default {
'photosync.choose_group': "选择群组",
'photosync.checking_group': "正在读取群组的文件夹…",
'photosync.group_offline': "目前没有托管此群组的 node 在线。请在其上线后重试。",
- 'photosync.no_folder': "此群组没有您可以上传、且其照片标签页会显示的文件夹。",
'photosync.folder': "文件夹",
'photosync.albums': "这部手机上的相册",
'photosync.album_camera': "相机",
@@ -1298,10 +1297,9 @@ export default {
'photosync.scope_existing': "手机上已有的照片,然后是新照片",
'photosync.scope_new': "仅从现在起拍摄的照片",
'photosync.start': "开始备份",
- 'photosync.confirm': "您的照片将发送到 {group}(由 @{owner} 托管,{members} 位成员)的文件夹 {folder}。此群组的所有成员都可以查看和下载这些照片。\n\n将发送 {count} 张照片({size}),之后每天发送一次新照片。\n\n您之后可以删除已发送的内容,但无法删除他人已下载的副本。",
- 'photosync.confirm_new': "您的照片将发送到 {group}(由 @{owner} 托管,{members} 位成员)的文件夹 {folder}。此群组的所有成员都可以查看和下载这些照片。\n\n新照片将每天发送一次。\n\n您之后可以删除已发送的内容,但无法删除他人已下载的副本。",
+ 'photosync.confirm': "将把 {count} 张照片({size})发送到 {dir},之后每天发送一次新照片。",
+ 'photosync.confirm_new': "新照片将每天一次发送到 {dir}。",
'photosync.metered_confirm': "您正在使用移动数据 — {count} 张照片,{size}。仍要发送吗?",
- 'photosync.summary': "{group} (@{owner}) → {folder}",
'photosync.other_account': "已为 @{account} 设置。请以 @{account} 登录以继续,或在此重新设置。",
'photosync.sent_total': "已发送照片:{n}",
'photosync.last_run': "上次备份:{when}",
@@ -1321,24 +1319,32 @@ export default {
'photosync.fail_permission': "照片备份已停止:MeshBay 已无法访问照片。",
'photosync.fail_other': "照片备份已停止。",
'contactsync.heading': "联系人备份",
- 'contactsync.intro': "在联系人有变动的日子里,每天一次将本机联系人的副本发送到一个只有你一个成员的群组的文件夹。每份副本都是任何手机都能导入的 .vcf 文件。",
+ 'contactsync.intro': "在联系人有变动的日子里,每天一次发送本机联系人的副本。每份副本都是任何手机都能导入的 .vcf 文件。",
'contactsync.permission_needed': "MeshBay 需要访问本机的联系人。",
- 'contactsync.confirm': "你的联系人副本将发送到 {group} 的 {dir},之后每逢联系人有变动的日子再发送一份新的。\n\n你是这个群组的唯一成员。如果有人加入,备份将停止。",
'phonesync.state_sending': "正在发送…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "已发送副本:{n}",
'phonesync.last_file': "最近一份副本:{name}",
- 'phonesync.checking_groups': "正在查找只有你一个成员的群组…",
- 'phonesync.no_private_group': "你没有自己专属的群组。请创建一个只有你一个成员的群组,然后回到这里。",
+ 'phonesync.checking_groups': "正在查找由你创建且只有你一个成员的群组…",
+ 'phonesync.no_private_group': "你没有由你创建且只有你一个成员的群组。请创建一个只属于你自己的群组,然后回到这里。",
'phonesync.no_folder': "这个群组没有你可以添加文件的文件夹。",
- 'phonesync.destination': "副本保存到 {dir}",
'phonesync.notice': "{what}。{reason}",
'phonesync.fail_disk_full': "已停止:node 的磁盘已满。",
'phonesync.fail_read_only': "已停止:该文件夹不再接受上传。",
'phonesync.fail_no_folder': "已停止:该文件夹在群组中已不存在。",
'phonesync.fail_unavailable': "已停止:该文件夹所在的磁盘未连接到 node。",
'phonesync.fail_not_member': "已停止:你已不再是这个群组的成员。",
- 'phonesync.fail_not_private': "已停止:有其他人加入了群组。请选择一个只有你一个成员的群组。",
+ 'phonesync.fail_not_private': "已停止:有其他人加入了群组。",
'phonesync.fail_permission': "已停止:MeshBay 已无法访问。",
'phonesync.fail_other': "已停止。",
+ 'androidsync.dest_heading': "备份保存位置",
+ 'androidsync.dest_intro': "备份会保存到一个由你创建且只有你一个成员的群组的文件夹中。每种备份在那里都有自己的文件夹。",
+ 'androidsync.dest_summary': "{group} → {folder}",
+ 'androidsync.dest_dirs': "使用的文件夹:{dirs}",
+ 'androidsync.dest_change_confirm': "所有备份将在 {group} 的 {folder} 中重新开始。已发送的内容保留在原处。",
+ 'androidsync.dest_not_in_photos': "该群组的照片标签页不显示 {dir}:照片会被备份,但在该文件夹加入标签页的文件夹之前(群组设置)不会在那里显示。",
+ 'androidsync.dest_needed': "请先在本页顶部选择备份保存位置。",
+ 'androidsync.dest_save': "保存",
+ 'phonesync.turn_on': "开启",
+ 'photosync.fail_not_private': "照片备份已停止:有其他人加入了群组。",
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
index 7df83b9..cec8b43 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
@@ -1,19 +1,17 @@
// The contacts 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.
+// only (docs/MESHBAY_DESIGN.md §9.13). Loaded on demand: no other client has
+// it. Where the copies go is the page's destination section; this one only
+// turns the backup on or off.
import { html, useState, useEffect, useCallback } from './vendor/htm-preact.js';
import { t } from './i18n.js';
-import { ask } from './ask.js';
-import {
- KINDS, subscribe, runDocSync, privateGroups, probeWritable, failureText,
-} from './phone-sync.js';
+import { KINDS, subscribe, runDocSync, failureText } from './phone-sync.js';
// What each kind says in its own words; everything else is shared.
const TEXT = {
contacts: {
intro: 'contactsync.intro',
permission: 'contactsync.permission_needed',
- confirm: 'contactsync.confirm',
},
};
@@ -28,11 +26,11 @@ function stateLine(st, status) {
}
}
-export function DocSyncSection({ kind, user, groups }) {
+export function DocSyncSection({ kind, destination }) {
const { sync, heading } = KINDS[kind];
const [status, setStatus] = useState(null);
const [run, setRun] = useState({ phase: 'idle' });
- const [editing, setEditing] = useState(false);
+ const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const refresh = useCallback(() => sync.status().then(setStatus).catch(() => {}), [sync]);
@@ -43,12 +41,22 @@ export function DocSyncSection({ kind, user, groups }) {
}), [kind, refresh]);
if (!status) return null;
- const cfg = status.config;
- const busy = run.phase === 'sending' || run.phase === 'connecting';
+ const running = run.phase === 'sending' || run.phase === 'connecting';
- const backUpNow = async () => {
+ const turnOn = async () => {
+ setBusy(true);
setError('');
- try { await runDocSync(kind, { force: true }); } catch (e) { setError(e.message); }
+ try {
+ let st = status;
+ if (st.permission !== 'granted') st = await sync.permit();
+ if (st.permission !== 'granted') { setStatus(st); return; }
+ setStatus(await sync.configure({}));
+ runDocSync(kind);
+ } catch (e) {
+ setError(e.message);
+ } finally {
+ setBusy(false);
+ }
};
const turnOff = async () => {
@@ -56,21 +64,30 @@ export function DocSyncSection({ kind, user, groups }) {
try { setStatus(await sync.configure(null)); } catch (e) { setError(e.message); }
};
+ const backUpNow = async () => {
+ setError('');
+ try { await runDocSync(kind, { force: true }); } catch (e) { setError(e.message); }
+ };
+
return html`
<div class="settings-section">
<h3 class="settings-heading">${t(heading)}</h3>
- ${!cfg && !editing && html`
+ ${!status.on && html`
<p class="settings-hint">${t(TEXT[kind].intro)}</p>
- <button class="admin-btn" onClick=${() => setEditing(true)}>${t('photosync.setup')}</button>
+ ${!destination && html`<p class="settings-hint">${t('androidsync.dest_needed')}</p>`}
+ ${destination && status.permission !== 'granted' && html`
+ <p class="settings-hint">${t(TEXT[kind].permission)}</p>`}
+ ${destination && html`
+ <button class="admin-btn" disabled=${busy} onClick=${turnOn}>${t('phonesync.turn_on')}</button>`}
`}
- ${cfg && !editing && html`
- <div class="settings-row">
- <span class="settings-label settings-name">
- ${t('phonesync.summary', { group: cfg.groupName, dir: status.dir })}
- </span>
- </div>
- ${cfg.account !== user.username && html`
- <p class="settings-hint">${t('photosync.other_account', { account: cfg.account })}</p>`}
+ ${status.on && html`
+ ${destination && html`
+ <div class="settings-row">
+ <span class="settings-label settings-name">
+ ${t('phonesync.summary', { group: destination.groupName, dir: status.dir })}
+ </span>
+ </div>`}
+ ${!destination && html`<p class="settings-hint">${t('androidsync.dest_needed')}</p>`}
<p class="settings-hint">
${t('phonesync.copies_sent', { n: status.sent })}
${' · '}
@@ -84,126 +101,11 @@ export function DocSyncSection({ kind, user, groups }) {
? 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=${busy} onClick=${backUpNow}>${t('photosync.now')}</button>
- <button class="btn-secondary" onClick=${() => setEditing(true)}>${t('photosync.change')}</button>
+ <button class="admin-btn" disabled=${running || !destination}
+ onClick=${backUpNow}>${t('photosync.now')}</button>
<button class="btn-secondary" onClick=${turnOff}>${t('photosync.turn_off')}</button>
</div>
`}
- ${editing && html`
- <${Setup} kind=${kind} user=${user} groups=${groups} status=${status}
- onPermission=${setStatus}
- onDone=${(next) => { setEditing(false); if (next) { setStatus(next); runDocSync(kind); } }} />`}
- ${error && html`<p class="error-msg">${error}</p>`}
- </div>
- `;
-}
-
-function Setup({ kind, user, groups, status, onPermission, onDone }) {
- const { sync, suffix } = KINDS[kind];
- const cfg = status.config && status.config.account === user.username ? status.config : null;
- const [candidates, setCandidates] = useState(null); // null while the hub is asked
- const [groupId, setGroupId] = useState(cfg ? cfg.groupId : '');
- const [folders, setFolders] = useState(null);
- const [probe, setProbe] = useState(''); // '', 'checking', 'offline'
- const [folder, setFolder] = useState(cfg ? cfg.folder : '');
- const [busy, setBusy] = useState(false);
- const [error, setError] = useState('');
- const permitted = status.permission === 'granted';
-
- // Only groups this account is alone in: what is sent is readable by every
- // member, and an address book or a conversation is not the group's.
- useEffect(() => {
- let gone = false;
- privateGroups(groups).then((list) => {
- if (gone) return;
- setCandidates(list);
- setGroupId((g) => (list.some((x) => x.id === g) ? g : ''));
- });
- return () => { gone = true; };
- }, [groups]);
-
- useEffect(() => {
- setFolders(null);
- if (!groupId) return undefined;
- let gone = false;
- setProbe('checking');
- probeWritable(groupId).then((r) => {
- if (gone) return;
- setProbe('');
- setFolders(r.folders);
- setFolder((f) => (r.folders.includes(f) ? f : (r.folders[0] || '')));
- }).catch(() => { if (!gone) setProbe('offline'); });
- return () => { gone = true; };
- }, [groupId]);
-
- const allow = async () => {
- setError('');
- try { onPermission(await sync.permit()); } catch (e) { setError(e.message); }
- };
-
- const dir = folder ? `${folder}/${user.username}-${suffix}` : '';
-
- const start = async () => {
- const group = (candidates || []).find((g) => g.id === groupId);
- if (!group || !folder) return;
- setBusy(true);
- setError('');
- try {
- if (!cfg || cfg.groupId !== groupId || cfg.folder !== folder) {
- const yes = await ask(t(TEXT[kind].confirm, { group: group.name, dir }));
- if (!yes) { setBusy(false); return; }
- }
- onDone(await sync.configure({
- account: user.username, groupId, groupName: group.name,
- owner: group.owner_username || '', folder,
- }));
- } catch (e) {
- setError(e.message);
- setBusy(false);
- }
- };
-
- return html`
- <div>
- ${!permitted && html`
- <p class="settings-hint">${t(TEXT[kind].permission)}</p>
- <div class="settings-row" style="gap:8px; justify-content:flex-start">
- <button class="admin-btn" onClick=${allow}>${t('photosync.permission_allow')}</button>
- <button class="btn-secondary" onClick=${() => onDone(null)}>${t('dialog.cancel')}</button>
- </div>
- `}
- ${permitted && html`
- ${candidates === null && html`<p class="settings-hint">${t('phonesync.checking_groups')}</p>`}
- ${candidates && !candidates.length && html`
- <p class="settings-hint">${t('phonesync.no_private_group')}</p>`}
- ${candidates && candidates.length > 0 && html`
- <div class="settings-row">
- <span class="settings-label">${t('photosync.group')}</span>
- <select class="settings-select" value=${groupId}
- onChange=${(e) => setGroupId(e.target.value)}>
- <option value="">${t('photosync.choose_group')}</option>
- ${candidates.map((g) => html`<option key=${g.id} value=${g.id}>${g.name}</option>`)}
- </select>
- </div>
- `}
- ${probe === 'checking' && html`<p class="settings-hint">${t('photosync.checking_group')}</p>`}
- ${probe === 'offline' && html`<p class="settings-hint">${t('photosync.group_offline')}</p>`}
- ${folders && !folders.length && html`<p class="settings-hint">${t('phonesync.no_folder')}</p>`}
- ${folders && folders.length > 0 && html`
- <div class="settings-row">
- <span class="settings-label">${t('photosync.folder')}</span>
- <select class="settings-select" value=${folder} onChange=${(e) => setFolder(e.target.value)}>
- ${folders.map((f) => html`<option key=${f} value=${f}>${f}</option>`)}
- </select>
- </div>
- <p class="settings-hint settings-name">${t('phonesync.destination', { dir })}</p>
- `}
- <div class="settings-row" style="gap:8px; justify-content:flex-start">
- <button class="admin-btn" onClick=${start}
- disabled=${busy || !groupId || !folder}>${t('photosync.start')}</button>
- <button class="btn-secondary" onClick=${() => onDone(null)}>${t('dialog.cancel')}</button>
- </div>
- `}
${error && html`<p class="error-msg">${error}</p>`}
</div>
`;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js
index 964f76c..b9db9f7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js
@@ -1,6 +1,6 @@
-// Contacts backup: a copy of this phone's address book sent to a folder of a
-// group only its owner belongs to, once a day when it changed
-// (docs/MESHBAY_DESIGN.md §9.13).
+// Contacts backup: a copy of this phone's address book sent to
+// `<folder>/<account>-contacts` of the destination every backup shares
+// (sync-destination.js), once a day when it changed (docs/MESHBAY_DESIGN.md §9.13).
//
// The same split as photos (photo-sync.js): the phone writes what is new into
// one file and remembers what the node took; this module decides when a run
@@ -15,6 +15,7 @@ import { ConnectionPool } from './connection-pool.js';
import * as platform from './platform.js';
import { t } from './i18n.js';
import { destinationProblem, makeDirs, upload } from './photo-sync.js';
+import { setSyncContext, checkDestination } from './sync-destination.js';
const DAY_MS = 24 * 3600 * 1000;
// A run cut short by something passing is tried again at the next chance,
@@ -79,7 +80,7 @@ function available() {
/** True when `status` says a scheduled run should go now. */
export function isDue(status, now = Date.now()) {
- if (!status || !status.config) return false;
+ if (!status || !status.on || !status.destination) return false;
if (status.failure && status.failureAt && now - status.failureAt < DAY_MS) return false;
const last = status.lastCompleted;
return last == null || now - last >= DAY_MS || now < last;
@@ -89,6 +90,7 @@ export function isDue(status, now = Date.now()) {
export function startPhoneSync(context) {
if (ctx || !available().length) return;
ctx = context;
+ setSyncContext(context);
document.addEventListener('visibilitychange', onVisible);
for (const kind of available()) runDocSync(kind);
}
@@ -107,7 +109,7 @@ function onVisible() {
function scheduleNext(kind, status) {
clearTimeout(timers[kind]);
- if (!ctx || !status || !status.config) return;
+ if (!ctx || !status || !status.on || !status.destination) return;
const base = Math.max(status.lastCompleted || 0, status.failure ? (status.failureAt || 0) : 0);
const wait = base + DAY_MS - Date.now();
if (wait > 0) timers[kind] = setTimeout(() => runDocSync(kind), Math.min(wait, DAY_MS));
@@ -136,9 +138,9 @@ export function runDocSync(kind, { force = false } = {}) {
}
async function run(kind, sync, status, force) {
- const cfg = status.config;
+ const cfg = status.destination;
const user = ctx && ctx.getUser();
- if (!cfg) { setState(kind, { phase: 'off' }); return; }
+ if (!status.on || !cfg) { setState(kind, { phase: 'off' }); return; }
if (!user || cfg.account !== user.username) { setState(kind, { phase: 'other_account' }); return; }
if (status.permission !== 'granted') { await lasting(kind, sync, 'permission'); return; }
if (!force && !isDue(status)) {
@@ -148,18 +150,11 @@ async function run(kind, sync, status, force) {
if (!force && Date.now() - (lastAttempt[kind] || 0) < RETRY_MS) return;
lastAttempt[kind] = Date.now();
- // Asked of the hub at every run, not only at set-up: somebody who joins the
- // group later would otherwise read every copy from then on.
+ // Before anything is read: still this account's own group, and nobody else's.
setState(kind, { phase: 'connecting' });
- let alone;
- try {
- alone = await isPrivate(cfg.groupId, user);
- } catch (e) {
- if (/not a member|not found/i.test(e.message || '')) { await lasting(kind, sync, 'not_a_member'); return; }
- setState(kind, { phase: 'offline' });
- return;
- }
- if (!alone) { await lasting(kind, sync, 'not_private'); return; }
+ const refused = await checkDestination(cfg, user);
+ if (refused === 'offline') { setState(kind, { phase: 'offline' }); return; }
+ if (refused) { await lasting(kind, sync, refused); return; }
const { item } = await sync.plan();
if (!item) {
@@ -207,43 +202,3 @@ async function lasting(kind, sync, code) {
const text = t('phonesync.notice', { what: t(KINDS[kind].heading), reason: failureText(code) });
await sync.failed(code, text).catch(() => {});
}
-
-// ── For the sections ────────────────────────────────────────────────────────
-
-/** True when this account is the group's one member and nobody is invited. */
-export async function isPrivate(groupId, user) {
- const r = await ctx.hubFetch(`/v1/groups/${groupId}/members`, { token: user.token });
- const members = r.members || [];
- return members.length === 1 && members[0].username === user.username
- && !(r.invited || []).length;
-}
-
-/** The groups among `groups` this account is alone in; a failed look leaves one out. */
-export async function privateGroups(groups) {
- const user = ctx && ctx.getUser();
- if (!user) return [];
- const alone = await Promise.all(groups.map((g) => isPrivate(g.id, user).catch(() => false)));
- return groups.filter((_, i) => alone[i]);
-}
-
-/** Every folder a member can add to: on a writable root whose drive is there. */
-export function writableFolders(index) {
- const roots = new Set((index.roots || [])
- .filter((r) => r.writable && r.available !== false).map((r) => r.name));
- const all = new Set([...(index.dirs || []), ...roots]);
- return [...all].filter((d) => roots.has(d.split('/')[0])).sort((a, b) => a.localeCompare(b));
-}
-
-/** A one-off look at a group, for choosing a folder: connected, read, closed. */
-export async function probeWritable(groupId) {
- const user = ctx && ctx.getUser();
- if (!user) throw new Error('Not signed in');
- const pool = new ConnectionPool(ctx.hub);
- try {
- const bundleKey = await ctx.getBundleKey();
- const conn = await pool.connect(groupId, user.token, bundleKey, user.username, user.userId);
- return { folders: writableFolders(await conn.transport.fetchIndex()) };
- } finally {
- pool.closeAll();
- }
-}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js
index b5e9a30..7809efa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js
@@ -1,14 +1,14 @@
// The "Photo backup" section of the Android Sync page, on the Android
// application only (docs/MESHBAY_DESIGN.md §9.12). Loaded on demand: no other
-// client has it.
+// client has it. Where the photos go is the page's destination section;
+// this one chooses which.
import { html, useState, useEffect, useCallback } from './vendor/htm-preact.js';
import { t } from './i18n.js';
import * as platform from './platform.js';
-import { hubFetch } from './hub-client.js';
import { formatSize } from './file-utils.js';
import { ask } from './ask.js';
-import { subscribe, runPhotoSync, probeGroup } from './photo-sync.js';
+import { subscribe, runPhotoSync } from './photo-sync.js';
function stateLine(st, status) {
switch (st.phase) {
@@ -30,10 +30,11 @@ const FAILURES = {
no_such_directory: 'photosync.fail_no_folder',
root_unavailable: 'photosync.fail_unavailable',
not_a_member: 'photosync.fail_not_member',
+ not_private: 'photosync.fail_not_private',
permission: 'photosync.fail_permission',
};
-export function PhotoSyncSection({ user, groups }) {
+export function PhotoSyncSection({ destination }) {
const [status, setStatus] = useState(null);
const [run, setRun] = useState({ phase: 'idle' });
const [editing, setEditing] = useState(false);
@@ -51,6 +52,16 @@ export function PhotoSyncSection({ user, groups }) {
if (!status) return null;
const cfg = status.config;
+ if (!destination) {
+ return html`
+ <div class="settings-section">
+ <h3 class="settings-heading">${t('photosync.heading')}</h3>
+ <p class="settings-hint">${t('photosync.intro')}</p>
+ <p class="settings-hint">${t('androidsync.dest_needed')}</p>
+ </div>
+ `;
+ }
+
const backUpNow = async () => {
setError('');
try {
@@ -82,11 +93,9 @@ export function PhotoSyncSection({ user, groups }) {
${cfg && !editing && html`
<div class="settings-row">
<span class="settings-label settings-name">
- ${t('photosync.summary', { group: cfg.groupName, owner: cfg.owner, folder: cfg.folder })}
+ ${t('phonesync.summary', { group: destination.groupName, dir: status.dir })}
</span>
</div>
- ${cfg.account !== user.username && html`
- <p class="settings-hint">${t('photosync.other_account', { account: cfg.account })}</p>`}
<p class="settings-hint">
${t('photosync.sent_total', { n: status.sent })}
${' · '}
@@ -107,7 +116,7 @@ export function PhotoSyncSection({ user, groups }) {
</div>
`}
${editing && html`
- <${Setup} user=${user} groups=${groups} status=${status}
+ <${Setup} status=${status}
onPermission=${setStatus}
onDone=${(next) => { setEditing(false); if (next) { setStatus(next); runPhotoSync(); } }} />`}
${error && html`<p class="error-msg">${error}</p>`}
@@ -115,12 +124,8 @@ export function PhotoSyncSection({ user, groups }) {
`;
}
-function Setup({ user, groups, status, onPermission, onDone }) {
- const cfg = status.config && status.config.account === user.username ? status.config : null;
- const [groupId, setGroupId] = useState(cfg ? cfg.groupId : '');
- const [folders, setFolders] = useState(null); // null while unknown
- const [probe, setProbe] = useState(''); // '', 'checking', 'offline'
- const [folder, setFolder] = useState(cfg ? cfg.folder : '');
+function Setup({ status, onPermission, onDone }) {
+ const cfg = status.config;
const [albums, setAlbums] = useState(null);
const [chosen, setChosen] = useState(() => new Set(cfg ? cfg.albums : []));
const [existing, setExisting] = useState(cfg ? cfg.includeExisting : true);
@@ -138,20 +143,6 @@ function Setup({ user, groups, status, onPermission, onDone }) {
}).catch((e) => setError(e.message));
}, [permitted]);
- useEffect(() => {
- setFolders(null);
- if (!groupId) return;
- let gone = false;
- setProbe('checking');
- probeGroup(groupId).then((r) => {
- if (gone) return;
- setProbe('');
- setFolders(r.folders);
- setFolder((f) => (r.folders.includes(f) ? f : (r.folders[0] || '')));
- }).catch(() => { if (!gone) setProbe('offline'); });
- return () => { gone = true; };
- }, [groupId]);
-
const allow = async () => {
setError('');
try { onPermission(await platform.photoSync.permit()); } catch (e) { setError(e.message); }
@@ -164,30 +155,16 @@ function Setup({ user, groups, status, onPermission, onDone }) {
});
const start = async () => {
- const group = groups.find((g) => g.id === groupId);
- if (!group || !folder || !chosen.size) return;
+ if (!chosen.size) return;
setBusy(true);
setError('');
try {
- const settings = {
- account: user.username, groupId, groupName: group.name,
- owner: group.owner_username || '', folder, albums: [...chosen], includeExisting: existing,
- };
- // Asked when the photos would go somewhere new, or from somewhere new in
- // time — never for a change of albums alone, which the person just made.
- const same = cfg && cfg.groupId === groupId && cfg.folder === folder
- && cfg.includeExisting === existing;
- if (!same) {
- const [est, members] = await Promise.all([
- platform.photoSync.estimate(settings),
- hubFetch(`/v1/groups/${groupId}/members`, { token: user.token })
- .then((r) => (r.members || []).length).catch(() => null),
- ]);
- const params = {
- group: group.name, owner: group.owner_username || '', folder,
- members: members == null ? '?' : members,
- count: est.count, size: formatSize(est.bytes),
- };
+ const settings = { albums: [...chosen], includeExisting: existing };
+ // Asked when the photos would come from somewhere new in time; never
+ // for a change of albums alone, which the person just made.
+ if (!cfg || cfg.includeExisting !== existing) {
+ const est = await platform.photoSync.estimate(settings);
+ const params = { count: est.count, size: formatSize(est.bytes), dir: status.dir };
const yes = await ask(t(existing && est.count > 0 ? 'photosync.confirm'
: 'photosync.confirm_new', params));
if (!yes) { setBusy(false); return; }
@@ -210,26 +187,6 @@ function Setup({ user, groups, status, onPermission, onDone }) {
<button class="btn-secondary" onClick=${allow}>${t('photosync.permission_allow')}</button>
`}
${permitted && html`
- <div class="settings-row">
- <span class="settings-label">${t('photosync.group')}</span>
- <select class="settings-select" value=${groupId}
- onChange=${(e) => setGroupId(e.target.value)}>
- <option value="">${t('photosync.choose_group')}</option>
- ${groups.map((g) => html`<option key=${g.id} value=${g.id}>
- ${g.owner_username ? `${g.name} (@${g.owner_username})` : g.name}</option>`)}
- </select>
- </div>
- ${probe === 'checking' && html`<p class="settings-hint">${t('photosync.checking_group')}</p>`}
- ${probe === 'offline' && html`<p class="settings-hint">${t('photosync.group_offline')}</p>`}
- ${folders && !folders.length && html`<p class="settings-hint">${t('photosync.no_folder')}</p>`}
- ${folders && folders.length > 0 && html`
- <div class="settings-row">
- <span class="settings-label">${t('photosync.folder')}</span>
- <select class="settings-select" value=${folder} onChange=${(e) => setFolder(e.target.value)}>
- ${folders.map((f) => html`<option key=${f} value=${f}>${f}</option>`)}
- </select>
- </div>
- `}
<p class="settings-label" style="margin-top:10px">${t('photosync.albums')}</p>
${(albums || []).map((a) => html`
<label class="settings-choice" key=${a.id}>
@@ -253,7 +210,7 @@ function Setup({ user, groups, status, onPermission, onDone }) {
</label>
<div class="settings-row" style="gap:8px; justify-content:flex-start">
<button class="admin-btn" onClick=${start}
- disabled=${busy || !groupId || !folder || !chosen.size}>${t('photosync.start')}</button>
+ disabled=${busy || !chosen.size}>${t('photosync.start')}</button>
<button class="btn-secondary" onClick=${() => onDone(null)}>${t('dialog.cancel')}</button>
</div>
`}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
index 77d050e..9c59c17 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
@@ -1,5 +1,6 @@
-// Photo backup: this phone's photos sent to one folder of one group, once a
-// day, on an unmetered network (docs/MESHBAY_DESIGN.md §9.12).
+// Photo backup: this phone's photos sent to `<folder>/<account>-photos` of the
+// destination every backup shares (sync-destination.js), once a day, on an
+// unmetered network (docs/MESHBAY_DESIGN.md §9.12).
//
// The phone (platform.photoSync) lists the photos, keeps the ledger of what was
// sent and hands each photo's bytes over; this module decides when a run is
@@ -14,6 +15,7 @@
import { ConnectionPool } from './connection-pool.js';
import * as platform from './platform.js';
import { t } from './i18n.js';
+import { setSyncContext, checkDestination } from './sync-destination.js';
const DAY_MS = 24 * 3600 * 1000;
// A run cut short by something passing — the node offline, the network gone —
@@ -34,6 +36,7 @@ const LASTING = {
no_such_directory: 'photosync.fail_no_folder',
root_unavailable: 'photosync.fail_unavailable',
not_a_member: 'photosync.fail_not_member',
+ not_private: 'photosync.fail_not_private',
permission: 'photosync.fail_permission',
};
@@ -57,7 +60,7 @@ export function subscribe(fn) {
// ── When it runs ────────────────────────────────────────────────────────────
-let ctx = null; // { hub, getUser, getBundleKey }
+let ctx = null; // { hub, hubFetch, getUser, getBundleKey }
let running = null;
let timer = 0;
let lastAttempt = 0;
@@ -65,7 +68,7 @@ let unmeteredNow = null;
/** True when `status` says a scheduled run should go now. */
export function isDue(status, now = Date.now()) {
- if (!status || !status.config) return false;
+ if (!status || !status.config || !status.destination) return false;
// A lasting refusal waits a day, like a finished run; anything else is
// decided by the last run that finished.
if (status.failure && status.failureAt && now - status.failureAt < DAY_MS) return false;
@@ -80,6 +83,7 @@ export function isDue(status, now = Date.now()) {
export function startPhotoSync(context) {
if (!platform.photoSync.available || ctx) return;
ctx = context;
+ setSyncContext(context);
document.addEventListener('visibilitychange', onVisible);
window.addEventListener('meshbay-network', onNetwork);
runPhotoSync();
@@ -104,7 +108,7 @@ function onNetwork(e) {
function scheduleNext(status) {
clearTimeout(timer);
- if (!ctx || !status || !status.config) return;
+ if (!ctx || !status || !status.config || !status.destination) return;
const base = Math.max(status.lastCompleted || 0, status.failure ? (status.failureAt || 0) : 0);
const wait = base + DAY_MS - Date.now();
// A page left open past midnight runs on time; the cap keeps a timer from
@@ -137,10 +141,10 @@ export function runPhotoSync({ force = false, metered = false } = {}) {
}
async function run(status, force, metered) {
- const cfg = status.config;
+ const dest = status.destination;
const user = ctx && ctx.getUser();
- if (!cfg) { setState({ phase: 'off' }); return; }
- if (!user || cfg.account !== user.username) { setState({ phase: 'other_account' }); return; }
+ if (!status.config || !dest) { setState({ phase: 'off' }); return; }
+ if (!user || dest.account !== user.username) { setState({ phase: 'other_account' }); return; }
if (status.permission === 'denied') { await lasting('permission'); return; }
if (!force && !isDue(status)) {
setState({ phase: status.failure ? 'failed' : 'up_to_date', reason: status.failure || '' });
@@ -151,6 +155,12 @@ async function run(status, force, metered) {
if (!force && Date.now() - lastAttempt < RETRY_MS) return;
lastAttempt = Date.now();
+ // Before anything is read: still this account's own group, and nobody else's.
+ setState({ phase: 'connecting', done: 0, total: 0 });
+ const refused = await checkDestination(dest, user);
+ if (refused === 'offline') { setState({ phase: 'offline' }); return; }
+ if (refused) { await lasting(refused); return; }
+
const { items } = await platform.photoSync.plan();
if (!items.length) {
await platform.photoSync.completed();
@@ -163,7 +173,7 @@ async function run(status, force, metered) {
let conn;
try {
const bundleKey = await ctx.getBundleKey();
- conn = await pool.connect(cfg.groupId, user.token, bundleKey, user.username, user.userId);
+ conn = await pool.connect(dest.groupId, user.token, bundleKey, user.username, user.userId);
} catch (e) {
pool.closeAll();
if (e.reason === 'not_a_member') { await lasting('not_a_member'); return; }
@@ -172,16 +182,16 @@ async function run(status, force, metered) {
}
try {
- await send(conn.transport, cfg, items, metered);
+ await send(conn.transport, dest, items, metered);
} finally {
platform.photoSync.keepAlive(false).catch(() => {});
pool.closeAll();
}
}
-async function send(transport, cfg, items, metered) {
+async function send(transport, dest, items, metered) {
const index = await transport.fetchIndex();
- const problem = destinationProblem(cfg.folder, index);
+ const problem = destinationProblem(dest.folder, index);
if (problem) { await lasting(problem); return; }
// What is already in each folder, by name and size: after a reinstall the
@@ -214,7 +224,7 @@ async function send(transport, cfg, items, metered) {
await platform.photoSync.sent(item.token, item.dir, known);
} else {
try {
- await makeDirs(transport, cfg.folder, item.dir, dirs);
+ await makeDirs(transport, dest.folder, item.dir, dirs);
const ack = await upload(transport, item);
await platform.photoSync.sent(item.token, ack.dir || item.dir, ack.stored_as || item.name);
} catch (e) {
@@ -252,7 +262,7 @@ export async function upload(transport, item, file = platform.photoSync.file) {
}
}
-/** `<folder>/YYYY/YYYY-MM`, one level at a time; a level that exists is fine. */
+/** `<folder>/<account>-photos/YYYY/YYYY-MM`, one level at a time; a level that exists is fine. */
export async function makeDirs(transport, folder, dir, dirs) {
let at = folder;
for (const name of dir.slice(folder.length + 1).split('/').filter(Boolean)) {
@@ -284,36 +294,3 @@ async function lasting(code) {
setState({ phase: 'failed', reason: code });
await platform.photoSync.failed(code, t(LASTING[code] || 'photosync.fail_other')).catch(() => {});
}
-
-// ── For the settings section ────────────────────────────────────────────────
-
-/**
- * The folders of a group this member can back up into: writable, available,
- * and shown by the group's Photos tab — a folder outside those would take the
- * photos and show them nowhere, which reads as a backup that failed.
- */
-export function backupFolders(ack, index) {
- const photoDirs = (ack && ack.photo_directories) || [];
- const roots = new Map((index.roots || [])
- .filter((r) => r.writable && r.available !== false).map((r) => [r.name, r]));
- const under = (d) => photoDirs.some((p) => d === p || d.startsWith(`${p}/`));
- const all = new Set([...(index.dirs || []), ...roots.keys()]);
- return [...all]
- .filter((d) => roots.has(d.split('/')[0]) && under(d))
- .sort((a, b) => a.localeCompare(b));
-}
-
-/** A one-off look at a group, for choosing a folder: connected, read, closed. */
-export async function probeGroup(groupId) {
- const user = ctx && ctx.getUser();
- if (!user) throw new Error('Not signed in');
- const pool = new ConnectionPool(ctx.hub);
- try {
- const bundleKey = await ctx.getBundleKey();
- const conn = await pool.connect(groupId, user.token, bundleKey, user.username, user.userId);
- const index = await conn.transport.fetchIndex();
- return { folders: backupFolders(conn.ack, index) };
- } finally {
- pool.closeAll();
- }
-}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index 1715d5f..148b2eb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -256,9 +256,20 @@ export const photoSync = {
};
/**
- * Backing up one kind of this phone's data (the contacts) to a group of
- * which the person is the only member (docs/MESHBAY_DESIGN.md §9.13). Android
- * only, like `photoSync`. See phone-sync.js.
+ * Where every backup of this phone goes: one folder of a group the account
+ * owns and is alone in (docs/MESHBAY_DESIGN.md §9.12). Android only, like
+ * `photoSync`. See sync-destination.js.
+ */
+export const phoneSync = {
+ available: Boolean(bridge && bridge.phoneSync),
+ destination() { return bridge.phoneSync.destination(); },
+ setDestination(d) { return bridge.phoneSync.setDestination(d); },
+};
+
+/**
+ * Backing up one kind of this phone's data (the contacts) to the destination
+ * every kind shares (docs/MESHBAY_DESIGN.md §9.13). Android only, like
+ * `photoSync`. See phone-sync.js.
*/
function docSync(name) {
const b = bridge && bridge[name];
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js b/packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js
new file mode 100644
index 0000000..2093098
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/sync-destination.js
@@ -0,0 +1,85 @@
+// Where the Android application's backups go (docs/MESHBAY_DESIGN.md §9.12):
+// one folder of one group the account owns and is the only member of, shared
+// by every kind of data. Each kind goes into its own folder under it,
+// `<folder>/<account>-<kind>`, named by the phone.
+//
+// What a group's folder holds every member can read; photos, an address book
+// or a conversation are not the group's. So the group is checked when it is
+// chosen and again at every run, before anything is read: somebody who joins
+// later stops the backups rather than reading them from then on.
+
+import { ConnectionPool } from './connection-pool.js';
+import * as platform from './platform.js';
+
+let ctx = null; // { hub, hubFetch, getUser, getBundleKey }
+
+/** Set by whichever runner starts first; both are handed the same context. */
+export function setSyncContext(context) { ctx = context; }
+
+export const destination = {
+ get: () => platform.phoneSync.destination(),
+ set: (d) => platform.phoneSync.setDestination(d),
+};
+
+/** True when this account owns the group, is its one member, and nobody is invited. */
+export async function isPrivate(groupId, user) {
+ const r = await ctx.hubFetch(`/v1/groups/${groupId}/members`, { token: user.token });
+ const members = r.members || [];
+ return members.length === 1 && members[0].user_id === user.userId
+ && r.admin_id === user.userId && !(r.invited || []).length;
+}
+
+/**
+ * Before a run reads anything: null when the destination may be written to,
+ * otherwise `not_private` or `not_a_member` (lasting) or `offline`.
+ */
+export async function checkDestination(dest, user) {
+ try {
+ return await isPrivate(dest.groupId, user) ? null : 'not_private';
+ } catch (e) {
+ return /not a member|not found/i.test(e.message || '') ? 'not_a_member' : 'offline';
+ }
+}
+
+/** The groups among `groups` this account owns and is alone in; a failed look leaves one out. */
+export async function privateGroups(groups) {
+ const user = ctx && ctx.getUser();
+ if (!user) return [];
+ const alone = await Promise.all(groups.map((g) => isPrivate(g.id, user).catch(() => false)));
+ return groups.filter((_, i) => alone[i]);
+}
+
+/** Every folder a member can add to: on a writable root whose drive is there. */
+export function writableFolders(index) {
+ const roots = new Set((index.roots || [])
+ .filter((r) => r.writable && r.available !== false).map((r) => r.name));
+ const all = new Set([...(index.dirs || []), ...roots]);
+ return [...all].filter((d) => roots.has(d.split('/')[0])).sort((a, b) => a.localeCompare(b));
+}
+
+/** True when the group's Photos tab shows `dir`. */
+export function photosTabShows(dir, photoDirectories) {
+ return (photoDirectories || []).some((p) => dir === p || dir.startsWith(`${p}/`));
+}
+
+/**
+ * A one-off look at a group, for choosing a folder: connected, read, closed.
+ * `photoDirectories` is what its Photos tab shows, for saying when photos sent
+ * there would show nowhere.
+ */
+export async function probeGroup(groupId) {
+ const user = ctx && ctx.getUser();
+ if (!user) throw new Error('Not signed in');
+ const pool = new ConnectionPool(ctx.hub);
+ try {
+ const bundleKey = await ctx.getBundleKey();
+ const conn = await pool.connect(groupId, user.token, bundleKey, user.username, user.userId);
+ const index = await conn.transport.fetchIndex();
+ return {
+ folders: writableFolders(index),
+ photoDirectories: (conn.ack && conn.ack.photo_directories) || [],
+ };
+ } finally {
+ pool.closeAll();
+ }
+}