feat(tracks): add share and queue actions menu
This commit is contained in:
1 parent
cf584aa1b2
commit
e9151b6c55
13 files changed
+852
-56
No files matched your search
@@ -116,6 +116,7 @@ pub struct Track {
|
||||
pub id: u64,
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
pub isrc: Option<String>,
|
||||
#[serde(default)]
|
||||
pub duration: u32,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -13,6 +13,14 @@ pub fn add_to_queue(track: QueueTrack, state: State<'_, AppState>) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a track to play next
|
||||
#[tauri::command]
|
||||
pub fn add_to_queue_next(track: QueueTrack, state: State<'_, AppState>) -> Result<(), String> {
|
||||
log::info!("Command: add_to_queue_next - {} by {}", track.title, track.artist);
|
||||
state.queue.add_track_next(track);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add multiple tracks to the queue
|
||||
#[tauri::command]
|
||||
pub fn add_tracks_to_queue(tracks: Vec<QueueTrack>, state: State<'_, AppState>) -> Result<(), String> {
|
||||
|
||||
@@ -130,6 +130,7 @@ pub fn run() {
|
||||
commands::get_audio_devices,
|
||||
// Queue commands
|
||||
commands::add_to_queue,
|
||||
commands::add_to_queue_next,
|
||||
commands::add_tracks_to_queue,
|
||||
commands::set_queue,
|
||||
commands::clear_queue,
|
||||
|
||||
@@ -115,6 +115,39 @@ impl QueueManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a track to play next (after current index if set)
|
||||
pub fn add_track_next(&self, track: QueueTrack) {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let insert_index = state.current_index.map(|idx| idx + 1).unwrap_or(state.tracks.len());
|
||||
|
||||
if insert_index >= state.tracks.len() {
|
||||
state.tracks.push(track);
|
||||
} else {
|
||||
state.tracks.insert(insert_index, track);
|
||||
}
|
||||
|
||||
if state.shuffle {
|
||||
for idx in state.shuffle_order.iter_mut() {
|
||||
if *idx >= insert_index {
|
||||
*idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let new_idx = insert_index;
|
||||
let next_pos = if state.current_index.is_some() {
|
||||
state.shuffle_position + 1
|
||||
} else {
|
||||
state.shuffle_order.len()
|
||||
};
|
||||
|
||||
if next_pos >= state.shuffle_order.len() {
|
||||
state.shuffle_order.push(new_idx);
|
||||
} else {
|
||||
state.shuffle_order.insert(next_pos, new_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the entire queue (replaces existing)
|
||||
pub fn set_queue(&self, new_tracks: Vec<QueueTrack>, start_index: Option<usize>) {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<script lang="ts">
|
||||
import { ChevronRight, MoreHorizontal } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
onPlayNow?: () => void;
|
||||
onPlayNext?: () => void;
|
||||
onPlayLater?: () => void;
|
||||
onAddFavorite?: () => void;
|
||||
onAddToPlaylist?: () => void;
|
||||
onShareQobuz?: () => void;
|
||||
onShareSonglink?: () => void;
|
||||
onGoToAlbum?: () => void;
|
||||
onGoToArtist?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
onPlayNow,
|
||||
onPlayNext,
|
||||
onPlayLater,
|
||||
onAddFavorite,
|
||||
onAddToPlaylist,
|
||||
onShareQobuz,
|
||||
onShareSonglink,
|
||||
onGoToAlbum,
|
||||
onGoToArtist
|
||||
}: Props = $props();
|
||||
|
||||
let isOpen = $state(false);
|
||||
let shareOpen = $state(false);
|
||||
let menuRef: HTMLDivElement | null = null;
|
||||
|
||||
const hasPlayback = $derived(!!(onPlayNow || onPlayNext || onPlayLater));
|
||||
const hasLibrary = $derived(!!(onAddFavorite || onAddToPlaylist));
|
||||
const hasShare = $derived(!!(onShareQobuz || onShareSonglink));
|
||||
const hasNav = $derived(!!(onGoToAlbum || onGoToArtist));
|
||||
const hasMenu = $derived(hasPlayback || hasLibrary || hasShare || hasNav);
|
||||
|
||||
function closeMenu() {
|
||||
isOpen = false;
|
||||
shareOpen = false;
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (menuRef && !menuRef.contains(event.target as Node)) {
|
||||
closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
function handleAction(action?: () => void) {
|
||||
if (!action) return;
|
||||
action();
|
||||
closeMenu();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if hasMenu}
|
||||
<div
|
||||
class="track-menu"
|
||||
bind:this={menuRef}
|
||||
onmousedown={(e) => e.stopPropagation()}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
class="menu-trigger"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
isOpen = !isOpen;
|
||||
shareOpen = false;
|
||||
}}
|
||||
aria-label="Track actions"
|
||||
>
|
||||
<MoreHorizontal size={18} />
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="menu">
|
||||
{#if hasPlayback}
|
||||
{#if onPlayNow}
|
||||
<button class="menu-item" onclick={() => handleAction(onPlayNow)}>Play now</button>
|
||||
{/if}
|
||||
{#if onPlayNext}
|
||||
<button class="menu-item" onclick={() => handleAction(onPlayNext)}>Play next</button>
|
||||
{/if}
|
||||
{#if onPlayLater}
|
||||
<button class="menu-item" onclick={() => handleAction(onPlayLater)}>Play later</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if hasPlayback && (hasLibrary || hasShare || hasNav)}
|
||||
<div class="separator"></div>
|
||||
{/if}
|
||||
|
||||
{#if hasLibrary}
|
||||
{#if onAddFavorite}
|
||||
<button class="menu-item" onclick={() => handleAction(onAddFavorite)}>Add to favorites</button>
|
||||
{/if}
|
||||
{#if onAddToPlaylist}
|
||||
<button class="menu-item" onclick={() => handleAction(onAddToPlaylist)}>Add to playlist</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if hasLibrary && (hasShare || hasNav)}
|
||||
<div class="separator"></div>
|
||||
{/if}
|
||||
|
||||
{#if hasShare}
|
||||
<div
|
||||
class="menu-item submenu-trigger"
|
||||
onmouseenter={() => (shareOpen = true)}
|
||||
onmouseleave={() => (shareOpen = false)}
|
||||
onclick={() => (shareOpen = !shareOpen)}
|
||||
>
|
||||
<span>Share</span>
|
||||
<ChevronRight size={14} />
|
||||
{#if shareOpen}
|
||||
<div class="submenu">
|
||||
{#if onShareQobuz}
|
||||
<button class="menu-item" onclick={() => handleAction(onShareQobuz)}>Share Qobuz link</button>
|
||||
{/if}
|
||||
{#if onShareSonglink}
|
||||
<button class="menu-item" onclick={() => handleAction(onShareSonglink)}>Share Song.link</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasShare && hasNav}
|
||||
<div class="separator"></div>
|
||||
{/if}
|
||||
|
||||
{#if hasNav}
|
||||
{#if onGoToAlbum}
|
||||
<button class="menu-item" onclick={() => handleAction(onGoToAlbum)}>Go to album</button>
|
||||
{/if}
|
||||
{#if onGoToArtist}
|
||||
<button class="menu-item" onclick={() => handleAction(onGoToArtist)}>Go to artist</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.track-menu {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-trigger {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.menu-trigger:hover {
|
||||
background-color: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 6px);
|
||||
min-width: 200px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 10px;
|
||||
padding: 6px 0;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
transition: background-color 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: var(--bg-hover);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.submenu-trigger {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: -6px;
|
||||
margin-left: 6px;
|
||||
min-width: 200px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 10px;
|
||||
padding: 6px 0;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
z-index: 70;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Play, Plus, ListPlus, MoreHorizontal } from 'lucide-svelte';
|
||||
import { Play } from 'lucide-svelte';
|
||||
import TrackMenu from './TrackMenu.svelte';
|
||||
|
||||
interface Props {
|
||||
number: number;
|
||||
@@ -9,8 +10,19 @@
|
||||
quality?: string;
|
||||
isPlaying?: boolean;
|
||||
onPlay?: () => void;
|
||||
menuActions?: TrackMenuActions;
|
||||
}
|
||||
|
||||
interface TrackMenuActions {
|
||||
onPlayNow?: () => void;
|
||||
onPlayNext?: () => void;
|
||||
onPlayLater?: () => void;
|
||||
onAddFavorite?: () => void;
|
||||
onAddToPlaylist?: () => void;
|
||||
onAddToQueue?: () => void;
|
||||
onShareQobuz?: () => void;
|
||||
onShareSonglink?: () => void;
|
||||
onGoToAlbum?: () => void;
|
||||
onGoToArtist?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -21,11 +33,11 @@
|
||||
quality,
|
||||
isPlaying = false,
|
||||
onPlay,
|
||||
onAddToPlaylist,
|
||||
onAddToQueue
|
||||
menuActions
|
||||
}: Props = $props();
|
||||
|
||||
let isHovered = $state(false);
|
||||
const playNowAction = $derived(menuActions?.onPlayNow ?? onPlay);
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -70,29 +82,19 @@
|
||||
<div class="track-quality">{quality}</div>
|
||||
{/if}
|
||||
|
||||
<!-- Hover Actions -->
|
||||
{#if isHovered && !isPlaying}
|
||||
<div class="track-actions">
|
||||
{#if onAddToQueue}
|
||||
<button
|
||||
class="action-btn"
|
||||
onclick={(e) => { e.stopPropagation(); onAddToQueue(); }}
|
||||
title="Add to queue"
|
||||
>
|
||||
<Plus size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onAddToPlaylist}
|
||||
<button
|
||||
class="action-btn"
|
||||
onclick={(e) => { e.stopPropagation(); onAddToPlaylist(); }}
|
||||
title="Add to playlist"
|
||||
>
|
||||
<ListPlus size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="track-actions">
|
||||
<TrackMenu
|
||||
onPlayNow={playNowAction}
|
||||
onPlayNext={menuActions?.onPlayNext}
|
||||
onPlayLater={menuActions?.onPlayLater}
|
||||
onAddFavorite={menuActions?.onAddFavorite}
|
||||
onAddToPlaylist={menuActions?.onAddToPlaylist}
|
||||
onShareQobuz={menuActions?.onShareQobuz}
|
||||
onShareSonglink={menuActions?.onShareSonglink}
|
||||
onGoToAlbum={menuActions?.onGoToAlbum}
|
||||
onGoToArtist={menuActions?.onGoToArtist}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -213,20 +215,15 @@
|
||||
}
|
||||
|
||||
.track-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: var(--text-primary);
|
||||
.track-row:hover .track-actions,
|
||||
.track-row.playing .track-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -14,6 +14,7 @@
|
||||
hires?: boolean;
|
||||
bitDepth?: number;
|
||||
samplingRate?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -22,6 +23,7 @@
|
||||
artwork: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
artistId?: number;
|
||||
year: string;
|
||||
label: string;
|
||||
genre: string;
|
||||
@@ -33,13 +35,36 @@
|
||||
onBack: () => void;
|
||||
onArtistClick?: () => void;
|
||||
onTrackPlay?: (track: Track) => void;
|
||||
onTrackPlayNext?: (track: Track) => void;
|
||||
onTrackPlayLater?: (track: Track) => void;
|
||||
onTrackAddFavorite?: (trackId: number) => void;
|
||||
onTrackShareQobuz?: (trackId: number) => void;
|
||||
onTrackShareSonglink?: (track: Track) => void;
|
||||
onTrackGoToAlbum?: (albumId: string) => void;
|
||||
onTrackGoToArtist?: (artistId: number) => void;
|
||||
onPlayAll?: () => void;
|
||||
onShuffleAll?: () => void;
|
||||
onAddToQueue?: () => void;
|
||||
onAddTrackToPlaylist?: (trackId: number) => void;
|
||||
}
|
||||
|
||||
let { album, onBack, onArtistClick, onTrackPlay, onPlayAll, onShuffleAll, onAddToQueue, onAddTrackToPlaylist }: Props = $props();
|
||||
let {
|
||||
album,
|
||||
onBack,
|
||||
onArtistClick,
|
||||
onTrackPlay,
|
||||
onTrackPlayNext,
|
||||
onTrackPlayLater,
|
||||
onTrackAddFavorite,
|
||||
onTrackShareQobuz,
|
||||
onTrackShareSonglink,
|
||||
onTrackGoToAlbum,
|
||||
onTrackGoToArtist,
|
||||
onPlayAll,
|
||||
onShuffleAll,
|
||||
onAddToQueue,
|
||||
onAddTrackToPlaylist
|
||||
}: Props = $props();
|
||||
|
||||
let currentTrack = $state<number | null>(null);
|
||||
let isFavorite = $state(false);
|
||||
@@ -150,7 +175,20 @@
|
||||
currentTrack = track.id;
|
||||
onTrackPlay?.(track);
|
||||
}}
|
||||
onAddToPlaylist={onAddTrackToPlaylist ? () => onAddTrackToPlaylist(track.id) : undefined}
|
||||
menuActions={{
|
||||
onPlayNow: () => {
|
||||
currentTrack = track.id;
|
||||
onTrackPlay?.(track);
|
||||
},
|
||||
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(track) : undefined,
|
||||
onPlayLater: onTrackPlayLater ? () => onTrackPlayLater(track) : undefined,
|
||||
onAddFavorite: onTrackAddFavorite ? () => onTrackAddFavorite(track.id) : undefined,
|
||||
onAddToPlaylist: onAddTrackToPlaylist ? () => onAddTrackToPlaylist(track.id) : undefined,
|
||||
onShareQobuz: onTrackShareQobuz ? () => onTrackShareQobuz(track.id) : undefined,
|
||||
onShareSonglink: onTrackShareSonglink ? () => onTrackShareSonglink(track) : undefined,
|
||||
onGoToAlbum: onTrackGoToAlbum ? () => onTrackGoToAlbum(album.id) : undefined,
|
||||
onGoToArtist: album.artistId && onTrackGoToArtist ? () => onTrackGoToArtist(album.artistId) : undefined
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { onMount } from 'svelte';
|
||||
import { ArrowLeft, User, ChevronDown, ChevronUp, Play, Plus, Music } from 'lucide-svelte';
|
||||
import { ArrowLeft, User, ChevronDown, ChevronUp, Play, Music } from 'lucide-svelte';
|
||||
import AlbumCard from '../AlbumCard.svelte';
|
||||
import TrackMenu from '../TrackMenu.svelte';
|
||||
|
||||
interface Album {
|
||||
id: string;
|
||||
@@ -27,10 +28,11 @@
|
||||
title: string;
|
||||
image?: { small?: string; thumbnail?: string; large?: string };
|
||||
};
|
||||
performer?: { name: string };
|
||||
performer?: { id?: number; name: string };
|
||||
hires_streamable?: boolean;
|
||||
maximum_bit_depth?: number;
|
||||
maximum_sampling_rate?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
interface SearchResults {
|
||||
@@ -64,9 +66,32 @@
|
||||
onLoadMore?: () => void;
|
||||
isLoadingMore?: boolean;
|
||||
onTrackPlay?: (track: DisplayTrack) => void;
|
||||
onTrackPlayNext?: (track: Track) => void;
|
||||
onTrackPlayLater?: (track: Track) => void;
|
||||
onTrackAddFavorite?: (trackId: number) => void;
|
||||
onTrackAddToPlaylist?: (trackId: number) => void;
|
||||
onTrackShareQobuz?: (trackId: number) => void;
|
||||
onTrackShareSonglink?: (track: Track) => void;
|
||||
onTrackGoToAlbum?: (albumId: string) => void;
|
||||
onTrackGoToArtist?: (artistId: number) => void;
|
||||
}
|
||||
|
||||
let { artist, onBack, onAlbumClick, onLoadMore, isLoadingMore = false, onTrackPlay }: Props = $props();
|
||||
let {
|
||||
artist,
|
||||
onBack,
|
||||
onAlbumClick,
|
||||
onLoadMore,
|
||||
isLoadingMore = false,
|
||||
onTrackPlay,
|
||||
onTrackPlayNext,
|
||||
onTrackPlayLater,
|
||||
onTrackAddFavorite,
|
||||
onTrackAddToPlaylist,
|
||||
onTrackShareQobuz,
|
||||
onTrackShareSonglink,
|
||||
onTrackGoToAlbum,
|
||||
onTrackGoToArtist
|
||||
}: Props = $props();
|
||||
|
||||
let bioExpanded = $state(false);
|
||||
let imageError = $state(false);
|
||||
@@ -233,7 +258,13 @@
|
||||
{:else}
|
||||
<div class="tracks-list">
|
||||
{#each topTracks as track, index}
|
||||
<button class="track-row" onclick={() => handleTrackPlay(track)}>
|
||||
<div
|
||||
class="track-row"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => handleTrackPlay(track)}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleTrackPlay(track)}
|
||||
>
|
||||
<div class="track-number">{index + 1}</div>
|
||||
<div class="track-artwork">
|
||||
{#if track.album?.image?.thumbnail || track.album?.image?.small}
|
||||
@@ -249,7 +280,20 @@
|
||||
<div class="track-album">{track.album?.title || ''}</div>
|
||||
</div>
|
||||
<div class="track-duration">{formatDuration(track.duration)}</div>
|
||||
</button>
|
||||
<div class="track-actions">
|
||||
<TrackMenu
|
||||
onPlayNow={() => handleTrackPlay(track)}
|
||||
onPlayNext={onTrackPlayNext ? () => onTrackPlayNext(track) : undefined}
|
||||
onPlayLater={onTrackPlayLater ? () => onTrackPlayLater(track) : undefined}
|
||||
onAddFavorite={onTrackAddFavorite ? () => onTrackAddFavorite(track.id) : undefined}
|
||||
onAddToPlaylist={onTrackAddToPlaylist ? () => onTrackAddToPlaylist(track.id) : undefined}
|
||||
onShareQobuz={onTrackShareQobuz ? () => onTrackShareQobuz(track.id) : undefined}
|
||||
onShareSonglink={onTrackShareSonglink ? () => onTrackShareSonglink(track) : undefined}
|
||||
onGoToAlbum={track.album?.id && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.album.id) : undefined}
|
||||
onGoToArtist={(track.performer?.id || artist.id) && onTrackGoToArtist ? () => onTrackGoToArtist(track.performer?.id || artist.id) : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -573,6 +617,18 @@
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.track-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.track-row:hover .track-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.artist-header {
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
title: string;
|
||||
duration: number;
|
||||
track_number: number;
|
||||
performer?: { name: string };
|
||||
performer?: { id?: number; name: string };
|
||||
album?: { id: string; title: string; image: { small?: string; thumbnail?: string; large?: string } };
|
||||
hires: boolean;
|
||||
maximum_bit_depth?: number;
|
||||
maximum_sampling_rate?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
interface FavoriteArtist {
|
||||
@@ -37,6 +38,14 @@
|
||||
onAlbumClick?: (albumId: string) => void;
|
||||
onTrackPlay?: (track: DisplayTrack) => void;
|
||||
onArtistClick?: (artistId: number) => void;
|
||||
onTrackPlayNext?: (track: DisplayTrack) => void;
|
||||
onTrackPlayLater?: (track: DisplayTrack) => void;
|
||||
onTrackAddFavorite?: (trackId: number) => void;
|
||||
onTrackAddToPlaylist?: (trackId: number) => void;
|
||||
onTrackShareQobuz?: (trackId: number) => void;
|
||||
onTrackShareSonglink?: (track: DisplayTrack) => void;
|
||||
onTrackGoToAlbum?: (albumId: string) => void;
|
||||
onTrackGoToArtist?: (artistId: number) => void;
|
||||
}
|
||||
|
||||
interface DisplayTrack {
|
||||
@@ -46,14 +55,29 @@
|
||||
artist?: string;
|
||||
album?: string;
|
||||
albumArt?: string;
|
||||
albumId?: string;
|
||||
artistId?: number;
|
||||
duration: string;
|
||||
durationSeconds: number;
|
||||
hires?: boolean;
|
||||
bitDepth?: number;
|
||||
samplingRate?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
let { onAlbumClick, onTrackPlay, onArtistClick }: Props = $props();
|
||||
let {
|
||||
onAlbumClick,
|
||||
onTrackPlay,
|
||||
onArtistClick,
|
||||
onTrackPlayNext,
|
||||
onTrackPlayLater,
|
||||
onTrackAddFavorite,
|
||||
onTrackAddToPlaylist,
|
||||
onTrackShareQobuz,
|
||||
onTrackShareSonglink,
|
||||
onTrackGoToAlbum,
|
||||
onTrackGoToArtist
|
||||
}: Props = $props();
|
||||
|
||||
type TabType = 'tracks' | 'albums' | 'artists';
|
||||
let activeTab = $state<TabType>('tracks');
|
||||
@@ -114,11 +138,14 @@
|
||||
artist: track.performer?.name,
|
||||
album: track.album?.title,
|
||||
albumArt: track.album?.image?.thumbnail || track.album?.image?.small,
|
||||
albumId: track.album?.id,
|
||||
artistId: track.performer?.id,
|
||||
duration: formatDuration(track.duration),
|
||||
durationSeconds: track.duration,
|
||||
hires: track.hires,
|
||||
bitDepth: track.maximum_bit_depth,
|
||||
samplingRate: track.maximum_sampling_rate,
|
||||
isrc: track.isrc,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -247,6 +274,62 @@
|
||||
duration={formatDuration(track.duration)}
|
||||
quality={track.hires ? 'Hi-Res' : undefined}
|
||||
onPlay={() => handleTrackClick(track, index)}
|
||||
menuActions={{
|
||||
onPlayNow: () => handleTrackClick(track, index),
|
||||
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext({
|
||||
id: track.id,
|
||||
number: index + 1,
|
||||
title: track.title,
|
||||
artist: track.performer?.name,
|
||||
album: track.album?.title,
|
||||
albumArt: track.album?.image?.thumbnail || track.album?.image?.small,
|
||||
albumId: track.album?.id,
|
||||
artistId: track.performer?.id,
|
||||
duration: formatDuration(track.duration),
|
||||
durationSeconds: track.duration,
|
||||
hires: track.hires,
|
||||
bitDepth: track.maximum_bit_depth,
|
||||
samplingRate: track.maximum_sampling_rate,
|
||||
isrc: track.isrc
|
||||
}) : undefined,
|
||||
onPlayLater: onTrackPlayLater ? () => onTrackPlayLater({
|
||||
id: track.id,
|
||||
number: index + 1,
|
||||
title: track.title,
|
||||
artist: track.performer?.name,
|
||||
album: track.album?.title,
|
||||
albumArt: track.album?.image?.thumbnail || track.album?.image?.small,
|
||||
albumId: track.album?.id,
|
||||
artistId: track.performer?.id,
|
||||
duration: formatDuration(track.duration),
|
||||
durationSeconds: track.duration,
|
||||
hires: track.hires,
|
||||
bitDepth: track.maximum_bit_depth,
|
||||
samplingRate: track.maximum_sampling_rate,
|
||||
isrc: track.isrc
|
||||
}) : undefined,
|
||||
onAddFavorite: onTrackAddFavorite ? () => onTrackAddFavorite(track.id) : undefined,
|
||||
onAddToPlaylist: onTrackAddToPlaylist ? () => onTrackAddToPlaylist(track.id) : undefined,
|
||||
onShareQobuz: onTrackShareQobuz ? () => onTrackShareQobuz(track.id) : undefined,
|
||||
onShareSonglink: onTrackShareSonglink ? () => onTrackShareSonglink({
|
||||
id: track.id,
|
||||
number: index + 1,
|
||||
title: track.title,
|
||||
artist: track.performer?.name,
|
||||
album: track.album?.title,
|
||||
albumArt: track.album?.image?.thumbnail || track.album?.image?.small,
|
||||
albumId: track.album?.id,
|
||||
artistId: track.performer?.id,
|
||||
duration: formatDuration(track.duration),
|
||||
durationSeconds: track.duration,
|
||||
hires: track.hires,
|
||||
bitDepth: track.maximum_bit_depth,
|
||||
samplingRate: track.maximum_sampling_rate,
|
||||
isrc: track.isrc
|
||||
}) : undefined,
|
||||
onGoToAlbum: track.album?.id && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.album.id) : undefined,
|
||||
onGoToArtist: track.performer?.id && onTrackGoToArtist ? () => onTrackGoToArtist(track.performer.id) : undefined
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -74,10 +74,18 @@
|
||||
interface Props {
|
||||
onAlbumClick?: (album: LocalAlbum) => void;
|
||||
onTrackPlay?: (track: LocalTrack) => void;
|
||||
onTrackPlayNext?: (track: LocalTrack) => void;
|
||||
onTrackPlayLater?: (track: LocalTrack) => void;
|
||||
onSetLocalQueue?: (trackIds: number[]) => void;
|
||||
}
|
||||
|
||||
let { onAlbumClick, onTrackPlay, onSetLocalQueue }: Props = $props();
|
||||
let {
|
||||
onAlbumClick,
|
||||
onTrackPlay,
|
||||
onTrackPlayNext,
|
||||
onTrackPlayLater,
|
||||
onSetLocalQueue
|
||||
}: Props = $props();
|
||||
|
||||
// View state
|
||||
type TabType = 'albums' | 'artists' | 'tracks';
|
||||
@@ -453,6 +461,11 @@
|
||||
duration={formatDuration(track.duration_secs)}
|
||||
quality={getQualityBadge(track)}
|
||||
onPlay={() => handleTrackPlay(track)}
|
||||
menuActions={{
|
||||
onPlayNow: () => handleTrackPlay(track),
|
||||
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(track) : undefined,
|
||||
onPlayLater: onTrackPlayLater ? () => onTrackPlayLater(track) : undefined
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -658,6 +671,11 @@
|
||||
duration={formatDuration(track.duration_secs)}
|
||||
quality={getQualityBadge(track)}
|
||||
onPlay={() => handleTrackPlay(track)}
|
||||
menuActions={{
|
||||
onPlayNow: () => handleTrackPlay(track),
|
||||
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(track) : undefined,
|
||||
onPlayLater: onTrackPlayLater ? () => onTrackPlayLater(track) : undefined
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
title: string;
|
||||
duration: number;
|
||||
track_number: number;
|
||||
performer?: { name: string };
|
||||
performer?: { id?: number; name: string };
|
||||
album?: { id: string; title: string; image: { small?: string; thumbnail?: string; large?: string } };
|
||||
hires: boolean;
|
||||
maximum_bit_depth?: number;
|
||||
maximum_sampling_rate?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
interface Playlist {
|
||||
@@ -36,11 +37,14 @@
|
||||
artist?: string;
|
||||
album?: string;
|
||||
albumArt?: string;
|
||||
albumId?: string;
|
||||
artistId?: number;
|
||||
duration: string;
|
||||
durationSeconds: number;
|
||||
hires?: boolean;
|
||||
bitDepth?: number;
|
||||
samplingRate?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
interface PlaylistSettings {
|
||||
@@ -59,9 +63,29 @@
|
||||
playlistId: number;
|
||||
onBack: () => void;
|
||||
onTrackPlay?: (track: DisplayTrack) => void;
|
||||
onTrackPlayNext?: (track: DisplayTrack) => void;
|
||||
onTrackPlayLater?: (track: DisplayTrack) => void;
|
||||
onTrackAddFavorite?: (trackId: number) => void;
|
||||
onTrackAddToPlaylist?: (trackId: number) => void;
|
||||
onTrackShareQobuz?: (trackId: number) => void;
|
||||
onTrackShareSonglink?: (track: DisplayTrack) => void;
|
||||
onTrackGoToAlbum?: (albumId: string) => void;
|
||||
onTrackGoToArtist?: (artistId: number) => void;
|
||||
}
|
||||
|
||||
let { playlistId, onBack, onTrackPlay }: Props = $props();
|
||||
let {
|
||||
playlistId,
|
||||
onBack,
|
||||
onTrackPlay,
|
||||
onTrackPlayNext,
|
||||
onTrackPlayLater,
|
||||
onTrackAddFavorite,
|
||||
onTrackAddToPlaylist,
|
||||
onTrackShareQobuz,
|
||||
onTrackShareSonglink,
|
||||
onTrackGoToAlbum,
|
||||
onTrackGoToArtist
|
||||
}: Props = $props();
|
||||
|
||||
let playlist = $state<Playlist | null>(null);
|
||||
let tracks = $state<DisplayTrack[]>([]);
|
||||
@@ -96,11 +120,14 @@
|
||||
artist: t.performer?.name,
|
||||
album: t.album?.title,
|
||||
albumArt: t.album?.image?.thumbnail || t.album?.image?.small,
|
||||
albumId: t.album?.id,
|
||||
artistId: t.performer?.id,
|
||||
duration: formatDuration(t.duration),
|
||||
durationSeconds: t.duration,
|
||||
hires: t.hires,
|
||||
bitDepth: t.maximum_bit_depth,
|
||||
samplingRate: t.maximum_sampling_rate,
|
||||
isrc: t.isrc,
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -453,6 +480,17 @@
|
||||
duration={track.duration}
|
||||
quality={track.hires ? 'Hi-Res' : undefined}
|
||||
onPlay={() => handleTrackClick(track)}
|
||||
menuActions={{
|
||||
onPlayNow: () => handleTrackClick(track),
|
||||
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(track) : undefined,
|
||||
onPlayLater: onTrackPlayLater ? () => onTrackPlayLater(track) : undefined,
|
||||
onAddFavorite: onTrackAddFavorite ? () => onTrackAddFavorite(track.id) : undefined,
|
||||
onAddToPlaylist: onTrackAddToPlaylist ? () => onTrackAddToPlaylist(track.id) : undefined,
|
||||
onShareQobuz: onTrackShareQobuz ? () => onTrackShareQobuz(track.id) : undefined,
|
||||
onShareSonglink: onTrackShareSonglink ? () => onTrackShareSonglink(track) : undefined,
|
||||
onGoToAlbum: track.albumId && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.albumId) : undefined,
|
||||
onGoToArtist: track.artistId && onTrackGoToArtist ? () => onTrackGoToArtist(track.artistId) : undefined
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Search, Disc3, Music, Mic2, User, X } from 'lucide-svelte';
|
||||
import AlbumCard from '../AlbumCard.svelte';
|
||||
import TrackMenu from '../TrackMenu.svelte';
|
||||
import { getSearchState, setSearchState, type SearchResults, type SearchTab } from '$lib/stores/searchState';
|
||||
|
||||
onMount(() => {
|
||||
@@ -24,10 +25,30 @@
|
||||
interface Props {
|
||||
onAlbumClick?: (albumId: string) => void;
|
||||
onTrackPlay?: (track: Track) => void;
|
||||
onTrackPlayNext?: (track: Track) => void;
|
||||
onTrackPlayLater?: (track: Track) => void;
|
||||
onTrackAddFavorite?: (trackId: number) => void;
|
||||
onTrackAddToPlaylist?: (trackId: number) => void;
|
||||
onTrackShareQobuz?: (trackId: number) => void;
|
||||
onTrackShareSonglink?: (track: Track) => void;
|
||||
onTrackGoToAlbum?: (albumId: string) => void;
|
||||
onTrackGoToArtist?: (artistId: number) => void;
|
||||
onArtistClick?: (artistId: number) => void;
|
||||
}
|
||||
|
||||
let { onAlbumClick, onTrackPlay, onArtistClick }: Props = $props();
|
||||
let {
|
||||
onAlbumClick,
|
||||
onTrackPlay,
|
||||
onTrackPlayNext,
|
||||
onTrackPlayLater,
|
||||
onTrackAddFavorite,
|
||||
onTrackAddToPlaylist,
|
||||
onTrackShareQobuz,
|
||||
onTrackShareSonglink,
|
||||
onTrackGoToAlbum,
|
||||
onTrackGoToArtist,
|
||||
onArtistClick
|
||||
}: Props = $props();
|
||||
|
||||
interface Album {
|
||||
id: string;
|
||||
@@ -45,13 +66,15 @@
|
||||
title: string;
|
||||
duration: number;
|
||||
album?: {
|
||||
id?: string;
|
||||
title: string;
|
||||
image?: { small?: string; thumbnail?: string; large?: string };
|
||||
};
|
||||
performer?: { name: string };
|
||||
performer?: { id?: number; name: string };
|
||||
hires_streamable?: boolean;
|
||||
maximum_bit_depth?: number;
|
||||
maximum_sampling_rate?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
interface Artist {
|
||||
@@ -340,9 +363,12 @@
|
||||
{:else}
|
||||
<div class="tracks-list">
|
||||
{#each trackResults.items as track, index}
|
||||
<button
|
||||
<div
|
||||
class="track-row"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => onTrackPlay?.(track)}
|
||||
onkeydown={(e) => e.key === 'Enter' && onTrackPlay?.(track)}
|
||||
>
|
||||
<div class="track-number">{index + 1}</div>
|
||||
{#if failedTrackImages.has(track.id) || !getTrackArtwork(track)}
|
||||
@@ -358,7 +384,20 @@
|
||||
</div>
|
||||
<div class="track-quality">{getQualityLabel(track)}</div>
|
||||
<div class="track-duration">{formatDuration(track.duration)}</div>
|
||||
</button>
|
||||
<div class="track-actions">
|
||||
<TrackMenu
|
||||
onPlayNow={() => onTrackPlay?.(track)}
|
||||
onPlayNext={onTrackPlayNext ? () => onTrackPlayNext(track) : undefined}
|
||||
onPlayLater={onTrackPlayLater ? () => onTrackPlayLater(track) : undefined}
|
||||
onAddFavorite={onTrackAddFavorite ? () => onTrackAddFavorite(track.id) : undefined}
|
||||
onAddToPlaylist={onTrackAddToPlaylist ? () => onTrackAddToPlaylist(track.id) : undefined}
|
||||
onShareQobuz={onTrackShareQobuz ? () => onTrackShareQobuz(track.id) : undefined}
|
||||
onShareSonglink={onTrackShareSonglink ? () => onTrackShareSonglink(track) : undefined}
|
||||
onGoToAlbum={track.album?.id && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.album.id) : undefined}
|
||||
onGoToArtist={track.performer?.id && onTrackGoToArtist ? () => onTrackGoToArtist(track.performer.id) : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if hasMoreTracks}
|
||||
@@ -643,6 +682,18 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.track-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.track-row:hover .track-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.artists-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
|
||||
+241
-2
@@ -30,13 +30,15 @@
|
||||
title: string;
|
||||
duration: number;
|
||||
album?: {
|
||||
id?: string;
|
||||
title: string;
|
||||
image?: { small?: string; thumbnail?: string; large?: string };
|
||||
};
|
||||
performer?: { name: string };
|
||||
performer?: { id?: number; name: string };
|
||||
hires_streamable?: boolean;
|
||||
maximum_bit_depth?: number;
|
||||
maximum_sampling_rate?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
interface QobuzAlbum {
|
||||
@@ -104,6 +106,9 @@
|
||||
hires?: boolean;
|
||||
bitDepth?: number;
|
||||
samplingRate?: number;
|
||||
albumId?: string;
|
||||
artistId?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
interface AlbumDetail {
|
||||
@@ -139,6 +144,16 @@
|
||||
artwork_url: string | null;
|
||||
}
|
||||
|
||||
interface SongLinkResponse {
|
||||
pageUrl: string;
|
||||
title?: string;
|
||||
artist?: string;
|
||||
thumbnailUrl?: string;
|
||||
platforms: Record<string, string>;
|
||||
identifier: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
interface BackendQueueState {
|
||||
current_track: BackendQueueTrack | null;
|
||||
current_index: number | null;
|
||||
@@ -286,7 +301,10 @@
|
||||
quality: track.hires_streamable ? 'Hi-Res' : 'CD',
|
||||
hires: track.hires_streamable,
|
||||
bitDepth: track.maximum_bit_depth,
|
||||
samplingRate: track.maximum_sampling_rate
|
||||
samplingRate: track.maximum_sampling_rate,
|
||||
albumId: album.id,
|
||||
artistId: track.performer?.id ?? album.artist?.id,
|
||||
isrc: track.isrc
|
||||
})) || []
|
||||
};
|
||||
}
|
||||
@@ -344,6 +362,151 @@
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
async function copyToClipboard(text: string, successMessage: string) {
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
showToast(successMessage, 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err);
|
||||
showToast('Failed to copy link', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueueTrackFromQobuz(track: QobuzTrack): BackendQueueTrack {
|
||||
const artwork = track.album?.image?.large || track.album?.image?.thumbnail || track.album?.image?.small || '';
|
||||
return {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.performer?.name || 'Unknown Artist',
|
||||
album: track.album?.title || '',
|
||||
duration_secs: track.duration,
|
||||
artwork_url: artwork || null
|
||||
};
|
||||
}
|
||||
|
||||
function buildQueueTrackFromAlbumTrack(track: Track): BackendQueueTrack {
|
||||
const artwork = selectedAlbum?.artwork || '';
|
||||
return {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist || selectedAlbum?.artist || 'Unknown Artist',
|
||||
album: selectedAlbum?.title || '',
|
||||
duration_secs: track.durationSeconds,
|
||||
artwork_url: artwork || null
|
||||
};
|
||||
}
|
||||
|
||||
function buildQueueTrackFromDisplayTrack(track: PlaylistTrack): BackendQueueTrack {
|
||||
return {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist || 'Unknown Artist',
|
||||
album: track.album || 'Playlist',
|
||||
duration_secs: track.durationSeconds,
|
||||
artwork_url: track.albumArt || null
|
||||
};
|
||||
}
|
||||
|
||||
function buildQueueTrackFromLocalTrack(track: LocalLibraryTrack): BackendQueueTrack {
|
||||
const artwork = track.artwork_path ? `file://${track.artwork_path}` : null;
|
||||
return {
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
album: track.album,
|
||||
duration_secs: track.duration_secs,
|
||||
artwork_url: artwork
|
||||
};
|
||||
}
|
||||
|
||||
async function queueTrackNext(queueTrack: BackendQueueTrack, isLocal = false) {
|
||||
try {
|
||||
await invoke('add_to_queue_next', { track: queueTrack });
|
||||
if (isLocal) {
|
||||
localTrackIds = new Set([...localTrackIds, queueTrack.id]);
|
||||
}
|
||||
await syncQueueState();
|
||||
showToast('Queued to play next', 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to queue track next:', err);
|
||||
showToast('Failed to queue track', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function queueTrackLater(queueTrack: BackendQueueTrack, isLocal = false) {
|
||||
try {
|
||||
await invoke('add_to_queue', { track: queueTrack });
|
||||
if (isLocal) {
|
||||
localTrackIds = new Set([...localTrackIds, queueTrack.id]);
|
||||
}
|
||||
await syncQueueState();
|
||||
showToast('Added to queue', 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to queue track:', err);
|
||||
showToast('Failed to add to queue', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function addTrackToFavorites(trackId: number) {
|
||||
try {
|
||||
await invoke('add_favorite', { favType: 'track', itemId: String(trackId) });
|
||||
showToast('Added to favorites', 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to add to favorites:', err);
|
||||
showToast('Failed to add to favorites', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function shareQobuzTrackLink(trackId: number) {
|
||||
try {
|
||||
const url = await invoke<string>('get_qobuz_track_url', { trackId });
|
||||
await copyToClipboard(url, 'Qobuz link copied');
|
||||
} catch (err) {
|
||||
console.error('Failed to get Qobuz link:', err);
|
||||
showToast('Failed to share link', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveTrackIsrc(trackId: number, isrc?: string): Promise<string | null> {
|
||||
if (isrc && isrc.trim()) {
|
||||
return isrc;
|
||||
}
|
||||
try {
|
||||
const fullTrack = await invoke<QobuzTrack>('get_track', { trackId });
|
||||
return fullTrack.isrc || null;
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch track ISRC:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function shareSonglinkTrack(trackId: number, isrc?: string) {
|
||||
const resolvedIsrc = await resolveTrackIsrc(trackId, isrc);
|
||||
if (!resolvedIsrc) {
|
||||
showToast('ISRC not available for this track', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await invoke<SongLinkResponse>('share_track_songlink', { isrc: resolvedIsrc });
|
||||
await copyToClipboard(response.pageUrl, 'Song.link copied');
|
||||
} catch (err) {
|
||||
console.error('Failed to get Song.link:', err);
|
||||
showToast('Failed to share link', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Playback Functions
|
||||
async function handleTrackPlay(track: QobuzTrack) {
|
||||
console.log('Playing track:', track);
|
||||
@@ -771,6 +934,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleQobuzTrackPlayNext(track: QobuzTrack) {
|
||||
queueTrackNext(buildQueueTrackFromQobuz(track));
|
||||
}
|
||||
|
||||
function handleQobuzTrackPlayLater(track: QobuzTrack) {
|
||||
queueTrackLater(buildQueueTrackFromQobuz(track));
|
||||
}
|
||||
|
||||
function handleAlbumTrackPlayNext(track: Track) {
|
||||
queueTrackNext(buildQueueTrackFromAlbumTrack(track));
|
||||
}
|
||||
|
||||
function handleAlbumTrackPlayLater(track: Track) {
|
||||
queueTrackLater(buildQueueTrackFromAlbumTrack(track));
|
||||
}
|
||||
|
||||
function handleDisplayTrackPlayNext(track: PlaylistTrack) {
|
||||
queueTrackNext(buildQueueTrackFromDisplayTrack(track));
|
||||
}
|
||||
|
||||
function handleDisplayTrackPlayLater(track: PlaylistTrack) {
|
||||
queueTrackLater(buildQueueTrackFromDisplayTrack(track));
|
||||
}
|
||||
|
||||
function handleLocalTrackPlayNext(track: LocalLibraryTrack) {
|
||||
queueTrackNext(buildQueueTrackFromLocalTrack(track), true);
|
||||
}
|
||||
|
||||
function handleLocalTrackPlayLater(track: LocalLibraryTrack) {
|
||||
queueTrackLater(buildQueueTrackFromLocalTrack(track), true);
|
||||
}
|
||||
|
||||
// Handle playing a track from playlist view
|
||||
interface PlaylistTrack {
|
||||
id: number;
|
||||
@@ -784,6 +979,9 @@
|
||||
hires?: boolean;
|
||||
bitDepth?: number;
|
||||
samplingRate?: number;
|
||||
albumId?: string;
|
||||
artistId?: number;
|
||||
isrc?: string;
|
||||
}
|
||||
|
||||
async function handlePlaylistTrackPlay(track: PlaylistTrack) {
|
||||
@@ -1223,6 +1421,14 @@
|
||||
<SearchView
|
||||
onAlbumClick={handleAlbumClick}
|
||||
onTrackPlay={handleTrackPlay}
|
||||
onTrackPlayNext={handleQobuzTrackPlayNext}
|
||||
onTrackPlayLater={handleQobuzTrackPlayLater}
|
||||
onTrackAddFavorite={addTrackToFavorites}
|
||||
onTrackAddToPlaylist={(trackId) => openAddToPlaylist([trackId])}
|
||||
onTrackShareQobuz={shareQobuzTrackLink}
|
||||
onTrackShareSonglink={(track) => shareSonglinkTrack(track.id, track.isrc)}
|
||||
onTrackGoToAlbum={handleAlbumClick}
|
||||
onTrackGoToArtist={handleArtistClick}
|
||||
onArtistClick={handleArtistClick}
|
||||
/>
|
||||
{:else if activeView === 'settings'}
|
||||
@@ -1238,6 +1444,13 @@
|
||||
onBack={goBack}
|
||||
onArtistClick={() => selectedAlbum?.artistId && handleArtistClick(selectedAlbum.artistId)}
|
||||
onTrackPlay={handleAlbumTrackPlay}
|
||||
onTrackPlayNext={handleAlbumTrackPlayNext}
|
||||
onTrackPlayLater={handleAlbumTrackPlayLater}
|
||||
onTrackAddFavorite={addTrackToFavorites}
|
||||
onTrackShareQobuz={shareQobuzTrackLink}
|
||||
onTrackShareSonglink={(track) => shareSonglinkTrack(track.id, track.isrc)}
|
||||
onTrackGoToAlbum={handleAlbumClick}
|
||||
onTrackGoToArtist={handleArtistClick}
|
||||
onPlayAll={handlePlayAllAlbum}
|
||||
onShuffleAll={handleShuffleAlbum}
|
||||
onAddToQueue={handleAddAlbumToQueue}
|
||||
@@ -1249,10 +1462,20 @@
|
||||
onBack={goBack}
|
||||
onAlbumClick={handleAlbumClick}
|
||||
onTrackPlay={handlePlaylistTrackPlay}
|
||||
onTrackPlayNext={handleQobuzTrackPlayNext}
|
||||
onTrackPlayLater={handleQobuzTrackPlayLater}
|
||||
onTrackAddFavorite={addTrackToFavorites}
|
||||
onTrackAddToPlaylist={(trackId) => openAddToPlaylist([trackId])}
|
||||
onTrackShareQobuz={shareQobuzTrackLink}
|
||||
onTrackShareSonglink={(track) => shareSonglinkTrack(track.id, track.isrc)}
|
||||
onTrackGoToAlbum={handleAlbumClick}
|
||||
onTrackGoToArtist={handleArtistClick}
|
||||
/>
|
||||
{:else if activeView === 'library'}
|
||||
<LocalLibraryView
|
||||
onTrackPlay={handleLocalTrackPlay}
|
||||
onTrackPlayNext={handleLocalTrackPlayNext}
|
||||
onTrackPlayLater={handleLocalTrackPlayLater}
|
||||
onSetLocalQueue={handleSetLocalQueue}
|
||||
/>
|
||||
{:else if activeView === 'playlist' && selectedPlaylistId}
|
||||
@@ -1260,12 +1483,28 @@
|
||||
playlistId={selectedPlaylistId}
|
||||
onBack={goBack}
|
||||
onTrackPlay={handlePlaylistTrackPlay}
|
||||
onTrackPlayNext={handleDisplayTrackPlayNext}
|
||||
onTrackPlayLater={handleDisplayTrackPlayLater}
|
||||
onTrackAddFavorite={addTrackToFavorites}
|
||||
onTrackAddToPlaylist={(trackId) => openAddToPlaylist([trackId])}
|
||||
onTrackShareQobuz={shareQobuzTrackLink}
|
||||
onTrackShareSonglink={(track) => shareSonglinkTrack(track.id, track.isrc)}
|
||||
onTrackGoToAlbum={handleAlbumClick}
|
||||
onTrackGoToArtist={handleArtistClick}
|
||||
/>
|
||||
{:else if activeView === 'favorites'}
|
||||
<FavoritesView
|
||||
onAlbumClick={handleAlbumClick}
|
||||
onTrackPlay={handlePlaylistTrackPlay}
|
||||
onArtistClick={handleArtistClick}
|
||||
onTrackPlayNext={handleDisplayTrackPlayNext}
|
||||
onTrackPlayLater={handleDisplayTrackPlayLater}
|
||||
onTrackAddFavorite={addTrackToFavorites}
|
||||
onTrackAddToPlaylist={(trackId) => openAddToPlaylist([trackId])}
|
||||
onTrackShareQobuz={shareQobuzTrackLink}
|
||||
onTrackShareSonglink={(track) => shareSonglinkTrack(track.id, track.isrc)}
|
||||
onTrackGoToAlbum={handleAlbumClick}
|
||||
onTrackGoToArtist={handleArtistClick}
|
||||
/>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
Reference in new issue
Block a user