Merge remote-tracking branch 'origin/development' into pr-1260

# Conflicts:
#	src/common/Fullscreen/FullscreenProvider.tsx
This commit is contained in:
Timothy Z. 2026-05-09 22:43:39 +03:00
commit 86d716187a
16 changed files with 102 additions and 188 deletions

View file

@ -17,7 +17,7 @@
"@babel/runtime": "7.29.2",
"@sentry/browser": "8.42.0",
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.56.4",
"@stremio/stremio-core-web": "0.57.0",
"@stremio/stremio-icons": "5.10.0",
"@stremio/stremio-video": "0.0.79",
"a-color-picker": "1.2.1",
@ -41,7 +41,7 @@
"react-i18next": "^15.7.4",
"react-is": "18.3.1",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "github:Stremio/stremio-translations#d9cd2fb88268b365b14101452665de698f9c15e9",
"stremio-translations": "github:Stremio/stremio-translations#c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0",
"url": "0.11.4",
"use-long-press": "^3.3.0"
},

View file

@ -18,8 +18,8 @@ importers:
specifier: 5.2.0
version: 5.2.0
'@stremio/stremio-core-web':
specifier: 0.56.4
version: 0.56.4
specifier: 0.57.0
version: 0.57.0
'@stremio/stremio-icons':
specifier: 5.10.0
version: 5.10.0
@ -90,8 +90,8 @@ importers:
specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6
version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6
stremio-translations:
specifier: github:Stremio/stremio-translations#d9cd2fb88268b365b14101452665de698f9c15e9
version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9
specifier: github:Stremio/stremio-translations#c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0
version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0
url:
specifier: 0.11.4
version: 0.11.4
@ -123,12 +123,12 @@ importers:
'@types/lodash.throttle':
specifier: ^4.1.9
version: 4.1.9
'@types/node':
specifier: ^25.6.0
version: 25.6.0
'@types/magnet-uri':
specifier: ^5.1.5
version: 5.1.5
'@types/node':
specifier: ^25.6.0
version: 25.6.0
'@types/react':
specifier: ^18.3.28
version: 18.3.28
@ -1394,8 +1394,8 @@ packages:
'@stremio/stremio-colors@5.2.0':
resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==}
'@stremio/stremio-core-web@0.56.4':
resolution: {integrity: sha512-tFAMYgKrJ1bkvHRMpxDykM/844sDjgRPFk6FLhjQiwh01OHIyEgDqGo/NgwFM+CuMR4mW676SDvwNHkK0Xqg3w==}
'@stremio/stremio-core-web@0.57.0':
resolution: {integrity: sha512-go8GZwGm6MFfjez6J/T1HrGNY2330EU3VoVinDYR0rE331aay6fenViLyCYLE829FTebW2eglrmc7MdHjmhqSA==}
'@stremio/stremio-icons@5.10.0':
resolution: {integrity: sha512-Zw/vGC3D2yeQfk8xv/tfMJTDvbCPOI91tBg4XpR2+EgbZSX8Xvm7Vz457PIhFPhTAwdOPHp0VX0M3gzjbt0zOg==}
@ -1497,9 +1497,6 @@ packages:
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/node-forge@1.3.14':
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
'@types/node@25.6.0':
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
@ -4424,8 +4421,8 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9:
resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9}
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0:
resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0}
version: 1.52.0
string-length@4.0.2:
@ -6447,7 +6444,7 @@ snapshots:
'@stremio/stremio-colors@5.2.0': {}
'@stremio/stremio-core-web@0.56.4':
'@stremio/stremio-core-web@0.57.0':
dependencies:
'@babel/runtime': 7.24.1
@ -10003,7 +10000,7 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/d9cd2fb88268b365b14101452665de698f9c15e9: {}
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c2d68dc590ac7d56f0df5e69a2144ba83e0d5ef0: {}
string-length@4.0.2:
dependencies:

View file

