web-player: split player, navigation and accent state into focused modules

This commit is contained in:
2026-09-24 05:25:35 +02:00
parent e89463fb83
commit 5cda7cb4c1
47 changed files with 520 additions and 457 deletions
@@ -1,23 +0,0 @@
import { describe, expect, it } from 'vitest';
import { appendUnique, shuffle } from './collections';
describe('appendUnique', () => {
const keyOf = (item: { id: number }) => item.id;
it('appends only unseen keys', () => {
const result = appendUnique([{ id: 1 }], [{ id: 1 }, { id: 2 }, { id: 2 }, { id: 3 }], keyOf);
expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
});
it('returns the same array when nothing is new', () => {
const current = [{ id: 1 }];
expect(appendUnique(current, [{ id: 1 }], keyOf)).toBe(current);
});
});
describe('shuffle', () => {
it('keeps the same elements and does not mutate the input', () => {
const input = [1, 2, 3, 4, 5, 6];
const result = shuffle(input);
expect([...result].sort()).toEqual(input);
expect(input).toEqual([1, 2, 3, 4, 5, 6]);
});
});
-24
View File
@@ -1,24 +0,0 @@
export function shuffle<T>(items: T[]): T[] {
const copy = [...items];
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
}
export function appendUnique<T>(
current: T[],
incoming: T[],
keyOf: (item: T) => string | number
): T[] {
const seen = new Set(current.map(keyOf));
const added: T[] = [];
for (const item of incoming) {
const key = keyOf(item);
if (seen.has(key)) continue;
seen.add(key);
added.push(item);
}
return added.length > 0 ? [...current, ...added] : current;
}
+15 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { findTopResult } from './search';
import { findTopResult, searchChips } from './search';
const empty = { tracks: [], albums: [], playlists: [], users: [] };
@@ -26,3 +26,17 @@ describe('findTopResult', () => {
expect(findTopResult({ ...empty, query: 'x' })).toBeNull();
});
});
describe('searchChips', () => {
it('adds an "all" chip with the total', () => {
const chips = searchChips({ tracks: 3, albums: 2, playlists: 0, users: 1 });
expect(chips.map((chip) => [chip.filter, chip.count])).toEqual([
['all', 6],
['tracks', 3],
['albums', 2],
['playlists', 0],
['users', 1]
]);
expect(chips[0].label).toBe('Todo');
});
});
+24 -22
View File
@@ -1,38 +1,40 @@
export const SEARCH_FILTERS = ['Todo', 'Canciones', 'Álbumes', 'Playlists', 'Usuarios'] as const;
export type SearchFilter = (typeof SEARCH_FILTERS)[number];
export const SEARCH_KINDS = ['tracks', 'albums', 'playlists', 'users'] as const;
export type SearchKind = (typeof SEARCH_KINDS)[number];
export type SearchFilter = 'all' | SearchKind;
export const SEARCH_GROUP_PREVIEW = 4;
export function showGroup(filter: SearchFilter, label: Exclude<SearchFilter, 'Todo'>) {
return filter === 'Todo' || filter === label;
export const SEARCH_LABELS: Record<SearchFilter, string> = {
all: 'Todo',
tracks: 'Canciones',
albums: 'Álbumes',
playlists: 'Playlists',
users: 'Usuarios'
};
export function showGroup(filter: SearchFilter, kind: SearchKind) {
return filter === 'all' || filter === kind;
}
export function capped<T>(filter: SearchFilter, list: T[]) {
return filter === 'Todo' ? list.slice(0, SEARCH_GROUP_PREVIEW) : list;
return filter === 'all' ? list.slice(0, SEARCH_GROUP_PREVIEW) : list;
}
export function matchPlaylists<T extends { name: string }>(playlists: T[], query: string): T[] {
return query ? playlists.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())) : [];
}
export function searchCounts(counts: {
tracks: number;
albums: number;
playlists: number;
users: number;
}) {
return {
Canciones: counts.tracks,
Álbumes: counts.albums,
Playlists: counts.playlists,
Usuarios: counts.users
};
export type SearchCounts = Record<SearchKind, number>;
export function searchTotal(counts: SearchCounts): number {
return SEARCH_KINDS.reduce((sum, kind) => sum + counts[kind], 0);
}
export function searchChips(counts: ReturnType<typeof searchCounts>) {
const total = counts.Canciones + counts.Álbumes + counts.Playlists + counts.Usuarios;
return SEARCH_FILTERS.map((label) => ({
label,
count: label === 'Todo' ? total : counts[label]
export function searchChips(counts: SearchCounts) {
const filters: SearchFilter[] = ['all', ...SEARCH_KINDS];
return filters.map((filter) => ({
filter,
label: SEARCH_LABELS[filter],
count: filter === 'all' ? searchTotal(counts) : counts[filter]
}));
}