feat: add quality/format selector for individual track downloads
When downloading a single track from the Purchases tab, if multiple formats are available a popup lets the user choose before the folder picker opens. Single-format tracks proceed directly as before.
This commit is contained in:
5 files changed
+118
-3
No files matched your search
@@ -16,7 +16,7 @@
|
||||
getHideDownloaded, setHideDownloaded,
|
||||
} from '$lib/stores/purchasesStore';
|
||||
import { getUserItem, setUserItem } from '$lib/utils/userStorage';
|
||||
import type { PurchasedAlbum, PurchasedTrack } from '$lib/types/purchases';
|
||||
import type { PurchasedAlbum, PurchasedTrack, PurchaseFormatOption } from '$lib/types/purchases';
|
||||
import type { DisplayTrack } from '$lib/types';
|
||||
|
||||
type PurchasesTab = 'albums' | 'tracks';
|
||||
@@ -96,6 +96,12 @@
|
||||
let trackGroupMode = $state<TrackGroupMode>('artist');
|
||||
let showTrackGroupMenu = $state(false);
|
||||
|
||||
// Track format picker popup
|
||||
let formatPickerTrack = $state<PurchasedTrack | null>(null);
|
||||
let formatPickerFormats = $state<PurchaseFormatOption[]>([]);
|
||||
let formatPickerLoading = $state(false);
|
||||
let formatPickerAnchor = $state<{ top: number; right: number } | null>(null);
|
||||
|
||||
// Persist filter changes
|
||||
$effect(() => { setHideUnavailable(filterHideUnavailable); });
|
||||
$effect(() => { setUserItem('qbz-purchases-quality-filter', filterQuality); });
|
||||
@@ -107,6 +113,7 @@
|
||||
showAlbumSortMenu = false;
|
||||
showFilterPanel = false;
|
||||
showTrackGroupMenu = false;
|
||||
closeFormatPicker();
|
||||
}
|
||||
|
||||
const albumSortOptions = [
|
||||
@@ -394,6 +401,13 @@
|
||||
return $allTrackStatuses[trackId] || null;
|
||||
}
|
||||
|
||||
function closeFormatPicker() {
|
||||
formatPickerTrack = null;
|
||||
formatPickerFormats = [];
|
||||
formatPickerLoading = false;
|
||||
formatPickerAnchor = null;
|
||||
}
|
||||
|
||||
async function handleTrackDownload(event: MouseEvent, track: PurchasedTrack) {
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -409,6 +423,28 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Single format: proceed directly
|
||||
if (formats.length === 1) {
|
||||
await executeTrackDownload(track, formats[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple formats: show picker popup
|
||||
const btn = event.currentTarget as HTMLElement;
|
||||
const rect = btn.getBoundingClientRect();
|
||||
formatPickerTrack = track;
|
||||
formatPickerFormats = formats;
|
||||
formatPickerAnchor = { top: rect.bottom + 4, right: window.innerWidth - rect.right };
|
||||
} catch (err) {
|
||||
console.error('Track download error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function executeTrackDownload(track: PurchasedTrack, format: PurchaseFormatOption) {
|
||||
closeFormatPicker();
|
||||
if (!track.album?.id) return;
|
||||
|
||||
try {
|
||||
const { open } = await import('@tauri-apps/plugin-dialog');
|
||||
const { audioDir } = await import('@tauri-apps/api/path');
|
||||
const defaultPath = await audioDir();
|
||||
@@ -421,8 +457,8 @@
|
||||
|
||||
if (!dest || typeof dest !== 'string') return;
|
||||
|
||||
const qualityDir = formats[0].label.replace(/\//g, '-').trim();
|
||||
startTrackDownload(track.album.id, track.id, formats[0].id, dest, qualityDir);
|
||||
const qualityDir = format.label.replace(/\//g, '-').trim();
|
||||
startTrackDownload(track.album.id, track.id, format.id, dest, qualityDir);
|
||||
} catch (err) {
|
||||
console.error('Track download error:', err);
|
||||
}
|
||||
@@ -941,6 +977,28 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Format picker popup for track downloads -->
|
||||
{#if formatPickerTrack && formatPickerAnchor}
|
||||
<div class="format-picker-backdrop" onclick={closeFormatPicker} role="presentation"></div>
|
||||
<div
|
||||
class="format-picker"
|
||||
style="top: {formatPickerAnchor.top}px; right: {formatPickerAnchor.right}px;"
|
||||
>
|
||||
<div class="format-picker-header">{$t('purchases.selectFormat')}</div>
|
||||
{#each formatPickerFormats as fmt (fmt.id)}
|
||||
<button
|
||||
class="format-picker-item"
|
||||
onclick={() => formatPickerTrack && executeTrackDownload(formatPickerTrack, fmt)}
|
||||
>
|
||||
<span class="format-label">{fmt.label}</span>
|
||||
{#if fmt.bit_depth && fmt.sampling_rate}
|
||||
<span class="format-detail">{fmt.bit_depth}/{fmt.sampling_rate}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -1783,4 +1841,57 @@
|
||||
.track-row.downloaded {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Format picker popup */
|
||||
.format-picker-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 3000;
|
||||
}
|
||||
|
||||
.format-picker {
|
||||
position: fixed;
|
||||
z-index: 3001;
|
||||
min-width: 180px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.format-picker-header {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
padding: 8px 12px 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.format-picker-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
transition: background 100ms ease;
|
||||
}
|
||||
|
||||
.format-picker-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.format-detail {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
@@ -1554,6 +1554,7 @@
|
||||
"downloadAll": "Alles herunterladen",
|
||||
"downloadTrack": "Titel herunterladen",
|
||||
"chooseFolder": "Download-Ordner wählen",
|
||||
"selectFormat": "Format auswählen",
|
||||
"format": "Format",
|
||||
"purchasedOn": "Gekauft:",
|
||||
"addToLibrary": "Zur lokalen Bibliothek hinzufügen",
|
||||
|
||||
@@ -1705,6 +1705,7 @@
|
||||
"downloadAll": "Download All",
|
||||
"downloadTrack": "Download track",
|
||||
"chooseFolder": "Choose download folder",
|
||||
"selectFormat": "Select format",
|
||||
"format": "Format",
|
||||
"purchasedOn": "Purchased:",
|
||||
"addToLibrary": "Add to Local Library",
|
||||
|
||||
@@ -1705,6 +1705,7 @@
|
||||
"downloadAll": "Descargar Todo",
|
||||
"downloadTrack": "Descargar pista",
|
||||
"chooseFolder": "Elegir carpeta de descarga",
|
||||
"selectFormat": "Seleccionar formato",
|
||||
"format": "Formato",
|
||||
"purchasedOn": "Comprado:",
|
||||
"addToLibrary": "Agregar a Biblioteca Local",
|
||||
|
||||
@@ -1554,6 +1554,7 @@
|
||||
"downloadAll": "Tout télécharger",
|
||||
"downloadTrack": "Télécharger la piste",
|
||||
"chooseFolder": "Choisir le dossier de téléchargement",
|
||||
"selectFormat": "Sélectionner le format",
|
||||
"format": "Format",
|
||||
"purchasedOn": "Acheté :",
|
||||
"addToLibrary": "Ajouter à la bibliothèque locale",
|
||||
|
||||
Reference in new issue
Block a user