mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-10-29 17:52:41 -04:00
222 lines
5.7 KiB
TypeScript
222 lines
5.7 KiB
TypeScript
/*
|
|
* 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 { 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 (
|
|
<View {...css({ flexDirection: "row" })}>
|
|
<View {...css({ flexDirection: "row" }, noTouch)}>
|
|
{previousSlug && (
|
|
<IconButton
|
|
icon={SkipPrevious}
|
|
as={Link}
|
|
href={previousSlug}
|
|
replace
|
|
{...tooltip(t("player.previous"), true)}
|
|
{...spacing}
|
|
/>
|
|
)}
|
|
<IconButton
|
|
icon={isPlaying ? Pause : PlayArrow}
|
|
onPress={() => setPlay(!isPlaying)}
|
|
{...tooltip(isPlaying ? t("player.pause") : t("player.play"), true)}
|
|
{...spacing}
|
|
/>
|
|
{nextSlug && (
|
|
<IconButton
|
|
icon={SkipNext}
|
|
as={Link}
|
|
href={nextSlug}
|
|
replace
|
|
{...tooltip(t("player.next"), true)}
|
|
{...spacing}
|
|
/>
|
|
)}
|
|
{Platform.OS === "web" && <VolumeSlider />}
|
|
</View>
|
|
<ProgressText {...css({ marginLeft: ts(1) })} />
|
|
</View>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<HoverTouch
|
|
{...css(
|
|
{
|
|
flexDirection: "row",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
},
|
|
props,
|
|
)}
|
|
>
|
|
{hover && (
|
|
<>
|
|
<IconButton
|
|
icon={SkipPrevious}
|
|
as={Link}
|
|
href={previousSlug!}
|
|
replace
|
|
size={ts(4)}
|
|
{...css([!previousSlug && { opacity: 0, pointerEvents: "none" }], common)}
|
|
/>
|
|
<IconButton
|
|
icon={isPlaying ? Pause : PlayArrow}
|
|
onPress={() => setPlay(!isPlaying)}
|
|
size={ts(8)}
|
|
{...common}
|
|
/>
|
|
<IconButton
|
|
icon={SkipNext}
|
|
as={Link}
|
|
href={nextSlug!}
|
|
replace
|
|
size={ts(4)}
|
|
{...css([!nextSlug && { opacity: 0, pointerEvents: "none" }], common)}
|
|
/>
|
|
</>
|
|
)}
|
|
</HoverTouch>
|
|
);
|
|
};
|
|
|
|
const VolumeSlider = () => {
|
|
const [volume, setVolume] = useAtom(volumeAtom);
|
|
const [isMuted, setMuted] = useAtom(mutedAtom);
|
|
const { css } = useYoshiki();
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<View
|
|
{...css({
|
|
display: { xs: "none", sm: "flex" },
|
|
alignItems: "center",
|
|
flexDirection: "row",
|
|
paddingRight: ts(1),
|
|
})}
|
|
>
|
|
<IconButton
|
|
icon={
|
|
isMuted || volume === 0
|
|
? VolumeOff
|
|
: volume < 25
|
|
? VolumeMute
|
|
: volume < 65
|
|
? VolumeDown
|
|
: VolumeUp
|
|
}
|
|
onPress={() => setMuted(!isMuted)}
|
|
{...tooltip(t("player.mute"), true)}
|
|
/>
|
|
<Slider
|
|
progress={volume}
|
|
setProgress={setVolume}
|
|
size={4}
|
|
{...css({ width: px(100) })}
|
|
{...tooltip(t("player.volume"), true)}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const ProgressText = (props: Stylable) => {
|
|
const progress = useAtomValue(progressAtom);
|
|
const duration = useAtomValue(durationAtom);
|
|
const { css } = useYoshiki();
|
|
|
|
return (
|
|
<P {...css({ alignSelf: "center" }, props)}>
|
|
{toTimerString(progress, duration)} : {toTimerString(duration)}
|
|
</P>
|
|
);
|
|
};
|
|
|
|
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)}`;
|
|
};
|