From 0e3aa9c2c88f55b65bd9689a3a1b7a67a0127eb7 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 15 Jul 2025 17:00:08 +0200 Subject: [PATCH 01/75] feat: scroll to last watched video on details page --- src/components/Video/Video.js | 34 +++++++------------ src/routes/MetaDetails/MetaDetails.js | 1 + .../MetaDetails/VideosList/VideosList.js | 5 ++- src/routes/Player/SideDrawer/SideDrawer.tsx | 15 ++++---- 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/components/Video/Video.js b/src/components/Video/Video.js index 229fe758d..9d604828a 100644 --- a/src/components/Video/Video.js +++ b/src/components/Video/Video.js @@ -12,11 +12,12 @@ const useProfile = require('stremio/common/useProfile'); const VideoPlaceholder = require('./VideoPlaceholder'); const styles = require('./styles'); -const Video = React.forwardRef(({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }, ref) => { +const Video = ({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, selected, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }) => { const routeFocused = useRouteFocused(); const profile = useProfile(); const { t } = useTranslation(); const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); + const popupLabelOnMouseUp = React.useCallback((event) => { if (!event.nativeEvent.togglePopupPrevented) { if (event.nativeEvent.ctrlKey || event.nativeEvent.button === 2) { @@ -68,27 +69,17 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo } } }, [deepLinks]); - const renderLabel = React.useMemo(() => function renderLabel({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, children, ref: popupRef, ...props }) { + const renderLabel = React.useMemo(() => function renderLabel({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, children, ref, ...props }) { const blurThumbnail = profile.settings.hideSpoilers && season && episode && !watched; - const handleRef = React.useCallback((node) => { - if (popupRef) { - if (typeof popupRef === 'function') { - popupRef(node); - } else { - popupRef.current = node; - } - } - if (ref) { - if (typeof ref === 'function') { - ref(node); - } else { - ref.current = node; - } - } - }, [popupRef]); + + React.useEffect(() => { + selected && ref.current?.scrollIntoView({ + behavior: 'smooth', + }); + }, [selected]); return ( - ); - }, []); + }, [selected]); const renderMenu = React.useMemo(() => function renderMenu() { return (
@@ -203,7 +194,7 @@ const Video = React.forwardRef(({ className, id, title, thumbnail, season, episo renderMenu={renderMenu} /> ); -}); +}; Video.Placeholder = VideoPlaceholder; @@ -220,6 +211,7 @@ Video.propTypes = { progress: PropTypes.number, scheduled: PropTypes.bool, seasonWatched: PropTypes.bool, + selected: PropTypes.bool, deepLinks: PropTypes.shape({ metaDetailsStreams: PropTypes.string, player: PropTypes.string diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index d806ffed5..fd27478b5 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -190,6 +190,7 @@ const MetaDetails = ({ urlParams, queryParams }) => { metaItem={metaDetails.metaItem} libraryItem={metaDetails.libraryItem} season={season} + selectedVideoId={metaDetails.libraryItem?.state?.video_id} seasonOnSelect={seasonOnSelect} toggleNotifications={toggleNotifications} /> diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js index 88d53d427..8891947db 100644 --- a/src/routes/MetaDetails/VideosList/VideosList.js +++ b/src/routes/MetaDetails/VideosList/VideosList.js @@ -11,9 +11,10 @@ const SeasonsBar = require('./SeasonsBar'); const { default: EpisodePicker } = require('../EpisodePicker'); const styles = require('./styles'); -const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, toggleNotifications }) => { +const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, selectedVideoId, toggleNotifications }) => { const { core } = useServices(); const profile = useProfile(); + const showNotificationsToggle = React.useMemo(() => { return metaItem?.content?.content?.inLibrary && metaItem?.content?.content?.videos?.length; }, [metaItem]); @@ -178,6 +179,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, deepLinks={video.deepLinks} scheduled={video.scheduled} seasonWatched={seasonWatched} + selected={video.id === selectedVideoId} onMarkVideoAsWatched={onMarkVideoAsWatched} onMarkSeasonAsWatched={onMarkSeasonAsWatched} /> @@ -195,6 +197,7 @@ VideosList.propTypes = { metaItem: PropTypes.object, libraryItem: PropTypes.object, season: PropTypes.number, + selectedVideoId: PropTypes.string, seasonOnSelect: PropTypes.func, toggleNotifications: PropTypes.func, }; diff --git a/src/routes/Player/SideDrawer/SideDrawer.tsx b/src/routes/Player/SideDrawer/SideDrawer.tsx index 299c37b23..d5b13e11f 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.tsx +++ b/src/routes/Player/SideDrawer/SideDrawer.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2024 Smart code 203358507 -import React, { useMemo, useCallback, useState, forwardRef, memo, useRef } from 'react'; +import React, { useMemo, useCallback, useState, forwardRef, memo } from 'react'; import classNames from 'classnames'; import Icon from '@stremio/stremio-icons/react'; import { useServices } from 'stremio/services'; @@ -21,7 +21,8 @@ type Props = { const SideDrawer = memo(forwardRef(({ seriesInfo, className, closeSideDrawer, selected, ...props }: Props, ref) => { const { core } = useServices(); const [season, setSeason] = useState(seriesInfo?.season); - const selectedVideoRef = useRef(null); + const [selectedVideoId, setSelectedVideoId] = useState(null); + const metaItem = useMemo(() => { return seriesInfo ? { @@ -78,11 +79,9 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa event.stopPropagation(); }; - const onTransitionEnd = () => { - selectedVideoRef.current?.scrollIntoView({ - behavior: 'smooth', - }); - }; + const onTransitionEnd = useCallback(() => { + setSelectedVideoId(selected); + }, [selected]); return (
@@ -114,7 +113,6 @@ const SideDrawer = memo(forwardRef(({ seriesInfo, classNa {videos.map((video, index) => (
+
+ ), document.body); +}; + +export default ShortcutsModal; diff --git a/src/App/ShortcutsModal/index.ts b/src/App/ShortcutsModal/index.ts new file mode 100644 index 000000000..5a7549fac --- /dev/null +++ b/src/App/ShortcutsModal/index.ts @@ -0,0 +1,2 @@ +import ShortcutsModal from './ShortcutsModal'; +export default ShortcutsModal; diff --git a/src/App/ShortcutsModal/styles.less b/src/App/ShortcutsModal/styles.less new file mode 100644 index 000000000..ebbc19c62 --- /dev/null +++ b/src/App/ShortcutsModal/styles.less @@ -0,0 +1,91 @@ +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.shortcuts-modal { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + + .backdrop { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: @color-background-dark5-40; + cursor: pointer; + } + + .container { + position: relative; + display: flex; + flex-direction: column; + gap: 1rem; + max-height: 80%; + max-width: 80%; + border-radius: var(--border-radius); + background-color: var(--modal-background-color); + box-shadow: var(--outer-glow); + overflow-y: auto; + + .header { + flex: none; + display: flex; + justify-content: space-between; + align-items: center; + height: 5rem; + padding-left: 2.5rem; + padding-right: 1rem; + + .title { + position: relative; + font-size: 1.5rem; + font-weight: 500; + color: var(--primary-foreground-color); + } + + .close-button { + position: relative; + width: 3rem; + height: 3rem; + padding: 0.5rem; + border-radius: var(--border-radius); + z-index: 2; + + .icon { + display: block; + width: 100%; + height: 100%; + color: var(--primary-foreground-color); + opacity: 0.4; + } + + &:hover, &:focus { + .icon { + opacity: 1; + color: var(--primary-foreground-color); + } + } + + &:focus { + outline-color: var(--primary-foreground-color); + } + } + } + + .content { + position: relative; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 3rem; + padding: 0 2.5rem; + padding-bottom: 2rem; + overflow-y: auto; + } + } +} \ No newline at end of file diff --git a/src/common/Shortcuts/Shortcuts.tsx b/src/common/Shortcuts/Shortcuts.tsx new file mode 100644 index 000000000..f41e08271 --- /dev/null +++ b/src/common/Shortcuts/Shortcuts.tsx @@ -0,0 +1,54 @@ +import React, { createContext, useCallback, useContext, useEffect } from 'react'; +import shortcuts from './shortcuts'; + +const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat(); + +export type ShortcutName = string; +export type ShortcutListener = () => void; + +interface ShortcutsContext { + grouped: ShortcutGroup[], +} + +const ShortcutsContext = createContext({} as ShortcutsContext); + +type Props = { + children: JSX.Element, + onShortcut: (name: ShortcutName) => void, +}; + +const ShortcutsProvider = ({ children, onShortcut }: Props) => { + const onKeyDown = useCallback(({ ctrlKey, shiftKey, 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())) { + onShortcut(name as ShortcutName); + } + })); + }, [onShortcut]); + + useEffect(() => { + document.addEventListener('keydown', onKeyDown); + + return () => { + document.removeEventListener('keydown', onKeyDown); + }; + }, [onKeyDown]); + + return ( + + {children} + + ); +}; + +const useShortcuts = () => { + return useContext(ShortcutsContext); +}; + +export { + ShortcutsProvider, + useShortcuts +}; diff --git a/src/common/Shortcuts/index.ts b/src/common/Shortcuts/index.ts new file mode 100644 index 000000000..f7fa38a18 --- /dev/null +++ b/src/common/Shortcuts/index.ts @@ -0,0 +1,5 @@ +import { ShortcutsProvider, useShortcuts } from './Shortcuts'; +export { + ShortcutsProvider, + useShortcuts, +}; diff --git a/src/common/Shortcuts/shortcuts.ts b/src/common/Shortcuts/shortcuts.ts new file mode 100644 index 000000000..d918733af --- /dev/null +++ b/src/common/Shortcuts/shortcuts.ts @@ -0,0 +1,91 @@ +const shortcuts: ShortcutGroup[] = [ + { + name: 'general', + label: 'SETTINGS_NAV_GENERAL', + shortcuts: [ + { + name: 'navigateTabs', + label: 'SETTINGS_SHORTCUT_NAVIGATE_MENUS', + combos: [['1', '2', '3', '4', '5', '6']], + }, + { + name: 'navigateSearch', + label: 'SETTINGS_SHORTCUT_GO_TO_SEARCH', + combos: [['0']], + }, + { + name: 'fullscreen', + label: 'SETTINGS_SHORTCUT_FULLSCREEN', + combos: [['F']], + }, + { + name: 'exit', + label: 'SETTINGS_SHORTCUT_EXIT_BACK', + combos: [['Escape']], + }, + { + name: 'shortcuts', + label: 'SETTINGS_SHORTCUT_SHORTCUTS', + combos: [['Ctrl', '?']], + }, + ] + }, + { + name: 'player', + label: 'SETTINGS_NAV_PLAYER', + shortcuts: [ + { + name: 'playPause', + label: 'SETTINGS_SHORTCUT_PLAY_PAUSE', + combos: [['Space']], + }, + { + name: 'seekForward', + label: 'SETTINGS_SHORTCUT_SEEK_FORWARD', + combos: [['ArrowRight'], ['Shift', 'ArrowRight']], + }, + { + name: 'seekBackward', + label: 'SETTINGS_SHORTCUT_SEEK_BACKWARD', + combos: [['ArrowLeft'], ['Shift', 'ArrowLeft']], + }, + { + name: 'volumeUp', + label: 'SETTINGS_SHORTCUT_VOLUME_UP', + combos: [['ArrowUp']], + }, + { + name: 'volumeDown', + label: 'SETTINGS_SHORTCUT_VOLUME_DOWN', + combos: [['ArrowDown']], + }, + { + name: 'subtitlesSize', + label: 'SETTINGS_SHORTCUT_SUBTITLES_SIZE', + combos: [['-'], ['=']], + }, + { + name: 'subtitlesDelay', + label: 'SETTINGS_SHORTCUT_SUBTITLES_DELAY', + combos: [['G'], ['H']], + }, + { + name: 'subtitlesMenu', + label: 'SETTINGS_SHORTCUT_MENU_SUBTITLES', + combos: [['S']], + }, + { + name: 'audioMenu', + label: 'SETTINGS_SHORTCUT_MENU_AUDIO', + combos: [['A']], + }, + { + name: 'infoMenu', + label: 'SETTINGS_SHORTCUT_MENU_INFO', + combos: [['I']], + }, + ] + }, +]; + +export default shortcuts; diff --git a/src/common/Shortcuts/types.d.ts b/src/common/Shortcuts/types.d.ts new file mode 100644 index 000000000..e4180616d --- /dev/null +++ b/src/common/Shortcuts/types.d.ts @@ -0,0 +1,11 @@ +type Shortcut = { + name: string, + label: string, + combos: string[][], +}; + +type ShortcutGroup = { + name: string, + label: string, + shortcuts: Shortcut[], +}; diff --git a/src/common/index.js b/src/common/index.js index 25df5c158..0b9cb252f 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -4,6 +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 comparatorWithPriorities = require('./comparatorWithPriorities'); const CONSTANTS = require('./CONSTANTS'); const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender'); @@ -35,6 +36,8 @@ module.exports = { onFileDrop, PlatformProvider, usePlatform, + ShortcutsProvider, + useShortcuts, ToastProvider, useToast, TooltipProvider, diff --git a/src/components/ShortcutsGroup/Combos/Combos.less b/src/components/ShortcutsGroup/Combos/Combos.less new file mode 100644 index 000000000..a862d54ca --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Combos.less @@ -0,0 +1,22 @@ +.combos { + position: relative; + display: flex; + overflow: visible; + + .combo { + position: relative; + display: flex; + overflow: visible; + + .separator { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 3.5rem; + font-size: 1rem; + color: var(--primary-foreground-color); + opacity: 0.6; + } + } +} \ No newline at end of file diff --git a/src/components/ShortcutsGroup/Combos/Combos.tsx b/src/components/ShortcutsGroup/Combos/Combos.tsx new file mode 100644 index 000000000..0168441bc --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Combos.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Keys from './Keys'; +import styles from './Combos.less'; + +type Props = { + combos: string[][], +}; + +const Combos = ({ combos }: Props) => { + const { t } = useTranslation(); + + return ( +
+ { + combos.map((keys, index) => ( +
+ + { + index < (combos.length - 1) && ( +
+ { t('SETTINGS_SHORTCUT_OR') } +
+ ) + } +
+ )) + } +
+ ); +}; + +export default Combos; diff --git a/src/components/ShortcutsGroup/Combos/Keys/Keys.less b/src/components/ShortcutsGroup/Combos/Keys/Keys.less new file mode 100644 index 000000000..7bb8c76e7 --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Keys/Keys.less @@ -0,0 +1,26 @@ +kbd { + flex: none; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + height: 2.5rem; + min-width: 2.5rem; + padding: 0 1rem; + font-size: 1rem; + font-weight: 500; + color: var(--primary-foreground-color); + border-radius: 0.25em; + box-shadow: 0 4px 0 1px rgba(255, 255, 255, 0.1); + background-color: var(--overlay-color); +} + +.separator { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + font-size: 1rem; + color: var(--primary-foreground-color); +} \ No newline at end of file diff --git a/src/components/ShortcutsGroup/Combos/Keys/Keys.tsx b/src/components/ShortcutsGroup/Combos/Keys/Keys.tsx new file mode 100644 index 000000000..71ec610da --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Keys/Keys.tsx @@ -0,0 +1,51 @@ +import React, { Fragment, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './Keys.less'; + +type Props = { + keys: string[], +}; + +const Keys = ({ keys }: Props) => { + const { t } = useTranslation(); + + const keyLabelMap: Record = useMemo(() => ({ + 'Shift': `⇧ ${t('SETTINGS_SHORTCUT_SHIFT')}`, + 'Space': t('SETTINGS_SHORTCUT_SPACE'), + 'Ctrl': t('SETTINGS_SHORTCUT_CTRL'), + 'Escape': t('SETTINGS_SHORTCUT_ESC'), + 'ArrowUp': '↑', + 'ArrowDown': '↓', + 'ArrowLeft': '←', + 'ArrowRight': '→', + }), [t]); + + const isRange = useMemo(() => { + return keys.length > 1 && keys.every((key) => !Number.isNaN(parseInt(key))); + }, [keys]); + + const filteredKeys = useMemo(() => { + return isRange ? [keys[0], keys[keys.length - 1]] : keys; + }, [keys, isRange]); + + return ( + filteredKeys.map((key, index) => ( + + + {keyLabelMap[key] ?? key.toUpperCase()} + + { + index < (filteredKeys.length - 1) && ( +
+ { + isRange ? t('SETTINGS_SHORTCUT_TO') : '+' + } +
+ ) + } +
+ )) + ); +}; + +export default Keys; diff --git a/src/components/ShortcutsGroup/Combos/Keys/index.ts b/src/components/ShortcutsGroup/Combos/Keys/index.ts new file mode 100644 index 000000000..ba8d58731 --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/Keys/index.ts @@ -0,0 +1,2 @@ +import Keys from './Keys'; +export default Keys; diff --git a/src/components/ShortcutsGroup/Combos/index.ts b/src/components/ShortcutsGroup/Combos/index.ts new file mode 100644 index 000000000..c66667f91 --- /dev/null +++ b/src/components/ShortcutsGroup/Combos/index.ts @@ -0,0 +1,2 @@ +import Combos from './Combos'; +export default Combos; diff --git a/src/components/ShortcutsGroup/ShortcutsGroup.less b/src/components/ShortcutsGroup/ShortcutsGroup.less new file mode 100644 index 000000000..f0fdd975c --- /dev/null +++ b/src/components/ShortcutsGroup/ShortcutsGroup.less @@ -0,0 +1,44 @@ +.shortcuts-group { + flex: 1 1 0; + position: relative; + min-width: 30rem; + display: flex; + flex-direction: column; + gap: 2rem; + overflow: visible; + + .title { + flex: none; + display: flex; + font-size: 1rem; + font-weight: 400; + color: var(--primary-foreground-color); + opacity: 0.6; + } + + .shortcuts { + position: relative; + display: flex; + flex-direction: column; + gap: 2rem; + overflow: visible; + + .shortcut { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; + overflow: visible; + + .label { + position: relative; + font-size: 1rem; + color: var(--primary-foreground-color); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + } +} diff --git a/src/components/ShortcutsGroup/ShortcutsGroup.tsx b/src/components/ShortcutsGroup/ShortcutsGroup.tsx new file mode 100644 index 000000000..069d5d1e8 --- /dev/null +++ b/src/components/ShortcutsGroup/ShortcutsGroup.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; +import Combos from './Combos'; +import styles from './ShortcutsGroup.less'; + +type Props = { + className?: string, + label: string, + shortcuts: Shortcut[], +}; + +const ShortcutsGroup = ({ className, label, shortcuts }: Props) => { + const { t } = useTranslation(); + + return ( +
+
+ {t(label)} +
+ +
+ { + shortcuts.map(({ name, label, combos }) => ( +
+
+ {t(label)} +
+ +
+ )) + } +
+
+ ); +}; + +export default ShortcutsGroup; diff --git a/src/components/ShortcutsGroup/index.ts b/src/components/ShortcutsGroup/index.ts new file mode 100644 index 000000000..11f8d0678 --- /dev/null +++ b/src/components/ShortcutsGroup/index.ts @@ -0,0 +1,2 @@ +import ShortcutsGroup from './ShortcutsGroup'; +export default ShortcutsGroup; diff --git a/src/components/index.ts b/src/components/index.ts index a5638007e..a47c2c709 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -25,6 +25,7 @@ import RadioButton from './RadioButton'; import SearchBar from './SearchBar'; import SharePrompt from './SharePrompt'; import Slider from './Slider'; +import ShortcutsGroup from './ShortcutsGroup'; import TextInput from './TextInput'; import Toggle from './Toggle'; import Transition from './Transition'; @@ -59,6 +60,7 @@ export { SearchBar, SharePrompt, Slider, + ShortcutsGroup, TextInput, Toggle, Transition, diff --git a/src/routes/Settings/Shortcuts/Shortcuts.less b/src/routes/Settings/Shortcuts/Shortcuts.less index 40d97987d..186cfa837 100644 --- a/src/routes/Settings/Shortcuts/Shortcuts.less +++ b/src/routes/Settings/Shortcuts/Shortcuts.less @@ -1,27 +1,4 @@ -.shortcut-container { - display: flex; - align-items: center; - justify-content: center; - padding: 0; - overflow: visible; - - kbd { - flex: 0 1 auto; - height: 2.5rem; - min-width: 2.5rem; - line-height: 2.5rem; - padding: 0 1rem; - font-weight: 500; - color: var(--primary-foreground-color); - border-radius: 0.25em; - box-shadow: 0 4px 0 1px var(--modal-background-color); - background-color: var(--overlay-color); - } - - .label { - flex: none; - margin: 0 1rem; - white-space: nowrap; - color: var(--primary-foreground-color); - } +.shortcuts-group { + width: 100%; + margin-bottom: 3rem; } \ No newline at end of file diff --git a/src/routes/Settings/Shortcuts/Shortcuts.tsx b/src/routes/Settings/Shortcuts/Shortcuts.tsx index d852280a6..a0599a503 100644 --- a/src/routes/Settings/Shortcuts/Shortcuts.tsx +++ b/src/routes/Settings/Shortcuts/Shortcuts.tsx @@ -1,97 +1,24 @@ import React, { forwardRef } from 'react'; -import { Section, Option } from '../components'; +import { Section } from '../components'; +import { ShortcutsGroup } from 'stremio/components'; +import { useShortcuts } from 'stremio/common'; import styles from './Shortcuts.less'; -import { useTranslation } from 'react-i18next'; const Shortcuts = forwardRef((_, ref) => { - const { t } = useTranslation(); + const { grouped } = useShortcuts(); return (
- - - - - - - - - - - - - - + { + grouped.map(({ name, label, shortcuts }) => ( + + )) + }
); }); diff --git a/tsconfig.json b/tsconfig.json index d55ac9356..c4b0a8626 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "lib": [ "ES2016", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "jsx": "react", "baseUrl": "./src", "outDir": "./dist", From 539a7ebc10c8840c34d69f4c667b63da0da9ae8b Mon Sep 17 00:00:00 2001 From: Victor Sales <36749678+v1ctorsales@users.noreply.github.com> Date: Sun, 12 Oct 2025 13:41:42 +0300 Subject: [PATCH 45/75] fix(calendar): align day and more indicator inline in narrow desktop viewports --- src/routes/Calendar/Table/Cell/Cell.less | 69 +++++++++++++++--------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/src/routes/Calendar/Table/Cell/Cell.less b/src/routes/Calendar/Table/Cell/Cell.less index e989b4cf7..d66084392 100644 --- a/src/routes/Calendar/Table/Cell/Cell.less +++ b/src/routes/Calendar/Table/Cell/Cell.less @@ -28,7 +28,7 @@ .heading { position: relative; - width: 60%; + width: 100%; display: flex; align-items: start; padding: 0; @@ -52,7 +52,7 @@ position: relative; display: flex; flex-direction: row; - gap: 1rem; + gap: 0.1em; padding: 0.1rem; width: 90%; @@ -80,13 +80,10 @@ } .poster { - flex: auto; - z-index: 0; - position: relative; - height: 100%; - width: 100%; + height: auto; + max-height: 100%; + aspect-ratio: 2 / 3; object-fit: cover; - opacity: 1; } .icon, .poster { @@ -134,27 +131,19 @@ } } -@media only screen and (orientation: portrait) { +@media @phone-portrait { .cell { flex-direction: column; display: grid; - .heading { - justify-content: center; - } .items { - padding: 1px; - gap: 0.15rem; - justify-content: space-evenly; + padding: 1px; + gap: 0.15rem; + justify-content: space-evenly; - .item{ - width: 80%; - } - } - - .more { - display: flex; - display: none; + .item { + width: 80%; + } } } } @@ -180,4 +169,36 @@ } } } -} \ No newline at end of file +} + +@media only screen and (max-width: @minimum) and (orientation: portrait) and (pointer: fine) { + .cell { + position: relative; + flex-direction: row; + align-items: center; + display: flex; + + .items { + display: none; + } + + .heading { + display: flex; + align-items: center; + gap: 0.25rem; + width: auto; + } + + .day { + margin-right: 0.25rem; + } + + .more { + display: inline-flex; + position: relative; + height: auto; + padding: 0; + width: 30%; + } + } +} From fb9497a85631f28bc4eca0a39a45e52f608d496a Mon Sep 17 00:00:00 2001 From: Victor Sales <36749678+v1ctorsales@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:23:34 +0300 Subject: [PATCH 46/75] feat(calendar): redesign calendar cell layout for responsiveness and banner support --- src/routes/Calendar/Table/Cell/Cell.less | 56 +++++++++++------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/src/routes/Calendar/Table/Cell/Cell.less b/src/routes/Calendar/Table/Cell/Cell.less index d66084392..0fdf615d2 100644 --- a/src/routes/Calendar/Table/Cell/Cell.less +++ b/src/routes/Calendar/Table/Cell/Cell.less @@ -28,10 +28,9 @@ .heading { position: relative; - width: 100%; + flex: 1 1 40%; display: flex; - align-items: start; - padding: 0; + align-items: flex-start; .day { flex: none; @@ -54,7 +53,10 @@ flex-direction: row; gap: 0.1em; padding: 0.1rem; - width: 90%; + flex: 1 1 60%; + overflow-x: auto; + overflow-y: hidden; + min-width: 0; .item { flex: none; @@ -63,8 +65,9 @@ justify-content: center; height: 100%; aspect-ratio: 2 / 3; - border-radius: var(--border-radius); - max-height: clamp(8rem, 25vh, 14rem); + border-radius: calc(var(--border-radius) /2); + max-height: 100%; + width: 100%; .icon { flex: none; @@ -84,6 +87,7 @@ max-height: 100%; aspect-ratio: 2 / 3; object-fit: cover; + border-radius: inherit } .icon, .poster { @@ -115,7 +119,10 @@ &.today { .heading { .day { + width: auto; background-color: var(--primary-accent-color); + height: auto; + padding: 0.3rem; } } } @@ -142,12 +149,21 @@ justify-content: space-evenly; .item { - width: 80%; + width: 100%; } } } } +@media only screen and (max-height: @xxsmall) and (max-width: @xxsmall) and (orientation: landscape) { + .cell{ + .items{ + width: 100%; + } + } +} + + @media only screen and (max-height: @medium) and (max-width: @medium) and (orientation: landscape) { .cell { gap: 0; @@ -161,12 +177,6 @@ .items { width: 100%; - - .item { - pointer-events: none; - border-radius: calc(var(--border-radius) / 2); - width: 80%; - } } } } @@ -175,30 +185,16 @@ .cell { position: relative; flex-direction: row; - align-items: center; display: flex; - .items { - display: none; - } - .heading { display: flex; - align-items: center; gap: 0.25rem; width: auto; - } + .day{ + font-size: 0.6rem; + } - .day { - margin-right: 0.25rem; - } - - .more { - display: inline-flex; - position: relative; - height: auto; - padding: 0; - width: 30%; } } } From 62a650018b2f61ad1a2eb6808b6fbc33abd4640d Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Mon, 13 Oct 2025 01:14:13 +0800 Subject: [PATCH 47/75] Fix GitHub Actions badge in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f84c3bb0c..b7cd999dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Stremio - Freedom to Stream -![Build](https://github.com/stremio/stremio-web/workflows/Build/badge.svg?branch=development) +[![Build](https://github.com/Stremio/stremio-web/actions/workflows/build.yml/badge.svg)](https://github.com/Stremio/stremio-web/actions/workflows/build.yml) [![Github Page](https://img.shields.io/website?label=Page&logo=github&up_message=online&down_message=offline&url=https%3A%2F%2Fstremio.github.io%2Fstremio-web%2F)](https://stremio.github.io/stremio-web/development) Stremio is a modern media center that's a one-stop solution for your video entertainment. You discover, watch and organize video content from easy to install addons. From 4860a028c2410bed51e6155b8147de8ceacc1e39 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 13 Oct 2025 11:29:16 +0200 Subject: [PATCH 48/75] chore: update translations --- package.json | 2 +- pnpm-lock.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index c824ad411..61e713041 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#abe7684165a031755e9aee39da26daa806ba7824", + "stremio-translations": "github:Stremio/stremio-translations#5e5e3776ff1a1684648eaa3fa6c3a6caeb67cc37", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 121d0dbea..110fd7d71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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#abe7684165a031755e9aee39da26daa806ba7824 - version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/abe7684165a031755e9aee39da26daa806ba7824 + specifier: github:Stremio/stremio-translations#5e5e3776ff1a1684648eaa3fa6c3a6caeb67cc37 + version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/5e5e3776ff1a1684648eaa3fa6c3a6caeb67cc37 url: specifier: 0.11.4 version: 0.11.4 @@ -4527,9 +4527,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/abe7684165a031755e9aee39da26daa806ba7824: - resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/abe7684165a031755e9aee39da26daa806ba7824} - version: 1.44.12 + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/5e5e3776ff1a1684648eaa3fa6c3a6caeb67cc37: + resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/5e5e3776ff1a1684648eaa3fa6c3a6caeb67cc37} + version: 1.44.13 string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -10283,7 +10283,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/abe7684165a031755e9aee39da26daa806ba7824: {} + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/5e5e3776ff1a1684648eaa3fa6c3a6caeb67cc37: {} string-length@4.0.2: dependencies: From 2e1ad64d02b23b7a508a708acab62616d4e79acd Mon Sep 17 00:00:00 2001 From: Victor Sales <36749678+v1ctorsales@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:06:52 +0300 Subject: [PATCH 49/75] refactor(calendar): replace fixed width with max-width for better banner scaling --- src/routes/Calendar/Table/Cell/Cell.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Calendar/Table/Cell/Cell.less b/src/routes/Calendar/Table/Cell/Cell.less index 0fdf615d2..95116de20 100644 --- a/src/routes/Calendar/Table/Cell/Cell.less +++ b/src/routes/Calendar/Table/Cell/Cell.less @@ -51,7 +51,7 @@ position: relative; display: flex; flex-direction: row; - gap: 0.1em; + gap: 0.2rem; padding: 0.1rem; flex: 1 1 60%; overflow-x: auto; @@ -67,7 +67,7 @@ aspect-ratio: 2 / 3; border-radius: calc(var(--border-radius) /2); max-height: 100%; - width: 100%; + max-width: 100%; .icon { flex: none; From 122e43dbe52452d26bf486e891282beaf4f6cfd3 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 13 Oct 2025 12:26:42 +0200 Subject: [PATCH 50/75] refactor(Player): remove handling of media keys --- src/routes/Player/Player.js | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 6342daa54..e9583ec3c 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -594,36 +594,6 @@ const Player = ({ urlParams, queryParams }) => { break; } - case 'MediaPlayPause': { - if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) { - event.preventDefault(); - if (video.state.paused) { - onPlayRequested(); - setSeeking(false); - } else { - onPauseRequested(); - } - } - - break; - } - case 'MediaPlay': { - if (!menusOpen && !nextVideoPopupOpen && video.state.paused === true) { - event.preventDefault(); - onPlayRequested(); - setSeeking(false); - } - - break; - } - case 'MediaPause': { - if (!menusOpen && !nextVideoPopupOpen && video.state.paused === false) { - event.preventDefault(); - onPauseRequested(); - } - - break; - } case 'ArrowRight': { if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) { const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration; From 3eff7f0903a8fd6f8ccec8cfbb99feb06a3e06b4 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 13 Oct 2025 12:33:46 +0200 Subject: [PATCH 51/75] refactor(Player): use poster for media session artwork --- src/routes/Player/Player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index e9583ec3c..eab38bd96 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -553,7 +553,7 @@ const Player = ({ urlParams, queryParams }) => { 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.poster : null; + const imageUrl = metaItem ? metaItem.logo : null; const title = videoTitle ?? metaTitle; const artist = videoTitle ? metaTitle : undefined; From e74072ebd5c2e548589630022d2dae156d3cebaf Mon Sep 17 00:00:00 2001 From: Victor Sales <36749678+v1ctorsales@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:40:13 +0300 Subject: [PATCH 52/75] fix(calendar): disable banner click in phone-portrait mode --- src/routes/Calendar/Table/Cell/Cell.less | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/routes/Calendar/Table/Cell/Cell.less b/src/routes/Calendar/Table/Cell/Cell.less index 95116de20..3a103c913 100644 --- a/src/routes/Calendar/Table/Cell/Cell.less +++ b/src/routes/Calendar/Table/Cell/Cell.less @@ -138,6 +138,17 @@ } } +@media only screen and (max-width: @minimum) and (orientation: portrait) { + .cell{ + .items{ + .item{ + pointer-events: none; + } + + } + } +} + @media @phone-portrait { .cell { flex-direction: column; @@ -150,6 +161,7 @@ .item { width: 100%; + pointer-events: none; } } } @@ -187,6 +199,12 @@ flex-direction: row; display: flex; + .items{ + .item{ + pointer-events: none; + } + } + .heading { display: flex; gap: 0.25rem; From a97dd01869f0342f0449b4d40278ea2b7a4a70e4 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 13 Oct 2025 12:55:12 +0200 Subject: [PATCH 53/75] refactor(shortcuts): use Ctrl + / for shortcuts modal --- src/common/Shortcuts/shortcuts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/Shortcuts/shortcuts.ts b/src/common/Shortcuts/shortcuts.ts index d918733af..614495f0a 100644 --- a/src/common/Shortcuts/shortcuts.ts +++ b/src/common/Shortcuts/shortcuts.ts @@ -26,7 +26,7 @@ const shortcuts: ShortcutGroup[] = [ { name: 'shortcuts', label: 'SETTINGS_SHORTCUT_SHORTCUTS', - combos: [['Ctrl', '?']], + combos: [['Ctrl', '/']], }, ] }, From d2d28be6ded2956837e6167854d99fd78f5cff8d Mon Sep 17 00:00:00 2001 From: Victor Sales <36749678+v1ctorsales@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:27:15 +0300 Subject: [PATCH 54/75] style(responsive): add @phone-landscape media query --- src/routes/Calendar/Table/Cell/Cell.less | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/routes/Calendar/Table/Cell/Cell.less b/src/routes/Calendar/Table/Cell/Cell.less index 3a103c913..754715f32 100644 --- a/src/routes/Calendar/Table/Cell/Cell.less +++ b/src/routes/Calendar/Table/Cell/Cell.less @@ -167,12 +167,19 @@ } } -@media only screen and (max-height: @xxsmall) and (max-width: @xxsmall) and (orientation: landscape) { - .cell{ - .items{ - width: 100%; +@media @phone-landscape { + .cell { + flex-direction: row; + + .items { + padding: 1px; + gap: 0.15rem; + + .item { + pointer-events: none; + } + } } - } } From 56b60beedbb0c04cad82d3d6ad32264340a7f0e9 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 14 Oct 2025 11:39:28 +0300 Subject: [PATCH 55/75] fix: useFullscreen - catch exception on Firefox when using keyboard F shortcut in web Signed-off-by: Lachezar Lechev --- src/common/useFullscreen.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts index 69cdcd494..0e5c13f42 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -14,7 +14,9 @@ const useFullscreen = () => { if (shell.active) { shell.send('win-set-visibility', { fullscreen: true }); } else { - document.documentElement.requestFullscreen(); + document.documentElement.requestFullscreen().catch((err) => { + console.error(`Error enabling fullscreen: ${err.message}`); + }); } }, []); From 91fbfc1178dacb9783f881e00dc1e3d5907b9e47 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 14 Oct 2025 12:00:43 +0300 Subject: [PATCH 56/75] fix: useFullscreen - catch exception on Firefox when using keyboard F shortcut in web Signed-off-by: Lachezar Lechev --- src/common/useFullscreen.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts index 0e5c13f42..8a1692254 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -10,13 +10,15 @@ const useFullscreen = () => { const [fullscreen, setFullscreen] = useState(false); - const requestFullscreen = useCallback(() => { + const requestFullscreen = useCallback(async () => { if (shell.active) { shell.send('win-set-visibility', { fullscreen: true }); } else { - document.documentElement.requestFullscreen().catch((err) => { - console.error(`Error enabling fullscreen: ${err.message}`); - }); + try { + await document.documentElement.requestFullscreen(); + } catch (err) { + console.error('Error enabling fullscreen', err); + } } }, []); From 0143bf914c1e343894d3d5f09d612b710445d1a9 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 13 Oct 2025 15:13:21 +0200 Subject: [PATCH 57/75] feat: add video mode setting --- src/routes/Player/Player.js | 1 + src/routes/Settings/Player/Player.tsx | 12 +++++++ .../Settings/Player/usePlayerOptions.ts | 33 +++++++++++++++++++ src/types/models/Ctx.d.ts | 1 + 4 files changed, 47 insertions(+) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index eab38bd96..4231418c6 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -345,6 +345,7 @@ const Player = ({ urlParams, queryParams }) => { forceTranscoding: forceTranscoding || casting, maxAudioChannels: settings.surroundSound ? 32 : 2, hardwareDecoding: settings.hardwareDecoding, + videoMode: settings.videoMode, streamingServerURL: streamingServer.baseUrl ? casting ? streamingServer.baseUrl diff --git a/src/routes/Settings/Player/Player.tsx b/src/routes/Settings/Player/Player.tsx index 72a941e81..29b98d650 100644 --- a/src/routes/Settings/Player/Player.tsx +++ b/src/routes/Settings/Player/Player.tsx @@ -3,6 +3,7 @@ import { ColorInput, MultiselectMenu, Toggle } from 'stremio/components'; import { useServices } from 'stremio/services'; import { Category, Option, Section } from '../components'; import usePlayerOptions from './usePlayerOptions'; +import { usePlatform } from 'stremio/common'; type Props = { profile: Profile, @@ -10,6 +11,7 @@ type Props = { const Player = forwardRef(({ profile }: Props, ref) => { const { shell } = useServices(); + const platform = usePlatform(); const { subtitlesLanguageSelect, @@ -26,6 +28,7 @@ const Player = forwardRef(({ profile }: Props, ref) => { bingeWatchingToggle, playInBackgroundToggle, hardwareDecodingToggle, + videoModeSelect, pauseOnMinimizeToggle, } = usePlayerOptions(profile); @@ -129,6 +132,15 @@ const Player = forwardRef(({ profile }: Props, ref) => { /> } + { + shell.active && platform.name === 'windows' && + + } { shell.active &&