mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-01-11 22:40:31 +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
|
||||
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
18
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
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,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
@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;
|
||||
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 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>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.discrete-input {
|
||||
.stepper {
|
||||
padding: 0 1.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue