feat(ux): increase artist album limits and add Others section
- Increase artist detail album fetch from 100 to 500 - Increase bio truncation from 300 to 500 chars - Add Others category for greatest hits/anthologies - Add configurable limits for Home sections - Update HomeSettingsModal with limits grid UI cp
This commit is contained in:
1 parent
dd9eb90343
commit
bf4050a592
7 files changed
+247
-20
No files matched your search
@@ -167,7 +167,7 @@ pub async fn get_artist(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get artist detail with albums, playlists, and appears-on tracks
|
/// Get artist detail with albums, playlists, and appears-on tracks
|
||||||
/// Fetches 100 albums initially to ensure each album type section
|
/// Fetches 500 albums initially to ensure each album type section
|
||||||
/// (Discography, EPs, Live, etc.) has enough albums loaded
|
/// (Discography, EPs, Live, etc.) has enough albums loaded
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_artist_detail(
|
pub async fn get_artist_detail(
|
||||||
@@ -178,7 +178,7 @@ pub async fn get_artist_detail(
|
|||||||
|
|
||||||
let client = state.client.lock().await;
|
let client = state.client.lock().await;
|
||||||
client
|
client
|
||||||
.get_artist_detail(artist_id, Some(100), Some(0))
|
.get_artist_detail(artist_id, Some(500), Some(0))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ function isLiveAlbum(album: QobuzAlbum): boolean {
|
|||||||
return /\blive\b|\blive at\b|\blive in\b|\ben concert\b|\bin concert\b|\bconcert\b|\bunplugged\b|\bsessions?\b/.test(title);
|
return /\blive\b|\blive at\b|\blive in\b|\ben concert\b|\bin concert\b|\bconcert\b|\bunplugged\b|\bsessions?\b/.test(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOtherAlbum(album: QobuzAlbum): boolean {
|
||||||
|
const title = album.title?.toLowerCase() ?? '';
|
||||||
|
|
||||||
|
return /\b(greatest hits|best of|anthology|collection|box set|essentials|retrospective|very best)\b/.test(title);
|
||||||
|
}
|
||||||
|
|
||||||
function toArtistAlbumSummary(album: QobuzAlbum): ArtistAlbumSummary {
|
function toArtistAlbumSummary(album: QobuzAlbum): ArtistAlbumSummary {
|
||||||
const artwork = getQobuzImage(album.image);
|
const artwork = getQobuzImage(album.image);
|
||||||
const quality = formatQuality(
|
const quality = formatQuality(
|
||||||
@@ -199,6 +205,7 @@ export function convertQobuzArtist(artist: QobuzArtist): ArtistDetail {
|
|||||||
const albums: ArtistAlbumSummary[] = [];
|
const albums: ArtistAlbumSummary[] = [];
|
||||||
const epsSingles: ArtistAlbumSummary[] = [];
|
const epsSingles: ArtistAlbumSummary[] = [];
|
||||||
const liveAlbums: ArtistAlbumSummary[] = [];
|
const liveAlbums: ArtistAlbumSummary[] = [];
|
||||||
|
const others: ArtistAlbumSummary[] = [];
|
||||||
|
|
||||||
for (const album of albumItems) {
|
for (const album of albumItems) {
|
||||||
const summary = toArtistAlbumSummary(album);
|
const summary = toArtistAlbumSummary(album);
|
||||||
@@ -206,6 +213,8 @@ export function convertQobuzArtist(artist: QobuzArtist): ArtistDetail {
|
|||||||
liveAlbums.push(summary);
|
liveAlbums.push(summary);
|
||||||
} else if (isEpOrSingle(album)) {
|
} else if (isEpOrSingle(album)) {
|
||||||
epsSingles.push(summary);
|
epsSingles.push(summary);
|
||||||
|
} else if (isOtherAlbum(album)) {
|
||||||
|
others.push(summary);
|
||||||
} else {
|
} else {
|
||||||
albums.push(summary);
|
albums.push(summary);
|
||||||
}
|
}
|
||||||
@@ -221,6 +230,7 @@ export function convertQobuzArtist(artist: QobuzArtist): ArtistDetail {
|
|||||||
epsSingles,
|
epsSingles,
|
||||||
liveAlbums,
|
liveAlbums,
|
||||||
compilations: buildCompilationAlbums(artist.tracks_appears_on?.items),
|
compilations: buildCompilationAlbums(artist.tracks_appears_on?.items),
|
||||||
|
others,
|
||||||
playlists: toArtistPlaylists(artist.playlists),
|
playlists: toArtistPlaylists(artist.playlists),
|
||||||
totalAlbums: artist.albums?.total || artist.albums_count || 0,
|
totalAlbums: artist.albums?.total || artist.albums_count || 0,
|
||||||
albumsFetched
|
albumsFetched
|
||||||
@@ -238,10 +248,12 @@ export function appendArtistAlbums(
|
|||||||
const existingAlbumIds = new Set(artist.albums.map(album => album.id));
|
const existingAlbumIds = new Set(artist.albums.map(album => album.id));
|
||||||
const existingEpIds = new Set(artist.epsSingles.map(album => album.id));
|
const existingEpIds = new Set(artist.epsSingles.map(album => album.id));
|
||||||
const existingLiveIds = new Set(artist.liveAlbums.map(album => album.id));
|
const existingLiveIds = new Set(artist.liveAlbums.map(album => album.id));
|
||||||
|
const existingOtherIds = new Set(artist.others.map(album => album.id));
|
||||||
|
|
||||||
const albums = [...artist.albums];
|
const albums = [...artist.albums];
|
||||||
const epsSingles = [...artist.epsSingles];
|
const epsSingles = [...artist.epsSingles];
|
||||||
const liveAlbums = [...artist.liveAlbums];
|
const liveAlbums = [...artist.liveAlbums];
|
||||||
|
const others = [...artist.others];
|
||||||
|
|
||||||
for (const album of newAlbums) {
|
for (const album of newAlbums) {
|
||||||
const summary = toArtistAlbumSummary(album);
|
const summary = toArtistAlbumSummary(album);
|
||||||
@@ -255,6 +267,11 @@ export function appendArtistAlbums(
|
|||||||
epsSingles.push(summary);
|
epsSingles.push(summary);
|
||||||
existingEpIds.add(summary.id);
|
existingEpIds.add(summary.id);
|
||||||
}
|
}
|
||||||
|
} else if (isOtherAlbum(album)) {
|
||||||
|
if (!existingOtherIds.has(summary.id)) {
|
||||||
|
others.push(summary);
|
||||||
|
existingOtherIds.add(summary.id);
|
||||||
|
}
|
||||||
} else if (!existingAlbumIds.has(summary.id)) {
|
} else if (!existingAlbumIds.has(summary.id)) {
|
||||||
albums.push(summary);
|
albums.push(summary);
|
||||||
existingAlbumIds.add(summary.id);
|
existingAlbumIds.add(summary.id);
|
||||||
@@ -266,6 +283,7 @@ export function appendArtistAlbums(
|
|||||||
albums,
|
albums,
|
||||||
epsSingles,
|
epsSingles,
|
||||||
liveAlbums,
|
liveAlbums,
|
||||||
|
others,
|
||||||
totalAlbums: totalAlbums ?? artist.totalAlbums,
|
totalAlbums: totalAlbums ?? artist.totalAlbums,
|
||||||
albumsFetched: albumsFetched ?? artist.albumsFetched + newAlbums.length
|
albumsFetched: albumsFetched ?? artist.albumsFetched + newAlbums.length
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
setGreetingEnabled,
|
setGreetingEnabled,
|
||||||
setCustomGreeting,
|
setCustomGreeting,
|
||||||
resetToDefaults,
|
resetToDefaults,
|
||||||
|
updateLimit,
|
||||||
type HomeSettings
|
type HomeSettings
|
||||||
} from '$lib/stores/homeSettingsStore';
|
} from '$lib/stores/homeSettingsStore';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
@@ -57,6 +58,10 @@
|
|||||||
function handleReset() {
|
function handleReset() {
|
||||||
resetToDefaults();
|
resetToDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleLimitChange(key: keyof HomeSettings['limits'], value: number) {
|
||||||
|
updateLimit(key, value);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} {onClose} title={$t('home.customizeHome')}>
|
<Modal {isOpen} {onClose} title={$t('home.customizeHome')}>
|
||||||
@@ -98,6 +103,92 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Item Limits -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<div class="section-title">Item Limits</div>
|
||||||
|
<p class="section-desc">Configure how many items to show in each section</p>
|
||||||
|
|
||||||
|
<div class="limits-grid">
|
||||||
|
<div class="limit-item">
|
||||||
|
<label class="limit-label" for="limit-recent">Recently Played</label>
|
||||||
|
<input
|
||||||
|
id="limit-recent"
|
||||||
|
type="number"
|
||||||
|
class="limit-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={settings.limits.recentAlbums}
|
||||||
|
onchange={(e) => handleLimitChange('recentAlbums', Number(e.currentTarget.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limit-item">
|
||||||
|
<label class="limit-label" for="limit-continue">Continue Listening</label>
|
||||||
|
<input
|
||||||
|
id="limit-continue"
|
||||||
|
type="number"
|
||||||
|
class="limit-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={settings.limits.continueTracks}
|
||||||
|
onchange={(e) => handleLimitChange('continueTracks', Number(e.currentTarget.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limit-item">
|
||||||
|
<label class="limit-label" for="limit-artists">Top Artists</label>
|
||||||
|
<input
|
||||||
|
id="limit-artists"
|
||||||
|
type="number"
|
||||||
|
class="limit-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={settings.limits.topArtists}
|
||||||
|
onchange={(e) => handleLimitChange('topArtists', Number(e.currentTarget.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limit-item">
|
||||||
|
<label class="limit-label" for="limit-fav-albums">Favorite Albums</label>
|
||||||
|
<input
|
||||||
|
id="limit-fav-albums"
|
||||||
|
type="number"
|
||||||
|
class="limit-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={settings.limits.favoriteAlbums}
|
||||||
|
onchange={(e) => handleLimitChange('favoriteAlbums', Number(e.currentTarget.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limit-item">
|
||||||
|
<label class="limit-label" for="limit-fav-tracks">Favorite Tracks</label>
|
||||||
|
<input
|
||||||
|
id="limit-fav-tracks"
|
||||||
|
type="number"
|
||||||
|
class="limit-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={settings.limits.favoriteTracks}
|
||||||
|
onchange={(e) => handleLimitChange('favoriteTracks', Number(e.currentTarget.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="limit-item">
|
||||||
|
<label class="limit-label" for="limit-featured">Featured Albums</label>
|
||||||
|
<input
|
||||||
|
id="limit-featured"
|
||||||
|
type="number"
|
||||||
|
class="limit-input"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={settings.limits.featuredAlbums}
|
||||||
|
onchange={(e) => handleLimitChange('featuredAlbums', Number(e.currentTarget.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Sections Order -->
|
<!-- Sections Order -->
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<div class="section-title">{$t('homeSettings.sections')}</div>
|
<div class="section-title">{$t('homeSettings.sections')}</div>
|
||||||
@@ -388,4 +479,41 @@
|
|||||||
border-color: var(--text-muted);
|
border-color: var(--text-muted);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.limits-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limit-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limit-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.limit-input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--bg-tertiary);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limit-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.limit-input:hover {
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -114,6 +114,7 @@
|
|||||||
let epsSinglesSection = $state<HTMLDivElement | null>(null);
|
let epsSinglesSection = $state<HTMLDivElement | null>(null);
|
||||||
let liveAlbumsSection = $state<HTMLDivElement | null>(null);
|
let liveAlbumsSection = $state<HTMLDivElement | null>(null);
|
||||||
let compilationsSection = $state<HTMLDivElement | null>(null);
|
let compilationsSection = $state<HTMLDivElement | null>(null);
|
||||||
|
let othersSection = $state<HTMLDivElement | null>(null);
|
||||||
let playlistsSection = $state<HTMLDivElement | null>(null);
|
let playlistsSection = $state<HTMLDivElement | null>(null);
|
||||||
let activeJumpSection = $state('about');
|
let activeJumpSection = $state('about');
|
||||||
let jumpObserver: IntersectionObserver | null = null;
|
let jumpObserver: IntersectionObserver | null = null;
|
||||||
@@ -149,7 +150,8 @@
|
|||||||
...artist.albums,
|
...artist.albums,
|
||||||
...artist.epsSingles,
|
...artist.epsSingles,
|
||||||
...artist.liveAlbums,
|
...artist.liveAlbums,
|
||||||
...artist.compilations
|
...artist.compilations,
|
||||||
|
...artist.others
|
||||||
];
|
];
|
||||||
loadAllAlbumDownloadStatuses(allAlbums);
|
loadAllAlbumDownloadStatuses(allAlbums);
|
||||||
}
|
}
|
||||||
@@ -185,7 +187,8 @@
|
|||||||
...artist.albums,
|
...artist.albums,
|
||||||
...artist.epsSingles,
|
...artist.epsSingles,
|
||||||
...artist.liveAlbums,
|
...artist.liveAlbums,
|
||||||
...artist.compilations
|
...artist.compilations,
|
||||||
|
...artist.others
|
||||||
];
|
];
|
||||||
await loadAllAlbumDownloadStatuses(allAlbums);
|
await loadAllAlbumDownloadStatuses(allAlbums);
|
||||||
}
|
}
|
||||||
@@ -339,7 +342,7 @@
|
|||||||
|
|
||||||
// Truncate bio for collapsed view
|
// Truncate bio for collapsed view
|
||||||
let truncatedBio = $derived(
|
let truncatedBio = $derived(
|
||||||
bioText && bioText.length > 300 ? bioText.slice(0, 300) + '...' : bioText
|
bioText && bioText.length > 500 ? bioText.slice(0, 500) + '...' : bioText
|
||||||
);
|
);
|
||||||
|
|
||||||
let hasMoreAlbums = $derived(!!onLoadMore && artist.albumsFetched < artist.totalAlbums);
|
let hasMoreAlbums = $derived(!!onLoadMore && artist.albumsFetched < artist.totalAlbums);
|
||||||
@@ -347,6 +350,7 @@
|
|||||||
let hasEpsSingles = $derived(artist.epsSingles.length > 0);
|
let hasEpsSingles = $derived(artist.epsSingles.length > 0);
|
||||||
let hasLiveAlbums = $derived(artist.liveAlbums.length > 0);
|
let hasLiveAlbums = $derived(artist.liveAlbums.length > 0);
|
||||||
let hasCompilations = $derived(artist.compilations.length > 0);
|
let hasCompilations = $derived(artist.compilations.length > 0);
|
||||||
|
let hasOthers = $derived(artist.others.length > 0);
|
||||||
let hasPlaylists = $derived(artist.playlists.length > 0);
|
let hasPlaylists = $derived(artist.playlists.length > 0);
|
||||||
let jumpSections = $derived.by(() => [
|
let jumpSections = $derived.by(() => [
|
||||||
{ id: 'about', label: 'About', el: aboutSection, visible: true },
|
{ id: 'about', label: 'About', el: aboutSection, visible: true },
|
||||||
@@ -355,6 +359,7 @@
|
|||||||
{ id: 'eps', label: 'EPs & Singles', el: epsSinglesSection, visible: hasEpsSingles },
|
{ id: 'eps', label: 'EPs & Singles', el: epsSinglesSection, visible: hasEpsSingles },
|
||||||
{ id: 'live', label: 'Live Albums', el: liveAlbumsSection, visible: hasLiveAlbums },
|
{ id: 'live', label: 'Live Albums', el: liveAlbumsSection, visible: hasLiveAlbums },
|
||||||
{ id: 'compilations', label: 'Compilations', el: compilationsSection, visible: hasCompilations },
|
{ id: 'compilations', label: 'Compilations', el: compilationsSection, visible: hasCompilations },
|
||||||
|
{ id: 'others', label: 'Others', el: othersSection, visible: hasOthers },
|
||||||
{ id: 'playlists', label: 'Playlists', el: playlistsSection, visible: hasPlaylists },
|
{ id: 'playlists', label: 'Playlists', el: playlistsSection, visible: hasPlaylists },
|
||||||
].filter(section => section.visible));
|
].filter(section => section.visible));
|
||||||
|
|
||||||
@@ -747,6 +752,36 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if artist.others.length > 0}
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="discography section-anchor" bind:this={othersSection}>
|
||||||
|
<h2 class="section-title">Others</h2>
|
||||||
|
<div class="albums-grid">
|
||||||
|
{#each artist.others as album}
|
||||||
|
<AlbumCard
|
||||||
|
albumId={album.id}
|
||||||
|
artwork={album.artwork}
|
||||||
|
title={album.title}
|
||||||
|
artist={album.year || ''}
|
||||||
|
quality={album.quality}
|
||||||
|
onPlay={onAlbumPlay ? () => onAlbumPlay(album.id) : undefined}
|
||||||
|
onPlayNext={onAlbumPlayNext ? () => onAlbumPlayNext(album.id) : undefined}
|
||||||
|
onPlayLater={onAlbumPlayLater ? () => onAlbumPlayLater(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>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if artist.playlists.length > 0}
|
{#if artist.playlists.length > 0}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,8 @@
|
|||||||
featuredAlbums: 12
|
featuredAlbums: 12
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let homeLimits = $state(getSettings().limits);
|
||||||
|
|
||||||
// Loading states for progressive render
|
// Loading states for progressive render
|
||||||
let isInitializing = $state(true);
|
let isInitializing = $state(true);
|
||||||
let isOverlayVisible = $state(true); // Overlay that fades out when ALL sections ready
|
let isOverlayVisible = $state(true); // Overlay that fades out when ALL sections ready
|
||||||
@@ -215,6 +217,7 @@
|
|||||||
// Subscribe to home settings changes
|
// Subscribe to home settings changes
|
||||||
const unsubscribe = subscribeHomeSettings(() => {
|
const unsubscribe = subscribeHomeSettings(() => {
|
||||||
homeSettings = getSettings();
|
homeSettings = getSettings();
|
||||||
|
homeLimits = getSettings().limits;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load home data
|
// Load home data
|
||||||
@@ -341,7 +344,7 @@
|
|||||||
return Array.from(counts.entries())
|
return Array.from(counts.entries())
|
||||||
.map(([artistId, playCount]) => ({ artistId, playCount }))
|
.map(([artistId, playCount]) => ({ artistId, playCount }))
|
||||||
.sort((a, b) => b.playCount - a.playCount)
|
.sort((a, b) => b.playCount - a.playCount)
|
||||||
.slice(0, LIMITS.topArtists);
|
.slice(0, homeLimits.topArtists);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadHome() {
|
async function loadHome() {
|
||||||
@@ -373,15 +376,15 @@
|
|||||||
|
|
||||||
// Start ML data loading FIRST (local SQLite) - this gets the seeds
|
// Start ML data loading FIRST (local SQLite) - this gets the seeds
|
||||||
const mlPromise = invoke<HomeSeeds>('reco_get_home_ml', {
|
const mlPromise = invoke<HomeSeeds>('reco_get_home_ml', {
|
||||||
limitRecentAlbums: LIMITS.recentAlbums,
|
limitRecentAlbums: homeLimits.recentAlbums,
|
||||||
limitContinueTracks: LIMITS.continueTracks,
|
limitContinueTracks: homeLimits.continueTracks,
|
||||||
limitTopArtists: LIMITS.topArtists,
|
limitTopArtists: homeLimits.topArtists,
|
||||||
limitFavorites: Math.max(LIMITS.favoriteAlbums, LIMITS.favoriteTracks)
|
limitFavorites: Math.max(homeLimits.favoriteAlbums, homeLimits.favoriteTracks)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start Qobuz API calls in parallel (don't await)
|
// Start Qobuz API calls in parallel (don't await)
|
||||||
if (isSectionVisible('newReleases')) {
|
if (isSectionVisible('newReleases')) {
|
||||||
fetchFeaturedAlbums('new-releases', LIMITS.featuredAlbums).then(async albums => {
|
fetchFeaturedAlbums('new-releases', homeLimits.featuredAlbums).then(async albums => {
|
||||||
newReleases = albums;
|
newReleases = albums;
|
||||||
await loadAllAlbumDownloadStatuses(albums);
|
await loadAllAlbumDownloadStatuses(albums);
|
||||||
loadingNewReleases = false;
|
loadingNewReleases = false;
|
||||||
@@ -392,7 +395,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSectionVisible('pressAwards')) {
|
if (isSectionVisible('pressAwards')) {
|
||||||
fetchFeaturedAlbums('press-awards', LIMITS.featuredAlbums).then(async albums => {
|
fetchFeaturedAlbums('press-awards', homeLimits.featuredAlbums).then(async albums => {
|
||||||
pressAwards = albums;
|
pressAwards = albums;
|
||||||
await loadAllAlbumDownloadStatuses(albums);
|
await loadAllAlbumDownloadStatuses(albums);
|
||||||
loadingPressAwards = false;
|
loadingPressAwards = false;
|
||||||
@@ -403,7 +406,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSectionVisible('mostStreamed')) {
|
if (isSectionVisible('mostStreamed')) {
|
||||||
fetchFeaturedAlbums('most-streamed', LIMITS.featuredAlbums).then(async albums => {
|
fetchFeaturedAlbums('most-streamed', homeLimits.featuredAlbums).then(async albums => {
|
||||||
mostStreamed = albums;
|
mostStreamed = albums;
|
||||||
await loadAllAlbumDownloadStatuses(albums);
|
await loadAllAlbumDownloadStatuses(albums);
|
||||||
loadingMostStreamed = false;
|
loadingMostStreamed = false;
|
||||||
@@ -414,7 +417,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSectionVisible('qobuzissimes')) {
|
if (isSectionVisible('qobuzissimes')) {
|
||||||
fetchFeaturedAlbums('qobuzissimes', LIMITS.featuredAlbums).then(async albums => {
|
fetchFeaturedAlbums('qobuzissimes', homeLimits.featuredAlbums).then(async albums => {
|
||||||
qobuzissimes = albums;
|
qobuzissimes = albums;
|
||||||
await loadAllAlbumDownloadStatuses(albums);
|
await loadAllAlbumDownloadStatuses(albums);
|
||||||
loadingQobuzissimes = false;
|
loadingQobuzissimes = false;
|
||||||
@@ -425,7 +428,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSectionVisible('editorPicks')) {
|
if (isSectionVisible('editorPicks')) {
|
||||||
fetchFeaturedAlbums('editor-picks', LIMITS.featuredAlbums).then(async albums => {
|
fetchFeaturedAlbums('editor-picks', homeLimits.featuredAlbums).then(async albums => {
|
||||||
editorPicks = albums;
|
editorPicks = albums;
|
||||||
await loadAllAlbumDownloadStatuses(albums);
|
await loadAllAlbumDownloadStatuses(albums);
|
||||||
loadingEditorPicks = false;
|
loadingEditorPicks = false;
|
||||||
@@ -454,7 +457,7 @@
|
|||||||
// Recently Played (albums) - start immediately with seeds
|
// Recently Played (albums) - start immediately with seeds
|
||||||
if (isSectionVisible('recentAlbums')) {
|
if (isSectionVisible('recentAlbums')) {
|
||||||
const recentAlbumIds = normalizeAlbumIds(seeds.recentlyPlayedAlbumIds);
|
const recentAlbumIds = normalizeAlbumIds(seeds.recentlyPlayedAlbumIds);
|
||||||
fetchAlbums(recentAlbumIds.slice(0, LIMITS.recentAlbums)).then(async albums => {
|
fetchAlbums(recentAlbumIds.slice(0, homeLimits.recentAlbums)).then(async albums => {
|
||||||
recentAlbums = albums;
|
recentAlbums = albums;
|
||||||
await loadAllAlbumDownloadStatuses(albums);
|
await loadAllAlbumDownloadStatuses(albums);
|
||||||
loadingRecentAlbums = false;
|
loadingRecentAlbums = false;
|
||||||
@@ -466,7 +469,7 @@
|
|||||||
|
|
||||||
// Top Artists
|
// Top Artists
|
||||||
if (isSectionVisible('topArtists')) {
|
if (isSectionVisible('topArtists')) {
|
||||||
fetchArtists(seeds.topArtistIds.slice(0, LIMITS.topArtists)).then(artists => {
|
fetchArtists(seeds.topArtistIds.slice(0, homeLimits.topArtists)).then(artists => {
|
||||||
topArtists = artists;
|
topArtists = artists;
|
||||||
loadingTopArtists = false;
|
loadingTopArtists = false;
|
||||||
markSectionFinished();
|
markSectionFinished();
|
||||||
@@ -478,12 +481,12 @@
|
|||||||
// Favorite Albums
|
// Favorite Albums
|
||||||
if (isSectionVisible('favoriteAlbums')) {
|
if (isSectionVisible('favoriteAlbums')) {
|
||||||
// Get favorite track details to extract album IDs
|
// Get favorite track details to extract album IDs
|
||||||
fetchTracks(seeds.favoriteTrackIds.slice(0, LIMITS.favoriteTracks)).then(async favoriteTrackDetails => {
|
fetchTracks(seeds.favoriteTrackIds.slice(0, homeLimits.favoriteTracks)).then(async favoriteTrackDetails => {
|
||||||
const favoriteAlbumIds = normalizeAlbumIds([
|
const favoriteAlbumIds = normalizeAlbumIds([
|
||||||
...seeds.favoriteAlbumIds,
|
...seeds.favoriteAlbumIds,
|
||||||
...favoriteTrackDetails.map(track => track.albumId)
|
...favoriteTrackDetails.map(track => track.albumId)
|
||||||
]);
|
]);
|
||||||
const albums = await fetchAlbums(favoriteAlbumIds.slice(0, LIMITS.favoriteAlbums));
|
const albums = await fetchAlbums(favoriteAlbumIds.slice(0, homeLimits.favoriteAlbums));
|
||||||
favoriteAlbums = albums;
|
favoriteAlbums = albums;
|
||||||
await loadAllAlbumDownloadStatuses(albums);
|
await loadAllAlbumDownloadStatuses(albums);
|
||||||
loadingFavoriteAlbums = false;
|
loadingFavoriteAlbums = false;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface HomeSection {
|
|||||||
label: string;
|
label: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
source?: 'qobuz' | 'ml'; // qobuz = editorial, ml = ML-powered recommendations
|
source?: 'qobuz' | 'ml'; // qobuz = editorial, ml = ML-powered recommendations
|
||||||
|
limit?: number; // configurable item count
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeSettings {
|
export interface HomeSettings {
|
||||||
@@ -30,6 +31,14 @@ export interface HomeSettings {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
customText: string | null; // null = use default time-based greeting
|
customText: string | null; // null = use default time-based greeting
|
||||||
};
|
};
|
||||||
|
limits: {
|
||||||
|
recentAlbums: number;
|
||||||
|
continueTracks: number;
|
||||||
|
topArtists: number;
|
||||||
|
favoriteAlbums: number;
|
||||||
|
favoriteTracks: number;
|
||||||
|
featuredAlbums: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'qbz-home-settings';
|
const STORAGE_KEY = 'qbz-home-settings';
|
||||||
@@ -52,6 +61,14 @@ const DEFAULT_SETTINGS: HomeSettings = {
|
|||||||
greeting: {
|
greeting: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
customText: null
|
customText: null
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
recentAlbums: 20,
|
||||||
|
continueTracks: 10,
|
||||||
|
topArtists: 20,
|
||||||
|
favoriteAlbums: 12,
|
||||||
|
favoriteTracks: 10,
|
||||||
|
featuredAlbums: 12
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,7 +105,8 @@ function loadSettings(): HomeSettings {
|
|||||||
function mergeWithDefaults(saved: Partial<HomeSettings>): HomeSettings {
|
function mergeWithDefaults(saved: Partial<HomeSettings>): HomeSettings {
|
||||||
const result: HomeSettings = {
|
const result: HomeSettings = {
|
||||||
sections: [],
|
sections: [],
|
||||||
greeting: saved.greeting ?? DEFAULT_SETTINGS.greeting
|
greeting: saved.greeting ?? DEFAULT_SETTINGS.greeting,
|
||||||
|
limits: { ...DEFAULT_SETTINGS.limits, ...(saved.limits || {}) }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build section map from saved
|
// Build section map from saved
|
||||||
@@ -304,3 +322,21 @@ export function resetToDefaults(): void {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a specific limit
|
||||||
|
*/
|
||||||
|
export function updateLimit(key: keyof HomeSettings['limits'], value: number): void {
|
||||||
|
if (value >= 1 && value <= 100) {
|
||||||
|
settings.limits[key] = value;
|
||||||
|
saveSettings();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current limits
|
||||||
|
*/
|
||||||
|
export function getLimits(): HomeSettings['limits'] {
|
||||||
|
return settings.limits;
|
||||||
|
}
|
||||||
@@ -152,6 +152,13 @@ export interface ArtistDetail {
|
|||||||
year?: string;
|
year?: string;
|
||||||
quality: string;
|
quality: string;
|
||||||
}[];
|
}[];
|
||||||
|
others: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
artwork: string;
|
||||||
|
year?: string;
|
||||||
|
quality: string;
|
||||||
|
}[];
|
||||||
playlists: {
|
playlists: {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
Reference in new issue
Block a user