aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-settings.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-settings.js114
1 files changed, 112 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
index a4db4cb..60b0184 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js
@@ -101,6 +101,79 @@ function RootFolderRow({
`;
}
+/**
+ * Which folder(s) are the Photos app's entry points for this group — a
+ * *set*, unlike RootFolderRow's single value above (docs/photos.md §2.1: a
+ * photo library is routinely scattered across several folders). An
+ * add/remove list rather than a `<select>`: pick a folder to add from the
+ * same `rootFolderOptions` the Videos/Music pickers use, list what is
+ * already configured with a remove button each, and one Save signs the
+ * whole resulting set in one op (same shape as the app-enable checkboxes
+ * below — several changes staged, one signature).
+ */
+function PhotoRootsRow({ folders, value, busy, msg, onSave }) {
+ const [draft, setDraft] = useState(value || []);
+ useEffect(() => { setDraft(value || []); }, [value]);
+ const [addSelection, setAddSelection] = useState('');
+
+ const available = folders.filter((p) => !draft.includes(p));
+ const addRoot = () => {
+ if (!addSelection || draft.includes(addSelection)) return;
+ setDraft((prev) => [...prev, addSelection].sort());
+ setAddSelection('');
+ };
+ const removeRoot = (path) => setDraft((prev) => prev.filter((p) => p !== path));
+
+ const unchanged = draft.length === (value || []).length
+ && draft.every((p) => (value || []).includes(p));
+
+ return html`
+ <div class="settings-root-row">
+ <div class="settings-root-row-title">
+ <${Icon} name="image" />
+ <h4>${t('settings_node.photo_roots_title')}</h4>
+ </div>
+ <p class="settings-hint">${t('settings_node.photo_roots_hint')}</p>
+ ${draft.length === 0 && html`
+ <p class="settings-hint">${t('settings_node.photo_roots_none')}</p>
+ `}
+ ${draft.length > 0 && html`
+ <ul class="settings-root-list">
+ ${draft.map((p) => html`
+ <li key=${p} class="settings-root-list-item">
+ <span>${' '.repeat(p.split('/').length - 1)}${p.split('/').pop()}</span>
+ <button class="link-btn" disabled=${busy} onClick=${() => removeRoot(p)}
+ title=${t('settings_node.photo_roots_remove')}>
+ <${Icon} name="close" /></button>
+ </li>
+ `)}
+ </ul>
+ `}
+ <div class="settings-row">
+ <label class="settings-label">
+ <select value=${addSelection} disabled=${busy || available.length === 0}
+ onChange=${(e) => setAddSelection(e.target.value)}>
+ <option value="">${t('settings_node.photo_roots_add_placeholder')}</option>
+ ${available.map((p) => html`
+ <option key=${p} value=${p}>
+ ${' '.repeat(p.split('/').length - 1)}${p.split('/').pop()}
+ </option>
+ `)}
+ </select>
+ </label>
+ <button class="btn btn-small btn-secondary" disabled=${busy || !addSelection}
+ onClick=${addRoot}>${t('settings_node.photo_roots_add')}</button>
+ </div>
+ <button class="btn btn-small btn-secondary" style="margin-top:8px"
+ disabled=${busy || unchanged} onClick=${() => onSave(draft)}>
+ ${busy ? t('settings_node.scan_saving') : t('settings_node.photo_roots_save')}
+ </button>
+ ${msg && html`<p class=${msg.ok ? 'success-msg' : 'error-msg'} style="margin-top:8px">
+ ${msg.text}</p>`}
+ </div>
+ `;
+}
+
// ── Members Panel ────────────────────────────────────────────────────────
/**
@@ -120,7 +193,8 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
tmdbConfig, onTmdbConfig, onTmdbEnabled,
musicbrainzConfig, onMusicbrainzConfig, onMusicbrainzEnabled,
entries, nodeDirs, videoRoot, onVideoRoot,
- audioRoot, onAudioRoot, onRefreshIndex,
+ audioRoot, onAudioRoot,
+ photoRoots, onPhotoRoots, onRefreshIndex,
onPaired, onLeft }) {
const [members, setMembers] = useState([]);
const [adminId, setAdminId] = useState('');
@@ -636,6 +710,36 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
}
}, [transportRef, onAudioRoot, audioRootDraft, audioRoot]);
+ // Photos app's own entry points — a set (docs/photos.md §2.1), unlike
+ // videoRoot/audioRoot above. No "removing a root is destructive" confirm
+ // dialog: removing one root only drops that root's albums from view, it
+ // does not replace the whole tab's content the way changing video_root
+ // does.
+ const [photoRootsBusy, setPhotoRootsBusy] = useState(false);
+ const [photoRootsMsg, setPhotoRootsMsg] = useState(null);
+
+ const savePhotoRoots = useCallback(async (nextRoots) => {
+ const transport = transportRef && transportRef.current;
+ setPhotoRootsMsg(null);
+ setPhotoRootsBusy(true);
+ try {
+ if (!transport || !transport.connected) {
+ throw new Error('Not connected to the node');
+ }
+ const sk = transport.sessionKeys && transport.sessionKeys.skEdB64;
+ const signFn = (sk && window.MeshBayKeys)
+ ? (transcript) => window.MeshBayKeys.signBytes(sk, transcript)
+ : null;
+ await transport.setPhotoRoots(nextRoots, signFn);
+ if (onPhotoRoots) onPhotoRoots(nextRoots);
+ setPhotoRootsMsg({ text: t('settings_node.scan_saved'), ok: true });
+ } catch (err) {
+ setPhotoRootsMsg({ text: err.message, ok: false });
+ } finally {
+ setPhotoRootsBusy(false);
+ }
+ }, [transportRef, onPhotoRoots]);
+
const [removing, setRemoving] = useState('');
/**
@@ -957,7 +1061,8 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
this group at all (daemon.py's _enrich_new_video_entries). */
isNodeAdmin && connected
&& ((nodeDetected && nodeRoots.length > 0)
- || activeApps.includes('video') || activeApps.includes('music')) && html`
+ || activeApps.includes('video') || activeApps.includes('music')
+ || activeApps.includes('photo')) && html`
<${CollapsibleSection} titleKey="settings_node.directories_title">
<p class="settings-hint">${t('settings_node.directories_hint')}</p>
@@ -977,6 +1082,11 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,
busy=${audioRootBusy} msg=${audioRootMsg} onSave=${saveAudioRoot}
noneKey="settings_node.audio_root_none" saveKey="settings_node.audio_root_save" />
`}
+ ${activeApps.includes('photo') && html`
+ <${PhotoRootsRow}
+ folders=${rootFolderOptions} value=${photoRoots}
+ busy=${photoRootsBusy} msg=${photoRootsMsg} onSave=${savePhotoRoots} />
+ `}
${/* Roots management (Electron-only, when node is local) — folded into
the same Directories section as the two root pickers above. */
nodeDetected && nodeRoots.length > 0 && html`