diff options
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.js | 114 |
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` |