feat: Added fullscreen mode (#179)

* Added theme entry menu to video player.
* Added overlay to video player.
* Improved video player styles for mobile.
* Fixed button focus ring showing on click.
* Fixed z-index issue with multi cover image.
* Fixed global search.
* Fixed various watch list issues.
This commit is contained in:
Mani
2023-04-22 23:18:48 +02:00
parent 6392ff5967
commit 9d2d981632
23 changed files with 812 additions and 359 deletions
+123 -6
View File
@@ -19,6 +19,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-select": "^1.2.0",
"@radix-ui/react-switch": "^1.0.1",
"ahooks": "^3.7.6",
"axios": "^1.2.2",
"common-tags": "^1.8.0",
"framer-motion": "^6.5.1",
@@ -1009,9 +1010,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz",
"integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==",
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
@@ -3522,6 +3523,11 @@
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA=="
},
"node_modules/@types/js-yaml": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
@@ -3979,6 +3985,34 @@
"node": ">=8"
}
},
"node_modules/ahooks": {
"version": "3.7.6",
"resolved": "https://registry.npmjs.org/ahooks/-/ahooks-3.7.6.tgz",
"integrity": "sha512-p+2j4H/BI9vqXR0fZI7S/q6fUPxPklQnHqvU7zAVBljMFNSFeYRWB2iHHbjpXGOwUTOBYCh2OuvIHyJYj6Lpag==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@types/js-cookie": "^2.x.x",
"ahooks-v3-count": "^1.0.0",
"dayjs": "^1.9.1",
"intersection-observer": "^0.12.0",
"js-cookie": "^2.x.x",
"lodash": "^4.17.21",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"tslib": "^2.4.1"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/ahooks-v3-count": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ahooks-v3-count/-/ahooks-v3-count-1.0.0.tgz",
"integrity": "sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ=="
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -5013,6 +5047,11 @@
"integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==",
"dev": true
},
"node_modules/dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
},
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -7060,6 +7099,11 @@
"node": ">= 0.4"
}
},
"node_modules/intersection-observer": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz",
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -7512,6 +7556,11 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"node_modules/js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
@@ -10106,6 +10155,11 @@
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"node_modules/resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@@ -10296,6 +10350,17 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"engines": {
"node": ">=0.10.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/scuid": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz",
@@ -12529,9 +12594,9 @@
}
},
"@babel/runtime": {
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz",
"integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==",
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"requires": {
"regenerator-runtime": "^0.13.11"
}
@@ -14539,6 +14604,11 @@
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA=="
},
"@types/js-yaml": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
@@ -14875,6 +14945,28 @@
"indent-string": "^4.0.0"
}
},
"ahooks": {
"version": "3.7.6",
"resolved": "https://registry.npmjs.org/ahooks/-/ahooks-3.7.6.tgz",
"integrity": "sha512-p+2j4H/BI9vqXR0fZI7S/q6fUPxPklQnHqvU7zAVBljMFNSFeYRWB2iHHbjpXGOwUTOBYCh2OuvIHyJYj6Lpag==",
"requires": {
"@babel/runtime": "^7.21.0",
"@types/js-cookie": "^2.x.x",
"ahooks-v3-count": "^1.0.0",
"dayjs": "^1.9.1",
"intersection-observer": "^0.12.0",
"js-cookie": "^2.x.x",
"lodash": "^4.17.21",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"tslib": "^2.4.1"
}
},
"ahooks-v3-count": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ahooks-v3-count/-/ahooks-v3-count-1.0.0.tgz",
"integrity": "sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ=="
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -15632,6 +15724,11 @@
"integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==",
"dev": true
},
"dayjs": {
"version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
},
"debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -17166,6 +17263,11 @@
"side-channel": "^1.0.4"
}
},
"intersection-observer": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz",
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -17466,6 +17568,11 @@
"integrity": "sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ==",
"dev": true
},
"js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
@@ -19235,6 +19342,11 @@
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@@ -19362,6 +19474,11 @@
"loose-envify": "^1.1.0"
}
},
"screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="
},
"scuid": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz",
+1
View File
@@ -21,6 +21,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-select": "^1.2.0",
"@radix-ui/react-switch": "^1.0.1",
"ahooks": "^3.7.6",
"axios": "^1.2.2",
"common-tags": "^1.8.0",
"framer-motion": "^6.5.1",
+1 -1
View File
@@ -76,7 +76,7 @@ const BaseButton = styled.button<{ $isCircle: boolean }>`
margin: -8px 8px -8px -16px;
}
&:focus {
&:focus:focus-visible {
box-shadow: 0 0 0 2px var(--focus-ring-color);
}
`;
+17 -14
View File
@@ -5,6 +5,7 @@ import theme from "theme";
import useMediaQuery from "hooks/useMediaQuery";
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
import { forwardRef } from "react";
const StyledButton = styled(Button)`
gap: 8px;
@@ -24,21 +25,23 @@ interface IconTextButtonProps extends ComponentPropsWithoutRef<typeof StyledButt
collapsible?: boolean
}
export function IconTextButton({ icon, children, collapsible = false, ...props }: IconTextButtonProps) {
const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.mobileMax})`);
const isCollapsed = collapsible && isMobile;
export const IconTextButton = forwardRef(
function IconTextButton({ icon, children, collapsible = false, ...props }: IconTextButtonProps, ref) {
const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.mobileMax})`);
const isCollapsed = collapsible && isMobile;
return (
<StyledButton variant="silent" isCircle={isCollapsed} {...props}>
{isIconDefinition(icon) ? (
<Icon icon={icon} color="text-disabled"/>
) : icon}
{children ? (
<StyledText collapsible={collapsible}>{children}</StyledText>
) : null}
</StyledButton>
);
}
return (
<StyledButton ref={ref} variant="silent" isCircle={isCollapsed} {...props}>
{isIconDefinition(icon) ? (
<Icon icon={icon} color="text-disabled"/>
) : icon}
{children ? (
<StyledText collapsible={collapsible}>{children}</StyledText>
) : null}
</StyledButton>
);
}
);
function isIconDefinition(icon: IconDefinition | ReactNode): icon is IconDefinition {
return !!icon && typeof icon === "object" && "icon" in icon;
+1
View File
@@ -32,6 +32,7 @@ const StyledCoverContainer = styled.div`
height: 100%;
border-radius: 0.5rem;
overflow: hidden;
isolation: isolate;
`;
const StyledCoverItemContainer = styled.div<{ $itemCount: number }>`
position: absolute;
+24 -1
View File
@@ -7,9 +7,10 @@ import * as RadixMenu from "@radix-ui/react-dropdown-menu";
const StyledMenuContent = styled(RadixMenu.Content)`
width: max-content;
max-height: var(--radix-dropdown-menu-content-available-height);
padding: 8px 0;
border-radius: 8px;
overflow: hidden;
overflow: auto;
background-color: ${theme.colors["solid"]};
box-shadow: 0 0 0 2px ${theme.colors["text-primary"]}, ${theme.shadows.high};
@@ -48,4 +49,26 @@ export const MenuItem = styled(RadixMenu.Item)`
color: ${theme.colors["text"]};
outline: none;
}
&[data-disabled] {
opacity: 0.5;
cursor: revert;
}
`;
export const MenuLabel = styled(RadixMenu.Label)`
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 16px;
color: ${theme.colors["text-muted"]};
font-size: 0.8rem;
`;
export const MenuSeparator = styled(RadixMenu.Separator)`
height: 2px;
background-color: ${theme.colors["text-primary"]};
margin: 8px 0;
`;
@@ -21,6 +21,10 @@ export const StyledNavigation = styled(Solid).attrs({ as: "nav" })<{ $floating:
background-color: transparent;
box-shadow: none;
`}
[data-fullscreen] & {
display: none;
}
`;
export const StyledNavigationContainer = styled(Container)`
+1 -1
View File
@@ -9,7 +9,7 @@ interface EpisodeTagProps {
export function EpisodeTag({ entry }: EpisodeTagProps) {
return (
<Tag icon={faFilm}>
<Tag icon={faFilm} title={entry.episodes ? `Used in episode ${entry.episodes}` : "Used in all episodes"}>
{entry.episodes || "—"}
</Tag>
);
+1 -1
View File
@@ -13,7 +13,7 @@ const StyledTag = styled.span`
gap: 4px;
& ${Icon} {
transform: translateY(0.2rem);
transform: translateY(0.2em);
}
`;
+20 -13
View File
@@ -1,7 +1,7 @@
import styled from "styled-components";
import theme from "theme";
import type { RefObject } from "react";
import { useEffect, useRef } from "react";
import { useContext, useEffect, useRef } from "react";
import { VideoPlayerContext } from "components/video-player-2/VideoPlayer2";
const StyledPlayerProgress = styled.div`
position: absolute;
@@ -11,6 +11,7 @@ const StyledPlayerProgress = styled.div`
padding: 16px 0;
cursor: pointer;
touch-action: none;
transform: translateY(-50%);
`;
@@ -69,12 +70,18 @@ const StyledPlayerProgressBarHover = styled.div`
}
`;
interface ProgressBarProps {
playerRef: RefObject<HTMLVideoElement | HTMLAudioElement>;
progressRef: RefObject<HTMLDivElement>;
}
export function ProgressBar() {
const context = useContext(VideoPlayerContext);
if (!context) {
throw new Error("ProgressBar needs to be inside VideoPlayer!");
}
const {
playerRef,
progressRef,
} = context;
export function ProgressBar({ playerRef, progressRef }: ProgressBarProps) {
const progressHoverRef = useRef<HTMLDivElement>(null);
const nextProgressRef = useRef(0);
@@ -101,18 +108,18 @@ export function ProgressBar({ playerRef, progressRef }: ProgressBarProps) {
}
}
window.addEventListener("mouseup", onMouseUp);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("pointerup", onMouseUp);
window.addEventListener("pointermove", onMouseMove);
return () => {
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("pointerup", onMouseUp);
window.removeEventListener("pointermove", onMouseMove);
};
}, [playerRef, progressRef]);
return (
<StyledPlayerProgress
onMouseDown={(event) => {
onPointerDown={(event) => {
const nextProgress = event.clientX / event.currentTarget.getBoundingClientRect().width;
isDraggingRef.current = true;
nextProgressRef.current = nextProgress;
@@ -121,7 +128,7 @@ export function ProgressBar({ playerRef, progressRef }: ProgressBarProps) {
progressRef.current.style.width = `${nextProgress * 100}%`;
}
}}
onMouseMove={(event) => {
onPointerMove={(event) => {
const nextProgress = event.clientX / event.currentTarget.getBoundingClientRect().width;
if (progressHoverRef.current) {
progressHoverRef.current.innerText = playerRef.current ? formatTime(nextProgress * playerRef.current.duration) : "";
@@ -1,8 +1,6 @@
import styled from "styled-components";
import theme from "theme";
import { Switcher } from "components/switcher";
import { Solid } from "components/box";
import { IconTextButton } from "components/button";
import { m } from "framer-motion";
export const StyledPlayer = styled.div`
@@ -15,6 +13,10 @@ export const StyledPlayer = styled.div`
&:not([data-background]) {
flex: 1;
}
&[data-relaxed] {
cursor: none;
}
`;
export const StyledPlayerContent = styled.div`
@@ -39,16 +41,29 @@ export const StyledPlayerContent = styled.div`
inset: 70px 16px 104px 16px;
pointer-events: none;
}
[data-fullscreen] & {
grid-template-columns: 1fr;
grid-template-rows: auto;
padding: 0;
}
`;
export const StyledPlaybackArea = styled(m.div)`
align-self: stretch;
justify-self: stretch;
padding: 16px 0;
position: relative;
margin: 32px 0;
@media (max-width: ${theme.breakpoints.tabletMax}) {
aspect-ratio: 16 / 9;
margin: 0 -16px;
[data-background] & {
display: none;
}
}
[data-background] & {
@@ -57,44 +72,44 @@ export const StyledPlaybackArea = styled(m.div)`
left: 0;
width: 300px;
aspect-ratio: 16 / 9;
margin: 0;
padding: 0;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px;
overflow: hidden;
pointer-events: all;
}
[data-fullscreen] & {
height: 100%;
margin: 0;
}
`;
export const StyledVideoBackground = styled.div`
position: relative;
display: flex;
width: 100%;
height: 100%;
`;
export const StyledVideo = styled.video`
position: absolute;
inset: 0;
margin: auto;
max-width: 100%;
max-height: 100%;
aspect-ratio: 16 / 9;
`;
export const StyledVideo = styled.video`
width: 100%;
height: 100%;
outline: none;
@media (max-width: ${theme.breakpoints.tabletMax}) {
position: static;
}
`;
export const StyledAudioBackground = styled.div`
position: relative;
display: flex;
width: 100%;
height: 100%;
position: absolute;
inset: 0;
margin: auto;
max-width: 100%;
max-height: 100%;
`;
export const StyledAudio = styled.audio`
@@ -102,12 +117,15 @@ export const StyledAudio = styled.audio`
`;
export const StyledAudioCover = styled.img`
position: absolute;
inset: 0;
margin: auto;
width: 100%;
height: 100%;
max-height: 100%;
pointer-events: none;
outline: none;
object-fit: contain;
[data-background] & {
pointer-events: none;
}
`;
export const StyledAside = styled.aside`
@@ -119,9 +137,10 @@ export const StyledAside = styled.aside`
@media (max-width: ${theme.breakpoints.tabletMax}) {
padding-top: 0;
gap: 16px;
}
[data-background] & {
[data-background] &, [data-fullscreen] & {
display: none;
}
`;
@@ -138,47 +157,3 @@ export const StyledScrollArea = styled.div`
padding-bottom: 16px;
overflow: auto;
`;
export const StyledPlayerBar = styled(Solid)`
position: relative;
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-gap: 32px;
align-items: center;
padding: 16px 32px;
@media (max-width: ${theme.breakpoints.tabletMax}) {
grid-template-columns: 1fr auto;
grid-gap: 16px;
padding: 16px;
}
`;
export const StyledPlayerBarControls = styled.div`
display: flex;
gap: 16px;
@media (max-width: ${theme.breakpoints.tabletMax}) {
& > :first-child, & > :last-child {
display: none;
}
}
`;
export const StyledPlayerBarControl = styled(IconTextButton)`
font-size: 2rem;
`;
export const StyledPlayerBarActions = styled.div`
display: flex;
gap: 16px;
margin-left: auto;
@media (max-width: ${theme.breakpoints.tabletMax}) {
display: none;
}
`;
+74 -149
View File
@@ -1,5 +1,5 @@
import type { ReactNode, SyntheticEvent } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import type { PointerEvent, ReactNode, RefObject, SyntheticEvent } from "react";
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
import {
StyledAside,
StyledAudio,
@@ -7,10 +7,6 @@ import {
StyledAudioCover,
StyledPlaybackArea,
StyledPlayer,
StyledPlayerBar,
StyledPlayerBarActions,
StyledPlayerBarControl,
StyledPlayerBarControls,
StyledPlayerContent,
StyledVideo,
StyledVideoBackground
@@ -22,52 +18,59 @@ import useSetting from "hooks/useSetting";
import { AudioMode, GlobalVolume } from "utils/settings";
import { AUDIO_URL, VIDEO_URL } from "utils/config";
import extractImages from "utils/extractImages";
import { Text } from "components/text";
import { Column } from "components/box";
import { Switcher } from "components/switcher";
import { SwitcherOption } from "components/switcher/Switcher";
import { Button, IconTextButton } from "components/button";
import { Icon } from "components/icon";
import { faBackwardStep, faForwardStep, faPause, faPlay, faShare, faXmark } from "@fortawesome/pro-solid-svg-icons";
import { Performances, SongTitle } from "components/utils";
import Link from "next/link";
import { PlaylistTrackAddDialog } from "components/dialog/PlaylistTrackAddDialog";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "components/menu/Menu";
import { Toast } from "components/toast";
import { useToasts } from "context/toastContext";
import useWatchHistory from "hooks/useWatchHistory";
import { ProgressBar } from "components/video-player-2/ProgressBar";
import type { VideoSummaryCardVideoFragment } from "generated/graphql";
import { ConditionalWrapper } from "components/utils/ConditionalWrapper";
import { VideoPlayerBar } from "components/video-player-2/VideoPlayerBar";
import useMouseRelax from "hooks/useMouseRelax";
interface VideoPlayerProps {
interface VideoPlayerContextValue {
video: VideoSummaryCardVideoFragment;
background: boolean;
playerRef: RefObject<HTMLVideoElement | HTMLAudioElement | null>;
progressRef: RefObject<HTMLDivElement>;
previousVideoPath: string | null;
playPreviousTrack(navigate: boolean): void;
nextVideoPath: string | null;
playNextTrack(navigate: boolean): void;
isPlaying: boolean;
togglePlay(): void;
videoUrl: string;
audioUrl: string;
updateAudioMode(audioMode: string): void;
}
export const VideoPlayerContext = createContext<VideoPlayerContextValue | null>(null);
type VideoPlayerProps = {
video: VideoSummaryCardVideoFragment;
background: boolean;
children?: ReactNode;
overlay?: ReactNode;
}
export function VideoPlayer2({ video, background, children, ...props }: VideoPlayerProps) {
export function VideoPlayer2({ video, background, children, overlay, ...props }: VideoPlayerProps) {
const entry = video.entries[0];
const theme = entry.theme!;
const anime = theme.anime!;
const videoPath = `/anime/${anime.slug}/${createVideoSlug(theme, entry, video)}`;
const theme = entry.theme;
const anime = theme.anime;
const videoUrl = `${VIDEO_URL}/${video.basename}`;
const audioUrl = `${AUDIO_URL}/${video.audio.basename}`;
const [isPlaying, setPlaying] = useState(false);
const [aspectRatio, setAspectRatio] = useState(16 / 9);
const containerRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<HTMLVideoElement | HTMLAudioElement | null>(null);
const progressRef = useRef<HTMLDivElement>(null);
const currentTimeBeforeModeSwitch = useRef<number | null>(null);
const { watchList, currentWatchListItem, setCurrentWatchListItem } = useContext(PlayerContext);
const router = useRouter();
const [globalVolume, setGlobalVolume] = useSetting(GlobalVolume);
const [canRenderPlayer, setCanRenderPlayer] = useState(false);
const { smallCover, largeCover } = extractImages(anime);
const [audioMode, setAudioMode] = useSetting(AudioMode, { storageSync: false });
const { dispatchToast } = useToasts();
const { addToHistory } = useWatchHistory();
const isRelaxed = useMouseRelax();
const previousVideo = getWatchListVideo(-1);
const previousEntry = previousVideo?.entries[0];
@@ -105,8 +108,6 @@ export function VideoPlayer2({ video, background, children, ...props }: VideoPla
}
}, [nextVideo, nextVideoPath, router, setCurrentWatchListItem]);
useEffect(() => setCanRenderPlayer(true), []);
useEffect(() => {
if (playerRef.current) {
playerRef.current.volume = globalVolume;
@@ -158,6 +159,9 @@ export function VideoPlayer2({ video, background, children, ...props }: VideoPla
playerRef.current = player;
if (playerRef.current) {
setPlaying(!playerRef.current.paused);
if ("videoWidth" in playerRef.current) {
setAspectRatio(playerRef.current.videoWidth / playerRef.current.videoHeight);
}
playerRef.current.volume = globalVolume;
if (currentTimeBeforeModeSwitch.current) {
playerRef.current.currentTime = currentTimeBeforeModeSwitch.current;
@@ -166,6 +170,12 @@ export function VideoPlayer2({ video, background, children, ...props }: VideoPla
}
}
function onPlayerClick(event: PointerEvent) {
if (!background && event.nativeEvent.pointerType === "mouse") {
togglePlay();
}
}
function togglePlay() {
if (isPlaying) {
playerRef.current?.pause();
@@ -182,11 +192,6 @@ export function VideoPlayer2({ video, background, children, ...props }: VideoPla
}
}
function saveToClipboard(url: string) {
navigator.clipboard.writeText(url)
.then(() => dispatchToast("clipboard", <Toast>Copied to clipboard!</Toast>));
}
function updateAudioMode(audioMode: string) {
currentTimeBeforeModeSwitch.current = playerRef.current?.currentTime ?? null;
setAudioMode(audioMode);
@@ -215,8 +220,27 @@ export function VideoPlayer2({ video, background, children, ...props }: VideoPla
const constraintRef = useRef<HTMLDivElement>(null);
return (
<>
<StyledPlayer data-background={background || undefined} {...props}>
<VideoPlayerContext.Provider value={{
video,
background,
playerRef,
progressRef,
previousVideoPath,
playPreviousTrack,
nextVideoPath,
playNextTrack,
isPlaying,
togglePlay,
videoUrl,
audioUrl,
updateAudioMode,
}}>
<StyledPlayer
ref={containerRef}
data-background={background || undefined}
data-relaxed={isRelaxed || undefined}
{...props}
>
<StyledPlayerContent ref={constraintRef}>
<StyledPlaybackArea
layout
@@ -229,7 +253,7 @@ export function VideoPlayer2({ video, background, children, ...props }: VideoPla
>
{audioMode === AudioMode.ENABLED ? (
<StyledAudioBackground>
<StyledAudioCover src={largeCover} onClick={background ? undefined : togglePlay}/>
<StyledAudioCover src={largeCover} onPointerDown={onPlayerClick}/>
<StyledAudio
ref={onPlayerMount}
src={audioUrl}
@@ -243,9 +267,10 @@ export function VideoPlayer2({ video, background, children, ...props }: VideoPla
onTimeUpdate={updateProgress}
onVolumeChange={(event: SyntheticEvent<HTMLAudioElement>) => setGlobalVolume(event.currentTarget.volume)}
/>
{overlay}
</StyledAudioBackground>
) : (
<StyledVideoBackground>
<StyledVideoBackground style={{ aspectRatio }}>
<StyledVideo
ref={onPlayerMount}
src={videoUrl}
@@ -256,10 +281,16 @@ export function VideoPlayer2({ video, background, children, ...props }: VideoPla
setPlaying(false);
playNextTrack(true);
}}
onClick={background ? undefined : togglePlay}
onPointerDown={onPlayerClick}
onTimeUpdate={updateProgress}
onVolumeChange={(event: SyntheticEvent<HTMLVideoElement>) => setGlobalVolume(event.currentTarget.volume)}
onLoadedMetadata={(event) => {
console.log(event.currentTarget.videoWidth);
console.log(event.currentTarget.videoHeight);
setAspectRatio(event.currentTarget.videoWidth / event.currentTarget.videoHeight);
}}
/>
{overlay}
</StyledVideoBackground>
)}
</StyledPlaybackArea>
@@ -267,114 +298,8 @@ export function VideoPlayer2({ video, background, children, ...props }: VideoPla
{children}
</StyledAside>
</StyledPlayerContent>
<StyledPlayerBar>
<Column style={{ "--gap": "8px" }}>
<Text color="text-muted" maxLines={1}>
<SongTitle song={theme.song} href={videoPath} />
<Text variant="small"> - </Text>
<Text weight={600}>{theme.type}{theme.sequence || null}{theme.group && ` (${theme.group})`}</Text>
<Text variant="small"> from </Text>
<Link href={`/anime/${anime.slug}`} passHref legacyBehavior>
<Text as="a" link>{anime.name}</Text>
</Link>
</Text>
{!!theme.song?.performances?.length && (
<Text variant="small" color="text-muted">
<Text>Performed</Text>
<Performances song={theme.song} maxPerformances={3} />
</Text>
)}
</Column>
<StyledPlayerBarControls>
{previousVideoPath ? (
<ConditionalWrapper
condition={!background}
wrap={(children) => <Link href={previousVideoPath}>{children}</Link>}
>
<StyledPlayerBarControl
icon={faBackwardStep}
isCircle
onClick={() => playPreviousTrack(!background)}
/>
</ConditionalWrapper>
) : (
<StyledPlayerBarControl
icon={faBackwardStep}
isCircle
disabled
/>
)}
<StyledPlayerBarControl
icon={isPlaying ? faPause : <Icon icon={faPlay} color="text-disabled" style={{ transform: "translateX(2px)" }} />}
isCircle
onClick={togglePlay}
/>
{nextVideoPath ? (
<ConditionalWrapper
condition={!background}
wrap={(children) => <Link href={nextVideoPath}>{children}</Link>}
>
<StyledPlayerBarControl
icon={faForwardStep}
isCircle
onClick={() => playNextTrack(!background)}
/>
</ConditionalWrapper>
) : (
<StyledPlayerBarControl
icon={faForwardStep}
isCircle
disabled
/>
)}
</StyledPlayerBarControls>
<StyledPlayerBarActions>
<PlaylistTrackAddDialog
video={{
// Flip the structure on it's head,
// because we need video as the root object here.
...video,
entries: [{
...entry,
theme,
}],
}}
/>
<Menu modal={false}>
<MenuTrigger asChild>
<Button style={{ "--gap": "8px" }}>
<Icon icon={faShare}/>
<Text>Share</Text>
</Button>
</MenuTrigger>
<MenuContent>
<MenuItem onSelect={() => saveToClipboard(location.href)}>Copy URL to this Page</MenuItem>
{audioMode === AudioMode.ENABLED ? (
<MenuItem onSelect={() => saveToClipboard(audioUrl)}>Copy URL to Embeddable Audio</MenuItem>
) : (
<MenuItem onSelect={() => saveToClipboard(videoUrl)}>Copy URL to Embeddable Video</MenuItem>
)}
</MenuContent>
</Menu>
{canRenderPlayer ? (
<Switcher selectedItem={audioMode} onChange={updateAudioMode}>
<SwitcherOption value={AudioMode.DISABLED}>Video</SwitcherOption>
<SwitcherOption value={AudioMode.ENABLED}>Audio</SwitcherOption>
</Switcher>
) : null}
<IconTextButton
icon={faXmark}
isCircle
disabled={!background}
onClick={() => setCurrentWatchListItem(null)}
/>
</StyledPlayerBarActions>
<ProgressBar
playerRef={playerRef}
progressRef={progressRef}
/>
</StyledPlayerBar>
<VideoPlayerBar />
</StyledPlayer>
</>
</VideoPlayerContext.Provider>
);
}
@@ -0,0 +1,216 @@
import { Column, Solid } from "components/box";
import { Text } from "components/text";
import { Performances, SongTitle } from "components/utils";
import Link from "next/link";
import { ConditionalWrapper } from "components/utils/ConditionalWrapper";
import { faBackwardStep, faForwardStep, faPause, faPlay, faShare, faXmark } from "@fortawesome/pro-solid-svg-icons";
import { Icon } from "components/icon";
import { PlaylistTrackAddDialog } from "components/dialog/PlaylistTrackAddDialog";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "components/menu/Menu";
import { Button, IconTextButton } from "components/button";
import { AudioMode } from "utils/settings";
import { ProgressBar } from "components/video-player-2/ProgressBar";
import styled from "styled-components";
import theme from "theme";
import { Toast } from "components/toast";
import { useToasts } from "context/toastContext";
import createVideoSlug from "utils/createVideoSlug";
import { useContext } from "react";
import { VideoPlayerContext } from "components/video-player-2/VideoPlayer2";
import useSetting from "hooks/useSetting";
import PlayerContext from "context/playerContext";
const StyledPlayerBar = styled(Solid)`
position: relative;
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-gap: 32px;
align-items: center;
padding: 16px 32px;
transition: opacity 100ms;
@media (max-width: ${theme.breakpoints.tabletMax}) {
grid-template-columns: 1fr auto;
grid-gap: 16px;
padding: 8px 16px;
}
[data-fullscreen] & {
position: fixed;
inset: auto 0 0 0;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: brightness(0.25);
}
[data-fullscreen] [data-relaxed] & {
opacity: 0;
transition: opacity 500ms;
}
`;
const StyledPlayerBarControls = styled.div`
display: flex;
gap: 16px;
@media (max-width: ${theme.breakpoints.tabletMax}) {
& > :first-child, & > :last-child {
display: none;
}
}
`;
const StyledPlayerBarControl = styled(IconTextButton)`
font-size: 2rem;
`;
const StyledPlayerBarActions = styled.div`
display: flex;
gap: 16px;
margin-left: auto;
@media (max-width: ${theme.breakpoints.tabletMax}) {
display: none;
}
`;
export function VideoPlayerBar() {
const context = useContext(VideoPlayerContext);
if (!context) {
throw new Error("VideoPlayerBar needs to be inside VideoPlayer!");
}
const {
video,
background,
previousVideoPath,
playPreviousTrack,
nextVideoPath,
playNextTrack,
isPlaying,
togglePlay,
videoUrl,
audioUrl,
} = context;
const entry = video.entries[0];
const theme = entry.theme;
const anime = theme.anime;
const videoPath = `/anime/${anime.slug}/${createVideoSlug(theme, entry, video)}`;
const { dispatchToast } = useToasts();
const [audioMode] = useSetting(AudioMode, { storageSync: false });
const { setCurrentWatchListItem } = useContext(PlayerContext);
function saveToClipboard(url: string) {
navigator.clipboard.writeText(url)
.then(() => dispatchToast("clipboard", <Toast>Copied to clipboard!</Toast>));
}
return (
<StyledPlayerBar>
<Column style={{ "--gap": "8px" }}>
<Text color="text-muted" maxLines={1}>
<SongTitle song={theme.song} href={videoPath} />
<Text variant="small"> - </Text>
<Text weight={600}>{theme.type}{theme.sequence || null}{theme.group && ` (${theme.group})`}</Text>
<Text variant="small"> from </Text>
<Link href={`/anime/${anime.slug}`} passHref legacyBehavior>
<Text as="a" link>{anime.name}</Text>
</Link>
</Text>
{!!theme.song?.performances?.length && (
<Text variant="small" color="text-muted">
<Text>Performed</Text>
<Performances song={theme.song} maxPerformances={3} />
</Text>
)}
</Column>
<StyledPlayerBarControls>
{previousVideoPath ? (
<ConditionalWrapper
condition={!background}
wrap={(children) => <Link href={previousVideoPath}>{children}</Link>}
>
<StyledPlayerBarControl
icon={faBackwardStep}
isCircle
onClick={() => playPreviousTrack(!background)}
/>
</ConditionalWrapper>
) : (
<StyledPlayerBarControl
icon={faBackwardStep}
isCircle
disabled
/>
)}
<StyledPlayerBarControl
icon={isPlaying ? faPause : <Icon icon={faPlay} color="text-disabled" style={{ transform: "translateX(2px)" }} />}
isCircle
onClick={togglePlay}
/>
{nextVideoPath ? (
<ConditionalWrapper
condition={!background}
wrap={(children) => <Link href={nextVideoPath}>{children}</Link>}
>
<StyledPlayerBarControl
icon={faForwardStep}
isCircle
onClick={() => playNextTrack(!background)}
/>
</ConditionalWrapper>
) : (
<StyledPlayerBarControl
icon={faForwardStep}
isCircle
disabled
/>
)}
</StyledPlayerBarControls>
<StyledPlayerBarActions>
<PlaylistTrackAddDialog
video={{
// Flip the structure on it's head,
// because we need video as the root object here.
...video,
entries: [{
...entry,
theme,
}],
}}
/>
<Menu modal={false}>
<MenuTrigger asChild>
<Button style={{ "--gap": "8px" }}>
<Icon icon={faShare}/>
<Text>Share</Text>
</Button>
</MenuTrigger>
<MenuContent>
<MenuItem onSelect={() => saveToClipboard(location.href)}>Copy URL to this Page</MenuItem>
{audioMode === AudioMode.ENABLED ? (
<MenuItem onSelect={() => saveToClipboard(audioUrl)}>Copy URL to Embeddable Audio</MenuItem>
) : (
<MenuItem onSelect={() => saveToClipboard(videoUrl)}>Copy URL to Embeddable Video</MenuItem>
)}
</MenuContent>
</Menu>
<IconTextButton
icon={faXmark}
isCircle
disabled={!background}
onClick={() => setCurrentWatchListItem(null)}
/>
</StyledPlayerBarActions>
<ProgressBar />
</StyledPlayerBar>
);
}
@@ -0,0 +1,134 @@
import { Row } from "components/box";
import { IconTextButton } from "components/button";
import { faCheck, faCog, faCompress, faExpand } from "@fortawesome/pro-solid-svg-icons";
import styled from "styled-components";
import { Fragment, useContext } from "react";
import FullscreenContext from "context/fullscreenContext";
import { Menu, MenuContent, MenuItem, MenuLabel, MenuSeparator, MenuTrigger } from "components/menu/Menu";
import { Text } from "components/text";
import { ThemeEntryTags } from "components/tag/ThemeEntryTags";
import Link from "next/link";
import createVideoSlug from "utils/createVideoSlug";
import { Icon } from "components/icon";
import { VideoTags } from "components/tag/VideoTags";
import type { VideoPageProps } from "pages/anime/[animeSlug]/[videoSlug]";
import PlayerContext from "context/playerContext";
import { VideoPlayerContext } from "components/video-player-2/VideoPlayer2";
import useSetting from "hooks/useSetting";
import { AudioMode } from "utils/settings";
import { StyledPlaybackArea, StyledPlayer } from "components/video-player-2/VideoPlayer2.style";
const StyledOverlay = styled.div`
position: absolute;
top: 0;
right: 0;
padding: 16px;
opacity: 0;
transition: opacity 500ms;
${StyledPlayer}:not([data-relaxed]) ${StyledPlaybackArea}:hover &, &:hover {
opacity: 1;
transition: opacity 100ms;
}
`;
const StyledOverlayButton = styled(IconTextButton)`
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
`;
export function VideoPlayerOverlay({ anime, themeIndex, entryIndex, videoIndex }: VideoPageProps) {
const theme = anime.themes[themeIndex];
const entry = theme.entries[entryIndex];
const video = entry.videos[videoIndex];
const { watchList, setWatchList, currentWatchListItem } = useContext(PlayerContext);
const { isFullscreen, toggleFullscreen, } = useContext(FullscreenContext);
const context = useContext(VideoPlayerContext);
const [audioMode] = useSetting(AudioMode, { storageSync: false });
if (!context) {
throw new Error("VideoPlayerOverlay needs to be inside VideoPlayer!");
}
const {
updateAudioMode,
} = context;
const otherEntries = theme.entries.map((otherEntry) => {
const videos = otherEntry.videos;
if (!videos.length) {
return null;
}
return {
...otherEntry,
videos
};
}).filter((otherEntry) => !!otherEntry);
return (
<StyledOverlay>
<Row style={{ "--gap": "16px" }}>
<Menu modal={false}>
<MenuTrigger asChild>
<StyledOverlayButton icon={faCog} isCircle />
</MenuTrigger>
<MenuContent collisionPadding={16}>
<MenuLabel>
<Text variant="small">Media type</Text>
</MenuLabel>
<MenuItem onSelect={() => updateAudioMode(AudioMode.DISABLED)}>
<Icon icon={faCheck} color={audioMode === AudioMode.DISABLED ? "text-primary" : "transparent"} />
Video
</MenuItem>
<MenuItem onSelect={() => updateAudioMode(AudioMode.ENABLED)}>
<Icon icon={faCheck} color={audioMode === AudioMode.ENABLED ? "text-primary" : "transparent"} />
Audio
</MenuItem>
<MenuSeparator />
{otherEntries.map((otherEntry) => otherEntry ? (
<Fragment key={otherEntry.version ?? 1}>
<MenuLabel>
<Row style={{ "--gap": "8px", "--align-items": "baseline" }}>
<Text variant="small">Version {otherEntry.version || 1}</Text>
<ThemeEntryTags entry={otherEntry}/>
</Row>
</MenuLabel>
{otherEntry.videos.map((otherVideo) => (
<MenuItem key={otherVideo.basename} asChild onSelect={() => {
const currentItemIndex = watchList.findIndex((item) => item.watchListId === currentWatchListItem?.watchListId);
const currentItem = watchList[currentItemIndex];
const newItem = {
watchListId: currentItem.watchListId,
...otherVideo,
entries: [
{
...otherEntry,
theme,
},
],
};
const newWatchList = [...watchList];
newWatchList[currentItemIndex] = newItem;
setWatchList(newWatchList);
}}>
<Link href={`/anime/${anime.slug}/${createVideoSlug(theme, otherEntry, otherVideo)}`}>
<Icon icon={faCheck} color={otherVideo.basename === video.basename ? "text-primary" : "transparent"} />
<VideoTags video={otherVideo} hideTextOnMobile />
</Link>
</MenuItem>
))}
</Fragment>
) : null)}
</MenuContent>
</Menu>
<StyledOverlayButton icon={isFullscreen ? faCompress : faExpand} isCircle onClick={toggleFullscreen} />
</Row>
</StyledOverlay>
);
}
+15
View File
@@ -0,0 +1,15 @@
import { createContext } from "react";
interface FullscreenContextValue {
isFullscreen: boolean;
toggleFullscreen(): void;
}
const FullscreenContext = createContext<FullscreenContextValue>({
isFullscreen: false,
toggleFullscreen() {
// Do nothing.
}
});
export default FullscreenContext;
+48 -48
View File
@@ -130,7 +130,7 @@ export type Entry = {
id: Scalars['Int'];
nsfw: Scalars['Boolean'];
spoiler: Scalars['Boolean'];
theme: Maybe<Theme>;
theme: Theme;
version: Maybe<Scalars['Int']>;
videos: Array<Video>;
};
@@ -451,7 +451,7 @@ export type Synonym = {
};
export type Theme = {
anime: Maybe<Anime>;
anime: Anime;
entries: Array<Entry>;
group: Maybe<Scalars['String']>;
id: Scalars['Int'];
@@ -555,7 +555,7 @@ export type Year = {
value: Scalars['Int'];
};
export type BracketThemeSummaryCardConstestantFragment = { name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null };
export type BracketThemeSummaryCardConstestantFragment = { name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null };
export type VideoButtonAnimeFragment = { slug: string };
@@ -567,7 +567,7 @@ export type VideoButtonVideoFragment = { id: number, tags: string, resolution: n
export type AnimeSummaryCardAnimeFragment = { slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null }>, images: Array<{ link: string, facet: string | null }> };
export type AnimeSummaryCardAnimeExpandableFragment = { themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }> };
export type AnimeSummaryCardAnimeExpandableFragment = { themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string }, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }> };
export type ArtistSummaryCardArtistFragment = { slug: string, name: string, images: Array<{ link: string, facet: string | null }> };
@@ -575,26 +575,26 @@ export type PlaylistSummaryCardPlaylistFragment = { id: string, name: string, vi
export type PlaylistSummaryCardShowOwnerFragment = { user: { name: string } };
export type ThemeDetailCardThemeFragment = { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> };
export type ThemeDetailCardThemeFragment = { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> };
export type ThemeSummaryCardThemeFragment = { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> };
export type ThemeSummaryCardThemeFragment = { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> };
export type ThemeSummaryCardArtistFragment = { slug: string };
export type ThemeSummaryCardThemeExpandableFragment = { type: string, sequence: number | null, slug: string, anime: { slug: string } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null };
export type ThemeSummaryCardThemeExpandableFragment = { type: string, sequence: number | null, slug: string, anime: { slug: string }, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null };
export type ThemeSummaryCardQueryVariables = Exact<{
themeId: Scalars['Int'];
}>;
export type ThemeSummaryCardQuery = { theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null };
export type ThemeSummaryCardQuery = { theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null };
export type VideoSummaryCardVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } | null }>, audio: { basename: string } };
export type VideoSummaryCardVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } };
export type PlaylistRemoveDialogPlaylistFragment = { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number };
export type PlaylistTrackAddDialogVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } | null }>, audio: { basename: string } };
export type PlaylistTrackAddDialogVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } };
export type PlaylistTrackAddFormPlaylistQueryVariables = Exact<{
filterVideoId: Scalars['Int'];
@@ -605,11 +605,11 @@ export type PlaylistTrackAddFormPlaylistQuery = { me: { playlistAll: Array<{ id:
export type PlaylistTrackRemoveDialogPlaylistFragment = { id: string, name: string };
export type PlaylistTrackRemoveDialogVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } | null }>, audio: { basename: string } };
export type PlaylistTrackRemoveDialogVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } };
export type FeaturedThemeThemeFragment = { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, entries: Array<{ version: number | null, videos: Array<{ basename: string, id: number, tags: string, audio: { basename: string } }> }>, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null };
export type FeaturedThemeThemeFragment = { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, entries: Array<{ version: number | null, videos: Array<{ basename: string, id: number, tags: string, audio: { basename: string } }> }>, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null };
export type AnimeThemeFilterThemeFragment = { type: string, group: string | null, slug: string, sequence: number | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> };
export type AnimeThemeFilterThemeFragment = { type: string, group: string | null, slug: string, sequence: number | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> };
type CoverImageResourceWithImages_Anime_Fragment = { images: Array<{ link: string, facet: string | null }> };
@@ -631,14 +631,14 @@ export type ProfileImageUserFragment = { name: string, email: string };
export type StudioCoverImageStudioFragment = { images: Array<{ link: string, facet: string | null }>, anime: Array<{ name: string, images: Array<{ link: string, facet: string | null }> }> };
export type ThemeMenuThemeFragment = { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> };
export type ThemeMenuThemeFragment = { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> };
export type SearchAnimeQueryVariables = Exact<{
args: SearchArgs;
}>;
export type SearchAnimeQuery = { searchAnime: { nextPage: number | null, data: Array<{ slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }>, images: Array<{ link: string, facet: string | null }> }> } };
export type SearchAnimeQuery = { searchAnime: { nextPage: number | null, data: Array<{ slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string }, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }>, images: Array<{ link: string, facet: string | null }> }> } };
export type SearchArtistQueryVariables = Exact<{
args: SearchArgs;
@@ -652,7 +652,7 @@ export type SearchGlobalQueryVariables = Exact<{
}>;
export type SearchGlobalQuery = { search: { anime: Array<{ slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }>, images: Array<{ link: string, facet: string | null }> }>, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }>, artists: Array<{ slug: string, name: string, images: Array<{ link: string, facet: string | null }> }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, playlists: Array<{ id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, user: { name: string } }> } };
export type SearchGlobalQuery = { search: { anime: Array<{ slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string }, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }>, images: Array<{ link: string, facet: string | null }> }>, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }>, artists: Array<{ slug: string, name: string, images: Array<{ link: string, facet: string | null }> }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, playlists: Array<{ id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, user: { name: string } }> } };
export type SearchPlaylistQueryVariables = Exact<{
args: SearchArgs;
@@ -680,9 +680,9 @@ export type SearchThemeQueryVariables = Exact<{
}>;
export type SearchThemeQuery = { searchTheme: { nextPage: number | null, data: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }> } };
export type SearchThemeQuery = { searchTheme: { nextPage: number | null, data: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }> } };
export type ThemeTableThemeFragment = { type: string, sequence: number | null, slug: string, anime: { slug: string } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null };
export type ThemeTableThemeFragment = { type: string, sequence: number | null, slug: string, anime: { slug: string }, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null };
export type ContentWarningTagsEntryFragment = { spoiler: boolean, nsfw: boolean };
@@ -696,11 +696,11 @@ export type PlaylistRemoveToastPlaylistFragment = { id: string, name: string };
export type PlaylistTrackAddToastPlaylistFragment = { id: string, name: string };
export type PlaylistTrackAddToastVideoFragment = { entries: Array<{ theme: { song: { title: string | null } | null } | null }> };
export type PlaylistTrackAddToastVideoFragment = { entries: Array<{ theme: { song: { title: string | null } | null } }> };
export type PlaylistTrackRemoveToastPlaylistFragment = { id: string, name: string };
export type PlaylistTrackRemoveToastVideoFragment = { entries: Array<{ theme: { song: { title: string | null } | null } | null }> };
export type PlaylistTrackRemoveToastVideoFragment = { entries: Array<{ theme: { song: { title: string | null } | null } }> };
export type PerformancesSongFragment = { performances: Array<{ as: string | null, artist: { slug: string, name: string } }> };
@@ -730,7 +730,7 @@ export type CheckAuthQuery = { me: { user: { id: number, name: string, email: st
export type RandomThemeQueryVariables = Exact<{ [key: string]: never; }>;
export type RandomThemeQuery = { searchTheme: { data: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ basename: string, id: number, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null }> } };
export type RandomThemeQuery = { searchTheme: { data: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ basename: string, id: number, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null }> } };
export type DumpIndexPageQueryVariables = Exact<{ [key: string]: never; }>;
@@ -758,33 +758,33 @@ export type TransparencyPageAllQueryVariables = Exact<{ [key: string]: never; }>
export type TransparencyPageAllQuery = { balanceAll: Array<{ id: number, date: string, service: string, frequency: string, usage: number, month_to_date_balance: number }>, transactionAll: Array<{ id: number, date: string, service: string, description: string, amount: number }> };
export type VideoPageAnimeFragment = { name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null }>, audio: { basename: string }, script: { link: string } | null }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> };
export type VideoPageAnimeFragment = { name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } }>, audio: { basename: string }, script: { link: string } | null }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> };
export type VideoPageQueryVariables = Exact<{
animeSlug: Scalars['String'];
}>;
export type VideoPageQuery = { anime: { name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null }>, audio: { basename: string }, script: { link: string } | null }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> } | null };
export type VideoPageQuery = { anime: { name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } }>, audio: { basename: string }, script: { link: string } | null }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> } | null };
export type VideoPageAllQueryVariables = Exact<{ [key: string]: never; }>;
export type VideoPageAllQuery = { animeAll: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null }>, audio: { basename: string }, script: { link: string } | null }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> }> };
export type VideoPageAllQuery = { animeAll: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } }>, audio: { basename: string }, script: { link: string } | null }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> }> };
export type AnimeDetailPageAnimeFragment = { slug: string, name: string, season: string | null, year: number | null, synopsis: string | null, synonyms: Array<{ text: string | null }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, resources: Array<{ site: string | null, link: string | null, as: string | null }>, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }>, images: Array<{ link: string, facet: string | null }> };
export type AnimeDetailPageAnimeFragment = { slug: string, name: string, season: string | null, year: number | null, synopsis: string | null, synonyms: Array<{ text: string | null }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, resources: Array<{ site: string | null, link: string | null, as: string | null }>, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }>, images: Array<{ link: string, facet: string | null }> };
export type AnimeDetailPageQueryVariables = Exact<{
animeSlug: Scalars['String'];
}>;
export type AnimeDetailPageQuery = { anime: { slug: string, name: string, season: string | null, year: number | null, synopsis: string | null, synonyms: Array<{ text: string | null }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, resources: Array<{ site: string | null, link: string | null, as: string | null }>, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }>, images: Array<{ link: string, facet: string | null }> } | null };
export type AnimeDetailPageQuery = { anime: { slug: string, name: string, season: string | null, year: number | null, synopsis: string | null, synonyms: Array<{ text: string | null }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, resources: Array<{ site: string | null, link: string | null, as: string | null }>, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }>, images: Array<{ link: string, facet: string | null }> } | null };
export type AnimeDetailPageAllQueryVariables = Exact<{ [key: string]: never; }>;
export type AnimeDetailPageAllQuery = { animeAll: Array<{ slug: string, name: string, season: string | null, year: number | null, synopsis: string | null, synonyms: Array<{ text: string | null }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, resources: Array<{ site: string | null, link: string | null, as: string | null }>, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }>, images: Array<{ link: string, facet: string | null }> }> };
export type AnimeDetailPageAllQuery = { animeAll: Array<{ slug: string, name: string, season: string | null, year: number | null, synopsis: string | null, synonyms: Array<{ text: string | null }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, resources: Array<{ site: string | null, link: string | null, as: string | null }>, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }>, images: Array<{ link: string, facet: string | null }> }> };
export type AnimeIndexPageQueryVariables = Exact<{ [key: string]: never; }>;
@@ -798,35 +798,35 @@ export type RevalidateAnimeQueryVariables = Exact<{
export type RevalidateAnimeQuery = { anime: { year: number | null, season: string | null, series: Array<{ slug: string }>, studios: Array<{ slug: string }>, themes: Array<{ slug: string, song: { performances: Array<{ artist: { slug: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> }> } | null };
export type ArtistDetailPageArtistFragment = { slug: string, name: string, performances: Array<{ as: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }>, themes: Array<{ id: number, slug: string, group: string | null, type: string, sequence: number | null, anime: { slug: string, year: number | null, season: string | null, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }> } }>, members: Array<{ as: string | null, member: { slug: string, name: string } }>, groups: Array<{ as: string | null, group: { slug: string, name: string } }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ facet: string | null, link: string }> };
export type ArtistDetailPageArtistFragment = { slug: string, name: string, performances: Array<{ as: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }>, themes: Array<{ id: number, slug: string, group: string | null, type: string, sequence: number | null, anime: { slug: string, year: number | null, season: string | null, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }> } }>, members: Array<{ as: string | null, member: { slug: string, name: string } }>, groups: Array<{ as: string | null, group: { slug: string, name: string } }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ facet: string | null, link: string }> };
export type ArtistDetailPageQueryVariables = Exact<{
artistSlug: Scalars['String'];
}>;
export type ArtistDetailPageQuery = { artist: { slug: string, name: string, performances: Array<{ as: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }>, themes: Array<{ id: number, slug: string, group: string | null, type: string, sequence: number | null, anime: { slug: string, year: number | null, season: string | null, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }> } }>, members: Array<{ as: string | null, member: { slug: string, name: string } }>, groups: Array<{ as: string | null, group: { slug: string, name: string } }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ facet: string | null, link: string }> } | null };
export type ArtistDetailPageQuery = { artist: { slug: string, name: string, performances: Array<{ as: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }>, themes: Array<{ id: number, slug: string, group: string | null, type: string, sequence: number | null, anime: { slug: string, year: number | null, season: string | null, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }> } }>, members: Array<{ as: string | null, member: { slug: string, name: string } }>, groups: Array<{ as: string | null, group: { slug: string, name: string } }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ facet: string | null, link: string }> } | null };
export type ArtistDetailPageAllQueryVariables = Exact<{ [key: string]: never; }>;
export type ArtistDetailPageAllQuery = { artistAll: Array<{ slug: string, name: string, performances: Array<{ as: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }>, themes: Array<{ id: number, slug: string, group: string | null, type: string, sequence: number | null, anime: { slug: string, year: number | null, season: string | null, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }> } }>, members: Array<{ as: string | null, member: { slug: string, name: string } }>, groups: Array<{ as: string | null, group: { slug: string, name: string } }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ facet: string | null, link: string }> }> };
export type ArtistDetailPageAllQuery = { artistAll: Array<{ slug: string, name: string, performances: Array<{ as: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }>, themes: Array<{ id: number, slug: string, group: string | null, type: string, sequence: number | null, anime: { slug: string, year: number | null, season: string | null, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ id: number, basename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }> } }>, members: Array<{ as: string | null, member: { slug: string, name: string } }>, groups: Array<{ as: string | null, group: { slug: string, name: string } }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ facet: string | null, link: string }> }> };
export type ArtistIndexPageQueryVariables = Exact<{ [key: string]: never; }>;
export type ArtistIndexPageQuery = { artistAll: Array<{ slug: string, name: string }> };
export type CharacterFragmentFragment = { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null };
export type CharacterFragmentFragment = { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null };
export type RoundFragmentFragment = { tier: number, name: string | null, pairings: Array<{ order: number, group: number, votesA: number | null, votesB: number | null, characterA: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null }, characterB: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null } }> };
export type RoundFragmentFragment = { tier: number, name: string | null, pairings: Array<{ order: number, group: number, votesA: number | null, votesB: number | null, characterA: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null }, characterB: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null } }> };
export type BracketPageQueryVariables = Exact<{
bracketSlug: Scalars['String'];
}>;
export type BracketPageQuery = { bracket: { name: string, currentGroup: number | null, currentRound: { tier: number, name: string | null, pairings: Array<{ order: number, group: number, votesA: number | null, votesB: number | null, characterA: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null }, characterB: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null } }> } | null, rounds: Array<{ tier: number, name: string | null, pairings: Array<{ order: number, group: number, votesA: number | null, votesB: number | null, characterA: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null }, characterB: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null } }> }> } | null };
export type BracketPageQuery = { bracket: { name: string, currentGroup: number | null, currentRound: { tier: number, name: string | null, pairings: Array<{ order: number, group: number, votesA: number | null, votesB: number | null, characterA: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null }, characterB: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null } }> } | null, rounds: Array<{ tier: number, name: string | null, pairings: Array<{ order: number, group: number, votesA: number | null, votesB: number | null, characterA: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null }, characterB: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null } }> }> } | null };
export type BracketPageAllQueryVariables = Exact<{ [key: string]: never; }>;
@@ -838,7 +838,7 @@ export type AwardPageThemeQueryVariables = Exact<{
}>;
export type AwardPageThemeQuery = { theme: { id: number, slug: string, anime: { slug: string, name: string, images: Array<{ facet: string | null, link: string }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ basename: string, tags: string }> }> } | null };
export type AwardPageThemeQuery = { theme: { id: number, slug: string, anime: { slug: string, name: string, images: Array<{ facet: string | null, link: string }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ basename: string, tags: string }> }> } | null };
export type EventPageQueryVariables = Exact<{ [key: string]: never; }>;
@@ -848,31 +848,31 @@ export type EventPageQuery = { bracketAll: Array<{ slug: string, name: string }>
export type HomePageRecentlyAddedQueryVariables = Exact<{ [key: string]: never; }>;
export type HomePageRecentlyAddedQuery = { videoAll: Array<{ id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } | null }>, audio: { basename: string } }> };
export type HomePageRecentlyAddedQuery = { videoAll: Array<{ id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } }> };
export type HomePageMostViewedQueryVariables = Exact<{ [key: string]: never; }>;
export type HomePageMostViewedQuery = { videoAll: Array<{ id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } | null }>, audio: { basename: string } }> };
export type HomePageMostViewedQuery = { videoAll: Array<{ id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } }> };
export type HomePageQueryVariables = Exact<{ [key: string]: never; }>;
export type HomePageQuery = { featuredTheme: { theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, entries: Array<{ version: number | null, videos: Array<{ basename: string, id: number, tags: string, audio: { basename: string } }> }>, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } } | null, announcementAll: Array<{ content: string }> };
export type HomePageQuery = { featuredTheme: { theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, entries: Array<{ version: number | null, videos: Array<{ basename: string, id: number, tags: string, audio: { basename: string } }> }>, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } } | null, announcementAll: Array<{ content: string }> };
export type PlaylistDetailPagePlaylistQueryVariables = Exact<{
playlistId: Scalars['String'];
}>;
export type PlaylistDetailPagePlaylistQuery = { playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } | null }>, audio: { basename: string } } }>, user: { name: string } } | null };
export type PlaylistDetailPagePlaylistQuery = { playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } } }>, user: { name: string } } | null };
export type PlaylistDetailPageMeQueryVariables = Exact<{ [key: string]: never; }>;
export type PlaylistDetailPageMeQuery = { me: { user: { name: string } | null } };
export type PlaylistDetailPagePlaylistFragment = { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } | null }>, audio: { basename: string } } }>, user: { name: string } };
export type PlaylistDetailPagePlaylistFragment = { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } } }>, user: { name: string } };
type PlaylistDetailPageUser_UserAuth_Fragment = { name: string };
@@ -885,7 +885,7 @@ export type PlaylistDetailPageQueryVariables = Exact<{
}>;
export type PlaylistDetailPageQuery = { playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } | null }>, audio: { basename: string } } }>, user: { name: string } } | null, me: { user: { name: string } | null } };
export type PlaylistDetailPageQuery = { playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, slug: string, type: string, sequence: number | null, group: string | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } } }>, user: { name: string } } | null, me: { user: { name: string } | null } };
export type GalleryPageQueryVariables = Exact<{ [key: string]: never; }>;
@@ -906,38 +906,38 @@ export type ProfilePageQueryVariables = Exact<{ [key: string]: never; }>;
export type ProfilePageQuery = { me: { user: { name: string, email: string, email_verified_at: string | null, created_at: string, roles: Array<{ name: string, color: string | null, priority: number | null, default: boolean }> } | null, playlistAll: Array<{ id: string, name: string, visibility: PlaylistVisibility, tracks_count: number }> | null } };
export type SeriesDetailPageSeriesFragment = { slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string } | null, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }> };
export type SeriesDetailPageSeriesFragment = { slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string }, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }> };
export type SeriesDetailPageQueryVariables = Exact<{
seriesSlug: Scalars['String'];
}>;
export type SeriesDetailPageQuery = { series: { slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string } | null, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }> } | null };
export type SeriesDetailPageQuery = { series: { slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string }, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }> } | null };
export type SeriesDetailPageAllQueryVariables = Exact<{ [key: string]: never; }>;
export type SeriesDetailPageAllQuery = { seriesAll: Array<{ slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string } | null, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }> }> };
export type SeriesDetailPageAllQuery = { seriesAll: Array<{ slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string }, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }> }> };
export type SeriesIndexPageQueryVariables = Exact<{ [key: string]: never; }>;
export type SeriesIndexPageQuery = { seriesAll: Array<{ slug: string, name: string }> };
export type StudioDetailPageStudioFragment = { slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string } | null, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ link: string, facet: string | null }> };
export type StudioDetailPageStudioFragment = { slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string }, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ link: string, facet: string | null }> };
export type StudioDetailPageQueryVariables = Exact<{
studioSlug: Scalars['String'];
}>;
export type StudioDetailPageQuery = { studio: { slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string } | null, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ link: string, facet: string | null }> } | null };
export type StudioDetailPageQuery = { studio: { slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string }, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ link: string, facet: string | null }> } | null };
export type StudioDetailPageAllQueryVariables = Exact<{ [key: string]: never; }>;
export type StudioDetailPageAllQuery = { studioAll: Array<{ slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string } | null, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ link: string, facet: string | null }> }> };
export type StudioDetailPageAllQuery = { studioAll: Array<{ slug: string, name: string, anime: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, anime: { slug: string }, song: { title: string | null } | null }>, images: Array<{ facet: string | null, link: string }> }>, resources: Array<{ link: string | null, site: string | null, as: string | null }>, images: Array<{ link: string, facet: string | null }> }> };
export type StudioIndexPageQueryVariables = Exact<{ [key: string]: never; }>;
@@ -967,7 +967,7 @@ export type SeasonDetailPageQueryVariables = Exact<{
}>;
export type SeasonDetailPageQuery = { year: { value: number, seasons: Array<{ value: string }> } | null, season: { value: string, anime: Array<{ slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }>, images: Array<{ link: string, facet: string | null }> }> } | null, yearAll: Array<{ value: number }> };
export type SeasonDetailPageQuery = { year: { value: number, seasons: Array<{ value: string }> } | null, season: { value: string, anime: Array<{ slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string }, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }>, images: Array<{ link: string, facet: string | null }> }> } | null, yearAll: Array<{ value: number }> };
export type SeasonDetailPageAllQueryVariables = Exact<{ [key: string]: never; }>;
@@ -979,7 +979,7 @@ export type YearDetailPageQueryVariables = Exact<{
}>;
export type YearDetailPageQuery = { year: { value: number, seasons: Array<{ value: string, anime: Array<{ slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }>, images: Array<{ link: string, facet: string | null }> }> }> } | null, yearAll: Array<{ value: number }> };
export type YearDetailPageQuery = { year: { value: number, seasons: Array<{ value: string, anime: Array<{ slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null, type: string, sequence: number | null, slug: string, anime: { slug: string }, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }>, song: { title: string | null } | null }>, images: Array<{ link: string, facet: string | null }> }> }> } | null, yearAll: Array<{ value: number }> };
export type YearDetailPageAllQueryVariables = Exact<{ [key: string]: never; }>;
+28
View File
@@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
export default function useMouseRelax() {
const [isRelaxed, setRelaxed] = useState(false);
useEffect(() => {
let timeoutId: number;
function onMouseMove() {
setRelaxed(false);
clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => setRelaxed(true), 2500);
}
addEventListener("pointermove", onMouseMove);
addEventListener("pointerdown", onMouseMove);
return () => {
removeEventListener("pointermove", onMouseMove);
removeEventListener("pointerdown", onMouseMove);
clearTimeout(timeoutId);
};
}, []);
return isRelaxed;
}
+1 -1
View File
@@ -106,7 +106,7 @@ export const searchResolvers = {
searchParams.append("fields[anime]", "name,slug,year,season");
searchParams.append("fields[animetheme]", "type,sequence,slug,group,id");
searchParams.append("fields[animethemeentry]", "version,episodes,spoiler,nsfw");
searchParams.append("fields[video]", "id,tags,resolution,nc,subbed,lyrics,uncen,source,overlap");
searchParams.append("fields[video]", "id,tags,resolution,nc,subbed,lyrics,uncen,source,overlap,basename");
searchParams.append("fields[image]", "facet,link");
searchParams.append("fields[song]", "title");
searchParams.append("fields[artist]", "name,slug");
+2 -2
View File
@@ -78,7 +78,7 @@ const typeDefs = `
sequence: Int
group: String
song: Song
anime: Anime
anime: Anime!
entries: [Entry!]!
}
@@ -88,7 +88,7 @@ const typeDefs = `
episodes: String
nsfw: Boolean!
spoiler: Boolean!
theme: Theme
theme: Theme!
videos: [Video!]!
}
+32 -5
View File
@@ -31,6 +31,9 @@ import { LazyMotion } from "framer-motion";
import { VideoPlayer2 } from "components/video-player-2/VideoPlayer2";
import { PageRevalidation } from "components/utils/PageRevalidation";
import type { VideoPageProps } from "pages/anime/[animeSlug]/[videoSlug]";
import FullscreenContext from "context/fullscreenContext";
import { VideoPlayerOverlay } from "components/video-player-2/VideoPlayerOverlay";
import { useFullscreen } from "ahooks";
import "@fortawesome/fontawesome-svg-core/styles.css";
import "styles/prism.scss";
@@ -86,16 +89,35 @@ export default function MyApp({ Component, pageProps }: AppProps) {
return [];
});
const [watchListFactory, setWatchListFactory] = useState<(() => Promise<WatchListItem[]>) | null>(null);
const [currentWatchListItem, setCurrentWatchListItem] = useState<WatchListItem | null>(() => {
const [currentWatchListItemId, setCurrentWatchListItemId] = useState<number | null>(() => {
if (watchList.length) {
return watchList[0];
return watchList[0].watchListId;
}
return null;
});
const currentWatchListItem = watchList.find((item) => item.watchListId === currentWatchListItemId) ?? null;
const currentBasename = getBasename(pageProps);
const [previousBasename, setPreviousBasename] = useState<string | null>(() => getBasename(pageProps));
const [isFullscreen, { toggleFullscreen }] = useFullscreen(
() => document.documentElement,
{
onEnter() {
document.documentElement.dataset.fullscreen = "true";
},
onExit() {
delete document.documentElement.dataset.fullscreen;
},
}
);
useEffect(() => {
if (!isVideoPage && isFullscreen) {
toggleFullscreen();
}
}, [isFullscreen, isVideoPage, toggleFullscreen]);
useEffect(() => {
const hotkeyListener = (event: KeyboardEvent) => {
if (!pageProps.isSearch && ((event.key === "s" && event.ctrlKey) || event.key === "/")) {
@@ -136,7 +158,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
setWatchList(watchList);
setWatchListFactory(null);
setCurrentWatchListItem(watchList[0]);
setCurrentWatchListItemId(watchList[0].watchListId);
}
return null;
@@ -145,6 +167,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
return (
<MultiContextProvider providers={[
stackContext(ThemeProvider, { theme }),
stackContext(FullscreenContext.Provider, { value: { isFullscreen, toggleFullscreen } }),
stackContext(ColorThemeContext.Provider, { value: { colorTheme, setColorTheme } }),
stackContext(PlayerContext.Provider, { value: {
currentVideo: null,
@@ -156,7 +179,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
setWatchListFactory: (factory) => setWatchListFactory(() => factory),
currentWatchListItem,
setCurrentWatchListItem: (watchListItem) => {
setCurrentWatchListItem(watchListItem);
setCurrentWatchListItemId(watchListItem?.watchListId ?? null);
if (watchListFactory && watchList.findIndex((item) => item.watchListId === watchListItem?.watchListId) === watchList.length - 1) {
watchListFactory().then((nextWatchList) => {
setWatchList([
@@ -230,7 +253,11 @@ export default function MyApp({ Component, pageProps }: AppProps) {
</>
) : null}
{currentWatchListItem && (
<VideoPlayer2 video={currentWatchListItem} background={!isVideoPage}>
<VideoPlayer2
video={currentWatchListItem}
background={!isVideoPage}
overlay={isVideoPage ? <VideoPlayerOverlay {...pageProps} /> : null}
>
{isVideoPage ? (
<Component {...pageProps}/>
) : null}
@@ -16,8 +16,7 @@ import { SwitcherOption } from "components/switcher/Switcher";
import { Column, Row } from "components/box";
import { VideoSummaryCard } from "components/card/VideoSummaryCard";
import { Text } from "components/text";
import { ThemeEntryTags } from "components/tag/ThemeEntryTags";
import { VideoButton } from "components/button";
import { IconTextButton } from "components/button";
import { useContext, useState } from "react";
import PlayerContext from "context/playerContext";
import { DeveloperMode } from "utils/settings";
@@ -26,6 +25,7 @@ import useSetting from "hooks/useSetting";
import { SEO } from "components/seo";
import extractImages from "utils/extractImages";
import { VIDEO_URL } from "utils/config";
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
export interface VideoPageProps extends SharedPageProps, RequiredNonNullable<VideoPageQuery> {
themeIndex: number
@@ -52,27 +52,16 @@ export default function VideoPage({ anime, themeIndex, entryIndex, videoIndex, l
const [selectedTab, setSelectedTab] = useState<"watch-list" | "info" | "related">(() => {
return watchList.length > 1 ? "watch-list" : "info";
});
const [showMoreRelatedThemes, setShowMoreRelatedThemes] = useState(false);
const relatedThemes = anime.themes
.filter((relatedTheme) => relatedTheme.slug !== theme.slug);
.filter((relatedTheme) => relatedTheme.slug !== theme.slug)
.slice(0, showMoreRelatedThemes ? undefined : 4);
const usedAlsoAs = video.entries
.map((entry) => entry.theme)
.filter((otherTheme) => otherTheme?.anime && otherTheme.anime.slug !== anime.slug);
const otherEntries = theme.entries.map(otherEntry => {
const videos = otherEntry.videos.filter((otherVideo) => otherVideo.filename !== video.filename);
if (!videos.length) {
return null;
}
return {
...otherEntry,
videos
};
}).filter((otherEntry) => !!otherEntry);
const pageTitle = entry.version
? `${songTitle} (${anime.name} ${theme.slug} v${entry.version})`
: `${songTitle} (${anime.name} ${theme.slug})`;
@@ -190,34 +179,16 @@ export default function VideoPage({ anime, themeIndex, entryIndex, videoIndex, l
{relatedThemes.map((theme) => (
<ThemeSummaryCard key={theme.slug} theme={{ ...theme, anime }}/>
))}
</>
)}
{!!otherEntries.length && (
<>
<Text variant="h2">Other versions</Text>
<Column style={{ "--gap": "32px" }}>
{otherEntries.map((otherEntry) => otherEntry ? (
<Column style={{ "--gap": "16px" }} key={otherEntry.version ?? 1}>
<Text color="text-muted">
<Row style={{ "--gap": "8px", "--align-items": "baseline" }}>
<Text variant="small">Version {otherEntry.version || 1}</Text>
<ThemeEntryTags entry={otherEntry}/>
</Row>
</Text>
<Row $wrap style={{ "--gap": "16px" }}>
{otherEntry.videos.map((video, index) => (
<VideoButton
key={index}
anime={anime}
theme={theme}
entry={otherEntry}
video={video}
/>
))}
</Row>
</Column>
) : null)}
</Column>
{anime.themes.length > 4 ? (
<Row style={{ "--justify-content": "center" }}>
<IconTextButton
icon={showMoreRelatedThemes ? faChevronUp : faChevronDown}
variant="silent"
isCircle
onClick={() => setShowMoreRelatedThemes(!showMoreRelatedThemes)}
/>
</Row>
) : null}
</>
)}
</Column>
+5 -1
View File
@@ -39,7 +39,11 @@ export default createGlobalStyle`
`}
}
html, body, #__next {
html {
height: 100dvh;
}
body, #__next {
// Every container in the page hierarchy needs to have the
// maximum height of the viewport. Using 100vh on innermost container
// would break certain layouts on mobile devices.
+2
View File
@@ -1,4 +1,5 @@
interface BaseColors {
"transparent": string
"white": string
"gray-100": string
"gray-200": string
@@ -52,6 +53,7 @@ export interface Shadows {
}
const baseColors: BaseColors = {
"transparent": "#00000000",
"white": "#ffffff",
"gray-100": "#f5f2fa",
"gray-200": "#e6e1f0",