From a4ee4db1b879fd2bcbce87add19cc33168b42722 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 19 Feb 2025 11:05:09 +0100 Subject: [PATCH 1/5] feat: add quit on close setting for shell --- src/App/App.js | 12 ++++++++++-- src/routes/Settings/Settings.js | 14 ++++++++++++++ .../Settings/useProfileSettingsInputs.js | 18 ++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index 6dc2d6e0b..fa7e7a3e3 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next'); const { Router } = require('stremio-router'); const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); -const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common'); +const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender, useShell } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); const DeepLinkHandler = require('./DeepLinkHandler'); const SearchParamsHandler = require('./SearchParamsHandler'); @@ -20,6 +20,7 @@ const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router)) const App = () => { const { i18n } = useTranslation(); + const shell = useShell(); const onPathNotMatch = React.useCallback(() => { return NotFound; }, []); @@ -104,6 +105,9 @@ const App = () => { if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') { i18n.changeLanguage(args.settings.interfaceLanguage); } + if (args?.settings) { + shell.send('update_settings', args.settings); + } break; } } @@ -112,6 +116,10 @@ const App = () => { if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') { i18n.changeLanguage(state.profile.settings.interfaceLanguage); } + + if (state?.profile?.settings) { + shell.send('update_settings', state.profile.settings); + } }; const onWindowFocus = () => { services.core.transport.dispatch({ @@ -146,7 +154,7 @@ const App = () => { services.core.transport .getState('ctx') .then(onCtxState) - .catch((e) => console.error(e)); + .catch(console.error); } return () => { if (services.core.active) { diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 6ad15163a..312ad9e49 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -41,6 +41,7 @@ const Settings = () => { seekTimeDurationSelect, seekShortTimeDurationSelect, escExitFullscreenToggle, + quitOnCloseToggle, playInExternalPlayerSelect, nextVideoPopupDurationSelect, bingeWatchingToggle, @@ -322,6 +323,19 @@ const Settings = () => { {...interfaceLanguageSelect} /> + { + shell.active && +
+
+
{ t('SETTINGS_QUIT_ON_CLOSE') }
+
+ +
+ }
{ t('SETTINGS_NAV_PLAYER') }
diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index d36b169f9..c193c6eaf 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -31,6 +31,23 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); + + const quitOnCloseToggle = React.useMemo(() => ({ + checked: profile.settings.quitOnClose, + onClick: () => { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'UpdateSettings', + args: { + ...profile.settings, + quitOnClose: !profile.settings.quitOnClose + } + } + }); + } + }), [profile.settings]); + const subtitlesLanguageSelect = React.useMemo(() => ({ options: Object.keys(languageNames).map((code) => ({ value: code, @@ -316,6 +333,7 @@ const useProfileSettingsInputs = (profile) => { audioLanguageSelect, surroundSoundToggle, escExitFullscreenToggle, + quitOnCloseToggle, seekTimeDurationSelect, seekShortTimeDurationSelect, playInExternalPlayerSelect, From 31121aab21098408726c6c54946a2a0a7b95b903 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 19 Feb 2025 11:11:40 +0100 Subject: [PATCH 2/5] refactor(App): use dash for shell update settings message --- src/App/App.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index fa7e7a3e3..92240aa0d 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -106,7 +106,7 @@ const App = () => { i18n.changeLanguage(args.settings.interfaceLanguage); } if (args?.settings) { - shell.send('update_settings', args.settings); + shell.send('update-settings', args.settings); } break; } @@ -118,7 +118,7 @@ const App = () => { } if (state?.profile?.settings) { - shell.send('update_settings', state.profile.settings); + shell.send('update-settings', state.profile.settings); } }; const onWindowFocus = () => { From a19ef95723d98000d21bc0f446b446a3564978ee Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 21 Feb 2025 13:02:53 +0100 Subject: [PATCH 3/5] chore: update stremio-core-web --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 642d0ea73..dadfc5ec8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", - "@stremio/stremio-core-web": "0.48.5", + "@stremio/stremio-core-web": "0.49.0", "@stremio/stremio-icons": "5.4.1", "@stremio/stremio-video": "0.0.53", "a-color-picker": "1.2.1", @@ -3371,9 +3371,9 @@ "integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==" }, "node_modules/@stremio/stremio-core-web": { - "version": "0.48.5", - "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.48.5.tgz", - "integrity": "sha512-oDTNBrv8zZi1VGbeV+1Bm6CliI2rF23ERdJpz+gv8EnbFjRIo78WIsoS0yO0EOg8HHXYsFytPq5+c0+YlxmBlA==", + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.49.0.tgz", + "integrity": "sha512-oxJRVAE6z6Eh1B0qomdz6L2CVaTkwt70kDNC1TmHyGNo+Hhp2RaMlygqBKvBLXyHUXi82R67Mc11gT/JqlmaMw==", "license": "MIT", "dependencies": { "@babel/runtime": "7.24.1" diff --git a/package.json b/package.json index 9ccb0cee8..04a96a75e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", - "@stremio/stremio-core-web": "0.48.5", + "@stremio/stremio-core-web": "0.49.0", "@stremio/stremio-icons": "5.4.1", "@stremio/stremio-video": "0.0.53", "a-color-picker": "1.2.1", From 3bef434f42069a17c6960e37adb4264f0e2a926d Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 24 Feb 2025 14:35:39 +0100 Subject: [PATCH 4/5] refactor: update quit on close logic --- src/App/App.js | 24 +++++++++++---- src/common/useShell.ts | 68 ++++++++++++++++++++++++++++++++++++------ src/types/global.d.ts | 7 ++++- 3 files changed, 84 insertions(+), 15 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index 92240aa0d..b6031be6e 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -21,6 +21,7 @@ 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; }, []); @@ -98,6 +99,17 @@ const App = () => { services.chromecast.off('stateChanged', onChromecastStateChange); }; }, []); + + // Handle shell window visibility changed event + React.useEffect(() => { + const onWindowVisibilityChanged = (state) => { + setWindowHidden(state.visible === false && state.visibility === 0); + }; + + shell.on('win-visibility-changed', onWindowVisibilityChanged); + return () => shell.off('win-visibility-changed', onWindowVisibilityChanged); + }, []); + React.useEffect(() => { const onCoreEvent = ({ event, args }) => { switch (event) { @@ -105,9 +117,11 @@ const App = () => { if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') { i18n.changeLanguage(args.settings.interfaceLanguage); } - if (args?.settings) { - shell.send('update-settings', args.settings); + + if (args?.settings?.quitOnClose && windowHidden) { + shell.send('quit'); } + break; } } @@ -117,8 +131,8 @@ const App = () => { i18n.changeLanguage(state.profile.settings.interfaceLanguage); } - if (state?.profile?.settings) { - shell.send('update-settings', state.profile.settings); + if (state?.profile?.settings?.quitOnClose && windowHidden) { + shell.send('quit'); } }; const onWindowFocus = () => { @@ -162,7 +176,7 @@ const App = () => { services.core.transport.off('CoreEvent', onCoreEvent); } }; - }, [initialized]); + }, [initialized, windowHidden]); return ( diff --git a/src/common/useShell.ts b/src/common/useShell.ts index 5e61bfe84..f4700d779 100644 --- a/src/common/useShell.ts +++ b/src/common/useShell.ts @@ -1,21 +1,71 @@ +import { useEffect } from 'react'; +import EventEmitter from 'eventemitter3'; + +const SHELL_EVENT_OBJECT = 'transport'; +const transport = globalThis?.qt?.webChannelTransport; +const events = new EventEmitter(); + +enum ShellEventType { + SIGNAL = 1, + INVOKE_METHOD = 6, +} + +type ShellEvent = { + id: number; + type: ShellEventType; + object: string; + args: string[]; +}; + + + const createId = () => Math.floor(Math.random() * 9999) + 1; const useShell = () => { - const transport = globalThis?.qt?.webChannelTransport; - const send = (method: string, ...args: (string | number)[]) => { - transport?.send(JSON.stringify({ - id: createId(), - type: 6, - object: 'transport', - method: 'onEvent', - args: [method, ...args], - })); + try { + transport?.send(JSON.stringify({ + id: createId(), + type: ShellEventType.INVOKE_METHOD, + object: SHELL_EVENT_OBJECT, + method: 'onEvent', + args: [method, ...args], + })); + } catch(e) { + console.error('Shell', 'Failed to send event', e); + } }; + const on = (name: string, listener: (arg: any) => void) => { + events.on(name, listener); + }; + + const off = (name: string, listener: (arg: any) => void) => { + events.off(name, listener); + }; + + useEffect(() => { + if (!transport) return; + + transport.onmessage = ({ data }) => { + try { + const { type, args } = JSON.parse(data) as ShellEvent; + + if (type === ShellEventType.SIGNAL) { + const [methodName, methodArg] = args; + events.emit(methodName, methodArg); + } + } catch (e) { + console.error('Shell', 'Failed to handle event', e); + } + }; + }, []); + return { active: !!transport, send, + on, + off, }; }; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 5effeffd4..7a50d7432 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,7 +1,12 @@ /* eslint-disable no-var */ +type QtTransportMessage = { + data: string; +}; + interface QtTransport { send: (message: string) => void, + onmessage: (message: QtTransportMessage) => void, } interface Qt { @@ -12,4 +17,4 @@ declare global { var qt: Qt | undefined; } -export { }; +export {} \ No newline at end of file From 4b56ac44c2bb4052c867cb4bbf489e73cd12e978 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 24 Feb 2025 14:40:32 +0100 Subject: [PATCH 5/5] style: code format --- src/App/App.js | 2 +- src/common/useShell.ts | 20 +++++++++----------- src/types/global.d.ts | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index b6031be6e..d3a1ce188 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -100,7 +100,7 @@ const App = () => { }; }, []); - // Handle shell window visibility changed event + // Handle shell window visibility changed event React.useEffect(() => { const onWindowVisibilityChanged = (state) => { setWindowHidden(state.visible === false && state.visibility === 0); diff --git a/src/common/useShell.ts b/src/common/useShell.ts index f4700d779..7baab60bc 100644 --- a/src/common/useShell.ts +++ b/src/common/useShell.ts @@ -17,11 +17,17 @@ type ShellEvent = { args: string[]; }; - - const createId = () => Math.floor(Math.random() * 9999) + 1; const useShell = () => { + const on = (name: string, listener: (arg: any) => void) => { + events.on(name, listener); + }; + + const off = (name: string, listener: (arg: any) => void) => { + events.off(name, listener); + }; + const send = (method: string, ...args: (string | number)[]) => { try { transport?.send(JSON.stringify({ @@ -31,19 +37,11 @@ const useShell = () => { method: 'onEvent', args: [method, ...args], })); - } catch(e) { + } catch (e) { console.error('Shell', 'Failed to send event', e); } }; - const on = (name: string, listener: (arg: any) => void) => { - events.on(name, listener); - }; - - const off = (name: string, listener: (arg: any) => void) => { - events.off(name, listener); - }; - useEffect(() => { if (!transport) return; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 7a50d7432..d1d601d5e 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -17,4 +17,4 @@ declare global { var qt: Qt | undefined; } -export {} \ No newline at end of file +export {};