mirror of
https://github.com/markuryy/shark.git
synced 2025-12-15 12:41:02 +00:00
feat(dz): add local caching and UI for user favorites
This commit is contained in:
@@ -1,44 +1,167 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { deezerAuth, loadDeezerAuth, saveArl, saveUser, clearDeezerAuth } from '$lib/stores/deezer';
|
||||
import { deezerAuth, loadDeezerAuth, saveArl, saveUser, clearDeezerAuth, saveCacheTimestamp } from '$lib/stores/deezer';
|
||||
import { deezerAPI } from '$lib/services/deezer';
|
||||
import { addDeezerTrackToQueue } from '$lib/services/deezer/addToQueue';
|
||||
import { settings } from '$lib/stores/settings';
|
||||
import {
|
||||
getCachedPlaylists,
|
||||
getCachedAlbums,
|
||||
getCachedArtists,
|
||||
getCachedTracks,
|
||||
upsertPlaylists,
|
||||
upsertAlbums,
|
||||
upsertArtists,
|
||||
upsertTracks,
|
||||
type DeezerPlaylist,
|
||||
type DeezerAlbum,
|
||||
type DeezerArtist,
|
||||
type DeezerTrack
|
||||
} from '$lib/library/deezer-database';
|
||||
|
||||
type ViewMode = 'playlists' | 'tracks' | 'artists' | 'albums' | 'user';
|
||||
|
||||
let viewMode = $state<ViewMode>('playlists');
|
||||
let playlists = $state<DeezerPlaylist[]>([]);
|
||||
let albums = $state<DeezerAlbum[]>([]);
|
||||
let artists = $state<DeezerArtist[]>([]);
|
||||
let tracks = $state<DeezerTrack[]>([]);
|
||||
let loading = $state(true);
|
||||
let syncing = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let selectedIndex = $state<number | null>(null);
|
||||
|
||||
// Login form state
|
||||
let arlInput = $state('');
|
||||
let isLoading = $state(false);
|
||||
let errorMessage = $state('');
|
||||
let successMessage = $state('');
|
||||
let testingAuth = $state(false);
|
||||
let authTestResult = $state<string | null>(null);
|
||||
let isLogging = $state(false);
|
||||
let loginError = $state('');
|
||||
let loginSuccess = $state('');
|
||||
|
||||
// Track add to queue test
|
||||
let trackIdInput = $state('3135556'); // Default: Daft Punk - One More Time
|
||||
let isFetchingTrack = $state(false);
|
||||
let isAddingToQueue = $state(false);
|
||||
let trackInfo = $state<any>(null);
|
||||
let queueStatus = $state('');
|
||||
let queueError = $state('');
|
||||
// User refresh state
|
||||
let refreshingUser = $state(false);
|
||||
let userRefreshMessage = $state('');
|
||||
|
||||
const CACHE_DURATION = 24 * 60 * 60; // 24 hours in seconds
|
||||
|
||||
onMount(async () => {
|
||||
await loadDeezerAuth();
|
||||
if ($deezerAuth.loggedIn) {
|
||||
await loadFavorites();
|
||||
} else {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadFavorites() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
// Check if we need to refresh cache
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const cacheAge = $deezerAuth.cacheTimestamp ? now - $deezerAuth.cacheTimestamp : Infinity;
|
||||
const needsRefresh = cacheAge > CACHE_DURATION;
|
||||
|
||||
if (needsRefresh) {
|
||||
await refreshFavorites();
|
||||
} else {
|
||||
// Load from cache
|
||||
const [cachedPlaylists, cachedAlbums, cachedArtists, cachedTracks] = await Promise.all([
|
||||
getCachedPlaylists(),
|
||||
getCachedAlbums(),
|
||||
getCachedArtists(),
|
||||
getCachedTracks()
|
||||
]);
|
||||
|
||||
playlists = cachedPlaylists;
|
||||
albums = cachedAlbums;
|
||||
artists = cachedArtists;
|
||||
tracks = cachedTracks;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error loading favorites: ' + (e instanceof Error ? e.message : String(e));
|
||||
// Switch to user tab to show error
|
||||
viewMode = 'user';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshFavorites() {
|
||||
if (!$deezerAuth.arl || syncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncing = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
deezerAPI.setArl($deezerAuth.arl);
|
||||
|
||||
// Fetch all favorites from API
|
||||
console.log('[Deezer] Fetching favorites from API...');
|
||||
const [apiPlaylists, apiAlbums, apiArtists, apiTracks] = await Promise.all([
|
||||
deezerAPI.getUserPlaylists(),
|
||||
deezerAPI.getUserAlbums(),
|
||||
deezerAPI.getUserArtists(),
|
||||
deezerAPI.getUserTracks()
|
||||
]);
|
||||
|
||||
console.log('[Deezer] Fetched from API:', {
|
||||
playlists: apiPlaylists.length,
|
||||
albums: apiAlbums.length,
|
||||
artists: apiArtists.length,
|
||||
tracks: apiTracks.length
|
||||
});
|
||||
|
||||
// Update database cache
|
||||
console.log('[Deezer] Updating database cache...');
|
||||
await upsertPlaylists(apiPlaylists);
|
||||
await upsertAlbums(apiAlbums);
|
||||
await upsertArtists(apiArtists);
|
||||
await upsertTracks(apiTracks);
|
||||
|
||||
// Update cache timestamp
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
await saveCacheTimestamp(now);
|
||||
|
||||
console.log('[Deezer] Reloading from cache...');
|
||||
// Reload from cache
|
||||
const [cachedPlaylists, cachedAlbums, cachedArtists, cachedTracks] = await Promise.all([
|
||||
getCachedPlaylists(),
|
||||
getCachedAlbums(),
|
||||
getCachedArtists(),
|
||||
getCachedTracks()
|
||||
]);
|
||||
|
||||
playlists = cachedPlaylists;
|
||||
albums = cachedAlbums;
|
||||
artists = cachedArtists;
|
||||
tracks = cachedTracks;
|
||||
|
||||
console.log('[Deezer] Refresh complete!');
|
||||
} catch (e) {
|
||||
console.error('Error refreshing favorites:', e);
|
||||
error = 'Error refreshing favorites: ' + (e instanceof Error ? e.message : String(e));
|
||||
// Switch to user tab to show error
|
||||
viewMode = 'user';
|
||||
} finally {
|
||||
syncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!arlInput || arlInput.trim().length === 0) {
|
||||
errorMessage = 'Please enter an ARL token';
|
||||
loginError = 'Please enter an ARL token';
|
||||
return;
|
||||
}
|
||||
|
||||
if (arlInput.trim().length !== 192) {
|
||||
errorMessage = 'ARL token should be 192 characters long';
|
||||
loginError = 'ARL token should be 192 characters long';
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
errorMessage = '';
|
||||
successMessage = '';
|
||||
isLogging = true;
|
||||
loginError = '';
|
||||
loginSuccess = '';
|
||||
|
||||
try {
|
||||
const result = await deezerAPI.loginViaArl(arlInput.trim());
|
||||
@@ -46,114 +169,76 @@
|
||||
if (result.success && result.user) {
|
||||
await saveArl(arlInput.trim());
|
||||
await saveUser(result.user);
|
||||
successMessage = `Successfully logged in as ${result.user.name}!`;
|
||||
loginSuccess = `Successfully logged in as ${result.user.name}!`;
|
||||
arlInput = '';
|
||||
|
||||
// Load favorites after login
|
||||
await loadFavorites();
|
||||
} else {
|
||||
errorMessage = result.error || 'Login failed. Please check your ARL token.';
|
||||
loginError = result.error || 'Login failed. Please check your ARL token.';
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage = `Login error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
loginError = `Login error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
} finally {
|
||||
isLoading = false;
|
||||
isLogging = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await clearDeezerAuth();
|
||||
successMessage = 'Logged out successfully';
|
||||
errorMessage = '';
|
||||
authTestResult = null;
|
||||
playlists = [];
|
||||
albums = [];
|
||||
artists = [];
|
||||
tracks = [];
|
||||
loginSuccess = '';
|
||||
loginError = '';
|
||||
}
|
||||
|
||||
async function testAuthentication() {
|
||||
if (!$deezerAuth.arl) {
|
||||
authTestResult = 'Not logged in';
|
||||
async function handleRefreshUser() {
|
||||
if (!$deezerAuth.arl || refreshingUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
testingAuth = true;
|
||||
authTestResult = null;
|
||||
refreshingUser = true;
|
||||
userRefreshMessage = '';
|
||||
|
||||
try {
|
||||
deezerAPI.setArl($deezerAuth.arl);
|
||||
const isValid = await deezerAPI.testAuth();
|
||||
authTestResult = isValid ? '✓ Authentication is working!' : '✗ Authentication failed';
|
||||
} catch (error) {
|
||||
authTestResult = `✗ Test failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
} finally {
|
||||
testingAuth = false;
|
||||
}
|
||||
}
|
||||
const result = await deezerAPI.loginViaArl($deezerAuth.arl);
|
||||
|
||||
async function fetchTrackInfo() {
|
||||
if (!$deezerAuth.arl || !$deezerAuth.user) {
|
||||
queueError = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
isFetchingTrack = true;
|
||||
queueError = '';
|
||||
trackInfo = null;
|
||||
|
||||
try {
|
||||
deezerAPI.setArl($deezerAuth.arl);
|
||||
const trackData = await deezerAPI.getTrack(trackIdInput);
|
||||
console.log('Track data:', trackData);
|
||||
|
||||
if (!trackData || !trackData.SNG_ID) {
|
||||
throw new Error('Track not found or invalid track ID');
|
||||
if (result.success && result.user) {
|
||||
await saveUser(result.user);
|
||||
userRefreshMessage = 'User info refreshed successfully!';
|
||||
} else {
|
||||
userRefreshMessage = 'Failed to refresh user info';
|
||||
}
|
||||
|
||||
trackInfo = trackData;
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
queueError = error instanceof Error ? error.message : 'Failed to fetch track';
|
||||
userRefreshMessage = 'Error refreshing user info: ' + (error instanceof Error ? error.message : 'Unknown error');
|
||||
} finally {
|
||||
isFetchingTrack = false;
|
||||
refreshingUser = false;
|
||||
setTimeout(() => {
|
||||
userRefreshMessage = '';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
async function addTrackToQueue() {
|
||||
if (!trackInfo) {
|
||||
queueError = 'Please fetch track info first';
|
||||
return;
|
||||
}
|
||||
function handleItemClick(index: number) {
|
||||
selectedIndex = index;
|
||||
}
|
||||
|
||||
if (!$settings.musicFolder) {
|
||||
queueError = 'Please set a music folder in Settings first';
|
||||
return;
|
||||
}
|
||||
|
||||
isAddingToQueue = true;
|
||||
queueStatus = '';
|
||||
queueError = '';
|
||||
|
||||
try {
|
||||
// Use shared utility to add track to queue
|
||||
await addDeezerTrackToQueue(trackIdInput);
|
||||
|
||||
queueStatus = '✓ Added to download queue!';
|
||||
|
||||
// Navigate to downloads page after brief delay
|
||||
setTimeout(() => {
|
||||
goto('/downloads');
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Queue error:', error);
|
||||
queueError = error instanceof Error ? error.message : 'Failed to add to queue';
|
||||
} finally {
|
||||
isAddingToQueue = false;
|
||||
}
|
||||
function formatDuration(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${String(secs).padStart(2, '0')}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="deezer-page">
|
||||
<h2>Deezer Authentication</h2>
|
||||
<div class="deezer-wrapper">
|
||||
<h2 style="padding: 8px">Deezer</h2>
|
||||
|
||||
{#if !$deezerAuth.loggedIn}
|
||||
<!-- Login Form -->
|
||||
<section class="window login-section">
|
||||
<section class="window login-section" style="max-width: 600px; margin: 8px;">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-text">Login to Deezer</div>
|
||||
</div>
|
||||
@@ -167,29 +252,28 @@
|
||||
type="password"
|
||||
bind:value={arlInput}
|
||||
placeholder="192 character ARL token"
|
||||
disabled={isLoading}
|
||||
disabled={isLogging}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if errorMessage}
|
||||
{#if loginError}
|
||||
<div class="error-message">
|
||||
⚠ {errorMessage}
|
||||
⚠ {loginError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if successMessage}
|
||||
{#if loginSuccess}
|
||||
<div class="success-message">
|
||||
✓ {successMessage}
|
||||
✓ {loginSuccess}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="button-row">
|
||||
<button onclick={handleLogin} disabled={isLoading}>
|
||||
{isLoading ? 'Logging in...' : 'Login'}
|
||||
<button onclick={handleLogin} disabled={isLogging}>
|
||||
{isLogging ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<details class="instructions">
|
||||
<summary>How to get your ARL token</summary>
|
||||
<div class="instructions-content">
|
||||
@@ -208,121 +292,233 @@
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
{:else if loading}
|
||||
<p style="padding: 8px;">Loading favorites...</p>
|
||||
{:else}
|
||||
<!-- Logged In View -->
|
||||
<section class="window user-section">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-text">User Info</div>
|
||||
</div>
|
||||
<div class="window-body">
|
||||
<div class="user-info">
|
||||
<div class="field-row">
|
||||
<span>Name:</span>
|
||||
<span>{$deezerAuth.user?.name || 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>User ID:</span>
|
||||
<span>{$deezerAuth.user?.id || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>Country:</span>
|
||||
<span>{$deezerAuth.user?.country || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>HQ Streaming:</span>
|
||||
<span>{$deezerAuth.user?.can_stream_hq ? '✓ Yes' : '✗ No'}</span>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>Lossless Streaming:</span>
|
||||
<span>{$deezerAuth.user?.can_stream_lossless ? '✓ Yes' : '✗ No'}</span>
|
||||
</div>
|
||||
<section class="favorites-content">
|
||||
<!-- Tabs -->
|
||||
<!--
|
||||
svelte-ignore a11y_no_noninteractive_element_to_interactive_role
|
||||
Reason: 98.css library requires <menu role="tablist"> for proper tab styling.
|
||||
-->
|
||||
<menu role="tablist">
|
||||
<li role="tab" aria-selected={viewMode === 'playlists'}>
|
||||
<button onclick={() => viewMode = 'playlists'}>Playlists</button>
|
||||
</li>
|
||||
<li role="tab" aria-selected={viewMode === 'tracks'}>
|
||||
<button onclick={() => viewMode = 'tracks'}>Tracks</button>
|
||||
</li>
|
||||
<li role="tab" aria-selected={viewMode === 'artists'}>
|
||||
<button onclick={() => viewMode = 'artists'}>Artists</button>
|
||||
</li>
|
||||
<li role="tab" aria-selected={viewMode === 'albums'}>
|
||||
<button onclick={() => viewMode = 'albums'}>Albums</button>
|
||||
</li>
|
||||
<li role="tab" aria-selected={viewMode === 'user'}>
|
||||
<button onclick={() => viewMode = 'user'}>User</button>
|
||||
</li>
|
||||
</menu>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="window tab-content" role="tabpanel">
|
||||
<div class="window-body">
|
||||
{#if syncing}
|
||||
<div class="sync-status">
|
||||
<p>Refreshing favorites from Deezer...</p>
|
||||
</div>
|
||||
{:else if viewMode === 'playlists'}
|
||||
<!-- Playlists View -->
|
||||
<div class="tab-header">
|
||||
<h4>Favorite Playlists</h4>
|
||||
<button onclick={refreshFavorites} disabled={syncing}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="sunken-panel table-container">
|
||||
<table class="interactive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Playlist</th>
|
||||
<th>Tracks</th>
|
||||
<th>Creator</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each playlists as playlist, i}
|
||||
<tr
|
||||
class:highlighted={selectedIndex === i}
|
||||
onclick={() => handleItemClick(i)}
|
||||
>
|
||||
<td>{playlist.title}</td>
|
||||
<td>{playlist.nb_tracks}</td>
|
||||
<td>{playlist.creator_name}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else if viewMode === 'tracks'}
|
||||
<!-- Tracks View -->
|
||||
<div class="tab-header">
|
||||
<h4>Favorite Tracks</h4>
|
||||
<button onclick={refreshFavorites} disabled={syncing}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="sunken-panel table-container">
|
||||
<table class="interactive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Artist</th>
|
||||
<th>Album</th>
|
||||
<th>Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each tracks as track, i}
|
||||
<tr
|
||||
class:highlighted={selectedIndex === i}
|
||||
onclick={() => handleItemClick(i)}
|
||||
>
|
||||
<td>{track.title}</td>
|
||||
<td>{track.artist_name}</td>
|
||||
<td>{track.album_title}</td>
|
||||
<td class="duration">{formatDuration(track.duration)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else if viewMode === 'artists'}
|
||||
<!-- Artists View -->
|
||||
<div class="tab-header">
|
||||
<h4>Favorite Artists</h4>
|
||||
<button onclick={refreshFavorites} disabled={syncing}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="sunken-panel table-container">
|
||||
<table class="interactive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artist</th>
|
||||
<th>Albums</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each artists as artist, i}
|
||||
<tr
|
||||
class:highlighted={selectedIndex === i}
|
||||
onclick={() => handleItemClick(i)}
|
||||
>
|
||||
<td>{artist.name}</td>
|
||||
<td>{artist.nb_album}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else if viewMode === 'albums'}
|
||||
<!-- Albums View -->
|
||||
<div class="tab-header">
|
||||
<h4>Favorite Albums</h4>
|
||||
<button onclick={refreshFavorites} disabled={syncing}>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="sunken-panel table-container">
|
||||
<table class="interactive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Album</th>
|
||||
<th>Artist</th>
|
||||
<th>Tracks</th>
|
||||
<th>Year</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each albums as album, i}
|
||||
<tr
|
||||
class:highlighted={selectedIndex === i}
|
||||
onclick={() => handleItemClick(i)}
|
||||
>
|
||||
<td>{album.title}</td>
|
||||
<td>{album.artist_name}</td>
|
||||
<td>{album.nb_tracks}</td>
|
||||
<td>{album.release_date ? new Date(album.release_date).getFullYear() : '—'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else if viewMode === 'user'}
|
||||
<!-- User Info View -->
|
||||
<div class="user-container">
|
||||
{#if error}
|
||||
<fieldset>
|
||||
<legend>Error</legend>
|
||||
<div class="error-message">
|
||||
{error}
|
||||
</div>
|
||||
</fieldset>
|
||||
{/if}
|
||||
|
||||
<fieldset>
|
||||
<legend>User Information</legend>
|
||||
<div class="field-row">
|
||||
<span class="field-label">Name:</span>
|
||||
<span>{$deezerAuth.user?.name || 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span class="field-label">Country:</span>
|
||||
<span>{$deezerAuth.user?.country || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span class="field-label">HQ Streaming:</span>
|
||||
<span>{$deezerAuth.user?.can_stream_hq ? '✓ Yes' : '✗ No'}</span>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span class="field-label">Lossless Streaming:</span>
|
||||
<span>{$deezerAuth.user?.can_stream_lossless ? '✓ Yes' : '✗ No'}</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{#if userRefreshMessage}
|
||||
<div class="message-box">
|
||||
{userRefreshMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<fieldset style="margin-top: 16px;">
|
||||
<legend>Actions</legend>
|
||||
<div class="button-row">
|
||||
<button onclick={handleRefreshUser} disabled={refreshingUser}>
|
||||
{refreshingUser ? 'Refreshing...' : 'Refresh User Info'}
|
||||
</button>
|
||||
<button onclick={handleLogout}>Logout</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if successMessage}
|
||||
<div class="success-message">
|
||||
✓ {successMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="button-row">
|
||||
<button onclick={handleLogout}>Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Add Track to Queue -->
|
||||
<section class="window test-section">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-text">Add Track to Download Queue</div>
|
||||
</div>
|
||||
<div class="window-body">
|
||||
<p>Add a track to the download queue:</p>
|
||||
|
||||
<div class="field-row-stacked">
|
||||
<label for="track-id">Track ID (from Deezer URL)</label>
|
||||
<input
|
||||
id="track-id"
|
||||
type="text"
|
||||
bind:value={trackIdInput}
|
||||
placeholder="e.g., 3135556"
|
||||
disabled={isFetchingTrack || isAddingToQueue}
|
||||
/>
|
||||
<small class="help-text">Default: 3135556 (Daft Punk - One More Time)</small>
|
||||
</div>
|
||||
|
||||
{#if trackInfo}
|
||||
<div class="track-info">
|
||||
<strong>{trackInfo.SNG_TITLE}</strong> by {trackInfo.ART_NAME}
|
||||
<br>
|
||||
<small>Album: {trackInfo.ALB_TITLE} • Duration: {Math.floor(trackInfo.DURATION / 60)}:{String(trackInfo.DURATION % 60).padStart(2, '0')}</small>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if queueStatus}
|
||||
<div class="success-message">
|
||||
{queueStatus}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if queueError}
|
||||
<div class="error-message">
|
||||
⚠ {queueError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="button-row">
|
||||
<button onclick={fetchTrackInfo} disabled={isFetchingTrack || isAddingToQueue}>
|
||||
{isFetchingTrack ? 'Fetching...' : 'Fetch Track Info'}
|
||||
</button>
|
||||
|
||||
<button onclick={addTrackToQueue} disabled={!trackInfo || isAddingToQueue || !$settings.musicFolder}>
|
||||
{isAddingToQueue ? 'Adding...' : 'Add to Queue'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if !$settings.musicFolder}
|
||||
<p class="help-text" style="margin-top: 8px;">
|
||||
⚠ Please set a music folder in Settings before adding to queue.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.deezer-page {
|
||||
max-width: 600px;
|
||||
.deezer-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-section,
|
||||
.user-section,
|
||||
.test-section {
|
||||
.login-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@@ -343,23 +539,16 @@
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.field-row span:first-child {
|
||||
.field-label {
|
||||
font-weight: bold;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
input[type="password"],
|
||||
input[type="text"] {
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
color: var(--text-color, #FFFFFF);
|
||||
opacity: 0.7;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
@@ -413,18 +602,76 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-bottom: 12px;
|
||||
.favorites-content {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.track-info {
|
||||
.tab-content {
|
||||
margin-top: -2px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tab-content .window-body {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--button-shadow, #808080);
|
||||
}
|
||||
|
||||
.tab-header h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.duration {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.user-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.message-box {
|
||||
padding: 8px;
|
||||
margin: 8px 0;
|
||||
background-color: var(--button-shadow, #2a2a2a);
|
||||
border: 1px solid var(--button-highlight, #606060);
|
||||
}
|
||||
|
||||
.track-info strong {
|
||||
font-weight: bold;
|
||||
.sync-status {
|
||||
padding: 16px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user