feat(stats): add library entry deletion and app-wide delete progress

- Add "Delete Entry" to the anime detail view: removes every episode, session, subtitle line, rollup and cover art for a title plus the vocab counts derived from them, then drops it from the Library grid; refused while that title is currently playing.
- Show delete progress (session, session group, episode, entry) app-wide via a top progress bar + status toast that persist across tab switches, detail views, and the stats overlay window instead of going blank off-tab.
- Fix delete and Vocabulary-tab performance: store seen_ms on word/kanji occurrences so deletes subtract only removed rows via a covering index instead of rescanning each word's full history, and paginate vocab rows before aggregating anime counts.
- Migrate existing databases in place on first launch after upgrade.
This commit is contained in:
2026-07-28 02:17:07 -07:00
parent 987b325edb
commit af826837ab
31 changed files with 1508 additions and 91 deletions
@@ -6,6 +6,8 @@ interface AnimeHeaderProps {
anilistEntries: AnilistEntry[];
coverRetryToken?: number;
onChangeAnilist?: () => void;
onDeleteAnime?: () => void;
isDeletingAnime?: boolean;
}
function AnilistButton({ entry }: { entry: AnilistEntry }) {
@@ -32,6 +34,8 @@ export function AnimeHeader({
anilistEntries,
coverRetryToken = 0,
onChangeAnilist,
onDeleteAnime,
isDeletingAnime = false,
}: AnimeHeaderProps) {
const altTitles = [detail.titleRomaji, detail.titleEnglish, detail.titleNative].filter(
(t): t is string => t != null && t !== detail.canonicalTitle,
@@ -95,6 +99,25 @@ export function AnimeHeader({
: 'Link to AniList'}
</button>
)}
{onDeleteAnime && (
<button
type="button"
onClick={onDeleteAnime}
disabled={isDeletingAnime}
title="Delete this title and every session and stat recorded for it"
className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded bg-ctp-surface1 text-ctp-red/80 hover:bg-ctp-red/15 hover:text-ctp-red transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isDeletingAnime ? (
<span
aria-hidden="true"
className="h-3 w-3 animate-spin rounded-full border-2 border-ctp-surface2 border-t-ctp-red"
/>
) : (
<span aria-hidden="true">{'\u2715'}</span>
)}
{isDeletingAnime ? 'Deleting\u2026' : 'Delete Entry'}
</button>
)}
</div>
{detail.description && (
<p className="text-xs text-ctp-subtext0 mt-3 line-clamp-3 leading-relaxed">