mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-09-29 15:30:53 -04:00
Implement player's enEnd
& loading indicator
This commit is contained in:
parent
908b120a7c
commit
ee32ecd527
@ -7,16 +7,16 @@ export * from "./divider";
|
||||
export * from "./icons";
|
||||
export * from "./image";
|
||||
export * from "./image-background";
|
||||
// export * from "./popup";
|
||||
// export * from "./select";
|
||||
export * from "./input";
|
||||
export * from "./links";
|
||||
// export * from "./progress";
|
||||
// export * from "./slider";
|
||||
// export * from "./snackbar";
|
||||
// export * from "./alert";
|
||||
export * from "./menu";
|
||||
export * from "./progress";
|
||||
// export * from "./popup";
|
||||
export * from "./select";
|
||||
export * from "./skeleton";
|
||||
export * from "./slider";
|
||||
export * from "./text";
|
||||
export * from "./theme";
|
||||
export * from "./tooltip";
|
||||
|
@ -1,26 +1,5 @@
|
||||
/*
|
||||
* 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 { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { Circle, Svg } from "react-native-svg";
|
||||
import { px, type Stylable, useYoshiki } from "yoshiki/native";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { type Stylable, useYoshiki } from "yoshiki/native";
|
||||
|
||||
export const CircularProgress = ({
|
||||
size = 48,
|
||||
@ -28,64 +7,9 @@ export const CircularProgress = ({
|
||||
color,
|
||||
...props
|
||||
}: { size?: number; tickness?: number; color?: string } & Stylable) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { theme } = useYoshiki();
|
||||
|
||||
if (Platform.OS !== "web")
|
||||
return (
|
||||
<ActivityIndicator size={size} color={color ?? theme.accent} {...props} />
|
||||
);
|
||||
|
||||
return (
|
||||
<View {...css({ width: size, height: size, overflow: "hidden" }, props)}>
|
||||
<style jsx global>{`
|
||||
@keyframes circularProgress-svg {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes circularProgress-circle {
|
||||
0% {
|
||||
stroke-dasharray: 1px, 200px;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 100px, 200px;
|
||||
stroke-dashoffset: -15px;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 100px, 200px;
|
||||
stroke-dashoffset: -125px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<Svg
|
||||
viewBox={`${size / 2} ${size / 2} ${size} ${size}`}
|
||||
{...css(
|
||||
// @ts-ignore Web only
|
||||
Platform.OS === "web" && {
|
||||
animation: "circularProgress-svg 1.4s ease-in-out infinite",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Circle
|
||||
cx={size}
|
||||
cy={size}
|
||||
r={(size - tickness) / 2}
|
||||
strokeWidth={tickness}
|
||||
fill="none"
|
||||
stroke={color ?? theme.accent}
|
||||
strokeDasharray={[px(80), px(200)]}
|
||||
{...css(
|
||||
Platform.OS === "web" && {
|
||||
// @ts-ignore Web only
|
||||
animation: "circularProgress-circle 1.4s ease-in-out infinite",
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
@ -1,23 +1,3 @@
|
||||
/*
|
||||
* 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 ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
|
||||
import { Button } from "./button";
|
||||
import { Icon } from "./icons";
|
||||
|
@ -1,26 +1,10 @@
|
||||
/*
|
||||
* 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 { useRef, useState } from "react";
|
||||
import { type GestureResponderEvent, Platform, View } from "react-native";
|
||||
import type { ViewProps } from "react-native-svg/lib/typescript/fabric/utils";
|
||||
import {
|
||||
type GestureResponderEvent,
|
||||
Platform,
|
||||
View,
|
||||
type ViewProps,
|
||||
} from "react-native";
|
||||
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||
import { focusReset } from "./utils";
|
||||
|
||||
|
@ -1,25 +1,24 @@
|
||||
/*
|
||||
* 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 type { Audio, Chapter, KyooImage, Subtitle } from "@kyoo/models";
|
||||
import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
type ImageStyle,
|
||||
Platform,
|
||||
Pressable,
|
||||
View,
|
||||
type ViewProps,
|
||||
} from "react-native";
|
||||
import { useEvent, type VideoPlayer } from "react-native-video";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import type { AudioTrack, Chapter, KImage, Subtitle } from "~/models";
|
||||
import {
|
||||
alpha,
|
||||
CircularProgress,
|
||||
ContrastArea,
|
||||
H1,
|
||||
@ -30,43 +29,14 @@ import {
|
||||
Skeleton,
|
||||
Slider,
|
||||
Tooltip,
|
||||
alpha,
|
||||
imageBorderRadius,
|
||||
tooltip,
|
||||
ts,
|
||||
useIsTouch,
|
||||
} from "@kyoo/primitives";
|
||||
import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { atom } from "jotai";
|
||||
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type ImageStyle, Platform, Pressable, View, type ViewProps } from "react-native";
|
||||
import { useRouter } from "solito/router";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import {
|
||||
bufferedAtom,
|
||||
durationAtom,
|
||||
fullscreenAtom,
|
||||
loadAtom,
|
||||
playAtom,
|
||||
progressAtom,
|
||||
} from "../state";
|
||||
} from "~/primitives";
|
||||
import { LeftButtons, TouchControls } from "./left-buttons";
|
||||
import { RightButtons } from "./right-buttons";
|
||||
import { BottomScrubber, ScrubberTooltip } from "./scrubber";
|
||||
|
||||
const hoverReasonAtom = atom({
|
||||
mouseMoved: false,
|
||||
mouseHover: false,
|
||||
menuOpened: false,
|
||||
});
|
||||
export const hoverAtom = atom((get) =>
|
||||
[!get(playAtom), ...Object.values(get(hoverReasonAtom))].includes(true),
|
||||
);
|
||||
export const seekingAtom = atom(false);
|
||||
export const seekProgressAtom = atom<number | null>(null);
|
||||
|
||||
export const Hover = ({
|
||||
isLoading,
|
||||
url,
|
||||
@ -145,7 +115,11 @@ export const Hover = ({
|
||||
padding: percent(1),
|
||||
})}
|
||||
>
|
||||
<VideoPoster poster={poster} alt={showName} isLoading={isLoading} />
|
||||
<VideoPoster
|
||||
poster={poster}
|
||||
alt={showName}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<View
|
||||
{...css({
|
||||
marginLeft: { xs: ts(0.5), sm: ts(3) },
|
||||
@ -157,7 +131,11 @@ export const Hover = ({
|
||||
>
|
||||
{!showBottomSeeker && (
|
||||
<H2 numberOfLines={1} {...css({ paddingBottom: ts(1) })}>
|
||||
{isLoading ? <Skeleton {...css({ width: rem(15), height: rem(2) })} /> : name}
|
||||
{isLoading ? (
|
||||
<Skeleton {...css({ width: rem(15), height: rem(2) })} />
|
||||
) : (
|
||||
name
|
||||
)}
|
||||
</H2>
|
||||
)}
|
||||
<ProgressBar chapters={chapters} url={url} />
|
||||
@ -172,15 +150,24 @@ export const Hover = ({
|
||||
flexWrap: "wrap",
|
||||
})}
|
||||
>
|
||||
<LeftButtons previousSlug={previousSlug} nextSlug={nextSlug} />
|
||||
<LeftButtons
|
||||
previousSlug={previousSlug}
|
||||
nextSlug={nextSlug}
|
||||
/>
|
||||
<RightButtons
|
||||
subtitles={subtitles}
|
||||
audios={audios}
|
||||
fonts={fonts}
|
||||
onMenuOpen={() => setHover((x) => ({ ...x, menuOpened: true }))}
|
||||
onMenuOpen={() =>
|
||||
setHover((x) => ({ ...x, menuOpened: true }))
|
||||
}
|
||||
onMenuClose={() => {
|
||||
// Disable hover since the menu overlay makes the mouseout unreliable.
|
||||
setHover((x) => ({ ...x, menuOpened: false, mouseHover: false }));
|
||||
setHover((x) => ({
|
||||
...x,
|
||||
menuOpened: false,
|
||||
mouseHover: false,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@ -198,7 +185,9 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
||||
const hover = useAtomValue(hoverAtom);
|
||||
const setHover = useSetAtom(hoverReasonAtom);
|
||||
const mouseCallback = useRef<NodeJS.Timeout | null>(null);
|
||||
const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({ count: 0 });
|
||||
const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({
|
||||
count: 0,
|
||||
});
|
||||
const playerWidth = useRef<number | null>(null);
|
||||
const isTouch = useIsTouch();
|
||||
|
||||
@ -226,7 +215,8 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
||||
// It also serves to hide the tooltip.
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") return;
|
||||
if (!hover && document.activeElement instanceof HTMLElement) document.activeElement.blur();
|
||||
if (!hover && document.activeElement instanceof HTMLElement)
|
||||
document.activeElement.blur();
|
||||
}, [hover]);
|
||||
|
||||
const { css } = useYoshiki();
|
||||
@ -282,7 +272,8 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
||||
<Pressable
|
||||
tabIndex={-1}
|
||||
onPointerLeave={(e) => {
|
||||
if (e.nativeEvent.pointerType === "mouse") setHover((x) => ({ ...x, mouseMoved: false }));
|
||||
if (e.nativeEvent.pointerType === "mouse")
|
||||
setHover((x) => ({ ...x, mouseMoved: false }));
|
||||
}}
|
||||
onPress={(e) => {
|
||||
e.preventDefault();
|
||||
@ -315,7 +306,13 @@ export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ProgressBar = ({ url, chapters }: { url: string; chapters?: Chapter[] }) => {
|
||||
const ProgressBar = ({
|
||||
url,
|
||||
chapters,
|
||||
}: {
|
||||
url: string;
|
||||
chapters?: Chapter[];
|
||||
}) => {
|
||||
const [progress, setProgress] = useAtom(progressAtom);
|
||||
const buffered = useAtomValue(bufferedAtom);
|
||||
const duration = useAtomValue(durationAtom);
|
||||
@ -353,10 +350,17 @@ const ProgressBar = ({ url, chapters }: { url: string; chapters?: Chapter[] }) =
|
||||
id={"progress-scrubber"}
|
||||
isOpen={hoverProgress !== null}
|
||||
place="top"
|
||||
position={{ x: layout.x + (layout.width * hoverProgress!) / (duration ?? 1), y: layout.y }}
|
||||
position={{
|
||||
x: layout.x + (layout.width * hoverProgress!) / (duration ?? 1),
|
||||
y: layout.y,
|
||||
}}
|
||||
render={() =>
|
||||
hoverProgress ? (
|
||||
<ScrubberTooltip seconds={hoverProgress} chapters={chapters} url={url} />
|
||||
<ScrubberTooltip
|
||||
seconds={hoverProgress}
|
||||
chapters={chapters}
|
||||
url={url}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
opacity={1}
|
||||
@ -449,9 +453,13 @@ const VideoPoster = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadingIndicator = () => {
|
||||
const isLoading = useAtomValue(loadAtom);
|
||||
export const LoadingIndicator = ({ player }: { player: VideoPlayer }) => {
|
||||
const { css } = useYoshiki();
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
useEvent(player, "onStatusChange", (status) => {
|
||||
setLoading(status === "loading");
|
||||
});
|
||||
|
||||
if (!isLoading) return null;
|
||||
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { Stack } from "expo-router";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import { useVideoPlayer, VideoView, VideoViewRef } from "react-native-video";
|
||||
import {
|
||||
useEvent,
|
||||
useVideoPlayer,
|
||||
VideoView,
|
||||
VideoViewRef,
|
||||
} from "react-native-video";
|
||||
import { entryDisplayNumber } from "~/components/entries";
|
||||
import { FullVideo, VideoInfo } from "~/models";
|
||||
import { Head } from "~/primitives";
|
||||
@ -9,6 +14,7 @@ import { useToken } from "~/providers/account-context";
|
||||
import { useLocalSetting } from "~/providers/settings";
|
||||
import { type QueryIdentifier, useFetch } from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { LoadingIndicator } from "./components/hover";
|
||||
|
||||
// import { Hover, LoadingIndicator } from "./components/hover";
|
||||
// import { useVideoKeyboard } from "./keyboard";
|
||||
@ -33,7 +39,8 @@ const mapMetadata = (item: FullVideo | undefined) => {
|
||||
};
|
||||
|
||||
export const Player = () => {
|
||||
const [slug] = useQueryState("slug", undefined!);
|
||||
const [slug, setSlug] = useQueryState<string>("slug", undefined!);
|
||||
const [start, setStart] = useQueryState<number | undefined>("t", undefined);
|
||||
|
||||
const { data, error } = useFetch(Player.query(slug));
|
||||
const { data: info, error: infoError } = useFetch(Player.infoQuery(slug));
|
||||
@ -60,10 +67,25 @@ export const Player = () => {
|
||||
(p) => {
|
||||
p.playWhenInactive = true;
|
||||
p.playInBackground = true;
|
||||
const seek = start ?? data?.progress.time;
|
||||
// TODO: fix console.error bellow
|
||||
if (seek) p.seekTo(seek);
|
||||
else console.error("Player got ready before progress info was loaded.");
|
||||
p.play();
|
||||
},
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
useEvent(player, "onEnd", () => {
|
||||
if (!data) return;
|
||||
if (data.next) {
|
||||
setStart(0);
|
||||
setSlug(data.next.video);
|
||||
} else {
|
||||
router.navigate(data.show!.href);
|
||||
}
|
||||
});
|
||||
|
||||
// const [playbackError, setPlaybackError] = useState<string | undefined>(
|
||||
// undefined,
|
||||
// );
|
||||
@ -120,7 +142,7 @@ export const Player = () => {
|
||||
controls
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
/>
|
||||
{/* <LoadingIndicator /> */}
|
||||
<LoadingIndicator player={player} />
|
||||
{/* <Hover {...mapData(data, info, previous, next)} url={`${type}/${slug}`} /> */}
|
||||
</View>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user