feat(download): add download button to playlist view and album download button
- Add download functionality to PlaylistDetailView (track-by-track) - Add handleDisplayTrackDownload handler for playlist tracks - Add "Download Album" button to AlbumDetailView header actions - handleDownloadAlbum queues all non-downloaded tracks for download
This commit is contained in:
1 parent
97951b2189
commit
6de37c296b
3 files changed
+83
-3
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { ArrowLeft, Play, Shuffle, Heart, Plus, MoreHorizontal } from 'lucide-svelte';
|
import { ArrowLeft, Play, Shuffle, Heart, Plus, MoreHorizontal, Download } from 'lucide-svelte';
|
||||||
import TrackRow from '../TrackRow.svelte';
|
import TrackRow from '../TrackRow.svelte';
|
||||||
import { getDownloadState, type DownloadStatus } from '$lib/stores/downloadState';
|
import { getDownloadState, type DownloadStatus } from '$lib/stores/downloadState';
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
onTrackDownload?: (track: Track) => void;
|
onTrackDownload?: (track: Track) => void;
|
||||||
onTrackRemoveDownload?: (trackId: number) => void;
|
onTrackRemoveDownload?: (trackId: number) => void;
|
||||||
getTrackDownloadStatus?: (trackId: number) => { status: DownloadStatus; progress: number };
|
getTrackDownloadStatus?: (trackId: number) => { status: DownloadStatus; progress: number };
|
||||||
|
onDownloadAlbum?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -71,7 +72,8 @@
|
|||||||
onAddTrackToPlaylist,
|
onAddTrackToPlaylist,
|
||||||
onTrackDownload,
|
onTrackDownload,
|
||||||
onTrackRemoveDownload,
|
onTrackRemoveDownload,
|
||||||
getTrackDownloadStatus
|
getTrackDownloadStatus,
|
||||||
|
onDownloadAlbum
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let currentTrack = $state<number | null>(null);
|
let currentTrack = $state<number | null>(null);
|
||||||
@@ -186,6 +188,11 @@
|
|||||||
<button class="icon-btn" onclick={onAddToQueue}>
|
<button class="icon-btn" onclick={onAddToQueue}>
|
||||||
<Plus size={20} color="white" />
|
<Plus size={20} color="white" />
|
||||||
</button>
|
</button>
|
||||||
|
{#if onDownloadAlbum}
|
||||||
|
<button class="icon-btn" onclick={onDownloadAlbum} title="Download album">
|
||||||
|
<Download size={20} color="white" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
<button class="icon-btn">
|
<button class="icon-btn">
|
||||||
<MoreHorizontal size={20} color="white" />
|
<MoreHorizontal size={20} color="white" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import TrackRow from '../TrackRow.svelte';
|
import TrackRow from '../TrackRow.svelte';
|
||||||
|
import { type DownloadStatus } from '$lib/stores/downloadState';
|
||||||
|
|
||||||
interface PlaylistTrack {
|
interface PlaylistTrack {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -71,6 +72,9 @@
|
|||||||
onTrackShareSonglink?: (track: DisplayTrack) => void;
|
onTrackShareSonglink?: (track: DisplayTrack) => void;
|
||||||
onTrackGoToAlbum?: (albumId: string) => void;
|
onTrackGoToAlbum?: (albumId: string) => void;
|
||||||
onTrackGoToArtist?: (artistId: number) => void;
|
onTrackGoToArtist?: (artistId: number) => void;
|
||||||
|
onTrackDownload?: (track: DisplayTrack) => void;
|
||||||
|
onTrackRemoveDownload?: (trackId: number) => void;
|
||||||
|
getTrackDownloadStatus?: (trackId: number) => { status: DownloadStatus; progress: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -84,7 +88,10 @@
|
|||||||
onTrackShareQobuz,
|
onTrackShareQobuz,
|
||||||
onTrackShareSonglink,
|
onTrackShareSonglink,
|
||||||
onTrackGoToAlbum,
|
onTrackGoToAlbum,
|
||||||
onTrackGoToArtist
|
onTrackGoToArtist,
|
||||||
|
onTrackDownload,
|
||||||
|
onTrackRemoveDownload,
|
||||||
|
getTrackDownloadStatus
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let playlist = $state<Playlist | null>(null);
|
let playlist = $state<Playlist | null>(null);
|
||||||
@@ -494,6 +501,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each displayTracks as track, idx (track.id)}
|
{#each displayTracks as track, idx (track.id)}
|
||||||
|
{@const downloadInfo = getTrackDownloadStatus?.(track.id) ?? { status: 'none' as const, progress: 0 }}
|
||||||
<TrackRow
|
<TrackRow
|
||||||
number={idx + 1}
|
number={idx + 1}
|
||||||
title={track.title}
|
title={track.title}
|
||||||
@@ -501,7 +509,11 @@
|
|||||||
duration={track.duration}
|
duration={track.duration}
|
||||||
quality={track.hires ? 'Hi-Res' : undefined}
|
quality={track.hires ? 'Hi-Res' : undefined}
|
||||||
isFavorite={favoriteTrackIds.has(track.id)}
|
isFavorite={favoriteTrackIds.has(track.id)}
|
||||||
|
downloadStatus={downloadInfo.status}
|
||||||
|
downloadProgress={downloadInfo.progress}
|
||||||
onPlay={() => handleTrackClick(track)}
|
onPlay={() => handleTrackClick(track)}
|
||||||
|
onDownload={onTrackDownload ? () => onTrackDownload(track) : undefined}
|
||||||
|
onRemoveDownload={onTrackRemoveDownload ? () => onTrackRemoveDownload(track.id) : undefined}
|
||||||
menuActions={{
|
menuActions={{
|
||||||
onPlayNow: () => handleTrackClick(track),
|
onPlayNow: () => handleTrackClick(track),
|
||||||
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(track) : undefined,
|
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(track) : undefined,
|
||||||
|
|||||||
@@ -1004,12 +1004,69 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDownloadAlbum() {
|
||||||
|
if (!selectedAlbum) return;
|
||||||
|
|
||||||
|
const tracksToDownload = selectedAlbum.tracks.filter(track => {
|
||||||
|
const status = getDownloadState(track.id).status;
|
||||||
|
return status === 'none' || status === 'failed';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tracksToDownload.length === 0) {
|
||||||
|
showToast('All tracks already downloaded', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`Downloading ${tracksToDownload.length} tracks from "${selectedAlbum.title}"`, 'info');
|
||||||
|
|
||||||
|
for (const track of tracksToDownload) {
|
||||||
|
try {
|
||||||
|
await downloadTrack({
|
||||||
|
id: track.id,
|
||||||
|
title: track.title,
|
||||||
|
artist: track.artist || selectedAlbum.artist || 'Unknown',
|
||||||
|
album: selectedAlbum.title,
|
||||||
|
albumId: selectedAlbum.id,
|
||||||
|
durationSecs: track.durationSeconds,
|
||||||
|
quality: track.quality || 'CD Quality',
|
||||||
|
bitDepth: track.bitDepth,
|
||||||
|
sampleRate: track.samplingRate,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to queue download for "${track.title}":`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getTrackDownloadStatus(trackId: number) {
|
function getTrackDownloadStatus(trackId: number) {
|
||||||
// Access downloadStateVersion to trigger reactivity
|
// Access downloadStateVersion to trigger reactivity
|
||||||
void downloadStateVersion;
|
void downloadStateVersion;
|
||||||
return getDownloadState(trackId);
|
return getDownloadState(trackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDisplayTrackDownload(track: PlaylistTrack) {
|
||||||
|
try {
|
||||||
|
const quality = track.hires && track.bitDepth && track.samplingRate
|
||||||
|
? `${track.bitDepth}bit/${track.samplingRate}kHz`
|
||||||
|
: 'CD Quality';
|
||||||
|
await downloadTrack({
|
||||||
|
id: track.id,
|
||||||
|
title: track.title,
|
||||||
|
artist: track.artist || 'Unknown',
|
||||||
|
album: track.album,
|
||||||
|
albumId: track.albumId,
|
||||||
|
durationSecs: track.durationSeconds,
|
||||||
|
quality,
|
||||||
|
bitDepth: track.bitDepth,
|
||||||
|
sampleRate: track.samplingRate,
|
||||||
|
});
|
||||||
|
showToast(`Downloading "${track.title}"`, 'info');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to start download:', err);
|
||||||
|
showToast('Failed to start download', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleDisplayTrackPlayNext(track: PlaylistTrack) {
|
function handleDisplayTrackPlayNext(track: PlaylistTrack) {
|
||||||
queueTrackNext(buildQueueTrackFromDisplayTrack(track));
|
queueTrackNext(buildQueueTrackFromDisplayTrack(track));
|
||||||
}
|
}
|
||||||
@@ -1532,6 +1589,7 @@
|
|||||||
onTrackDownload={handleTrackDownload}
|
onTrackDownload={handleTrackDownload}
|
||||||
onTrackRemoveDownload={handleTrackRemoveDownload}
|
onTrackRemoveDownload={handleTrackRemoveDownload}
|
||||||
getTrackDownloadStatus={getTrackDownloadStatus}
|
getTrackDownloadStatus={getTrackDownloadStatus}
|
||||||
|
onDownloadAlbum={handleDownloadAlbum}
|
||||||
/>
|
/>
|
||||||
{:else if activeView === 'artist' && selectedArtist}
|
{:else if activeView === 'artist' && selectedArtist}
|
||||||
<ArtistDetailView
|
<ArtistDetailView
|
||||||
@@ -1568,6 +1626,9 @@
|
|||||||
onTrackShareSonglink={(track) => shareSonglinkTrack(track.id, track.isrc)}
|
onTrackShareSonglink={(track) => shareSonglinkTrack(track.id, track.isrc)}
|
||||||
onTrackGoToAlbum={handleAlbumClick}
|
onTrackGoToAlbum={handleAlbumClick}
|
||||||
onTrackGoToArtist={handleArtistClick}
|
onTrackGoToArtist={handleArtistClick}
|
||||||
|
onTrackDownload={handleDisplayTrackDownload}
|
||||||
|
onTrackRemoveDownload={handleTrackRemoveDownload}
|
||||||
|
getTrackDownloadStatus={getTrackDownloadStatus}
|
||||||
/>
|
/>
|
||||||
{:else if activeView === 'favorites'}
|
{:else if activeView === 'favorites'}
|
||||||
<FavoritesView
|
<FavoritesView
|
||||||
|
|||||||
Reference in new issue
Block a user