@ -5,7 +5,7 @@ const React = require('react');
const { useTranslation } = require('react-i18next');
const { useCore } = require('stremio/core');
const { Router } = require('stremio-router');
const { Shell, Chromecast, KeyboardShortcuts, ServicesProvider, GamepadProvider } = require('stremio/services');
const { Shell, Chromecast, ServicesProvider, GamepadProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
const { FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender, onFileDrop } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
@ -33,13 +33,12 @@ const App = () => {
return {
shell: new Shell(),
chromecast: new Chromecast(),
keyboardShortcuts: new KeyboardShortcuts(),
};
}, []);
const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false);
const [gamepadModalOpen,, closeGamepadModal, toggleGamepadModal] = useBinaryState(false);
const onShortcut = React.useCallback((name) => {
const onShortcut = React.useCallback((name, combo, key) => {
switch (name) {
case 'shortcuts':
toggleShortcutModal();
@ -47,6 +46,18 @@ const App = () => {
case 'gamepadGuide':
toggleGamepadModal();
break;
case 'navigateSearch':
window.location = '#/search';
break;
case 'navigateTabs': {
const routes = ['', 'discover', 'library', 'calendar', 'addons', 'settings'];
const index = key - 1;
if (index in routes) window.location = `#/${routes[index]}`;
break;
}
case 'navigateHistory':
combo === 0 ? window.history.back() : window.history.forward();
break;
}
}, [toggleShortcutModal, toggleGamepadModal]);
@ -90,12 +101,10 @@ const App = () => {
services.chromecast.on('stateChanged', onChromecastStateChange);
services.shell.start();
services.chromecast.start();
services.keyboardShortcuts.start();
window.services = services;
return () => {
services.shell.stop();
services.chromecast.stop();
services.keyboardShortcuts.stop();
services.chromecast.off('stateChanged', onChromecastStateChange);
};
}, []);

View file

@ -11,16 +11,6 @@ type Props = {
children: React.ReactNode,
};
const isTextInputFocused = () => {
const activeElement = document.activeElement;
return activeElement instanceof HTMLElement &&
(activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.tagName === 'SELECT' ||
activeElement.isContentEditable);
};
const hasWebkitFullscreen = typeof HTMLVideoElement !== 'undefined' &&
typeof HTMLVideoElement.prototype.webkitEnterFullscreen === 'function';
@ -48,7 +38,11 @@ const FullscreenProvider = ({ children }: Props) => {
if (shell.active) {
shell.send('win-set-visibility', { fullscreen: true });
} else if (document.fullscreenEnabled) {
await document.documentElement.requestFullscreen();
try {
await document.documentElement.requestFullscreen();
} catch (err) {
console.error('Error enabling fullscreen', err);
}
} else if (videoElementRef.current && hasWebkitFullscreen) {
(videoElementRef.current as any).webkitEnterFullscreen();
}
@ -68,12 +62,7 @@ const FullscreenProvider = ({ children }: Props) => {
fullscreen ? exitFullscreen() : requestFullscreen();
}, [fullscreen, exitFullscreen, requestFullscreen]);
const toggleFullscreenFromShortcut = useCallback(() => {
if (isTextInputFocused()) return;
toggleFullscreen();
}, [toggleFullscreen]);
onShortcut('fullscreen', toggleFullscreenFromShortcut, [toggleFullscreenFromShortcut]);
onShortcut('fullscreen', toggleFullscreen, [toggleFullscreen]);
useEffect(() => {
const videoElement = videoElementRef.current;

View file

@ -16,16 +16,26 @@ const ShortcutsContext = createContext<ShortcutsContext>({} as ShortcutsContext)
type Props = {
children: JSX.Element,
onShortcut: (name: ShortcutName) => void,
onShortcut: (name: ShortcutName, combo: number, key: string) => void,
};
const REPEAT_THROTTLE_MS = 130;
const isInputFocused = () => {
const inputElements = ['INPUT', 'TEXTAREA', 'SELECT'];
const activeElement = document.activeElement;
return activeElement instanceof HTMLElement &&
(inputElements.includes(activeElement.tagName) || activeElement.isContentEditable);
};
const ShortcutsProvider = ({ children, onShortcut }: Props) => {
const listeners = useRef<Map<ShortcutName, Set<ShortcutListener>>>(new Map());
const lastRepeatTime = useRef<Map<string, number>>(new Map());
const onKeyDown = useCallback(({ ctrlKey, shiftKey, altKey, metaKey, code, key, repeat }: KeyboardEvent) => {
if (isInputFocused()) return;
if (repeat) {
const now = Date.now();
const last = lastRepeatTime.current.get(code) ?? 0;
@ -43,7 +53,7 @@ const ShortcutsProvider = ({ children, onShortcut }: Props) => {
const combo = combos.indexOf(keys);
listeners.current.get(name)?.forEach((listener) => listener(combo));
onShortcut(name as ShortcutName);
onShortcut(name as ShortcutName, combo, key);
}
}));
}, [onShortcut]);

View file

@ -13,16 +13,16 @@
"label": "SETTINGS_SHORTCUT_GO_TO_SEARCH",
"combos": [["0"]]
},
{
"name": "navigateHistory",
"label": "SETTINGS_SHORTCUT_NAVIGATE_HISTORY",
"combos": [["Backspace"], ["Ctrl", "Backspace"]]
},
{
"name": "fullscreen",
"label": "SETTINGS_SHORTCUT_FULLSCREEN",
"combos": [["F"]]
},
{
"name": "exit",
"label": "SETTINGS_SHORTCUT_EXIT_BACK",
"combos": [["Backspace"]]
},
{
"name": "shortcuts",
"label": "SETTINGS_SHORTCUT_SHORTCUTS",
@ -55,14 +55,9 @@
"combos": [["ArrowLeft"], ["Shift", "ArrowLeft"]]
},
{
"name": "volumeUp",
"label": "SETTINGS_SHORTCUT_VOLUME_UP",
"combos": [["ArrowUp"]]
},
{
"name": "volumeDown",
"label": "SETTINGS_SHORTCUT_VOLUME_DOWN",
"combos": [["ArrowDown"]]
"name": "volume",
"label": "SETTINGS_SHORTCUT_VOLUME",
"combos": [["ArrowUp"], ["ArrowDown"]]
},
{
"name": "mute",
@ -80,14 +75,9 @@
"combos": [["G"], ["H"]]
},
{
"name": "speedDown",
"label": "SETTINGS_SHORTCUT_DECREASE_PLAYBACK_SPEED",
"combos": [["["]]
},
{
"name": "speedUp",
"label": "SETTINGS_SHORTCUT_INCREASE_PLAYBACK_SPEED",
"combos": [["]"]]
"name": "speed",
"label": "SETTINGS_SHORTCUT_PLAYBACK_SPEED",
"combos": [["["], ["]"]]
},
{
"name": "toggleSubtitles",
@ -123,6 +113,11 @@
"name": "playNext",
"label": "SETTINGS_SHORTCUT_PLAY_NEXT",
"combos": [["Shift", "N"]]
},
{
"name": "exit",
"label": "SETTINGS_SHORTCUT_EXIT_BACK",
"combos": [["Escape"]]
}
]
}

