mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-04 22:24:14 -04:00
Add a basic bottom scrubber that displays thumbnails
This commit is contained in:
parent
d3d59443fa
commit
733d6c8c7b
@ -24,7 +24,6 @@ import {
|
|||||||
QueryClient,
|
QueryClient,
|
||||||
QueryFunctionContext,
|
QueryFunctionContext,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useMutation,
|
|
||||||
useQuery,
|
useQuery,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -49,6 +48,7 @@ export const queryFn = async <Data,>(
|
|||||||
authenticated?: boolean;
|
authenticated?: boolean;
|
||||||
apiUrl?: string;
|
apiUrl?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
plainText?: boolean;
|
||||||
},
|
},
|
||||||
type?: z.ZodType<Data>,
|
type?: z.ZodType<Data>,
|
||||||
token?: string | null,
|
token?: string | null,
|
||||||
@ -112,12 +112,14 @@ export const queryFn = async <Data,>(
|
|||||||
// @ts-expect-error Assume Data is nullable.
|
// @ts-expect-error Assume Data is nullable.
|
||||||
if (resp.status === 204) return null;
|
if (resp.status === 204) return null;
|
||||||
|
|
||||||
|
if ("plainText" in context && context.plainText) return (await resp.text()) as unknown as Data;
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await resp.json();
|
data = await resp.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Invald json from kyoo", e);
|
console.error("Invalid json from kyoo", e);
|
||||||
throw { errors: ["Invalid repsonse from kyoo"] };
|
throw { errors: ["Invalid response from kyoo"] };
|
||||||
}
|
}
|
||||||
if (!type) return data;
|
if (!type) return data;
|
||||||
const parsed = await type.safeParseAsync(data);
|
const parsed = await type.safeParseAsync(data);
|
||||||
@ -170,6 +172,7 @@ export type QueryIdentifier<T = unknown, Ret = T> = {
|
|||||||
placeholderData?: T | (() => T);
|
placeholderData?: T | (() => T);
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
options?: Partial<Parameters<typeof queryFn>[0]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QueryPage<Props = {}, Items = unknown> = ComponentType<
|
export type QueryPage<Props = {}, Items = unknown> = ComponentType<
|
||||||
@ -203,7 +206,7 @@ export const toQueryKey = (query: {
|
|||||||
export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
||||||
return useQuery<Data, KyooErrors>({
|
return useQuery<Data, KyooErrors>({
|
||||||
queryKey: toQueryKey(query),
|
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,
|
placeholderData: query.placeholderData as any,
|
||||||
enabled: query.enabled,
|
enabled: query.enabled,
|
||||||
});
|
});
|
||||||
|
49
front/packages/primitives/src/image/fast-image.tsx
Normal file
49
front/packages/primitives/src/image/fast-image.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
44
front/packages/primitives/src/image/fast-image.web.tsx
Normal file
44
front/packages/primitives/src/image/fast-image.web.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -27,6 +27,7 @@ import { ContrastArea } from "../themes";
|
|||||||
import { percent } from "yoshiki/native";
|
import { percent } from "yoshiki/native";
|
||||||
import { imageBorderRadius } from "../constants";
|
import { imageBorderRadius } from "../constants";
|
||||||
|
|
||||||
|
export { FastImage } from "./fast-image";
|
||||||
export { BlurhashContainer } from "./blurhash";
|
export { BlurhashContainer } from "./blurhash";
|
||||||
export { type Props as ImageProps, Image };
|
export { type Props as ImageProps, Image };
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@ import {
|
|||||||
} from "../state";
|
} from "../state";
|
||||||
import { ReactNode, useCallback, useEffect, useRef } from "react";
|
import { ReactNode, useCallback, useEffect, useRef } from "react";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
|
import { BottomScrubber } from "./scrubber";
|
||||||
|
|
||||||
const hoverReasonAtom = atom({
|
const hoverReasonAtom = atom({
|
||||||
mouseMoved: false,
|
mouseMoved: false,
|
||||||
@ -63,6 +64,7 @@ export const hoverAtom = atom((get) =>
|
|||||||
|
|
||||||
export const Hover = ({
|
export const Hover = ({
|
||||||
isLoading,
|
isLoading,
|
||||||
|
url,
|
||||||
name,
|
name,
|
||||||
showName,
|
showName,
|
||||||
poster,
|
poster,
|
||||||
@ -75,6 +77,7 @@ export const Hover = ({
|
|||||||
qualitiesAvailables = true,
|
qualitiesAvailables = true,
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
url: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
showName?: string;
|
showName?: string;
|
||||||
poster?: KyooImage | null;
|
poster?: KyooImage | null;
|
||||||
@ -170,6 +173,7 @@ export const Hover = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<BottomScrubber url={url}/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
98
front/packages/ui/src/player/components/scrubber.tsx
Normal file
98
front/packages/ui/src/player/components/scrubber.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
@ -157,7 +157,7 @@ export const Player = ({ slug, type }: { slug: string; type: "episode" | "movie"
|
|||||||
{...css(StyleSheet.absoluteFillObject)}
|
{...css(StyleSheet.absoluteFillObject)}
|
||||||
/>
|
/>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
<Hover {...mapData(data, info, previous, next)} />
|
<Hover {...mapData(data, info, previous, next)} url={`${type}/${slug}`} />
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user