mirror of
https://github.com/markuryy/shark.git
synced 2026-06-18 18:41:03 +00:00
feat: add refresh button to collection views
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
<p class="warning-text">Deezer login required to download Spotify tracks</p>
|
<div class="actions-row">
|
||||||
|
<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;
|
||||||
|
|||||||
@@ -352,6 +352,9 @@
|
|||||||
onTrackClick={handleTrackClick}
|
onTrackClick={handleTrackClick}
|
||||||
onDownloadTrack={handleDownloadTrack}
|
onDownloadTrack={handleDownloadTrack}
|
||||||
onDownloadPlaylist={handleDownloadPlaylist}
|
onDownloadPlaylist={handleDownloadPlaylist}
|
||||||
|
onRefresh={refreshPlaylistTracks}
|
||||||
|
{refreshing}
|
||||||
|
{lastCached}
|
||||||
{downloadingTrackIds}
|
{downloadingTrackIds}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user