mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-18 21:12:13 +00:00
Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/ass-subtitles-styling-setting
This commit is contained in:
commit
55b86179ca
30 changed files with 275 additions and 229 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/pages_cleanup.yml
vendored
2
.github/workflows/pages_cleanup.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: gh-pages
|
||||
fetch-depth: 0
|
||||
|
|
|
|||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
|
|
@ -9,8 +9,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Install NPM dependencies
|
||||
uses: actions/checkout@v6
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Build
|
||||
env:
|
||||
|
|
@ -19,7 +24,7 @@ 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.11.2
|
||||
uses: svenstaro/upload-release-action@2.11.3
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: stremio-web.zip
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "stremio",
|
||||
"displayName": "Stremio",
|
||||
"version": "5.0.0-beta.27",
|
||||
"version": "5.0.0-beta.29",
|
||||
"author": "Smart Code OOD",
|
||||
"private": true,
|
||||
"license": "gpl-2.0",
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
"@babel/runtime": "7.26.0",
|
||||
"@sentry/browser": "8.42.0",
|
||||
"@stremio/stremio-colors": "5.2.0",
|
||||
"@stremio/stremio-core-web": "0.50.0",
|
||||
"@stremio/stremio-icons": "5.7.1",
|
||||
"@stremio/stremio-core-web": "0.51.1",
|
||||
"@stremio/stremio-icons": "5.8.0",
|
||||
"@stremio/stremio-video": "0.0.64",
|
||||
"a-color-picker": "1.2.1",
|
||||
"bowser": "2.11.0",
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ importers:
|
|||
specifier: 5.2.0
|
||||
version: 5.2.0
|
||||
'@stremio/stremio-core-web':
|
||||
specifier: 0.50.0
|
||||
version: 0.50.0
|
||||
specifier: 0.51.1
|
||||
version: 0.51.1
|
||||
'@stremio/stremio-icons':
|
||||
specifier: 5.7.1
|
||||
version: 5.7.1
|
||||
specifier: 5.8.0
|
||||
version: 5.8.0
|
||||
'@stremio/stremio-video':
|
||||
specifier: 0.0.64
|
||||
version: 0.0.64
|
||||
|
|
@ -1302,11 +1302,11 @@ packages:
|
|||
'@stremio/stremio-colors@5.2.0':
|
||||
resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==}
|
||||
|
||||
'@stremio/stremio-core-web@0.50.0':
|
||||
resolution: {integrity: sha512-SRE9nStgYNbhjJAw7mXfmM0wdnSLS4GMSJsSMTXvoGxnUgd+yisJUkN/9Sughe4t2IU7Uct8QWpdx9zFdlil+g==}
|
||||
'@stremio/stremio-core-web@0.51.1':
|
||||
resolution: {integrity: sha512-BD8i6zkDdMPeCyH50Bb7SB8r4nYx4eJwz4kLEJEl0PFjdr0gOmwHtEIgNa89ShJLNXUjPnpv4sVSNxFRG8fb5Q==}
|
||||
|
||||
'@stremio/stremio-icons@5.7.1':
|
||||
resolution: {integrity: sha512-Z96p36LLX3G+ewMnFKmNZVsO/AtcHA33WQ3wGOYFubxiYADPRAkcLVU5rHIfiGSC9IUaUVhxQWTPVB9ScY4Q5Q==}
|
||||
'@stremio/stremio-icons@5.8.0':
|
||||
resolution: {integrity: sha512-IVUvQbIWfA4YEHCTed7v/sdQJCJ+OOCf84LTWpkE2W6GLQ+15WHcMEJrVkE1X3ekYJnGg3GjT0KLO6tKSU0P4w==}
|
||||
|
||||
'@stremio/stremio-video@0.0.64':
|
||||
resolution: {integrity: sha512-29w/lwU8BB6ai8LUyCnpRc2F9kPf7cpys40NCobt70MqBP/UqvYISsrnD/ijoBwvtpKdZ6ptv5h9BbDj6rrerw==}
|
||||
|
|
@ -6561,11 +6561,11 @@ snapshots:
|
|||
|
||||
'@stremio/stremio-colors@5.2.0': {}
|
||||
|
||||
'@stremio/stremio-core-web@0.50.0':
|
||||
'@stremio/stremio-core-web@0.51.1':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.1
|
||||
|
||||
'@stremio/stremio-icons@5.7.1': {}
|
||||
'@stremio/stremio-icons@5.8.0': {}
|
||||
|
||||
'@stremio/stremio-video@0.0.64':
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
--color-x: #000000;
|
||||
--color-reddit: #FF4500;
|
||||
--color-imdb: #f5c518;
|
||||
--color-trakt: #ED2224;
|
||||
--color-trakt: rgb(255, 255, 255);
|
||||
--color-placeholder: #60606080;
|
||||
--color-placeholder-text: @color-surface-50;
|
||||
--color-placeholder-background: @color-surface-dark5-20;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React, { createContext, useCallback, useContext, useEffect } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import shortcuts from './shortcuts.json';
|
||||
|
||||
const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat();
|
||||
|
||||
export type ShortcutName = string;
|
||||
export type ShortcutListener = () => void;
|
||||
export type ShortcutListener = (combo: number) => void;
|
||||
|
||||
interface ShortcutsContext {
|
||||
grouped: ShortcutGroup[],
|
||||
on: (name: ShortcutName, listener: ShortcutListener) => void,
|
||||
off: (name: ShortcutName, listener: ShortcutListener) => void,
|
||||
}
|
||||
|
||||
const ShortcutsContext = createContext<ShortcutsContext>({} as ShortcutsContext);
|
||||
|
|
@ -18,27 +20,38 @@ type Props = {
|
|||
};
|
||||
|
||||
const ShortcutsProvider = ({ children, onShortcut }: Props) => {
|
||||
const onKeyDown = useCallback(({ ctrlKey, shiftKey, key }: KeyboardEvent) => {
|
||||
const listeners = useRef<Map<ShortcutName, Set<ShortcutListener>>>(new Map());
|
||||
|
||||
const onKeyDown = useCallback(({ ctrlKey, shiftKey, code, key }: KeyboardEvent) => {
|
||||
SHORTCUTS.forEach(({ name, combos }) => combos.forEach((keys) => {
|
||||
const modifers = (keys.includes('Ctrl') ? ctrlKey : true)
|
||||
&& (keys.includes('Shift') ? shiftKey : true);
|
||||
|
||||
if (modifers && keys.includes(key.toUpperCase())) {
|
||||
if (modifers && (keys.includes(code) || keys.includes(key.toUpperCase()))) {
|
||||
const combo = combos.indexOf(keys);
|
||||
listeners.current.get(name)?.forEach((listener) => listener(combo));
|
||||
|
||||
onShortcut(name as ShortcutName);
|
||||
}
|
||||
}));
|
||||
}, [onShortcut]);
|
||||
|
||||
const on = (name: ShortcutName, listener: ShortcutListener) => {
|
||||
!listeners.current.has(name) && listeners.current.set(name, new Set());
|
||||
listeners.current.get(name)!.add(listener);
|
||||
};
|
||||
|
||||
const off = (name: ShortcutName, listener: ShortcutListener) => {
|
||||
listeners.current.get(name)?.delete(listener);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
return () => document.removeEventListener('keydown', onKeyDown);
|
||||
}, [onKeyDown]);
|
||||
|
||||
return (
|
||||
<ShortcutsContext.Provider value={{ grouped: shortcuts }}>
|
||||
<ShortcutsContext.Provider value={{ grouped: shortcuts, on, off }}>
|
||||
{children}
|
||||
</ShortcutsContext.Provider>
|
||||
);
|
||||
|
|
@ -50,5 +63,5 @@ const useShortcuts = () => {
|
|||
|
||||
export {
|
||||
ShortcutsProvider,
|
||||
useShortcuts
|
||||
useShortcuts,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { ShortcutsProvider, useShortcuts } from './Shortcuts';
|
||||
import onShortcut from './onShortcut';
|
||||
|
||||
export {
|
||||
ShortcutsProvider,
|
||||
useShortcuts,
|
||||
onShortcut,
|
||||
};
|
||||
|
|
|
|||
15
src/common/Shortcuts/onShortcut.ts
Normal file
15
src/common/Shortcuts/onShortcut.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { DependencyList, useCallback, useEffect } from 'react';
|
||||
import { ShortcutListener, ShortcutName, useShortcuts } from './Shortcuts';
|
||||
|
||||
const onShortcut = (name: ShortcutName, listener: ShortcutListener, deps: DependencyList) => {
|
||||
const shortcuts = useShortcuts();
|
||||
|
||||
const listenerCallback = useCallback(listener, deps);
|
||||
|
||||
useEffect(() => {
|
||||
shortcuts.on(name, listenerCallback);
|
||||
return () => shortcuts.off(name, listenerCallback);
|
||||
}, [listenerCallback]);
|
||||
};
|
||||
|
||||
export default onShortcut;
|
||||
|
|
@ -59,6 +59,11 @@
|
|||
"label": "SETTINGS_SHORTCUT_VOLUME_DOWN",
|
||||
"combos": [["ArrowDown"]]
|
||||
},
|
||||
{
|
||||
"name": "mute",
|
||||
"label": "SETTINGS_SHORTCUT_MUTE",
|
||||
"combos": [["M"]]
|
||||
},
|
||||
{
|
||||
"name": "subtitlesSize",
|
||||
"label": "SETTINGS_SHORTCUT_SUBTITLES_SIZE",
|
||||
|
|
@ -83,6 +88,16 @@
|
|||
"name": "infoMenu",
|
||||
"label": "SETTINGS_SHORTCUT_MENU_INFO",
|
||||
"combos": [["I"]]
|
||||
},
|
||||
{
|
||||
"name": "speedMenu",
|
||||
"label": "SETTINGS_SHORTCUT_MENU_PLAYBACK_SPEED",
|
||||
"combos": [["R"]]
|
||||
},
|
||||
{
|
||||
"name": "statisticsMenu",
|
||||
"label": "SETTINGS_SHORTCUT_MENU_STATISTICS",
|
||||
"combos": [["D"]]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
&.error {
|
||||
.icon-container {
|
||||
.icon {
|
||||
color: var(--color-trakt);
|
||||
color: var(--danger-accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const { FileDropProvider, onFileDrop } = require('./FileDrop');
|
|||
const { PlatformProvider, usePlatform } = require('./Platform');
|
||||
const { ToastProvider, useToast } = require('./Toast');
|
||||
const { TooltipProvider, Tooltip } = require('./Tooltips');
|
||||
const { ShortcutsProvider, useShortcuts } = require('./Shortcuts');
|
||||
const { ShortcutsProvider, useShortcuts, onShortcut } = require('./Shortcuts');
|
||||
const comparatorWithPriorities = require('./comparatorWithPriorities');
|
||||
const CONSTANTS = require('./CONSTANTS');
|
||||
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
|
||||
|
|
@ -38,6 +38,7 @@ module.exports = {
|
|||
usePlatform,
|
||||
ShortcutsProvider,
|
||||
useShortcuts,
|
||||
onShortcut,
|
||||
ToastProvider,
|
||||
useToast,
|
||||
TooltipProvider,
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--color-trakt);
|
||||
border-color: var(--danger-accent-color);
|
||||
}
|
||||
|
||||
&.checked {
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const { Button } = require('stremio/components');
|
||||
const useTranslate = require('stremio/common/useTranslate');
|
||||
const styles = require('./styles');
|
||||
|
||||
const MetaLinks = ({ className, label, links }) => {
|
||||
const { t } = useTranslation();
|
||||
const { string, stringWithPrefix } = useTranslate();
|
||||
return (
|
||||
<div className={classnames(className, styles['meta-links-container'])}>
|
||||
{
|
||||
typeof label === 'string' && label.length > 0 ?
|
||||
<div className={styles['label-container']}>
|
||||
{t(`LINKS_${label.toUpperCase()}`)}
|
||||
{ stringWithPrefix(label.toUpperCase(), 'LINKS') }
|
||||
</div>
|
||||
:
|
||||
null
|
||||
|
|
@ -24,7 +24,7 @@ const MetaLinks = ({ className, label, links }) => {
|
|||
<div className={styles['links-container']}>
|
||||
{links.map(({ label, href }, index) => (
|
||||
<Button key={index} className={styles['link-container']} title={label} href={href}>
|
||||
{ t(label) }
|
||||
{ string(label) }
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
border-radius: 2rem;
|
||||
height: @height;
|
||||
width: fit-content;
|
||||
backdrop-filter: blur(5px);
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--color-trakt);
|
||||
border-color: var(--danger-accent-color);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.shortcuts-group {
|
||||
flex: 1 1 0;
|
||||
position: relative;
|
||||
min-width: 30rem;
|
||||
width: 30rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
|
|
|
|||
|
|
@ -142,11 +142,11 @@ const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabl
|
|||
<div className={styles['layer']}>
|
||||
<div
|
||||
className={classnames(styles['track-after'], { [styles['audio-boost']]: audioBoost })}
|
||||
style={{ '--mask-width': `calc(${thumbPosition} * 100%)` }}
|
||||
style={{ '--mask-width': `calc(${thumbPosition.toFixed(3)} * 100%)` }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['layer']}>
|
||||
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} />
|
||||
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition.toFixed(3)})` }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -73,15 +73,19 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
|
|||
const blurThumbnail = profile.settings.hideSpoilers && season && episode && !watched;
|
||||
|
||||
React.useEffect(() => {
|
||||
selected && !watched && ref.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'start'
|
||||
});
|
||||
if (selected && ref.current) {
|
||||
if ((progress && watched) || !watched) {
|
||||
ref.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'start'
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
return (
|
||||
<Button {...props} ref={ref} className={classnames(className, styles['video-container'])} title={title}>
|
||||
<Button {...props} ref={ref} className={classnames(className, styles['video-container'], { [styles['selected']]: selected })} title={title}>
|
||||
{
|
||||
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
||||
<div className={styles['thumbnail-container']}>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@
|
|||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
border: 0.15rem solid transparent;
|
||||
|
||||
@supports (scroll-margin: 1.25rem) {
|
||||
scroll-margin: 1.25rem;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
|
|
@ -172,6 +177,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
animation: border 3s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes border {
|
||||
0% {
|
||||
border: 0.15rem solid var(--primary-accent-color);
|
||||
}
|
||||
100% {
|
||||
border: 0.15rem solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.context-menu-container {
|
||||
max-width: calc(90% - 1.5rem);
|
||||
z-index: 2;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => {
|
|||
remoteAddons.selected !== null ?
|
||||
t.stringWithPrefix(remoteAddons.selected.request.path.type, 'TYPE_')
|
||||
:
|
||||
typeSelect.title;
|
||||
t.string('SELECT_TYPE');
|
||||
},
|
||||
onSelect: (value) => {
|
||||
window.location = value;
|
||||
|
|
|
|||
|
|
@ -22,11 +22,10 @@ const Board = () => {
|
|||
const profile = useProfile();
|
||||
const boardCatalogsOffset = continueWatchingPreview.items.length > 0 ? 1 : 0;
|
||||
const scrollContainerRef = React.useRef();
|
||||
const streamingServerWarningDismissed = React.useMemo(() => {
|
||||
return streamingServer.settings !== null && streamingServer.settings.type === 'Ready' || (
|
||||
!isNaN(profile.settings.streamingServerWarningDismissed.getTime()) &&
|
||||
profile.settings.streamingServerWarningDismissed.getTime() > Date.now()
|
||||
);
|
||||
const showStreamingServerWarning = React.useMemo(() => {
|
||||
return streamingServer.settings !== null && streamingServer.settings.type === 'Err' && (
|
||||
isNaN(profile.settings.streamingServerWarningDismissed.getTime()) ||
|
||||
profile.settings.streamingServerWarningDismissed.getTime() < Date.now());
|
||||
}, [profile.settings, streamingServer.settings]);
|
||||
const onVisibleRangeChange = React.useCallback(() => {
|
||||
const range = getVisibleChildrenRange(scrollContainerRef.current);
|
||||
|
|
@ -103,7 +102,7 @@ const Board = () => {
|
|||
</div>
|
||||
</MainNavBars>
|
||||
{
|
||||
!streamingServerWarningDismissed ?
|
||||
showStreamingServerWarning ?
|
||||
<StreamingServerWarning className={styles['board-warning-container']} />
|
||||
:
|
||||
null
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ const OptionsMenu = ({ className, stream, playbackDevices, extraSubtitlesTracks,
|
|||
}
|
||||
}, [streamingUrl, downloadUrl]);
|
||||
const onDownloadVideoButtonClick = React.useCallback(() => {
|
||||
if (streamingUrl || downloadUrl) {
|
||||
platform.openExternal(streamingUrl || downloadUrl);
|
||||
if (downloadUrl || streamingUrl ) {
|
||||
platform.openExternal(downloadUrl || streamingUrl);
|
||||
}
|
||||
}, [streamingUrl, downloadUrl]);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const langs = require('langs');
|
|||
const { useTranslation } = require('react-i18next');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { onFileDrop, useSettings, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform } = require('stremio/common');
|
||||
const { onFileDrop, useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform, onShortcut } = require('stremio/common');
|
||||
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
|
||||
const BufferingLoader = require('./BufferingLoader');
|
||||
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
|
||||
|
|
@ -36,7 +36,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const forceTranscoding = React.useMemo(() => {
|
||||
return queryParams.has('forceTranscoding');
|
||||
}, [queryParams]);
|
||||
|
||||
const profile = useProfile();
|
||||
const [player, videoParamsChanged, timeChanged, seek, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
|
||||
const [settings, updateSettings] = useSettings();
|
||||
const streamingServer = useStreamingServer();
|
||||
|
|
@ -105,13 +105,27 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
|
||||
}, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]);
|
||||
|
||||
const handleNextVideoNavigation = React.useCallback((deepLinks) => {
|
||||
if (deepLinks.player) {
|
||||
isNavigating.current = true;
|
||||
window.location.replace(deepLinks.player);
|
||||
} else if (deepLinks.metaDetailsStreams) {
|
||||
isNavigating.current = true;
|
||||
window.location.replace(deepLinks.metaDetailsStreams);
|
||||
const handleNextVideoNavigation = React.useCallback((deepLinks, bingeWatching, ended) => {
|
||||
if (ended) {
|
||||
if (bingeWatching) {
|
||||
if (deepLinks.player) {
|
||||
isNavigating.current = true;
|
||||
window.location.replace(deepLinks.player);
|
||||
} else if (deepLinks.metaDetailsStreams) {
|
||||
isNavigating.current = true;
|
||||
window.location.replace(deepLinks.metaDetailsStreams);
|
||||
}
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
} else {
|
||||
if (deepLinks.player) {
|
||||
isNavigating.current = true;
|
||||
window.location.replace(deepLinks.player);
|
||||
} else if (deepLinks.metaDetailsStreams) {
|
||||
isNavigating.current = true;
|
||||
window.location.replace(deepLinks.metaDetailsStreams);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -127,7 +141,8 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
nextVideo();
|
||||
|
||||
const deepLinks = window.playerNextVideo.deepLinks;
|
||||
handleNextVideoNavigation(deepLinks);
|
||||
handleNextVideoNavigation(deepLinks, profile.settings.bingeWatching, true);
|
||||
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
|
|
@ -257,9 +272,9 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
nextVideo();
|
||||
|
||||
const deepLinks = player.nextVideo.deepLinks;
|
||||
handleNextVideoNavigation(deepLinks);
|
||||
handleNextVideoNavigation(deepLinks, profile.settings.bingeWatching, false);
|
||||
}
|
||||
}, [player.nextVideo, handleNextVideoNavigation]);
|
||||
}, [player.nextVideo, handleNextVideoNavigation, profile.settings]);
|
||||
|
||||
const onVideoClick = React.useCallback(() => {
|
||||
if (video.state.paused !== null) {
|
||||
|
|
@ -323,10 +338,10 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
setError(null);
|
||||
video.unload();
|
||||
|
||||
if (player.selected && streamingServer.settings?.type !== 'Loading') {
|
||||
if (player.selected && player.stream?.type === 'Ready' && streamingServer.settings?.type !== 'Loading') {
|
||||
video.load({
|
||||
stream: {
|
||||
...player.selected.stream,
|
||||
...player.stream.content,
|
||||
subtitles: Array.isArray(player.selected.stream.subtitles) ?
|
||||
player.selected.stream.subtitles.map((subtitles) => ({
|
||||
...subtitles,
|
||||
|
|
@ -362,7 +377,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
shellTransport: services.shell.active ? services.shell.transport : null,
|
||||
});
|
||||
}
|
||||
}, [streamingServer.baseUrl, player.selected, forceTranscoding, casting]);
|
||||
}, [streamingServer.baseUrl, player.selected, player.stream, forceTranscoding, casting]);
|
||||
React.useEffect(() => {
|
||||
if (video.state.stream !== null) {
|
||||
const tracks = player.subtitles.map((subtitles) => ({
|
||||
|
|
@ -413,7 +428,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}, [video.state.videoParams]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!!settings.bingeWatching && player.nextVideo !== null && !nextVideoPopupDismissed.current) {
|
||||
if (player.nextVideo !== null && !nextVideoPopupDismissed.current) {
|
||||
if (video.state.time !== null && video.state.duration !== null && video.state.time < video.state.duration && (video.state.duration - video.state.time) <= settings.nextVideoNotificationDuration) {
|
||||
openNextVideoPopup();
|
||||
} else {
|
||||
|
|
@ -554,7 +569,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
const videoId = player.selected ? player.selected?.streamRequest?.path?.id : null;
|
||||
const video = metaItem ? metaItem.videos.find(({ id }) => id === videoId) : null;
|
||||
|
||||
const videoInfo = video && video.season && video.episode ? ` (${video.season}x${video.episode})`: null;
|
||||
const videoInfo = video && video.season && video.episode ? ` (${video.season}x${video.episode})` : null;
|
||||
const videoTitle = video ? `${video.title}${videoInfo}` : null;
|
||||
const metaTitle = metaItem ? metaItem.name : null;
|
||||
const imageUrl = metaItem ? metaItem.logo : null;
|
||||
|
|
@ -583,117 +598,99 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
navigator.mediaSession.setActionHandler('nexttrack', nexVideoCallback);
|
||||
}, [player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const onKeyDown = (event) => {
|
||||
switch (event.code) {
|
||||
case 'Space': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
|
||||
if (video.state.paused) {
|
||||
onPlayRequested();
|
||||
setSeeking(false);
|
||||
} else {
|
||||
onPauseRequested();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
|
||||
const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
||||
setSeeking(true);
|
||||
onSeekRequested(video.state.time + seekDuration);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
|
||||
const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
||||
setSeeking(true);
|
||||
onSeekRequested(video.state.time - seekDuration);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
||||
onVolumeChangeRequested(Math.min(video.state.volume + 5, 200));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
||||
onVolumeChangeRequested(Math.max(video.state.volume - 5, 0));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyS': {
|
||||
closeMenus();
|
||||
if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) ||
|
||||
(Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0)) {
|
||||
toggleSubtitlesMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyA': {
|
||||
closeMenus();
|
||||
if (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0) {
|
||||
toggleAudioMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyI': {
|
||||
closeMenus();
|
||||
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
|
||||
toggleSideDrawer();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyR': {
|
||||
closeMenus();
|
||||
if (video.state.playbackSpeed !== null) {
|
||||
toggleSpeedMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyD': {
|
||||
closeMenus();
|
||||
if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
|
||||
toggleStatisticsMenu();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'KeyG': {
|
||||
onDecreaseSubtitlesDelay();
|
||||
break;
|
||||
}
|
||||
case 'KeyH': {
|
||||
onIncreaseSubtitlesDelay();
|
||||
break;
|
||||
}
|
||||
case 'Minus': {
|
||||
onUpdateSubtitlesSize(-1);
|
||||
break;
|
||||
}
|
||||
case 'Equal': {
|
||||
onUpdateSubtitlesSize(1);
|
||||
break;
|
||||
}
|
||||
case 'Escape': {
|
||||
closeMenus();
|
||||
!settings.escExitFullscreen && window.history.back();
|
||||
break;
|
||||
}
|
||||
onShortcut('playPause', () => {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
|
||||
if (video.state.paused) {
|
||||
onPlayRequested();
|
||||
setSeeking(false);
|
||||
} else {
|
||||
onPauseRequested();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [menusOpen, nextVideoPopupOpen, video.state.paused, onPlayRequested, onPauseRequested]);
|
||||
|
||||
onShortcut('seekForward', (combo) => {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
|
||||
const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
||||
setSeeking(true);
|
||||
onSeekRequested(video.state.time + seekDuration);
|
||||
}
|
||||
}, [menusOpen, nextVideoPopupOpen, video.state.time, onSeekRequested]);
|
||||
|
||||
onShortcut('seekBackward', (combo) => {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
|
||||
const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
||||
setSeeking(true);
|
||||
onSeekRequested(video.state.time - seekDuration);
|
||||
}
|
||||
}, [menusOpen, nextVideoPopupOpen, video.state.time, onSeekRequested]);
|
||||
|
||||
onShortcut('mute', () => {
|
||||
video.state.muted === true ? onUnmuteRequested() : onMuteRequested();
|
||||
}, [video.state.muted]);
|
||||
|
||||
onShortcut('volumeUp', () => {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
||||
onVolumeChangeRequested(Math.min(video.state.volume + 5, 200));
|
||||
}
|
||||
}, [menusOpen, nextVideoPopupOpen, video.state.volume]);
|
||||
|
||||
onShortcut('volumeDown', () => {
|
||||
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
||||
onVolumeChangeRequested(Math.min(video.state.volume - 5, 200));
|
||||
}
|
||||
}, [menusOpen, nextVideoPopupOpen, video.state.volume]);
|
||||
|
||||
onShortcut('subtitlesDelay', (combo) => {
|
||||
combo === 1 ? onIncreaseSubtitlesDelay() : onDecreaseSubtitlesDelay();
|
||||
}, [onIncreaseSubtitlesDelay, onDecreaseSubtitlesDelay]);
|
||||
|
||||
onShortcut('subtitlesSize', (combo) => {
|
||||
combo === 1 ? onUpdateSubtitlesSize(-1) : onUpdateSubtitlesSize(1);
|
||||
}, [onUpdateSubtitlesSize, onUpdateSubtitlesSize]);
|
||||
|
||||
onShortcut('subtitlesMenu', () => {
|
||||
closeMenus();
|
||||
if (video.state?.subtitlesTracks?.length > 0 || video.state?.extraSubtitlesTracks?.length > 0) {
|
||||
toggleSubtitlesMenu();
|
||||
}
|
||||
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, toggleSubtitlesMenu]);
|
||||
|
||||
onShortcut('audioMenu', () => {
|
||||
closeMenus();
|
||||
if (video.state?.audioTracks?.length > 0) {
|
||||
toggleAudioMenu();
|
||||
}
|
||||
}, [video.state.audioTracks, toggleAudioMenu]);
|
||||
|
||||
onShortcut('infoMenu', () => {
|
||||
closeMenus();
|
||||
if (player.metaItem?.type === 'Ready') {
|
||||
toggleSideDrawer();
|
||||
}
|
||||
}, [player.metaItem, toggleSideDrawer]);
|
||||
|
||||
onShortcut('speedMenu', () => {
|
||||
closeMenus();
|
||||
if (video.state.playbackSpeed !== null) {
|
||||
toggleSpeedMenu();
|
||||
}
|
||||
}, [video.state.playbackSpeed, toggleSpeedMenu]);
|
||||
|
||||
onShortcut('statisticsMenu', () => {
|
||||
closeMenus();
|
||||
const stream = player.selected?.stream;
|
||||
if (streamingServer?.statistics?.type !== 'Err' && typeof stream === 'string' && typeof stream === 'number') {
|
||||
toggleStatisticsMenu();
|
||||
}
|
||||
}, [player.selected, streamingServer.statistics, toggleStatisticsMenu]);
|
||||
|
||||
onShortcut('exit', () => {
|
||||
closeMenus();
|
||||
!settings.escExitFullscreen && window.history.back();
|
||||
}, [settings.escExitFullscreen]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const onKeyUp = (event) => {
|
||||
if (event.code === 'ArrowRight' || event.code === 'ArrowLeft') {
|
||||
setSeeking(false);
|
||||
|
|
@ -711,39 +708,14 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
};
|
||||
if (routeFocused) {
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
window.addEventListener('wheel', onWheel);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKeyDown);
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
window.removeEventListener('wheel', onWheel);
|
||||
};
|
||||
}, [
|
||||
player.metaItem,
|
||||
player.selected,
|
||||
streamingServer.statistics,
|
||||
settings.seekTimeDuration,
|
||||
settings.seekShortTimeDuration,
|
||||
settings.escExitFullscreen,
|
||||
routeFocused,
|
||||
menusOpen,
|
||||
nextVideoPopupOpen,
|
||||
video.state.paused,
|
||||
video.state.time,
|
||||
video.state.volume,
|
||||
video.state.audioTracks,
|
||||
video.state.subtitlesTracks,
|
||||
video.state.extraSubtitlesTracks,
|
||||
video.state.playbackSpeed,
|
||||
toggleSubtitlesMenu,
|
||||
toggleStatisticsMenu,
|
||||
toggleSideDrawer,
|
||||
onDecreaseSubtitlesDelay,
|
||||
onIncreaseSubtitlesDelay,
|
||||
onUpdateSubtitlesSize,
|
||||
]);
|
||||
}, [routeFocused, menusOpen, video.state.volume]);
|
||||
|
||||
React.useEffect(() => {
|
||||
video.events.on('error', onError);
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ const useStatistics = (player, streamingServer) => {
|
|||
const { core } = useServices();
|
||||
|
||||
const stream = React.useMemo(() => {
|
||||
return player.selected?.stream ?
|
||||
player.selected.stream
|
||||
:
|
||||
null;
|
||||
}, [player.selected]);
|
||||
if (player.stream?.type === 'Ready') {
|
||||
return player.stream.content;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [player.stream]);
|
||||
|
||||
const infoHash = React.useMemo(() => {
|
||||
return stream?.infoHash ?
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
|
|||
<Option label={'SETTINGS_NEXT_VIDEO_POPUP_DURATION'}>
|
||||
<MultiselectMenu
|
||||
className={'multiselect'}
|
||||
disabled={!profile.settings.bingeWatching}
|
||||
{...nextVideoPopupDurationSelect}
|
||||
/>
|
||||
</Option>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from '
|
|||
import classnames from 'classnames';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { useRouteFocused } from 'stremio-router';
|
||||
import { useProfile, useStreamingServer, withCoreSuspender } from 'stremio/common';
|
||||
import { usePlatform, useProfile, useStreamingServer, withCoreSuspender } from 'stremio/common';
|
||||
import { MainNavBars } from 'stremio/components';
|
||||
import { SECTIONS } from './constants';
|
||||
import Menu from './Menu';
|
||||
|
|
@ -18,6 +18,7 @@ import styles from './Settings.less';
|
|||
const Settings = () => {
|
||||
const { routeFocused } = useRouteFocused();
|
||||
const profile = useProfile();
|
||||
const platform = usePlatform();
|
||||
const streamingServer = useStreamingServer();
|
||||
|
||||
const sectionsContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -37,14 +38,10 @@ const Settings = () => {
|
|||
|
||||
const updateSelectedSectionId = useCallback(() => {
|
||||
const container = sectionsContainerRef.current;
|
||||
if (container!.scrollTop + container!.clientHeight >= container!.scrollHeight - 50) {
|
||||
setSelectedSectionId(sections[sections.length - 1].id);
|
||||
} else {
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
if (sections[i].ref.current!.offsetTop - container!.offsetTop <= container!.scrollTop) {
|
||||
setSelectedSectionId(sections[i].id);
|
||||
break;
|
||||
}
|
||||
for (const section of sections) {
|
||||
const sectionContainer = section.ref.current;
|
||||
if (sectionContainer && (sectionContainer.offsetTop + container!.offsetTop) < container!.scrollTop + 50) {
|
||||
setSelectedSectionId(section.id);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
|
@ -94,7 +91,9 @@ const Settings = () => {
|
|||
profile={profile}
|
||||
streamingServer={streamingServer}
|
||||
/>
|
||||
<Shortcuts ref={shortcutsSectionRef} />
|
||||
{
|
||||
!platform.isMobile && <Shortcuts ref={shortcutsSectionRef} />
|
||||
}
|
||||
<Info streamingServer={streamingServer} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
.cancel {
|
||||
&:hover {
|
||||
.icon {
|
||||
color: var(--color-trakt);
|
||||
color: var(--danger-accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
|
||||
&.error {
|
||||
background-color: var(--color-trakt);
|
||||
background-color: var(--danger-accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
background-color: var(--overlay-color);
|
||||
|
||||
.icon {
|
||||
color: var(--color-trakt);
|
||||
color: var(--danger-accent-color);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@
|
|||
gap: 0.75rem;
|
||||
|
||||
.icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue