diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 175 |
1 files changed, 59 insertions, 116 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 703bd47..50a972b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -8,14 +8,16 @@ import { transfers, formatSpeed } from './transfers.js'; import * as downloads from './downloads.js'; import * as platform from './platform.js'; import { Icon } from './icon.js'; -import { FILE_ICONS, formatSize } from './file-utils.js'; +import { formatSize } from './file-utils.js'; import { - HUB, navigate, session, getCachedGroupIndex, getAllCachedIndexes, + HUB, navigate, session, getCachedGroupIndex, _storeBundleKey, _loadBundleKey, _clearKeyDB, loadAuth, saveAuth, setAuth, setAuthChangeListener, ensureFreshToken, hubFetch, refreshAccessToken, } from './hub-client.js'; import { GroupPage } from './group-page.js'; +import { SearchPage, ConnectionPool } from './search-page.js'; +import { MusicPlayerBar } from './music-player.js'; import { GroupName } from './group-name.js'; import { APPS } from './apps.js'; @@ -1293,118 +1295,6 @@ function CreateGroupWizard({ token, username, onCreated, allowPublicGroups = tru </div>`; } - -// ── Search Page (cross-group file search) ─────────────────────────────────── - -/** - * "3 hours ago", in the reader's language. - * - * The search page needs it because its results come from a cache: a file that - * was deleted an hour ago is still listed until the group is opened again, and - * the honest thing is to say how old the answer is rather than to imply it is - * live. - */ -function formatAgo(ts) { - if (!ts) return ''; - const rtf = new Intl.RelativeTimeFormat(getLocale(), { numeric: 'auto' }); - let delta = (ts - Date.now()) / 1000; - const steps = [['second', 60], ['minute', 60], ['hour', 24], - ['day', 7], ['week', 4.35], ['month', 12], ['year', Infinity]]; - for (const [unit, span] of steps) { - if (Math.abs(delta) < span || span === Infinity) { - return rtf.format(Math.round(delta), unit); - } - delta /= span; - } - return ''; -} - -function SearchPage() { - const [query, setQuery] = useState(''); - const [results, setResults] = useState([]); - const [searched, setSearched] = useState(false); - - const doSearch = useCallback(async (q) => { - const term = q.trim().toLowerCase(); - if (!term) { setResults([]); setSearched(false); return; } - const indexes = await getAllCachedIndexes(); - const hits = []; - for (const idx of indexes) { - for (const e of (idx.entries || [])) { - if (e.name.toLowerCase().includes(term) || - (e.path && e.path.toLowerCase().includes(term))) { - hits.push({ ...e, groupId: idx.groupId, groupName: idx.groupName, - groupOwner: idx.groupOwner, syncedAt: idx.cachedAt }); - } - } - } - setResults(hits); - setSearched(true); - }, []); - - const onInput = useCallback((e) => { - const q = e.target.value; - setQuery(q); - doSearch(q); - }, [doSearch]); - - return html` - <div> - <h2>${t('search.title')}</h2> - <div class="file-toolbar" style="margin-bottom:16px"> - <input type="text" class="admin-search" style="width:100%" - placeholder="${t('search.placeholder')}" - value=${query} onInput=${onInput} autofocus /> - </div> - ${searched && results.length === 0 && html` - <p class="page-message">${t('search.no_results')}</p> - `} - ${results.length > 0 && html` - <table class="file-table"> - <thead> - <tr> - <th></th> - <th>${t('group.col_name')}</th> - <th>${t('group.col_size')}</th> - <th>${t('search.col_group')}</th> - <th>${t('group.col_type')}</th> - </tr> - </thead> - <tbody> - ${results.map(r => html` - <tr class="file-row" key=${r.id + r.groupId}> - <td>${FILE_ICONS[r.type] || FILE_ICONS.other}</td> - <td class="file-name"> - <a href="#/group/${r.groupId}" style="color:inherit">${r.name}</a> - </td> - <td class="file-size">${formatSize(r.size)}</td> - <td> - <a href="#/group/${r.groupId}" class="badge">${r.groupName - ? html`<${GroupName} name=${r.groupName} owner=${r.groupOwner} inline=${true} />` - : r.groupId.slice(0, 8)}</a> - ${r.syncedAt && html` - <div class="search-synced"> - ${t('search.synced', { ago: formatAgo(r.syncedAt) })} - </div> - `} - </td> - <td class="file-type">${r.type}</td> - </tr> - `)} - </tbody> - </table> - <p class="settings-value" style="margin-top:8px"> - ${t('search.result_count', { n: results.length })} - </p> - `} - ${!searched && html` - <p class="page-message">${t('search.hint')}</p> - `} - <p class="settings-hint" style="margin-top:12px">${t('search.cache_note')}</p> - </div> - `; -} - // ── Settings Page ─────────────────────────────────────────────────────────── const THEME_OPTIONS = ['light', 'dark', 'system']; @@ -3010,6 +2900,48 @@ function App() { const [hubInfo, setHubInfo] = useState(null); const allowPublicGroups = !hubInfo || hubInfo.allow_public_groups !== false; + // -- Persistent music player (lifted from group-page.js) -- + const [musicQueue, setMusicQueue] = useState(null); + const musicPoolRef = useRef(null); + const userRef = useRef(user); + userRef.current = user; + const groupTransportRef = useRef(null); + + useEffect(() => { + musicPoolRef.current = new ConnectionPool(HUB); + return () => { if (musicPoolRef.current) musicPoolRef.current.closeAll(); }; + }, []); + + const getMusicConnection = useCallback(async (groupId) => { + const gt = groupTransportRef.current; + if (gt && gt.groupId === groupId) { + const tr = gt.transportRef.current; + if (tr && tr.connected) return { transport: tr, gek: gt.gekRef.current }; + } + const u = userRef.current; + if (!u || !musicPoolRef.current) throw new Error('no connection'); + const bundleKey = session.bundleKey || await _loadBundleKey(); + if (bundleKey) session.bundleKey = bundleKey; + const conn = await musicPoolRef.current.connect( + groupId, u.token, bundleKey, u.username, u.userId); + return { transport: conn.transport, gek: conn.gek }; + }, []); + + const handlePlayQueue = useCallback((tracks, startIndex, source) => { + if (source && source.transportRef) { + groupTransportRef.current = { + groupId: source.groupId, + transportRef: source.transportRef, + gekRef: source.gekRef, + }; + } else { + groupTransportRef.current = null; + } + setMusicQueue({ tracks, startIndex, nonce: Date.now() }); + }, []); + + const handleStopMusic = useCallback(() => setMusicQueue(null), []); + const resolved = resolveTheme(theme); // Keep the session alive without anyone having to think about it. @@ -3286,7 +3218,10 @@ function App() { } else if (!user) { page = html`<${LoginPage} />`; } else if (route === '/search') { - page = html`<${SearchPage} />`; + page = html`<${SearchPage} + token=${user.token} username=${user.username} userId=${user.userId} + groups=${groups} userPrefs=${userPrefs} + onPlayQueue=${handlePlayQueue} />`; } else if (route === '/explore') { page = html`<${ExplorePage} token=${user.token} myGroupIds=${groups.map(g => g.id)} @@ -3311,7 +3246,8 @@ function App() { userPrefs=${userPrefs} onRefreshAuth=${refreshAuth} onJoined=${dismissGroupNotifications} onGroupUpdated=${updateGroup} onPresence=${notePresence} - onLeft=${handleLeftGroup} />`; + onLeft=${handleLeftGroup} + onPlayQueue=${handlePlayQueue} onStopMusic=${handleStopMusic} />`; } else if (route === '/admin') { page = (user.role === 'moderator' || user.role === 'admin') ? html`<${AdminPage} token=${user.token} role=${user.role} />` @@ -3360,6 +3296,13 @@ function App() { ${page} </main> </div> + ${musicQueue && html` + <${MusicPlayerBar} + getConnection=${getMusicConnection} + queue=${musicQueue} + userPrefs=${userPrefs} + onClose=${handleStopMusic} /> + `} <//> `; } |