mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-01-12 02:01:26 +00:00
fix: scrolling in view anime page
feat: new relations tree
This commit is contained in:
parent
4efd9449d6
commit
b7e06e89cb
11 changed files with 483 additions and 56 deletions
|
|
@ -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",
|
||||
|
|
|
|||
157
pnpm-lock.yaml
157
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
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
--custom: #fff;
|
||||
}
|
||||
|
||||
.dark {
|
||||
html.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
|
|
|
|||
144
src/lib/components/ui/relations/Relations.svelte
Normal file
144
src/lib/components/ui/relations/Relations.svelte
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<script lang='ts'>
|
||||
import Dagre from '@dagrejs/dagre'
|
||||
import { SvelteFlow, Background, useSvelteFlow, type Node, type Edge, Controls, ControlButton, type NodeTypes } from '@xyflow/svelte'
|
||||
import '@xyflow/svelte/dist/style.css'
|
||||
import Maximize2 from 'lucide-svelte/icons/maximize-2'
|
||||
import Minimize2 from 'lucide-svelte/icons/minimize-2'
|
||||
import { writable } from 'simple-store-svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import TextNode from './TextNode.svelte'
|
||||
|
||||
import type { Media } from '$lib/modules/anilist'
|
||||
|
||||
import { client } from '$lib/modules/anilist'
|
||||
|
||||
export let media: Media
|
||||
|
||||
export let expanded: boolean
|
||||
|
||||
// WARN: this is non-reactive, only set on init, but it shouldn't matter as the anime page can only navigate to entries already visible in the graph
|
||||
// this is done to make sure the graph doesn't reset when navigating to a relation
|
||||
const nodesStore = client.relationsTree(media)
|
||||
|
||||
const nodes = writable<Node[]>([])
|
||||
const edges = writable<Edge[]>([])
|
||||
|
||||
$: $nodes = [...$nodesStore.nodes.values()]
|
||||
$: $edges = [...$nodesStore.edges.values()]
|
||||
|
||||
const { fitView } = useSvelteFlow()
|
||||
|
||||
$: media && onLayout()
|
||||
|
||||
$: $nodesStore && fitAndLayout()
|
||||
|
||||
function getLayoutedElements (nodes: Node[], edges: Edge[]) {
|
||||
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}))
|
||||
g.setGraph({ rankdir: 'LR', edgesep: 50, nodesep: 50, ranksep: 120, ranker: 'tight-tree' })
|
||||
// TODO: switch between longest-path and tight-tree based on number of nodes?
|
||||
|
||||
edges.forEach((edge) => g.setEdge(edge.source, edge.target))
|
||||
nodes.forEach((node) =>
|
||||
g.setNode(node.id, {
|
||||
...node,
|
||||
width: node.measured?.width ?? 120,
|
||||
height: node.measured?.height ?? 32
|
||||
})
|
||||
)
|
||||
|
||||
Dagre.layout(g)
|
||||
|
||||
return {
|
||||
nodes: nodes.map((node) => {
|
||||
const position = g.node(node.id)
|
||||
// We are shifting the dagre node position (anchor=center center) to the top left
|
||||
// so it matches the Svelte Flow node anchor point (top left).
|
||||
const x = position.x - (node.measured?.width ?? 0) / 2
|
||||
const y = position.y - (node.measured?.height ?? 0) / 2
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
current: node.data.id === media.id
|
||||
},
|
||||
type: 'customText',
|
||||
position: { x, y },
|
||||
sourcePosition: 'right',
|
||||
targetPosition: 'left'
|
||||
}
|
||||
}) as Node[],
|
||||
edges: edges.map(e => ({
|
||||
...e,
|
||||
style: (e.data?.ids as number[]).includes(media.id) ? '--xy-edge-stroke: var(--custom)' : '',
|
||||
labelStyle: (e.data?.ids as number[]).includes(media.id) ? '--xy-edge-label-color: var(--custom)' : ''
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function onLayout () {
|
||||
const { nodes, edges } = getLayoutedElements($nodes, $edges)
|
||||
|
||||
$nodes = nodes
|
||||
$edges = edges
|
||||
}
|
||||
function fitAndLayout () {
|
||||
onLayout()
|
||||
fitView()
|
||||
}
|
||||
|
||||
// turbo hacky but cba
|
||||
let frameId: number
|
||||
function loopFitView () {
|
||||
cancelAnimationFrame(frameId)
|
||||
fitView()
|
||||
frameId = requestAnimationFrame(loopFitView)
|
||||
}
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout>
|
||||
function expand () {
|
||||
expanded = !expanded
|
||||
loopFitView()
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => {
|
||||
cancelAnimationFrame(frameId)
|
||||
if (expanded) document.querySelector('.svelte-flow')?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' })
|
||||
fitView()
|
||||
}, 150)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fitAndLayout()
|
||||
setTimeout(fitAndLayout)
|
||||
})
|
||||
|
||||
const nodeTypes = {
|
||||
customText: TextNode as NodeTypes['customText']
|
||||
}
|
||||
</script>
|
||||
|
||||
<SvelteFlow {nodes} {edges} colorMode='dark'
|
||||
proOptions={{ hideAttribution: true }}
|
||||
nodesConnectable={false}
|
||||
nodesDraggable={false}
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={expanded}
|
||||
preventScrolling={expanded}
|
||||
zoomActivationKey={['Control', 'Meta', 'Ctrl', 'Shift', 'ShiftLeft']}
|
||||
onlyRenderVisibleElements={true}
|
||||
minZoom={0}
|
||||
maxZoom={1.2}
|
||||
{nodeTypes}
|
||||
elementsSelectable={false}>
|
||||
<Background bgColor='black' />
|
||||
<Controls showLock={false} orientation='horizontal'>
|
||||
<ControlButton on:click={expand}>
|
||||
{#if expanded}
|
||||
<Minimize2 />
|
||||
{:else}
|
||||
<Maximize2 />
|
||||
{/if}
|
||||
</ControlButton>
|
||||
</Controls>
|
||||
</SvelteFlow>
|
||||
30
src/lib/components/ui/relations/TextNode.svelte
Normal file
30
src/lib/components/ui/relations/TextNode.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<script lang='ts'>
|
||||
|
||||
import { Handle } from '@xyflow/svelte'
|
||||
import { Position } from '@xyflow/system'
|
||||
|
||||
import { cn } from '$lib/utils'
|
||||
|
||||
export let data: { label: string, id: number, current?: boolean }
|
||||
export let id: string
|
||||
export let targetPosition: Position = Position.Top
|
||||
export let sourcePosition: Position = Position.Bottom
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
$$restProps
|
||||
</script>
|
||||
|
||||
<a class={cn('node p-2.5 w-[150px] text-xs text-center border bg-[#1e1e1e] rounded-sm cursor-pointer block font-semibold transition-colors', data.current ? 'border-custom text-custom' : 'border-border text-white')} href='/app/anime/{data.id}'>
|
||||
<div class='relative'>
|
||||
<Handle type='target' position={targetPosition} />
|
||||
{data?.label}
|
||||
<Handle type='source' position={sourcePosition} />
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.node {
|
||||
--xy-handle-background-color: none;
|
||||
--xy-handle-border-color: none;
|
||||
}
|
||||
</style>
|
||||
2
src/lib/components/ui/relations/index.ts
Normal file
2
src/lib/components/ui/relations/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Relations } from './Relations.svelte'
|
||||
export { default as TextNode } from './TextNode.svelte'
|
||||
|
|
@ -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<number, RelationsStore>()
|
||||
|
||||
relationsTree (media: ResultOf<typeof FullMedia>): 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<NonNullable<ResultOf<typeof RecrusiveRelations>['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<number, Node>, edges: Map<string, Edge> }>
|
||||
|
||||
// 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
|
||||
|
||||
|
|
|
|||
13
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
13
src/lib/modules/anilist/graphql-turbo.d.ts
vendored
|
|
@ -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 }}":
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
</script>
|
||||
|
||||
<div class='min-w-0 -ml-14 pl-14 grow items-center flex flex-col h-full overflow-y-auto z-10 pointer-events-none pb-10' use:dragScroll on:scroll={handleScroll} bind:this={container} style:--custom={media.coverImage?.color ?? '#fff'} style:--red={r} style:--green={g} style:--blue={b}>
|
||||
<div class='min-w-0 -ml-14 pl-14 grow items-center flex flex-col h-full overflow-y-auto -z-1 pb-10' use:dragScroll on:scroll={handleScroll} bind:this={container} style:--custom={media.coverImage?.color ?? '#fff'} style:--red={r} style:--green={g} style:--blue={b}>
|
||||
<div class='gap-6 w-full pt-4 md:pt-32 flex flex-col items-center justify-center max-w-[1600px] px-3 xl:px-14 pointer-events-auto'>
|
||||
<div class='flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12'>
|
||||
<Dialog.Root portal='#root'>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
<script lang='ts'>
|
||||
import { SvelteFlowProvider } from '@xyflow/svelte'
|
||||
|
||||
import type { PageData } from './$types'
|
||||
|
||||
import EpisodesList from '$lib/components/EpisodesList.svelte'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { Threads } from '$lib/components/ui/forums'
|
||||
import { Load } from '$lib/components/ui/img'
|
||||
import { Relations } from '$lib/components/ui/relations'
|
||||
import * as Tabs from '$lib/components/ui/tabs'
|
||||
import { Themes } from '$lib/components/ui/themes'
|
||||
import { format, relation } from '$lib/modules/anilist'
|
||||
import { authAggregator } from '$lib/modules/auth'
|
||||
import { dragScroll } from '$lib/modules/navigate'
|
||||
import '@xyflow/svelte/dist/style.css'
|
||||
import { cn } from '$lib/utils'
|
||||
|
||||
export let data: PageData
|
||||
|
||||
|
|
@ -17,13 +18,7 @@
|
|||
|
||||
$: media = $anime.Media!
|
||||
|
||||
$: relations = media.relations?.edges?.filter(edge => edge?.node?.type === 'ANIME')
|
||||
|
||||
let showRelations = false
|
||||
|
||||
function showMore () {
|
||||
showRelations = !showRelations
|
||||
}
|
||||
let expanded = false
|
||||
|
||||
$: mediaId = media.id
|
||||
$: following = authAggregator.following(mediaId)
|
||||
|
|
@ -33,37 +28,11 @@
|
|||
let value: string
|
||||
</script>
|
||||
|
||||
{#if relations?.length}
|
||||
<div class='w-full'>
|
||||
<div class='flex justify-between items-center'>
|
||||
<div class='text-[20px] md:text-2xl font-bold'>Relations</div>
|
||||
{#if relations.length > 3}
|
||||
<Button variant='ghost' class='text-muted-foreground font-bold text-sm' on:click={showMore}>{showRelations ? 'Show Less' : 'Show More'}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class='md:w-full flex gap-5 overflow-x-scroll md:overflow-x-visible md:grid md:grid-cols-3 justify-items-center py-3' use:dragScroll>
|
||||
{#each showRelations ? relations : relations.slice(0, 3) as rel (rel?.node?.id)}
|
||||
{@const media = rel?.node}
|
||||
{#if media}
|
||||
<a class='select:scale-[1.02] select:shadow-lg scale-100 transition-all duration-200 shrink-0 ease-out focus-visible:ring-ring focus-visible:ring-1 rounded-md w-96 md:w-full h-[126px] bg-neutral-950 text-secondary-foreground select:bg-neutral-900 flex' style:-webkit-user-drag='none' href='/app/anime/{media.id}'>
|
||||
<div class='w-[90px] bg-image rounded-l-md shrink-0'>
|
||||
<Load src={media.coverImage?.medium} class='object-cover h-full w-full shrink-0 rounded-l-md' />
|
||||
</div>
|
||||
<div class='h-full grid px-3 items-center'>
|
||||
<div class='text-custom font-bold capitalize'>{relation(rel.relationType)}</div>
|
||||
<div class='line-clamp-2'>{media.title?.userPreferred ?? 'N/A'}</div>
|
||||
<div class='font-thin'>{format(media)}</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Tabs.Root bind:value class='w-full' activateOnFocus={false}>
|
||||
<div class='flex justify-between items-center gap-3 sm:flex-row flex-col'>
|
||||
<Tabs.List class='flex'>
|
||||
<Tabs.Trigger value='episodes' tabindex={0} class='px-8 data-[state=active]:bg-custom data-[state=active]:text-contrast data-[state=active]:font-bold'>Episodes</Tabs.Trigger>
|
||||
<Tabs.Trigger value='relations' tabindex={0} class='px-8 data-[state=active]:bg-custom data-[state=active]:text-contrast data-[state=active]:font-bold'>Relations</Tabs.Trigger>
|
||||
<Tabs.Trigger value='threads' tabindex={0} class='px-8 data-[state=active]:bg-custom data-[state=active]:text-contrast data-[state=active]:font-bold'>Threads</Tabs.Trigger>
|
||||
<Tabs.Trigger value='themes' tabindex={0} class='px-8 data-[state=active]:bg-custom data-[state=active]:text-contrast data-[state=active]:font-bold'>Themes</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
|
@ -71,6 +40,15 @@
|
|||
<Tabs.Content value='episodes' tabindex={-1}>
|
||||
<EpisodesList {media} {eps} {following} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value='relations' tabindex={-1}>
|
||||
{#if value === 'relations'}
|
||||
<div class={cn('border border-border rounded overflow-clip mt-3 transition-[height]', expanded ? 'h-[80vh]' : 'h-72')}>
|
||||
<SvelteFlowProvider>
|
||||
<Relations {media} bind:expanded />
|
||||
</SvelteFlowProvider>
|
||||
</div>
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value='threads' tabindex={-1}>
|
||||
{#key mediaId}
|
||||
{#if value === 'threads'}
|
||||
|
|
|
|||
Loading…
Reference in a new issue