aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-05 12:23:18 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-05 12:23:18 +0200
commit1d94a92936abb3d37a8f9bfb36c8850246fee15d (patch)
treef07aa4f65277d703ac64a84882b0c559bf379f30 /packages/meshbay-hub/src/meshbay_hub/static/search-page.js
parent8f25294b0f6bc3f292442edd69a2e149f0717b52 (diff)
downloadmeshbay-1d94a92936abb3d37a8f9bfb36c8850246fee15d.tar.gz
feat: copy a file's or folder's #/name@owner link from Files, Music, Photos and Search
"Copy link" puts the address group-link.js resolves on the clipboard, on the hub's origin rather than the page's, so a link copied in the desktop application is not app://meshbay. Files offers it for one row, from the right-click menu or the toolbar with one row ticked (a phone's way in); Music on one track's menu, whose dots a phone has; Photos on a right-clicked tile and in the lightbox's bar. The video player and the file preview carry a link button next to Download. Applications get a `linkFor(entry | folderPath)` prop (MESHBAY_DESIGN.md ยง9.2) and offer the action only when it names a link. The group page builds it from the hub's row; Search from each result's own group and its path before the merged views prefixed it, and names no link for a folder of the merged tree, which a group name alone does not identify. harness/copy_link_probe.py mounts the three applications in Chrome and reads what reached the clipboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/search-page.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/search-page.js28
1 files changed, 24 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
index 874b7b0..1ede1f6 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
@@ -13,6 +13,7 @@ import { MusicApp, groupMusicEntries, foldKey } from './music-app.js';
import { PhotosApp, groupPhotoAlbums } from './photos-app.js';
import { VideoPlayer } from './video-player.js';
import { transfers } from './transfers.js';
+import { copyLink, entryPath, groupItemLink } from './copy-link.js';
import { mergeUnitEntries } from './source-merge.js';
import { useStickyBand } from './sticky.js';
import { ConnectionPool, MAX_IN_FLIGHT, MAX_POOL_SIZE } from './connection-pool.js';
@@ -514,6 +515,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
result.push({
...e,
path: SEARCH_VIDEO_ROOT + '/' + e.path,
+ _origPath: e.path,
groupId,
groupName: data.groupName,
groupOwner: data.groupOwner,
@@ -540,6 +542,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
result.push({
...e,
path: SEARCH_AUDIO_ROOT + '/' + e.path,
+ _origPath: e.path,
groupId,
groupName: data.groupName,
groupOwner: data.groupOwner,
@@ -567,6 +570,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
result.push({
...e,
path: '__search_photos__/' + e.path,
+ _origPath: e.path,
groupId,
groupName: data.groupName,
groupOwner: data.groupOwner,
@@ -640,6 +644,19 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
// No-op setters for FilesPanel
const noop = useCallback(() => {}, []);
+
+ // "Copy link" for a result: its own group's handle and its path there, from
+ // `_origPath` โ€” every view here prefixes `path` to merge groups into one tree.
+ // A merged entry is the source it was resolved to (source-merge.js), so the
+ // link names a group that has the file. A folder is a path in that merged
+ // tree, named by group name alone, which two owners may share: no link.
+ const linkFor = useCallback((target) => {
+ if (!target || typeof target === 'string' || !target.groupName || !target.groupOwner) {
+ return null;
+ }
+ return groupItemLink({ name: target.groupName, owner_username: target.groupOwner },
+ entryPath(target, target._origPath));
+ }, []);
// The search field and its view toggle are this page's equivalent of a
// group's tab bar: the same band, pinned the same way, publishing the same
// property for the toolbar underneath (style.css, "Sticky chrome").
@@ -726,6 +743,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
readOnly=${true}
getTransport=${getTransport}
showRefresh=${true}
+ linkFor=${linkFor}
onRefreshIndex=${() => setRefreshTick((n) => n + 1)} />
`}
@@ -754,7 +772,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
entries=${musicEntries}
musicDirectories=${[SEARCH_AUDIO_ROOT]}
musicbrainzConfig=${{ enabled: true }}
- onPlayQueue=${handleMusicPlay} userId=${userId}
+ onPlayQueue=${handleMusicPlay} userId=${userId} linkFor=${linkFor}
userPrefs=${userPrefs} pageResetKey=${q}
hideFilter=${true} />
`}
@@ -767,7 +785,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
status="connected"
entries=${photoEntries}
photoDirectories=${SEARCH_PHOTO_ROOTS}
- setError=${noop}
+ setError=${noop} linkFor=${linkFor}
hideFilter=${true}
readOnly=${true} />
`}
@@ -788,7 +806,8 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
transportRef=${modalTransportRef}
gekRef=${modalGekRef}
onClose=${() => setPreviewEntry(null)}
- onDownload=${() => downloadForModal(previewEntry)} />
+ onDownload=${() => downloadForModal(previewEntry)}
+ onCopyLink=${linkFor(previewEntry) && (() => copyLink(linkFor(previewEntry)))} />
`}
${videoEntry && html`
<${VideoPlayer}
@@ -796,7 +815,8 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
transportRef=${modalTransportRef}
gekRef=${modalGekRef}
onClose=${() => setVideoEntry(null)}
- onDownload=${() => downloadForModal(videoEntry)} />
+ onDownload=${() => downloadForModal(videoEntry)}
+ onCopyLink=${linkFor(videoEntry) && (() => copyLink(linkFor(videoEntry)))} />
`}
</div>
`;