Skip to content

Commit 6370908

Browse files
authoredFeb 27, 2025··
Merge pull request #840 from Stremio/feat/shell-quit-on-close
App(Shell): add quit on close setting
2 parents 0a6d70f + 4b56ac4 commit 6370908

File tree

5 files changed

+119
-12
lines changed

5 files changed

+119
-12
lines changed
 

‎src/App/App.js

+25-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next');
66
const { Router } = require('stremio-router');
77
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
88
const { NotFound } = require('stremio/routes');
9-
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
9+
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender, useShell } = require('stremio/common');
1010
const ServicesToaster = require('./ServicesToaster');
1111
const DeepLinkHandler = require('./DeepLinkHandler');
1212
const SearchParamsHandler = require('./SearchParamsHandler');
@@ -20,6 +20,8 @@ const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router))
2020

2121
const App = () => {
2222
const { i18n } = useTranslation();
23+
const shell = useShell();
24+
const [windowHidden, setWindowHidden] = React.useState(false);
2325
const onPathNotMatch = React.useCallback(() => {
2426
return NotFound;
2527
}, []);
@@ -97,13 +99,29 @@ const App = () => {
9799
services.chromecast.off('stateChanged', onChromecastStateChange);
98100
};
99101
}, []);
102+
103+
// Handle shell window visibility changed event
104+
React.useEffect(() => {
105+
const onWindowVisibilityChanged = (state) => {
106+
setWindowHidden(state.visible === false && state.visibility === 0);
107+
};
108+
109+
shell.on('win-visibility-changed', onWindowVisibilityChanged);
110+
return () => shell.off('win-visibility-changed', onWindowVisibilityChanged);
111+
}, []);
112+
100113
React.useEffect(() => {
101114
const onCoreEvent = ({ event, args }) => {
102115
switch (event) {
103116
case 'SettingsUpdated': {
104117
if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') {
105118
i18n.changeLanguage(args.settings.interfaceLanguage);
106119
}
120+
121+
if (args?.settings?.quitOnClose && windowHidden) {
122+
shell.send('quit');
123+
}
124+
107125
break;
108126
}
109127
}
@@ -112,6 +130,10 @@ const App = () => {
112130
if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') {
113131
i18n.changeLanguage(state.profile.settings.interfaceLanguage);
114132
}
133+
134+
if (state?.profile?.settings?.quitOnClose && windowHidden) {
135+
shell.send('quit');
136+
}
115137
};
116138
const onWindowFocus = () => {
117139
services.core.transport.dispatch({
@@ -146,15 +168,15 @@ const App = () => {
146168
services.core.transport
147169
.getState('ctx')
148170
.then(onCtxState)
149-
.catch((e) => console.error(e));
171+
.catch(console.error);
150172
}
151173
return () => {
152174
if (services.core.active) {
153175
window.removeEventListener('focus', onWindowFocus);
154176
services.core.transport.off('CoreEvent', onCoreEvent);
155177
}
156178
};
157-
}, [initialized]);
179+
}, [initialized, windowHidden]);
158180
return (
159181
<React.StrictMode>
160182
<ServicesProvider services={services}>

‎src/common/useShell.ts

+56-8
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,69 @@
1+
import { useEffect } from 'react';
2+
import EventEmitter from 'eventemitter3';
3+
4+
const SHELL_EVENT_OBJECT = 'transport';
5+
const transport = globalThis?.qt?.webChannelTransport;
6+
const events = new EventEmitter();
7+
8+
enum ShellEventType {
9+
SIGNAL = 1,
10+
INVOKE_METHOD = 6,
11+
}
12+
13+
type ShellEvent = {
14+
id: number;
15+
type: ShellEventType;
16+
object: string;
17+
args: string[];
18+
};
19+
120
const createId = () => Math.floor(Math.random() * 9999) + 1;
221

322
const useShell = () => {
4-
const transport = globalThis?.qt?.webChannelTransport;
23+
const on = (name: string, listener: (arg: any) => void) => {
24+
events.on(name, listener);
25+
};
26+
27+
const off = (name: string, listener: (arg: any) => void) => {
28+
events.off(name, listener);
29+
};
530

631
const send = (method: string, ...args: (string | number)[]) => {
7-
transport?.send(JSON.stringify({
8-
id: createId(),
9-
type: 6,
10-
object: 'transport',
11-
method: 'onEvent',
12-
args: [method, ...args],
13-
}));
32+
try {
33+
transport?.send(JSON.stringify({
34+
id: createId(),
35+
type: ShellEventType.INVOKE_METHOD,
36+
object: SHELL_EVENT_OBJECT,
37+
method: 'onEvent',
38+
args: [method, ...args],
39+
}));
40+
} catch (e) {
41+
console.error('Shell', 'Failed to send event', e);
42+
}
1443
};
1544

45+
useEffect(() => {
46+
if (!transport) return;
47+
48+
transport.onmessage = ({ data }) => {
49+
try {
50+
const { type, args } = JSON.parse(data) as ShellEvent;
51+
52+
if (type === ShellEventType.SIGNAL) {
53+
const [methodName, methodArg] = args;
54+
events.emit(methodName, methodArg);
55+
}
56+
} catch (e) {
57+
console.error('Shell', 'Failed to handle event', e);
58+
}
59+
};
60+
}, []);
61+
1662
return {
1763
active: !!transport,
1864
send,
65+
on,
66+
off,
1967
};
2068
};
2169

‎src/routes/Settings/Settings.js

+14
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const Settings = () => {
4141
seekTimeDurationSelect,
4242
seekShortTimeDurationSelect,
4343
escExitFullscreenToggle,
44+
quitOnCloseToggle,
4445
playInExternalPlayerSelect,
4546
nextVideoPopupDurationSelect,
4647
bingeWatchingToggle,
@@ -322,6 +323,19 @@ const Settings = () => {
322323
{...interfaceLanguageSelect}
323324
/>
324325
</div>
326+
{
327+
shell.active &&
328+
<div className={styles['option-container']}>
329+
<div className={styles['option-name-container']}>
330+
<div className={styles['label']}>{ t('SETTINGS_QUIT_ON_CLOSE') }</div>
331+
</div>
332+
<Toggle
333+
className={classnames(styles['option-input-container'], styles['toggle-container'])}
334+
tabIndex={-1}
335+
{...quitOnCloseToggle}
336+
/>
337+
</div>
338+
}
325339
</div>
326340
<div ref={playerSectionRef} className={styles['section-container']}>
327341
<div className={styles['section-title']}>{ t('SETTINGS_NAV_PLAYER') }</div>

‎src/routes/Settings/useProfileSettingsInputs.js

+18
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ const useProfileSettingsInputs = (profile) => {
3131
});
3232
}
3333
}), [profile.settings]);
34+
35+
const quitOnCloseToggle = React.useMemo(() => ({
36+
checked: profile.settings.quitOnClose,
37+
onClick: () => {
38+
core.transport.dispatch({
39+
action: 'Ctx',
40+
args: {
41+
action: 'UpdateSettings',
42+
args: {
43+
...profile.settings,
44+
quitOnClose: !profile.settings.quitOnClose
45+
}
46+
}
47+
});
48+
}
49+
}), [profile.settings]);
50+
3451
const subtitlesLanguageSelect = React.useMemo(() => ({
3552
options: Object.keys(languageNames).map((code) => ({
3653
value: code,
@@ -316,6 +333,7 @@ const useProfileSettingsInputs = (profile) => {
316333
audioLanguageSelect,
317334
surroundSoundToggle,
318335
escExitFullscreenToggle,
336+
quitOnCloseToggle,
319337
seekTimeDurationSelect,
320338
seekShortTimeDurationSelect,
321339
playInExternalPlayerSelect,

‎src/types/global.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/* eslint-disable no-var */
22

3+
type QtTransportMessage = {
4+
data: string;
5+
};
6+
37
interface QtTransport {
48
send: (message: string) => void,
9+
onmessage: (message: QtTransportMessage) => void,
510
}
611

712
interface Qt {
@@ -12,4 +17,4 @@ declare global {
1217
var qt: Qt | undefined;
1318
}
1419

15-
export { };
20+
export {};

0 commit comments

Comments
 (0)
Please sign in to comment.