mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-20 14:32:04 +00:00
feat: local sync
feat: video deband
This commit is contained in:
parent
0128ce7efe
commit
14476a19aa
18 changed files with 241 additions and 2863 deletions
|
|
@ -70,6 +70,7 @@
|
|||
"tailwind-variants": "^0.2.1",
|
||||
"uint8-util": "^2.2.5",
|
||||
"urql": "^4.2.1",
|
||||
"video-deband": "^1.0.6",
|
||||
"workbox-core": "^7.3.0",
|
||||
"workbox-precaching": "^7.3.0"
|
||||
}
|
||||
|
|
|
|||
2788
pnpm-lock.yaml
2788
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -16,8 +16,6 @@
|
|||
export let size: NonNullable<$$Props['size']> = 'icon-sm'
|
||||
export let variant: NonNullable<$$Props['variant']> = 'ghost'
|
||||
|
||||
const hasAuth = authAggregator.hasAuth
|
||||
|
||||
function toggleBookmark () {
|
||||
if (!media.mediaListEntry?.status) {
|
||||
authAggregator.entry({ id: media.id, status: 'PLANNING', lists: lists(media)?.filter(({ enabled }) => enabled).map(({ name }) => name) })
|
||||
|
|
@ -27,6 +25,6 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Button {size} {variant} class={className} disabled={!$hasAuth} on:click={clickwrap(toggleBookmark)} on:keydown={keywrap(toggleBookmark)}>
|
||||
<Button {size} {variant} class={className} on:click={clickwrap(toggleBookmark)} on:keydown={keywrap(toggleBookmark)}>
|
||||
<Bookmark fill={list(media) ? 'currentColor' : 'transparent'} size={iconSizes[size]} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,8 @@
|
|||
export let media: Media
|
||||
export let size: NonNullable<$$Props['size']> = 'icon-sm'
|
||||
export let variant: NonNullable<$$Props['variant']> = 'ghost'
|
||||
|
||||
const hasAuth = authAggregator.hasAuth
|
||||
</script>
|
||||
|
||||
<Button {size} {variant} class={className} disabled={!$hasAuth} on:click={clickwrap(() => authAggregator.toggleFav(media.id))} on:keydown={keywrap(() => authAggregator.toggleFav(media.id))}>
|
||||
<Button {size} {variant} class={className} on:click={clickwrap(() => authAggregator.toggleFav(media.id))} on:keydown={keywrap(() => authAggregator.toggleFav(media.id))}>
|
||||
<Heart fill={fav(media) ? 'currentColor' : 'transparent'} size={iconSizes[size]} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@
|
|||
import { persisted } from 'svelte-persisted-store'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { loadWithDefaults } from 'svelte-keybinds'
|
||||
import VideoDeband from 'video-deband'
|
||||
|
||||
import Seekbar from './seekbar.svelte'
|
||||
import { autoPiP, burnIn, getChapterTitle, sanitizeChapters, type Chapter, type MediaInfo } from './util'
|
||||
|
|
@ -80,7 +81,7 @@
|
|||
}
|
||||
|
||||
function pip (enable = !$pictureInPictureElement) {
|
||||
return enable ? burnIn(video, subtitles) : document.exitPictureInPicture()
|
||||
return enable ? burnIn(video, subtitles, deband) : document.exitPictureInPicture()
|
||||
}
|
||||
|
||||
function toggleCast () {
|
||||
|
|
@ -180,7 +181,6 @@
|
|||
let animations: Animation[] = []
|
||||
|
||||
const thumbnailer = new Thumbnailer(mediaInfo.file.url)
|
||||
$: thumbnailer.updateSource(mediaInfo.file.url)
|
||||
onMount(() => thumbnailer.setVideo(video))
|
||||
|
||||
let chapters: Chapter[] = []
|
||||
|
|
@ -191,14 +191,31 @@
|
|||
$: loadChapters(chaptersPromise, safeduration)
|
||||
|
||||
let subtitles: Subs | undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
$: if (video && !subtitles) {
|
||||
|
||||
function createSubtitles (video: HTMLVideoElement) {
|
||||
subtitles = new Subs(video, files, mediaInfo.file)
|
||||
return {
|
||||
destroy: () => {
|
||||
subtitles?.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let deband: VideoDeband | undefined
|
||||
|
||||
function createDeband (video: HTMLVideoElement, playerDeband: boolean) {
|
||||
if (!playerDeband) return
|
||||
deband = new VideoDeband(video)
|
||||
deband.canvas.classList.add('deband-canvas', 'w-full', 'h-full', 'grow', 'pointer-events-none', 'object-contain')
|
||||
video.before(deband.canvas)
|
||||
return {
|
||||
destroy: () => {
|
||||
deband?.destroy()
|
||||
deband?.canvas.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (subtitles) subtitles.destroy()
|
||||
})
|
||||
// other
|
||||
|
||||
let currentSkippable: string | null = null
|
||||
|
|
@ -493,8 +510,11 @@
|
|||
|
||||
<svelte:document bind:fullscreenElement use:bindPiP={pictureInPictureElement} />
|
||||
|
||||
<div class='w-full h-full relative content-center fullscreen:bg-black overflow-clip text-left' bind:this={wrapper}>
|
||||
<video class='w-full h-full grow bg-black' preload='auto' class:cursor-none={immersed} class:cursor-pointer={isMiniplayer} class:object-cover={fitWidth}
|
||||
<div class='w-full h-full relative content-center fullscreen:bg-black overflow-clip text-left' class:fitWidth bind:this={wrapper}>
|
||||
<video class='w-full h-full grow bg-black' preload='auto' class:cursor-none={immersed} class:cursor-pointer={isMiniplayer} class:object-cover={fitWidth} class:opacity-0={deband} class:absolute={deband} class:top-0={deband}
|
||||
use:createDeband={$settings.playerDeband}
|
||||
use:createSubtitles
|
||||
use:autoPiP={pip}
|
||||
crossorigin='anonymous'
|
||||
src={mediaInfo.file.url}
|
||||
bind:videoHeight
|
||||
|
|
@ -513,7 +533,6 @@
|
|||
on:dblclick={fullscreen}
|
||||
on:loadeddata={checkAudio}
|
||||
on:timeupdate={checkSkippableChapters}
|
||||
use:autoPiP={pip}
|
||||
/>
|
||||
<div class='absolute w-full h-full flex items-center justify-center top-0 pointer-events-none'>
|
||||
{#if seeking}
|
||||
|
|
@ -652,6 +671,9 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.fitWidth :global(.deband-canvas) {
|
||||
object-fit: cover !important;
|
||||
}
|
||||
.gradient {
|
||||
background: linear-gradient(to top, oklab(0 0 0 / 0.85) 0%, oklab(0 0 0 / 0.7) 35%, oklab(0 0 0 / 0) 100%);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { Media } from '$lib/modules/anilist'
|
|||
import type { Track } from '../../../../app'
|
||||
import type Subtitles from './subtitles'
|
||||
import type { ResolvedFile } from './resolver'
|
||||
import type VideoDeband from 'video-deband'
|
||||
|
||||
import { settings } from '$lib/modules/settings'
|
||||
|
||||
|
|
@ -198,7 +199,8 @@ export function autoPiP (video: HTMLVideoElement, pipfn: (enable: boolean) => vo
|
|||
}
|
||||
}
|
||||
|
||||
export function burnIn (video: HTMLVideoElement, subtitles?: Subtitles) {
|
||||
// TODO: de-shittify this code, abort signal, play/pause sync
|
||||
export function burnIn (video: HTMLVideoElement, subtitles?: Subtitles, deband?: VideoDeband) {
|
||||
if (!subtitles?.renderer) return video.requestPictureInPicture()
|
||||
|
||||
const canvasVideo = document.createElement('video')
|
||||
|
|
@ -211,8 +213,7 @@ export function burnIn (video: HTMLVideoElement, subtitles?: Subtitles) {
|
|||
canvas.height = video.videoHeight
|
||||
subtitles.renderer.resize(video.videoWidth, video.videoHeight)
|
||||
const renderFrame = () => {
|
||||
// context.drawImage(deband ? deband.canvas : video, 0, 0)
|
||||
context.drawImage(video, 0, 0)
|
||||
context.drawImage(deband?.canvas ?? video, 0, 0)
|
||||
// @ts-expect-error internal call on canvas
|
||||
if (canvas.width && canvas.height && subtitles.renderer?._canvas) context.drawImage(subtitles.renderer._canvas, 0, 0, canvas.width, canvas.height)
|
||||
loop = video.requestVideoFrameCallback(renderFrame)
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@
|
|||
export const CHANGELOG_URL = 'https://api.github.com/repos/ThaUnknown/miru/releases'
|
||||
export const WEB_URL = 'https://miru.watch'
|
||||
export const DEFAULT_EXTENSIONS = 'gh:hayase-app/extensions'
|
||||
export const SETUP_VERSION = 1
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class AnilistClient {
|
|||
if (!list?.entries) return list
|
||||
return {
|
||||
...list,
|
||||
entries: list.entries.filter(entry => entry?.id !== id)
|
||||
entries: list.entries.filter(entry => entry?.media?.mediaListEntry?.id !== id)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -101,15 +101,15 @@ class AnilistClient {
|
|||
|
||||
const entry = result.SaveMediaListEntry
|
||||
|
||||
if (entry?.customLists) entry.customLists = (entry.customLists as string[]).map(name => ({ enabled: true, name }))
|
||||
if (entry?.media?.mediaListEntry?.customLists) entry.media.mediaListEntry.customLists = (entry.media.mediaListEntry.customLists as string[]).map(name => ({ enabled: true, name }))
|
||||
cache.writeFragment(media, {
|
||||
id: mediaId as number,
|
||||
mediaListEntry: entry
|
||||
mediaListEntry: entry?.media?.mediaListEntry ?? null
|
||||
})
|
||||
cache.updateQuery({ query: UserLists, variables: { id: this.viewer.value?.viewer?.id } }, data => {
|
||||
if (!data?.MediaListCollection?.lists) return data
|
||||
const oldLists = data.MediaListCollection.lists
|
||||
const oldEntry = oldLists.flatMap(list => list?.entries).find(entry => entry?.media?.id === mediaId) ?? result.SaveMediaListEntry
|
||||
const oldEntry = oldLists.flatMap(list => list?.entries).find(entry => entry?.media?.id === mediaId)
|
||||
|
||||
const lists = oldLists.map(list => {
|
||||
if (!list?.entries) return list
|
||||
|
|
@ -119,7 +119,7 @@ class AnilistClient {
|
|||
}
|
||||
})
|
||||
|
||||
const status = result.SaveMediaListEntry?.status ?? oldEntry?.status ?? 'PLANNING' as const
|
||||
const status = result.SaveMediaListEntry?.media?.mediaListEntry?.status ?? oldEntry?.media?.mediaListEntry?.status ?? 'PLANNING' as const
|
||||
|
||||
const fallback: NonNullable<typeof oldLists[0]> = { status, entries: [] }
|
||||
let targetList = lists.find(list => list?.status === status)
|
||||
|
|
@ -187,8 +187,7 @@ class AnilistClient {
|
|||
MediaTitle: () => null,
|
||||
MediaCoverImage: () => null,
|
||||
AiringSchedule: () => null,
|
||||
// @ts-expect-error idk
|
||||
MediaListCollection: e => e.user?.id as string | null,
|
||||
MediaListCollection: e => (e.user as any)?.id as string | null,
|
||||
MediaListGroup: () => null,
|
||||
UserAvatar: () => null
|
||||
}
|
||||
|
|
@ -302,7 +301,7 @@ class AnilistClient {
|
|||
|
||||
const ids = mediaList.filter(entry => {
|
||||
if (entry?.media?.status === 'FINISHED') return true
|
||||
const progress = entry?.progress ?? 0
|
||||
const progress = entry?.media?.mediaListEntry?.progress ?? 0
|
||||
return progress < (entry?.media?.nextAiringEpisode?.episode ?? (progress + 2)) - 1
|
||||
}).map(entry => entry?.media?.id) as number[]
|
||||
|
||||
|
|
@ -386,8 +385,8 @@ class AnilistClient {
|
|||
return Object.entries(searchResults).map(([filename, id]) => [filename, search.data!.Page!.media!.find(media => media!.id === id)]) as Array<[string, Media | undefined]>
|
||||
}
|
||||
|
||||
schedule () {
|
||||
return queryStore({ client: this.client, query: Schedule, variables: { seasonCurrent: currentSeason, seasonYearCurrent: currentYear, seasonLast: lastSeason, seasonYearLast: lastYear, seasonNext: nextSeason, seasonYearNext: nextYear }, pause: true })
|
||||
schedule (ids?: number[]) {
|
||||
return queryStore({ client: this.client, query: Schedule, variables: { ids, seasonCurrent: currentSeason, seasonYearCurrent: currentYear, seasonLast: lastSeason, seasonYearLast: lastYear, seasonNext: nextSeason, seasonYearNext: nextYear }, pause: true })
|
||||
}
|
||||
|
||||
async toggleFav (id: number) {
|
||||
|
|
|
|||
28
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
28
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
|
|
@ -12,26 +12,26 @@ declare module 'gql.tada' {
|
|||
TadaDocumentNode<{ id: number; idMal: number | null; title: { romaji: string | null; english: string | null; native: string | null; userPreferred: string | null; } | null; description: string | null; season: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null; seasonYear: number | null; format: "MANGA" | "TV" | "TV_SHORT" | "MOVIE" | "SPECIAL" | "OVA" | "ONA" | "MUSIC" | "NOVEL" | "ONE_SHOT" | null; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; episodes: number | null; duration: number | null; averageScore: number | null; genres: (string | null)[] | null; isFavourite: boolean; coverImage: { extraLarge: string | null; medium: string | null; color: string | null; } | null; source: "OTHER" | "ANIME" | "MANGA" | "NOVEL" | "ORIGINAL" | "LIGHT_NOVEL" | "VISUAL_NOVEL" | "VIDEO_GAME" | "DOUJINSHI" | "WEB_NOVEL" | "LIVE_ACTION" | "GAME" | "COMIC" | "MULTIMEDIA_PROJECT" | "PICTURE_BOOK" | null; countryOfOrigin: unknown; isAdult: boolean | null; bannerImage: string | null; synonyms: (string | null)[] | null; nextAiringEpisode: { id: number; timeUntilAiring: number; episode: number; } | null; startDate: { year: number | null; month: number | null; day: number | null; } | null; trailer: { id: string | null; site: string | null; } | null; mediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; } | null; studios: { nodes: ({ id: number; name: string; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; title: { userPreferred: string | null; } | null; coverImage: { medium: string | null; } | null; type: "ANIME" | "MANGA" | null; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; format: "MANGA" | "TV" | "TV_SHORT" | "MOVIE" | "SPECIAL" | "OVA" | "ONA" | "MUSIC" | "NOVEL" | "ONE_SHOT" | null; episodes: number | null; synonyms: (string | null)[] | null; season: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null; seasonYear: number | null; startDate: { year: number | null; month: number | null; day: number | null; } | null; endDate: { year: number | null; month: number | null; day: number | null; } | null; } | null; } | null)[] | null; } | null; }, {}, { fragment: "FullMedia"; on: "Media"; masked: false; }>;
|
||||
"\n query Search($page: Int, $perPage: Int, $search: String, $genre: [String], $format: [MediaFormat], $status: [MediaStatus], $statusNot: [MediaStatus], $season: MediaSeason, $seasonYear: Int, $isAdult: Boolean, $sort: [MediaSort], $onList: Boolean, $ids: [Int]) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n hasNextPage\n },\n media(type: ANIME, format_not: MUSIC, id_in: $ids, search: $search, genre_in: $genre, format_in: $format, status_in: $status, status_not_in: $statusNot, season: $season, seasonYear: $seasonYear, isAdult: $isAdult, sort: $sort, onList: $onList) {\n ...FullMedia\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ Page: { pageInfo: { hasNextPage: boolean | null; } | null; media: ({ id: number; idMal: number | null; title: { romaji: string | null; english: string | null; native: string | null; userPreferred: string | null; } | null; description: string | null; season: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null; seasonYear: number | null; format: "MANGA" | "TV" | "TV_SHORT" | "MOVIE" | "SPECIAL" | "OVA" | "ONA" | "MUSIC" | "NOVEL" | "ONE_SHOT" | null; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; episodes: number | null; duration: number | null; averageScore: number | null; genres: (string | null)[] | null; isFavourite: boolean; coverImage: { extraLarge: string | null; medium: string | null; color: string | null; } | null; source: "OTHER" | "ANIME" | "MANGA" | "NOVEL" | "ORIGINAL" | "LIGHT_NOVEL" | "VISUAL_NOVEL" | "VIDEO_GAME" | "DOUJINSHI" | "WEB_NOVEL" | "LIVE_ACTION" | "GAME" | "COMIC" | "MULTIMEDIA_PROJECT" | "PICTURE_BOOK" | null; countryOfOrigin: unknown; isAdult: boolean | null; bannerImage: string | null; synonyms: (string | null)[] | null; nextAiringEpisode: { id: number; timeUntilAiring: number; episode: number; } | null; startDate: { year: number | null; month: number | null; day: number | null; } | null; trailer: { id: string | null; site: string | null; } | null; mediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; } | null; studios: { nodes: ({ id: number; name: string; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; title: { userPreferred: string | null; } | null; coverImage: { medium: string | null; } | null; type: "ANIME" | "MANGA" | null; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; format: "MANGA" | "TV" | "TV_SHORT" | "MOVIE" | "SPECIAL" | "OVA" | "ONA" | "MUSIC" | "NOVEL" | "ONE_SHOT" | null; episodes: number | null; synonyms: (string | null)[] | null; season: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null; seasonYear: number | null; startDate: { year: number | null; month: number | null; day: number | null; } | null; endDate: { year: number | null; month: number | null; day: number | null; } | null; } | null; } | null)[] | null; } | null; } | null)[] | null; } | null; }, { ids?: (number | null)[] | null | undefined; onList?: boolean | null | undefined; sort?: ("ID" | "ID_DESC" | "TITLE_ROMAJI" | "TITLE_ROMAJI_DESC" | "TITLE_ENGLISH" | "TITLE_ENGLISH_DESC" | "TITLE_NATIVE" | "TITLE_NATIVE_DESC" | "TYPE" | "TYPE_DESC" | "FORMAT" | "FORMAT_DESC" | "START_DATE" | "START_DATE_DESC" | "END_DATE" | "END_DATE_DESC" | "SCORE" | "SCORE_DESC" | "POPULARITY" | "POPULARITY_DESC" | "TRENDING" | "TRENDING_DESC" | "EPISODES" | "EPISODES_DESC" | "DURATION" | "DURATION_DESC" | "STATUS" | "STATUS_DESC" | "CHAPTERS" | "CHAPTERS_DESC" | "VOLUMES" | "VOLUMES_DESC" | "UPDATED_AT" | "UPDATED_AT_DESC" | "SEARCH_MATCH" | "FAVOURITES" | "FAVOURITES_DESC" | null)[] | null | undefined; isAdult?: boolean | null | undefined; seasonYear?: number | null | undefined; season?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; statusNot?: ("FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null)[] | null | undefined; status?: ("FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null)[] | null | undefined; format?: ("MANGA" | "TV" | "TV_SHORT" | "MOVIE" | "SPECIAL" | "OVA" | "ONA" | "MUSIC" | "NOVEL" | "ONE_SHOT" | null)[] | null | undefined; genre?: (string | null)[] | null | undefined; search?: string | null | undefined; perPage?: number | null | undefined; page?: number | null | undefined; }, void>;
|
||||
"\n query IDMedia($id: Int) {\n Media(id: $id, type: ANIME) {\n ...FullMedia\n }\n }\n":
|
||||
TadaDocumentNode<{ Media: { id: number; idMal: number | null; title: { romaji: string | null; english: string | null; native: string | null; userPreferred: string | null; } | null; description: string | null; season: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null; seasonYear: number | null; format: "MANGA" | "TV" | "TV_SHORT" | "MOVIE" | "SPECIAL" | "OVA" | "ONA" | "MUSIC" | "NOVEL" | "ONE_SHOT" | null; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; episodes: number | null; duration: number | null; averageScore: number | null; genres: (string | null)[] | null; isFavourite: boolean; coverImage: { extraLarge: string | null; medium: string | null; color: string | null; } | null; source: "OTHER" | "ANIME" | "MANGA" | "NOVEL" | "ORIGINAL" | "LIGHT_NOVEL" | "VISUAL_NOVEL" | "VIDEO_GAME" | "DOUJINSHI" | "WEB_NOVEL" | "LIVE_ACTION" | "GAME" | "COMIC" | "MULTIMEDIA_PROJECT" | "PICTURE_BOOK" | null; countryOfOrigin: unknown; isAdult: boolean | null; bannerImage: string | null; synonyms: (string | null)[] | null; nextAiringEpisode: { id: number; timeUntilAiring: number; episode: number; } | null; startDate: { year: number | null; month: number | null; day: number | null; } | null; trailer: { id: string | null; site: string | null; } | null; mediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; } | null; studios: { nodes: ({ id: number; name: string; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; title: { userPreferred: string | null; } | null; coverImage: { medium: string | null; } | null; type: "ANIME" | "MANGA" | null; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; format: "MANGA" | "TV" | "TV_SHORT" | "MOVIE" | "SPECIAL" | "OVA" | "ONA" | "MUSIC" | "NOVEL" | "ONE_SHOT" | null; episodes: number | null; synonyms: (string | null)[] | null; season: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null; seasonYear: number | null; startDate: { year: number | null; month: number | null; day: number | null; } | null; endDate: { year: number | null; month: number | null; day: number | null; } | null; } | null; } | null)[] | null; } | null; } | null; }, { id?: number | null | undefined; }, void>;
|
||||
"\n query IDMedia($id: Int!) {\n Media(id: $id, type: ANIME) {\n ...FullMedia\n }\n }\n":
|
||||
TadaDocumentNode<{ Media: { id: number; idMal: number | null; title: { romaji: string | null; english: string | null; native: string | null; userPreferred: string | null; } | null; description: string | null; season: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null; seasonYear: number | null; format: "MANGA" | "TV" | "TV_SHORT" | "MOVIE" | "SPECIAL" | "OVA" | "ONA" | "MUSIC" | "NOVEL" | "ONE_SHOT" | null; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; episodes: number | null; duration: number | null; averageScore: number | null; genres: (string | null)[] | null; isFavourite: boolean; coverImage: { extraLarge: string | null; medium: string | null; color: string | null; } | null; source: "OTHER" | "ANIME" | "MANGA" | "NOVEL" | "ORIGINAL" | "LIGHT_NOVEL" | "VISUAL_NOVEL" | "VIDEO_GAME" | "DOUJINSHI" | "WEB_NOVEL" | "LIVE_ACTION" | "GAME" | "COMIC" | "MULTIMEDIA_PROJECT" | "PICTURE_BOOK" | null; countryOfOrigin: unknown; isAdult: boolean | null; bannerImage: string | null; synonyms: (string | null)[] | null; nextAiringEpisode: { id: number; timeUntilAiring: number; episode: number; } | null; startDate: { year: number | null; month: number | null; day: number | null; } | null; trailer: { id: string | null; site: string | null; } | null; mediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; } | null; studios: { nodes: ({ id: number; name: string; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; title: { userPreferred: string | null; } | null; coverImage: { medium: string | null; } | null; type: "ANIME" | "MANGA" | null; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; format: "MANGA" | "TV" | "TV_SHORT" | "MOVIE" | "SPECIAL" | "OVA" | "ONA" | "MUSIC" | "NOVEL" | "ONE_SHOT" | null; episodes: number | null; synonyms: (string | null)[] | null; season: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null; seasonYear: number | null; startDate: { year: number | null; month: number | null; day: number | null; } | null; endDate: { year: number | null; month: number | null; day: number | null; } | null; } | null; } | null)[] | null; } | null; } | null; }, { id: number; }, void>;
|
||||
"\n query Viewer {\n Viewer {\n avatar {\n medium\n },\n name,\n id,\n mediaListOptions {\n animeList {\n customLists\n }\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ Viewer: { avatar: { medium: string | null; } | null; name: string; id: number; mediaListOptions: { animeList: { customLists: (string | null)[] | null; } | null; } | null; } | null; }, {}, void>;
|
||||
"\n query UserLists($id: Int) {\n MediaListCollection(userId: $id, type: ANIME, forceSingleCompletedList: true, sort: UPDATED_TIME_DESC) {\n user {\n id\n }\n lists {\n status,\n entries {\n ...FullMediaList,\n media {\n id,\n status,\n nextAiringEpisode {\n episode\n },\n relations {\n edges {\n relationType(version:2)\n node {\n id\n }\n }\n }\n }\n }\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ MediaListCollection: { user: { id: number; } | null; lists: ({ status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; entries: ({ id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; media: { id: number; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; nextAiringEpisode: { episode: number; } | null; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; } | null; } | null)[] | null; } | null; } | null; } | null)[] | null; } | null)[] | null; } | null; }, { id?: number | null | undefined; }, void>;
|
||||
"\n query UserLists($id: Int) {\n MediaListCollection(userId: $id, type: ANIME, forceSingleCompletedList: true, sort: UPDATED_TIME_DESC) {\n user {\n id\n }\n lists {\n status,\n entries {\n id,\n media {\n id,\n status,\n mediaListEntry {\n ...FullMediaList\n },\n nextAiringEpisode {\n episode\n },\n relations {\n edges {\n relationType(version:2)\n node {\n id\n }\n }\n }\n }\n }\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ MediaListCollection: { user: { id: number; } | null; lists: ({ status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; entries: ({ id: number; media: { id: number; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; mediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; } | null; nextAiringEpisode: { episode: number; } | null; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; } | null; } | null)[] | null; } | null; } | null; } | null)[] | null; } | null)[] | null; } | null; }, { id?: number | null | undefined; }, void>;
|
||||
"\n mutation CustomLists($lists: [String]) {\n UpdateUser(animeListOptions: { customLists: $lists }) {\n id\n }\n }\n":
|
||||
TadaDocumentNode<{ UpdateUser: { id: number; } | null; }, { lists?: (string | null)[] | null | undefined; }, void>;
|
||||
"\n fragment ScheduleMedia on Media @_unmask {\n id,\n title {\n userPreferred\n }\n mediaListEntry {\n status\n }\n aired: airingSchedule(page: 1, perPage: 50, notYetAired: false) {\n n: nodes {\n a: airingAt,\n e: episode\n }\n },\n notaired: airingSchedule(page: 1, perPage: 50, notYetAired: true) {\n n: nodes {\n a: airingAt,\n e: episode\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; }, {}, { fragment: "ScheduleMedia"; on: "Media"; masked: false; }>;
|
||||
"\n query Schedule($seasonCurrent: MediaSeason, $seasonYearCurrent: Int, $seasonLast: MediaSeason, $seasonYearLast: Int, $seasonNext: MediaSeason, $seasonYearNext: Int) {\n curr1: Page(page: 1) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {\n ...ScheduleMedia\n }\n }\n curr2: Page(page: 2) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {\n ...ScheduleMedia\n }\n }\n curr3: Page(page: 3) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {\n ...ScheduleMedia\n }\n }\n residue: Page(page: 1) {\n media(type: ANIME, season: $seasonLast, seasonYear: $seasonYearLast, episodes_greater: 16, countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {\n ...ScheduleMedia\n }\n },\n next1: Page(page: 1) {\n media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {\n ...ScheduleMedia\n }\n },\n next2: Page(page: 2) {\n media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {\n ...ScheduleMedia\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ curr1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr3: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; residue: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; }, { seasonYearNext?: number | null | undefined; seasonNext?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearLast?: number | null | undefined; seasonLast?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearCurrent?: number | null | undefined; seasonCurrent?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; }, void>;
|
||||
"\n query Following($id: Int) {\n Page {\n mediaList(mediaId: $id, isFollowing: true, sort: UPDATED_TIME_DESC) {\n id,\n status,\n score,\n progress,\n user {\n id,\n name,\n avatar {\n medium\n }\n }\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ Page: { mediaList: ({ id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; score: number | null; progress: number | null; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; } | null)[] | null; } | null; }, { id?: number | null | undefined; }, void>;
|
||||
"\n mutation Entry($lists: [String], $id: Int, $status: MediaListStatus, $progress: Int, $repeat: Int, $score: Int) {\n SaveMediaListEntry(mediaId: $id, status: $status, progress: $progress, repeat: $repeat, scoreRaw: $score, customLists: $lists) {\n ...FullMediaList\n media {\n id,\n status,\n nextAiringEpisode {\n episode\n }\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ SaveMediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; media: { id: number; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; nextAiringEpisode: { episode: number; } | null; } | null; } | null; }, { score?: number | null | undefined; repeat?: number | null | undefined; progress?: number | null | undefined; status?: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null | undefined; id?: number | null | undefined; lists?: (string | null)[] | null | undefined; }, void>;
|
||||
"\n mutation DeleteEntry($id: Int) {\n DeleteMediaListEntry(id: $id) {\n deleted\n }\n }\n":
|
||||
TadaDocumentNode<{ DeleteMediaListEntry: { deleted: boolean | null; } | null; }, { id?: number | null | undefined; }, void>;
|
||||
"\n mutation ToggleFavourite($id: Int) {\n ToggleFavourite(animeId: $id) { anime { nodes { id } } } \n }\n":
|
||||
TadaDocumentNode<{ ToggleFavourite: { anime: { nodes: ({ id: number; } | null)[] | null; } | null; } | null; }, { id?: number | null | undefined; }, void>;
|
||||
"\n query Schedule($seasonCurrent: MediaSeason, $seasonYearCurrent: Int, $seasonLast: MediaSeason, $seasonYearLast: Int, $seasonNext: MediaSeason, $seasonYearNext: Int, $ids: [Int]) {\n curr1: Page(page: 1) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n curr2: Page(page: 2) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n curr3: Page(page: 3) {\n media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n residue: Page(page: 1) {\n media(type: ANIME, season: $seasonLast, seasonYear: $seasonYearLast, episodes_greater: 16, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n },\n next1: Page(page: 1) {\n media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n },\n next2: Page(page: 2) {\n media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {\n ...ScheduleMedia\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ curr1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; curr3: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; residue: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next1: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; next2: { media: ({ id: number; title: { userPreferred: string | null; } | null; mediaListEntry: { status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; } | null; aired: { n: ({ a: number; e: number; } | null)[] | null; } | null; notaired: { n: ({ a: number; e: number; } | null)[] | null; } | null; } | null)[] | null; } | null; }, { ids?: (number | null)[] | null | undefined; seasonYearNext?: number | null | undefined; seasonNext?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearLast?: number | null | undefined; seasonLast?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; seasonYearCurrent?: number | null | undefined; seasonCurrent?: "WINTER" | "SPRING" | "SUMMER" | "FALL" | null | undefined; }, void>;
|
||||
"\n query Following($id: Int!) {\n Page {\n mediaList(mediaId: $id, isFollowing: true, sort: UPDATED_TIME_DESC) {\n id,\n status,\n score,\n progress,\n user {\n id,\n name,\n avatar {\n medium\n }\n }\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ Page: { mediaList: ({ id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; score: number | null; progress: number | null; user: { id: number; name: string; avatar: { medium: string | null; } | null; } | null; } | null)[] | null; } | null; }, { id: number; }, void>;
|
||||
"\n mutation Entry($lists: [String], $id: Int!, $status: MediaListStatus, $progress: Int, $repeat: Int, $score: Int) {\n SaveMediaListEntry(mediaId: $id, status: $status, progress: $progress, repeat: $repeat, scoreRaw: $score, customLists: $lists) {\n id,\n media {\n id,\n status,\n mediaListEntry {\n ...FullMediaList\n },\n nextAiringEpisode {\n episode\n }\n }\n }\n }\n":
|
||||
TadaDocumentNode<{ SaveMediaListEntry: { id: number; media: { id: number; status: "FINISHED" | "RELEASING" | "NOT_YET_RELEASED" | "CANCELLED" | "HIATUS" | null; mediaListEntry: { id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; } | null; nextAiringEpisode: { episode: number; } | null; } | null; } | null; }, { score?: number | null | undefined; repeat?: number | null | undefined; progress?: number | null | undefined; status?: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null | undefined; id: number; lists?: (string | null)[] | null | undefined; }, void>;
|
||||
"\n mutation DeleteEntry($id: Int!) {\n DeleteMediaListEntry(id: $id) {\n deleted\n }\n }\n":
|
||||
TadaDocumentNode<{ DeleteMediaListEntry: { deleted: boolean | null; } | null; }, { id: number; }, void>;
|
||||
"\n mutation ToggleFavourite($id: Int!) {\n ToggleFavourite(animeId: $id) { anime { nodes { id } } } \n }\n":
|
||||
TadaDocumentNode<{ ToggleFavourite: { anime: { nodes: ({ id: number; } | null)[] | null; } | null; } | null; }, { id: number; }, void>;
|
||||
"fragment Med on Media {id, isFavourite}":
|
||||
TadaDocumentNode<{ id: number; isFavourite: boolean; }, {}, { fragment: "Med"; on: "Media"; masked: true; }>;
|
||||
"fragment Med on Media {id, mediaListEntry {status, progress, repeat, score, customLists }}":
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export const Search = gql(`
|
|||
`, [FullMedia])
|
||||
|
||||
export const IDMedia = gql(`
|
||||
query IDMedia($id: Int) {
|
||||
query IDMedia($id: Int!) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
...FullMedia
|
||||
}
|
||||
|
|
@ -158,10 +158,13 @@ export const UserLists = gql(`
|
|||
lists {
|
||||
status,
|
||||
entries {
|
||||
...FullMediaList,
|
||||
id,
|
||||
media {
|
||||
id,
|
||||
status,
|
||||
mediaListEntry {
|
||||
...FullMediaList
|
||||
},
|
||||
nextAiringEpisode {
|
||||
episode
|
||||
},
|
||||
|
|
@ -213,34 +216,34 @@ export const ScheduleMedia = gql(`
|
|||
`)
|
||||
|
||||
export const Schedule = gql(`
|
||||
query Schedule($seasonCurrent: MediaSeason, $seasonYearCurrent: Int, $seasonLast: MediaSeason, $seasonYearLast: Int, $seasonNext: MediaSeason, $seasonYearNext: Int) {
|
||||
query Schedule($seasonCurrent: MediaSeason, $seasonYearCurrent: Int, $seasonLast: MediaSeason, $seasonYearLast: Int, $seasonNext: MediaSeason, $seasonYearNext: Int, $ids: [Int]) {
|
||||
curr1: Page(page: 1) {
|
||||
media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {
|
||||
media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {
|
||||
...ScheduleMedia
|
||||
}
|
||||
}
|
||||
curr2: Page(page: 2) {
|
||||
media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {
|
||||
media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {
|
||||
...ScheduleMedia
|
||||
}
|
||||
}
|
||||
curr3: Page(page: 3) {
|
||||
media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {
|
||||
media(type: ANIME, season: $seasonCurrent, seasonYear: $seasonYearCurrent, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {
|
||||
...ScheduleMedia
|
||||
}
|
||||
}
|
||||
residue: Page(page: 1) {
|
||||
media(type: ANIME, season: $seasonLast, seasonYear: $seasonYearLast, episodes_greater: 16, countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {
|
||||
media(type: ANIME, season: $seasonLast, seasonYear: $seasonYearLast, episodes_greater: 16, countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {
|
||||
...ScheduleMedia
|
||||
}
|
||||
},
|
||||
next1: Page(page: 1) {
|
||||
media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {
|
||||
media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {
|
||||
...ScheduleMedia
|
||||
}
|
||||
},
|
||||
next2: Page(page: 2) {
|
||||
media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true) {
|
||||
media(type: ANIME, season: $seasonNext, seasonYear: $seasonYearNext, sort: [START_DATE], countryOfOrigin: JP, format_not: TV_SHORT, onList: true, id_in: $ids) {
|
||||
...ScheduleMedia
|
||||
}
|
||||
}
|
||||
|
|
@ -248,7 +251,7 @@ export const Schedule = gql(`
|
|||
`, [ScheduleMedia])
|
||||
|
||||
export const Following = gql(`
|
||||
query Following($id: Int) {
|
||||
query Following($id: Int!) {
|
||||
Page {
|
||||
mediaList(mediaId: $id, isFollowing: true, sort: UPDATED_TIME_DESC) {
|
||||
id,
|
||||
|
|
@ -268,12 +271,15 @@ export const Following = gql(`
|
|||
`)
|
||||
|
||||
export const Entry = gql(`
|
||||
mutation Entry($lists: [String], $id: Int, $status: MediaListStatus, $progress: Int, $repeat: Int, $score: Int) {
|
||||
mutation Entry($lists: [String], $id: Int!, $status: MediaListStatus, $progress: Int, $repeat: Int, $score: Int) {
|
||||
SaveMediaListEntry(mediaId: $id, status: $status, progress: $progress, repeat: $repeat, scoreRaw: $score, customLists: $lists) {
|
||||
...FullMediaList
|
||||
id,
|
||||
media {
|
||||
id,
|
||||
status,
|
||||
mediaListEntry {
|
||||
...FullMediaList
|
||||
},
|
||||
nextAiringEpisode {
|
||||
episode
|
||||
}
|
||||
|
|
@ -283,7 +289,7 @@ export const Entry = gql(`
|
|||
`, [FullMediaList])
|
||||
|
||||
export const DeleteEntry = gql(`
|
||||
mutation DeleteEntry($id: Int) {
|
||||
mutation DeleteEntry($id: Int!) {
|
||||
DeleteMediaListEntry(id: $id) {
|
||||
deleted
|
||||
}
|
||||
|
|
@ -291,7 +297,7 @@ export const DeleteEntry = gql(`
|
|||
`)
|
||||
|
||||
export const ToggleFavourite = gql(`
|
||||
mutation ToggleFavourite($id: Int) {
|
||||
mutation ToggleFavourite($id: Int!) {
|
||||
ToggleFavourite(animeId: $id) { anime { nodes { id } } }
|
||||
}
|
||||
`)
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@ import { readable } from 'simple-store-svelte'
|
|||
|
||||
import { client } from '../anilist'
|
||||
|
||||
import local from './local'
|
||||
|
||||
import type { VariablesOf } from 'gql.tada'
|
||||
import type { Entry } from '../anilist/queries'
|
||||
|
||||
export default new class AuthAggregator {
|
||||
hasAuth = readable(this.checkAuth(), set => {
|
||||
// add other subscriptions here
|
||||
const unsAL = client.viewer.subscribe(() => set(this.checkAuth()))
|
||||
// add other subscriptions here for MAL, kitsu, tvdb, etc
|
||||
const unsub = [
|
||||
client.viewer.subscribe(() => set(this.checkAuth()))
|
||||
]
|
||||
|
||||
return () => {
|
||||
unsAL()
|
||||
}
|
||||
return () => unsub.forEach(fn => fn())
|
||||
})
|
||||
|
||||
// AUTH
|
||||
|
|
@ -33,8 +35,6 @@ export default new class AuthAggregator {
|
|||
|
||||
profile () {
|
||||
if (this.anilist()) return client.viewer.value?.viewer
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// QUERIES/MUTATIONS
|
||||
|
|
@ -42,25 +42,22 @@ export default new class AuthAggregator {
|
|||
schedule () {
|
||||
if (this.anilist()) return client.schedule()
|
||||
|
||||
return client.schedule()
|
||||
return local.schedule()
|
||||
}
|
||||
|
||||
toggleFav (id: number) {
|
||||
if (this.anilist()) return client.toggleFav(id)
|
||||
|
||||
return client.toggleFav(id)
|
||||
if (this.anilist()) client.toggleFav(id)
|
||||
local.toggleFav(id)
|
||||
}
|
||||
|
||||
delete (id: number) {
|
||||
if (this.anilist()) return client.deleteEntry(id)
|
||||
if (this.anilist()) client.deleteEntry(id)
|
||||
|
||||
return client.deleteEntry(id)
|
||||
local.deleteEntry(id)
|
||||
}
|
||||
|
||||
following (id: number) {
|
||||
if (this.anilist()) return client.following(id)
|
||||
|
||||
return client.following(id)
|
||||
}
|
||||
|
||||
continueIDs () {
|
||||
|
|
@ -82,8 +79,8 @@ export default new class AuthAggregator {
|
|||
} else {
|
||||
delete variables.lists
|
||||
}
|
||||
if (this.anilist()) return client.entry(variables)
|
||||
if (this.anilist()) client.entry(variables)
|
||||
|
||||
return client.entry(variables)
|
||||
local.entry(variables)
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
94
src/lib/modules/auth/local.ts
Normal file
94
src/lib/modules/auth/local.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { createStore, set, get } from 'idb-keyval'
|
||||
import { writable } from 'simple-store-svelte'
|
||||
|
||||
import { client, type Media } from '../anilist'
|
||||
|
||||
import type { Entry } from '../anilist/queries'
|
||||
import type { VariablesOf } from 'gql.tada'
|
||||
|
||||
type StoredMedia = Pick<Media, 'isFavourite' | 'mediaListEntry' | 'id'>
|
||||
|
||||
export default new class LocalSync {
|
||||
store = createStore('watchlist', 'local')
|
||||
|
||||
entries = writable<Record<number, StoredMedia>>({})
|
||||
|
||||
constructor () {
|
||||
get('entries', this.store).then(s => {
|
||||
this.entries.value = s ?? {}
|
||||
})
|
||||
this.entries.subscribe(entries => {
|
||||
set('entries', entries, this.store)
|
||||
})
|
||||
}
|
||||
|
||||
get (id: number) {
|
||||
return this.entries.value[id]
|
||||
}
|
||||
|
||||
_createEntry (id: number): StoredMedia {
|
||||
// const media = client.client.readQuery(IDMedia, { id })?.data?.Media
|
||||
return this.entries.value[id] ?? {
|
||||
id,
|
||||
isFavourite: false,
|
||||
mediaListEntry: {
|
||||
id,
|
||||
customLists: null,
|
||||
progress: null,
|
||||
repeat: null,
|
||||
score: null,
|
||||
status: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schedule (): ReturnType<typeof client.schedule> {
|
||||
return client.schedule(Object.keys(this.entries.value).map(id => parseInt(id)))
|
||||
}
|
||||
|
||||
toggleFav (id: number) {
|
||||
this.entries.update(entries => {
|
||||
const entry = this._createEntry(id)
|
||||
|
||||
entry.isFavourite = !entry.isFavourite
|
||||
return { ...entries, [id]: entry }
|
||||
})
|
||||
}
|
||||
|
||||
deleteEntry (id: number) {
|
||||
this.entries.update(entries => {
|
||||
const entry = this._createEntry(id)
|
||||
|
||||
entry.mediaListEntry = null
|
||||
return { ...entries, [id]: entry }
|
||||
})
|
||||
}
|
||||
|
||||
// this is in theory doable, but hard to sync media's airing status
|
||||
// continueIDs () {
|
||||
// }
|
||||
// sequelIDs () {
|
||||
// }
|
||||
|
||||
_fillOutKeys (entry: StoredMedia, variables: VariablesOf<typeof Entry>) {
|
||||
const keys = ['status', 'score', 'repeat', 'progress'] as const
|
||||
|
||||
for (const key of keys) {
|
||||
// @ts-expect-error idk how to fix this tbf
|
||||
entry.mediaListEntry[key] = variables[key] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
entry (variables: VariablesOf<typeof Entry>) {
|
||||
this.entries.update(entries => {
|
||||
const entry = this._createEntry(variables.id)
|
||||
|
||||
const keys = ['status', 'score', 'repeat', 'progress'] as const
|
||||
for (const key of keys) {
|
||||
// @ts-expect-error idk how to fix this tbf
|
||||
entry.mediaListEntry![key] = variables[key] ?? entry.mediaListEntry![key] ?? null
|
||||
}
|
||||
return { ...entries, [variables.id]: entry }
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
|
@ -1,31 +1,35 @@
|
|||
import { episodes, type Media } from '../anilist'
|
||||
|
||||
export function progress ({ mediaListEntry }: Pick<Media, 'mediaListEntry'>): number | undefined {
|
||||
if (!mediaListEntry?.progress) return
|
||||
// TODO: these should probably be in auth aggregator
|
||||
|
||||
import local from './local'
|
||||
|
||||
export function progress ({ mediaListEntry, id }: Pick<Media, 'mediaListEntry' | 'id'>): number | undefined {
|
||||
if (!mediaListEntry?.progress) return local.get(id)?.mediaListEntry?.progress ?? undefined
|
||||
return mediaListEntry.progress
|
||||
}
|
||||
|
||||
export function fav (media: Pick<Media, 'isFavourite'>): boolean {
|
||||
return media.isFavourite
|
||||
export function fav (media: Pick<Media, 'isFavourite' | 'id'>): boolean {
|
||||
return media.isFavourite || (local.get(media.id)?.isFavourite ?? false)
|
||||
}
|
||||
|
||||
export function list (media: Pick<Media, 'mediaListEntry'>): 'CURRENT' | 'PLANNING' | 'COMPLETED' | 'DROPPED' | 'PAUSED' | 'REPEATING' | null | undefined {
|
||||
return media.mediaListEntry?.status
|
||||
export function list (media: Pick<Media, 'mediaListEntry' | 'id'>): 'CURRENT' | 'PLANNING' | 'COMPLETED' | 'DROPPED' | 'PAUSED' | 'REPEATING' | null | undefined {
|
||||
return media.mediaListEntry?.status ?? local.get(media.id)?.mediaListEntry?.status
|
||||
}
|
||||
|
||||
export function lists (media: Pick<Media, 'mediaListEntry'>): Array<{ enabled: boolean, name: string }> | undefined {
|
||||
export function lists (media: Pick<Media, 'mediaListEntry' | 'id'>): Array<{ enabled: boolean, name: string }> | undefined {
|
||||
return media.mediaListEntry?.customLists as Array<{ enabled: boolean, name: string }> | undefined
|
||||
}
|
||||
|
||||
export function repeat (media: Pick<Media, 'mediaListEntry'>): number | null | undefined {
|
||||
return media.mediaListEntry?.repeat
|
||||
export function repeat (media: Pick<Media, 'mediaListEntry' | 'id'>): number | null | undefined {
|
||||
return media.mediaListEntry?.repeat ?? local.get(media.id)?.mediaListEntry?.repeat
|
||||
}
|
||||
|
||||
export function score (media: Pick<Media, 'mediaListEntry'>): number | null | undefined {
|
||||
return media.mediaListEntry?.score
|
||||
export function score (media: Pick<Media, 'mediaListEntry' | 'id'>): number | null | undefined {
|
||||
return media.mediaListEntry?.score ?? local.get(media.id)?.mediaListEntry?.score
|
||||
}
|
||||
|
||||
export function of (media: Pick<Media, 'aired' | 'notaired' | 'episodes' | 'mediaListEntry'>): string | undefined {
|
||||
export function of (media: Pick<Media, 'aired' | 'notaired' | 'episodes' | 'mediaListEntry' | 'id'>): string | undefined {
|
||||
const count = episodes(media)
|
||||
if (count === 1 || !count) return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { redirect } from '@sveltejs/kit'
|
||||
|
||||
import { outdatedComponent } from '$lib/modules/update'
|
||||
import { SETUP_VERSION } from '$lib'
|
||||
|
||||
export function load () {
|
||||
if (outdatedComponent) return redirect(307, '/update/')
|
||||
redirect(307, localStorage.getItem('setup-finished') ? '/app/home/' : '/setup')
|
||||
redirect(307, Number(localStorage.getItem('setup-finished')) >= SETUP_VERSION ? '/app/home/' : '/setup')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import { error, redirect } from '@sveltejs/kit'
|
|||
import { dev } from '$app/environment'
|
||||
import native from '$lib/modules/native'
|
||||
import { outdatedComponent } from '$lib/modules/update'
|
||||
import { SETUP_VERSION } from '$lib'
|
||||
|
||||
export function load () {
|
||||
if (!dev && !native.isApp) return error(401, 'How did you get here?')
|
||||
if (Number(localStorage.getItem('setup-finished')) < SETUP_VERSION) redirect(307, '/setup')
|
||||
|
||||
if (outdatedComponent) redirect(307, '/update/')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
import { cover, desc, duration, format, relation, season, status, title } from '$lib/modules/anilist'
|
||||
import { authAggregator, of } from '$lib/modules/auth'
|
||||
import native from '$lib/modules/native'
|
||||
import { cn } from '$lib/utils'
|
||||
import EpisodesList from '$lib/components/EpisodesList.svelte'
|
||||
import MyAnimeList from '$lib/components/icons/MyAnimeList.svelte'
|
||||
import Anilist from '$lib/components/icons/Anilist.svelte'
|
||||
|
|
@ -55,10 +54,8 @@
|
|||
hideBanner.value = target.scrollTop > 100
|
||||
}
|
||||
|
||||
const hasAuth = authAggregator.hasAuth
|
||||
|
||||
$: mediaID = media.id
|
||||
$: following = $hasAuth ? authAggregator.following(mediaID) : undefined
|
||||
$: following = authAggregator.following(mediaID)
|
||||
|
||||
$: eps = data.eps
|
||||
</script>
|
||||
|
|
@ -116,10 +113,8 @@
|
|||
</div>
|
||||
<div class='flex gap-2 items-center justify-center md:justify-start w-full lex-wrap'>
|
||||
<div class='flex md:mr-3 w-full min-[380px]:w-[180px]'>
|
||||
<PlayButton size='default' {media} class={cn($hasAuth && 'rounded-r-none', 'w-full')} />
|
||||
{#if $hasAuth}
|
||||
<EntryEditor {media} />
|
||||
{/if}
|
||||
<PlayButton size='default' {media} class='rounded-r-none' />
|
||||
<EntryEditor {media} />
|
||||
</div>
|
||||
<FavoriteButton {media} variant='secondary' size='icon' class='min-[380px]:-order-1 md:order-none' />
|
||||
<BookmarkButton {media} variant='secondary' size='icon' class='min-[380px]:-order-2 md:order-none' />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang='ts'>
|
||||
import { addMonths, endOfMonth, endOfWeek, format, isSameMonth, isToday, startOfMonth, startOfWeek, subMonths } from 'date-fns'
|
||||
import { Button as ButtonPrimitive } from 'bits-ui'
|
||||
import { get } from 'svelte/store'
|
||||
import { onMount, tick } from 'svelte'
|
||||
import { Cross2 } from 'svelte-radix'
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-svelte'
|
||||
|
|
@ -46,7 +45,7 @@
|
|||
$: dayList = listDays(firstDay, lastDay)
|
||||
|
||||
const paused = query.isPaused$
|
||||
if (get(query.isPaused$)) query.resume()
|
||||
if ($paused) query.resume()
|
||||
|
||||
interface DayAirTimes { day: { date: Date, number: number }, episodes: Array<ResultOf<typeof ScheduleMedia> & { episode: number, airTime: Date }> }
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
function checkNext () {
|
||||
if (step !== 2) return
|
||||
localStorage.setItem('setup-finished', 'true')
|
||||
localStorage.setItem('setup-finished', '1')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue