mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
5-tab React dashboard with Catppuccin Mocha theme: - Overview: hero stats, streak calendar, watch time chart, recent sessions - Anime: grid with cover art, episode list with completion %, detail view - Trends: 15 charts across Activity, Efficiency, Anime, and Patterns - Vocabulary: POS-filtered word/kanji lists with detail panels - Sessions: expandable session history with event timeline Features: - Cross-tab navigation (anime <-> vocabulary) - Global word detail panel overlay - Expandable episode detail with Anki card links (Expression field) - Per-anime multi-line trend charts - Watch time by day-of-week and hour-of-day - Collapsible sections with accessibility (aria-expanded) - Card size selector for anime grid - Cover art caching via AniList - HTTP API client with file:// protocol fallback for Electron overlay
97 lines
4.6 KiB
TypeScript
97 lines
4.6 KiB
TypeScript
import type {
|
|
OverviewData, DailyRollup, MonthlyRollup,
|
|
SessionSummary, SessionTimelinePoint, SessionEvent,
|
|
VocabularyEntry, KanjiEntry,
|
|
VocabularyOccurrenceEntry,
|
|
MediaLibraryItem, MediaDetailData,
|
|
AnimeLibraryItem, AnimeDetailData, AnimeWord,
|
|
StreakCalendarDay, EpisodesPerDay, NewAnimePerDay, WatchTimePerAnime,
|
|
WordDetailData, KanjiDetailData,
|
|
EpisodeDetailData,
|
|
} from '../types/stats';
|
|
|
|
interface StatsElectronAPI {
|
|
stats: {
|
|
getOverview: () => Promise<OverviewData>;
|
|
getDailyRollups: (limit?: number) => Promise<DailyRollup[]>;
|
|
getMonthlyRollups: (limit?: number) => Promise<MonthlyRollup[]>;
|
|
getSessions: (limit?: number) => Promise<SessionSummary[]>;
|
|
getSessionTimeline: (id: number, limit?: number) => Promise<SessionTimelinePoint[]>;
|
|
getSessionEvents: (id: number, limit?: number) => Promise<SessionEvent[]>;
|
|
getVocabulary: (limit?: number) => Promise<VocabularyEntry[]>;
|
|
getWordOccurrences: (
|
|
headword: string,
|
|
word: string,
|
|
reading: string,
|
|
limit?: number,
|
|
offset?: number,
|
|
) => Promise<VocabularyOccurrenceEntry[]>;
|
|
getKanji: (limit?: number) => Promise<KanjiEntry[]>;
|
|
getKanjiOccurrences: (
|
|
kanji: string,
|
|
limit?: number,
|
|
offset?: number,
|
|
) => Promise<VocabularyOccurrenceEntry[]>;
|
|
getMediaLibrary: () => Promise<MediaLibraryItem[]>;
|
|
getMediaDetail: (videoId: number) => Promise<MediaDetailData>;
|
|
getAnimeLibrary: () => Promise<AnimeLibraryItem[]>;
|
|
getAnimeDetail: (animeId: number) => Promise<AnimeDetailData>;
|
|
getAnimeWords: (animeId: number, limit?: number) => Promise<AnimeWord[]>;
|
|
getAnimeRollups: (animeId: number, limit?: number) => Promise<DailyRollup[]>;
|
|
getAnimeCoverUrl: (animeId: number) => string;
|
|
getStreakCalendar: (days?: number) => Promise<StreakCalendarDay[]>;
|
|
getEpisodesPerDay: (limit?: number) => Promise<EpisodesPerDay[]>;
|
|
getNewAnimePerDay: (limit?: number) => Promise<NewAnimePerDay[]>;
|
|
getWatchTimePerAnime: (limit?: number) => Promise<WatchTimePerAnime[]>;
|
|
getWordDetail: (wordId: number) => Promise<WordDetailData>;
|
|
getKanjiDetail: (kanjiId: number) => Promise<KanjiDetailData>;
|
|
getEpisodeDetail: (videoId: number) => Promise<EpisodeDetailData>;
|
|
ankiBrowse: (noteId: number) => Promise<void>;
|
|
ankiNotesInfo: (noteIds: number[]) => Promise<Array<{ noteId: number; fields: Record<string, { value: string }> }>>;
|
|
hideOverlay: () => void;
|
|
};
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
electronAPI?: StatsElectronAPI;
|
|
}
|
|
}
|
|
|
|
function getIpc(): StatsElectronAPI['stats'] {
|
|
const api = window.electronAPI?.stats;
|
|
if (!api) throw new Error('Electron IPC not available');
|
|
return api;
|
|
}
|
|
|
|
export const ipcClient = {
|
|
getOverview: () => getIpc().getOverview(),
|
|
getDailyRollups: (limit = 60) => getIpc().getDailyRollups(limit),
|
|
getMonthlyRollups: (limit = 24) => getIpc().getMonthlyRollups(limit),
|
|
getSessions: (limit = 50) => getIpc().getSessions(limit),
|
|
getSessionTimeline: (id: number, limit = 200) => getIpc().getSessionTimeline(id, limit),
|
|
getSessionEvents: (id: number, limit = 500) => getIpc().getSessionEvents(id, limit),
|
|
getVocabulary: (limit = 100) => getIpc().getVocabulary(limit),
|
|
getWordOccurrences: (headword: string, word: string, reading: string, limit = 50, offset = 0) =>
|
|
getIpc().getWordOccurrences(headword, word, reading, limit, offset),
|
|
getKanji: (limit = 100) => getIpc().getKanji(limit),
|
|
getKanjiOccurrences: (kanji: string, limit = 50, offset = 0) =>
|
|
getIpc().getKanjiOccurrences(kanji, limit, offset),
|
|
getMediaLibrary: () => getIpc().getMediaLibrary(),
|
|
getMediaDetail: (videoId: number) => getIpc().getMediaDetail(videoId),
|
|
getAnimeLibrary: () => getIpc().getAnimeLibrary(),
|
|
getAnimeDetail: (animeId: number) => getIpc().getAnimeDetail(animeId),
|
|
getAnimeWords: (animeId: number, limit = 50) => getIpc().getAnimeWords(animeId, limit),
|
|
getAnimeRollups: (animeId: number, limit = 90) => getIpc().getAnimeRollups(animeId, limit),
|
|
getAnimeCoverUrl: (animeId: number) => getIpc().getAnimeCoverUrl(animeId),
|
|
getStreakCalendar: (days = 90) => getIpc().getStreakCalendar(days),
|
|
getEpisodesPerDay: (limit = 90) => getIpc().getEpisodesPerDay(limit),
|
|
getNewAnimePerDay: (limit = 90) => getIpc().getNewAnimePerDay(limit),
|
|
getWatchTimePerAnime: (limit = 90) => getIpc().getWatchTimePerAnime(limit),
|
|
getWordDetail: (wordId: number) => getIpc().getWordDetail(wordId),
|
|
getKanjiDetail: (kanjiId: number) => getIpc().getKanjiDetail(kanjiId),
|
|
getEpisodeDetail: (videoId: number) => getIpc().getEpisodeDetail(videoId),
|
|
ankiBrowse: (noteId: number) => getIpc().ankiBrowse(noteId),
|
|
ankiNotesInfo: (noteIds: number[]) => getIpc().ankiNotesInfo(noteIds),
|
|
};
|