feat(labels): follow / unfollow labels (mirrors Follow Artist)
Adds the same follow flow we already have for artists, applied to labels. Confirmed against the inferred OpenAPI spec (qbz-nix-docs/qobuz-api-inferred-openapi-v9.7.0.3.yaml) — the existing /favorite/create, /favorite/delete and /favorite/getUserFavorites endpoints already accept fav_type='label' and label_ids, so no new Qobuz endpoints were needed. Backend - favorites_cache: new favorite_labels SQLite table + add / remove / sync / get methods alongside the artist counterparts. clear_all drops the new table on logout. - commands_v2::favorites: 4 new commands — v2_get_cached_favorite_labels, v2_sync_cached_favorite_labels, v2_cache_favorite_label, v2_uncache_favorite_label. Same wiring as the artist commands. - lib.rs invoke_handler registers the four new commands. Frontend - src/lib/stores/labelFavoritesStore.ts: clone of artistFavoritesStore — same in-memory Set, same isLoaded flag, toggle calls /favorite/create with fav_type=label, sync paginates /favorite/getUserFavorites?type=labels. - routes/+page.svelte: loadLabelFavorites() called alongside the other three after login is confirmed. - LabelView.svelte: heart-shape Follow button in the label header info column, mirroring ArtistDetailView's .artist-actions treatment. Reactive to the store via labelFavoritesVersion bump so any future Favorites view that toggles a label will keep this view in sync. - i18n: label.follow / label.unfollow in all five locales. cc
This commit is contained in:
11 files changed
+457
-5
No files matched your search
@@ -351,3 +351,79 @@ pub async fn v2_uncache_favorite_artist(
|
||||
.remove_favorite_artist(artistId)
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
|
||||
// ============ Label favorites cache (mirrors artist) ============
|
||||
|
||||
/// Get cached favorite labels (V2)
|
||||
#[tauri::command]
|
||||
pub async fn v2_get_cached_favorite_labels(
|
||||
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
|
||||
) -> Result<Vec<i64>, RuntimeError> {
|
||||
let guard = cache_state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
|
||||
store
|
||||
.get_favorite_label_ids()
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
|
||||
/// Sync cached favorite labels (V2) — replaces the entire cached set
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_sync_cached_favorite_labels(
|
||||
labelIds: Vec<i64>,
|
||||
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let guard = cache_state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
|
||||
store
|
||||
.sync_favorite_labels(&labelIds)
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
|
||||
/// Cache a single favorite label (V2)
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_cache_favorite_label(
|
||||
labelId: i64,
|
||||
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let guard = cache_state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
|
||||
store
|
||||
.add_favorite_label(labelId)
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
|
||||
/// Uncache a favorite label (V2)
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_uncache_favorite_label(
|
||||
labelId: i64,
|
||||
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let guard = cache_state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
|
||||
store
|
||||
.remove_favorite_label(labelId)
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
@@ -33,6 +33,12 @@ pub struct CachedFavoriteArtist {
|
||||
pub artist_id: i64,
|
||||
}
|
||||
|
||||
/// Represents a cached favorite label
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CachedFavoriteLabel {
|
||||
pub label_id: i64,
|
||||
}
|
||||
|
||||
pub struct FavoritesCacheStore {
|
||||
conn: Connection,
|
||||
}
|
||||
@@ -77,6 +83,18 @@ impl FavoritesCacheStore {
|
||||
)
|
||||
.map_err(|e| format!("Failed to create favorite_artists table: {}", e))?;
|
||||
|
||||
// Labels — added as part of the Follow Label feature; same shape as
|
||||
// favorite_artists. CREATE IF NOT EXISTS is the migration story
|
||||
// for existing databases (no separate ALTER needed).
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS favorite_labels (
|
||||
label_id INTEGER PRIMARY KEY,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| format!("Failed to create favorite_labels table: {}", e))?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
@@ -296,6 +314,74 @@ impl FavoritesCacheStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Label favorites ============
|
||||
|
||||
pub fn get_favorite_label_ids(&self) -> Result<Vec<i64>, String> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT label_id FROM favorite_labels")
|
||||
.map_err(|e| format!("Failed to prepare query: {}", e))?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map([], |row| row.get(0))
|
||||
.map_err(|e| format!("Failed to query favorite labels: {}", e))?;
|
||||
|
||||
let mut ids = Vec::new();
|
||||
for row in rows {
|
||||
ids.push(row.map_err(|e| format!("Failed to read row: {}", e))?);
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
pub fn is_label_favorite(&self, label_id: i64) -> Result<bool, String> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT 1 FROM favorite_labels WHERE label_id = ?1")
|
||||
.map_err(|e| format!("Failed to prepare query: {}", e))?;
|
||||
|
||||
let exists = stmt
|
||||
.exists(params![label_id])
|
||||
.map_err(|e| format!("Failed to check favorite: {}", e))?;
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
pub fn add_favorite_label(&self, label_id: i64) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO favorite_labels (label_id) VALUES (?1)",
|
||||
params![label_id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to add favorite label: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_favorite_label(&self, label_id: i64) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"DELETE FROM favorite_labels WHERE label_id = ?1",
|
||||
params![label_id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to remove favorite label: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sync_favorite_labels(&self, label_ids: &[i64]) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute("DELETE FROM favorite_labels", [])
|
||||
.map_err(|e| format!("Failed to clear favorite labels: {}", e))?;
|
||||
|
||||
for &label_id in label_ids {
|
||||
self.conn
|
||||
.execute(
|
||||
"INSERT INTO favorite_labels (label_id) VALUES (?1)",
|
||||
params![label_id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert favorite label: {}", e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Clear all (for logout) ============
|
||||
|
||||
pub fn clear_all(&self) -> Result<(), String> {
|
||||
@@ -308,6 +394,9 @@ impl FavoritesCacheStore {
|
||||
self.conn
|
||||
.execute("DELETE FROM favorite_artists", [])
|
||||
.map_err(|e| format!("Failed to clear favorite artists: {}", e))?;
|
||||
self.conn
|
||||
.execute("DELETE FROM favorite_labels", [])
|
||||
.map_err(|e| format!("Failed to clear favorite labels: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1688,6 +1688,10 @@ pub fn run() {
|
||||
commands_v2::v2_sync_cached_favorite_artists,
|
||||
commands_v2::v2_cache_favorite_artist,
|
||||
commands_v2::v2_uncache_favorite_artist,
|
||||
commands_v2::v2_get_cached_favorite_labels,
|
||||
commands_v2::v2_sync_cached_favorite_labels,
|
||||
commands_v2::v2_cache_favorite_label,
|
||||
commands_v2::v2_uncache_favorite_label,
|
||||
commands_v2::v2_share_track_songlink,
|
||||
commands_v2::v2_share_album_songlink,
|
||||
commands_v2::v2_library_backfill_downloads,
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
isTrackToggling,
|
||||
toggleTrackFavorite
|
||||
} from '$lib/stores/favoritesStore';
|
||||
import {
|
||||
subscribe as subscribeLabelFavorites,
|
||||
isLabelFavorite,
|
||||
isLabelToggling,
|
||||
toggleLabelFavorite
|
||||
} from '$lib/stores/labelFavoritesStore';
|
||||
import type { QobuzAlbum, LabelPageData, LabelExploreItem, DisplayTrack } from '$lib/types';
|
||||
|
||||
interface Track {
|
||||
@@ -668,6 +674,23 @@
|
||||
return albumOfflineCacheStatuses.get(albumId) || false;
|
||||
}
|
||||
|
||||
// Label follow state — driven entirely by the labelFavoritesStore so any
|
||||
// toggle elsewhere (e.g. a future favorites view) updates this view too.
|
||||
let labelFavoritesVersion = $state(0);
|
||||
let unsubLabelFavorites: (() => void) | null = null;
|
||||
const labelIsFavorite = $derived.by(() => {
|
||||
void labelFavoritesVersion;
|
||||
return isLabelFavorite(labelId);
|
||||
});
|
||||
const labelIsToggling = $derived.by(() => {
|
||||
void labelFavoritesVersion;
|
||||
return isLabelToggling(labelId);
|
||||
});
|
||||
|
||||
async function handleToggleLabelFavorite() {
|
||||
await toggleLabelFavorite(labelId);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadLabelPage();
|
||||
loadLabelAlbumsAndDescription();
|
||||
@@ -675,10 +698,14 @@
|
||||
unsubFavorites = subscribeFavorites(() => {
|
||||
trackFavoritesVersion++;
|
||||
});
|
||||
unsubLabelFavorites = subscribeLabelFavorites(() => {
|
||||
labelFavoritesVersion++;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubFavorites?.();
|
||||
unsubLabelFavorites?.();
|
||||
jumpObserver?.disconnect();
|
||||
});
|
||||
|
||||
@@ -809,6 +836,24 @@
|
||||
{descriptionExpanded ? $t('label.readLess') : $t('label.readMore')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Follow Label — same heart/circle treatment as ArtistDetailView -->
|
||||
<div class="label-actions">
|
||||
<button
|
||||
class="favorite-btn"
|
||||
class:is-favorite={labelIsFavorite}
|
||||
onclick={handleToggleLabelFavorite}
|
||||
disabled={labelIsToggling}
|
||||
title={labelIsFavorite ? $t('label.unfollow') : $t('label.follow')}
|
||||
aria-label={labelIsFavorite ? $t('label.unfollow') : $t('label.follow')}
|
||||
>
|
||||
{#if labelIsFavorite}
|
||||
<Heart size={24} fill="var(--accent-primary)" color="var(--accent-primary)" />
|
||||
{:else}
|
||||
<Heart size={24} />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1294,6 +1339,42 @@
|
||||
|
||||
/* Header */
|
||||
.label-header { display: flex; gap: 24px; margin-bottom: 40px; }
|
||||
|
||||
/* Follow button — visual parity with ArtistDetailView .artist-actions */
|
||||
.label-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.favorite-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: var(--bg-tertiary);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
transition: color 150ms ease, background-color 150ms ease, border-color 150ms ease, opacity 150ms ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.favorite-btn:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.favorite-btn.is-favorite {
|
||||
background: rgba(var(--accent-primary-rgb, 139, 92, 246), 0.15);
|
||||
}
|
||||
|
||||
.favorite-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.label-image-wrapper {
|
||||
width: 180px; height: 180px; border-radius: 50%;
|
||||
overflow: hidden; flex-shrink: 0; background: var(--bg-tertiary);
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"readLess": "WENIGER",
|
||||
"seeAll": "Alle anzeigen",
|
||||
"showMore": "Mehr anzeigen",
|
||||
"showLess": "Weniger anzeigen"
|
||||
"showLess": "Weniger anzeigen",
|
||||
"follow": "Label folgen",
|
||||
"unfollow": "Nicht mehr folgen"
|
||||
},
|
||||
"labelReleases": {
|
||||
"albumCount": "{count} Alben",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"readLess": "READ LESS",
|
||||
"seeAll": "See all",
|
||||
"showMore": "Show more",
|
||||
"showLess": "Show less"
|
||||
"showLess": "Show less",
|
||||
"follow": "Follow label",
|
||||
"unfollow": "Unfollow label"
|
||||
},
|
||||
"labelReleases": {
|
||||
"albumCount": "{count} albums",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"readLess": "LEER MENOS",
|
||||
"seeAll": "Ver todo",
|
||||
"showMore": "Ver más",
|
||||
"showLess": "Ver menos"
|
||||
"showLess": "Ver menos",
|
||||
"follow": "Seguir sello",
|
||||
"unfollow": "Dejar de seguir"
|
||||
},
|
||||
"labelReleases": {
|
||||
"albumCount": "{count} albums",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"readLess": "Lire moins",
|
||||
"seeAll": "Voir tout",
|
||||
"showMore": "Voir plus",
|
||||
"showLess": "Voir moins"
|
||||
"showLess": "Voir moins",
|
||||
"follow": "Suivre le label",
|
||||
"unfollow": "Ne plus suivre"
|
||||
},
|
||||
"labelReleases": {
|
||||
"albumCount": "{count} albums",
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
"readLess": "LEIA MENOS",
|
||||
"seeAll": "Ver tudo",
|
||||
"showMore": "Mostrar mais",
|
||||
"showLess": "Mostrar menos"
|
||||
"showLess": "Mostrar menos",
|
||||
"follow": "Seguir selo",
|
||||
"unfollow": "Deixar de seguir"
|
||||
},
|
||||
"labelReleases": {
|
||||
"albumCount": "{count} álbuns",
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Label Favorites Store
|
||||
*
|
||||
* Centralized store for tracking favorite (followed) labels. Mirrors
|
||||
* artistFavoritesStore one-for-one — same SQLite-backed cache, same
|
||||
* sync strategy, same toggle flow. Added as part of the Follow Label
|
||||
* feature; the underlying Qobuz endpoints (/favorite/create,
|
||||
* /favorite/delete, /favorite/getUserFavorites) already accept
|
||||
* `label_ids` / `fav_type=labels` per
|
||||
* qbz-nix-docs/qobuz-api-inferred-openapi-v9.7.0.3.yaml.
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { skipIfRemote } from '$lib/services/commandRouter';
|
||||
|
||||
let favoriteLabelIds = new Set<number>();
|
||||
let togglingLabelIds = new Set<number>();
|
||||
let isLoaded = false;
|
||||
let isLoading = false;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function notifyListeners(): void {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
export function isLabelFavorite(labelId: number): boolean {
|
||||
return favoriteLabelIds.has(labelId);
|
||||
}
|
||||
|
||||
export function isLabelToggling(labelId: number): boolean {
|
||||
return togglingLabelIds.has(labelId);
|
||||
}
|
||||
|
||||
export function getFavoriteLabelIds(): number[] {
|
||||
return Array.from(favoriteLabelIds);
|
||||
}
|
||||
|
||||
export function isFavoritesLoaded(): boolean {
|
||||
return isLoaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load label favorites from local cache first, then sync from API.
|
||||
*/
|
||||
export async function loadLabelFavorites(): Promise<void> {
|
||||
if (skipIfRemote()) return;
|
||||
if (isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
try {
|
||||
try {
|
||||
const cachedIds = await invoke<number[]>('v2_get_cached_favorite_labels');
|
||||
if (cachedIds.length > 0) {
|
||||
favoriteLabelIds = new Set(cachedIds);
|
||||
isLoaded = true;
|
||||
notifyListeners();
|
||||
console.log(`[LabelFavorites] Loaded ${cachedIds.length} labels from local cache`);
|
||||
}
|
||||
} catch (cacheErr) {
|
||||
console.debug('[LabelFavorites] No local cache available:', cacheErr);
|
||||
}
|
||||
|
||||
await syncFromApi();
|
||||
} catch (err) {
|
||||
console.error('[LabelFavorites] Failed to load favorites:', err);
|
||||
if (!isLoaded) {
|
||||
favoriteLabelIds = new Set();
|
||||
isLoaded = true;
|
||||
notifyListeners();
|
||||
}
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync label favorites from Qobuz API to local cache.
|
||||
*/
|
||||
export async function syncFromApi(): Promise<void> {
|
||||
if (skipIfRemote()) return;
|
||||
try {
|
||||
const allLabelIds: number[] = [];
|
||||
let offset = 0;
|
||||
const limit = 500;
|
||||
|
||||
while (true) {
|
||||
const result = await invoke<{ labels?: { items: Array<{ id: number }>; total?: number } }>('v2_get_favorites', {
|
||||
favType: 'labels',
|
||||
limit,
|
||||
offset
|
||||
});
|
||||
|
||||
const items = result.labels?.items ?? [];
|
||||
if (!items.length) break;
|
||||
|
||||
allLabelIds.push(...items.map(item => item.id));
|
||||
offset += items.length;
|
||||
|
||||
if (items.length < limit) break;
|
||||
if (result.labels?.total && offset >= result.labels.total) break;
|
||||
}
|
||||
|
||||
await invoke('v2_sync_cached_favorite_labels', { labelIds: allLabelIds });
|
||||
|
||||
favoriteLabelIds = new Set(allLabelIds);
|
||||
isLoaded = true;
|
||||
notifyListeners();
|
||||
|
||||
console.log(`[LabelFavorites] Synced ${allLabelIds.length} labels from API`);
|
||||
} catch (err) {
|
||||
console.error('[LabelFavorites] Failed to sync from API:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle label favorite status: API call first → on success → update
|
||||
* local cache → notify UI.
|
||||
*/
|
||||
export async function toggleLabelFavorite(labelId: number): Promise<boolean> {
|
||||
if (skipIfRemote()) return favoriteLabelIds.has(labelId);
|
||||
if (togglingLabelIds.has(labelId)) {
|
||||
return favoriteLabelIds.has(labelId);
|
||||
}
|
||||
|
||||
const wasFavorite = favoriteLabelIds.has(labelId);
|
||||
const newState = !wasFavorite;
|
||||
|
||||
togglingLabelIds.add(labelId);
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (newState) {
|
||||
await invoke('v2_add_favorite', { favType: 'label', itemId: String(labelId) });
|
||||
await invoke('v2_cache_favorite_label', { labelId });
|
||||
favoriteLabelIds.add(labelId);
|
||||
} else {
|
||||
await invoke('v2_remove_favorite', { favType: 'label', itemId: String(labelId) });
|
||||
await invoke('v2_uncache_favorite_label', { labelId });
|
||||
favoriteLabelIds.delete(labelId);
|
||||
}
|
||||
return newState;
|
||||
} catch (err) {
|
||||
console.error('[LabelFavorites] Failed to toggle favorite:', err);
|
||||
return wasFavorite;
|
||||
} finally {
|
||||
togglingLabelIds.delete(labelId);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync local cache from an array of label IDs (e.g. after fetching the
|
||||
* full favorites page elsewhere).
|
||||
*/
|
||||
export async function syncCache(labelIds: number[]): Promise<void> {
|
||||
if (skipIfRemote()) return;
|
||||
try {
|
||||
await invoke('v2_sync_cached_favorite_labels', { labelIds });
|
||||
favoriteLabelIds = new Set(labelIds);
|
||||
notifyListeners();
|
||||
} catch (err) {
|
||||
console.error('[LabelFavorites] Failed to sync cache:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export function markAsFavorite(labelId: number): void {
|
||||
if (skipIfRemote()) return;
|
||||
if (!favoriteLabelIds.has(labelId)) {
|
||||
favoriteLabelIds.add(labelId);
|
||||
invoke('v2_cache_favorite_label', { labelId }).catch(() => {});
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
export function unmarkAsFavorite(labelId: number): void {
|
||||
if (skipIfRemote()) return;
|
||||
if (favoriteLabelIds.has(labelId)) {
|
||||
favoriteLabelIds.delete(labelId);
|
||||
invoke('v2_uncache_favorite_label', { labelId }).catch(() => {});
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,7 @@
|
||||
import { loadFavorites } from '$lib/stores/favoritesStore';
|
||||
import { loadAlbumFavorites } from '$lib/stores/albumFavoritesStore';
|
||||
import { loadArtistFavorites } from '$lib/stores/artistFavoritesStore';
|
||||
import { loadLabelFavorites } from '$lib/stores/labelFavoritesStore';
|
||||
import { getDefaultFavoritesTab } from '$lib/utils/favorites';
|
||||
import { platform } from '$lib/utils/platform';
|
||||
import type { FavoritesPreferences, ResolvedMusician } from '$lib/types';
|
||||
@@ -3664,6 +3665,7 @@
|
||||
loadFavorites(); // Track favorites
|
||||
loadAlbumFavorites(); // Album favorites
|
||||
loadArtistFavorites(); // Artist favorites
|
||||
loadLabelFavorites(); // Label favorites
|
||||
|
||||
// Refresh offline status now that we're logged in
|
||||
refreshOfflineStatus().catch(err => console.debug('[Offline] Status refresh deferred:', err));
|
||||
|
||||
Reference in new issue
Block a user