- hoverProgress ? (
-
- ) : null
- }
- opacity={1}
- style={{ padding: 0, borderRadius: imageBorderRadius }}
- />
- >
- );
-};
-
-export const Back = ({
- isLoading,
- name,
- ...props
-}: { isLoading: boolean; name?: string } & ViewProps) => {
- const { css } = useYoshiki();
- const { t } = useTranslation();
- const router = useRouter();
-
- return (
- theme.darkOverlay,
- display: "flex",
- flexDirection: "row",
- alignItems: "center",
- padding: percent(0.33),
- color: "white",
- },
- props,
- )}
- >
-
-
- {isLoading ? (
-
- ) : (
-
- {name}
-
- )}
-
-
- );
-};
-
-const VideoPoster = ({
- poster,
- alt,
- isLoading,
-}: {
- poster?: KyooImage | null;
- alt?: string;
- isLoading: boolean;
-}) => {
- const { css } = useYoshiki();
-
- return (
-
-
-
- );
-};
-
-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;
-
- return (
- alpha(theme.colors.black, 0.3),
- justifyContent: "center",
- })}
- >
-
-
- );
-};
diff --git a/front/src/ui/player/components/left-buttons.tsx b/front/src/ui/player/components/left-buttons.tsx
deleted file mode 100644
index e92c733b..00000000
--- a/front/src/ui/player/components/left-buttons.tsx
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * 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 .
- */
-
-import { IconButton, Link, P, Slider, noTouch, tooltip, touchOnly, ts } from "@kyoo/primitives";
-import Pause from "@material-symbols/svg-400/rounded/pause-fill.svg";
-import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
-import SkipNext from "@material-symbols/svg-400/rounded/skip_next-fill.svg";
-import SkipPrevious from "@material-symbols/svg-400/rounded/skip_previous-fill.svg";
-import VolumeDown from "@material-symbols/svg-400/rounded/volume_down-fill.svg";
-import VolumeMute from "@material-symbols/svg-400/rounded/volume_mute-fill.svg";
-import VolumeOff from "@material-symbols/svg-400/rounded/volume_off-fill.svg";
-import VolumeUp from "@material-symbols/svg-400/rounded/volume_up-fill.svg";
-import { useAtom, useAtomValue } from "jotai";
-import { useTranslation } from "react-i18next";
-import { Platform, View } from "react-native";
-import { type Stylable, px, useYoshiki } from "yoshiki/native";
-import { durationAtom, mutedAtom, playAtom, progressAtom, volumeAtom } from "../state";
-import { HoverTouch, hoverAtom } from "./hover";
-
-export const LeftButtons = ({
- previousSlug,
- nextSlug,
-}: {
- previousSlug?: string | null;
- nextSlug?: string | null;
-}) => {
- const { css } = useYoshiki();
- const { t } = useTranslation();
- const [isPlaying, setPlay] = useAtom(playAtom);
-
- const spacing = css({ marginHorizontal: ts(1) });
-
- return (
-
-
- {previousSlug && (
-
- )}
- setPlay(!isPlaying)}
- {...tooltip(isPlaying ? t("player.pause") : t("player.play"), true)}
- {...spacing}
- />
- {nextSlug && (
-
- )}
- {Platform.OS === "web" && }
-
-
-
- );
-};
-
-export const TouchControls = ({
- previousSlug,
- nextSlug,
- ...props
-}: {
- previousSlug?: string | null;
- nextSlug?: string | null;
-}) => {
- const { css } = useYoshiki();
- const [isPlaying, setPlay] = useAtom(playAtom);
- const hover = useAtomValue(hoverAtom);
-
- const common = css(
- [
- {
- backgroundColor: (theme) => theme.darkOverlay,
- marginHorizontal: ts(3),
- },
- ],
- touchOnly,
- );
-
- return (
-
- {hover && (
- <>
-
- setPlay(!isPlaying)}
- size={ts(8)}
- {...common}
- />
-
- >
- )}
-
- );
-};
-
-const VolumeSlider = () => {
- const [volume, setVolume] = useAtom(volumeAtom);
- const [isMuted, setMuted] = useAtom(mutedAtom);
- const { css } = useYoshiki();
- const { t } = useTranslation();
-
- return (
-
- setMuted(!isMuted)}
- {...tooltip(t("player.mute"), true)}
- />
-
-
- );
-};
-
-const ProgressText = (props: Stylable) => {
- const progress = useAtomValue(progressAtom);
- const duration = useAtomValue(durationAtom);
- const { css } = useYoshiki();
-
- return (
-
- {toTimerString(progress, duration)} : {toTimerString(duration)}
-
- );
-};
-
-export const toTimerString = (timer?: number, duration?: number) => {
- if (!duration) duration = timer;
- if (
- timer === undefined ||
- duration === undefined ||
- Number.isNaN(duration) ||
- Number.isNaN(timer)
- )
- return "??:??";
- const h = Math.floor(timer / 3600);
- const min = Math.floor((timer / 60) % 60);
- const sec = Math.floor(timer % 60);
- const fmt = (n: number) => n.toString().padStart(2, "0");
-
- if (duration >= 3600) return `${fmt(h)}:${fmt(min)}:${fmt(sec)}`;
- return `${fmt(min)}:${fmt(sec)}`;
-};
diff --git a/front/src/ui/player/controls/back.tsx b/front/src/ui/player/controls/back.tsx
new file mode 100644
index 00000000..72e9fc3c
--- /dev/null
+++ b/front/src/ui/player/controls/back.tsx
@@ -0,0 +1,88 @@
+import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
+import { useRouter } from "expo-router";
+import { useTranslation } from "react-i18next";
+import { View, type ViewProps } from "react-native";
+import { percent, rem, useYoshiki } from "yoshiki/native";
+import {
+ H1,
+ IconButton,
+ PressableFeedback,
+ Skeleton,
+ tooltip,
+} from "~/primitives";
+
+export const Back = ({ name, ...props }: { name: string } & ViewProps) => {
+ const { css } = useYoshiki();
+ const { t } = useTranslation();
+ const router = useRouter();
+
+ return (
+ theme.darkOverlay,
+ display: "flex",
+ flexDirection: "row",
+ alignItems: "center",
+ padding: percent(0.33),
+ color: "white",
+ },
+ props,
+ )}
+ >
+
+
+ {name}
+
+
+ );
+};
+
+Back.Loader = (props: ViewProps) => {
+ const { css } = useYoshiki();
+ const { t } = useTranslation();
+ const router = useRouter();
+
+ return (
+ theme.darkOverlay,
+ display: "flex",
+ flexDirection: "row",
+ alignItems: "center",
+ padding: percent(0.33),
+ color: "white",
+ },
+ props,
+ )}
+ >
+
+
+
+ );
+};
diff --git a/front/src/ui/player/controls/index.tsx b/front/src/ui/player/controls/index.tsx
new file mode 100644
index 00000000..744ad206
--- /dev/null
+++ b/front/src/ui/player/controls/index.tsx
@@ -0,0 +1,307 @@
+import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg";
+import { useRouter } from "expo-router";
+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,
+ H1,
+ H2,
+ IconButton,
+ Poster,
+ PressableFeedback,
+ Skeleton,
+ Slider,
+ Tooltip,
+ tooltip,
+ ts,
+ useIsTouch,
+} from "~/primitives";
+import { LeftButtons } from "./components/left-buttons";
+import { RightButtons } from "./components/right-buttons";
+import { BottomScrubber, ScrubberTooltip } from "./scrubber";
+
+export const Controls = ({
+ player,
+ title,
+}: {
+ player: VideoPlayer;
+ title: string;
+ description: string | null;
+ poster: KImage | null;
+}) => {
+ const { css } = useYoshiki();
+ // const show = useAtomValue(hoverAtom);
+ // const setHover = useSetAtom(hoverReasonAtom);
+ // const isSeeking = useAtomValue(seekingAtom);
+ // const isTouch = useIsTouch();
+
+ // const showBottomSeeker = isSeeking && isTouch;
+
+ //
+ return (
+ {
+ // if (e.nativeEvent.pointerType === "mouse")
+ // setHover((x) => ({ ...x, mouseHover: true }));
+ // }}
+ // onPointerLeave={(e) => {
+ // if (e.nativeEvent.pointerType === "mouse")
+ // setHover((x) => ({ ...x, mouseHover: false }));
+ // }}
+ {...css({
+ // TODO: animate show
+ //display: !show ? "none" : "flex",
+ position: "absolute",
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+ // box-none does not work on the web while none does not work on android
+ pointerEvents: Platform.OS === "web" ? "none" : "box-none",
+ })}
+ >
+
+ theme.darkOverlay,
+ flexDirection: "row",
+ pointerEvents: "auto",
+ padding: percent(1),
+ })}
+ >
+
+
+ {!showBottomSeeker && (
+
+ {isLoading ? (
+
+ ) : (
+ name
+ )}
+
+ )}
+
+ {showBottomSeeker ? (
+
+ ) : (
+
+
+ setHover((x) => ({ ...x, menuOpened: true }))}
+ onMenuClose={() => {
+ // Disable hover since the menu overlay makes the mouseout unreliable.
+ setHover((x) => ({
+ ...x,
+ menuOpened: false,
+ mouseHover: false,
+ }));
+ }}
+ />
+
+ )}
+
+
+
+ );
+};
+
+export const HoverTouch = ({ children, ...props }: { children: ReactNode }) => {
+ const hover = useAtomValue(hoverAtom);
+ const setHover = useSetAtom(hoverReasonAtom);
+ const mouseCallback = useRef(null);
+ const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({
+ count: 0,
+ });
+ const playerWidth = useRef(null);
+ const isTouch = useIsTouch();
+
+ const show = useCallback(() => {
+ setHover((x) => ({ ...x, mouseMoved: true }));
+ if (mouseCallback.current) clearTimeout(mouseCallback.current);
+ mouseCallback.current = setTimeout(() => {
+ setHover((x) => ({ ...x, mouseMoved: false }));
+ }, 2500);
+ }, [setHover]);
+
+ // On mouse move
+ useEffect(() => {
+ if (Platform.OS !== "web") return;
+ const handler = (e: PointerEvent) => {
+ if (e.pointerType !== "mouse") return;
+ show();
+ };
+
+ document.addEventListener("pointermove", handler);
+ return () => document.removeEventListener("pointermove", handler);
+ }, [show]);
+
+ // When the controls hide, remove focus so space can be used to play/pause instead of triggering the button
+ // It also serves to hide the tooltip.
+ useEffect(() => {
+ if (Platform.OS !== "web") return;
+ if (!hover && document.activeElement instanceof HTMLElement)
+ document.activeElement.blur();
+ }, [hover]);
+
+ const { css } = useYoshiki();
+
+ const duration = useAtomValue(durationAtom);
+ const setPlay = useSetAtom(playAtom);
+ const setProgress = useSetAtom(progressAtom);
+ const setFullscreen = useSetAtom(fullscreenAtom);
+
+ const onPress = (e: { pointerType: string; x: number }) => {
+ if (Platform.OS === "web" && e.pointerType === "mouse") {
+ setPlay((x) => !x);
+ return;
+ }
+ if (hover) setHover((x) => ({ ...x, mouseMoved: false }));
+ else show();
+ };
+ const onDoublePress = (e: { pointerType: string; x: number }) => {
+ if (Platform.OS === "web" && e.pointerType === "mouse") {
+ // Only reset touch count for the web, on mobile you can continue to seek by pressing again.
+ touch.current.count = 0;
+ setFullscreen((x) => !x);
+ return;
+ }
+
+ show();
+ if (!duration || !playerWidth.current) return;
+
+ if (e.x < playerWidth.current * 0.33) {
+ setProgress((x) => Math.max(x - 10, 0));
+ }
+ if (e.x > playerWidth.current * 0.66) {
+ setProgress((x) => Math.min(x + 10, duration));
+ }
+ };
+
+ const onAnyPress = (e: { pointerType: string; x: number }) => {
+ touch.current.count++;
+ if (touch.current.count >= 2) {
+ onDoublePress(e);
+ clearTimeout(touch.current.timeout);
+ } else {
+ onPress(e);
+ }
+
+ touch.current.timeout = setTimeout(() => {
+ touch.current.count = 0;
+ touch.current.timeout = undefined;
+ }, 400);
+ };
+
+ return (
+ {
+ if (e.nativeEvent.pointerType === "mouse")
+ setHover((x) => ({ ...x, mouseMoved: false }));
+ }}
+ onPress={(e) => {
+ e.preventDefault();
+ onAnyPress({
+ pointerType: isTouch ? "touch" : "mouse",
+ x: e.nativeEvent.locationX ?? e.nativeEvent.pageX,
+ });
+ }}
+ onLayout={(e) => {
+ playerWidth.current = e.nativeEvent.layout.width;
+ }}
+ {...css(
+ // @ts-expect-error Web only property (cursor: unset)
+ {
+ flexDirection: "row",
+ justifyContent: "center",
+ alignItems: "center",
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ cursor: hover ? "unset" : "none",
+ },
+ props,
+ )}
+ >
+ {children}
+
+ );
+};
+
+const VideoPoster = ({
+ poster,
+ alt,
+ isLoading,
+}: {
+ poster?: KyooImage | null;
+ alt?: string;
+ isLoading: boolean;
+}) => {
+ const { css } = useYoshiki();
+
+ return (
+
+
+
+ );
+};
diff --git a/front/src/ui/player/controls/misc.tsx b/front/src/ui/player/controls/misc.tsx
new file mode 100644
index 00000000..3a6744f0
--- /dev/null
+++ b/front/src/ui/player/controls/misc.tsx
@@ -0,0 +1,118 @@
+import Pause from "@material-symbols/svg-400/rounded/pause-fill.svg";
+import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
+import VolumeDown from "@material-symbols/svg-400/rounded/volume_down-fill.svg";
+import VolumeMute from "@material-symbols/svg-400/rounded/volume_mute-fill.svg";
+import VolumeOff from "@material-symbols/svg-400/rounded/volume_off-fill.svg";
+import VolumeUp from "@material-symbols/svg-400/rounded/volume_up-fill.svg";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { View } from "react-native";
+import { useEvent, type VideoPlayer } from "react-native-video";
+import { px, useYoshiki } from "yoshiki/native";
+import {
+ alpha,
+ CircularProgress,
+ IconButton,
+ Slider,
+ tooltip,
+ ts,
+} from "~/primitives";
+
+export const PlayButton = ({ player, ...props }: { player: VideoPlayer }) => {
+ const { t } = useTranslation();
+
+ const [playing, setPlay] = useState(player.isPlaying);
+ useEvent(player, "onPlaybackStateChange", (status) => {
+ setPlay(status.isPlaying);
+ });
+
+ return (
+ {
+ if (playing) player.pause();
+ else player.play();
+ }}
+ {...tooltip(playing ? t("player.pause") : t("player.play"), true)}
+ {...props}
+ />
+ );
+};
+
+export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => {
+ const { css } = useYoshiki();
+ const { t } = useTranslation();
+
+ const [volume, setVolume] = useState(player.volume);
+ useEvent(player, "onVolumeChange", setVolume);
+ // TODO: listen to `player.muted` changes (currently hook does not exists
+ // const [muted, setMuted] = useState(player.muted);
+ const muted = player.muted;
+
+ return (
+
+ {
+ player.muted = !muted;
+ }}
+ {...tooltip(t("player.mute"), true)}
+ />
+ {
+ player.volume = vol;
+ }}
+ size={4}
+ {...css({ width: px(100) })}
+ {...tooltip(t("player.volume"), true)}
+ />
+
+ );
+};
+
+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;
+
+ return (
+ alpha(theme.colors.black, 0.3),
+ justifyContent: "center",
+ })}
+ >
+
+
+ );
+};
diff --git a/front/src/ui/player/controls/progress.tsx b/front/src/ui/player/controls/progress.tsx
new file mode 100644
index 00000000..24a5030d
--- /dev/null
+++ b/front/src/ui/player/controls/progress.tsx
@@ -0,0 +1,111 @@
+import { useState } from "react";
+import type { TextProps } from "react-native";
+import { useEvent, type VideoPlayer } from "react-native-video";
+import { useYoshiki } from "yoshiki/native";
+import type { Chapter } from "~/models";
+import { P, Slider } from "~/primitives";
+
+export const ProgressBar = ({
+ player,
+ // url,
+ chapters,
+}: {
+ player: VideoPlayer;
+ // url: string;
+ chapters?: Chapter[];
+}) => {
+ const [duration, setDuration] = useState(player.duration || 100);
+ useEvent(player, "onLoad", (info) => {
+ if (info.duration) setDuration(info.duration);
+ });
+
+ const [progress, setProgress] = useState(player.currentTime || 0);
+ const [buffer, setBuffer] = useState(0);
+ useEvent(player, "onProgress", (progress) => {
+ setProgress(progress.currentTime);
+ setBuffer(progress.bufferDuration);
+ });
+
+ const [seek, setSeek] = useState(null);
+
+ return (
+ <>
+ {
+ player.pause();
+ }}
+ setProgress={setSeek}
+ endSeek={() => {
+ setProgress(seek!);
+ setSeek(null);
+ setTimeout(player.play, 10);
+ }}
+ // onHover={(progress, layout) => {
+ // setHoverProgress(progress);
+ // setLayout(layout);
+ // }}
+ markers={chapters?.map((x) => x.startTime)}
+ // dataSet={{ tooltipId: "progress-scrubber" }}
+ />
+ {/* */}
+ {/* hoverProgress ? ( */}
+ {/* */}
+ {/* ) : null */}
+ {/* } */}
+ {/* opacity={1} */}
+ {/* style={{ padding: 0, borderRadius: imageBorderRadius }} */}
+ {/* /> */}
+ >
+ );
+};
+
+export const ProgressText = ({
+ player,
+ ...props
+}: { player: VideoPlayer } & TextProps) => {
+ const { css } = useYoshiki();
+
+ const [progress, setProgress] = useState(player.currentTime || 0);
+ useEvent(player, "onProgress", (progress) => {
+ setProgress(progress.currentTime);
+ });
+ const [duration, setDuration] = useState(player.duration || 100);
+ useEvent(player, "onLoad", (info) => {
+ if (info.duration) setDuration(info.duration);
+ });
+
+ return (
+
+ {toTimerString(progress, duration)} : {toTimerString(duration)}
+
+ );
+};
+
+const toTimerString = (timer?: number, duration?: number) => {
+ if (!duration) duration = timer;
+ if (timer === undefined || Number.isNaN(timer)) return "??:??";
+
+ const h = Math.floor(timer / 3600);
+ const min = Math.floor((timer / 60) % 60);
+ const sec = Math.floor(timer % 60);
+ const fmt = (n: number) => n.toString().padStart(2, "0");
+
+ return h !== 0 || (duration && duration >= 3600)
+ ? `${fmt(h)}:${fmt(min)}:${fmt(sec)}`
+ : `${fmt(min)}:${fmt(sec)}`;
+};
diff --git a/front/src/ui/player/index.tsx b/front/src/ui/player/index.tsx
index cb947041..bef34123 100644
--- a/front/src/ui/player/index.tsx
+++ b/front/src/ui/player/index.tsx
@@ -1,24 +1,14 @@
import { Stack, useRouter } from "expo-router";
-import { useEffect, useRef } from "react";
import { StyleSheet, View } from "react-native";
-import {
- useEvent,
- useVideoPlayer,
- VideoView,
- VideoViewRef,
-} from "react-native-video";
+import { useEvent, useVideoPlayer, VideoView } from "react-native-video";
import { entryDisplayNumber } from "~/components/entries";
import { FullVideo, VideoInfo } from "~/models";
-import { Head } from "~/primitives";
+import { ContrastArea, Head } from "~/primitives";
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";
-// import { durationAtom, fullscreenAtom, Video } from "./state";
+import { LoadingIndicator } from "./controls";
const mapMetadata = (item: FullVideo | undefined) => {
if (!item) return null;
@@ -142,8 +132,10 @@ export const Player = () => {
controls
style={StyleSheet.absoluteFillObject}
/>
-
- {/* */}
+
+
+
+
);
};
diff --git a/front/src/ui/player/keyboard.tsx b/front/src/ui/player/old/keyboard.tsx
similarity index 99%
rename from front/src/ui/player/keyboard.tsx
rename to front/src/ui/player/old/keyboard.tsx
index 5cd828b9..13ba652e 100644
--- a/front/src/ui/player/keyboard.tsx
+++ b/front/src/ui/player/old/keyboard.tsx
@@ -31,7 +31,7 @@ import {
progressAtom,
subtitleAtom,
volumeAtom,
-} from "./state";
+} from "./old/statee";
type Action =
| { type: "play" }
diff --git a/front/src/ui/player/old/left-buttons.tsx b/front/src/ui/player/old/left-buttons.tsx
new file mode 100644
index 00000000..cd97c70f
--- /dev/null
+++ b/front/src/ui/player/old/left-buttons.tsx
@@ -0,0 +1,141 @@
+import {
+ IconButton,
+ Link,
+ noTouch,
+ tooltip,
+ touchOnly,
+ ts,
+} from "@kyoo/primitives";
+import Pause from "@material-symbols/svg-400/rounded/pause-fill.svg";
+import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
+import SkipNext from "@material-symbols/svg-400/rounded/skip_next-fill.svg";
+import SkipPrevious from "@material-symbols/svg-400/rounded/skip_previous-fill.svg";
+import { useAtom, useAtomValue } from "jotai";
+import { useTranslation } from "react-i18next";
+import { Platform, View } from "react-native";
+import { px, type Stylable, useYoshiki } from "yoshiki/native";
+import { HoverTouch, hoverAtom } from "../controls";
+import { playAtom } from "./state";
+
+export const LeftButtons = ({
+ previousSlug,
+ nextSlug,
+}: {
+ previousSlug?: string | null;
+ nextSlug?: string | null;
+}) => {
+ const { css } = useYoshiki();
+ const { t } = useTranslation();
+ const [isPlaying, setPlay] = useAtom(playAtom);
+
+ const spacing = css({ marginHorizontal: ts(1) });
+
+ return (
+
+
+ {previousSlug && (
+
+ )}
+ setPlay(!isPlaying)}
+ {...tooltip(isPlaying ? t("player.pause") : t("player.play"), true)}
+ {...spacing}
+ />
+ {nextSlug && (
+
+ )}
+ {Platform.OS === "web" && }
+
+
+
+ );
+};
+
+export const TouchControls = ({
+ previousSlug,
+ nextSlug,
+ ...props
+}: {
+ previousSlug?: string | null;
+ nextSlug?: string | null;
+}) => {
+ const { css } = useYoshiki();
+ const [isPlaying, setPlay] = useAtom(playAtom);
+ const hover = useAtomValue(hoverAtom);
+
+ const common = css(
+ [
+ {
+ backgroundColor: (theme) => theme.darkOverlay,
+ marginHorizontal: ts(3),
+ },
+ ],
+ touchOnly,
+ );
+
+ return (
+
+ {hover && (
+ <>
+
+ setPlay(!isPlaying)}
+ size={ts(8)}
+ {...common}
+ />
+
+ >
+ )}
+
+ );
+};
diff --git a/front/src/ui/player/media-session.tsx b/front/src/ui/player/old/media-session.tsx
similarity index 96%
rename from front/src/ui/player/media-session.tsx
rename to front/src/ui/player/old/media-session.tsx
index f5d2d77a..64b74dfd 100644
--- a/front/src/ui/player/media-session.tsx
+++ b/front/src/ui/player/old/media-session.tsx
@@ -21,8 +21,8 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { useEffect } from "react";
import { useRouter } from "solito/router";
-import { reducerAtom } from "./keyboard";
-import { durationAtom, playAtom, progressAtom } from "./state";
+import { reducerAtom } from "./old/keyboardd";
+import { durationAtom, playAtom, progressAtom } from "./old/statee";
export const MediaSessionManager = ({
title,
diff --git a/front/src/ui/player/components/right-buttons.tsx b/front/src/ui/player/old/right-buttons.tsx
similarity index 96%
rename from front/src/ui/player/components/right-buttons.tsx
rename to front/src/ui/player/old/right-buttons.tsx
index 72ac2bd9..3f095ed9 100644
--- a/front/src/ui/player/components/right-buttons.tsx
+++ b/front/src/ui/player/old/right-buttons.tsx
@@ -30,8 +30,8 @@ import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native";
import { type Stylable, useYoshiki } from "yoshiki/native";
import { useSubtitleName } from "../../../../packages/ui/src/utils";
-import { fullscreenAtom, subtitleAtom } from "../state";
-import { AudiosMenu, QualitiesMenu } from "../video";
+import { fullscreenAtom, subtitleAtom } from "./state";
+import { AudiosMenu, QualitiesMenu } from "./video";
export const RightButtons = ({
audios,
diff --git a/front/src/ui/player/components/scrubber.tsx b/front/src/ui/player/old/scrubber.tsx
similarity index 97%
rename from front/src/ui/player/components/scrubber.tsx
rename to front/src/ui/player/old/scrubber.tsx
index 0487cce1..8cdb374e 100644
--- a/front/src/ui/player/components/scrubber.tsx
+++ b/front/src/ui/player/old/scrubber.tsx
@@ -25,9 +25,9 @@ import { useMemo } from "react";
import { Platform, View } from "react-native";
import { type Theme, percent, px, useForceRerender, useYoshiki } from "yoshiki/native";
import { ErrorView } from "../../errors";
-import { durationAtom } from "../state";
-import { seekProgressAtom } from "./hover";
-import { toTimerString } from "./left-buttons";
+import { durationAtom } from "./state";
+import { seekProgressAtom } from "../controls";
+import { toTimerString } from "../controls/left-buttonsttons";
type Thumb = {
from: number;
diff --git a/front/src/ui/player/state.tsx b/front/src/ui/player/old/state.tsx
similarity index 99%
rename from front/src/ui/player/state.tsx
rename to front/src/ui/player/old/state.tsx
index 0bb704b8..24d97cdb 100644
--- a/front/src/ui/player/state.tsx
+++ b/front/src/ui/player/old/state.tsx
@@ -33,7 +33,7 @@ import {
} from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
-import NativeVideo, { canPlay, type VideoMetadata, type VideoProps } from "./video";
+import NativeVideo, { canPlay, type VideoMetadata, type VideoProps } from "../videoideo";
export const playAtom = atom(true);
export const loadAtom = atom(false);
diff --git a/front/src/ui/player/video.tsx b/front/src/ui/player/old/video.tsx
similarity index 98%
rename from front/src/ui/player/video.tsx
rename to front/src/ui/player/old/video.tsx
index 3ce3e985..c032bf1f 100644
--- a/front/src/ui/player/video.tsx
+++ b/front/src/ui/player/old/video.tsx
@@ -50,8 +50,8 @@ import NativeVideo, {
SelectedVideoTrackType,
} from "react-native-video";
import { useYoshiki } from "yoshiki/native";
-import { useDisplayName } from "../../../packages/ui/src/utils";
-import { PlayMode, audioAtom, playModeAtom, subtitleAtom } from "./state";
+import { useDisplayName } from "../../../../packages/ui/src/utils";
+import { PlayMode, audioAtom, playModeAtom, subtitleAtom } from "./old/statee";
const MimeTypes: Map = new Map([
["subrip", "application/x-subrip"],
diff --git a/front/src/ui/player/video.web.tsx b/front/src/ui/player/old/video.web.tsx
similarity index 98%
rename from front/src/ui/player/video.web.tsx
rename to front/src/ui/player/old/video.web.tsx
index 60be3f20..b4de146e 100644
--- a/front/src/ui/player/video.web.tsx
+++ b/front/src/ui/player/old/video.web.tsx
@@ -36,9 +36,9 @@ import { useTranslation } from "react-i18next";
import type { VideoProps } from "react-native-video";
import toVttBlob from "srt-webvtt";
import { useForceRerender, useYoshiki } from "yoshiki";
-import { useDisplayName } from "../../../packages/ui/src/utils";
-import { MediaSessionManager } from "./media-session";
-import { PlayMode, audioAtom, playAtom, playModeAtom, progressAtom, subtitleAtom } from "./state";
+import { useDisplayName } from "../../../../packages/ui/src/utils";
+import { MediaSessionManager } from "./old/media-sessionn";
+import { PlayMode, audioAtom, playAtom, playModeAtom, progressAtom, subtitleAtom } from "./old/statee";
let hls: Hls | null = null;
diff --git a/front/src/ui/player/watch-status-observer.tsx b/front/src/ui/player/old/watch-status-observer.tsx
similarity index 98%
rename from front/src/ui/player/watch-status-observer.tsx
rename to front/src/ui/player/old/watch-status-observer.tsx
index decd2f7f..ba06b60f 100644
--- a/front/src/ui/player/watch-status-observer.tsx
+++ b/front/src/ui/player/old/watch-status-observer.tsx
@@ -23,7 +23,7 @@ import { useMutation } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { useAtomCallback } from "jotai/utils";
import { useCallback, useEffect } from "react";
-import { playAtom, progressAtom } from "./state";
+import { playAtom, progressAtom } from "./old/statee";
export const WatchStatusObserver = ({
type,