diff --git a/package-lock.json b/package-lock.json index 3fca2228f..3592aca2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stremio", - "version": "5.0.0-beta.20", + "version": "5.0.0-beta.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.20", + "version": "5.0.0-beta.21", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.26.0", @@ -36,7 +36,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#4bb1b7e31df274f538b8588c2a2b360d6e14ab27", + "stremio-translations": "github:Stremio/stremio-translations#a6be0425573917c2e82b66d28968c1a4d444cb96", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -13373,8 +13373,8 @@ }, "node_modules/stremio-translations": { "version": "1.44.10", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#4bb1b7e31df274f538b8588c2a2b360d6e14ab27", - "integrity": "sha512-+RLkoytMyqP90mn9Wkh1MhwB2fxVuvMxsxxceGnFgYlyyEL8fxuHTRnSaBjWBw+xFtsaeMLmDfA1n3l+UEzg4A==", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a6be0425573917c2e82b66d28968c1a4d444cb96", + "integrity": "sha512-77kVE/eos/SA16kzeK7TTWmqoLF0mLPCJXjITwVIVzMHr8XyBPZFOfmiVEg4M6W1W7qYqA+dHhzicyLs7hJhlw==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index afed7e885..8eebb2685 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stremio", "displayName": "Stremio", - "version": "5.0.0-beta.20", + "version": "5.0.0-beta.21", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", @@ -40,7 +40,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#4bb1b7e31df274f538b8588c2a2b360d6e14ab27", + "stremio-translations": "github:Stremio/stremio-translations#a6be0425573917c2e82b66d28968c1a4d444cb96", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/src/App/App.js b/src/App/App.js index 803515b09..38be271ac 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -21,7 +21,6 @@ const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router)) const App = () => { const { i18n } = useTranslation(); const shell = useShell(); - const [windowHidden, setWindowHidden] = React.useState(false); const onPathNotMatch = React.useCallback(() => { return NotFound; }, []); @@ -102,10 +101,6 @@ const App = () => { // Handle shell events React.useEffect(() => { - const onWindowVisibilityChanged = (state) => { - setWindowHidden(state.visible === false && state.visibility === 0); - }; - const onOpenMedia = (data) => { if (data.startsWith('stremio:///')) return; if (data.startsWith('stremio://')) { @@ -116,11 +111,9 @@ const App = () => { } }; - shell.on('win-visibility-changed', onWindowVisibilityChanged); shell.on('open-media', onOpenMedia); return () => { - shell.off('win-visibility-changed', onWindowVisibilityChanged); shell.off('open-media', onOpenMedia); }; }, []); @@ -133,7 +126,7 @@ const App = () => { i18n.changeLanguage(args.settings.interfaceLanguage); } - if (args?.settings?.quitOnClose && windowHidden) { + if (args?.settings?.quitOnClose && shell.windowClosed) { shell.send('quit'); } @@ -146,7 +139,7 @@ const App = () => { i18n.changeLanguage(state.profile.settings.interfaceLanguage); } - if (state?.profile?.settings?.quitOnClose && windowHidden) { + if (state?.profile?.settings?.quitOnClose && shell.windowClosed) { shell.send('quit'); } }; @@ -191,7 +184,7 @@ const App = () => { services.core.transport.off('CoreEvent', onCoreEvent); } }; - }, [initialized, windowHidden]); + }, [initialized, shell.windowClosed]); return ( diff --git a/src/App/styles.less b/src/App/styles.less index 50819f883..373b46900 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -151,14 +151,13 @@ svg { html { width: @html-width; height: @html-height; - font-family: 'PlusJakartaSans', 'sans-serif'; + font-family: 'PlusJakartaSans', 'Arial', 'Helvetica', 'sans-serif'; overflow: auto; overscroll-behavior: none; user-select: none; touch-action: manipulation; -webkit-tap-highlight-color: transparent; - @media (display-mode: standalone) { width: @html-standalone-width; height: @html-standalone-height; @@ -168,6 +167,7 @@ html { width: 100%; height: 100%; background: linear-gradient(41deg, var(--primary-background-color) 0%, var(--secondary-background-color) 100%); + -webkit-font-smoothing: antialiased; :global(#app) { position: relative; diff --git a/src/common/index.js b/src/common/index.js index 82f7a6a0c..55ccfe045 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -14,12 +14,13 @@ const languages = require('./languages'); const routesRegexp = require('./routesRegexp'); const useAnimationFrame = require('./useAnimationFrame'); const useBinaryState = require('./useBinaryState'); -const useFullscreen = require('./useFullscreen'); +const { default: useFullscreen } = require('./useFullscreen'); const useLiveRef = require('./useLiveRef'); const useModelState = require('./useModelState'); const useNotifications = require('./useNotifications'); const useOnScrollToBottom = require('./useOnScrollToBottom'); const useProfile = require('./useProfile'); +const { default: useSettings } = require('./useSettings'); const { default: useShell } = require('./useShell'); const useStreamingServer = require('./useStreamingServer'); const useTorrent = require('./useTorrent'); @@ -52,6 +53,7 @@ module.exports = { useNotifications, useOnScrollToBottom, useProfile, + useSettings, useShell, useStreamingServer, useTorrent, diff --git a/src/common/useFullscreen.js b/src/common/useFullscreen.js deleted file mode 100644 index caf7a0219..000000000 --- a/src/common/useFullscreen.js +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const React = require('react'); - -const useFullscreen = () => { - const [fullscreen, setFullscreen] = React.useState(document.fullscreenElement === document.documentElement); - const requestFullscreen = React.useCallback(() => { - document.documentElement.requestFullscreen(); - }, []); - const exitFullscreen = React.useCallback(() => { - document.exitFullscreen(); - }, []); - const toggleFullscreen = React.useCallback(() => { - if (fullscreen) { - exitFullscreen(); - } else { - requestFullscreen(); - } - }, [fullscreen]); - React.useEffect(() => { - const onFullscreenChange = () => { - setFullscreen(document.fullscreenElement === document.documentElement); - }; - document.addEventListener('fullscreenchange', onFullscreenChange); - return () => { - document.removeEventListener('fullscreenchange', onFullscreenChange); - }; - }, []); - return [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen]; -}; - -module.exports = useFullscreen; diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts new file mode 100644 index 000000000..b63fb9dd2 --- /dev/null +++ b/src/common/useFullscreen.ts @@ -0,0 +1,66 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +import { useCallback, useEffect, useState } from 'react'; +import useShell, { type WindowVisibility } from './useShell'; +import useSettings from './useSettings'; + +const useFullscreen = () => { + const shell = useShell(); + const [settings] = useSettings(); + + const [fullscreen, setFullscreen] = useState(false); + + const requestFullscreen = useCallback(() => { + if (shell.active) { + shell.send('win-set-visibility', { fullscreen: true }); + } else { + document.documentElement.requestFullscreen(); + } + }, []); + + const exitFullscreen = useCallback(() => { + if (shell.active) { + shell.send('win-set-visibility', { fullscreen: false }); + } else { + document.exitFullscreen(); + } + }, []); + + const toggleFullscreen = useCallback(() => { + fullscreen ? exitFullscreen() : requestFullscreen(); + }, [fullscreen]); + + useEffect(() => { + const onWindowVisibilityChanged = (state: WindowVisibility) => { + setFullscreen(state.isFullscreen === true); + }; + + const onFullscreenChange = () => { + setFullscreen(document.fullscreenElement === document.documentElement); + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.code === 'Escape' && settings.escExitFullscreen) { + exitFullscreen(); + } + + if (event.code === 'F11' && shell.active) { + toggleFullscreen(); + } + }; + + shell.on('win-visibility-changed', onWindowVisibilityChanged); + document.addEventListener('keydown', onKeyDown); + document.addEventListener('fullscreenchange', onFullscreenChange); + + return () => { + shell.off('win-visibility-changed', onWindowVisibilityChanged); + document.removeEventListener('keydown', onKeyDown); + document.removeEventListener('fullscreenchange', onFullscreenChange); + }; + }, [settings.escExitFullscreen]); + + return [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen]; +}; + +export default useFullscreen; diff --git a/src/routes/Player/useSettings.js b/src/common/useSettings.ts similarity index 54% rename from src/routes/Player/useSettings.js rename to src/common/useSettings.ts index e6976cd9d..16dc2cd2f 100644 --- a/src/routes/Player/useSettings.js +++ b/src/common/useSettings.ts @@ -1,13 +1,14 @@ -// Copyright (C) 2017-2023 Smart code 203358507 +// Copyright (C) 2017-2025 Smart code 203358507 -const React = require('react'); -const { useServices } = require('stremio/services'); -const { useProfile } = require('stremio/common'); +import { useCallback } from 'react'; +import { useServices } from 'stremio/services'; +import useProfile from './useProfile'; -const useSettings = () => { +const useSettings = (): [Settings, (settings: Settings) => void] => { const { core } = useServices(); const profile = useProfile(); - const updateSettings = React.useCallback((settings) => { + + const updateSettings = useCallback((settings: Settings) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -19,7 +20,8 @@ const useSettings = () => { } }); }, [profile]); + return [profile.settings, updateSettings]; }; -module.exports = useSettings; +export default useSettings; diff --git a/src/common/useShell.ts b/src/common/useShell.ts index 1a7bcb6ee..0471e38ab 100644 --- a/src/common/useShell.ts +++ b/src/common/useShell.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import EventEmitter from 'eventemitter3'; const SHELL_EVENT_OBJECT = 'transport'; @@ -17,9 +17,22 @@ type ShellEvent = { args: string[]; }; +export type WindowVisibility = { + visible: boolean; + visibility: number; + isFullscreen: boolean; +}; + +export type WindowState = { + state: number; +}; + const createId = () => Math.floor(Math.random() * 9999) + 1; const useShell = () => { + const [windowClosed, setWindowClosed] = useState(false); + const [windowHidden, setWindowHidden] = useState(false); + const on = (name: string, listener: (arg: any) => void) => { events.on(name, listener); }; @@ -28,7 +41,7 @@ const useShell = () => { events.off(name, listener); }; - const send = (method: string, ...args: (string | number)[]) => { + const send = (method: string, ...args: (string | number | object)[]) => { try { transport?.postMessage(JSON.stringify({ id: createId(), @@ -42,6 +55,24 @@ const useShell = () => { } }; + useEffect(() => { + const onWindowVisibilityChanged = (data: WindowVisibility) => { + setWindowClosed(data.visible === false && data.visibility === 0); + }; + + const onWindowStateChanged = (data: WindowState) => { + setWindowHidden(data.state === 9); + }; + + on('win-visibility-changed', onWindowVisibilityChanged); + on('win-state-changed', onWindowStateChanged); + + return () => { + off('win-visibility-changed', onWindowVisibilityChanged); + off('win-state-changed', onWindowStateChanged); + }; + }, []); + useEffect(() => { if (!transport) return; @@ -66,6 +97,8 @@ const useShell = () => { send, on, off, + windowClosed, + windowHidden, }; }; diff --git a/src/components/ContextMenu/ContextMenu.less b/src/components/ContextMenu/ContextMenu.less new file mode 100644 index 000000000..e38b7bad2 --- /dev/null +++ b/src/components/ContextMenu/ContextMenu.less @@ -0,0 +1,17 @@ +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.context-menu-container { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + + .context-menu { + position: fixed; + border-radius: var(--border-radius); + background-color: var(--modal-background-color); + box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40, + 0 1.1rem 0.85rem @color-background-dark5-20; + } +} \ No newline at end of file diff --git a/src/components/ContextMenu/ContextMenu.tsx b/src/components/ContextMenu/ContextMenu.tsx new file mode 100644 index 000000000..0f78454e8 --- /dev/null +++ b/src/components/ContextMenu/ContextMenu.tsx @@ -0,0 +1,101 @@ +import React, { memo, RefObject, useCallback, useEffect, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; +import styles from './ContextMenu.less'; + +const PADDING = 8; + +type Coordinates = [number, number]; +type Size = [number, number]; + +type Props = { + children: React.ReactNode, + on: RefObject[], + autoClose: boolean, +}; + +const ContextMenu = ({ children, on, autoClose }: Props) => { + const [active, setActive] = useState(false); + const [position, setPosition] = useState([0, 0]); + const [containerSize, setContainerSize] = useState([0, 0]); + + const ref = useCallback((element: HTMLDivElement) => { + element && setContainerSize([element.offsetWidth, element.offsetHeight]); + }, []); + + const style = useMemo(() => { + const [viewportWidth, viewportHeight] = [window.innerWidth, window.innerHeight]; + const [containerWidth, containerHeight] = containerSize; + const [x, y] = position; + + const left = Math.max( + PADDING, + Math.min( + x + containerWidth > viewportWidth - PADDING ? x - containerWidth : x, + viewportWidth - containerWidth - PADDING + ) + ); + + const top = Math.max( + PADDING, + Math.min( + y + containerHeight > viewportHeight - PADDING ? y - containerHeight : y, + viewportHeight - containerHeight - PADDING + ) + ); + + return { top, left }; + }, [position, containerSize]); + + const close = () => { + setPosition([0, 0]); + setActive(false); + }; + + const stopPropagation = (event: React.MouseEvent | React.TouchEvent) => { + event.stopPropagation(); + }; + + const onContextMenu = (event: MouseEvent) => { + event.preventDefault(); + + setPosition([event.clientX, event.clientY]); + setActive(true); + }; + + const handleKeyDown = useCallback((event: KeyboardEvent) => event.key === 'Escape' && close(), []); + + const onClick = useCallback(() => { + autoClose && close(); + }, [autoClose]); + + useEffect(() => { + on.forEach((ref) => ref.current && ref.current.addEventListener('contextmenu', onContextMenu)); + document.addEventListener('keydown', handleKeyDown); + + return () => { + on.forEach((ref) => ref.current && ref.current.removeEventListener('contextmenu', onContextMenu)); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [on]); + + return active && createPortal(( +
+
+ {children} +
+
+ ), document.body); +}; + +export default memo(ContextMenu); diff --git a/src/components/ContextMenu/index.ts b/src/components/ContextMenu/index.ts new file mode 100644 index 000000000..10ae5e5d7 --- /dev/null +++ b/src/components/ContextMenu/index.ts @@ -0,0 +1,2 @@ +import ContextMenu from './ContextMenu'; +export default ContextMenu; diff --git a/src/components/MetaPreview/styles.less b/src/components/MetaPreview/styles.less index 8006346f0..0725a384e 100644 --- a/src/components/MetaPreview/styles.less +++ b/src/components/MetaPreview/styles.less @@ -107,7 +107,9 @@ display: flex; flex-direction: row; align-items: center; - border-radius: 2.5rem; + border-radius: 0.5rem; + border: var(--focus-outline-size) solid transparent; + padding: 0rem 0.5rem; &:focus { outline: none; diff --git a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js index 0cedd8638..6be35cd5d 100644 --- a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js +++ b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js @@ -5,7 +5,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { Button, Image } = require('stremio/components'); -const useFullscreen = require('stremio/common/useFullscreen'); +const { default: useFullscreen } = require('stremio/common/useFullscreen'); const usePWA = require('stremio/common/usePWA'); const SearchBar = require('./SearchBar'); const NavMenu = require('./NavMenu'); diff --git a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js index 59946165a..de0a02212 100644 --- a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js +++ b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js @@ -7,7 +7,7 @@ const { useTranslation } = require('react-i18next'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { useServices } = require('stremio/services'); const { Button } = require('stremio/components'); -const useFullscreen = require('stremio/common/useFullscreen'); +const { default: useFullscreen } = require('stremio/common/useFullscreen'); const useProfile = require('stremio/common/useProfile'); const usePWA = require('stremio/common/usePWA'); const useTorrent = require('stremio/common/useTorrent'); diff --git a/src/components/index.ts b/src/components/index.ts index 7ef75f888..bd819f658 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -4,6 +4,7 @@ import Button from './Button'; import Checkbox from './Checkbox'; import Chips from './Chips'; import ColorInput from './ColorInput'; +import ContextMenu from './ContextMenu'; import ContinueWatchingItem from './ContinueWatchingItem'; import DelayedRenderer from './DelayedRenderer'; import EventModal from './EventModal'; @@ -35,6 +36,7 @@ export { Checkbox, Chips, ColorInput, + ContextMenu, ContinueWatchingItem, DelayedRenderer, EventModal, diff --git a/src/routes/Calendar/List/Item/Item.less b/src/routes/Calendar/List/Item/Item.less index b11feda2a..0cce6c582 100644 --- a/src/routes/Calendar/List/Item/Item.less +++ b/src/routes/Calendar/List/Item/Item.less @@ -82,6 +82,46 @@ } } + &.placeholder { + opacity: 0.7; + pointer-events: none; + + .text { + width: 8rem; + height: 1.2rem; + background-color: var(--overlay-color); + border-radius: 0.2rem; + } + + .video { + flex: none; + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 1rem; + height: 3rem; + padding: 0 1rem; + + .name { + flex: auto; + width: 12rem; + height: 1.2rem; + background-color: var(--overlay-color); + border-radius: 0.2rem; + } + + .info { + flex: none; + width: 4rem; + height: 1.2rem; + background-color: var(--overlay-color); + border-radius: 0.2rem; + } + } + } + &.today { .heading { background-color: var(--primary-accent-color); diff --git a/src/routes/Calendar/List/Item/ItemPlaceholder.tsx b/src/routes/Calendar/List/Item/ItemPlaceholder.tsx new file mode 100644 index 000000000..5d032e712 --- /dev/null +++ b/src/routes/Calendar/List/Item/ItemPlaceholder.tsx @@ -0,0 +1,23 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import React from 'react'; +import classNames from 'classnames'; +import styles from './Item.less'; + +const ItemPlaceholder = () => { + return ( +
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +export default ItemPlaceholder; diff --git a/src/routes/Calendar/List/Item/index.ts b/src/routes/Calendar/List/Item/index.ts index 1cf96beb5..1ab8e0585 100644 --- a/src/routes/Calendar/List/Item/index.ts +++ b/src/routes/Calendar/List/Item/index.ts @@ -1,5 +1,6 @@ // Copyright (C) 2017-2024 Smart code 203358507 import Item from './Item'; +import ItemPlaceholder from './ItemPlaceholder'; -export default Item; +export { Item, ItemPlaceholder }; diff --git a/src/routes/Calendar/List/List.tsx b/src/routes/Calendar/List/List.tsx index 13745e380..b07dfc828 100644 --- a/src/routes/Calendar/List/List.tsx +++ b/src/routes/Calendar/List/List.tsx @@ -1,7 +1,7 @@ // Copyright (C) 2017-2024 Smart code 203358507 import React, { useMemo } from 'react'; -import Item from './Item'; +import { Item, ItemPlaceholder } from './Item'; import styles from './List.less'; type Props = { @@ -20,16 +20,21 @@ const List = ({ items, selected, monthInfo, profile, onChange }: Props) => { return (
{ - filteredItems.map((item) => ( - - )) + items.length === 0 ? + [1, 2, 3].map((index) => ( + + )) + : + filteredItems.map((item) => ( + + )) }
); diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index abcdfb205..bab40a1c2 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -11,7 +11,7 @@ const useDiscover = require('./useDiscover'); const useSelectableInputs = require('./useSelectableInputs'); const styles = require('./styles'); -const SCROLL_TO_BOTTOM_TRESHOLD = 400; +const SCROLL_TO_BOTTOM_THRESHOLD = 400; const Discover = ({ urlParams, queryParams }) => { const { core } = useServices(); @@ -26,6 +26,15 @@ const Discover = ({ urlParams, queryParams }) => { metasContainerRef.current.scrollTop = 0; } }, [discover.catalog]); + React.useEffect(() => { + if (hasNextPage && metasContainerRef.current) { + const containerHeight = metasContainerRef.current.scrollHeight; + const viewportHeight = metasContainerRef.current.clientHeight; + if (containerHeight <= viewportHeight + SCROLL_TO_BOTTOM_THRESHOLD) { + loadNextPage(); + } + } + }, [hasNextPage, loadNextPage]); const selectedMetaItem = React.useMemo(() => { return discover.catalog !== null && discover.catalog.content.type === 'Ready' && @@ -76,7 +85,7 @@ const Discover = ({ urlParams, queryParams }) => { loadNextPage(); } }, [hasNextPage, loadNextPage]); - const onScroll = useOnScrollToBottom(onScrollToBottom, SCROLL_TO_BOTTOM_TRESHOLD); + const onScroll = useOnScrollToBottom(onScrollToBottom, SCROLL_TO_BOTTOM_THRESHOLD); React.useEffect(() => { closeInputsModal(); closeAddonModal(); diff --git a/src/routes/Player/BufferingLoader/BufferingLoader.js b/src/routes/Player/BufferingLoader/BufferingLoader.js index 2fea2f4a0..3d5664ae4 100644 --- a/src/routes/Player/BufferingLoader/BufferingLoader.js +++ b/src/routes/Player/BufferingLoader/BufferingLoader.js @@ -6,9 +6,9 @@ const classnames = require('classnames'); const { Image } = require('stremio/components'); const styles = require('./styles'); -const BufferingLoader = ({ className, logo }) => { +const BufferingLoader = React.forwardRef(({ className, logo }, ref) => { return ( -
+
{ />
); -}; +}); BufferingLoader.propTypes = { className: PropTypes.string, - logo: PropTypes.string + logo: PropTypes.string, }; module.exports = BufferingLoader; diff --git a/src/routes/Player/Error/Error.js b/src/routes/Player/Error/Error.js index c9a7c3bef..61db0e45f 100644 --- a/src/routes/Player/Error/Error.js +++ b/src/routes/Player/Error/Error.js @@ -8,7 +8,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react'); const { Button } = require('stremio/components'); const styles = require('./styles'); -const Error = ({ className, code, message, stream }) => { +const Error = React.forwardRef(({ className, code, message, stream }, ref) => { const { t } = useTranslation(); const [playlist, fileName] = React.useMemo(() => { @@ -19,7 +19,7 @@ const Error = ({ className, code, message, stream }) => { }, [stream]); return ( -
+
{message}
{ code === 2 ? @@ -44,7 +44,7 @@ const Error = ({ className, code, message, stream }) => { }
); -}; +}); Error.propTypes = { className: PropTypes.string, diff --git a/src/routes/Player/OptionsMenu/OptionsMenu.js b/src/routes/Player/OptionsMenu/OptionsMenu.js index 11738cb3e..56241e009 100644 --- a/src/routes/Player/OptionsMenu/OptionsMenu.js +++ b/src/routes/Player/OptionsMenu/OptionsMenu.js @@ -69,6 +69,7 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => { const onMouseDown = React.useCallback((event) => { event.nativeEvent.optionsMenuClosePrevented = true; }, []); + return (
{ @@ -112,7 +113,7 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => { OptionsMenu.propTypes = { className: PropTypes.string, stream: PropTypes.object, - playbackDevices: PropTypes.array + playbackDevices: PropTypes.array, }; module.exports = OptionsMenu; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index cd8a65a9a..9c9ae5e92 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -8,8 +8,8 @@ const langs = require('langs'); const { useTranslation } = require('react-i18next'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); -const { onFileDrop, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS } = require('stremio/common'); -const { HorizontalNavBar, Transition } = require('stremio/components'); +const { onFileDrop, useSettings, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell } = require('stremio/common'); +const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components'); const BufferingLoader = require('./BufferingLoader'); const VolumeChangeIndicator = require('./VolumeChangeIndicator'); const Error = require('./Error'); @@ -23,7 +23,6 @@ const SpeedMenu = require('./SpeedMenu'); const { default: SideDrawerButton } = require('./SideDrawerButton'); const { default: SideDrawer } = require('./SideDrawer'); const usePlayer = require('./usePlayer'); -const useSettings = require('./useSettings'); const useStatistics = require('./useStatistics'); const useVideo = require('./useVideo'); const styles = require('./styles'); @@ -31,7 +30,8 @@ const Video = require('./Video'); const Player = ({ urlParams, queryParams }) => { const { t } = useTranslation(); - const { chromecast, shell, core } = useServices(); + const services = useServices(); + const shell = useShell(); const forceTranscoding = React.useMemo(() => { return queryParams.has('forceTranscoding'); }, [queryParams]); @@ -47,8 +47,12 @@ const Player = ({ urlParams, queryParams }) => { const [seeking, setSeeking] = React.useState(false); const [casting, setCasting] = React.useState(() => { - return chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED; + return services.chromecast.active && services.chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED; }); + const playbackDevices = React.useMemo(() => streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : [], [streamingServer]); + + const bufferingRef = React.useRef(); + const errorRef = React.useRef(); const [immersed, setImmersed] = React.useState(true); const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []); @@ -317,8 +321,8 @@ const Player = ({ urlParams, queryParams }) => { null, seriesInfo: player.seriesInfo, }, { - chromecastTransport: chromecast.active ? chromecast.transport : null, - shellTransport: shell.active ? shell.transport : null, + chromecastTransport: services.chromecast.active ? services.chromecast.transport : null, + shellTransport: services.shell.active ? services.shell.transport : null, }); } }, [streamingServer.baseUrl, player.selected, forceTranscoding, casting]); @@ -439,12 +443,12 @@ const Player = ({ urlParams, queryParams }) => { const toastFilter = (item) => item?.dataset?.type === 'CoreEvent'; toast.addFilter(toastFilter); const onCastStateChange = () => { - setCasting(chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED); + setCasting(services.chromecast.active && services.chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED); }; const onChromecastServiceStateChange = () => { onCastStateChange(); - if (chromecast.active) { - chromecast.transport.on( + if (services.chromecast.active) { + services.chromecast.transport.on( cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChange ); @@ -455,15 +459,15 @@ const Player = ({ urlParams, queryParams }) => { onPauseRequested(); } }; - chromecast.on('stateChanged', onChromecastServiceStateChange); - core.transport.on('CoreEvent', onCoreEvent); + services.chromecast.on('stateChanged', onChromecastServiceStateChange); + services.core.transport.on('CoreEvent', onCoreEvent); onChromecastServiceStateChange(); return () => { toast.removeFilter(toastFilter); - chromecast.off('stateChanged', onChromecastServiceStateChange); - core.transport.off('CoreEvent', onCoreEvent); - if (chromecast.active) { - chromecast.transport.off( + services.chromecast.off('stateChanged', onChromecastServiceStateChange); + services.core.transport.off('CoreEvent', onCoreEvent); + if (services.chromecast.active) { + services.chromecast.transport.off( cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChange ); @@ -471,6 +475,12 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); + React.useEffect(() => { + if (settings.pauseOnMinimize && (shell.windowClosed || shell.windowHidden)) { + onPauseRequested(); + } + }, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]); + React.useLayoutEffect(() => { const onKeyDown = (event) => { switch (event.code) { @@ -561,6 +571,7 @@ const Player = ({ urlParams, queryParams }) => { } case 'Escape': { closeMenus(); + !settings.escExitFullscreen && window.history.back(); break; } } @@ -591,7 +602,7 @@ const Player = ({ urlParams, queryParams }) => { window.removeEventListener('keyup', onKeyUp); window.removeEventListener('wheel', onWheel); }; - }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, 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]); + }, [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]); React.useEffect(() => { video.events.on('error', onError); @@ -626,7 +637,7 @@ const Player = ({ urlParams, queryParams }) => { onMouseOver={onContainerMouseMove} onMouseLeave={onContainerMouseLeave}>
} + { + shell.active && +
+
+
{ t('SETTINGS_FULLSCREEN_EXIT') }
+
+ +
+ }
{ t('SETTINGS_BLUR_UNWATCHED_IMAGE') }
@@ -357,7 +370,7 @@ const Settings = () => {
{ t('SETTINGS_NAV_PLAYER') }
-
{t('SETTINGS_CLOSE_WINDOW')}
+
{t('SETTINGS_SECTION_SUBTITLES')}
@@ -368,20 +381,6 @@ const Settings = () => { {...subtitlesLanguageSelect} />
- { - shell.active ? -
-
-
{ t('SETTINGS_FULLSCREEN_EXIT') }
-
- -
- : - null - }
{ t('SETTINGS_SUBTITLES_SIZE') }
@@ -531,6 +530,18 @@ const Settings = () => { />
} + { + shell.active && +
+
+
{ t('SETTINGS_PAUSE_MINIMIZED') }
+
+ +
+ }
{ t('SETTINGS_NAV_STREAMING') }
diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index afad298ed..2a31fc254 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -339,6 +339,21 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); + const pauseOnMinimizeToggle = React.useMemo(() => ({ + checked: profile.settings.pauseOnMinimize, + onClick: () => { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'UpdateSettings', + args: { + ...profile.settings, + pauseOnMinimize: !profile.settings.pauseOnMinimize, + } + } + }); + } + }), [profile.settings]); return { interfaceLanguageSelect, hideSpoilersToggle, @@ -358,6 +373,7 @@ const useProfileSettingsInputs = (profile) => { bingeWatchingToggle, playInBackgroundToggle, hardwareDecodingToggle, + pauseOnMinimizeToggle, }; };