diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/group-page.js | 107 |
1 files changed, 74 insertions, 33 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index dfde172..747cb74 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -128,15 +128,18 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // TMDB on/off + whether a custom token is set, node-wide (not per-group) — // docs/mediacenter.md §5.5. Null until the handshake ack arrives. const [tmdbConfig, setTmdbConfig] = useState(null); - // Which folder is the Videos app's entry point for this group — '' - // (the default) means the whole group index. Set from Files, per-group. - const [videoRoot, setVideoRoot] = useState(''); - // Same shape — the Music app's own entry point. - const [audioRoot, setAudioRoot] = useState(''); - // The Photos app's entry points — a *list*, unlike videoRoot/audioRoot - // above (docs/photos.md §2.1: a photo library is routinely scattered - // across several folders). Empty means nothing configured yet. - const [photoRoots, setPhotoRoots] = useState([]); + // Which folders each app works over. One shape for all of them — a list, + // always, even where an app only wants one (docs/refactor-groups.md §1.6): + // Videos and Music were single values, which meant a library spread over two + // drives could not be described at all. Empty means nothing configured yet, + // which every app reads as "show nothing", never "the whole group index". + const [appDirectories, setAppDirectories] = useState({}); + const appDirs = useCallback( + (key) => appDirectories[key] || [], [appDirectories]); + // Where chat attachments are written — one directory, because Chat has one + // destination rather than a set of folders it reads. + const [chatDirectory, setChatDirectory] = useState(''); + const [chatLinkPreview, setChatLinkPreview] = useState(true); // MusicBrainz on/off (per-group) — docs/musicbay.md §3.2. const [musicbrainzConfig, setMusicbrainzConfig] = useState(null); const onPlayQueue = useCallback((tracks, startIndex) => { @@ -226,8 +229,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, if (indexMsg.roots) setNodeRoots(indexMsg.roots); cacheGroupIndex(groupId, group ? group.name : groupId, group ? group.owner_username : null, fresh, - { videoRoot, audioRoot, photoRoots }); - }, [groupId, group, videoRoot, audioRoot, photoRoots]); + { video: appDirs('video'), music: appDirs('music'), + photo: appDirs('photo') }); + }, [groupId, group, appDirs]); // additions/deletions/updates (daemon.py _broadcast_index_change, once // there is a previous snapshot to diff against) — applied on top of @@ -249,10 +253,11 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, const fresh = updated.concat(additions); cacheGroupIndex(groupId, group ? group.name : groupId, group ? group.owner_username : null, fresh, - { videoRoot, audioRoot, photoRoots }); + { video: appDirs('video'), music: appDirs('music'), + photo: appDirs('photo') }); return fresh; }); - }, [groupId, group, videoRoot, audioRoot, photoRoots]); + }, [groupId, group, appDirs]); useEffect(() => { let cancelled = false; @@ -326,9 +331,19 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, tokenCustomized: !!ack.tmdb_token_customized, language: ack.tmdb_language || '', }); - setVideoRoot(ack.video_root || ''); - setAudioRoot(ack.audio_root || ''); - setPhotoRoots(ack.photo_roots || []); + // The plural form when the node speaks it, the old scalars when it + // does not — an MNP 1.0 node sends only the latter, and reading its + // missing `video_directories` as "nothing configured" would empty a + // working Videos tab. + setAppDirectories({ + video: ack.video_directories + || (ack.video_root ? [ack.video_root] : []), + music: ack.music_directories + || (ack.audio_root ? [ack.audio_root] : []), + photo: ack.photo_directories || ack.photo_roots || [], + }); + setChatDirectory(ack.chat_directory || ''); + setChatLinkPreview(ack.chat_link_preview !== false); setMusicbrainzConfig({ enabled: ack.musicbrainz_enabled !== false, }); @@ -344,9 +359,18 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // recent value. transport.onTmdbConfig = (cfg) => setTmdbConfig((prev) => ({ ...(prev || {}), ...cfg })); transport.onTmdbEnabled = (enabled) => setTmdbConfig((prev) => ({ ...(prev || {}), enabled })); - transport.onVideoRoot = (path) => setVideoRoot(path); - transport.onAudioRoot = (path) => setAudioRoot(path); - transport.onPhotoRoots = (roots) => setPhotoRoots(roots); + // One handler for every app's directories, plus the three older + // per-app messages a node that predates the generic op still sends. + transport.onAppDirectories = (app, dirs) => + setAppDirectories((prev) => ({ ...prev, [app]: dirs })); + transport.onVideoRoot = (path) => setAppDirectories( + (prev) => ({ ...prev, video: path ? [path] : [] })); + transport.onAudioRoot = (path) => setAppDirectories( + (prev) => ({ ...prev, music: path ? [path] : [] })); + transport.onPhotoRoots = (roots) => setAppDirectories( + (prev) => ({ ...prev, photo: roots || [] })); + transport.onChatDirectory = (path) => setChatDirectory(path); + transport.onChatLinkPreview = (on) => setChatLinkPreview(on); transport.onMusicbrainzEnabled = (enabled) => setMusicbrainzConfig((prev) => ({ ...(prev || {}), enabled })); transport.onRootsChanged = (msg) => { @@ -600,15 +624,36 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, return !root || !unavailRoots.has(root); }), [entries, unavailRoots]); + // Everything the operator settings panes read, in one object. Built here + // because this is where the state already lives, and passed through + // `group-settings.js` untouched — that page renders the panes without + // knowing what any of them is for, which is what makes adding an app a + // registry entry rather than an edit to the page. + const appSettings = useMemo(() => ({ + videoDirectories: appDirs('video'), + musicDirectories: appDirs('music'), + photoDirectories: appDirs('photo'), + chatDirectory, + chatLinkPreview, + tmdbEnabled: tmdbConfig ? tmdbConfig.enabled !== false : true, + tmdbLanguage: (tmdbConfig && tmdbConfig.language) || '', + tmdbTokenCustomized: Boolean(tmdbConfig && tmdbConfig.tokenCustomized), + musicbrainzEnabled: musicbrainzConfig + ? musicbrainzConfig.enabled !== false : true, + }), [appDirs, chatDirectory, chatLinkPreview, tmdbConfig, musicbrainzConfig]); + const commonProps = { groupId, transportRef, gekRef, status, username, entries, availableEntries, nodeDirs, nodeRoots, setEntries, setNodeDirs, setNodeRoots, applyIndex, isNodeAdmin, operatorPaired, attachRoot, userId, setError, onPreview, onRefreshIndex: refreshIndex, onActivity: touchActivity, - videoRoot, onVideoRoot: (path) => setVideoRoot(path), - audioRoot, onAudioRoot: (path) => setAudioRoot(path), - photoRoots, onPhotoRoots: (roots) => setPhotoRoots(roots), + // Plural everywhere: Videos and Music read a list now, and Photos always + // did. The scalar `videoRoot`/`audioRoot` shapes survive only on the wire, + // for a node that speaks MNP 1.0 — nothing in the client carries them. + videoDirectories: appDirs('video'), + musicDirectories: appDirs('music'), + photoDirectories: appDirs('photo'), tmdbConfig, musicbrainzConfig, onPlayQueue, }; @@ -739,18 +784,14 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, onEnabledApps=${(keys) => setEnabledApps(keys)} scanSettings=${scanSettings} onScanSettings=${(s) => setScanSettings(s)} - tmdbConfig=${tmdbConfig} - onTmdbConfig=${(cfg) => setTmdbConfig((prev) => ({ ...(prev || {}), ...cfg }))} - onTmdbEnabled=${(enabled) => setTmdbConfig((prev) => ({ ...(prev || {}), enabled }))} - musicbrainzConfig=${musicbrainzConfig} - onMusicbrainzEnabled=${(enabled) => setMusicbrainzConfig((prev) => ({ ...(prev || {}), enabled }))} entries=${entries} nodeDirs=${nodeDirs} - videoRoot=${videoRoot} - onVideoRoot=${(path) => setVideoRoot(path)} - audioRoot=${audioRoot} - onAudioRoot=${(path) => setAudioRoot(path)} - photoRoots=${photoRoots} - onPhotoRoots=${(roots) => setPhotoRoots(roots)} + appSettings=${appSettings} + ${/* The saving pane already knows what it asked for; this is so + the page's own copy moves at the same time, rather than + waiting for the ack it will not be handed (transport.js + resolves an admin ack against the pending request). */''} + onAppDirectories=${(app, dirs) => + setAppDirectories((prev) => ({ ...prev, [app]: dirs }))} onRefreshIndex=${refreshIndex} onLeft=${onLeft} onPaired=${() => setOperatorPaired(true)} /> |