- Spotify playlist, library, album and artist browsing with in-app playback - Song, playlist and album downloads via spotdl with YouTube and Bandcamp fallbacks - Electron desktop app (castLabs Widevine) with a permanent sidebar layout - Linux and Windows packaging scripts
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
Clock3,
|
|
CopyCheck,
|
|
FileAudio,
|
|
Search,
|
|
Trash2,
|
|
} from "lucide-react";
|
|
import { Button } from "../components/Button";
|
|
import { Input } from "../components/Input";
|
|
import {
|
|
clearLibraryHistory,
|
|
listLibraryHistory,
|
|
type LibraryTrackRecord,
|
|
} from "../helpers/libraryIndex";
|
|
import styles from "./history.module.css";
|
|
import { MobileNav } from "../components/MobileNav";
|
|
|
|
export default function HistoryPage() {
|
|
const [records, setRecords] = useState<LibraryTrackRecord[]>([]);
|
|
const [filter, setFilter] = useState("");
|
|
|
|
useEffect(() => {
|
|
listLibraryHistory().then(setRecords).catch(() => setRecords([]));
|
|
}, []);
|
|
|
|
const visible = useMemo(() => {
|
|
const query = filter.trim().toLowerCase();
|
|
if (!query) return records;
|
|
return records.filter((record) =>
|
|
[record.title, record.artist, record.album || "", record.isrc || ""]
|
|
.some((value) => value.toLowerCase().includes(query)),
|
|
);
|
|
}, [records, filter]);
|
|
|
|
async function clearHistory() {
|
|
await clearLibraryHistory();
|
|
setRecords([]);
|
|
}
|
|
|
|
return (
|
|
<main className={styles.page}>
|
|
<header className={styles.topbar}>
|
|
<span>Library History</span>
|
|
</header>
|
|
|
|
<section className={styles.heading}>
|
|
<div>
|
|
<span>LOCAL LIBRARY</span>
|
|
<h1>History & Dedup Index</h1>
|
|
<p>Tracks SpotiRip has repaired or saved on this device, including ISRC identities used for duplicate detection.</p>
|
|
</div>
|
|
<div className={styles.actions}>
|
|
<div className={styles.searchBox}><Search size={15} /><Input value={filter} onChange={(event) => setFilter(event.target.value)} placeholder="Filter history" /></div>
|
|
<Button variant="outline" onClick={clearHistory} disabled={!records.length}><Trash2 size={15} /> Clear history</Button>
|
|
</div>
|
|
</section>
|
|
|
|
<section className={styles.panel}>
|
|
<div className={styles.summary}>
|
|
<div><strong>{records.length}</strong><span>Indexed tracks</span></div>
|
|
<div><strong>{records.filter((record) => record.isrc).length}</strong><span>ISRC identified</span></div>
|
|
<div><strong>{new Set(records.map((record) => record.artist)).size}</strong><span>Artists</span></div>
|
|
</div>
|
|
|
|
<div className={styles.tableHeader}>
|
|
<span>Track</span><span>Album</span><span>ISRC</span><span>Saved</span>
|
|
</div>
|
|
{visible.map((record) => (
|
|
<div className={styles.row} key={record.id}>
|
|
<div className={styles.track}>
|
|
<div className={styles.art}>
|
|
{record.artworkUrl ? <img src={record.artworkUrl} alt="" /> : <FileAudio size={16} />}
|
|
</div>
|
|
<div><strong>{record.title}</strong><span>{record.artist}</span></div>
|
|
</div>
|
|
<span>{record.album || "—"}</span>
|
|
<span className={styles.mono}>{record.isrc || "artist/title key"}</span>
|
|
<div className={styles.saved}>
|
|
<Clock3 size={13} />
|
|
<span>{new Date(record.addedAt).toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{!visible.length ? (
|
|
<div className={styles.empty}>
|
|
<CopyCheck size={24} />
|
|
<strong>{records.length ? "No matching history" : "No local history yet"}</strong>
|
|
<span>Repaired files will appear here and become part of smart duplicate detection.</span>
|
|
</div>
|
|
) : null}
|
|
</section>
|
|
<MobileNav />
|
|
</main>
|
|
);
|
|
}
|