mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-16 13:12:23 +00:00
Merge branch 'Stremio:development' into feat/meta-preview-action-buttons-tooltips
This commit is contained in:
commit
5611a36783
16 changed files with 407 additions and 85 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"displayName": "Stremio",
|
"displayName": "Stremio",
|
||||||
"version": "5.0.0-beta.34",
|
"version": "5.0.0-beta.35",
|
||||||
"author": "Smart Code OOD",
|
"author": "Smart Code OOD",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "gpl-2.0",
|
"license": "gpl-2.0",
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
"@sentry/browser": "8.42.0",
|
"@sentry/browser": "8.42.0",
|
||||||
"@stremio/stremio-colors": "5.2.0",
|
"@stremio/stremio-colors": "5.2.0",
|
||||||
"@stremio/stremio-core-web": "0.56.4",
|
"@stremio/stremio-core-web": "0.56.4",
|
||||||
"@stremio/stremio-icons": "5.8.0",
|
"@stremio/stremio-icons": "5.10.0",
|
||||||
"@stremio/stremio-video": "0.0.75",
|
"@stremio/stremio-video": "0.0.77",
|
||||||
"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#90ea718c18750a0e9cd6824b0ef7c512a41cb90b",
|
"stremio-translations": "github:Stremio/stremio-translations#c23317eec194b5a3318e98c2ea6acae5cfa32e2a",
|
||||||
"url": "0.11.4",
|
"url": "0.11.4",
|
||||||
"use-long-press": "^3.2.0"
|
"use-long-press": "^3.2.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ importers:
|
||||||
specifier: 0.56.4
|
specifier: 0.56.4
|
||||||
version: 0.56.4
|
version: 0.56.4
|
||||||
'@stremio/stremio-icons':
|
'@stremio/stremio-icons':
|
||||||
specifier: 5.8.0
|
specifier: 5.10.0
|
||||||
version: 5.8.0
|
version: 5.10.0
|
||||||
'@stremio/stremio-video':
|
'@stremio/stremio-video':
|
||||||
specifier: 0.0.75
|
specifier: 0.0.77
|
||||||
version: 0.0.75
|
version: 0.0.77
|
||||||
a-color-picker:
|
a-color-picker:
|
||||||
specifier: 1.2.1
|
specifier: 1.2.1
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
|
|
@ -90,8 +90,8 @@ importers:
|
||||||
specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6
|
specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6
|
||||||
version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6
|
version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6
|
||||||
stremio-translations:
|
stremio-translations:
|
||||||
specifier: github:Stremio/stremio-translations#90ea718c18750a0e9cd6824b0ef7c512a41cb90b
|
specifier: github:Stremio/stremio-translations#c23317eec194b5a3318e98c2ea6acae5cfa32e2a
|
||||||
version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/90ea718c18750a0e9cd6824b0ef7c512a41cb90b
|
version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a
|
||||||
url:
|
url:
|
||||||
specifier: 0.11.4
|
specifier: 0.11.4
|
||||||
version: 0.11.4
|
version: 0.11.4
|
||||||
|
|
@ -1123,11 +1123,11 @@ packages:
|
||||||
'@stremio/stremio-core-web@0.56.4':
|
'@stremio/stremio-core-web@0.56.4':
|
||||||
resolution: {integrity: sha512-tFAMYgKrJ1bkvHRMpxDykM/844sDjgRPFk6FLhjQiwh01OHIyEgDqGo/NgwFM+CuMR4mW676SDvwNHkK0Xqg3w==}
|
resolution: {integrity: sha512-tFAMYgKrJ1bkvHRMpxDykM/844sDjgRPFk6FLhjQiwh01OHIyEgDqGo/NgwFM+CuMR4mW676SDvwNHkK0Xqg3w==}
|
||||||
|
|
||||||
'@stremio/stremio-icons@5.8.0':
|
'@stremio/stremio-icons@5.10.0':
|
||||||
resolution: {integrity: sha512-IVUvQbIWfA4YEHCTed7v/sdQJCJ+OOCf84LTWpkE2W6GLQ+15WHcMEJrVkE1X3ekYJnGg3GjT0KLO6tKSU0P4w==}
|
resolution: {integrity: sha512-Zw/vGC3D2yeQfk8xv/tfMJTDvbCPOI91tBg4XpR2+EgbZSX8Xvm7Vz457PIhFPhTAwdOPHp0VX0M3gzjbt0zOg==}
|
||||||
|
|
||||||
'@stremio/stremio-video@0.0.75':
|
'@stremio/stremio-video@0.0.77':
|
||||||
resolution: {integrity: sha512-oKXMq156BVagzziWoTsmgNYABCSfwV9hR/TM6+JR4lne5pW4qmUN17ba/Fxsr+USKHeCKUaz1u0asKBj06HfyA==}
|
resolution: {integrity: sha512-bnKBS5a9R3+M0zx95YpDUiPs1gXcKCsybgdxfZmpWuQaN0RE9bTBAUlIfBSrcEjVhufMOvg+cfXScT+0fBzTTw==}
|
||||||
|
|
||||||
'@stylistic/eslint-plugin-jsx@4.4.1':
|
'@stylistic/eslint-plugin-jsx@4.4.1':
|
||||||
resolution: {integrity: sha512-83SInq4u7z71vWwGG+6ViOtlOmZ6tSrDkMPhrvdBBTGMLA0gs22WSdhQ4vZP3oJ5Xg4ythvqeUiFSedvVxzhyA==}
|
resolution: {integrity: sha512-83SInq4u7z71vWwGG+6ViOtlOmZ6tSrDkMPhrvdBBTGMLA0gs22WSdhQ4vZP3oJ5Xg4ythvqeUiFSedvVxzhyA==}
|
||||||
|
|
@ -4133,9 +4133,9 @@ packages:
|
||||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/90ea718c18750a0e9cd6824b0ef7c512a41cb90b:
|
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a:
|
||||||
resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/90ea718c18750a0e9cd6824b0ef7c512a41cb90b}
|
resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a}
|
||||||
version: 1.48.0
|
version: 1.51.0
|
||||||
|
|
||||||
string-length@4.0.2:
|
string-length@4.0.2:
|
||||||
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
|
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
|
||||||
|
|
@ -5874,9 +5874,9 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.24.1
|
'@babel/runtime': 7.24.1
|
||||||
|
|
||||||
'@stremio/stremio-icons@5.8.0': {}
|
'@stremio/stremio-icons@5.10.0': {}
|
||||||
|
|
||||||
'@stremio/stremio-video@0.0.75':
|
'@stremio/stremio-video@0.0.77':
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
color: 4.2.3
|
color: 4.2.3
|
||||||
|
|
@ -9378,7 +9378,7 @@ snapshots:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
internal-slot: 1.1.0
|
internal-slot: 1.1.0
|
||||||
|
|
||||||
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/90ea718c18750a0e9cd6824b0ef7c512a41cb90b: {}
|
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a: {}
|
||||||
|
|
||||||
string-length@4.0.2:
|
string-length@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ type Props = {
|
||||||
style?: object,
|
style?: object,
|
||||||
href?: string,
|
href?: string,
|
||||||
target?: string
|
target?: string
|
||||||
|
download?: string,
|
||||||
title?: string,
|
title?: string,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
tabIndex?: number,
|
tabIndex?: number,
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,20 @@ const PADDING = 8;
|
||||||
|
|
||||||
type Coordinates = [number, number];
|
type Coordinates = [number, number];
|
||||||
type Size = [number, number];
|
type Size = [number, number];
|
||||||
|
type Lock = 'top' | 'right' | 'bottom' | 'left';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
on: RefObject<HTMLElement>[],
|
on: RefObject<HTMLElement>[],
|
||||||
autoClose: boolean,
|
autoClose: boolean,
|
||||||
|
lock?: Lock,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContextMenu = ({ children, on, autoClose }: Props) => {
|
const ContextMenu = ({ children, on, autoClose, lock }: Props) => {
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
const [position, setPosition] = useState<Coordinates>([0, 0]);
|
const [position, setPosition] = useState<Coordinates>([0, 0]);
|
||||||
const [containerSize, setContainerSize] = useState<Size>([0, 0]);
|
const [containerSize, setContainerSize] = useState<Size>([0, 0]);
|
||||||
|
const [triggerRect, setTriggerRect] = useState<DOMRect | null>(null);
|
||||||
|
|
||||||
const ref = useCallback((element: HTMLDivElement) => {
|
const ref = useCallback((element: HTMLDivElement) => {
|
||||||
element && setContainerSize([element.offsetWidth, element.offsetHeight]);
|
element && setContainerSize([element.offsetWidth, element.offsetHeight]);
|
||||||
|
|
@ -26,7 +29,32 @@ const ContextMenu = ({ children, on, autoClose }: Props) => {
|
||||||
const style = useMemo(() => {
|
const style = useMemo(() => {
|
||||||
const [viewportWidth, viewportHeight] = [window.innerWidth, window.innerHeight];
|
const [viewportWidth, viewportHeight] = [window.innerWidth, window.innerHeight];
|
||||||
const [containerWidth, containerHeight] = containerSize;
|
const [containerWidth, containerHeight] = containerSize;
|
||||||
const [x, y] = position;
|
|
||||||
|
let x: number;
|
||||||
|
let y: number;
|
||||||
|
|
||||||
|
if (lock && triggerRect) {
|
||||||
|
switch (lock) {
|
||||||
|
case 'top':
|
||||||
|
x = triggerRect.left;
|
||||||
|
y = triggerRect.top - containerHeight;
|
||||||
|
break;
|
||||||
|
case 'bottom':
|
||||||
|
x = triggerRect.left;
|
||||||
|
y = triggerRect.bottom;
|
||||||
|
break;
|
||||||
|
case 'left':
|
||||||
|
x = triggerRect.left - containerWidth;
|
||||||
|
y = triggerRect.top;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
x = triggerRect.right;
|
||||||
|
y = triggerRect.top;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
[x, y] = position;
|
||||||
|
}
|
||||||
|
|
||||||
const left = Math.max(
|
const left = Math.max(
|
||||||
PADDING,
|
PADDING,
|
||||||
|
|
@ -45,7 +73,7 @@ const ContextMenu = ({ children, on, autoClose }: Props) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return { top, left };
|
return { top, left };
|
||||||
}, [position, containerSize]);
|
}, [position, containerSize, lock, triggerRect]);
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
setActive(false);
|
setActive(false);
|
||||||
|
|
@ -55,12 +83,17 @@ const ContextMenu = ({ children, on, autoClose }: Props) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onContextMenu = (event: MouseEvent) => {
|
const onContextMenu = useCallback((event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
setPosition([event.clientX, event.clientY]);
|
if (lock) {
|
||||||
|
const target = event.currentTarget as HTMLElement;
|
||||||
|
setTriggerRect(target.getBoundingClientRect());
|
||||||
|
} else {
|
||||||
|
setPosition([event.clientX, event.clientY]);
|
||||||
|
}
|
||||||
setActive(true);
|
setActive(true);
|
||||||
};
|
}, [lock]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((event: KeyboardEvent) => event.key === 'Escape' && close(), []);
|
const handleKeyDown = useCallback((event: KeyboardEvent) => event.key === 'Escape' && close(), []);
|
||||||
|
|
||||||
|
|
@ -76,7 +109,7 @@ const ContextMenu = ({ children, on, autoClose }: Props) => {
|
||||||
on.forEach((ref) => ref.current && ref.current.removeEventListener('contextmenu', onContextMenu));
|
on.forEach((ref) => ref.current && ref.current.removeEventListener('contextmenu', onContextMenu));
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [on]);
|
}, [on, onContextMenu, handleKeyDown]);
|
||||||
|
|
||||||
return createPortal((
|
return createPortal((
|
||||||
<Transition when={active} name={'fade'}>
|
<Transition when={active} name={'fade'}>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const NavMenu = require('./NavMenu');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
const { t } = require('i18next');
|
const { t } = require('i18next');
|
||||||
|
|
||||||
const HorizontalNavBar = React.memo(({ className, route, query, title, backButton, searchBar, fullscreenButton, navMenu, ...props }) => {
|
const HorizontalNavBar = React.memo(({ className, route, query, title, backButton, searchBar, fullscreenButton, navMenu, hdrInfo, ...props }) => {
|
||||||
const backButtonOnClick = React.useCallback(() => {
|
const backButtonOnClick = React.useCallback(() => {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -53,6 +53,14 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<div className={styles['buttons-container']}>
|
<div className={styles['buttons-container']}>
|
||||||
|
{
|
||||||
|
hdrInfo && (hdrInfo.gamma === 'pq' || hdrInfo.gamma === 'hlg') ?
|
||||||
|
<div className={styles['hdr-indicator']} title={hdrInfo.gamma === 'pq' ? 'HDR10' : 'HLG'}>
|
||||||
|
<Icon className={styles['icon']} name={'hdr'} />
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
{
|
{
|
||||||
!isIOSPWA && fullscreenButton ?
|
!isIOSPWA && fullscreenButton ?
|
||||||
<Button className={styles['button-container']} title={fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')} tabIndex={-1} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
|
<Button className={styles['button-container']} title={fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')} tabIndex={-1} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
|
||||||
|
|
@ -82,7 +90,10 @@ HorizontalNavBar.propTypes = {
|
||||||
backButton: PropTypes.bool,
|
backButton: PropTypes.bool,
|
||||||
searchBar: PropTypes.bool,
|
searchBar: PropTypes.bool,
|
||||||
fullscreenButton: PropTypes.bool,
|
fullscreenButton: PropTypes.bool,
|
||||||
navMenu: PropTypes.bool
|
navMenu: PropTypes.bool,
|
||||||
|
hdrInfo: PropTypes.shape({
|
||||||
|
gamma: PropTypes.string,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = HorizontalNavBar;
|
module.exports = HorizontalNavBar;
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,30 @@
|
||||||
.buttons-container {
|
.buttons-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hdr-indicator {
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 3.5rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex: none;
|
||||||
|
width: 3rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.button-container {
|
.button-container {
|
||||||
flex: none;
|
flex: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ const ControlBar = React.forwardRef(({
|
||||||
onToggleSpeedMenu,
|
onToggleSpeedMenu,
|
||||||
onToggleSideDrawer,
|
onToggleSideDrawer,
|
||||||
onToggleOptionsMenu,
|
onToggleOptionsMenu,
|
||||||
|
videoScale,
|
||||||
|
videoScaleLabel,
|
||||||
|
onVideoScaleChanged,
|
||||||
onToggleStatisticsMenu,
|
onToggleStatisticsMenu,
|
||||||
onTouchEnd,
|
onTouchEnd,
|
||||||
...props
|
...props
|
||||||
|
|
@ -176,6 +179,9 @@ const ControlBar = React.forwardRef(({
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
<Button className={classnames(styles['control-bar-button'], { 'disabled': videoScale === null })} title={videoScaleLabel} tabIndex={-1} onClick={onVideoScaleChanged}>
|
||||||
|
<Icon className={styles['icon']} name={'scale'} />
|
||||||
|
</Button>
|
||||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': !stream })} tabIndex={-1} onMouseDown={onOptionsButtonMouseDown} onClick={onToggleOptionsMenu}>
|
<Button className={classnames(styles['control-bar-button'], { 'disabled': !stream })} tabIndex={-1} onMouseDown={onOptionsButtonMouseDown} onClick={onToggleOptionsMenu}>
|
||||||
<Icon className={styles['icon']} name={'more-horizontal'} />
|
<Icon className={styles['icon']} name={'more-horizontal'} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -194,6 +200,9 @@ ControlBar.propTypes = {
|
||||||
volume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
playbackSpeed: PropTypes.number,
|
playbackSpeed: PropTypes.number,
|
||||||
|
videoScale: PropTypes.string,
|
||||||
|
videoScaleLabel: PropTypes.string,
|
||||||
|
onVideoScaleChanged: PropTypes.func,
|
||||||
subtitlesTracks: PropTypes.array,
|
subtitlesTracks: PropTypes.array,
|
||||||
audioTracks: PropTypes.array,
|
audioTracks: PropTypes.array,
|
||||||
metaItem: PropTypes.object,
|
metaItem: PropTypes.object,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,13 @@ import styles from './Indicator.less';
|
||||||
|
|
||||||
type Property = {
|
type Property = {
|
||||||
label: string,
|
label: string,
|
||||||
format: (value: number) => string,
|
format: (value: number | string) => string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const VIDEO_SCALE_KEYS: Record<string, string> = {
|
||||||
|
'contain': 'PLAYER_SCALE_FIT',
|
||||||
|
'cover': 'PLAYER_SCALE_CROP',
|
||||||
|
'fill': 'PLAYER_SCALE_STRETCH',
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROPERTIES: Record<string, Property> = {
|
const PROPERTIES: Record<string, Property> = {
|
||||||
|
|
@ -15,9 +21,13 @@ const PROPERTIES: Record<string, Property> = {
|
||||||
label: 'SUBTITLES_DELAY',
|
label: 'SUBTITLES_DELAY',
|
||||||
format: (value) => `${(value / 1000).toFixed(2)}s`,
|
format: (value) => `${(value / 1000).toFixed(2)}s`,
|
||||||
},
|
},
|
||||||
|
'videoScale': {
|
||||||
|
label: 'VIDEO_SCALE',
|
||||||
|
format: (value) => t(VIDEO_SCALE_KEYS[String(value)] || String(value)),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type VideoState = Record<string, number>;
|
type VideoState = Record<string, number | string>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className: string,
|
className: string,
|
||||||
|
|
@ -28,6 +38,7 @@ type Props = {
|
||||||
const Indicator = ({ className, videoState, disabled }: 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);
|
||||||
|
const initialized = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
const [shown, show, hide] = useBinaryState(false);
|
const [shown, show, hide] = useBinaryState(false);
|
||||||
const [current, setCurrent] = useState<string | null>(null);
|
const [current, setCurrent] = useState<string | null>(null);
|
||||||
|
|
@ -49,11 +60,15 @@ const Indicator = ({ className, videoState, disabled }: Props) => {
|
||||||
const next = videoState[property];
|
const next = videoState[property];
|
||||||
|
|
||||||
if (next && next !== prev) {
|
if (next && next !== prev) {
|
||||||
setCurrent(property);
|
if (!initialized.current.has(property)) {
|
||||||
show();
|
initialized.current.add(property);
|
||||||
|
} else {
|
||||||
|
setCurrent(property);
|
||||||
|
show();
|
||||||
|
|
||||||
timeout.current && clearTimeout(timeout.current);
|
timeout.current && clearTimeout(timeout.current);
|
||||||
timeout.current = setTimeout(hide, 1000);
|
timeout.current = setTimeout(hide, 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,9 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
|
|
||||||
const isNavigating = React.useRef(false);
|
const isNavigating = React.useRef(false);
|
||||||
|
|
||||||
|
const VIDEO_SCALES = ['contain', 'cover', 'fill'];
|
||||||
|
const VIDEO_SCALE_LABELS = { contain: t('PLAYER_SCALE_FIT'), cover: t('PLAYER_SCALE_CROP'), fill: t('PLAYER_SCALE_STRETCH') };
|
||||||
|
|
||||||
const playbackSpeed = React.useRef(video.state.playbackSpeed || 1);
|
const playbackSpeed = React.useRef(video.state.playbackSpeed || 1);
|
||||||
const pressTimer = React.useRef(null);
|
const pressTimer = React.useRef(null);
|
||||||
const longPress = React.useRef(false);
|
const longPress = React.useRef(false);
|
||||||
|
|
@ -236,6 +239,13 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onVideoScaleChanged = React.useCallback(() => {
|
||||||
|
const currentScale = video.state.videoScale || 'contain';
|
||||||
|
const currentIndex = VIDEO_SCALES.indexOf(currentScale);
|
||||||
|
const nextScale = VIDEO_SCALES[(currentIndex + 1) % VIDEO_SCALES.length];
|
||||||
|
video.setVideoScale(nextScale);
|
||||||
|
}, [video.state.videoScale]);
|
||||||
|
|
||||||
const onSubtitlesTrackSelected = React.useCallback((track) => {
|
const onSubtitlesTrackSelected = React.useCallback((track) => {
|
||||||
video.setSubtitlesTrack(track?.id ?? null);
|
video.setSubtitlesTrack(track?.id ?? null);
|
||||||
streamStateChanged({
|
streamStateChanged({
|
||||||
|
|
@ -599,6 +609,29 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
|
|
||||||
useMediaSession(video.state, player, onPlayRequested, onPauseRequested, onNextVideoRequested);
|
useMediaSession(video.state, player, onPlayRequested, onPauseRequested, onNextVideoRequested);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onMediaKey = (action) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'play-pause':
|
||||||
|
video.state.paused ? onPlayRequested() : onPauseRequested();
|
||||||
|
break;
|
||||||
|
case 'next-track':
|
||||||
|
if (player.nextVideo !== null) {
|
||||||
|
video.setTime(0);
|
||||||
|
onNextVideoRequested();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'previous-track':
|
||||||
|
if (video.state.time !== null && video.state.time > 5000) {
|
||||||
|
onSeekRequested(0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
shell.on('media-key', onMediaKey);
|
||||||
|
return () => shell.off('media-key', onMediaKey);
|
||||||
|
}, [video.state.paused, video.state.time, player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested, onSeekRequested]);
|
||||||
|
|
||||||
onShortcut('seekForward', (combo) => {
|
onShortcut('seekForward', (combo) => {
|
||||||
if (video.state.time !== null) {
|
if (video.state.time !== null) {
|
||||||
const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
||||||
|
|
@ -916,6 +949,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
title={player.title !== null ? player.title : ''}
|
title={player.title !== null ? player.title : ''}
|
||||||
backButton={true}
|
backButton={true}
|
||||||
fullscreenButton={true}
|
fullscreenButton={true}
|
||||||
|
hdrInfo={video.state.hdrInfo}
|
||||||
onMouseMove={onBarMouseMove}
|
onMouseMove={onBarMouseMove}
|
||||||
onMouseOver={onBarMouseMove}
|
onMouseOver={onBarMouseMove}
|
||||||
/>
|
/>
|
||||||
|
|
@ -955,6 +989,9 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
||||||
onToggleAudioMenu={toggleAudioMenu}
|
onToggleAudioMenu={toggleAudioMenu}
|
||||||
onToggleSpeedMenu={toggleSpeedMenu}
|
onToggleSpeedMenu={toggleSpeedMenu}
|
||||||
|
videoScale={video.state.videoScale}
|
||||||
|
videoScaleLabel={VIDEO_SCALE_LABELS[video.state.videoScale || 'contain']}
|
||||||
|
onVideoScaleChanged={onVideoScaleChanged}
|
||||||
onToggleStatisticsMenu={toggleStatisticsMenu}
|
onToggleStatisticsMenu={toggleStatisticsMenu}
|
||||||
onToggleSideDrawer={toggleSideDrawer}
|
onToggleSideDrawer={toggleSideDrawer}
|
||||||
onMouseMove={onBarMouseMove}
|
onMouseMove={onBarMouseMove}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright (C) 2017-2026 Smart code 203358507
|
||||||
|
|
||||||
|
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||||
|
|
||||||
|
.variant-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 4rem;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
&:global(.selected), &:hover {
|
||||||
|
background-color: var(--overlay-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
.variant-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
text-wrap: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant-origin {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-placeholder-text);
|
||||||
|
text-wrap: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex: none;
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 100%;
|
||||||
|
margin-left: 1rem;
|
||||||
|
background-color: var(--secondary-accent-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: 16rem;
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
background-color: var(--overlay-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
flex: none;
|
||||||
|
width: 1.4rem;
|
||||||
|
height: 1.4rem;
|
||||||
|
color: var(--color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-option-label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
// Copyright (C) 2017-2026 Smart code 203358507
|
||||||
|
|
||||||
|
import React, { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, ContextMenu } from 'stremio/components';
|
||||||
|
import { languages, useToast } from 'stremio/common';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Icon from '@stremio/stremio-icons/react';
|
||||||
|
import styles from './SubtitleVariant.less';
|
||||||
|
|
||||||
|
type SubtitlesTrack = {
|
||||||
|
id: string,
|
||||||
|
addonSubtitleId?: string,
|
||||||
|
lang: string,
|
||||||
|
origin: string,
|
||||||
|
label?: string,
|
||||||
|
url?: string,
|
||||||
|
fallbackUrl?: string,
|
||||||
|
embedded?: boolean,
|
||||||
|
local?: boolean,
|
||||||
|
exclusive?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
track: SubtitlesTrack,
|
||||||
|
selected: boolean,
|
||||||
|
onSelect: (track: SubtitlesTrack) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasValidLabel = (label?: string) => label && label.length > 0 && !label.startsWith('http');
|
||||||
|
|
||||||
|
const SubtitleVariant = ({ track, selected, onSelect }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
const buttonRef = useRef<HTMLElement>(null);
|
||||||
|
const triggers = useMemo(() => [buttonRef], []);
|
||||||
|
|
||||||
|
const downloadUrl = track.fallbackUrl || track.url;
|
||||||
|
const variantLabel = hasValidLabel(track.label) ? track.label : languages.label(track.lang);
|
||||||
|
const downloadFileName = hasValidLabel(track.label) ? track.label : `subtitle-${track.lang || 'unknown'}`;
|
||||||
|
const canCopyUrl = typeof downloadUrl === 'string' && !downloadUrl.startsWith('blob:');
|
||||||
|
const hoverTitle = hasValidLabel(track.label)
|
||||||
|
? track.label
|
||||||
|
: downloadUrl?.split('/').pop()?.split('?')[0] || variantLabel;
|
||||||
|
|
||||||
|
const onSelectClick = useCallback(() => {
|
||||||
|
onSelect(track);
|
||||||
|
}, [onSelect, track]);
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback((value: string, successKey: string, errorKey: string) => {
|
||||||
|
navigator.clipboard.writeText(value)
|
||||||
|
.then(() => toast.show({ type: 'success', title: t(successKey), timeout: 4000 }))
|
||||||
|
.catch(() => toast.show({ type: 'error', title: t(errorKey), timeout: 4000 }));
|
||||||
|
}, [toast, t]);
|
||||||
|
|
||||||
|
const onCopyUrlClick = useCallback(() => {
|
||||||
|
if (downloadUrl) {
|
||||||
|
copyToClipboard(downloadUrl, 'PLAYER_COPY_SUBTITLE_URL_SUCCESS', 'PLAYER_COPY_SUBTITLE_URL_ERROR');
|
||||||
|
}
|
||||||
|
}, [downloadUrl, copyToClipboard]);
|
||||||
|
|
||||||
|
const onCopyIdClick = useCallback(() => {
|
||||||
|
if (track.addonSubtitleId) {
|
||||||
|
copyToClipboard(track.addonSubtitleId, 'PLAYER_COPY_SUBTITLE_ID_SUCCESS', 'PLAYER_COPY_SUBTITLE_ID_ERROR');
|
||||||
|
}
|
||||||
|
}, [track.addonSubtitleId, copyToClipboard]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={buttonRef}
|
||||||
|
title={hoverTitle}
|
||||||
|
onClick={onSelectClick}
|
||||||
|
className={classNames(styles['variant-option'], { 'selected': selected })}
|
||||||
|
>
|
||||||
|
<div className={styles['info']}>
|
||||||
|
<div className={styles['variant-label']}>
|
||||||
|
{variantLabel}
|
||||||
|
</div>
|
||||||
|
<div className={styles['variant-origin']}>
|
||||||
|
{t(track.origin)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selected ? <div className={styles['icon']} /> : null}
|
||||||
|
{!track.embedded &&
|
||||||
|
<ContextMenu on={triggers} autoClose={true} lock={'bottom'}>
|
||||||
|
{downloadUrl ?
|
||||||
|
<Button
|
||||||
|
className={styles['context-menu-option']}
|
||||||
|
title={t('CTX_DOWNLOAD_SUBTITLE')}
|
||||||
|
href={downloadUrl}
|
||||||
|
target={'_blank'}
|
||||||
|
download={downloadFileName}
|
||||||
|
>
|
||||||
|
<Icon className={styles['menu-icon']} name={'download'} />
|
||||||
|
<div className={styles['context-menu-option-label']}>
|
||||||
|
{t('CTX_DOWNLOAD_SUBTITLE')}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
{canCopyUrl ?
|
||||||
|
<Button
|
||||||
|
className={styles['context-menu-option']}
|
||||||
|
title={t('CTX_COPY_SUBTITLE_URL')}
|
||||||
|
onClick={onCopyUrlClick}
|
||||||
|
>
|
||||||
|
<Icon className={styles['menu-icon']} name={'link'} />
|
||||||
|
<div className={styles['context-menu-option-label']}>
|
||||||
|
{t('CTX_COPY_SUBTITLE_URL')}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
{track.addonSubtitleId ?
|
||||||
|
<Button
|
||||||
|
className={styles['context-menu-option']}
|
||||||
|
title={t('CTX_COPY_SUBTITLE_ID')}
|
||||||
|
onClick={onCopyIdClick}
|
||||||
|
>
|
||||||
|
<Icon className={styles['menu-icon']} name={'share'} />
|
||||||
|
<div className={styles['context-menu-option-label']}>
|
||||||
|
{t('CTX_COPY_SUBTITLE_ID')}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</ContextMenu>
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubtitleVariant;
|
||||||
5
src/routes/Player/SubtitlesMenu/SubtitleVariant/index.ts
Normal file
5
src/routes/Player/SubtitlesMenu/SubtitleVariant/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright (C) 2017-2026 Smart code 203358507
|
||||||
|
|
||||||
|
import SubtitleVariant from './SubtitleVariant';
|
||||||
|
|
||||||
|
export default SubtitleVariant;
|
||||||
|
|
@ -9,6 +9,7 @@ const { Button } = require('stremio/components');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
const { t } = require('i18next');
|
const { t } = require('i18next');
|
||||||
const { default: Stepper } = require('./Stepper');
|
const { default: Stepper } = require('./Stepper');
|
||||||
|
const { default: SubtitleVariant } = require('./SubtitleVariant');
|
||||||
|
|
||||||
const ORIGIN_PRIORITIES = [
|
const ORIGIN_PRIORITIES = [
|
||||||
'LOCAL',
|
'LOCAL',
|
||||||
|
|
@ -102,9 +103,8 @@ const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [allSubtitles, props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
}, [allSubtitles, props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
||||||
const subtitlesTrackOnClick = React.useCallback((event) => {
|
const subtitlesTrackOnSelect = React.useCallback((track) => {
|
||||||
const track = subtitlesTracksForLanguage.find((t) => t.id === event.currentTarget.dataset.id) ?? null;
|
if (track.embedded) {
|
||||||
if (track?.embedded) {
|
|
||||||
if (typeof props.onSubtitlesTrackSelected === 'function') {
|
if (typeof props.onSubtitlesTrackSelected === 'function') {
|
||||||
props.onSubtitlesTrackSelected(track);
|
props.onSubtitlesTrackSelected(track);
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +113,7 @@ const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => {
|
||||||
props.onExtraSubtitlesTrackSelected(track);
|
props.onExtraSubtitlesTrackSelected(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [subtitlesTracksForLanguage, props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
}, [props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
||||||
const onSubtitlesDelayChanged = React.useCallback((value) => {
|
const onSubtitlesDelayChanged = React.useCallback((value) => {
|
||||||
if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
||||||
if (props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay)) {
|
if (props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay)) {
|
||||||
|
|
@ -190,24 +190,12 @@ const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => {
|
||||||
subtitlesTracksForLanguage.length > 0 ?
|
subtitlesTracksForLanguage.length > 0 ?
|
||||||
<div className={styles['variants-list']}>
|
<div className={styles['variants-list']}>
|
||||||
{subtitlesTracksForLanguage.map((track, index) => (
|
{subtitlesTracksForLanguage.map((track, index) => (
|
||||||
<Button key={index} title={track.label} className={classnames(styles['variant-option'], { 'selected': props.selectedSubtitlesTrackId === track.id || props.selectedExtraSubtitlesTrackId === track.id })} data-id={track.id} data-origin={track.origin} onClick={subtitlesTrackOnClick}>
|
<SubtitleVariant
|
||||||
<div className={styles['info']}>
|
key={index}
|
||||||
<div className={styles['variant-label']}>
|
track={track}
|
||||||
{
|
selected={props.selectedSubtitlesTrackId === track.id || props.selectedExtraSubtitlesTrackId === track.id}
|
||||||
(track.label && track.label.length > 0 && !track.label.startsWith('http')) ? track.label : languages.label(track.lang)
|
onSelect={subtitlesTrackOnSelect}
|
||||||
}
|
/>
|
||||||
</div>
|
|
||||||
<div className={styles['variant-origin']}>
|
|
||||||
{ t(track.origin) }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
props.selectedSubtitlesTrackId === track.id || props.selectedExtraSubtitlesTrackId === track.id ?
|
|
||||||
<div className={styles['icon']} />
|
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
|
|
@ -276,7 +264,11 @@ SubtitlesMenu.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
lang: PropTypes.string.isRequired,
|
lang: PropTypes.string.isRequired,
|
||||||
origin: PropTypes.string.isRequired,
|
origin: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string.isRequired
|
label: PropTypes.string,
|
||||||
|
url: PropTypes.string,
|
||||||
|
embedded: PropTypes.bool,
|
||||||
|
local: PropTypes.bool,
|
||||||
|
exclusive: PropTypes.bool
|
||||||
})),
|
})),
|
||||||
selectedExtraSubtitlesTrackId: PropTypes.string,
|
selectedExtraSubtitlesTrackId: PropTypes.string,
|
||||||
extraSubtitlesOffset: PropTypes.number,
|
extraSubtitlesOffset: PropTypes.number,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
|
|
||||||
.language-option, .variant-option {
|
.language-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -40,13 +40,10 @@
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--overlay-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.language-label, .variant-label {
|
.language-label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
color: var(--primary-foreground-color);
|
color: var(--primary-foreground-color);
|
||||||
}
|
|
||||||
|
|
||||||
.language-label, .variant-label, .variant-origin {
|
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
@ -60,26 +57,6 @@
|
||||||
background-color: var(--secondary-accent-color);
|
background-color: var(--secondary-accent-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.variant-option {
|
|
||||||
height: 4rem;
|
|
||||||
|
|
||||||
.info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
|
|
||||||
.variant-label {
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.variant-origin {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--color-placeholder-text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ html:not(.active-slider-within) {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
box-shadow: 0 0 8rem 6rem @color-background-dark5;
|
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +101,6 @@ html:not(.active-slider-within) {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
box-shadow: 0 0 8rem 8rem @color-background-dark5;
|
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const useVideo = () => {
|
||||||
muted: null,
|
muted: null,
|
||||||
playbackSpeed: null,
|
playbackSpeed: null,
|
||||||
videoParams: null,
|
videoParams: null,
|
||||||
|
hdrInfo: null,
|
||||||
audioTracks: [],
|
audioTracks: [],
|
||||||
selectedAudioTrackId: null,
|
selectedAudioTrackId: null,
|
||||||
subtitlesTracks: [],
|
subtitlesTracks: [],
|
||||||
|
|
@ -142,6 +143,10 @@ const useVideo = () => {
|
||||||
setProp('extraSubtitlesOffset', offset);
|
setProp('extraSubtitlesOffset', offset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setVideoScale = (scale) => {
|
||||||
|
setProp('videoScale', scale);
|
||||||
|
};
|
||||||
|
|
||||||
const setSubtitlesTextColor = (color) => {
|
const setSubtitlesTextColor = (color) => {
|
||||||
setProp('subtitlesTextColor', color);
|
setProp('subtitlesTextColor', color);
|
||||||
setProp('extraSubtitlesTextColor', color);
|
setProp('extraSubtitlesTextColor', color);
|
||||||
|
|
@ -238,6 +243,7 @@ const useVideo = () => {
|
||||||
setSubtitlesBackgroundColor,
|
setSubtitlesBackgroundColor,
|
||||||
setSubtitlesOutlineColor,
|
setSubtitlesOutlineColor,
|
||||||
setExtraSubtitlesTrack,
|
setExtraSubtitlesTrack,
|
||||||
|
setVideoScale,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue