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/common/useFullscreen.ts b/src/common/useFullscreen.ts index d81003266..b63fb9dd2 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -1,7 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 import { useCallback, useEffect, useState } from 'react'; -import useShell, { type WindowVisibilityState } from './useShell'; +import useShell, { type WindowVisibility } from './useShell'; import useSettings from './useSettings'; const useFullscreen = () => { @@ -31,7 +31,7 @@ const useFullscreen = () => { }, [fullscreen]); useEffect(() => { - const onWindowVisibilityChanged = (state: WindowVisibilityState) => { + const onWindowVisibilityChanged = (state: WindowVisibility) => { setFullscreen(state.isFullscreen === true); }; diff --git a/src/common/useShell.ts b/src/common/useShell.ts index 7ef7ce0a4..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,13 +17,22 @@ type ShellEvent = { args: string[]; }; -export type WindowVisibilityState = { +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); }; @@ -46,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; @@ -70,6 +97,8 @@ const useShell = () => { send, on, off, + windowClosed, + windowHidden, }; }; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index e631e3000..9c9ae5e92 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -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 } = require('stremio/common'); +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'); @@ -30,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]); @@ -46,7 +47,7 @@ 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]); @@ -320,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]); @@ -442,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 ); @@ -458,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 ); @@ -474,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) { diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 1d695c177..d6fe9b7a4 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -48,6 +48,7 @@ const Settings = () => { bingeWatchingToggle, playInBackgroundToggle, hardwareDecodingToggle, + pauseOnMinimizeToggle, } = useProfileSettingsInputs(profile); const { streamingServerRemoteUrlInput, @@ -529,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, }; };