View file

@ -1,6 +1,9 @@
.combos {
position: relative;
display: flex;
flex-wrap: wrap;
row-gap: 1rem;
justify-content: end;
overflow: visible;
.combo {

View file

@ -1,7 +1,7 @@
.shortcuts-group {
flex: 1 1 0;
position: relative;
width: 30rem;
width: 35rem;
display: flex;
flex-direction: column;
gap: 2rem;
@ -26,7 +26,7 @@
.shortcut {
position: relative;
display: flex;
align-items: center;
align-items: baseline;
justify-content: space-between;
gap: 2rem;
overflow: visible;
@ -35,7 +35,6 @@
position: relative;
font-size: 1rem;
color: var(--primary-foreground-color);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}

View file

@ -590,15 +590,10 @@ const Player = ({ urlParams, queryParams }) => {
video.state.muted === true ? onUnmuteRequested() : onMuteRequested();
}, [video.state.muted], !menusOpen);
onShortcut('volumeUp', () => {
onShortcut('volume', (combo) => {
if (video.state.volume !== null) {
onVolumeChangeRequested(Math.min(video.state.volume + 5, 200));
}
}, [video.state.volume], !menusOpen);
onShortcut('volumeDown', () => {
if (video.state.volume !== null) {
onVolumeChangeRequested(Math.max(video.state.volume - 5, 0));
const volume = combo === 0 ? Math.min(video.state.volume + 5, 200) : Math.max(video.state.volume - 5, 0);
onVolumeChangeRequested(volume);
}
}, [video.state.volume], !menusOpen);
@ -623,15 +618,10 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [video.state.playbackSpeed, toggleSpeedMenu]);
onShortcut('speedUp', () => {
onShortcut('speed', (combo) => {
if (video.state.playbackSpeed !== null) {
onPlaybackSpeedChanged(Math.min(video.state.playbackSpeed + 0.25, 2));
}
}, [video.state.playbackSpeed, onPlaybackSpeedChanged], !menusOpen);
onShortcut('speedDown', () => {
if (video.state.playbackSpeed !== null) {
onPlaybackSpeedChanged(Math.max(video.state.playbackSpeed - 0.25, 0.25));
const speed = combo === 0 ? Math.max(video.state.playbackSpeed - 0.25, 0.25) : Math.min(video.state.playbackSpeed + 0.25, 2);
onPlaybackSpeedChanged(speed);
}
}, [video.state.playbackSpeed, onPlaybackSpeedChanged], !menusOpen);

View file

@ -7,6 +7,11 @@
action-buttons-container: action-buttons-container;
}
:import('~stremio/components/MultiselectMenu/Dropdown/Dropdown.less') {
dropdown: dropdown;
open: open;
}
@padding: 1rem;
.side-drawer {
@ -69,6 +74,7 @@
.info {
padding: @padding;
min-height: 0;
overflow-y: auto;
.side-drawer-meta-preview {
@ -89,8 +95,11 @@
flex: 2;
display: flex;
flex-direction: column;
min-height: 0;
.videos {
flex: 1;
min-height: 0;
overflow-y: auto;
}
}
@ -109,6 +118,14 @@
@media @phone-landscape {
.side-drawer {
max-width: 50dvw;
.info {
max-height: 40dvh;
}
.dropdown.open {
max-height: calc(3rem * 4);
}
}
}

View file

@ -1,6 +1,6 @@
// Copyright (C) 2017-2024 Smart code 203358507
import React, { useMemo, useCallback, useState, forwardRef, memo } from 'react';
import React, { useMemo, useCallback, useState, useRef, forwardRef, memo } from 'react';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import { useCore } from 'stremio/core';
@ -22,6 +22,7 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
const core = useCore();
const [season, setSeason] = useState<number>(seriesInfo?.season);
const [selectedVideoId, setSelectedVideoId] = useState<string | null>(null);
const videosRef = useRef<HTMLDivElement>(null);
const metaItem = useMemo(() => {
return seriesInfo ?
@ -47,8 +48,9 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
.sort((a, b) => (a || Number.MAX_SAFE_INTEGER) - (b || Number.MAX_SAFE_INTEGER));
}, [props.metaItem.videos]);
const seasonOnSelect = useCallback((event: { value: string }) => {
setSeason(parseInt(event.value));
const seasonOnSelect = useCallback((event: { value: string | number }) => {
setSeason(parseInt(String(event.value), 10));
videosRef.current?.scrollTo({ top: 0, left: 0 });
}, []);
const seasonWatched = React.useMemo(() => {
@ -109,7 +111,7 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
seasons={seasons}
onSelect={seasonOnSelect}
/>
<div className={styles['videos']}>
<div ref={videosRef} className={styles['videos']}>
{videos.map((video, index) => (
<Video
key={index}

View file

@ -55,6 +55,8 @@ html:not(.active-slider-within) {
}
&.nav-bar-layer {
left: var(--safe-area-inset-left);
right: var(--safe-area-inset-right);
bottom: initial;
background: transparent;
overflow: visible;
@ -95,9 +97,11 @@ html:not(.active-slider-within) {
}
&.control-bar-layer {
left: var(--safe-area-inset-left);
right: var(--safe-area-inset-right);
top: initial;
overflow: visible;
padding-bottom: 0.5rem;
padding-bottom: calc(0.5rem + var(--safe-area-inset-bottom));
&::before {
position: absolute;

View file

@ -1,93 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const EventEmitter = require('eventemitter3');
function KeyboardShortcuts() {
let active = false;
const events = new EventEmitter();
function onKeyDown(event) {
if (event.keyboardShortcutPrevented || event.target.tagName === 'INPUT' || event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) {
return;
}
switch (event.code) {
case 'Digit0': {
event.preventDefault();
window.location = '#/search';
break;
}
case 'Digit1': {
event.preventDefault();
window.location = '#/';
break;
}
case 'Digit2': {
event.preventDefault();
window.location = '#/discover';
break;
}
case 'Digit3': {
event.preventDefault();
window.location = '#/library';
break;
}
case 'Digit4': {
event.preventDefault();
window.location = '#/calendar';
break;
}
case 'Digit5': {
event.preventDefault();
window.location = '#/addons';
break;
}
case 'Digit6': {
event.preventDefault();
window.location = '#/settings';
break;
}
case 'Backspace': {
event.preventDefault();
if (event.ctrlKey) {
window.history.forward();
} else {
window.history.back();
}
break;
}
}
}
function onStateChanged() {
events.emit('stateChanged');
}
Object.defineProperties(this, {
active: {
configurable: false,
enumerable: true,
get: function() {
return active;
}
}
});
this.start = function() {
if (active) {
return;
}
window.addEventListener('keydown', onKeyDown);
active = true;
onStateChanged();
};
this.stop = function() {
window.removeEventListener('keydown', onKeyDown);
active = false;
onStateChanged();
};
}
module.exports = KeyboardShortcuts;

View file

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

View file

@ -1,5 +1,4 @@
type ServicesContext = {
shell: any,
chromecast: any,
keyboardShortcuts: any,
};

View file

@ -1,14 +1,12 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Chromecast = require('./Chromecast');
const KeyboardShortcuts = require('./KeyboardShortcuts');
const { ServicesProvider, useServices } = require('./ServicesContext');
const { GamepadProvider, useGamepad } = require('./GamepadContext');
const Shell = require('./Shell');
module.exports = {
Chromecast,
KeyboardShortcuts,
ServicesProvider,
useServices,
Shell,