mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add keybindings for the player
This commit is contained in:
parent
6bcff8d9a2
commit
2c06924792
186
front/src/player/keyboard.tsx
Normal file
186
front/src/player/keyboard.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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 { atom, useSetAtom } from "jotai";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { Font, Track } from "~/models/resources/watch-item";
|
||||
import {
|
||||
durationAtom,
|
||||
fullscreenAtom,
|
||||
mutedAtom,
|
||||
playAtom,
|
||||
progressAtom,
|
||||
subtitleAtom,
|
||||
volumeAtom,
|
||||
} from "./state";
|
||||
|
||||
type Action =
|
||||
| { type: "play" }
|
||||
| { type: "mute" }
|
||||
| { type: "fullscreen" }
|
||||
| { type: "seek"; value: number }
|
||||
| { type: "seekPercent"; value: number }
|
||||
| { type: "volume"; value: number }
|
||||
| { type: "subtitle"; subtitles: Track[]; fonts: Font[] };
|
||||
|
||||
const keyboardReducerAtom = atom<null, Action>(null, (get, set, action) => {
|
||||
switch (action.type) {
|
||||
case "play":
|
||||
set(playAtom, !get(playAtom));
|
||||
break;
|
||||
case "mute":
|
||||
set(mutedAtom, !get(mutedAtom));
|
||||
break;
|
||||
case "fullscreen":
|
||||
set(fullscreenAtom, !get(fullscreenAtom));
|
||||
break;
|
||||
case "seek":
|
||||
set(progressAtom, get(progressAtom) + action.value);
|
||||
break;
|
||||
case "seekPercent":
|
||||
set(progressAtom, (get(durationAtom) * action.value) / 100);
|
||||
break;
|
||||
case "volume":
|
||||
set(volumeAtom, get(volumeAtom) + action.value);
|
||||
break;
|
||||
case "subtitle":
|
||||
const subtitle = get(subtitleAtom);
|
||||
const index = subtitle ? action.subtitles.findIndex((x) => x.id === subtitle.id) : -1;
|
||||
set(
|
||||
subtitleAtom,
|
||||
index === -1
|
||||
? null
|
||||
: {
|
||||
track: action.subtitles[(index + 1) % action.subtitles.length],
|
||||
fonts: action.fonts,
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
export const useVideoKeyboard = (
|
||||
subtitles?: Track[],
|
||||
fonts?: Font[],
|
||||
previousEpisode?: string,
|
||||
nextEpisode?: string,
|
||||
) => {
|
||||
const reducer = useSetAtom(keyboardReducerAtom);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
||||
|
||||
switch (event.key) {
|
||||
case " ":
|
||||
case "k":
|
||||
case "MediaPlay":
|
||||
case "MediaPause":
|
||||
case "MediaPlayPause":
|
||||
reducer({ type: "play" });
|
||||
break;
|
||||
|
||||
case "m":
|
||||
reducer({ type: "mute" });
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
reducer({ type: "seek", value: -5 });
|
||||
break;
|
||||
case "ArrowRight":
|
||||
reducer({ type: "seek", value: +5 });
|
||||
break;
|
||||
|
||||
case "j":
|
||||
reducer({ type: "seek", value: -10 });
|
||||
break;
|
||||
case "l":
|
||||
reducer({ type: "seek", value: +10 });
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
reducer({ type: "volume", value: +5 });
|
||||
break;
|
||||
case "ArrowDown":
|
||||
reducer({ type: "volume", value: -5 });
|
||||
break;
|
||||
|
||||
case "0":
|
||||
reducer({ type: "seekPercent", value: 0 });
|
||||
break;
|
||||
case "1":
|
||||
reducer({ type: "seekPercent", value: 10 });
|
||||
break;
|
||||
case "2":
|
||||
reducer({ type: "seekPercent", value: 20 });
|
||||
break;
|
||||
case "3":
|
||||
reducer({ type: "seekPercent", value: 30 });
|
||||
break;
|
||||
case "4":
|
||||
reducer({ type: "seekPercent", value: 40 });
|
||||
break;
|
||||
case "5":
|
||||
reducer({ type: "seekPercent", value: 50 });
|
||||
break;
|
||||
case "6":
|
||||
reducer({ type: "seekPercent", value: 60 });
|
||||
break;
|
||||
case "7":
|
||||
reducer({ type: "seekPercent", value: 70 });
|
||||
break;
|
||||
case "8":
|
||||
reducer({ type: "seekPercent", value: 80 });
|
||||
break;
|
||||
case "9":
|
||||
reducer({ type: "seekPercent", value: 90 });
|
||||
break;
|
||||
|
||||
case "f":
|
||||
reducer({ type: "fullscreen" });
|
||||
break;
|
||||
|
||||
case "v":
|
||||
case "c":
|
||||
if (!subtitles || !fonts) return;
|
||||
reducer({ type: "subtitle", subtitles, fonts });
|
||||
break;
|
||||
|
||||
case "n":
|
||||
case "N":
|
||||
if (nextEpisode) router.push(nextEpisode);
|
||||
break;
|
||||
|
||||
case "p":
|
||||
case "P":
|
||||
if (previousEpisode) router.push(previousEpisode);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keyup", handler);
|
||||
return () => document.removeEventListener("keyup", handler);
|
||||
}, [subtitles, fonts, nextEpisode, previousEpisode, router, reducer]);
|
||||
};
|
@ -32,6 +32,7 @@ import { useRouter } from "next/router";
|
||||
import Head from "next/head";
|
||||
import { makeTitle } from "~/utils/utils";
|
||||
import { episodeDisplayNumber } from "~/components/episode";
|
||||
import { useVideoKeyboard } from "./keyboard";
|
||||
|
||||
// Callback used to hide the controls when the mouse goes iddle. This is stored globally to clear the old timeout
|
||||
// if the mouse moves again (if this is stored as a state, the whole page is redrawn on mouse move)
|
||||
@ -76,14 +77,24 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
setPlay(true);
|
||||
}, [slug, setPlay]);
|
||||
|
||||
useSubtitleController(playerRef, data?.subtitles, data?.fonts);
|
||||
|
||||
useEffect(() => {
|
||||
if (!/Mobi/i.test(window.navigator.userAgent)) return;
|
||||
setFullscreen(true);
|
||||
return () => setFullscreen(false);
|
||||
}, [setFullscreen]);
|
||||
|
||||
useSubtitleController(playerRef, data?.subtitles, data?.fonts);
|
||||
useVideoKeyboard(
|
||||
data?.subtitles,
|
||||
data?.fonts,
|
||||
data && !data.isMovie && data.previousEpisode
|
||||
? `/watch/${data.previousEpisode.slug}`
|
||||
: undefined,
|
||||
data && !data.isMovie && data.nextEpisode
|
||||
? `/watch/${data.nextEpisode.slug}`
|
||||
: undefined,
|
||||
);
|
||||
|
||||
if (error) return <ErrorPage {...error} />;
|
||||
|
||||
return (
|
||||
|
@ -43,7 +43,8 @@ export const [_playAtom, playAtom] = bakedAtom(true, async (get, set, value) =>
|
||||
try {
|
||||
await player.current.play();
|
||||
} catch (e) {
|
||||
if (e instanceof DOMException && e.name === "NotSupportedError") set(playModeAtom, PlayMode.Transmux);
|
||||
if (e instanceof DOMException && e.name === "NotSupportedError")
|
||||
set(playModeAtom, PlayMode.Transmux);
|
||||
else if (!(e instanceof DOMException && e.name === "NotAllowedError")) console.log(e);
|
||||
}
|
||||
} else {
|
||||
@ -63,7 +64,7 @@ export const [_volumeAtom, volumeAtom] = bakedAtom(100, (get, set, value, baker)
|
||||
const player = get(playerAtom);
|
||||
if (!player?.current) return;
|
||||
set(baker, value);
|
||||
if (player.current) player.current.volume = value / 100;
|
||||
if (player.current) player.current.volume = Math.max(0, Math.min(value, 100)) / 100;
|
||||
});
|
||||
export const [_mutedAtom, mutedAtom] = bakedAtom(false, (get, set, value, baker) => {
|
||||
const player = get(playerAtom);
|
||||
@ -75,12 +76,13 @@ export const [_, fullscreenAtom] = bakedAtom(false, async (_, set, value, baker)
|
||||
try {
|
||||
if (value) {
|
||||
await document.body.requestFullscreen();
|
||||
set(baker, true);
|
||||
await screen.orientation.lock("landscape");
|
||||
} else {
|
||||
await document.exitFullscreen();
|
||||
set(baker, false);
|
||||
screen.orientation.unlock();
|
||||
}
|
||||
set(baker, value);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
@ -140,11 +142,7 @@ export const useVideoController = (links?: { direct: string; transmux: string })
|
||||
const videoProps: BoxProps<"video"> = {
|
||||
ref: player,
|
||||
onDoubleClick: () => {
|
||||
if (document.fullscreenElement) {
|
||||
setFullscreen(false);
|
||||
} else {
|
||||
setFullscreen(true);
|
||||
}
|
||||
setFullscreen(!document.fullscreenElement);
|
||||
},
|
||||
onPlay: () => setPlay(true),
|
||||
onPause: () => setPlay(false),
|
||||
|
Loading…
x
Reference in New Issue
Block a user