Release: web UX overhaul + Android native port + server polish #59

Merged
bvandeusen merged 298 commits from dev into main 2026-06-01 16:11:21 -04:00
2 changed files with 67 additions and 0 deletions
Showing only changes of commit 5393174a27 - Show all commits
+29
View File
@@ -49,6 +49,30 @@
.slice(0, 3)
);
// Secondary system kinds the server generates that don't get pinned
// to fixed Home slots: surfaced after the Songs-like slots when they
// exist, in server-registry order (internal/playlists/system.go).
// No placeholders for these — they depend on library shape (Deep
// cuts needs deep albums, On this day needs prior history, etc.) so
// a missing one means "not enough data," not "still building."
// Mirrors Android's SECONDARY_SYSTEM_VARIANTS (HomeScreen.kt). Operator
// request 2026-06-01 for web parity.
const SECONDARY_SYSTEM_VARIANTS = [
'deep_cuts',
'rediscover',
'new_for_you',
'on_this_day',
'first_listens'
] as const;
const secondarySystemPlaylists = $derived(
SECONDARY_SYSTEM_VARIANTS
.map((variant) =>
(systemPlaylistsQ.data?.owned ?? []).find((p) => p.system_variant === variant)
)
.filter((p): p is Playlist => p != null)
);
const userPlaylists = $derived(userPlaylistsQ.data?.owned ?? []);
type PlaceholderVariant = 'building' | 'failed' | 'pending' | 'seed-needed';
@@ -93,6 +117,11 @@
}
}
// Secondary system kinds in server-registry order, when generated.
for (const p of secondarySystemPlaylists) {
out.push({ kind: 'real', playlist: p });
}
// User playlists trail (server returns most-recently-updated first).
for (const p of userPlaylists) {
out.push({ kind: 'real', playlist: p });
+38
View File
@@ -109,4 +109,42 @@ describe('home Playlists section', () => {
// real For-You tile.
expect(placeholders).toHaveLength(4);
});
test('renders secondary system kinds (deep_cuts / new_for_you) after Songs-like slots', () => {
// Operator backflow 2026-06-01: web Home surfaces the 5 secondary
// system kinds when generated. No placeholders for them — they
// depend on library shape, so missing means "not enough data."
const makePlaylist = (id: string, variant: string, name: string): Playlist => ({
id,
user_id: 'u1',
owner_username: 'u1',
name,
description: '',
is_public: false,
kind: 'system',
system_variant: variant,
refreshable: variant !== 'songs_like_artist',
seed_artist_id: null,
cover_url: '',
track_count: 25,
duration_sec: 1500,
created_at: '2026-05-04T00:00:00Z',
updated_at: '2026-05-04T00:00:00Z'
});
const owned = [
makePlaylist('fy', 'for_you', 'For You'),
makePlaylist('dc', 'deep_cuts', 'Deep cuts'),
makePlaylist('nfy', 'new_for_you', 'New for you')
];
(createPlaylistsQuery as unknown as ReturnType<typeof vi.fn>).mockImplementation(
(kind?: string) =>
kind === 'system'
? readable({ data: { owned, public: [] }, isPending: false, isError: false })
: readable({ data: emptyPlaylistsResponse, isPending: false, isError: false })
);
render(Page);
expect(screen.getByText('For You')).toBeInTheDocument();
expect(screen.getByText('Deep cuts')).toBeInTheDocument();
expect(screen.getByText('New for you')).toBeInTheDocument();
});
});