mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-20 21:22:22 +00:00
Merge branch 'development' into pr/704
This commit is contained in:
commit
56989781c8
22 changed files with 325 additions and 175 deletions
36
.github/workflows/pages_cleanup.yml
vendored
Normal file
36
.github/workflows/pages_cleanup.yml
vendored
Normal 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
|
||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -19,16 +19,10 @@ jobs:
|
||||||
- name: Zip build artifact
|
- name: Zip build artifact
|
||||||
run: zip -r stremio-web.zip ./build
|
run: zip -r stremio-web.zip ./build
|
||||||
- name: Upload build artifact to GitHub release assets
|
- 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:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
file: stremio-web.zip
|
file: stremio-web.zip
|
||||||
asset_name: stremio-web.zip
|
asset_name: stremio-web.zip
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref }}
|
||||||
overwrite: true
|
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
|
|
||||||
18
package-lock.json
generated
18
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"version": "5.0.0-beta.25",
|
"version": "5.0.0-beta.26",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"version": "5.0.0-beta.25",
|
"version": "5.0.0-beta.26",
|
||||||
"license": "gpl-2.0",
|
"license": "gpl-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.26.0",
|
"@babel/runtime": "7.26.0",
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"@stremio/stremio-colors": "5.2.0",
|
"@stremio/stremio-colors": "5.2.0",
|
||||||
"@stremio/stremio-core-web": "0.49.4",
|
"@stremio/stremio-core-web": "0.49.4",
|
||||||
"@stremio/stremio-icons": "5.7.1",
|
"@stremio/stremio-icons": "5.7.1",
|
||||||
"@stremio/stremio-video": "0.0.60",
|
"@stremio/stremio-video": "0.0.61",
|
||||||
"a-color-picker": "1.2.1",
|
"a-color-picker": "1.2.1",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
"react-i18next": "^15.1.3",
|
"react-i18next": "^15.1.3",
|
||||||
"react-is": "18.3.1",
|
"react-is": "18.3.1",
|
||||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
"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",
|
"url": "0.11.4",
|
||||||
"use-long-press": "^3.2.0"
|
"use-long-press": "^3.2.0"
|
||||||
},
|
},
|
||||||
|
|
@ -3412,9 +3412,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@stremio/stremio-video": {
|
"node_modules/@stremio/stremio-video": {
|
||||||
"version": "0.0.60",
|
"version": "0.0.61",
|
||||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.60.tgz",
|
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.61.tgz",
|
||||||
"integrity": "sha512-RbmSi+Lk+3pb6f2ZkGVCnoMoJoujvVvSLDHiLGkXnzQwjYf2B2022NKlAQmHRuHN1sjD+VEsKD8foQH4hXGG1A==",
|
"integrity": "sha512-+m3ScDmImTilcpCnY5WO091SdWuDMrW8KkUs7y+ZL6PioZXNtd8fvRsmQoHKkWkkKX3K3LNTIfA7w5unITv1jA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
|
@ -13434,8 +13434,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/stremio-translations": {
|
"node_modules/stremio-translations": {
|
||||||
"version": "1.44.12",
|
"version": "1.44.12",
|
||||||
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8212fa77c4febd22ddb611590e9fb574dc845416",
|
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#abe7684165a031755e9aee39da26daa806ba7824",
|
||||||
"integrity": "sha512-5DladLUsghLlVRsZh2bBnb7UMqU8NEYMHc+YbzBvb1llgMk9elXFSHtAjInepZlC5zWx2pJYOQ8lQzzqogQdFw==",
|
"integrity": "sha512-bMpdJTFZqgemdoOQAARMPG7XaFgeu/zW/0vHmzavTM9DYUNIGuQaTC5RbVXIIII00RLOXoGLYf+dsxRVFiS9mA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"displayName": "Stremio",
|
"displayName": "Stremio",
|
||||||
"version": "5.0.0-beta.25",
|
"version": "5.0.0-beta.26",
|
||||||
"author": "Smart Code OOD",
|
"author": "Smart Code OOD",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "gpl-2.0",
|
"license": "gpl-2.0",
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
"@stremio/stremio-colors": "5.2.0",
|
"@stremio/stremio-colors": "5.2.0",
|
||||||
"@stremio/stremio-core-web": "0.49.4",
|
"@stremio/stremio-core-web": "0.49.4",
|
||||||
"@stremio/stremio-icons": "5.7.1",
|
"@stremio/stremio-icons": "5.7.1",
|
||||||
"@stremio/stremio-video": "0.0.60",
|
"@stremio/stremio-video": "0.0.61",
|
||||||
"a-color-picker": "1.2.1",
|
"a-color-picker": "1.2.1",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
"react-i18next": "^15.1.3",
|
"react-i18next": "^15.1.3",
|
||||||
"react-is": "18.3.1",
|
"react-is": "18.3.1",
|
||||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
"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",
|
"url": "0.11.4",
|
||||||
"use-long-press": "^3.2.0"
|
"use-long-press": "^3.2.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const routesRegexp = require('./routesRegexp');
|
||||||
const useAnimationFrame = require('./useAnimationFrame');
|
const useAnimationFrame = require('./useAnimationFrame');
|
||||||
const useBinaryState = require('./useBinaryState');
|
const useBinaryState = require('./useBinaryState');
|
||||||
const { default: useFullscreen } = require('./useFullscreen');
|
const { default: useFullscreen } = require('./useFullscreen');
|
||||||
|
const { default: useInterval } = require('./useInterval');
|
||||||
const useLiveRef = require('./useLiveRef');
|
const useLiveRef = require('./useLiveRef');
|
||||||
const useModelState = require('./useModelState');
|
const useModelState = require('./useModelState');
|
||||||
const useNotifications = require('./useNotifications');
|
const useNotifications = require('./useNotifications');
|
||||||
|
|
@ -23,6 +24,7 @@ const useProfile = require('./useProfile');
|
||||||
const { default: useSettings } = require('./useSettings');
|
const { default: useSettings } = require('./useSettings');
|
||||||
const { default: useShell } = require('./useShell');
|
const { default: useShell } = require('./useShell');
|
||||||
const useStreamingServer = require('./useStreamingServer');
|
const useStreamingServer = require('./useStreamingServer');
|
||||||
|
const { default: useTimeout } = require('./useTimeout');
|
||||||
const useTorrent = require('./useTorrent');
|
const useTorrent = require('./useTorrent');
|
||||||
const useTranslate = require('./useTranslate');
|
const useTranslate = require('./useTranslate');
|
||||||
const { default: useOrientation } = require('./useOrientation');
|
const { default: useOrientation } = require('./useOrientation');
|
||||||
|
|
@ -49,6 +51,7 @@ module.exports = {
|
||||||
useAnimationFrame,
|
useAnimationFrame,
|
||||||
useBinaryState,
|
useBinaryState,
|
||||||
useFullscreen,
|
useFullscreen,
|
||||||
|
useInterval,
|
||||||
useLiveRef,
|
useLiveRef,
|
||||||
useModelState,
|
useModelState,
|
||||||
useNotifications,
|
useNotifications,
|
||||||
|
|
@ -57,6 +60,7 @@ module.exports = {
|
||||||
useSettings,
|
useSettings,
|
||||||
useShell,
|
useShell,
|
||||||
useStreamingServer,
|
useStreamingServer,
|
||||||
|
useTimeout,
|
||||||
useTorrent,
|
useTorrent,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
useOrientation,
|
useOrientation,
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,21 @@ const useFullscreen = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
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) {
|
if (event.code === 'Escape' && settings.escExitFullscreen) {
|
||||||
exitFullscreen();
|
exitFullscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.code === 'KeyF') {
|
if (event.code === 'KeyF' && !inputFocused) {
|
||||||
toggleFullscreen();
|
toggleFullscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
26
src/common/useInterval.ts
Normal file
26
src/common/useInterval.ts
Normal 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
26
src/common/useTimeout.ts
Normal 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;
|
||||||
|
|
@ -16,6 +16,8 @@ type Props = {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
onKeyDown?: (event: React.KeyboardEvent) => void,
|
onKeyDown?: (event: React.KeyboardEvent) => void,
|
||||||
onMouseDown?: (event: React.MouseEvent) => void,
|
onMouseDown?: (event: React.MouseEvent) => void,
|
||||||
|
onMouseUp?: (event: React.MouseEvent) => void,
|
||||||
|
onMouseLeave?: (event: React.MouseEvent) => void,
|
||||||
onLongPress?: () => void,
|
onLongPress?: () => void,
|
||||||
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void,
|
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void,
|
||||||
onDoubleClick?: () => void,
|
onDoubleClick?: () => void,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ const Transition = ({ children, when, name }: Props) => {
|
||||||
|
|
||||||
const [state, setState] = useState('enter');
|
const [state, setState] = useState('enter');
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
const [transitionEnded, setTransitionEnded] = useState(false);
|
|
||||||
|
|
||||||
const callbackRef = useCallback((element: HTMLElement | null) => {
|
const callbackRef = useCallback((element: HTMLElement | null) => {
|
||||||
setElement(element);
|
setElement(element);
|
||||||
|
|
@ -31,14 +30,12 @@ const Transition = ({ children, when, name }: Props) => {
|
||||||
}, [name, state, active, children]);
|
}, [name, state, active, children]);
|
||||||
|
|
||||||
const onTransitionEnd = useCallback(() => {
|
const onTransitionEnd = useCallback(() => {
|
||||||
setTransitionEnded(true);
|
|
||||||
state === 'exit' && setMounted(false);
|
state === 'exit' && setMounted(false);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState(when ? 'enter' : 'exit');
|
setState(when ? 'enter' : 'exit');
|
||||||
when && setMounted(true);
|
when && setMounted(true);
|
||||||
setTransitionEnded(false);
|
|
||||||
}, [when]);
|
}, [when]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -56,7 +53,6 @@ const Transition = ({ children, when, name }: Props) => {
|
||||||
mounted && cloneElement(children, {
|
mounted && cloneElement(children, {
|
||||||
ref: callbackRef,
|
ref: callbackRef,
|
||||||
className,
|
className,
|
||||||
transitionEnded
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,10 @@ type VideoState = Record<string, number>;
|
||||||
type Props = {
|
type Props = {
|
||||||
className: string,
|
className: string,
|
||||||
videoState: VideoState,
|
videoState: VideoState,
|
||||||
|
disabled: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Indicator = ({ className, videoState }: Props) => {
|
const Indicator = ({ className, videoState, disabled }: Props) => {
|
||||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const prevVideoState = useRef<VideoState>(videoState);
|
const prevVideoState = useRef<VideoState>(videoState);
|
||||||
|
|
||||||
|
|
@ -60,7 +61,7 @@ const Indicator = ({ className, videoState }: Props) => {
|
||||||
}, [videoState]);
|
}, [videoState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition when={shown} name={'fade'}>
|
<Transition when={shown && !disabled} name={'fade'}>
|
||||||
<div className={classNames(className, styles['indicator-container'])}>
|
<div className={classNames(className, styles['indicator-container'])}>
|
||||||
<div className={styles['indicator']}>
|
<div className={styles['indicator']}>
|
||||||
<div>{label} {value}</div>
|
<div>{label} {value}</div>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const { useServices } = require('stremio/services');
|
||||||
const Option = require('./Option');
|
const Option = require('./Option');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const OptionsMenu = ({ className, stream, playbackDevices }) => {
|
const OptionsMenu = ({ className, stream, playbackDevices, extraSubtitlesTracks, selectedExtraSubtitlesTrackId }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { core } = useServices();
|
const { core } = useServices();
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
|
|
@ -25,6 +25,12 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
|
||||||
const externalDevices = React.useMemo(() => {
|
const externalDevices = React.useMemo(() => {
|
||||||
return playbackDevices.filter(({ type }) => type === 'external');
|
return playbackDevices.filter(({ type }) => type === 'external');
|
||||||
}, [playbackDevices]);
|
}, [playbackDevices]);
|
||||||
|
|
||||||
|
const subtitlesTrackUrl = React.useMemo(() => {
|
||||||
|
const track = extraSubtitlesTracks?.find(({ id }) => id === selectedExtraSubtitlesTrackId);
|
||||||
|
return track?.fallbackUrl ?? track?.url ?? null;
|
||||||
|
}, [extraSubtitlesTracks, selectedExtraSubtitlesTrackId]);
|
||||||
|
|
||||||
const onCopyStreamButtonClick = React.useCallback(() => {
|
const onCopyStreamButtonClick = React.useCallback(() => {
|
||||||
if (streamingUrl || downloadUrl) {
|
if (streamingUrl || downloadUrl) {
|
||||||
navigator.clipboard.writeText(streamingUrl || downloadUrl)
|
navigator.clipboard.writeText(streamingUrl || downloadUrl)
|
||||||
|
|
@ -52,6 +58,11 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
|
||||||
platform.openExternal(streamingUrl || downloadUrl);
|
platform.openExternal(streamingUrl || downloadUrl);
|
||||||
}
|
}
|
||||||
}, [streamingUrl, downloadUrl]);
|
}, [streamingUrl, downloadUrl]);
|
||||||
|
|
||||||
|
const onDownloadSubtitlesClick = React.useCallback(() => {
|
||||||
|
subtitlesTrackUrl && platform.openExternal(subtitlesTrackUrl);
|
||||||
|
}, [subtitlesTrackUrl]);
|
||||||
|
|
||||||
const onExternalDeviceRequested = React.useCallback((deviceId) => {
|
const onExternalDeviceRequested = React.useCallback((deviceId) => {
|
||||||
if (streamingUrl) {
|
if (streamingUrl) {
|
||||||
core.transport.dispatch({
|
core.transport.dispatch({
|
||||||
|
|
@ -94,6 +105,17 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => {
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
subtitlesTrackUrl ?
|
||||||
|
<Option
|
||||||
|
icon={'download'}
|
||||||
|
label={t('CTX_DOWNLOAD_SUBS')}
|
||||||
|
disabled={stream === null}
|
||||||
|
onClick={onDownloadSubtitlesClick}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
{
|
{
|
||||||
streamingUrl && externalDevices.map(({ id, name }) => (
|
streamingUrl && externalDevices.map(({ id, name }) => (
|
||||||
<Option
|
<Option
|
||||||
|
|
@ -114,6 +136,8 @@ OptionsMenu.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
stream: PropTypes.object,
|
stream: PropTypes.object,
|
||||||
playbackDevices: PropTypes.array,
|
playbackDevices: PropTypes.array,
|
||||||
|
extraSubtitlesTracks: PropTypes.array,
|
||||||
|
selectedExtraSubtitlesTrackId: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = OptionsMenu;
|
module.exports = OptionsMenu;
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,12 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
updateSettings({ subtitlesSize: size });
|
updateSettings({ subtitlesSize: size });
|
||||||
}, [updateSettings]);
|
}, [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) => {
|
const onSubtitlesOffsetChanged = React.useCallback((offset) => {
|
||||||
updateSettings({ subtitlesOffset: offset });
|
updateSettings({ subtitlesOffset: offset });
|
||||||
}, [updateSettings]);
|
}, [updateSettings]);
|
||||||
|
|
@ -622,6 +628,14 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
onIncreaseSubtitlesDelay();
|
onIncreaseSubtitlesDelay();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'Minus': {
|
||||||
|
onUpdateSubtitlesSize(-1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'Equal': {
|
||||||
|
onUpdateSubtitlesSize(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'Escape': {
|
case 'Escape': {
|
||||||
closeMenus();
|
closeMenus();
|
||||||
!settings.escExitFullscreen && window.history.back();
|
!settings.escExitFullscreen && window.history.back();
|
||||||
|
|
@ -677,6 +691,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
toggleSideDrawer,
|
toggleSideDrawer,
|
||||||
onDecreaseSubtitlesDelay,
|
onDecreaseSubtitlesDelay,
|
||||||
onIncreaseSubtitlesDelay,
|
onIncreaseSubtitlesDelay,
|
||||||
|
onUpdateSubtitlesSize,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -766,6 +781,8 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||||
stream={player?.selected?.stream}
|
stream={player?.selected?.stream}
|
||||||
playbackDevices={playbackDevices}
|
playbackDevices={playbackDevices}
|
||||||
|
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
|
||||||
|
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<HorizontalNavBar
|
<HorizontalNavBar
|
||||||
|
|
@ -820,6 +837,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
<Indicator
|
<Indicator
|
||||||
className={classnames(styles['layer'], styles['indicator-layer'])}
|
className={classnames(styles['layer'], styles['indicator-layer'])}
|
||||||
videoState={video.state}
|
videoState={video.state}
|
||||||
|
disabled={subtitlesMenuOpen}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
nextVideoPopupOpen ?
|
nextVideoPopupOpen ?
|
||||||
|
|
@ -902,6 +920,8 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||||
stream={player.selected.stream}
|
stream={player.selected.stream}
|
||||||
playbackDevices={playbackDevices}
|
playbackDevices={playbackDevices}
|
||||||
|
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
|
||||||
|
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright (C) 2017-2024 Smart code 203358507
|
// 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 classNames from 'classnames';
|
||||||
import Icon from '@stremio/stremio-icons/react';
|
import Icon from '@stremio/stremio-icons/react';
|
||||||
import { useServices } from 'stremio/services';
|
import { useServices } from 'stremio/services';
|
||||||
|
|
@ -18,7 +18,7 @@ type Props = {
|
||||||
transitionEnded: boolean;
|
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 { core } = useServices();
|
||||||
const [season, setSeason] = useState<number>(seriesInfo?.season);
|
const [season, setSeason] = useState<number>(seriesInfo?.season);
|
||||||
const selectedVideoRef = useRef<HTMLDivElement>(null);
|
const selectedVideoRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -78,16 +78,14 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedRef = useCallback((video: Video) => {
|
const onTransitionEnd = () => {
|
||||||
return video.id === selected ? selectedVideoRef : null;
|
selectedVideoRef.current?.scrollIntoView({
|
||||||
}, [selected]);
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
useEffect(() => {
|
};
|
||||||
transitionEnded && selectedVideoRef?.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}, [transitionEnded]);
|
|
||||||
|
|
||||||
return (
|
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}>
|
<div className={styles['close-button']} onClick={closeSideDrawer}>
|
||||||
<Icon className={styles['icon']} name={'chevron-forward'} />
|
<Icon className={styles['icon']} name={'chevron-forward'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -116,6 +114,7 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
|
||||||
{videos.map((video, index) => (
|
{videos.map((video, index) => (
|
||||||
<Video
|
<Video
|
||||||
key={index}
|
key={index}
|
||||||
|
ref={video.id === selected ? selectedVideoRef : null}
|
||||||
className={styles['video']}
|
className={styles['video']}
|
||||||
id={video.id}
|
id={video.id}
|
||||||
title={video.title}
|
title={video.title}
|
||||||
|
|
@ -131,7 +130,6 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
|
||||||
scheduled={video.scheduled}
|
scheduled={video.scheduled}
|
||||||
onMarkVideoAsWatched={onMarkVideoAsWatched}
|
onMarkVideoAsWatched={onMarkVideoAsWatched}
|
||||||
onMarkSeasonAsWatched={onMarkSeasonAsWatched}
|
onMarkSeasonAsWatched={onMarkSeasonAsWatched}
|
||||||
ref={getSelectedRef(video)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// Copyright (C) 2017-2023 Smart code 203358507
|
|
||||||
|
|
||||||
const DiscreteSelectInput = require('./DiscreteSelectInput');
|
|
||||||
|
|
||||||
module.exports = DiscreteSelectInput;
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
// Copyright (C) 2017-2023 Smart code 203358507
|
.stepper {
|
||||||
|
|
||||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
|
||||||
|
|
||||||
.discrete-input-container {
|
|
||||||
&:global(.disabled) {
|
&:global(.disabled) {
|
||||||
.header {
|
.header {
|
||||||
color: var(--primary-foreground-color);
|
color: var(--primary-foreground-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container {
|
.content {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -19,14 +15,14 @@
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 3.5rem;
|
border-radius: 3.5rem;
|
||||||
background: var(--overlay-color);
|
background: var(--overlay-color);
|
||||||
|
|
||||||
.button-container {
|
.button {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 3.5rem;
|
width: 3.5rem;
|
||||||
height: 3.5rem;
|
height: 3.5rem;
|
||||||
|
|
@ -42,7 +38,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-label {
|
.value {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
98
src/routes/Player/SubtitlesMenu/Stepper/Stepper.tsx
Normal file
98
src/routes/Player/SubtitlesMenu/Stepper/Stepper.tsx
Normal 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;
|
||||||
2
src/routes/Player/SubtitlesMenu/Stepper/index.ts
Normal file
2
src/routes/Player/SubtitlesMenu/Stepper/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Stepper from './Stepper';
|
||||||
|
export default Stepper;
|
||||||
|
|
@ -3,11 +3,12 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
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 { Button } = require('stremio/components');
|
||||||
const DiscreteSelectInput = require('./DiscreteSelectInput');
|
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
const { t } = require('i18next');
|
const { t } = require('i18next');
|
||||||
|
const { default: Stepper } = require('./Stepper');
|
||||||
|
|
||||||
const ORIGIN_PRIORITIES = {
|
const ORIGIN_PRIORITIES = {
|
||||||
'LOCAL': 3,
|
'LOCAL': 3,
|
||||||
|
|
@ -98,51 +99,41 @@ const SubtitlesMenu = React.memo((props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
}, [props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
||||||
const onSubtitlesDelayChanged = React.useCallback((event) => {
|
const onSubtitlesDelayChanged = React.useCallback((value) => {
|
||||||
const delta = event.value === 'increment' ? 250 : -250;
|
|
||||||
if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
||||||
if (props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay)) {
|
if (props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay)) {
|
||||||
const extraDelay = props.extraSubtitlesDelay + delta;
|
|
||||||
if (typeof props.onExtraSubtitlesDelayChanged === 'function') {
|
if (typeof props.onExtraSubtitlesDelayChanged === 'function') {
|
||||||
props.onExtraSubtitlesDelayChanged(extraDelay);
|
props.onExtraSubtitlesDelayChanged(value * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [props.selectedExtraSubtitlesTrackId, props.extraSubtitlesDelay, props.onExtraSubtitlesDelayChanged]);
|
}, [props.selectedExtraSubtitlesTrackId, props.extraSubtitlesDelay, props.onExtraSubtitlesDelayChanged]);
|
||||||
const onSubtitlesSizeChanged = React.useCallback((event) => {
|
const onSubtitlesSizeChanged = React.useCallback((value) => {
|
||||||
const delta = event.value === 'increment' ? 1 : -1;
|
|
||||||
if (typeof props.selectedSubtitlesTrackId === 'string') {
|
if (typeof props.selectedSubtitlesTrackId === 'string') {
|
||||||
if (props.subtitlesSize !== null && !isNaN(props.subtitlesSize)) {
|
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') {
|
if (typeof props.onSubtitlesSizeChanged === 'function') {
|
||||||
props.onSubtitlesSizeChanged(size);
|
props.onSubtitlesSizeChanged(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
||||||
if (props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize)) {
|
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') {
|
if (typeof props.onExtraSubtitlesSizeChanged === 'function') {
|
||||||
props.onExtraSubtitlesSizeChanged(extraSize);
|
props.onExtraSubtitlesSizeChanged(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesSize, props.extraSubtitlesSize, props.onSubtitlesSizeChanged, props.onExtraSubtitlesSizeChanged]);
|
}, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesSize, props.extraSubtitlesSize, props.onSubtitlesSizeChanged, props.onExtraSubtitlesSizeChanged]);
|
||||||
const onSubtitlesOffsetChanged = React.useCallback((event) => {
|
const onSubtitlesOffsetChanged = React.useCallback((value) => {
|
||||||
const delta = event.value === 'increment' ? 1 : -1;
|
|
||||||
if (typeof props.selectedSubtitlesTrackId === 'string') {
|
if (typeof props.selectedSubtitlesTrackId === 'string') {
|
||||||
if (props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset)) {
|
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') {
|
if (typeof props.onSubtitlesOffsetChanged === 'function') {
|
||||||
props.onSubtitlesOffsetChanged(offset);
|
props.onSubtitlesOffsetChanged(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
} else if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
||||||
if (props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset)) {
|
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') {
|
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['subtitles-settings-container']}>
|
||||||
<div className={styles['settings-header']}>{t('PLAYER_SUBTITLES_SETTINGS')}</div>
|
<div className={styles['settings-header']}>{t('PLAYER_SUBTITLES_SETTINGS')}</div>
|
||||||
<div className={styles['settings-list']}>
|
<div className={styles['settings-list']}>
|
||||||
<DiscreteSelectInput
|
<Stepper
|
||||||
className={styles['discrete-input']}
|
className={styles['stepper']}
|
||||||
label={t('DELAY')}
|
label={'DELAY'}
|
||||||
value={typeof props.selectedExtraSubtitlesTrackId === 'string' && props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay) ? `${(props.extraSubtitlesDelay / 1000).toFixed(2)}s` : '--'}
|
value={props.extraSubtitlesDelay / 1000}
|
||||||
disabled={typeof props.selectedExtraSubtitlesTrackId !== 'string' || props.extraSubtitlesDelay === null || isNaN(props.extraSubtitlesDelay)}
|
unit={'s'}
|
||||||
|
step={0.25}
|
||||||
|
disabled={props.extraSubtitlesDelay === null}
|
||||||
onChange={onSubtitlesDelayChanged}
|
onChange={onSubtitlesDelayChanged}
|
||||||
/>
|
/>
|
||||||
<DiscreteSelectInput
|
<Stepper
|
||||||
className={styles['discrete-input']}
|
className={styles['stepper']}
|
||||||
label={t('SIZE')}
|
label={'SIZE'}
|
||||||
value={
|
value={props.selectedSubtitlesTrackId ? props.subtitlesSize : props.selectedExtraSubtitlesTrackId ? props.extraSubtitlesSize : null}
|
||||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
unit={'%'}
|
||||||
props.subtitlesSize !== null && !isNaN(props.subtitlesSize) ? `${props.subtitlesSize}%` : '--'
|
step={25}
|
||||||
:
|
min={SUBTITLES_SIZES[0]}
|
||||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
max={SUBTITLES_SIZES[SUBTITLES_SIZES.length - 1]}
|
||||||
props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize) ? `${props.extraSubtitlesSize}%` : '--'
|
disabled={(props.selectedSubtitlesTrackId && props.subtitlesSize === null) || (props.selectedExtraSubtitlesTrackId && props.extraSubtitlesSize === null)}
|
||||||
:
|
|
||||||
'--'
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
|
||||||
props.subtitlesSize === null || isNaN(props.subtitlesSize)
|
|
||||||
:
|
|
||||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
|
||||||
props.extraSubtitlesSize === null || isNaN(props.extraSubtitlesSize)
|
|
||||||
:
|
|
||||||
true
|
|
||||||
}
|
|
||||||
onChange={onSubtitlesSizeChanged}
|
onChange={onSubtitlesSizeChanged}
|
||||||
/>
|
/>
|
||||||
<DiscreteSelectInput
|
<Stepper
|
||||||
className={styles['discrete-input']}
|
className={styles['stepper']}
|
||||||
label={t('PLAYER_SUBTITLES_VERTICAL_POSIITON')}
|
label={'PLAYER_SUBTITLES_VERTICAL_POSIITON'}
|
||||||
value={
|
value={props.selectedSubtitlesTrackId ? props.subtitlesOffset : props.selectedExtraSubtitlesTrackId ? props.extraSubtitlesOffset : null}
|
||||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
unit={'%'}
|
||||||
props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset) ? `${props.subtitlesOffset}%` : '--'
|
step={1}
|
||||||
:
|
min={0}
|
||||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
max={100}
|
||||||
props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset) ? `${props.extraSubtitlesOffset}%` : '--'
|
disabled={(props.selectedSubtitlesTrackId && props.subtitlesOffset === null) || (props.selectedExtraSubtitlesTrackId && props.extraSubtitlesOffset === null)}
|
||||||
:
|
|
||||||
'--'
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
|
||||||
props.subtitlesOffset === null || isNaN(props.subtitlesOffset)
|
|
||||||
:
|
|
||||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
|
||||||
props.extraSubtitlesOffset === null || isNaN(props.extraSubtitlesOffset)
|
|
||||||
:
|
|
||||||
true
|
|
||||||
}
|
|
||||||
onChange={onSubtitlesOffsetChanged}
|
onChange={onSubtitlesOffsetChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discrete-input {
|
.stepper {
|
||||||
padding: 0 1.5rem 1rem;
|
padding: 0 1.5rem 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,16 +56,18 @@ const Shortcuts = forwardRef<HTMLDivElement>((_, ref) => {
|
||||||
<kbd>I</kbd>
|
<kbd>I</kbd>
|
||||||
</div>
|
</div>
|
||||||
</Option>
|
</Option>
|
||||||
<Option label={'SETTINGS_SHORTCUT_MENU_VIDEOS'}>
|
|
||||||
<div className={styles['shortcut-container']}>
|
|
||||||
<kbd>V</kbd>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
<Option label={'SETTINGS_SHORTCUT_FULLSCREEN'}>
|
<Option label={'SETTINGS_SHORTCUT_FULLSCREEN'}>
|
||||||
<div className={styles['shortcut-container']}>
|
<div className={styles['shortcut-container']}>
|
||||||
<kbd>F</kbd>
|
<kbd>F</kbd>
|
||||||
</div>
|
</div>
|
||||||
</Option>
|
</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'}>
|
<Option label={'SETTINGS_SHORTCUT_SUBTITLES_DELAY'}>
|
||||||
<div className={styles['shortcut-container']}>
|
<div className={styles['shortcut-container']}>
|
||||||
<kbd>G</kbd>
|
<kbd>G</kbd>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue