feat(discover): add Release Watch section to Home and For You
The Qobuz mobile client (v9.7) surfaces a 'Release Watch' /
'Radar de Novedades' feed in its Discover screens — new releases
from the artists, labels and award series the user follows. We
didn't have parity for it. This adds it as a module in both
Discover tabs.
Backend:
- qbz-qobuz: new ALBUMS_RELEASE_WATCH endpoint constant
(/albums/releaseWatch, plural as in mobile) and signed_get_auth
client method returning a SearchResultsPage<Album>. Signing
method is 'albumsreleaseWatch' per the Qobuz secret protocol.
- qbz-core: passthrough get_release_watch on QbzCore.
- src-tauri: core_bridge wrapper, new v2_get_release_watch Tauri
command registered in lib.rs. Filters through the artist
blacklist like other album endpoints.
Frontend:
- HomeView: new renderable section 'releaseWatch' in the home tab
(default visible, placed right after New Releases). Parallel
fetch alongside the existing discover_index call; mapped via
the existing toAlbumCard helper.
- ForYouTab: section appears ahead of 'Essentials' since followed
artists/labels are typically more personal.
- homeSettingsStore: new HomeSectionId 'releaseWatch' in the type
union and DEFAULT_SECTIONS so the user can toggle/reorder it
from home customization.
- i18n: discover.releaseWatch.title + subtitle added to all five
locales, using Qobuz's own translations where they exist
('Radar de Novedades' for Spanish, 'Release-Radar' for German)
with the 'followed artists/labels/awards' subtitle copy adapted
from the mobile app's strings.xml resources.
cc
This commit is contained in:
14 files changed
+307
-10
No files matched your search
@@ -760,6 +760,22 @@ impl<A: FrontendAdapter + Send + Sync + 'static> QbzCore<A> {
|
||||
.map_err(CoreError::Api)
|
||||
}
|
||||
|
||||
/// Get Release Watch — new releases from followed artists, labels and
|
||||
/// awards (the "Radar de Novedades" feed in the Qobuz mobile client).
|
||||
pub async fn get_release_watch(
|
||||
&self,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<SearchResultsPage<Album>, CoreError> {
|
||||
let client = self.client.read().await;
|
||||
let client = client.as_ref().ok_or(CoreError::NotInitialized)?;
|
||||
|
||||
client
|
||||
.get_release_watch(limit, offset)
|
||||
.await
|
||||
.map_err(CoreError::Api)
|
||||
}
|
||||
|
||||
/// Get artist page (full artist details with albums, tracks, similar)
|
||||
pub async fn get_artist_page(
|
||||
&self,
|
||||
|
||||
@@ -767,6 +767,47 @@ impl QobuzClient {
|
||||
Ok(serde_json::from_value(albums.clone())?)
|
||||
}
|
||||
|
||||
/// Get Release Watch — new and upcoming releases from artists, labels
|
||||
/// and awards the user follows. Exposed on mobile/web as "Release Watch"
|
||||
/// or "Radar de Novedades".
|
||||
///
|
||||
/// Returns the same `{items, offset, limit, total}` page structure as
|
||||
/// `get_featured_albums`. Requires the user to be authenticated; the
|
||||
/// endpoint uses the follows on the caller's account.
|
||||
pub async fn get_release_watch(
|
||||
&self,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<SearchResultsPage<Album>> {
|
||||
let url = endpoints::build_url(paths::ALBUMS_RELEASE_WATCH);
|
||||
let params: Vec<(&str, String)> = vec![
|
||||
("limit", limit.to_string()),
|
||||
("offset", offset.to_string()),
|
||||
];
|
||||
|
||||
let http_response = self
|
||||
.signed_get_auth(&url, "albumsreleaseWatch", ¶ms)
|
||||
.await?;
|
||||
log::debug!(
|
||||
"[API] get_release_watch(limit={}, offset={}) status={}",
|
||||
limit,
|
||||
offset,
|
||||
http_response.status()
|
||||
);
|
||||
let response: Value = http_response.json().await?;
|
||||
|
||||
// The mobile client calls into /albums/releaseWatch and expects an
|
||||
// `albums` key wrapping the page. If the server changes this someday
|
||||
// (e.g. to `releaseWatch`), fall back transparently.
|
||||
let page = response
|
||||
.get("albums")
|
||||
.or_else(|| response.get("releaseWatch"))
|
||||
.cloned()
|
||||
.unwrap_or(response);
|
||||
|
||||
Ok(serde_json::from_value(page)?)
|
||||
}
|
||||
|
||||
/// Get list of genres
|
||||
pub async fn get_genres(&self, parent_id: Option<u64>) -> Result<Vec<GenreInfo>> {
|
||||
let url = endpoints::build_url(paths::GENRE_LIST);
|
||||
|
||||
@@ -20,6 +20,10 @@ pub mod paths {
|
||||
pub const ALBUM_GET: &str = "/album/get";
|
||||
pub const ALBUM_SEARCH: &str = "/album/search";
|
||||
pub const ALBUM_GET_FEATURED: &str = "/album/getFeatured";
|
||||
// Release Watch: releases from artists, labels, and awards the user
|
||||
// follows. Mobile/web apps expose this in Discover as "Release Watch" /
|
||||
// "Radar de Novedades". Endpoint path uses the `/albums` (plural) prefix.
|
||||
pub const ALBUMS_RELEASE_WATCH: &str = "/albums/releaseWatch";
|
||||
|
||||
// Artist
|
||||
pub const ARTIST_GET: &str = "/artist/get";
|
||||
|
||||
@@ -258,6 +258,50 @@ pub async fn v2_get_featured_albums(
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Get Release Watch (V2 — releases from followed artists, labels and awards)
|
||||
///
|
||||
/// Surfaces the mobile client's "Radar de Novedades" / "Release Watch" feed.
|
||||
/// Filtered through the user's artist blacklist like other album results.
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_get_release_watch(
|
||||
limit: Option<u32>,
|
||||
offset: Option<u32>,
|
||||
bridge: State<'_, CoreBridgeState>,
|
||||
blacklist_state: State<'_, BlacklistState>,
|
||||
runtime: State<'_, RuntimeManagerState>,
|
||||
) -> Result<SearchResultsPage<Album>, RuntimeError> {
|
||||
runtime
|
||||
.manager()
|
||||
.check_requirements(CommandRequirement::RequiresCoreBridgeAuth)
|
||||
.await?;
|
||||
|
||||
let limit = limit.unwrap_or(20);
|
||||
let offset = offset.unwrap_or(0);
|
||||
log::info!("[V2] get_release_watch: limit={}, offset={}", limit, offset);
|
||||
|
||||
let bridge = bridge.get().await;
|
||||
let mut results = bridge
|
||||
.get_release_watch(limit, offset)
|
||||
.await
|
||||
.map_err(RuntimeError::Internal)?;
|
||||
|
||||
let original_count = results.items.len();
|
||||
results
|
||||
.items
|
||||
.retain(|album| !blacklist_state.is_blacklisted(album.artist.id));
|
||||
let filtered_count = original_count - results.items.len();
|
||||
if filtered_count > 0 {
|
||||
log::debug!(
|
||||
"[V2/Blacklist] Filtered {} albums from release watch",
|
||||
filtered_count
|
||||
);
|
||||
results.total = results.total.saturating_sub(filtered_count as u32);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Get artist page (V2 - uses QbzCore)
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
|
||||
@@ -498,6 +498,20 @@ impl CoreBridge {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Get Release Watch — releases from artists, labels, and awards the user
|
||||
/// follows. Surfaces the Qobuz "Radar de Novedades" / "Release Watch"
|
||||
/// feed used in the official mobile client's Discover screens.
|
||||
pub async fn get_release_watch(
|
||||
&self,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Result<SearchResultsPage<Album>, String> {
|
||||
self.core
|
||||
.get_release_watch(limit, offset)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Get artist page (full artist details with albums, tracks, similar)
|
||||
pub async fn get_artist_page(
|
||||
&self,
|
||||
|
||||
@@ -1594,6 +1594,7 @@ pub fn run() {
|
||||
commands_v2::v2_get_playlist_tags,
|
||||
commands_v2::v2_get_discover_albums,
|
||||
commands_v2::v2_get_featured_albums,
|
||||
commands_v2::v2_get_release_watch,
|
||||
commands_v2::v2_get_artist_page,
|
||||
commands_v2::v2_get_similar_artists,
|
||||
commands_v2::v2_get_artist_with_albums,
|
||||
|
||||
@@ -210,6 +210,11 @@
|
||||
let essentialsAlbums = $state<AlbumCardData[]>([]);
|
||||
let loadingEssentials = $state(false);
|
||||
|
||||
// Release Watch — the Qobuz mobile "Radar de Novedades" feed: new releases
|
||||
// from artists, labels, and awards the user follows.
|
||||
let releaseWatchAlbums = $state<AlbumCardData[]>([]);
|
||||
let loadingReleaseWatch = $state(false);
|
||||
|
||||
// Radio Stations: mix of 3 recent + 3 favorites + 3 top-artist albums, no dupes
|
||||
let radioAlbums = $state<AlbumCardData[]>([]);
|
||||
let radioBuilt = false;
|
||||
@@ -273,9 +278,38 @@
|
||||
loadSimilarAlbums();
|
||||
loadForgottenFavorites();
|
||||
loadEssentials();
|
||||
loadReleaseWatch();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadReleaseWatch() {
|
||||
loadingReleaseWatch = true;
|
||||
try {
|
||||
const result = await invoke<{ items: AlbumSuggestResult[]; total: number }>(
|
||||
'v2_get_release_watch',
|
||||
{ limit: 20, offset: 0 }
|
||||
);
|
||||
releaseWatchAlbums = (result.items || []).map(album => ({
|
||||
id: album.id,
|
||||
artwork: album.image?.large || album.image?.small || '',
|
||||
title: album.title,
|
||||
artist: album.artist?.name || '',
|
||||
artistId: album.artist?.id,
|
||||
genre: album.genre?.name || '',
|
||||
quality: formatQuality(
|
||||
album.hires,
|
||||
album.maximum_bit_depth,
|
||||
album.maximum_sampling_rate
|
||||
),
|
||||
releaseDate: album.release_date_original
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Failed to load release watch:', err);
|
||||
} finally {
|
||||
loadingReleaseWatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadArtistsToFollow() {
|
||||
if (topArtists.length === 0) return;
|
||||
loadingSuggestedArtists = true;
|
||||
@@ -1192,6 +1226,54 @@
|
||||
</HorizontalScrollRow>
|
||||
{/if}
|
||||
|
||||
<!-- Release Watch — followed artists/labels/awards, Qobuz mobile parity -->
|
||||
{#if loadingReleaseWatch}
|
||||
<div class="skeleton-section">
|
||||
<div class="skeleton-title"></div>
|
||||
<div class="skeleton-row">
|
||||
{#each { length: 6 } as _}<div class="skeleton-card"></div>{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if releaseWatchAlbums.length > 0}
|
||||
<HorizontalScrollRow>
|
||||
{#snippet header()}
|
||||
<div class="section-header-col">
|
||||
<h2 class="section-title">{$t('discover.releaseWatch.title')}</h2>
|
||||
<p class="section-subtitle">{$t('discover.releaseWatch.subtitle')}</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
{#each releaseWatchAlbums as album}
|
||||
<AlbumCard
|
||||
albumId={album.id}
|
||||
artwork={album.artwork}
|
||||
title={album.title}
|
||||
artist={album.artist}
|
||||
artistId={album.artistId}
|
||||
onArtistClick={onArtistClick}
|
||||
genre={album.genre}
|
||||
releaseDate={album.releaseDate}
|
||||
size="large"
|
||||
quality={album.quality}
|
||||
onPlay={onAlbumPlay ? () => onAlbumPlay(album.id) : undefined}
|
||||
onPlayNext={onAlbumPlayNext ? () => onAlbumPlayNext(album.id) : undefined}
|
||||
onPlayLater={onAlbumPlayLater ? () => onAlbumPlayLater(album.id) : undefined}
|
||||
onAddAlbumToPlaylist={onAddAlbumToPlaylist ? () => onAddAlbumToPlaylist(album.id) : undefined}
|
||||
onShareQobuz={onAlbumShareQobuz ? () => onAlbumShareQobuz(album.id) : undefined}
|
||||
onShareSonglink={onAlbumShareSonglink ? () => onAlbumShareSonglink(album.id) : undefined}
|
||||
onDownload={onAlbumDownload ? () => onAlbumDownload(album.id) : undefined}
|
||||
isAlbumFullyDownloaded={isAlbumDownloaded(album.id)}
|
||||
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
|
||||
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
|
||||
{downloadStateVersion}
|
||||
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }}
|
||||
/>
|
||||
{/each}
|
||||
<div class="spacer"></div>
|
||||
{/snippet}
|
||||
</HorizontalScrollRow>
|
||||
{/if}
|
||||
|
||||
<!-- Essentials [Genre] -->
|
||||
{#if loadingEssentials}
|
||||
<div class="skeleton-section">
|
||||
|
||||
@@ -269,13 +269,14 @@
|
||||
let loadingFavoriteAlbums = $state(true);
|
||||
let loadingQobuzPlaylists = $state(true);
|
||||
let loadingEssentialDiscography = $state(true);
|
||||
let loadingReleaseWatch = $state(true);
|
||||
|
||||
// True when all sections have finished loading (for empty state detection)
|
||||
const anyLoading = $derived(
|
||||
loadingNewReleases || loadingPressAwards || loadingMostStreamed ||
|
||||
loadingQobuzissimes || loadingEditorPicks || loadingRecentAlbums ||
|
||||
loadingContinueTracks || loadingTopArtists || loadingFavoriteAlbums ||
|
||||
loadingQobuzPlaylists || loadingEssentialDiscography
|
||||
loadingQobuzPlaylists || loadingEssentialDiscography || loadingReleaseWatch
|
||||
);
|
||||
|
||||
// Featured albums (from Qobuz editorial)
|
||||
@@ -285,6 +286,10 @@
|
||||
let qobuzissimes = $state<AlbumCardData[]>([]);
|
||||
let editorPicks = $state<AlbumCardData[]>([]);
|
||||
|
||||
// Release Watch — followed-artists/labels/awards feed from the mobile
|
||||
// client (Qobuz "Radar de Novedades").
|
||||
let releaseWatchAlbums = $state<AlbumCardData[]>([]);
|
||||
|
||||
// User-specific content
|
||||
let recentAlbums = $state<AlbumCardData[]>([]);
|
||||
let continueTracks = $state<DisplayTrack[]>([]);
|
||||
@@ -359,6 +364,7 @@
|
||||
|| favoriteAlbums.length > 0
|
||||
|| qobuzPlaylists.length > 0
|
||||
|| essentialDiscography.length > 0
|
||||
|| releaseWatchAlbums.length > 0
|
||||
);
|
||||
|
||||
|
||||
@@ -832,6 +838,7 @@
|
||||
loadingFavoriteAlbums = true;
|
||||
loadingQobuzPlaylists = true;
|
||||
loadingEssentialDiscography = true;
|
||||
loadingReleaseWatch = true;
|
||||
}
|
||||
|
||||
// Get current genre filter (array of IDs for multi-select)
|
||||
@@ -840,7 +847,9 @@
|
||||
// Two parallel paths:
|
||||
// 1. Single Qobuz discover API call (all editorial content)
|
||||
// 2. ML seeds from local SQLite -> user-specific sections
|
||||
// Plus release-watch which is its own endpoint.
|
||||
const discoverPromise = fetchAllDiscoverData(genreIds);
|
||||
const releaseWatchPromise = fetchReleaseWatch();
|
||||
|
||||
// Single IPC call returns fully-resolved card data (3-tier cache in Rust)
|
||||
const mlPromise = invoke<HomeResolved>('v2_reco_get_home_resolved', {
|
||||
@@ -878,17 +887,32 @@
|
||||
loadingFavoriteAlbums = false;
|
||||
}
|
||||
|
||||
// Ensure discover promise completes (errors already handled internally)
|
||||
await discoverPromise;
|
||||
// Ensure discover + release watch promises complete (errors already
|
||||
// handled internally)
|
||||
await Promise.all([discoverPromise, releaseWatchPromise]);
|
||||
|
||||
// Single batch download status check for ALL albums at once
|
||||
const allAlbums = [
|
||||
...newReleases, ...pressAwards, ...mostStreamed,
|
||||
...qobuzissimes, ...editorPicks,
|
||||
...recentAlbums, ...favoriteAlbums
|
||||
...recentAlbums, ...favoriteAlbums, ...releaseWatchAlbums
|
||||
];
|
||||
loadAllAlbumDownloadStatusesBatch(allAlbums).catch(() => {});
|
||||
}
|
||||
|
||||
async function fetchReleaseWatch() {
|
||||
try {
|
||||
const result = await invoke<{ items: QobuzAlbum[]; total: number }>(
|
||||
'v2_get_release_watch',
|
||||
{ limit: 20, offset: 0 }
|
||||
);
|
||||
releaseWatchAlbums = (result.items || []).map(toAlbumCard);
|
||||
} catch (err) {
|
||||
console.error('fetchReleaseWatch failed:', err);
|
||||
} finally {
|
||||
loadingReleaseWatch = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="home-view" bind:this={homeViewEl} onscroll={handleHomeScroll}>
|
||||
@@ -1307,6 +1331,55 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if sectionId === 'releaseWatch'}
|
||||
{#if loadingReleaseWatch}
|
||||
<div class="skeleton-section">
|
||||
<div class="skeleton-title"></div>
|
||||
<div class="skeleton-row">
|
||||
{#each { length: 6 } as _}<div class="skeleton-card"></div>{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if releaseWatchAlbums.length > 0}
|
||||
<HorizontalScrollRow>
|
||||
{#snippet header()}
|
||||
<div class="section-header-col">
|
||||
<h2 class="section-title">{$t('discover.releaseWatch.title')}</h2>
|
||||
<p class="section-subtitle">{$t('discover.releaseWatch.subtitle')}</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
{#each releaseWatchAlbums as album (album.id)}
|
||||
<AlbumCard
|
||||
albumId={album.id}
|
||||
artwork={album.artwork}
|
||||
title={album.title}
|
||||
artist={album.artist}
|
||||
artistId={album.artistId}
|
||||
onArtistClick={onArtistClick}
|
||||
genre={album.genre}
|
||||
releaseDate={album.releaseDate}
|
||||
size="large"
|
||||
quality={album.quality}
|
||||
onPlay={onAlbumPlay ? () => onAlbumPlay(album.id) : undefined}
|
||||
onPlayNext={onAlbumPlayNext ? () => onAlbumPlayNext(album.id) : undefined}
|
||||
onPlayLater={onAlbumPlayLater ? () => onAlbumPlayLater(album.id) : undefined}
|
||||
onAddAlbumToPlaylist={onAddAlbumToPlaylist ? () => onAddAlbumToPlaylist(album.id) : undefined}
|
||||
onShareQobuz={onAlbumShareQobuz ? () => onAlbumShareQobuz(album.id) : undefined}
|
||||
onShareSonglink={onAlbumShareSonglink ? () => onAlbumShareSonglink(album.id) : undefined}
|
||||
onDownload={onAlbumDownload ? () => onAlbumDownload(album.id) : undefined}
|
||||
isAlbumFullyDownloaded={isAlbumDownloaded(album.id)}
|
||||
onOpenContainingFolder={onOpenAlbumFolder ? () => onOpenAlbumFolder(album.id) : undefined}
|
||||
onReDownloadAlbum={onReDownloadAlbum ? () => onReDownloadAlbum(album.id) : undefined}
|
||||
{downloadStateVersion}
|
||||
onclick={() => { onAlbumClick?.(album.id); loadAlbumDownloadStatus(album.id); }}
|
||||
/>
|
||||
{/each}
|
||||
<div class="spacer"></div>
|
||||
{/snippet}
|
||||
</HorizontalScrollRow>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if sectionId === 'essentialDiscography'}
|
||||
{#if loadingEssentialDiscography}
|
||||
<div class="skeleton-section">
|
||||
|
||||
@@ -91,7 +91,11 @@
|
||||
"pressAccolades": "Presseauszeichnungen",
|
||||
"qobuzPlaylists": "Qobuz-Playlists",
|
||||
"searchPlaylistsPlaceholder": "Playlists suchen...",
|
||||
"noPlaylistResults": "Keine Playlists gefunden"
|
||||
"noPlaylistResults": "Keine Playlists gefunden",
|
||||
"releaseWatch": {
|
||||
"title": "Release-Radar",
|
||||
"subtitle": "Die neuesten Releases der Künstler, Labels und Auszeichnungen, denen Sie folgen."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "Suchen",
|
||||
|
||||
@@ -91,7 +91,11 @@
|
||||
"pressAccolades": "Press Accolades",
|
||||
"qobuzPlaylists": "Qobuz Playlists",
|
||||
"searchPlaylistsPlaceholder": "Search playlists...",
|
||||
"noPlaylistResults": "No playlists found"
|
||||
"noPlaylistResults": "No playlists found",
|
||||
"releaseWatch": {
|
||||
"title": "Release Watch",
|
||||
"subtitle": "Releases from artists, labels and awards you follow."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "Search",
|
||||
|
||||
@@ -91,7 +91,11 @@
|
||||
"pressAccolades": "Reconocimientos de Prensa",
|
||||
"qobuzPlaylists": "Playlists de Qobuz",
|
||||
"searchPlaylistsPlaceholder": "Buscar playlists...",
|
||||
"noPlaylistResults": "No se encontraron playlists"
|
||||
"noPlaylistResults": "No se encontraron playlists",
|
||||
"releaseWatch": {
|
||||
"title": "Radar de Novedades",
|
||||
"subtitle": "Novedades de los artistas, sellos y premios que sigues."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "Buscar",
|
||||
|
||||
@@ -91,7 +91,11 @@
|
||||
"pressAccolades": "Distinctions de la presse",
|
||||
"qobuzPlaylists": "Playlists Qobuz",
|
||||
"searchPlaylistsPlaceholder": "Rechercher des playlists...",
|
||||
"noPlaylistResults": "Aucune playlist trouvée"
|
||||
"noPlaylistResults": "Aucune playlist trouvée",
|
||||
"releaseWatch": {
|
||||
"title": "Radar des sorties",
|
||||
"subtitle": "Sorties des artistes, labels et récompenses que vous suivez."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "Recherche",
|
||||
|
||||
@@ -91,7 +91,11 @@
|
||||
"pressAccolades": "Elogios da Crítica",
|
||||
"qobuzPlaylists": "Playlists Qobuz",
|
||||
"searchPlaylistsPlaceholder": "Buscar playlists...",
|
||||
"noPlaylistResults": "Nenhuma playlist encontrada"
|
||||
"noPlaylistResults": "Nenhuma playlist encontrada",
|
||||
"releaseWatch": {
|
||||
"title": "Radar de Novidades",
|
||||
"subtitle": "Lançamentos dos artistas, selos e prêmios que você segue."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "Buscar",
|
||||
|
||||
@@ -18,7 +18,8 @@ export type HomeSectionId =
|
||||
| 'continueTracks'
|
||||
| 'topArtists'
|
||||
| 'favoriteAlbums'
|
||||
| 'yourMixes';
|
||||
| 'yourMixes'
|
||||
| 'releaseWatch';
|
||||
|
||||
export interface HomeSection {
|
||||
id: HomeSectionId;
|
||||
@@ -49,6 +50,7 @@ const STORAGE_KEY = 'qbz-home-settings';
|
||||
// Default section order and visibility
|
||||
const DEFAULT_SECTIONS: HomeSection[] = [
|
||||
{ id: 'newReleases', label: 'New Releases', visible: true, source: 'qobuz' },
|
||||
{ id: 'releaseWatch', label: 'Release Watch', visible: true, source: 'qobuz' },
|
||||
{ id: 'pressAwards', label: 'Press Awards', visible: true, source: 'qobuz' },
|
||||
{ id: 'mostStreamed', label: 'Popular Albums', visible: false, source: 'qobuz' },
|
||||
{ id: 'qobuzissimes', label: 'Qobuzissimes', visible: false, source: 'qobuz' },
|
||||
|
||||
Reference in new issue
Block a user