Add track support and UI list in DetailView

Introduce Track and ApiTrack types and add tracks to ApiMediaItem/Media. Map ApiMediaItem.tracks into Media in convertApiToMedia. Implement a conditional Tracks section in DetailView that lists sorted tracks with search input, track number, title, artist, duration and a play button (only shown when tracks exist). Files changed: src/types.ts, src/api.ts, src/components/DetailView.tsx.
This commit is contained in:
Lars Behrends
2026-04-11 01:42:45 +02:00
parent 6c316fbf84
commit dff599e5af
3 changed files with 78 additions and 2 deletions

View File

@@ -39,6 +39,15 @@ export interface ApiEpisode {
thumbnail: string;
}
export interface ApiTrack {
id: number;
media_id: number;
track_number: number;
title: string;
duration: number | null;
artist: string;
}
export interface ApiMediaItem {
id: number;
title: string;
@@ -70,6 +79,7 @@ export interface ApiMediaItem {
lastActivity?: string | null;
playtime?: number;
episodes?: ApiEpisode[];
tracks?: ApiTrack[];
}
export interface ApiStaff {
@@ -334,7 +344,8 @@ export function convertApiToMedia(apiItem: ApiMediaItem): Media {
playCount: apiItem.playCount,
lastActivity: apiItem.lastActivity,
playtime: apiItem.playtime,
episodes: apiItem.episodes
episodes: apiItem.episodes,
tracks: apiItem.tracks
};
}

View File

@@ -1,4 +1,4 @@
import { Media, Staff } from '@/types';
import { Media, Staff, Track } from '@/types';
import { useNavigate } from 'react-router-dom';
import { useState, useMemo, useEffect } from 'react';
import {
@@ -364,6 +364,61 @@ export default function DetailView({ media, onPersonClick }: DetailViewProps) {
</div>
</section>
)}
{/* Tracks Section - Only show if tracks data exists (Music) */}
{media.tracks && media.tracks.length > 0 && (
<section className="mt-20">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 text-[#6d28d9] font-black text-xl">
<span className="opacity-40">{media.tracks.length}</span> Track{media.tracks.length !== 1 ? 's' : ''}
</div>
</div>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
<Input placeholder="Search" className="pl-10 w-[200px] bg-muted border-none rounded-full h-9 text-sm" />
</div>
<Button variant="ghost" size="icon" className="text-muted-foreground">
<MoreHorizontal size={20} />
</Button>
<Button variant="ghost" size="icon" className="text-muted-foreground">
<ListFilter size={20} />
</Button>
</div>
</div>
<div className="border border-border rounded-2xl overflow-hidden">
<div className="divide-y divide-border">
{media.tracks
.sort((a, b) => a.track_number - b.track_number)
.map((track, index) => (
<div key={track.id} className="group cursor-pointer hover:bg-muted/50 transition-colors">
<div className="flex items-center gap-4 p-4">
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center text-xs font-bold text-muted-foreground group-hover:bg-[#6d28d9] group-hover:text-white transition-colors">
{track.track_number}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-foreground group-hover:text-[#6d28d9] transition-colors truncate">
{track.title}
</h3>
<p className="text-sm text-muted-foreground">{track.artist}</p>
</div>
{track.duration && (
<span className="text-xs font-bold text-muted-foreground">
{track.duration}s
</span>
)}
<Button size="icon" variant="ghost" className="text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
<Play size={18} />
</Button>
</div>
</div>
))}
</div>
</div>
</section>
)}
</div>
</div>
);

View File

@@ -16,6 +16,7 @@ export interface Media {
studios?: string[];
status?: 'watching' | 'completed' | 'planned' | 'dropped' | 'reading' | 'listening' | 'playing' | 'on-hold';
episodes?: Episode[];
tracks?: Track[];
staff?: Staff[];
categories?: string[];
platforms?: string[];
@@ -39,6 +40,15 @@ export interface Episode {
thumbnail: string;
}
export interface Track {
id: number;
media_id: number;
track_number: number;
title: string;
duration: number | null;
artist: string;
}
export interface Staff {
id: string;
name: string;