Merge branch 'development' into pr/704

This commit is contained in:
Timothy Z. 2025-07-16 17:40:00 +03:00
commit 56989781c8
22 changed files with 325 additions and 175 deletions

36
.github/workflows/pages_cleanup.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: GitHub Pages Cleanup
on:
schedule:
- cron: '0 0 * * 0'
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: gh-pages
fetch-depth: 0
- name: Delete directories older than 1 year
run: |
for dir in $(find . -mindepth 1 -maxdepth 2 -type d -not -path '*/\.*'); do
if ! git log -1 --since="1 year ago" -- "$dir" | grep -q .; then
echo "Deleting $dir"
rm -rf "$dir"
fi
done
- name: Commit and push
run: |
git config --global user.name 'GitHub Pages Cleanup'
git config --global user.email 'actions@stremio.com'
git add -A
git diff --cached --quiet || git commit -m "cleanup"
git push origin gh-pages

View file

@ -19,16 +19,10 @@ jobs:
- name: Zip build artifact
run: zip -r stremio-web.zip ./build
- name: Upload build artifact to GitHub release assets
uses: svenstaro/upload-release-action@2.10.0
uses: svenstaro/upload-release-action@2.11.2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: stremio-web.zip
asset_name: stremio-web.zip
tag: ${{ github.ref }}
overwrite: true
- name: Upload build artifact to Netlify
run: |
curl -H "Content-Type: application/zip" \
-H "Authorization: Bearer ${{ secrets.netlify_access_token }}" \
--data-binary "@stremio-web.zip" \
https://api.netlify.com/api/v1/sites/stremio-development.netlify.com/deploys
overwrite: true

