feat: add refresh button to collection views

This commit is contained in:
Markury
2026-03-18 11:08:23 -04:00
parent 2c471370e4
commit e5d12c9041
4 changed files with 142 additions and 9 deletions

View File

@@ -13,6 +13,9 @@
onTrackClick?: (index: number) => void; onTrackClick?: (index: number) => void;
onDownloadTrack?: (index: number) => void; onDownloadTrack?: (index: number) => void;
onDownloadPlaylist?: () => void; onDownloadPlaylist?: () => void;
onRefresh?: () => void;
refreshing?: boolean;
lastCached?: number | null;
downloadingTrackIds?: Set<string>; downloadingTrackIds?: Set<string>;
} }
@@ -27,9 +30,18 @@
onTrackClick, onTrackClick,
onDownloadTrack, onDownloadTrack,
onDownloadPlaylist, onDownloadPlaylist,
onRefresh,
refreshing = false,
lastCached = null,
downloadingTrackIds = new Set() downloadingTrackIds = new Set()
}: Props = $props(); }: Props = $props();
function formatTimestamp(timestamp: number | null): string {
if (!timestamp) return 'Never';
const date = new Date(timestamp * 1000);
return date.toLocaleString();
}
type ViewMode = 'tracks' | 'info'; type ViewMode = 'tracks' | 'info';
let viewMode = $state<ViewMode>('tracks'); let viewMode = $state<ViewMode>('tracks');
@@ -177,14 +189,27 @@
<span class="field-label">Tracks:</span> <span class="field-label">Tracks:</span>
<span>{tracks.length}</span> <span>{tracks.length}</span>
</div> </div>
{#if lastCached}
<div class="field-row">
<span class="field-label">Last updated:</span>
<span>{formatTimestamp(lastCached)}</span>
</div>
{/if}
</fieldset> </fieldset>
<fieldset style="margin-top: 16px;"> <fieldset style="margin-top: 16px;">
<legend>Actions</legend> <legend>Actions</legend>
<button onclick={onDownloadPlaylist}> <div class="actions-row">
Download Playlist <div>
</button> <button onclick={onDownloadPlaylist}>Download Playlist</button>
<p class="help-text">Download all tracks and save as m3u8 playlist</p> <p class="help-text">Download all tracks and save as m3u8 playlist</p>
</div>
{#if onRefresh}
<button onclick={onRefresh} disabled={refreshing}>
{refreshing ? 'Refreshing...' : 'Refresh Tracks'}
</button>
{/if}
</div>
</fieldset> </fieldset>
</div> </div>
{/if} {/if}
@@ -300,6 +325,13 @@
text-align: center; text-align: center;
} }
.actions-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
}
.download-btn { .download-btn {
padding: 2px 8px; padding: 2px 8px;
font-size: 11px; font-size: 11px;

View File

@@ -13,6 +13,9 @@
onTrackClick?: (index: number) => void; onTrackClick?: (index: number) => void;
onDownloadTrack?: (index: number) => void; onDownloadTrack?: (index: number) => void;
onDownloadPlaylist?: () => void; onDownloadPlaylist?: () => void;
onRefresh?: () => void;
refreshing?: boolean;
lastCached?: number | null;
downloadingTrackIds?: Set<string>; downloadingTrackIds?: Set<string>;
} }
@@ -26,9 +29,18 @@
onTrackClick, onTrackClick,
onDownloadTrack, onDownloadTrack,
onDownloadPlaylist, onDownloadPlaylist,
onRefresh,
refreshing = false,
lastCached = null,
downloadingTrackIds = new Set() downloadingTrackIds = new Set()
}: Props = $props(); }: Props = $props();
function formatTimestamp(timestamp: number | null): string {
if (!timestamp) return 'Never';
const date = new Date(timestamp * 1000);
return date.toLocaleString();
}
type ViewMode = 'tracks' | 'info'; type ViewMode = 'tracks' | 'info';
let viewMode = $state<ViewMode>('tracks'); let viewMode = $state<ViewMode>('tracks');
@@ -164,20 +176,40 @@
<span class="field-label">Tracks:</span> <span class="field-label">Tracks:</span>
<span>{tracks.length}</span> <span>{tracks.length}</span>
</div> </div>
{#if lastCached}
<div class="field-row">
<span class="field-label">Last updated:</span>
<span>{formatTimestamp(lastCached)}</span>
</div>
{/if}
</fieldset> </fieldset>
{#if $deezerAuth.loggedIn} {#if $deezerAuth.loggedIn}
<fieldset style="margin-top: 16px;"> <fieldset style="margin-top: 16px;">
<legend>Actions</legend> <legend>Actions</legend>
<button onclick={onDownloadPlaylist}> <div class="actions-row">
Download Playlist <div>
</button> <button onclick={onDownloadPlaylist}>Download Playlist</button>
<p class="help-text">Download all tracks via Deezer and save as m3u8 playlist</p> <p class="help-text">Download all tracks via Deezer and save as m3u8 playlist</p>
</div>
{#if onRefresh}
<button onclick={onRefresh} disabled={refreshing}>
{refreshing ? 'Refreshing...' : 'Refresh Tracks'}
</button>
{/if}
</div>
</fieldset> </fieldset>
{:else} {:else}
<fieldset style="margin-top: 16px;"> <fieldset style="margin-top: 16px;">
<legend>Downloads</legend> <legend>Downloads</legend>
<div class="actions-row">
<p class="warning-text">Deezer login required to download Spotify tracks</p> <p class="warning-text">Deezer login required to download Spotify tracks</p>
{#if onRefresh}
<button onclick={onRefresh} disabled={refreshing}>
{refreshing ? 'Refreshing...' : 'Refresh Tracks'}
</button>
{/if}
</div>
<p class="help-text">Sign in to Deezer in Services → Deezer to enable downloads</p> <p class="help-text">Sign in to Deezer in Services → Deezer to enable downloads</p>
</fieldset> </fieldset>
{/if} {/if}
@@ -306,6 +338,13 @@
color: #808080; color: #808080;
} }
.actions-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
}
.warning-text { .warning-text {
margin: 0 0 8px 0; margin: 0 0 8px 0;
font-size: 12px; font-size: 12px;

View File

@@ -352,6 +352,9 @@
onTrackClick={handleTrackClick} onTrackClick={handleTrackClick}
onDownloadTrack={handleDownloadTrack} onDownloadTrack={handleDownloadTrack}
onDownloadPlaylist={handleDownloadPlaylist} onDownloadPlaylist={handleDownloadPlaylist}
onRefresh={refreshPlaylistTracks}
{refreshing}
{lastCached}
{downloadingTrackIds} {downloadingTrackIds}
/> />
{/if} {/if}

View File

@@ -8,6 +8,7 @@
getCachedPlaylistTracks, getCachedPlaylistTracks,
getCachedTracks, getCachedTracks,
upsertPlaylistTracks, upsertPlaylistTracks,
upsertTracks,
type SpotifyPlaylist, type SpotifyPlaylist,
type SpotifyPlaylistTrack, type SpotifyPlaylistTrack,
type SpotifyTrack type SpotifyTrack
@@ -22,12 +23,14 @@
let playlistId = $derived($page.params.id!); let playlistId = $derived($page.params.id!);
let loading = $state(true); let loading = $state(true);
let refreshing = $state(false);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let playlist = $state<SpotifyPlaylist | null>(null); let playlist = $state<SpotifyPlaylist | null>(null);
let playlistTracks = $state<SpotifyPlaylistTrack[]>([]); let playlistTracks = $state<SpotifyPlaylistTrack[]>([]);
let selectedTrackIndex = $state<number | null>(null); let selectedTrackIndex = $state<number | null>(null);
let coverImageUrl = $state<string | undefined>(undefined); let coverImageUrl = $state<string | undefined>(undefined);
let downloadingTrackIds = $state(new Set<string>()); let downloadingTrackIds = $state(new Set<string>());
let lastCached = $state<number | null>(null);
// Convert Spotify tracks to Track type for CollectionView // Convert Spotify tracks to Track type for CollectionView
let tracks = $derived<Track[]>( let tracks = $derived<Track[]>(
@@ -96,6 +99,8 @@
cached_at: track.cached_at cached_at: track.cached_at
})); }));
lastCached = allTracks[0]?.cached_at || null;
// Set cover art from first track's album // Set cover art from first track's album
if (allTracks.length > 0) { if (allTracks.length > 0) {
if (allTracks[0].album_image_url) { if (allTracks[0].album_image_url) {
@@ -171,6 +176,7 @@
} else { } else {
playlist = cachedPlaylist; playlist = cachedPlaylist;
coverImageUrl = playlist.image_url; coverImageUrl = playlist.image_url;
lastCached = playlist.cached_at;
} }
// Load tracks // Load tracks
@@ -204,6 +210,56 @@
} }
} }
function ensureSpotifyAuth(): boolean {
if (!$spotifyAuth.accessToken || !$spotifyAuth.clientId || !$spotifyAuth.clientSecret || !$spotifyAuth.refreshToken) {
return false;
}
spotifyAPI.setClientCredentials($spotifyAuth.clientId, $spotifyAuth.clientSecret);
spotifyAPI.setTokens($spotifyAuth.accessToken, $spotifyAuth.refreshToken, $spotifyAuth.expiresAt!);
return true;
}
async function refreshPlaylist() {
if (refreshing) return;
if (!ensureSpotifyAuth()) {
setError('Not logged in to Spotify');
return;
}
refreshing = true;
try {
if (playlistId === 'spotify-likes') {
// Refresh liked tracks
const apiTracks = await spotifyAPI.getAllUserTracks();
await upsertTracks(apiTracks);
// Reload from cache
await loadSpotifyLikes();
} else {
// Refresh regular playlist tracks
const apiTracks = await spotifyAPI.getPlaylistTracks(playlistId);
await upsertPlaylistTracks(playlistId, apiTracks);
// Reload from cache
const cachedTracks = await getCachedPlaylistTracks(playlistId);
playlistTracks = cachedTracks;
if (playlist) {
playlist.track_count = cachedTracks.length;
}
}
lastCached = Math.floor(Date.now() / 1000);
console.log('[Spotify Playlist] Refresh complete!');
} catch (e) {
console.error('Error refreshing playlist:', e);
setError('Error refreshing playlist: ' + (e instanceof Error ? e.message : String(e)));
} finally {
refreshing = false;
}
}
function handleTrackClick(index: number) { function handleTrackClick(index: number) {
selectedTrackIndex = index; selectedTrackIndex = index;
} }
@@ -294,6 +350,9 @@
onTrackClick={handleTrackClick} onTrackClick={handleTrackClick}
onDownloadTrack={handleDownloadTrack} onDownloadTrack={handleDownloadTrack}
onDownloadPlaylist={handleDownloadPlaylist} onDownloadPlaylist={handleDownloadPlaylist}
onRefresh={refreshPlaylist}
{refreshing}
{lastCached}
/> />
</div> </div>
{/if} {/if}