Aurora SpotiRip desktop and web app
- 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
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user