18
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "stremio",
"version": "5.0.0-beta.25",
"version": "5.0.0-beta.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "stremio",
"version": "5.0.0-beta.25",
"version": "5.0.0-beta.26",
"license": "gpl-2.0",
"dependencies": {
"@babel/runtime": "7.26.0",
@ -14,7 +14,7 @@
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.49.4",
"@stremio/stremio-icons": "5.7.1",
"@stremio/stremio-video": "0.0.60",
"@stremio/stremio-video": "0.0.61",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
@ -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#8212fa77c4febd22ddb611590e9fb574dc845416",
"stremio-translations": "github:Stremio/stremio-translations#abe7684165a031755e9aee39da26daa806ba7824",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
@ -3412,9 +3412,9 @@
]
},
"node_modules/@stremio/stremio-video": {
"version": "0.0.60",
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.60.tgz",
"integrity": "sha512-RbmSi+Lk+3pb6f2ZkGVCnoMoJoujvVvSLDHiLGkXnzQwjYf2B2022NKlAQmHRuHN1sjD+VEsKD8foQH4hXGG1A==",
"version": "0.0.61",
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.61.tgz",
"integrity": "sha512-+m3ScDmImTilcpCnY5WO091SdWuDMrW8KkUs7y+ZL6PioZXNtd8fvRsmQoHKkWkkKX3K3LNTIfA7w5unITv1jA==",
"license": "MIT",
"dependencies": {
"buffer": "6.0.3",
@ -13434,8 +13434,8 @@
},
"node_modules/stremio-translations": {
"version": "1.44.12",
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8212fa77c4febd22ddb611590e9fb574dc845416",
"integrity": "sha512-5DladLUsghLlVRsZh2bBnb7UMqU8NEYMHc+YbzBvb1llgMk9elXFSHtAjInepZlC5zWx2pJYOQ8lQzzqogQdFw==",
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#abe7684165a031755e9aee39da26daa806ba7824",
"integrity": "sha512-bMpdJTFZqgemdoOQAARMPG7XaFgeu/zW/0vHmzavTM9DYUNIGuQaTC5RbVXIIII00RLOXoGLYf+dsxRVFiS9mA==",
"license": "MIT"
},
"node_modules/string_decoder": {

View file

@ -1,7 +1,7 @@
{
"name": "stremio",
"displayName": "Stremio",
"version": "5.0.0-beta.25",
"version": "5.0.0-beta.26",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
@ -19,7 +19,7 @@
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.49.4",
"@stremio/stremio-icons": "5.7.1",
"@stremio/stremio-video": "0.0.60",
"@stremio/stremio-video": "0.0.61",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
@ -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#8212fa77c4febd22ddb611590e9fb574dc845416",
"stremio-translations": "github:Stremio/stremio-translations#abe7684165a031755e9aee39da26daa806ba7824",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},

View file

@ -15,6 +15,7 @@ const routesRegexp = require('./routesRegexp');
const useAnimationFrame = require('./useAnimationFrame');
const useBinaryState = require('./useBinaryState');
const { default: useFullscreen } = require('./useFullscreen');
const { default: useInterval } = require('./useInterval');
const useLiveRef = require('./useLiveRef');
const useModelState = require('./useModelState');
const useNotifications = require('./useNotifications');
@ -23,6 +24,7 @@ const useProfile = require('./useProfile');
const { default: useSettings } = require('./useSettings');
const { default: useShell } = require('./useShell');
const useStreamingServer = require('./useStreamingServer');
const { default: useTimeout } = require('./useTimeout');
const useTorrent = require('./useTorrent');
const useTranslate = require('./useTranslate');
const { default: useOrientation } = require('./useOrientation');
@ -49,6 +51,7 @@ module.exports = {
useAnimationFrame,
useBinaryState,
useFullscreen,
useInterval,
useLiveRef,
useModelState,
useNotifications,
@ -57,6 +60,7 @@ module.exports = {
useSettings,
useShell,
useStreamingServer,
useTimeout,
useTorrent,
useTranslate,
useOrientation,

View file

@ -42,11 +42,21 @@ const useFullscreen = () => {
};
const onKeyDown = (event: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement;
const inputFocused =
activeElement &&
(activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.tagName === 'SELECT' ||
activeElement.isContentEditable);
if (event.code === 'Escape' && settings.escExitFullscreen) {
exitFullscreen();
}
if (event.code === 'KeyF') {
if (event.code === 'KeyF' && !inputFocused) {
toggleFullscreen();
}

26
src/common/useInterval.ts Normal file
View file

@ -0,0 +1,26 @@
import { useEffect, useRef } from 'react';
const useInterval = (duration: number) => {
const interval = useRef<NodeJS.Timer | null>(null);
const start = (callback: () => void) => {
cancel();
interval.current = setInterval(callback, duration);
};
const cancel = () => {
interval.current && clearInterval(interval.current);
interval.current = null;
};
useEffect(() => {
return () => cancel();
}, []);
return {
start,
cancel,
};
};
export default useInterval;

26
src/common/useTimeout.ts Normal file
View file

@ -0,0 +1,26 @@
import { useEffect, useRef } from 'react';
const useTimeout = (duration: number) => {
const timeout = useRef<NodeJS.Timeout | null>(null);
const start = (callback: () => void) => {
cancel();
timeout.current = setTimeout(callback, duration);
};
const cancel = () => {
timeout.current && clearTimeout(timeout.current);
timeout.current = null;
};
useEffect(() => {
return () => cancel();
}, []);
return {
start,
cancel,
};
};
export default useTimeout;

View file

@ -16,6 +16,8 @@ type Props = {
children: React.ReactNode,
onKeyDown?: (event: React.KeyboardEvent) => void,
onMouseDown?: (event: React.MouseEvent) => void,
onMouseUp?: (event: React.MouseEvent) => void,
onMouseLeave?: (event: React.MouseEvent) => void,
onLongPress?: () => void,
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void,
onDoubleClick?: () => void,

View file

@ -13,7 +13,6 @@ const Transition = ({ children, when, name }: Props) => {
const [state, setState] = useState('enter');
const [active, setActive] = useState(false);
const [transitionEnded, setTransitionEnded] = useState(false);
const callbackRef = useCallback((element: HTMLElement | null) => {
setElement(element);
@ -31,14 +30,12 @@ const Transition = ({ children, when, name }: Props) => {
}, [name, state, active, children]);
const onTransitionEnd = useCallback(() => {
setTransitionEnded(true);
state === 'exit' && setMounted(false);
}, [state]);
useEffect(() => {
setState(when ? 'enter' : 'exit');
when && setMounted(true);
setTransitionEnded(false);
}, [when]);
useEffect(() => {
@ -56,7 +53,6 @@ const Transition = ({ children, when, name }: Props) => {
mounted && cloneElement(children, {
ref: callbackRef,
className,
transitionEnded
})
);
};

View file

@ -22,9 +22,10 @@ type VideoState = Record<string, number>;
type Props = {
className: string,
videoState: VideoState,
disabled: boolean,
};
const Indicator = ({ className, videoState }: Props) => {
const Indicator = ({ className, videoState, disabled }: Props) => {
const timeout = useRef<NodeJS.Timeout | null>(null);
const prevVideoState = useRef<VideoState>(videoState);
@ -60,7 +61,7 @@ const Indicator = ({ className, videoState }: Props) => {
}, [videoState]);
return (
<Transition when={shown} name={'fade'}>
<Transition when={shown && !disabled} name={'fade'}>
<div className={classNames(className, styles['indicator-container'])}>
<div className={styles['indicator']}>
<div>{label} {value}</div>

View file

@ -9,7 +9,7 @@ const { useServices } = require('stremio/services');
const Option = require('./Option');
const styles = require('./styles');
const OptionsMenu = ({ className, stream, playbackDevices }) => {
const OptionsMenu = ({ className, stream, playbackDevices, extraSubtitlesTracks, selectedExtraSubtitlesTrackId }) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
@ -25,6 +25,12 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
const externalDevices = React.useMemo(() => {
return playbackDevices.filter(({ type }) => type === 'external');
}, [playbackDevices]);
const subtitlesTrackUrl = React.useMemo(() => {
const track = extraSubtitlesTracks?.find(({ id }) => id === selectedExtraSubtitlesTrackId);
return track?.fallbackUrl ?? track?.url ?? null;
}, [extraSubtitlesTracks, selectedExtraSubtitlesTrackId]);
const onCopyStreamButtonClick = React.useCallback(() => {
if (streamingUrl || downloadUrl) {
navigator.clipboard.writeText(streamingUrl || downloadUrl)
@ -52,6 +58,11 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
platform.openExternal(streamingUrl || downloadUrl);
}
}, [streamingUrl, downloadUrl]);
const onDownloadSubtitlesClick = React.useCallback(() => {
subtitlesTrackUrl && platform.openExternal(subtitlesTrackUrl);
}, [subtitlesTrackUrl]);
const onExternalDeviceRequested = React.useCallback((deviceId) => {
if (streamingUrl) {
core.transport.dispatch({
@ -94,6 +105,17 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
:
null
}
{
subtitlesTrackUrl ?
<Option
icon={'download'}
label={t('CTX_DOWNLOAD_SUBS')}
disabled={stream === null}
onClick={onDownloadSubtitlesClick}
/>
:
null
}
{
streamingUrl && externalDevices.map(({ id, name }) => (
<Option
@ -114,6 +136,8 @@ OptionsMenu.propTypes = {
className: PropTypes.string,
stream: PropTypes.object,
playbackDevices: PropTypes.array,
extraSubtitlesTracks: PropTypes.array,
selectedExtraSubtitlesTrackId: PropTypes.string,
};
module.exports = OptionsMenu;

View file

@ -236,6 +236,12 @@ const Player = ({ urlParams, queryParams }) => {
updateSettings({ subtitlesSize: size });
}, [updateSettings]);
const onUpdateSubtitlesSize = React.useCallback((delta) => {
const sizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(video.state.subtitlesSize);
const size = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, sizeIndex + delta))];
onSubtitlesSizeChanged(size);
}, [video.state.subtitlesSize, onSubtitlesSizeChanged]);
const onSubtitlesOffsetChanged = React.useCallback((offset) => {
updateSettings({ subtitlesOffset: offset });
}, [updateSettings]);
@ -622,6 +628,14 @@ const Player = ({ urlParams, queryParams }) => {
onIncreaseSubtitlesDelay();
break;
}
case 'Minus': {
onUpdateSubtitlesSize(-1);
break;
}
case 'Equal': {
onUpdateSubtitlesSize(1);
break;
}
case 'Escape': {
closeMenus();
!settings.escExitFullscreen && window.history.back();
@ -677,6 +691,7 @@ const Player = ({ urlParams, queryParams }) => {
toggleSideDrawer,
onDecreaseSubtitlesDelay,
onIncreaseSubtitlesDelay,
onUpdateSubtitlesSize,
]);
React.useEffect(() => {
@ -766,6 +781,8 @@ const Player = ({ urlParams, queryParams }) => {
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player?.selected?.stream}
playbackDevices={playbackDevices}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
/>
</ContextMenu>
<HorizontalNavBar
@ -820,6 +837,7 @@ const Player = ({ urlParams, queryParams }) => {
<Indicator
className={classnames(styles['layer'], styles['indicator-layer'])}
videoState={video.state}
disabled={subtitlesMenuOpen}
/>
{
nextVideoPopupOpen ?
@ -902,6 +920,8 @@ const Player = ({ urlParams, queryParams }) => {
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player.selected.stream}
playbackDevices={playbackDevices}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
/>
:
null

View file

@ -1,6 +1,6 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useMemo, useCallback, useState, forwardRef, memo, useRef, useEffect } from 'react';
import React, { useMemo, useCallback, useState, forwardRef, memo, useRef } from 'react';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import { useServices } from 'stremio/services';
@ -18,7 +18,7 @@ type Props = {
transitionEnded: boolean;
};
const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, className, closeSideDrawer, selected, transitionEnded, ...props }: Props, ref) => {
const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, className, closeSideDrawer, selected, ...props }: Props, ref) => {
const { core } = useServices();
const [season, setSeason] = useState<number>(seriesInfo?.season);
const selectedVideoRef = useRef<HTMLDivElement>(null);
@ -78,16 +78,14 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
event.stopPropagation();
};
const getSelectedRef = useCallback((video: Video) => {
return video.id === selected ? selectedVideoRef : null;
}, [selected]);
useEffect(() => {
transitionEnded && selectedVideoRef?.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, [transitionEnded]);
const onTransitionEnd = () => {
selectedVideoRef.current?.scrollIntoView({
behavior: 'smooth',
});
};
return (
<div ref={ref} className={classNames(styles['side-drawer'], className)} onMouseDown={onMouseDown}>
<div ref={ref} className={classNames(styles['side-drawer'], className)} onMouseDown={onMouseDown} onTransitionEnd={onTransitionEnd}>
<div className={styles['close-button']} onClick={closeSideDrawer}>
<Icon className={styles['icon']} name={'chevron-forward'} />
</div>
@ -116,6 +114,7 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
{videos.map((video, index) => (
<Video
key={index}
ref={video.id === selected ? selectedVideoRef : null}
className={styles['video']}
id={video.id}
title={video.title}
@ -131,7 +130,6 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
scheduled={video.scheduled}
onMarkVideoAsWatched={onMarkVideoAsWatched}
onMarkSeasonAsWatched={onMarkSeasonAsWatched}
ref={getSelectedRef(video)}
/>
))}
</div>

View file

@ -1,49 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { Button } = require('stremio/components');
const styles = require('./styles');
const DiscreteSelectInput = ({ className, value, label, disabled, dataset, onChange }) => {
const { t } = useTranslation();
const buttonOnClick = React.useCallback((event) => {
if (typeof onChange === 'function') {
onChange({
type: 'change',
value: event.currentTarget.dataset.type,
dataset: dataset,
reactEvent: event,
nativeEvent: event.nativeEvent
});
}
}, [dataset, onChange]);
return (
<div className={classnames(className, styles['discrete-input-container'], { 'disabled': disabled })}>
<div className={styles['header']}>{label}</div>
<div className={styles['input-container']} title={disabled ? t('DISABLED_LABEL', { label }) : null}>
<Button className={classnames(styles['button-container'], { 'disabled': disabled })} data-type={'decrement'} onClick={buttonOnClick}>
<Icon className={styles['icon']} name={'remove'} />
</Button>
<div className={styles['option-label']} title={value}>{value}</div>
<Button className={classnames(styles['button-container'], { 'disabled': disabled })} data-type={'increment'} onClick={buttonOnClick}>
<Icon className={styles['icon']} name={'add'} />
</Button>
</div>
</div>
);
};
DiscreteSelectInput.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
label: PropTypes.string,
disabled: PropTypes.bool,
dataset: PropTypes.object,
onChange: PropTypes.func
};
module.exports = DiscreteSelectInput;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const DiscreteSelectInput = require('./DiscreteSelectInput');
module.exports = DiscreteSelectInput;

View file

@ -1,14 +1,10 @@
// Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.discrete-input-container {
.stepper {
&:global(.disabled) {
.header {
color: var(--primary-foreground-color);
}
.input-container {
.content {
opacity: 0.4;
}
}
@ -19,14 +15,14 @@
opacity: 0.6;
}
.input-container {
.content {
display: flex;
flex-direction: row;
align-items: center;
border-radius: 3.5rem;
background: var(--overlay-color);
.button-container {
.button {
flex: none;
width: 3.5rem;
height: 3.5rem;
@ -42,7 +38,7 @@
}
}
.option-label {
.value {
flex: 1;
font-weight: 500;
text-align: center;

View file

@ -0,0 +1,98 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import { Button } from 'stremio/components';
import { useInterval, useTimeout } from 'stremio/common';
import styles from './Stepper.less';
const clamp = (value: number, min?: number, max?: number) => {
const minClamped = typeof min === 'number' ? Math.max(value, min) : value;
const maxClamped = typeof max === 'number' ? Math.min(minClamped, max) : minClamped;
return maxClamped;
};
type Props = {
className: string,
label: string,
value: number,
unit?: string,
step: number,
min?: number,
max?: number,
disabled?: boolean,
onChange: (value: number) => void,
};
const Stepper = ({ className, label, value, unit, step, min, max, disabled, onChange }: Props) => {
const { t } = useTranslation();
const localValue = useRef(value);
const interval = useInterval(100);
const timeout = useTimeout(250);
const cancel = () => {
interval.cancel();
timeout.cancel();
};
const updateValue = useCallback((delta: number) => {
onChange(clamp(localValue.current + delta, min, max));
}, [onChange]);
const onDecrementMouseDown = useCallback(() => {
cancel();
timeout.start(() => interval.start(() => updateValue(-step)));
}, [updateValue]);
const onDecrementMouseUp = useCallback(() => {
cancel();
updateValue(-step);
}, [updateValue]);
const onIncrementMouseDown = useCallback(() => {
cancel();
timeout.start(() => interval.start(() => updateValue(step)));
}, [updateValue]);
const onIncrementMouseUp = useCallback(() => {
cancel();
updateValue(step);
}, [updateValue]);
useEffect(() => {
localValue.current = value;
}, [value]);
return (
<div className={classNames(styles['stepper'], className)}>
<div className={styles['header']}>
{ t(label) }
</div>
<div className={styles['content']}>
<Button
className={classNames(styles['button'], { 'disabled': disabled })}
onMouseDown={onDecrementMouseDown}
onMouseUp={onDecrementMouseUp}
onMouseLeave={cancel}
>
<Icon className={styles['icon']} name={'remove'} />
</Button>
<div className={styles['value']}>
{ disabled ? '--' : `${value}${unit}` }
</div>
<Button
className={classNames(styles['button'], { 'disabled': disabled })}
onMouseDown={onIncrementMouseDown}
onMouseUp={onIncrementMouseUp}
onMouseLeave={cancel}
>
<Icon className={styles['icon']} name={'add'} />
</Button>
</div>
</div>
);
};
export default Stepper;

View file

@ -0,0 +1,2 @@
import Stepper from './Stepper';
export default Stepper;

View file

@ -3,11 +3,12 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { CONSTANTS, comparatorWithPriorities, languages } = require('stremio/common');
const { comparatorWithPriorities, languages } = require('stremio/common');
const { SUBTITLES_SIZES } = require('stremio/common/CONSTANTS');
const { Button } = require('stremio/components');
const DiscreteSelectInput = require('./DiscreteSelectInput');
const styles = require('./styles');
const { t } = require('i18next');
const { default: Stepper } = require('./Stepper');
const ORIGIN_PRIORITIES = {
'LOCAL': 3,
@ -98,51 +99,41 @@ const SubtitlesMenu = React.memo((props) => {
}
}
}, [props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
const onSubtitlesDelayChanged = React.useCallback((event) => {
const delta = event.value === 'increment' ? 250 : -250;
const onSubtitlesDelayChanged = React.useCallback((value) => {
if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
if (props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay)) {
const extraDelay = props.extraSubtitlesDelay + delta;
if (typeof props.onExtraSubtitlesDelayChanged === 'function') {
props.onExtraSubtitlesDelayChanged(extraDelay);
props.onExtraSubtitlesDelayChanged(value * 1000);
}
}
}
}, [props.selectedExtraSubtitlesTrackId, props.extraSubtitlesDelay, props.onExtraSubtitlesDelayChanged]);
const onSubtitlesSizeChanged = React.useCallback((event) => {
const delta = event.value === 'increment' ? 1 : -1;
const onSubtitlesSizeChanged = React.useCallback((value) => {
if (typeof props.selectedSubtitlesTrackId === 'string') {
if (props.subtitlesSize !== null && !isNaN(props.subtitlesSize)) {
const sizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(props.subtitlesSize);
const size = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, sizeIndex + delta))];
if (typeof props.onSubtitlesSizeChanged === 'function') {
props.onSubtitlesSizeChanged(size);
props.onSubtitlesSizeChanged(value);
}
}
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
if (props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize)) {
const extraSizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(props.extraSubtitlesSize);
const extraSize = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, extraSizeIndex + delta))];
if (typeof props.onExtraSubtitlesSizeChanged === 'function') {
props.onExtraSubtitlesSizeChanged(extraSize);
props.onExtraSubtitlesSizeChanged(value);
}
}
}
}, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesSize, props.extraSubtitlesSize, props.onSubtitlesSizeChanged, props.onExtraSubtitlesSizeChanged]);
const onSubtitlesOffsetChanged = React.useCallback((event) => {
const delta = event.value === 'increment' ? 1 : -1;
const onSubtitlesOffsetChanged = React.useCallback((value) => {
if (typeof props.selectedSubtitlesTrackId === 'string') {
if (props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset)) {
const offset = Math.max(0, Math.min(100, Math.floor(props.subtitlesOffset + delta)));
if (typeof props.onSubtitlesOffsetChanged === 'function') {
props.onSubtitlesOffsetChanged(offset);
props.onSubtitlesOffsetChanged(value);
}
}
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
if (props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset)) {
const offset = Math.max(0, Math.min(100, Math.floor(props.extraSubtitlesOffset + delta)));
if (typeof props.onExtraSubtitlesOffsetChanged === 'function') {
props.onExtraSubtitlesOffsetChanged(offset);
props.onExtraSubtitlesOffsetChanged(value);
}
}
}
@ -215,57 +206,35 @@ const SubtitlesMenu = React.memo((props) => {
<div className={styles['subtitles-settings-container']}>
<div className={styles['settings-header']}>{t('PLAYER_SUBTITLES_SETTINGS')}</div>
<div className={styles['settings-list']}>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('DELAY')}
value={typeof props.selectedExtraSubtitlesTrackId === 'string' && props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay) ? `${(props.extraSubtitlesDelay / 1000).toFixed(2)}s` : '--'}
disabled={typeof props.selectedExtraSubtitlesTrackId !== 'string' || props.extraSubtitlesDelay === null || isNaN(props.extraSubtitlesDelay)}
<Stepper
className={styles['stepper']}
label={'DELAY'}
value={props.extraSubtitlesDelay / 1000}
unit={'s'}
step={0.25}
disabled={props.extraSubtitlesDelay === null}
onChange={onSubtitlesDelayChanged}
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('SIZE')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesSize !== null && !isNaN(props.subtitlesSize) ? `${props.subtitlesSize}%` : '--'
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize) ? `${props.extraSubtitlesSize}%` : '--'
:
'--'
}
disabled={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesSize === null || isNaN(props.subtitlesSize)
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesSize === null || isNaN(props.extraSubtitlesSize)
:
true
}
<Stepper
className={styles['stepper']}
label={'SIZE'}
value={props.selectedSubtitlesTrackId ? props.subtitlesSize : props.selectedExtraSubtitlesTrackId ? props.extraSubtitlesSize : null}
unit={'%'}
step={25}
min={SUBTITLES_SIZES[0]}
max={SUBTITLES_SIZES[SUBTITLES_SIZES.length - 1]}
disabled={(props.selectedSubtitlesTrackId && props.subtitlesSize === null) || (props.selectedExtraSubtitlesTrackId && props.extraSubtitlesSize === null)}
onChange={onSubtitlesSizeChanged}
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('PLAYER_SUBTITLES_VERTICAL_POSIITON')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset) ? `${props.subtitlesOffset}%` : '--'
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset) ? `${props.extraSubtitlesOffset}%` : '--'
:
'--'
}
disabled={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesOffset === null || isNaN(props.subtitlesOffset)
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesOffset === null || isNaN(props.extraSubtitlesOffset)
:
true
}
<Stepper
className={styles['stepper']}
label={'PLAYER_SUBTITLES_VERTICAL_POSIITON'}
value={props.selectedSubtitlesTrackId ? props.subtitlesOffset : props.selectedExtraSubtitlesTrackId ? props.extraSubtitlesOffset : null}
unit={'%'}
step={1}
min={0}
max={100}
disabled={(props.selectedSubtitlesTrackId && props.subtitlesOffset === null) || (props.selectedExtraSubtitlesTrackId && props.extraSubtitlesOffset === null)}
onChange={onSubtitlesOffsetChanged}
/>
</div>

View file

@ -114,7 +114,7 @@
flex: 1;
}
.discrete-input {
.stepper {
padding: 0 1.5rem 1rem;
}
}

View file

@ -56,16 +56,18 @@ const Shortcuts = forwardRef<HTMLDivElement>((_, ref) => {
<kbd>I</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_MENU_VIDEOS'}>
<div className={styles['shortcut-container']}>
<kbd>V</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_FULLSCREEN'}>
<div className={styles['shortcut-container']}>
<kbd>F</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_SUBTITLES_SIZE'}>
<div className={styles['shortcut-container']}>
<kbd>-</kbd>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_AND') }</div>
<kbd>=</kbd>
</div>
</Option>
<Option label={'SETTINGS_SHORTCUT_SUBTITLES_DELAY'}>
<div className={styles['shortcut-container']}>
<kbd>G</kbd>