mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-13 13:55:57 -07:00
feat(stats): speed up session maintenance and improve stats UI (#111)
This commit is contained in:
+40
-11
@@ -6,6 +6,7 @@ import type {
|
||||
SessionTimelinePoint,
|
||||
SessionEvent,
|
||||
VocabularyEntry,
|
||||
SentenceSearchResult,
|
||||
KanjiEntry,
|
||||
VocabularyOccurrenceEntry,
|
||||
MediaLibraryItem,
|
||||
@@ -23,7 +24,10 @@ import type {
|
||||
EpisodeDetailData,
|
||||
StatsAnkiNoteInfo,
|
||||
StatsExcludedWord,
|
||||
StatsCoverImagesData,
|
||||
} from '../types/stats';
|
||||
import type { StatsMineCardParams, StatsMineCardResponse } from './mining';
|
||||
import { appendCoverRetryToken } from './cover-retry';
|
||||
|
||||
type StatsLocationLike = Pick<Location, 'protocol' | 'origin' | 'search'>;
|
||||
|
||||
@@ -65,6 +69,16 @@ async function fetchJson<T>(path: string): Promise<T> {
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
function uniquePositiveIds(ids: number[]): number[] {
|
||||
const uniqueIds = new Set<number>();
|
||||
for (const id of ids) {
|
||||
if (Number.isFinite(id) && id > 0) {
|
||||
uniqueIds.add(Math.floor(id));
|
||||
}
|
||||
}
|
||||
return Array.from(uniqueIds).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
export const apiClient = {
|
||||
getOverview: () => fetchJson<OverviewData>('/api/stats/overview'),
|
||||
getDailyRollups: (limit = 60) =>
|
||||
@@ -103,6 +117,14 @@ export const apiClient = {
|
||||
fetchJson<VocabularyOccurrenceEntry[]>(
|
||||
`/api/stats/vocabulary/occurrences?headword=${encodeURIComponent(headword)}&word=${encodeURIComponent(word)}&reading=${encodeURIComponent(reading)}&limit=${limit}&offset=${offset}`,
|
||||
),
|
||||
searchSentences: (query: string, limit = 50, searchByHeadword = true) =>
|
||||
fetchJson<SentenceSearchResult[]>(
|
||||
`/api/stats/sentences/search?${new URLSearchParams({
|
||||
q: query,
|
||||
limit: String(limit),
|
||||
headword: String(searchByHeadword),
|
||||
}).toString()}`,
|
||||
),
|
||||
getKanji: (limit = 100) => fetchJson<KanjiEntry[]>(`/api/stats/kanji?limit=${limit}`),
|
||||
getKanjiOccurrences: (kanji: string, limit = 50, offset = 0) =>
|
||||
fetchJson<VocabularyOccurrenceEntry[]>(
|
||||
@@ -116,7 +138,22 @@ export const apiClient = {
|
||||
fetchJson<AnimeWord[]>(`/api/stats/anime/${animeId}/words?limit=${limit}`),
|
||||
getAnimeRollups: (animeId: number, limit = 90) =>
|
||||
fetchJson<DailyRollup[]>(`/api/stats/anime/${animeId}/rollups?limit=${limit}`),
|
||||
getAnimeCoverUrl: (animeId: number) => `${BASE_URL}/api/stats/anime/${animeId}/cover`,
|
||||
getAnimeCoverUrl: (animeId: number, retryToken = 0) =>
|
||||
appendCoverRetryToken(`${BASE_URL}/api/stats/anime/${animeId}/cover`, retryToken),
|
||||
getCoverImages: async (params: {
|
||||
animeIds: number[];
|
||||
videoIds: number[];
|
||||
}): Promise<StatsCoverImagesData> => {
|
||||
const res = await fetchResponse('/api/stats/covers', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
animeIds: uniquePositiveIds(params.animeIds),
|
||||
videoIds: uniquePositiveIds(params.videoIds),
|
||||
}),
|
||||
});
|
||||
return res.json() as Promise<StatsCoverImagesData>;
|
||||
},
|
||||
getStreakCalendar: (days = 90) =>
|
||||
fetchJson<StreakCalendarDay[]>(`/api/stats/streak-calendar?days=${days}`),
|
||||
getEpisodesPerDay: (limit = 90) =>
|
||||
@@ -175,6 +212,7 @@ export const apiClient = {
|
||||
episodes: number | null;
|
||||
season: string | null;
|
||||
seasonYear: number | null;
|
||||
description: string | null;
|
||||
coverImage: { large: string | null; medium: string | null } | null;
|
||||
title: { romaji: string | null; english: string | null; native: string | null } | null;
|
||||
}>
|
||||
@@ -197,16 +235,7 @@ export const apiClient = {
|
||||
body: JSON.stringify(info),
|
||||
});
|
||||
},
|
||||
mineCard: async (params: {
|
||||
sourcePath: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
sentence: string;
|
||||
word: string;
|
||||
secondaryText?: string | null;
|
||||
videoTitle: string;
|
||||
mode: 'word' | 'sentence' | 'audio';
|
||||
}): Promise<{ noteId?: number; error?: string; errors?: string[] }> => {
|
||||
mineCard: async (params: StatsMineCardParams): Promise<StatsMineCardResponse> => {
|
||||
const res = await fetch(`${BASE_URL}/api/stats/mine-card?mode=${params.mode}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
Reference in New Issue
Block a user