Add a basic bottom scrubber that displays thumbnails

This commit is contained in:
Zoe Roux 2024-01-22 16:11:24 +01:00
parent d3d59443fa
commit 733d6c8c7b
7 changed files with 204 additions and 5 deletions

View File

@ -24,7 +24,6 @@ import {
QueryClient,
QueryFunctionContext,
useInfiniteQuery,
useMutation,
useQuery,
} from "@tanstack/react-query";
import { z } from "zod";
@ -49,6 +48,7 @@ export const queryFn = async <Data,>(
authenticated?: boolean;
apiUrl?: string;
timeout?: number;
plainText?: boolean;
},
type?: z.ZodType<Data>,
token?: string | null,
@ -112,12 +112,14 @@ export const queryFn = async <Data,>(
// @ts-expect-error Assume Data is nullable.
if (resp.status === 204) return null;
if ("plainText" in context && context.plainText) return (await resp.text()) as unknown as Data;
let data;
try {
data = await resp.json();
} catch (e) {
console.error("Invald json from kyoo", e);
throw { errors: ["Invalid repsonse from kyoo"] };
console.error("Invalid json from kyoo", e);
throw { errors: ["Invalid response from kyoo"] };
}
if (!type) return data;
const parsed = await type.safeParseAsync(data);
@ -170,6 +172,7 @@ export type QueryIdentifier<T = unknown, Ret = T> = {
placeholderData?: T | (() => T);
enabled?: boolean;
timeout?: number;
options?: Partial<Parameters<typeof queryFn>[0]>;
};
export type QueryPage<Props = {}, Items = unknown> = ComponentType<
@ -203,7 +206,7 @@ export const toQueryKey = (query: {
export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
return useQuery<Data, KyooErrors>({
queryKey: toQueryKey(query),
queryFn: (ctx) => queryFn({ ...ctx, timeout: query.timeout }, query.parser),
queryFn: (ctx) => queryFn({ ...ctx, timeout: query.timeout, ...query.options }, query.parser),
placeholderData: query.placeholderData as any,
enabled: query.enabled,
});

View File

@ -0,0 +1,49 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import FImage from "react-native-fast-image";
export const FastImage = ({
src,
alt,
width,
height,
style,
...props
}: {
src: string;
alt: string;
width: number | string;
height: number | string;
style?: object;
}) => {
return (
<FImage
source={{
uri: src,
priority: FImage.priority.low,
}}
accessibilityLabel={alt}
resizeMode={FImage.resizeMode.cover}
style={style}
{...props}
/>
);
};

View File

@ -0,0 +1,44 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import NextImage from "next/image";
export const FastImage = ({
src,
alt,
style,
...props
}: {
src: string;
alt: string;
style?: object;
}) => {
return (
<NextImage
src={src}
priority={false}
alt={alt!}
// Don't use next's server to reprocess images, they are already optimized by kyoo.
unoptimized={true}
style={style}
{...props}
/>
);
};

View File

@ -27,6 +27,7 @@ import { ContrastArea } from "../themes";
import { percent } from "yoshiki/native";
import { imageBorderRadius } from "../constants";
export { FastImage } from "./fast-image";
export { BlurhashContainer } from "./blurhash";
export { type Props as ImageProps, Image };

View File

@ -51,6 +51,7 @@ import {
} from "../state";
import { ReactNode, useCallback, useEffect, useRef } from "react";
import { atom } from "jotai";
import { BottomScrubber } from "./scrubber";
const hoverReasonAtom = atom({
mouseMoved: false,
@ -63,6 +64,7 @@ export const hoverAtom = atom((get) =>
export const Hover = ({
isLoading,
url,
name,
showName,
poster,
@ -75,6 +77,7 @@ export const Hover = ({
qualitiesAvailables = true,
}: {
isLoading: boolean;
url: string;
name?: string | null;
showName?: string;
poster?: KyooImage | null;
@ -170,6 +173,7 @@ export const Hover = ({
}}
/>
</View>
<BottomScrubber url={url}/>
</View>
</View>
</View>

View File

@ -0,0 +1,98 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { useFetch, QueryIdentifier, imageFn } from "@kyoo/models";
import { FastImage } from "@kyoo/primitives";
import { Platform, View } from "react-native";
import { percent, useYoshiki, vh } from "yoshiki/native";
import { ErrorView } from "../../fetch";
import { useMemo } from "react";
import { CssObject } from "yoshiki/src/web/generator";
type Thumb = { to: number; url: string; x: number; y: number; width: number; height: number };
export const BottomScrubber = ({ url }: { url: string }) => {
const { css } = useYoshiki();
const { data, error } = useFetch(BottomScrubber.query(url));
const info = useMemo(() => {
if (!data) return [];
const lines = data.split("\n").filter((x) => x);
lines.shift();
/* lines now contains something like
*
* 00:00:00.000 --> 00:00:01.000
* image1.png#xywh=0,0,190,120
* 00:00:01.000 --> 00:00:02.000
* image1.png#xywh=190,0,190,120
*/
const ret = new Array<Thumb>(lines.length / 2);
for (let i = 0; i < ret.length; i++) {
const times = lines[i * 2].split(" --> ");
const timesV = times[1].split(":");
const ts =
(parseInt(timesV[0]) * 3600 + parseInt(timesV[1]) * 60 + parseFloat(timesV[2])) * 1000;
const url = lines[i * 2 + 1].split("#xywh=");
const xywh = url[1].split(",").map((x) => parseInt(x));
ret[i] = {
to: ts,
url: imageFn("/video/" + url[0]),
x: xywh[0],
y: xywh[1],
width: xywh[2],
height: xywh[3],
};
}
return ret;
}, [data]);
if (error) return <ErrorView error={error} />;
return (
<View {...css({ flexDirection: "row", width: percent(100) })}>
{info.map((thumb) => (
<FastImage
key={thumb.to}
src={thumb.url}
alt=""
width={thumb.width}
height={thumb.height}
style={
Platform.OS === "web"
? ({
objectFit: "none",
objectPosition: `${-thumb.x}px ${-thumb.y}px`,
} as CssObject)
: undefined
}
/>
))}
</View>
);
};
BottomScrubber.query = (url: string): QueryIdentifier<string> => ({
path: ["video", url, "thumbnails.vtt"],
parser: null!,
options: {
plainText: true,
},
});

View File

@ -157,7 +157,7 @@ export const Player = ({ slug, type }: { slug: string; type: "episode" | "movie"
{...css(StyleSheet.absoluteFillObject)}
/>
<LoadingIndicator />
<Hover {...mapData(data, info, previous, next)} />
<Hover {...mapData(data, info, previous, next)} url={`${type}/${slug}`} />
</View>
</>
);