aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-10 15:11:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-10 15:11:52 +0200
commit1fd7bbcd038cddf40540bae7083b9adf4c728912 (patch)
tree00574a2ce5c3f592c71d5a339dde9fb263636eeb /packages/meshbay-hub/src/meshbay_hub
parent3bad2b50187f55463588a2d4e74fa9df4beb35b8 (diff)
downloadmeshbay-1fd7bbcd038cddf40540bae7083b9adf4c728912.tar.gz
feat(android): back the videos of the chosen albums up beside the photos
A 'Videos too' option, off by default, sends them into the same YYYY/YYYY-MM folders. Files are read from the phone a ranged chunk at a time, so a large video is never whole in the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photo-sync-settings.js57
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js28
13 files changed, 118 insertions, 19 deletions
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 5f508da..ca15345 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -1313,6 +1313,11 @@ export default {
'photosync.albums': "Alben auf diesem Telefon",
'photosync.album_camera': "Kamera",
'photosync.album_count': "{n} Fotos · {size}",
+ 'photosync.include_videos': "Auch Videos",
+ 'photosync.include_videos_hint': "Videos sind groß: Sie werden wie Fotos über WLAN gesendet, und die erste Sicherung kann dauern.",
+ 'photosync.album_count_videos': "{n} Fotos, {v} Videos · {size}",
+ 'photosync.confirm_videos': "{count} Fotos und Videos ({size}), davon {videos} Videos ({videoSize}), werden nach {dir} gesendet, danach einmal täglich die neuen.",
+ 'photosync.video_permission_needed': "MeshBay braucht auch Zugriff auf die Videos dieses Telefons.",
'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",
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 6b7d73f..8619bdb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -1294,6 +1294,11 @@ export default {
'photosync.albums': "Albums on this phone",
'photosync.album_camera': "Camera",
'photosync.album_count': "{n} photos · {size}",
+ 'photosync.include_videos': "Videos too",
+ 'photosync.include_videos_hint': "Videos are large: they go on Wi-Fi like photos, and the first backup can take a while.",
+ 'photosync.album_count_videos': "{n} photos, {v} videos · {size}",
+ 'photosync.confirm_videos': "{count} photos and videos ({size}), including {videos} videos ({videoSize}), will be sent to {dir}, then new ones once a day.",
+ 'photosync.video_permission_needed': "MeshBay needs access to this phone's videos too.",
'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",
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 f2c1cfc..19db8f7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -1307,6 +1307,11 @@ export default {
'photosync.albums': "Álbumes de este teléfono",
'photosync.album_camera': "Cámara",
'photosync.album_count': "{n} fotos · {size}",
+ 'photosync.include_videos': "También los vídeos",
+ 'photosync.include_videos_hint': "Los vídeos ocupan mucho: se envían por Wi-Fi como las fotos, y la primera copia puede tardar.",
+ 'photosync.album_count_videos': "{n} fotos, {v} vídeos · {size}",
+ 'photosync.confirm_videos': "Se enviarán {count} fotos y vídeos ({size}), de ellos {videos} vídeos ({videoSize}), a {dir} y luego los nuevos una vez al día.",
+ 'photosync.video_permission_needed': "MeshBay también necesita acceso a los vídeos de este teléfono.",
'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",
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 3b5b881..3f3cd42 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -1322,6 +1322,11 @@ export default {
'photosync.albums': "Albums de ce téléphone",
'photosync.album_camera': "Appareil photo",
'photosync.album_count': "{n} photos · {size}",
+ 'photosync.include_videos': "Les vidéos aussi",
+ 'photosync.include_videos_hint': "Les vidéos sont lourdes : elles partent en Wi-Fi comme les photos, et la première sauvegarde peut être longue.",
+ 'photosync.album_count_videos': "{n} photos, {v} vidéos · {size}",
+ 'photosync.confirm_videos': "{count} photos et vidéos ({size}), dont {videos} vidéos ({videoSize}), seront envoyées dans {dir}, puis les nouvelles une fois par jour.",
+ 'photosync.video_permission_needed': "MeshBay a aussi besoin d'accéder aux vidéos de ce téléphone.",
'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",
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 9935edb..f238495 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -1321,6 +1321,11 @@ export default {
'photosync.albums': "Album su questo telefono",
'photosync.album_camera': "Fotocamera",
'photosync.album_count': "{n} foto · {size}",
+ 'photosync.include_videos': "Anche i video",
+ 'photosync.include_videos_hint': "I video sono pesanti: partono in Wi-Fi come le foto, e il primo backup può richiedere tempo.",
+ 'photosync.album_count_videos': "{n} foto, {v} video · {size}",
+ 'photosync.confirm_videos': "{count} foto e video ({size}), di cui {videos} video ({videoSize}), verranno inviati in {dir}, poi quelli nuovi una volta al giorno.",
+ 'photosync.video_permission_needed': "MeshBay ha bisogno di accedere anche ai video di questo telefono.",
'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",
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 2af2f49..6a59d1d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -1305,6 +1305,11 @@ export default {
'photosync.albums': "この電話のアルバム",
'photosync.album_camera': "カメラ",
'photosync.album_count': "{n} 枚 · {size}",
+ 'photosync.include_videos': "動画も含める",
+ 'photosync.include_videos_hint': "動画はサイズが大きいため、写真と同じく Wi-Fi で送信され、最初のバックアップには時間がかかることがあります。",
+ 'photosync.album_count_videos': "写真 {n} 枚、動画 {v} 本 · {size}",
+ 'photosync.confirm_videos': "写真と動画 {count} 件 ({size}、うち動画 {videos} 本 {videoSize}) を {dir} に送り、その後は新しいものを1日1回送ります。",
+ 'photosync.video_permission_needed': "MeshBay にこのスマートフォンの動画へのアクセスも許可してください。",
'photosync.scope_existing': "この電話にすでにある写真と、これからの写真",
'photosync.scope_new': "これから撮る写真のみ",
'photosync.start': "バックアップを開始",
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 9e83c86..4b68975 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -1323,6 +1323,11 @@ export default {
'photosync.albums': "Albums op deze telefoon",
'photosync.album_camera': "Camera",
'photosync.album_count': "{n} foto's · {size}",
+ 'photosync.include_videos': "Ook video's",
+ 'photosync.include_videos_hint': "Video's zijn groot: ze gaan net als foto's via wifi, en de eerste back-up kan even duren.",
+ 'photosync.album_count_videos': "{n} foto's, {v} video's · {size}",
+ 'photosync.confirm_videos': "{count} foto's en video's ({size}), waarvan {videos} video's ({videoSize}), worden naar {dir} gestuurd, daarna eenmaal per dag de nieuwe.",
+ 'photosync.video_permission_needed': "MeshBay heeft ook toegang nodig tot de video's op deze telefoon.",
'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",
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 cb0d0b6..8b1b5db 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -1349,6 +1349,11 @@ export default {
'photosync.albums': "Albumy na tym telefonie",
'photosync.album_camera': "Aparat",
'photosync.album_count': "Zdjęcia: {n} · {size}",
+ 'photosync.include_videos': "Również filmy",
+ 'photosync.include_videos_hint': "Filmy są duże: wysyłane są przez Wi-Fi jak zdjęcia, a pierwsza kopia może potrwać.",
+ 'photosync.album_count_videos': "Zdjęcia: {n}, filmy: {v} · {size}",
+ 'photosync.confirm_videos': "Zdjęcia i filmy ({count}, {size}), w tym filmy: {videos} ({videoSize}), zostaną wysłane do {dir}, a potem nowe raz dziennie.",
+ 'photosync.video_permission_needed': "MeshBay potrzebuje też dostępu do filmów w tym telefonie.",
'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ę",
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 cb3aa70..9fc634b 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
@@ -1308,6 +1308,11 @@ export default {
'photosync.albums': "Álbuns deste celular",
'photosync.album_camera': "Câmera",
'photosync.album_count': "{n} fotos · {size}",
+ 'photosync.include_videos': "Vídeos também",
+ 'photosync.include_videos_hint': "Vídeos são pesados: vão pelo Wi-Fi como as fotos, e o primeiro backup pode demorar.",
+ 'photosync.album_count_videos': "{n} fotos, {v} vídeos · {size}",
+ 'photosync.confirm_videos': "{count} fotos e vídeos ({size}), sendo {videos} vídeos ({videoSize}), serão enviados para {dir} e depois os novos uma vez por dia.",
+ 'photosync.video_permission_needed': "O MeshBay também precisa de acesso aos vídeos deste celular.",
'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",
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 bb47b00..5859934 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
@@ -1294,6 +1294,11 @@ export default {
'photosync.albums': "这部手机上的相册",
'photosync.album_camera': "相机",
'photosync.album_count': "{n} 张照片 · {size}",
+ 'photosync.include_videos': "包括视频",
+ 'photosync.include_videos_hint': "视频体积较大:它们和照片一样通过 Wi-Fi 发送,首次备份可能需要一些时间。",
+ 'photosync.album_count_videos': "{n} 张照片,{v} 个视频 · {size}",
+ 'photosync.confirm_videos': "将把 {count} 个照片和视频({size},其中 {videos} 个视频,{videoSize})发送到 {dir},之后每天发送一次新的。",
+ 'photosync.video_permission_needed': "MeshBay 还需要访问本机的视频。",
'photosync.scope_existing': "手机上已有的照片,然后是新照片",
'photosync.scope_new': "仅从现在起拍摄的照片",
'photosync.start': "开始备份",
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 7809efa..088de76 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
@@ -105,6 +105,11 @@ export function PhotoSyncSection({ destination }) {
</p>
${status.permission === 'partial' && html`
<p class="settings-hint">${t('photosync.permission_partial')}</p>`}
+ ${cfg.includeVideos && status.videoPermission === 'denied' && html`
+ <p class="settings-hint">${t('photosync.video_permission_needed')}</p>
+ <button class="btn-secondary" onClick=${async () => {
+ try { setStatus(await platform.photoSync.permit(true)); } catch (e) { setError(e.message); }
+ }}>${t('photosync.permission_allow')}</button>`}
${status.failure
? html`<p class="error-msg">${t(FAILURES[status.failure] || 'photosync.fail_other')}</p>`
: stateLine(run, status) && html`<p class="settings-hint">${stateLine(run, status)}</p>`}
@@ -129,19 +134,32 @@ function Setup({ status, onPermission, onDone }) {
const [albums, setAlbums] = useState(null);
const [chosen, setChosen] = useState(() => new Set(cfg ? cfg.albums : []));
const [existing, setExisting] = useState(cfg ? cfg.includeExisting : true);
+ const [videos, setVideos] = useState(cfg ? !!cfg.includeVideos : false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const permitted = status.permission !== 'denied';
+ const videosAllowed = status.videoPermission !== 'denied';
useEffect(() => {
if (!permitted) return;
- platform.photoSync.albums().then((list) => {
+ platform.photoSync.albums(videos && videosAllowed).then((list) => {
setAlbums(list);
// The camera, and only the camera, unless the person chose before:
// screenshots and saved images are where photos nobody meant to share live.
- if (!cfg) setChosen(new Set(list.filter((a) => a.camera).map((a) => a.id)));
+ if (!cfg) {
+ setChosen((prev) => (prev.size ? prev : new Set(list.filter((a) => a.camera).map((a) => a.id))));
+ }
}).catch((e) => setError(e.message));
- }, [permitted]);
+ }, [permitted, videos, videosAllowed]);
+
+ // Ticking videos asks for them then, not before: Android 13+ asks apart.
+ const toggleVideos = async () => {
+ const next = !videos;
+ setVideos(next);
+ if (next && !videosAllowed) {
+ try { onPermission(await platform.photoSync.permit(true)); } catch (e) { setError(e.message); }
+ }
+ };
const allow = async () => {
setError('');
@@ -159,14 +177,19 @@ function Setup({ status, onPermission, onDone }) {
setBusy(true);
setError('');
try {
- 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 settings = { albums: [...chosen], includeExisting: existing, includeVideos: videos };
+ // Asked when the photos would come from somewhere new in time, or when
+ // videos start going; never for a change of albums alone, which the
+ // person just made.
+ if (!cfg || cfg.includeExisting !== existing || (videos && !cfg.includeVideos)) {
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));
+ const params = {
+ count: est.count, size: formatSize(est.bytes), dir: status.dir,
+ videos: est.videos || 0, videoSize: formatSize(est.videoBytes || 0),
+ };
+ const key = !(est.count > 0) ? 'photosync.confirm_new'
+ : est.videos > 0 ? 'photosync.confirm_videos' : 'photosync.confirm';
+ const yes = await ask(t(key, params));
if (!yes) { setBusy(false); return; }
}
onDone(await platform.photoSync.configure(settings));
@@ -194,11 +217,23 @@ function Setup({ status, onPermission, onDone }) {
<span class="settings-name">
<strong>${a.camera ? t('photosync.album_camera') : a.name}</strong>
<span class="settings-hint">
- ${t('photosync.album_count', { n: a.count, size: formatSize(a.bytes) })}</span>
+ ${videos && a.videos
+ ? t('photosync.album_count_videos',
+ { n: a.count, v: a.videos, size: formatSize(a.bytes + a.videoBytes) })
+ : t('photosync.album_count', { n: a.count, size: formatSize(a.bytes) })}</span>
</span>
</label>
`)}
<label class="settings-choice">
+ <input type="checkbox" checked=${videos} onChange=${toggleVideos} />
+ <span class="settings-name">
+ <strong>${t('photosync.include_videos')}</strong>
+ <span class="settings-hint">${t('photosync.include_videos_hint')}</span>
+ </span>
+ </label>
+ ${videos && !videosAllowed && html`
+ <p class="settings-hint">${t('photosync.video_permission_needed')}</p>`}
+ <label class="settings-choice">
<input type="radio" name="photosync-scope" checked=${existing}
onChange=${() => setExisting(true)} />
<span><strong>${t('photosync.scope_existing')}</strong></span>
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 9c59c17..ffb07aa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
@@ -253,7 +253,7 @@ export async function upload(transport, item, file = platform.photoSync.file) {
timeout = setTimeout(() => reject(new Error('The node is busy')), LEASE_WAIT_MS);
}),
]).finally(() => clearTimeout(timeout));
- const bytes = await file(item.token, item.name);
+ const bytes = await file(item.token, item.name, item.size);
return await transport.uploadFile(bytes, {
dir: item.dir, root: item.dir.split('/')[0], tr: lease.tr,
});
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index 2cfa673..8effc21 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -238,8 +238,8 @@ export const push = {
export const photoSync = {
available: Boolean(bridge && bridge.photoSync),
status() { return bridge.photoSync.status(); },
- permit() { return bridge.photoSync.permit(); },
- albums() { return bridge.photoSync.albums(); },
+ permit(withVideos) { return bridge.photoSync.permit(withVideos); },
+ albums(withVideos) { return bridge.photoSync.albums(withVideos); },
configure(settings) { return bridge.photoSync.configure(settings); },
estimate(settings) { return bridge.photoSync.estimate(settings); },
plan() { return bridge.photoSync.plan(); },
@@ -247,11 +247,25 @@ export const photoSync = {
completed() { return bridge.photoSync.completed(); },
failed(code, text) { return bridge.photoSync.failed(code, text); },
keepAlive(on, text) { return bridge.photoSync.keepAlive(on, text); },
- /** The bytes of a photo the last plan issued, as a File named for the node. */
- async file(token, name) {
- const r = await fetch(`/photosync/${encodeURIComponent(token)}`, { cache: 'no-store' });
- if (!r.ok) throw new Error('This photo is no longer on the phone');
- return new File([await r.blob()], name);
+ /**
+ * A photo or video the last plan issued, as what an upload reads (`name`,
+ * `size`, `slice(a, b).arrayBuffer()`): each slice is fetched from the phone
+ * when it is read, so a video of gigabytes is never whole in this page.
+ */
+ file(token, name, size) {
+ const url = `/photosync/${encodeURIComponent(token)}`;
+ const part = (start, end) => ({
+ async arrayBuffer() {
+ if (end <= start) return new ArrayBuffer(0);
+ const r = await fetch(url, { cache: 'no-store', headers: { Range: `bytes=${start}-${end - 1}` } });
+ if (r.status !== 206) throw new Error('This photo is no longer on the phone');
+ return r.arrayBuffer();
+ },
+ });
+ return {
+ name, size,
+ slice: (a = 0, b = size) => part(Math.max(0, a), Math.min(b, size)),
+ };
},
};