From 28be8eead316f5026e1c0c8ffd46b873d479091d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 24 Aug 2026 20:04:36 +0200 Subject: feat(hub): play audio files from Explorer, scoped to the current folder Audio files were invisible to file-utils.js's canPreview, so their name never rendered as a clickable link and the toolbar's Play/View buttons never enabled for them - the only way to do anything with an audio file in Files was to download it. - file-utils.js: canPreview now includes 'audio' alongside image/ video/document. - files-app.js: the toolbar's canPlay/canView split treats audio like video (Play, not View) rather than falling into the generic preview path, which was never built for it anyway. - group-page.js: onPreview routes an audio entry to onPlayQueue - the same persistent player Music uses - instead of the preview modal. Deliberately not Music's artist/album grouping: the queue is every audio entry sharing the clicked file's literal containing directory (entry.path), sorted by filename, so previous/next in Explorer stays scoped to what's actually in that folder, tags or no tags. onPlayQueue already replaces whatever queue is playing unconditionally, so starting a track from Files while Music (or another Files folder) is already playing needs no special handling - it's the same "just a new queue" path either way. Client-side only, no protocol/index change. npm run sync-ui re-run. Full suite: 1129 passed, no regressions. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy --- .../src/meshbay_hub/static/file-utils.js | 2 +- .../src/meshbay_hub/static/files-app.js | 5 +++-- .../src/meshbay_hub/static/group-page.js | 22 +++++++++++++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) (limited to 'packages') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js index 4541290..a005e2e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js @@ -23,7 +23,7 @@ const PREVIEWABLE_TEXT = /\.(txt|md|json|csv|log|xml|yaml|yml|ini|conf|py|js|html|css|sh|c|h|java|rs|go|rb|toml)$/i; function canPreview(e) { - return ['image', 'video', 'document'].includes(e.type) + return ['image', 'video', 'audio', 'document'].includes(e.type) || PREVIEWABLE_TEXT.test(e.name); } diff --git a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js index 561d8a7..c09ef40 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js @@ -307,8 +307,9 @@ function FilesPanel({ `; - const canPlay = !!(onlyFile && onlyFile.type === 'video'); - const canView = !!(onlyFile && onlyFile.type !== 'video' && canPreview(onlyFile)); + const canPlay = !!(onlyFile && (onlyFile.type === 'video' || onlyFile.type === 'audio')); + const canView = !!(onlyFile && onlyFile.type !== 'video' && onlyFile.type !== 'audio' + && canPreview(onlyFile)); const deletableCount = deletableFiles.length + (operatorPaired ? selectedDirs.length : 0); // The operator can always delete; anyone else only ever sees the button if 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 d2d56b5..2331977 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -432,10 +432,26 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // A single dispatcher so any app can open the right modal without owning // video/preview state itself — Files' table and Chat's attachments both - // call this the same way. + // call this the same way. Audio goes to the same persistent player Music + // uses (onPlayQueue) rather than a modal — but the queue it builds is + // Explorer's own next/previous, deliberately not Music's: every audio + // entry sharing this file's literal containing directory, in filename + // order, never Music's artist/album grouping, which would pull in files + // nowhere near this one on disk. onPlayQueue always replaces whatever is + // already playing (from Music, or a previous Files click) rather than + // merging with it, so there is nothing to special-case here. const onPreview = useCallback((entry) => { - if (entry.type === 'video') setVideoEntry(entry); else setPreviewEntry(entry); - }, []); + if (entry.type === 'video') { setVideoEntry(entry); return; } + if (entry.type === 'audio') { + const siblings = entries + .filter((e) => e.type === 'audio' && e.path === entry.path) + .sort((a, b) => a.name.localeCompare(b.name)); + const startIndex = Math.max(0, siblings.findIndex((e) => e.id === entry.id)); + onPlayQueue(siblings, startIndex); + return; + } + setPreviewEntry(entry); + }, [entries, onPlayQueue]); const apps = visibleApps(enabledApps); const commonProps = { -- cgit v1.2.3