From 53a119087dd8d0eae04a5e9cebcea7cdec23ae54 Mon Sep 17 00:00:00 2001 From: ImSavsis Date: Sun, 19 Jul 2026 06:24:02 +0500 Subject: [PATCH] feat: continue watching row on home, synced via account timecodes --- src/renderer/src/lib/api.ts | 4 ++ src/renderer/src/lib/continueWatching.ts | 37 +++++++++++++++++ src/renderer/src/pages/Home.tsx | 51 +++++++++++++++++++++++- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/lib/continueWatching.ts diff --git a/src/renderer/src/lib/api.ts b/src/renderer/src/lib/api.ts index 752c9c7..a3bdd15 100644 --- a/src/renderer/src/lib/api.ts +++ b/src/renderer/src/lib/api.ts @@ -1,6 +1,7 @@ import type { CatalogFilters, CatalogResponse, + Episode, Genre, Release, ScheduleDay, @@ -41,6 +42,9 @@ export const api = { release: (idOrAlias: string | number) => req(`/anime/releases/${idOrAlias}`), + episode: (releaseEpisodeId: string) => + req(`/anime/releases/episodes/${releaseEpisodeId}`), + search: (query: string) => req(`/app/search/releases?query=${encodeURIComponent(query)}`), diff --git a/src/renderer/src/lib/continueWatching.ts b/src/renderer/src/lib/continueWatching.ts new file mode 100644 index 0000000..30d3fce --- /dev/null +++ b/src/renderer/src/lib/continueWatching.ts @@ -0,0 +1,37 @@ +import { api } from './api' +import { getAllLocalProgress } from './localProgress' +import type { Episode, Release } from '../../../shared/types' + +export interface ContinueWatchingItem { + release: Release + episode: Episode + time: number +} + +export async function getContinueWatching(limit = 12): Promise { + const merged = new Map() + + for (const [id, p] of Object.entries(getAllLocalProgress())) merged.set(id, p) + + try { + const remote = await window.anisav.timecodes() + for (const t of remote) merged.set(t.release_episode_id, { time: t.time, is_watched: t.is_watched }) + } catch {} + + const candidates = [...merged.entries()] + .filter(([, p]) => !p.is_watched && p.time > 10) + .slice(0, limit) + + const resolved = await Promise.all( + candidates.map(async ([id, p]) => { + try { + const { release, ...episode } = await api.episode(id) + return { release, episode, time: p.time } + } catch { + return null + } + }) + ) + + return resolved.filter((x): x is ContinueWatchingItem => x !== null) +} diff --git a/src/renderer/src/pages/Home.tsx b/src/renderer/src/pages/Home.tsx index c5690a0..f62ca1b 100644 --- a/src/renderer/src/pages/Home.tsx +++ b/src/renderer/src/pages/Home.tsx @@ -1,20 +1,23 @@ import { useEffect, useState } from 'react' import { motion } from 'framer-motion' +import { Link } from 'react-router-dom' import { api, imageUrl } from '../lib/api' +import { getContinueWatching, ContinueWatchingItem } from '../lib/continueWatching' import type { Release, ScheduleDay } from '../../../shared/types' import Row from '../components/Row' import { GridSkeleton } from '../components/Skeleton' -import { Link } from 'react-router-dom' export default function Home() { const [latest, setLatest] = useState(null) const [recommended, setRecommended] = useState(null) const [today, setToday] = useState(null) + const [continueWatching, setContinueWatching] = useState([]) useEffect(() => { api.latest(21).then(setLatest).catch(() => setLatest([])) api.recommended(14).then(setRecommended).catch(() => setRecommended([])) api.scheduleNow().then((s) => setToday(s.today)).catch(() => setToday([])) + getContinueWatching().then(setContinueWatching).catch(() => setContinueWatching([])) }, []) const hero = latest?.[0] @@ -64,6 +67,52 @@ export default function Home() { )} + {continueWatching.length > 0 && ( +
+

Продолжить просмотр

+ + {continueWatching.map((c) => ( + + +
+ {c.release.name.main} +
+
+

+ Серия {c.episode.ordinal} +

+ {c.episode.duration && ( +
+
+
+ )} +
+
+

+ {c.release.name.main} +

+ + + ))} + +
+ )} + s.release).filter(Boolean) ?? []} />