diff --git a/package.json b/package.json index fde43d6..f77ffb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "6.4.128", + "version": "6.4.129", "license": "BUSL-1.1", "private": true, "packageManager": "pnpm@9.15.5", @@ -51,6 +51,7 @@ "type": "module", "dependencies": { "@cloudflare/speedtest": "^1.6.0", + "@dagrejs/dagre": "^1.1.5", "@fontsource-variable/nunito": "^5.2.6", "@fontsource/geist-mono": "^5.2.6", "@prgm/sveltekit-progress-bar": "2.0.0", @@ -62,6 +63,7 @@ "@urql/exchange-request-policy": "^2.0.0", "@urql/exchange-retry": "^2.0.0", "@urql/svelte": "^5.0.0", + "@xyflow/svelte": "^0.1.36", "abslink": "^1.1.2", "anitomyscript": "github:thaunknown/anitomyscript", "bittorrent-tracker": "10.0.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc142de..7905798 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@cloudflare/speedtest': specifier: ^1.6.0 version: 1.6.0 + '@dagrejs/dagre': + specifier: ^1.1.5 + version: 1.1.5 '@fontsource-variable/nunito': specifier: ^5.2.6 version: 5.2.6 @@ -44,6 +47,9 @@ importers: '@urql/svelte': specifier: ^5.0.0 version: 5.0.0(@urql/core@6.0.1(graphql@16.10.0))(svelte@4.2.19) + '@xyflow/svelte': + specifier: ^0.1.36 + version: 0.1.36(svelte@4.2.19) abslink: specifier: ^1.1.2 version: 1.1.2 @@ -254,6 +260,13 @@ packages: resolution: {integrity: sha512-5EfTvWcDCAK6zOJpl7i4Ablzvxje7+dgVmhJxdK/uDuTIivyUVat/cCnxE67YYpuxKs+gbo569PbmHl+oI5eFA==} engines: {node: '>=12'} + '@dagrejs/dagre@1.1.5': + resolution: {integrity: sha512-Ghgrh08s12DCL5SeiR6AoyE80mQELTWhJBRmXfFoqDiFkR458vPEdgTbbjA0T+9ETNxUblnD0QW55tfdvi5pjQ==} + + '@dagrejs/graphlib@2.2.4': + resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} + engines: {node: '>17.0.0'} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -670,6 +683,11 @@ packages: peerDependencies: eslint: '>=9.0.0' + '@svelte-put/shortcut@3.1.1': + resolution: {integrity: sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==} + peerDependencies: + svelte: ^3.55.0 || ^4.0.0 || ^5.0.0 + '@sveltejs/acorn-typescript@1.0.5': resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} peerDependencies: @@ -723,6 +741,24 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -845,6 +881,14 @@ packages: '@urql/core': ^6.0.0 svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 + '@xyflow/svelte@0.1.36': + resolution: {integrity: sha512-kvu5qz7hGj50Rf6COMFhikhS9766lCm+QPjuxpf6SEppAMhml3r9G98+Pf78MGKDC/C5m2YShpktbtoAl72J6g==} + peerDependencies: + svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 + + '@xyflow/system@0.0.56': + resolution: {integrity: sha512-Xc3LvEumjJD+CqPqlYkrlszJ4hWQ0DE+r5M4e5WpS/hKT4T6ktAjt7zeMNJ+vvTsXHulGnEoDRA8zbIfB6tPdQ==} + abslink@1.1.2: resolution: {integrity: sha512-vnuzpBWd09oAdyFmseonzDoyfZtjKN9XuFzoXQUgU0Sme4ksYRwdH9YgOeserpDdtDMq7nd82gnKbTrYQQ3FnQ==} @@ -1064,6 +1108,9 @@ packages: chrome-dgram@3.0.6: resolution: {integrity: sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA==} + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + clone@2.1.2: resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} engines: {node: '>=0.8'} @@ -1137,6 +1184,18 @@ packages: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + d3-format@3.1.0: resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} engines: {node: '>=12'} @@ -1149,6 +1208,10 @@ packages: resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} engines: {node: '>=12'} + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + d3-time-format@4.1.0: resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} engines: {node: '>=12'} @@ -1157,6 +1220,20 @@ packages: resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} engines: {node: '>=12'} + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2871,6 +2948,12 @@ snapshots: transitivePeerDependencies: - encoding + '@dagrejs/dagre@1.1.5': + dependencies: + '@dagrejs/graphlib': 2.2.4 + + '@dagrejs/graphlib@2.2.4': {} + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -3191,6 +3274,10 @@ snapshots: estraverse: 5.3.0 picomatch: 4.0.3 + '@svelte-put/shortcut@3.1.1(svelte@4.2.19)': + dependencies: + svelte: 4.2.19 + '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': dependencies: acorn: 8.15.0 @@ -3276,6 +3363,27 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/d3-color@3.1.3': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -3433,6 +3541,23 @@ snapshots: svelte: 4.2.19 wonka: 6.3.5 + '@xyflow/svelte@0.1.36(svelte@4.2.19)': + dependencies: + '@svelte-put/shortcut': 3.1.1(svelte@4.2.19) + '@xyflow/system': 0.0.56 + classcat: 5.0.5 + svelte: 4.2.19 + + '@xyflow/system@0.0.56': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + abslink@1.1.2: {} acorn-jsx@5.3.2(acorn@8.15.0): @@ -3697,6 +3822,8 @@ snapshots: inherits: 2.0.4 run-series: 1.1.9 + classcat@5.0.5: {} + clone@2.1.2: {} clsx@2.1.1: {} @@ -3764,6 +3891,15 @@ snapshots: d3-color@3.1.0: {} + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-ease@3.0.1: {} + d3-format@3.1.0: {} d3-interpolate@3.0.1: @@ -3778,6 +3914,8 @@ snapshots: d3-time: 3.1.0 d3-time-format: 4.1.0 + d3-selection@3.0.0: {} + d3-time-format@4.1.0: dependencies: d3-time: 3.1.0 @@ -3786,6 +3924,25 @@ snapshots: dependencies: d3-array: 3.2.4 + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 diff --git a/src/app.css b/src/app.css index 9bae33a..cbd77ae 100644 --- a/src/app.css +++ b/src/app.css @@ -94,7 +94,7 @@ --custom: #fff; } - .dark { + html.dark { --background: 240 10% 3.9%; --foreground: 0 0% 98%; diff --git a/src/lib/components/ui/relations/Relations.svelte b/src/lib/components/ui/relations/Relations.svelte new file mode 100644 index 0000000..61d5791 --- /dev/null +++ b/src/lib/components/ui/relations/Relations.svelte @@ -0,0 +1,144 @@ + + + + + + + {#if expanded} + + {:else} + + {/if} + + + diff --git a/src/lib/components/ui/relations/TextNode.svelte b/src/lib/components/ui/relations/TextNode.svelte new file mode 100644 index 0000000..db0b77a --- /dev/null +++ b/src/lib/components/ui/relations/TextNode.svelte @@ -0,0 +1,30 @@ + + + +
+ + {data?.label} + +
+
+ + diff --git a/src/lib/components/ui/relations/index.ts b/src/lib/components/ui/relations/index.ts new file mode 100644 index 0000000..270dd0e --- /dev/null +++ b/src/lib/components/ui/relations/index.ts @@ -0,0 +1,2 @@ +export { default as Relations } from './Relations.svelte' +export { default as TextNode } from './TextNode.svelte' diff --git a/src/lib/modules/anilist/client.ts b/src/lib/modules/anilist/client.ts index c3edaad..18cb604 100644 --- a/src/lib/modules/anilist/client.ts +++ b/src/lib/modules/anilist/client.ts @@ -1,13 +1,14 @@ import { queryStore, type OperationResultState, gql as _gql } from '@urql/svelte' import Debug from 'debug' import lavenshtein from 'js-levenshtein' -import { derived, readable, writable, type Writable } from 'svelte/store' +import { derived, get, readable, writable, type Writable } from 'svelte/store' -import { Comments, DeleteEntry, DeleteThreadComment, Entry, Following, IDMedia, SaveThreadComment, Schedule, Search, Threads, ToggleFavourite, ToggleLike, UserLists } from './queries' +import { Comments, DeleteEntry, DeleteThreadComment, Entry, Following, type FullMedia, IDMedia, RecrusiveRelations, SaveThreadComment, Schedule, Search, Threads, ToggleFavourite, ToggleLike, UserLists } from './queries' import urqlClient from './urql-client' import { currentSeason, currentYear, lastSeason, lastYear, nextSeason, nextYear } from './util' import type { Media } from './types' +import type { Edge, Node } from '@xyflow/svelte' import type { ResultOf, VariablesOf } from 'gql.tada' import type { AnyVariables, OperationContext, RequestPolicy, TypedDocumentNode } from 'urql' @@ -252,8 +253,82 @@ class AnilistClient { debug('deleteComment: deleting comment with ID', id, 'rootCommentId', rootCommentId) return await this.client.mutation(DeleteThreadComment, { id, rootCommentId }) } + + _relationsTreeCache = new Map() + + relationsTree (media: ResultOf): RelationsStore { + if (this._relationsTreeCache.has(media.id)) return this._relationsTreeCache.get(media.id)! + + const store: RelationsStore = writable({ nodes: new Map(), edges: new Map() }) + + this._generateRelationsTree(store, media) + + return store + } + + async _generateRelationsTree (store: RelationsStore, media: NonNullable['Page']>['media']>[0]) { + const { nodes, edges } = get(store) + + const startMedia = media + + const position = { x: 0, y: 0 } + + const lastEdgeMedia: number[] = [] + + const processEdges = (media: typeof startMedia) => { + if (!media) return + if ('type' in media && media.type !== 'ANIME') return + if (!nodes.has(media.id)) { + if (!media.relations) lastEdgeMedia.push(media.id) + nodes.set(media.id, { + id: '' + media.id, + data: { label: media.title?.userPreferred ?? 'No title', id: media.id }, + position + }) + } + + for (const edge of media.relations?.edges ?? []) { + if (!edge?.node) continue + const { node, relationType } = edge + if (node.type !== 'ANIME' || relationType === 'CHARACTER') continue + const edgeName = [node.id, media.id].sort((a, b) => a - b).join('-') + if (!edges.has(edgeName)) { + const isPrequel = relationType === 'PREQUEL' + edges.set(edgeName, { + id: 'e' + edgeName, + source: '' + (isPrequel ? node.id : media.id), + target: '' + (isPrequel ? media.id : node.id), + data: { ids: [media.id, node.id] }, + animated: true, + label: isPrequel ? 'SEQUEL' : relationType?.replaceAll('_', ' ') ?? '' + }) + + // @ts-expect-error yeah recursive, last node has different types since it doesnt have relations + processEdges(node) + } + } + } + + const totalSize = nodes.size + edges.size + processEdges(startMedia) + + for (const id of nodes.keys()) this._relationsTreeCache.set(id, store) + if (totalSize !== (nodes.size + edges.size)) store.set({ nodes, edges }) + + if (!lastEdgeMedia.length) return + const res = await this.client.query(RecrusiveRelations, { ids: lastEdgeMedia }, { requestPolicy: 'cache-first' }) + if (res.error) console.error(res.error) + + if (res.data?.Page) { + for (const media of res.data.Page.media ?? []) { + await this._generateRelationsTree(store, media) + } + } + } } +type RelationsStore = Writable<{ nodes: Map, edges: Map }> + // sveltekit/vite does the funny and evaluates at compile, this is a hack to fix development mode const client = (typeof indexedDB !== 'undefined' && new AnilistClient()) as AnilistClient diff --git a/src/lib/modules/anilist/graphql-turbo.d.ts b/src/lib/modules/anilist/graphql-turbo.d.ts index 5d40940..7579217 100644 --- a/src/lib/modules/anilist/graphql-turbo.d.ts +++ b/src/lib/modules/anilist/graphql-turbo.d.ts @@ -1,21 +1,22 @@ /* eslint-disable */ /* prettier-ignore */ import type { TadaDocumentNode, $tada } from 'gql.tada'; +import type { introspection } from './graphql-env.d' declare module 'gql.tada' { interface setupCache { "\n fragment FullMediaList on MediaList @_unmask {\n id,\n status,\n progress,\n repeat,\n score(format: POINT_10),\n customLists(asArray: true)\n }\n": TadaDocumentNode<{ id: number; status: "CURRENT" | "PLANNING" | "COMPLETED" | "DROPPED" | "PAUSED" | "REPEATING" | null; progress: number | null; repeat: number | null; score: number | null; customLists: unknown; }, {}, { fragment: "FullMediaList"; on: "MediaList"; masked: false; }>; - "\n fragment MediaEdgeFrag on MediaEdge @_unmask {\n relationType(version:2),\n node {\n id,\n title {userPreferred},\n coverImage {medium},\n type,\n status,\n format,\n episodes,\n synonyms,\n season,\n seasonYear,\n startDate {\n year,\n month,\n day\n },\n endDate {\n year,\n month,\n day\n }\n }\n }\n": - TadaDocumentNode<{ 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; }, {}, { fragment: "MediaEdgeFrag"; on: "MediaEdge"; masked: false; }>; + "\n fragment MediaEdgeFrag on MediaEdge @_unmask {\n relationType(version:2),\n node {\n id,\n title {userPreferred},\n coverImage {medium},\n type,\n status,\n format,\n episodes,\n synonyms,\n season,\n seasonYear,\n relations {\n edges {\n relationType(version:2),\n node {\n id,\n type,\n title { userPreferred },\n coverImage { medium }\n }\n }\n },\n startDate {\n year,\n month,\n day\n },\n endDate {\n year,\n month,\n day\n }\n }\n }\n": + TadaDocumentNode<{ 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; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; type: "ANIME" | "MANGA" | null; title: { userPreferred: string | null; } | null; coverImage: { medium: string | null; } | null; } | null; } | null)[] | null; } | null; startDate: { year: number | null; month: number | null; day: number | null; } | null; endDate: { year: number | null; month: number | null; day: number | null; } | null; } | null; }, {}, { fragment: "MediaEdgeFrag"; on: "MediaEdge"; masked: false; }>; "\n fragment FullMedia on Media @_unmask {\nid,\nidMal,\ntitle {\n romaji,\n english,\n native,\n userPreferred\n},\ndescription(asHtml: false),\nseason,\nseasonYear,\nformat,\nstatus,\nepisodes,\nduration,\naverageScore,\ngenres,\nisFavourite,\ncoverImage {\n extraLarge,\n medium,\n color,\n},\nsource,\ncountryOfOrigin,\nisAdult,\nbannerImage,\nsynonyms,\nnextAiringEpisode {\n id,\n timeUntilAiring,\n episode\n},\nstartDate {\n year,\n month,\n day\n},\ntrailer {\n id,\n site\n},\nmediaListEntry {\n ...FullMediaList\n},\nstudios(isMain: true) {\n nodes {\n id,\n name\n }\n},\nnotaired: airingSchedule(page: 1, perPage: 50, notYetAired: true) {\n n: nodes {\n a: airingAt,\n e: episode\n }\n},\naired: airingSchedule(page: 1, perPage: 50, notYetAired: false) {\n n: nodes {\n a: airingAt,\n e: episode\n }\n},\nrelations {\n edges {\n ...MediaEdgeFrag\n }\n}\n}": - 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; }>; + 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; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; type: "ANIME" | "MANGA" | null; title: { userPreferred: string | null; } | null; coverImage: { medium: string | null; } | null; } | null; } | null)[] | null; } | 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 fragment UserFrag on User @_unmask {\n id,\n bannerImage,\n about,\n isFollowing,\n isFollower,\n donatorBadge,\n options {\n profileColor\n },\n createdAt,\n name,\n avatar {\n large\n },\n statistics {\n anime {\n count,\n minutesWatched,\n episodesWatched,\n genres(limit: 3, sort: COUNT_DESC) {\n genre,\n count\n }\n }\n }\n }\n": TadaDocumentNode<{ id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { large: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; }, {}, { fragment: "UserFrag"; on: "User"; 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>; + 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; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; type: "ANIME" | "MANGA" | null; title: { userPreferred: string | null; } | null; coverImage: { medium: string | null; } | null; } | null; } | null)[] | null; } | 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; }, void>; + 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; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; type: "ANIME" | "MANGA" | null; title: { userPreferred: string | null; } | null; coverImage: { medium: string | null; } | null; } | null; } | null)[] | null; } | 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 ...UserFrag,\n mediaListOptions {\n animeList {\n customLists\n }\n }\n }\n }\n": TadaDocumentNode<{ Viewer: { id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { large: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; 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 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": @@ -50,6 +51,8 @@ declare module 'gql.tada' { TadaDocumentNode<{ SaveThreadComment: { id: number; comment: string | null; isLiked: boolean | null; likeCount: number; createdAt: number; user: { id: number; bannerImage: string | null; about: string | null; isFollowing: boolean | null; isFollower: boolean | null; donatorBadge: string | null; options: { profileColor: string | null; } | null; createdAt: number | null; name: string; avatar: { large: string | null; } | null; statistics: { anime: { count: number; minutesWatched: number; episodesWatched: number; genres: ({ genre: string | null; count: number; } | null)[] | null; } | null; } | null; } | null; childComments: unknown; isLocked: boolean | null; } | null; }, { comment?: string | null | undefined; parentCommentId?: number | null | undefined; threadId?: number | null | undefined; id?: number | null | undefined; }, void>; "\n mutation DeleteThreadComment ($id: Int) {\n DeleteThreadComment(id: $id) {\n deleted\n }\n }\n": TadaDocumentNode<{ DeleteThreadComment: { deleted: boolean | null; } | null; }, { id?: number | null | undefined; }, void>; + "\n query RecrusiveRelations ($ids: [Int]!) {\n Page {\n pageInfo { hasNextPage },\n media(id_in: $ids, type: ANIME) {\n id,\n title { userPreferred },\n relations {\n edges {\n relationType,\n node {\n id,\n type,\n title { userPreferred },\n relations {\n edges {\n relationType,\n node {\n id,\n type,\n title { userPreferred }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": + TadaDocumentNode<{ Page: { pageInfo: { hasNextPage: boolean | null; } | null; media: ({ id: number; title: { userPreferred: string | null; } | null; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; type: "ANIME" | "MANGA" | null; title: { userPreferred: string | null; } | null; relations: { edges: ({ relationType: "ADAPTATION" | "PREQUEL" | "SEQUEL" | "PARENT" | "SIDE_STORY" | "CHARACTER" | "SUMMARY" | "ALTERNATIVE" | "SPIN_OFF" | "OTHER" | "SOURCE" | "COMPILATION" | "CONTAINS" | null; node: { id: number; type: "ANIME" | "MANGA" | null; title: { userPreferred: string | null; } | null; } | null; } | null)[] | null; } | null; } | null; } | null)[] | null; } | null; } | null)[] | null; } | null; }, { ids: (number | null)[]; }, 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 }}": diff --git a/src/lib/modules/anilist/queries.ts b/src/lib/modules/anilist/queries.ts index ee402be..1d6508a 100644 --- a/src/lib/modules/anilist/queries.ts +++ b/src/lib/modules/anilist/queries.ts @@ -25,6 +25,17 @@ export const MediaEdgeFrag = gql(` synonyms, season, seasonYear, + relations { + edges { + relationType(version:2), + node { + id, + type, + title { userPreferred }, + coverImage { medium } + } + } + }, startDate { year, month, @@ -461,3 +472,35 @@ export const DeleteThreadComment = gql(` } } `) + +export const RecrusiveRelations = gql(` + query RecrusiveRelations ($ids: [Int]!) { + Page { + pageInfo { hasNextPage }, + media(id_in: $ids, type: ANIME) { + id, + title { userPreferred }, + relations { + edges { + relationType, + node { + id, + type, + title { userPreferred }, + relations { + edges { + relationType, + node { + id, + type, + title { userPreferred } + } + } + } + } + } + } + } + } + } +`) diff --git a/src/routes/app/anime/[id]/+layout.svelte b/src/routes/app/anime/[id]/+layout.svelte index 65ec2a6..f152447 100644 --- a/src/routes/app/anime/[id]/+layout.svelte +++ b/src/routes/app/anime/[id]/+layout.svelte @@ -7,7 +7,7 @@ import type { LayoutData } from './$types' - import { goto, onNavigate } from '$app/navigation' + import { goto } from '$app/navigation' import EntryEditor from '$lib/components/EntryEditor.svelte' import Anilist from '$lib/components/icons/Anilist.svelte' import MyAnimeList from '$lib/components/icons/MyAnimeList.svelte' @@ -66,17 +66,10 @@ let container: HTMLDivElement - onNavigate(() => { - container.scrollTo({ - top: 0, - behavior: 'smooth' - }) - }) - $: ({ r, g, b } = colors(media.coverImage?.color ?? undefined)) -
+
diff --git a/src/routes/app/anime/[id]/+page.svelte b/src/routes/app/anime/[id]/+page.svelte index 4870ac6..6f8cee7 100644 --- a/src/routes/app/anime/[id]/+page.svelte +++ b/src/routes/app/anime/[id]/+page.svelte @@ -1,15 +1,16 @@ -{#if relations?.length} -
-
-
Relations
- {#if relations.length > 3} - - {/if} -
-
- {#each showRelations ? relations : relations.slice(0, 3) as rel (rel?.node?.id)} - {@const media = rel?.node} - {#if media} - -
- -
-
-
{relation(rel.relationType)}
-
{media.title?.userPreferred ?? 'N/A'}
-
{format(media)}
-
-
- {/if} - {/each} -
-
-{/if}
Episodes + Relations Threads Themes @@ -71,6 +40,15 @@ + + {#if value === 'relations'} +
+ + + +
+ {/if} +
{#key mediaId} {#if value === 'threads'}