diff --git a/frontend/src/utils/easyshare.test.ts b/frontend/src/utils/easyshare.test.ts index 1960381..5a49f67 100644 --- a/frontend/src/utils/easyshare.test.ts +++ b/frontend/src/utils/easyshare.test.ts @@ -4,6 +4,7 @@ import type { EasySharePayload } from '@/types/easyshare' import { easyShareCrewLinkInviteCode, easyShareDestinationAppIds, + easyShareMusicTarget, easyShareRoute, } from '@/utils/easyshare' @@ -95,6 +96,35 @@ describe('EasyShare deep links', () => { easyShareCrewLinkInviteCode('link', '', 'invalid-code'), ).toBeNull() }) + + it.each([ + [ + 'track', + 'skyphone://music/server/city-after-dark', + { id: 'city-after-dark', kind: 'track', source: 'server' }, + ], + [ + 'track', + 'skyphone://music/youtube/music-youtube-1', + { id: 'music-youtube-1', kind: 'track', source: 'youtube' }, + ], + [ + 'playlist', + 'skyphone://music/playlist/music-playlist-1', + { id: 'music-playlist-1', kind: 'playlist' }, + ], + ] as const)('extracts the exact shared music target', (kind, link, target) => { + expect(easyShareMusicTarget(kind, link)).toEqual(target) + }) + + it('rejects mismatched music share kinds', () => { + expect( + easyShareMusicTarget( + 'playlist', + 'skyphone://music/server/city-after-dark', + ), + ).toBeNull() + }) }) describe('EasyShare destination suggestions', () => { diff --git a/frontend/src/utils/easyshare.ts b/frontend/src/utils/easyshare.ts index c21ea79..05521ca 100644 --- a/frontend/src/utils/easyshare.ts +++ b/frontend/src/utils/easyshare.ts @@ -75,6 +75,34 @@ export function easyShareCrewLinkInviteCode( return null } +export type EasyShareMusicTarget = + | { id: string; kind: 'playlist' } + | { id: string; kind: 'track'; source: 'server' | 'youtube' } + +export function easyShareMusicTarget( + kind: unknown, + link: unknown, +): EasyShareMusicTarget | null { + if (typeof link !== 'string') return null + if (kind === 'track') { + const match = link.match( + /^skyphone:\/\/music\/(server|youtube)\/([^/?#]+)$/, + ) + if (match) { + return { + id: match[2], + kind: 'track', + source: match[1] as 'server' | 'youtube', + } + } + } + if (kind === 'playlist') { + const match = link.match(/^skyphone:\/\/music\/playlist\/([^/?#]+)$/) + if (match) return { id: match[1], kind: 'playlist' } + } + return null +} + export async function openEasySharePayload( router: Router, payload: EasySharePayload, diff --git a/frontend/src/views/apps/MusicApp.vue b/frontend/src/views/apps/MusicApp.vue index 56b6b96..1ac9a67 100644 --- a/frontend/src/views/apps/MusicApp.vue +++ b/frontend/src/views/apps/MusicApp.vue @@ -46,11 +46,13 @@ import { } from 'lucide-vue-next' import type { CSSProperties } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' +import { useRoute } from 'vue-router' import { useMusicStore } from '@/stores/music' import { useEasyShareStore } from '@/stores/easyshare' import { usePhoneStore } from '@/stores/phone' import type { MusicPlaylist, MusicTrack } from '@/types/music' +import { easyShareMusicTarget } from '@/utils/easyshare' type MusicTab = 'library' | 'playlists' | 'search' type MusicSheet = @@ -69,6 +71,7 @@ const MUSIC_SHEET_TRANSITION_MS = 420 const music = useMusicStore() const easyShare = useEasyShareStore() const phone = usePhoneStore() +const route = useRoute() const activeTab = ref('library') const activePlaylist = ref(null) const addMenuOpened = ref(false) @@ -273,10 +276,11 @@ function openTrackMenu(event: MouseEvent, track: MusicTrack): void { actionMenuOpened.value = true } -function shareTrack(): void { - const track = actionTrack.value +function shareTrack(selectedTrack: MusicTrack | null = actionTrack.value): void { + const track = selectedTrack if (!track) return closeMenus() + playerOpened.value = false easyShare.open({ appId: 'music', copyText: `${track.title} — ${track.artist}`, @@ -505,8 +509,24 @@ watch( }, ) -onMounted(() => { - void music.load() +onMounted(async () => { + await music.load() + const target = easyShareMusicTarget( + route.query.easyShareKind, + route.query.easyShareLink, + ) + if (!target) return + if (target.kind === 'playlist') { + const playlist = music.playlists.find((entry) => entry.id === target.id) + if (playlist) openPlaylist(playlist) + return + } + const track = music.allTracks.find( + (entry) => entry.id === target.id && entry.source === target.source, + ) + if (!track) return + await playTrack(track) + playerOpened.value = true }) onBeforeUnmount(() => { @@ -1271,14 +1291,24 @@ onBeforeUnmount(() => { {{ phone.t('Apps.music.nowPlaying') }} - - - +
+ + + + + + +
{ color: #fff; } -.music-player > header > :last-child { +.music-player-header-actions { justify-self: end; + display: flex; + align-items: center; + gap: 5px; } .music-player-art {