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:
13
src/api.ts
13
src/api.ts
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
10
src/types.ts
10
src/types.ts
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user