feat: redesign web interface to match aonsoku layout

Replace the early prototype UI with a darker Aonsoku-inspired shell featuring a compact top bar, library sidebar, command palette, settings overlay, dense track list, artists table, albums grid, and a bottom player bar. Add a supporting albums browse endpoint so the frontend can render the same navigation shape without faking data.
This commit is contained in:
2026-04-02 22:53:13 +03:00
parent 2f7034fae2
commit 2e7283baad
16 changed files with 1201 additions and 242 deletions

View File

@@ -6,21 +6,21 @@ export type User = {
isAdmin: boolean
}
export type HomePayload = {
recentAlbums: Array<{
id: string
artistId: string
artistName: string
title: string
year: number
trackCount: number
coverArtId: string
}>
artists: Array<{
id: string
name: string
albumCount: number
}>
export type Artist = {
id: string
name: string
albumCount: number
coverArtId: string
}
export type Album = {
id: string
artistId: string
artistName: string
title: string
year: number
trackCount: number
coverArtId: string
}
export type Track = {
@@ -32,6 +32,36 @@ export type Track = {
albumTitle: string
trackNumber: number
durationSeconds: number
coverArtId?: string
}
export type ArtistDetail = Artist & {
albums: Album[]
}
export type AlbumDetail = Album & {
tracks: Track[]
}
export type SearchResults = {
artists: Artist[]
albums: Album[]
tracks: Track[]
}
export type ScanStatus = {
scanning: boolean
startedAt?: string
finishedAt?: string
lastError?: string
artists: number
albums: number
tracks: number
}
export type HomePayload = {
recentAlbums: Album[]
artists: Artist[]
}
const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:4040'
@@ -65,11 +95,48 @@ export async function fetchHome() {
return request<HomePayload>('/api/home')
}
export async function fetchArtists() {
return request<{ items: Artist[] }>('/api/artists')
}
export async function fetchArtist(id: string) {
return request<ArtistDetail>(`/api/artists/${id}`)
}
export async function fetchAlbums() {
return request<{ items: Album[] }>('/api/albums')
}
export async function fetchAlbum(id: string) {
return request<AlbumDetail>(`/api/albums/${id}`)
}
export async function fetchTracks() {
return request<{ items: Track[] }>('/api/tracks')
}
export async function fetchTrack(id: string) {
return request<Track>(`/api/tracks/${id}`)
}
export async function searchLibrary(query: string) {
return request<SearchResults>(`/api/search?q=${encodeURIComponent(query)}`)
}
export async function fetchScanStatus() {
return request<ScanStatus>('/api/admin/scan-status')
}
export async function triggerScan() {
return request<ScanStatus>('/api/admin/scan', { method: 'POST' })
}
export function coverArtUrl(id: string) {
const token = useSessionStore.getState().token
return `${API_BASE}/api/cover-art/${id}${token ? `?token=${encodeURIComponent(token)}` : ''}`
}
export function streamUrl(id: string) {
const token = useSessionStore.getState().token
return `${API_BASE}/api/stream/${id}${token ? `?token=${encodeURIComponent(token)}` : ''}`
}