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:
vicrodh committed 2026-01-09 20:02:59 -06:00
1 parent 97951b2189
commit 6de37c296b
3 files changed
+83 -3

No files matched your search

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
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 { getDownloadState, type DownloadStatus } from '$lib/stores/downloadState';
@@ -51,6 +51,7 @@
onTrackDownload?: (track: Track) => void;
onTrackRemoveDownload?: (trackId: number) => void;
getTrackDownloadStatus?: (trackId: number) => { status: DownloadStatus; progress: number };
onDownloadAlbum?: () => void;
}
let {
@@ -71,7 +72,8 @@
onAddTrackToPlaylist,
onTrackDownload,
onTrackRemoveDownload,
getTrackDownloadStatus
getTrackDownloadStatus,
onDownloadAlbum
}: Props = $props();
let currentTrack = $state<number | null>(null);
@@ -186,6 +188,11 @@
<button class="icon-btn" onclick={onAddToQueue}>
<Plus size={20} color="white" />
</button>
{#if onDownloadAlbum}
<button class="icon-btn" onclick={onDownloadAlbum} title="Download album">
<Download size={20} color="white" />
</button>
{/if}
<button class="icon-btn">
<MoreHorizontal size={20} color="white" />
</button>
@@ -4,6 +4,7 @@
import { open } from '@tauri-apps/plugin-dialog';
import { onMount } from 'svelte';
import TrackRow from '../TrackRow.svelte';
import { type DownloadStatus } from '$lib/stores/downloadState';
interface PlaylistTrack {
id: number;
@@ -71,6 +72,9 @@
onTrackShareSonglink?: (track: DisplayTrack) => void;
onTrackGoToAlbum?: (albumId: string) => void;
onTrackGoToArtist?: (artistId: number) => void;
onTrackDownload?: (track: DisplayTrack) => void;
onTrackRemoveDownload?: (trackId: number) => void;
getTrackDownloadStatus?: (trackId: number) => { status: DownloadStatus; progress: number };
}
let {
@@ -84,7 +88,10 @@
onTrackShareQobuz,
onTrackShareSonglink,
onTrackGoToAlbum,
onTrackGoToArtist
onTrackGoToArtist,
onTrackDownload,
onTrackRemoveDownload,
getTrackDownloadStatus
}: Props = $props();
let playlist = $state<Playlist | null>(null);
@@ -494,6 +501,7 @@
</div>
{#each displayTracks as track, idx (track.id)}
{@const downloadInfo = getTrackDownloadStatus?.(track.id) ?? { status: 'none' as const, progress: 0 }}
<TrackRow
number={idx + 1}
title={track.title}
@@ -501,7 +509,11 @@
duration={track.duration}
quality={track.hires ? 'Hi-Res' : undefined}
isFavorite={favoriteTrackIds.has(track.id)}
downloadStatus={downloadInfo.status}
downloadProgress={downloadInfo.progress}
onPlay={() => handleTrackClick(track)}
onDownload={onTrackDownload ? () => onTrackDownload(track) : undefined}
onRemoveDownload={onTrackRemoveDownload ? () => onTrackRemoveDownload(track.id) : undefined}
menuActions={{
onPlayNow: () => handleTrackClick(track),
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(track) : undefined,
+61
View File
@@ -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) {
// Access downloadStateVersion to trigger reactivity
void downloadStateVersion;
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) {
queueTrackNext(buildQueueTrackFromDisplayTrack(track));
}
@@ -1532,6 +1589,7 @@
onTrackDownload={handleTrackDownload}
onTrackRemoveDownload={handleTrackRemoveDownload}
getTrackDownloadStatus={getTrackDownloadStatus}
onDownloadAlbum={handleDownloadAlbum}
/>
{:else if activeView === 'artist' && selectedArtist}
<ArtistDetailView
@@ -1568,6 +1626,9 @@
onTrackShareSonglink={(track) => shareSonglinkTrack(track.id, track.isrc)}
onTrackGoToAlbum={handleAlbumClick}
onTrackGoToArtist={handleArtistClick}
onTrackDownload={handleDisplayTrackDownload}
onTrackRemoveDownload={handleTrackRemoveDownload}
getTrackDownloadStatus={getTrackDownloadStatus}
/>
{:else if activeView === 'favorites'}
<FavoritesView