mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Merge pull request #938 from Stremio/feat/player-subtitles-delay-shortcuts
Some checks failed
Build / build (push) Has been cancelled
Some checks failed
Build / build (push) Has been cancelled
Player: add subtitles delay shortcuts
This commit is contained in:
commit
5f085d259c
8 changed files with 176 additions and 5 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -36,7 +36,7 @@
|
|||
"react-i18next": "^15.1.3",
|
||||
"react-is": "18.3.1",
|
||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#8212fa77c4febd22ddb611590e9fb574dc845416",
|
||||
"url": "0.11.4",
|
||||
"use-long-press": "^3.2.0"
|
||||
},
|
||||
|
|
@ -13405,8 +13405,8 @@
|
|||
},
|
||||
"node_modules/stremio-translations": {
|
||||
"version": "1.44.12",
|
||||
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f",
|
||||
"integrity": "sha512-b38OjGwlsvFm/aNn/ia18mPxPjZvnI/GaToppn1XaQqCuZuSHxQlYDddwOYTztskWo4VO/IZmCi3UFewqpsqCQ==",
|
||||
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8212fa77c4febd22ddb611590e9fb574dc845416",
|
||||
"integrity": "sha512-5DladLUsghLlVRsZh2bBnb7UMqU8NEYMHc+YbzBvb1llgMk9elXFSHtAjInepZlC5zWx2pJYOQ8lQzzqogQdFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
"react-i18next": "^15.1.3",
|
||||
"react-is": "18.3.1",
|
||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#8212fa77c4febd22ddb611590e9fb574dc845416",
|
||||
"url": "0.11.4",
|
||||
"use-long-press": "^3.2.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -82,6 +82,19 @@
|
|||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.fade-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-active {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s cubic-bezier(0.32, 0, 0.67, 0);
|
||||
}
|
||||
|
||||
.fade-exit {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fade-in-no-motion {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
|
|
|||
23
src/routes/Player/Indicator/Indicator.less
Normal file
23
src/routes/Player/Indicator/Indicator.less
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.indicator-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 4rem;
|
||||
user-select: none;
|
||||
|
||||
.indicator {
|
||||
flex: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 2rem;
|
||||
border-radius: 4rem;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: var(--primary-foreground-color);
|
||||
background-color: var(--modal-background-color);
|
||||
}
|
||||
}
|
||||
73
src/routes/Player/Indicator/Indicator.tsx
Normal file
73
src/routes/Player/Indicator/Indicator.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { t } from 'i18next';
|
||||
import { Transition } from 'stremio/components';
|
||||
import { useBinaryState } from 'stremio/common';
|
||||
import styles from './Indicator.less';
|
||||
|
||||
type Property = {
|
||||
label: string,
|
||||
format: (value: number) => string,
|
||||
};
|
||||
|
||||
const PROPERTIES: Record<string, Property> = {
|
||||
'extraSubtitlesDelay': {
|
||||
label: 'SUBTITLES_DELAY',
|
||||
format: (value) => `${(value / 1000).toFixed(2)}s`,
|
||||
},
|
||||
};
|
||||
|
||||
type VideoState = Record<string, number>;
|
||||
|
||||
type Props = {
|
||||
className: string,
|
||||
videoState: VideoState,
|
||||
};
|
||||
|
||||
const Indicator = ({ className, videoState }: Props) => {
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const prevVideoState = useRef<VideoState>(videoState);
|
||||
|
||||
const [shown, show, hide] = useBinaryState(false);
|
||||
const [current, setCurrent] = useState<string | null>(null);
|
||||
|
||||
const label = useMemo(() => {
|
||||
const property = current && PROPERTIES[current];
|
||||
return property && t(property.label);
|
||||
}, [current]);
|
||||
|
||||
const value = useMemo(() => {
|
||||
const property = current && PROPERTIES[current];
|
||||
const value = current && videoState[current];
|
||||
return property && value && property.format(value);
|
||||
}, [current, videoState]);
|
||||
|
||||
useEffect(() => {
|
||||
for (const property of Object.keys(PROPERTIES)) {
|
||||
const prev = prevVideoState.current[property];
|
||||
const next = videoState[property];
|
||||
|
||||
if (next && next !== prev) {
|
||||
setCurrent(property);
|
||||
show();
|
||||
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
timeout.current = setTimeout(hide, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
prevVideoState.current = videoState;
|
||||
}, [videoState]);
|
||||
|
||||
return (
|
||||
<Transition when={shown} name={'fade'}>
|
||||
<div className={classNames(className, styles['indicator-container'])}>
|
||||
<div className={styles['indicator']}>
|
||||
<div>{label} {value}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
export default Indicator;
|
||||
|
|
@ -27,6 +27,7 @@ const useStatistics = require('./useStatistics');
|
|||
const useVideo = require('./useVideo');
|
||||
const styles = require('./styles');
|
||||
const Video = require('./Video');
|
||||
const { default: Indicator } = require('./Indicator/Indicator');
|
||||
|
||||
const Player = ({ urlParams, queryParams }) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -216,6 +217,16 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
video.setProp('extraSubtitlesDelay', delay);
|
||||
}, []);
|
||||
|
||||
const onIncreaseSubtitlesDelay = React.useCallback(() => {
|
||||
const delay = video.state.extraSubtitlesDelay + 250;
|
||||
onExtraSubtitlesDelayChanged(delay);
|
||||
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
|
||||
|
||||
const onDecreaseSubtitlesDelay = React.useCallback(() => {
|
||||
const delay = video.state.extraSubtitlesDelay - 250;
|
||||
onExtraSubtitlesDelayChanged(delay);
|
||||
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
|
||||
|
||||
const onSubtitlesSizeChanged = React.useCallback((size) => {
|
||||
updateSettings({ subtitlesSize: size });
|
||||
}, [updateSettings]);
|
||||
|
|
@ -587,6 +598,14 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
|
||||
break;
|
||||
}
|
||||
case 'KeyG': {
|
||||
onDecreaseSubtitlesDelay();
|
||||
break;
|
||||
}
|
||||
case 'KeyH': {
|
||||
onIncreaseSubtitlesDelay();
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
closeMenus();
|
||||
!settings.escExitFullscreen && window.history.back();
|
||||
|
|
@ -620,7 +639,29 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
window.removeEventListener('keyup', onKeyUp);
|
||||
window.removeEventListener('wheel', onWheel);
|
||||
};
|
||||
}, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, settings.escExitFullscreen, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleStatisticsMenu, toggleSideDrawer]);
|
||||
}, [
|
||||
player.metaItem,
|
||||
player.selected,
|
||||
streamingServer.statistics,
|
||||
settings.seekTimeDuration,
|
||||
settings.seekShortTimeDuration,
|
||||
settings.escExitFullscreen,
|
||||
routeFocused,
|
||||
menusOpen,
|
||||
nextVideoPopupOpen,
|
||||
video.state.paused,
|
||||
video.state.time,
|
||||
video.state.volume,
|
||||
video.state.audioTracks,
|
||||
video.state.subtitlesTracks,
|
||||
video.state.extraSubtitlesTracks,
|
||||
video.state.playbackSpeed,
|
||||
toggleSubtitlesMenu,
|
||||
toggleStatisticsMenu,
|
||||
toggleSideDrawer,
|
||||
onDecreaseSubtitlesDelay,
|
||||
onIncreaseSubtitlesDelay,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
video.events.on('error', onError);
|
||||
|
|
@ -760,6 +801,10 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
onMouseOver={onBarMouseMove}
|
||||
onTouchEnd={onContainerMouseLeave}
|
||||
/>
|
||||
<Indicator
|
||||
className={classnames(styles['layer'], styles['indicator-layer'])}
|
||||
videoState={video.state}
|
||||
/>
|
||||
{
|
||||
nextVideoPopupOpen ?
|
||||
<NextVideoPopup
|
||||
|
|
|
|||
|
|
@ -107,6 +107,13 @@ html:not(.active-slider-within) {
|
|||
}
|
||||
}
|
||||
|
||||
&.indicator-layer {
|
||||
top: initial;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 10rem;
|
||||
}
|
||||
|
||||
&.menu-layer {
|
||||
top: initial;
|
||||
left: initial;
|
||||
|
|
|
|||
|
|
@ -709,6 +709,16 @@ const Settings = () => {
|
|||
<kbd>F</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_SUBTITLES_DELAY') }</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
|
||||
<kbd>G</kbd>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_AND') }</div>
|
||||
<kbd>H</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_NAVIGATE_MENUS') }</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue