aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
diff options
context:
space:
mode:
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.js107
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)} />