diff --git a/front/src/primitives/progress.tsx b/front/src/primitives/progress.tsx index 060a207f..d531fa4b 100644 --- a/front/src/primitives/progress.tsx +++ b/front/src/primitives/progress.tsx @@ -1,15 +1,18 @@ import { ActivityIndicator } from "react-native"; -import { type Stylable, useYoshiki } from "yoshiki/native"; +import { cn } from "~/utils"; export const CircularProgress = ({ - size = 48, tickness = 5, - color, ...props -}: { size?: number; tickness?: number; color?: string } & Stylable) => { - const { theme } = useYoshiki(); - +}: { + tickness?: number; + className?: string; +}) => { return ( - + ); }; diff --git a/front/src/primitives/slider.tsx b/front/src/primitives/slider.tsx index 2455a172..34a27ce3 100644 --- a/front/src/primitives/slider.tsx +++ b/front/src/primitives/slider.tsx @@ -5,8 +5,7 @@ import { View, type ViewProps, } from "react-native"; -import { percent, px, useYoshiki } from "yoshiki/native"; -import { focusReset } from "./utils"; +import { cn } from "~/utils"; export const Slider = ({ progress, @@ -17,7 +16,7 @@ export const Slider = ({ startSeek, endSeek, onHover, - size = 6, + className, ...props }: { progress: number; @@ -31,9 +30,7 @@ export const Slider = ({ position: number | null, layout: { x: number; y: number; width: number; height: number }, ) => void; - size?: number; } & Partial) => { - const { css } = useYoshiki(); const ref = useRef(null); const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 }); const [isSeeking, setSeek] = useState(false); @@ -41,8 +38,6 @@ export const Slider = ({ const [isFocus, setFocus] = useState(false); const smallBar = !(isSeeking || isHover || isFocus); - const ts = (value: number) => px(value * size); - const change = (event: GestureResponderEvent) => { event.preventDefault(); const locationX = Platform.select({ @@ -57,7 +52,6 @@ export const Slider = ({ ref={ref} // @ts-expect-error Web only onMouseEnter={() => setHover(true)} - // @ts-expect-error Web only onMouseLeave={() => { setHover(false); onHover?.(null, layout); @@ -104,98 +98,39 @@ export const Slider = ({ break; } }} - {...css( - { - paddingVertical: ts(1), - // @ts-expect-error Web only - cursor: "pointer", - ...focusReset, - }, - props, - )} + className={cn("cursor-pointer justify-center py-2 outline-0", className)} + {...props} > theme.overlay0, - }, - smallBar && { transform: "scaleY(0.4)" as any }, - ])} + className={cn( + "h-2 w-full overflow-hidden rounded bg-slate-400", + smallBar && "scale-y-50", + )} > {subtleProgress !== undefined && ( theme.overlay1, - position: "absolute", - top: 0, - bottom: 0, - left: 0, - }, - { - style: { - width: percent((subtleProgress / max) * 100), - }, - }, - )} + className={cn("absolute left-0 h-full bg-slate-300")} + style={{ width: `${(subtleProgress / max) * 100}%` }} /> )} theme.accent, - position: "absolute", - top: 0, - bottom: 0, - left: 0, - }, - { - // In an inline style because yoshiki's insertion can not catch up with the constant redraw - style: { - width: percent((progress / max) * 100), - }, - }, - )} + className="absolute left-0 h-full bg-accent" + style={{ width: `${(progress / max) * 100}%` }} /> {markers?.map((x) => ( theme.accent, - width: ts(0.5), - height: ts(1), - })} + className="absolute h-full w-1 bg-accent" + style={{ left: `${Math.min(100, (x / max) * 100)}%` }} /> ))} theme.accent, - width: ts(2), - height: ts(2), - borderRadius: ts(1), - marginLeft: ts(-1), - }, - smallBar && { opacity: 0 }, - ], - { - style: { - left: percent((progress / max) * 100), - }, - }, + className={cn( + "absolute my-1 ml-[-6px] h-3 w-3 rounded-full bg-accent", + smallBar && "opacity-0", )} + style={{ left: `${(progress / max) * 100}%` }} /> ); diff --git a/front/src/ui/player/controls/back.tsx b/front/src/ui/player/controls/back.tsx index f97609ac..729224ac 100644 --- a/front/src/ui/player/controls/back.tsx +++ b/front/src/ui/player/controls/back.tsx @@ -2,7 +2,6 @@ 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, @@ -10,29 +9,19 @@ import { Skeleton, tooltip, } from "~/primitives"; +import { cn } from "~/utils"; export const Back = ({ name, showHref, + className, ...props }: { showHref?: string; name?: string } & ViewProps) => { - const { css } = useYoshiki(); const { t } = useTranslation(); const router = useRouter(); return ( - + {name ? ( -

- {name} -

+

{name}

) : ( - + )}
); diff --git a/front/src/ui/player/controls/bottom-controls.tsx b/front/src/ui/player/controls/bottom-controls.tsx index e9211696..84f775a0 100644 --- a/front/src/ui/player/controls/bottom-controls.tsx +++ b/front/src/ui/player/controls/bottom-controls.tsx @@ -2,9 +2,13 @@ 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 type { ComponentProps } from "react"; import { useTranslation } from "react-i18next"; -import { Platform, View, type ViewProps } from "react-native"; +import { + Platform, + type PressableProps, + View, + type ViewProps, +} from "react-native"; import type { VideoPlayer } from "react-native-video"; -import { percent, rem, useYoshiki } from "yoshiki/native"; import type { Chapter, KImage } from "~/models"; import { H2, @@ -14,9 +18,9 @@ import { Poster, Skeleton, tooltip, - ts, useIsTouch, } from "~/primitives"; +import { cn } from "~/utils"; import { FullscreenButton, PlayButton, VolumeSlider } from "./misc"; import { ProgressBar, ProgressText } from "./progress"; import { AudioMenu, QualityMenu, SubtitleMenu, VideoMenu } from "./tracks-menu"; @@ -29,6 +33,7 @@ export const BottomControls = ({ previous, next, setMenu, + className, ...props }: { player: VideoPlayer; @@ -39,52 +44,26 @@ export const BottomControls = ({ next?: string | null; setMenu: (isOpen: boolean) => void; } & ViewProps) => { - const { css } = useYoshiki(); - return ( - - + + {poster !== undefined ? ( ) : ( - + )} - + {name ? ( -

+

{name}

) : ( - + )} void; + className?: string; }) => { - const { css } = useYoshiki(); const { t } = useTranslation(); const isTouch = useIsTouch(); - const spacing = css({ marginHorizontal: ts(1) }); const menuProps = { onMenuOpen: () => setMenu(true), onMenuClose: () => setMenu(false), - ...spacing, - } satisfies Partial>; + className: "mr-4", + iconClassName: "fill-slate-200", + } satisfies Partial< + ComponentProps< + typeof Menu>> + > + >; return ( - + {!isTouch && ( - + {previous && ( )} - + {next && ( )} - {Platform.OS === "web" && } + {Platform.OS === "web" && ( + + )} )} - + - + - {Platform.OS === "web" && } + {Platform.OS === "web" && ( + + )} ); diff --git a/front/src/ui/player/controls/index.tsx b/front/src/ui/player/controls/index.tsx index b93b293b..290a99b3 100644 --- a/front/src/ui/player/controls/index.tsx +++ b/front/src/ui/player/controls/index.tsx @@ -1,9 +1,7 @@ import { useCallback, useState } from "react"; import type { ViewProps } from "react-native"; -import { StyleSheet, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { View } from "react-native"; import type { VideoPlayer } from "react-native-video"; -import { useYoshiki } from "yoshiki/native"; import type { Chapter, KImage } from "~/models"; import { useIsTouch } from "~/primitives"; import { Back } from "./back"; @@ -30,8 +28,6 @@ export const Controls = ({ previous?: string | null; next?: string | null; }) => { - const { css } = useYoshiki(); - const insets = useSafeAreaInsets(); const isTouch = useIsTouch(); const [hover, setHover] = useState(false); @@ -53,28 +49,17 @@ export const Controls = ({ }, []); return ( - + theme.darkOverlay, - paddingTop: insets.top, - paddingLeft: insets.left, - paddingRight: insets.right, - }, - hoverControls, - )} + className="absolute top-0 w-full bg-slate-900/50 px-safe pt-safe" + {...hoverControls} /> {isTouch && ( @@ -87,21 +72,10 @@ export const Controls = ({ previous={previous} next={next} setMenu={setMenu} - {...css( - { - // Fixed is used because firefox android make the hover disappear under the navigation bar in absolute - // position: Platform.OS === "web" ? ("fixed" as any) : "absolute", - position: "absolute", - bottom: 0, - left: 0, - right: 0, - bg: (theme) => theme.darkOverlay, - paddingLeft: insets.left, - paddingRight: insets.right, - paddingBottom: insets.bottom, - }, - hoverControls, - )} + // Fixed is used because firefox android make the hover disappear under the navigation bar in absolute + // position: Platform.OS === "web" ? ("fixed" as any) : "absolute", + className="absolute bottom-0 w-full bg-slate-900/50 px-safe pt-safe" + {...hoverControls} /> diff --git a/front/src/ui/player/controls/middle-controls.tsx b/front/src/ui/player/controls/middle-controls.tsx index 01c0dcc2..4218d53a 100644 --- a/front/src/ui/player/controls/middle-controls.tsx +++ b/front/src/ui/player/controls/middle-controls.tsx @@ -2,59 +2,53 @@ 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 { View } from "react-native"; import type { VideoPlayer } from "react-native-video"; -import { useYoshiki } from "yoshiki/native"; -import { IconButton, Link, ts } from "~/primitives"; +import { IconButton, Link } from "~/primitives"; +import { cn } from "~/utils"; import { PlayButton } from "./misc"; export const MiddleControls = ({ player, previous, next, + className, ...props }: { player: VideoPlayer; previous?: string | null; next?: string | null; + className?: string; }) => { - const { css } = useYoshiki(); - - const common = css({ - backgroundColor: (theme) => theme.darkOverlay, - marginHorizontal: ts(3), - }); - return ( + - ); diff --git a/front/src/ui/player/controls/misc.tsx b/front/src/ui/player/controls/misc.tsx index 2a4c57bb..1a95f760 100644 --- a/front/src/ui/player/controls/misc.tsx +++ b/front/src/ui/player/controls/misc.tsx @@ -10,15 +10,8 @@ import { type ComponentProps, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { type PressableProps, 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"; +import { CircularProgress, IconButton, Slider, tooltip } from "~/primitives"; +import { cn } from "~/utils"; export const PlayButton = ({ player, @@ -85,8 +78,16 @@ export const FullscreenButton = ( ); }; -export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => { - const { css } = useYoshiki(); +export const VolumeSlider = ({ + player, + className, + iconClassName, + ...props +}: { + player: VideoPlayer; + className?: string; + iconClassName?: string; +}) => { const { t } = useTranslation(); const [volume, setVolume] = useState(player.volume); @@ -98,15 +99,8 @@ export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => { return ( { onPress={() => { player.muted = !muted; }} + iconClassName={iconClassName} {...tooltip(t("player.mute"), true)} /> { setProgress={(vol) => { player.volume = vol / 100; }} - size={4} - {...css({ width: px(100) })} + className="h-1 w-24" {...tooltip(t("player.volume"), true)} /> @@ -137,7 +131,6 @@ export const VolumeSlider = ({ player, ...props }: { player: VideoPlayer }) => { }; export const LoadingIndicator = ({ player }: { player: VideoPlayer }) => { - const { css } = useYoshiki(); const [isLoading, setLoading] = useState(false); useEvent(player, "onStatusChange", (status) => { @@ -147,19 +140,8 @@ export const LoadingIndicator = ({ player }: { player: VideoPlayer }) => { 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 index 4c92adab..89e33bb5 100644 --- a/front/src/ui/player/controls/progress.tsx +++ b/front/src/ui/player/controls/progress.tsx @@ -1,9 +1,9 @@ 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"; +import { cn } from "~/utils"; export const ProgressBar = ({ player, @@ -76,10 +76,9 @@ export const ProgressBar = ({ export const ProgressText = ({ player, + className, ...props }: { player: VideoPlayer } & TextProps) => { - const { css } = useYoshiki(); - const [progress, setProgress] = useState(player.currentTime); useEvent(player, "onProgress", (progress) => { setProgress(progress.currentTime); @@ -90,7 +89,7 @@ export const ProgressText = ({ }); return ( -

+

{toTimerString(progress, duration)} : {toTimerString(duration)}

); diff --git a/front/src/ui/player/controls/touch.tsx b/front/src/ui/player/controls/touch.tsx index 0737fbb0..64d68fe7 100644 --- a/front/src/ui/player/controls/touch.tsx +++ b/front/src/ui/player/controls/touch.tsx @@ -4,13 +4,12 @@ import { Platform, Pressable, type PressableProps, - StyleSheet, View, type ViewProps, } from "react-native"; import { useEvent, type VideoPlayer } from "react-native-video"; -import { useYoshiki } from "yoshiki/native"; import { useIsTouch } from "~/primitives"; +import { cn } from "~/utils"; import { toggleFullscreen } from "./misc"; export const TouchControls = ({ @@ -22,7 +21,6 @@ export const TouchControls = ({ player: VideoPlayer; forceShow?: boolean; } & ViewProps) => { - const { css } = useYoshiki(); const isTouch = useIsTouch(); const [playing, setPlay] = useState(player.isPlaying); @@ -31,7 +29,7 @@ export const TouchControls = ({ }); const [_show, setShow] = useState(false); - const hideTimeout = useRef(null); + const hideTimeout = useRef(null); const shouldShow = forceShow || _show || !playing; const show = useCallback((val: boolean = true) => { setShow(val); @@ -90,10 +88,7 @@ export const TouchControls = ({ // instantly hide the controls when mouse leaves the view if (e.nativeEvent.pointerType === "mouse") show(false); }} - {...css({ - cursor: (shouldShow ? "unset" : "none") as any, - ...StyleSheet.absoluteFillObject, - })} + className={cn("absolute inset-0", !shouldShow && "cursor-none")} /> {shouldShow && children}
@@ -107,7 +102,7 @@ const DoublePressable = ({ }: { onDoublePress: (e: GestureResponderEvent) => boolean | undefined; } & PressableProps) => { - const touch = useRef<{ count: number; timeout?: NodeJS.Timeout }>({ + const touch = useRef<{ count: number; timeout?: NodeJS.Timeout | number }>({ count: 0, }); diff --git a/front/src/ui/player/controls/tracks-menu.tsx b/front/src/ui/player/controls/tracks-menu.tsx index 698066c9..c4712bc8 100644 --- a/front/src/ui/player/controls/tracks-menu.tsx +++ b/front/src/ui/player/controls/tracks-menu.tsx @@ -5,12 +5,11 @@ import VideoSettings from "@material-symbols/svg-400/rounded/video_settings-fill import { type ComponentProps, createContext, useContext } from "react"; import { useTranslation } from "react-i18next"; import { useEvent, type VideoPlayer } from "react-native-video"; -import { useForceRerender } from "yoshiki"; import { IconButton, Menu, tooltip } from "~/primitives"; import { useFetch } from "~/query"; import { useDisplayName, useSubtitleName } from "~/track-utils"; import { Info } from "~/ui/info"; -import { useQueryState } from "~/utils"; +import { useForceRerender, useQueryState } from "~/utils"; type MenuProps = ComponentProps>>; diff --git a/front/src/ui/player/index.tsx b/front/src/ui/player/index.tsx index 9b7de115..46bee6ee 100644 --- a/front/src/ui/player/index.tsx +++ b/front/src/ui/player/index.tsx @@ -5,10 +5,9 @@ import { useCallback, useEffect, useState } from "react"; import { Platform, StyleSheet, View } from "react-native"; import { useEvent, useVideoPlayer, VideoView } from "react-native-video"; import { v4 as uuidv4 } from "uuid"; -import { useYoshiki } from "yoshiki/native"; import { entryDisplayNumber } from "~/components/entries"; import { FullVideo, type KyooError } from "~/models"; -import { ContrastArea, Head } from "~/primitives"; +import { Head } from "~/primitives"; import { useToken } from "~/providers/account-context"; import { useLocalSetting } from "~/providers/settings"; import { type QueryIdentifier, useFetch } from "~/query"; @@ -158,14 +157,13 @@ export const Player = () => { setPlayMode("hls"); else setPlaybackError({ status: error.code, message: error.message }); }); - const { css } = useYoshiki(); if (error || infoError || playbackError) { return ( <> theme.accent })} + className="bg-accent" /> @@ -173,12 +171,7 @@ export const Player = () => { } return ( - + { resizeMode={"contain"} style={StyleSheet.absoluteFillObject} /> - - - - x) - .join(" - ") - : undefined - } - chapters={info?.chapters ?? []} - previous={data?.previous?.video} - next={data?.next?.video} - /> - - + + + x) + .join(" - ") + : undefined + } + chapters={info?.chapters ?? []} + previous={data?.previous?.video} + next={data?.next?.video} + /> + ); }; diff --git a/front/src/utils.ts b/front/src/utils.ts index 8820dbf3..5708ac43 100644 --- a/front/src/utils.ts +++ b/front/src/utils.ts @@ -1,13 +1,16 @@ import { type ClassValue, clsx } from "clsx"; import { useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback } from "react"; +import { useCallback, useReducer } from "react"; import { twMerge } from "tailwind-merge"; -import type { Movie, Show } from "~/models"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +export const useForceRerender = () => { + return useReducer((x) => x + 1, 0)[1]; +}; + export function setServerData(_key: string, _val: any) {} export function getServerData(key: string) { return key;