mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-21 15:52:02 +00:00
Merge pull request #840 from Stremio/feat/shell-quit-on-close
App(Shell): add quit on close setting
This commit is contained in:
commit
6370908f93
5 changed files with 119 additions and 12 deletions
|
|
@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next');
|
||||||
const { Router } = require('stremio-router');
|
const { Router } = require('stremio-router');
|
||||||
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
|
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
|
||||||
const { NotFound } = require('stremio/routes');
|
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 ServicesToaster = require('./ServicesToaster');
|
||||||
const DeepLinkHandler = require('./DeepLinkHandler');
|
const DeepLinkHandler = require('./DeepLinkHandler');
|
||||||
const SearchParamsHandler = require('./SearchParamsHandler');
|
const SearchParamsHandler = require('./SearchParamsHandler');
|
||||||
|
|
@ -20,6 +20,8 @@ const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router))
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
const shell = useShell();
|
||||||
|
const [windowHidden, setWindowHidden] = React.useState(false);
|
||||||
const onPathNotMatch = React.useCallback(() => {
|
const onPathNotMatch = React.useCallback(() => {
|
||||||
return NotFound;
|
return NotFound;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -97,6 +99,17 @@ const App = () => {
|
||||||
services.chromecast.off('stateChanged', onChromecastStateChange);
|
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(() => {
|
React.useEffect(() => {
|
||||||
const onCoreEvent = ({ event, args }) => {
|
const onCoreEvent = ({ event, args }) => {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
|
@ -104,6 +117,11 @@ const App = () => {
|
||||||
if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') {
|
if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') {
|
||||||
i18n.changeLanguage(args.settings.interfaceLanguage);
|
i18n.changeLanguage(args.settings.interfaceLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args?.settings?.quitOnClose && windowHidden) {
|
||||||
|
shell.send('quit');
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +130,10 @@ const App = () => {
|
||||||
if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') {
|
if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') {
|
||||||
i18n.changeLanguage(state.profile.settings.interfaceLanguage);
|
i18n.changeLanguage(state.profile.settings.interfaceLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state?.profile?.settings?.quitOnClose && windowHidden) {
|
||||||
|
shell.send('quit');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const onWindowFocus = () => {
|
const onWindowFocus = () => {
|
||||||
services.core.transport.dispatch({
|
services.core.transport.dispatch({
|
||||||
|
|
@ -146,7 +168,7 @@ const App = () => {
|
||||||
services.core.transport
|
services.core.transport
|
||||||
.getState('ctx')
|
.getState('ctx')
|
||||||
.then(onCtxState)
|
.then(onCtxState)
|
||||||
.catch((e) => console.error(e));
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (services.core.active) {
|
if (services.core.active) {
|
||||||
|
|
@ -154,7 +176,7 @@ const App = () => {
|
||||||
services.core.transport.off('CoreEvent', onCoreEvent);
|
services.core.transport.off('CoreEvent', onCoreEvent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [initialized]);
|
}, [initialized, windowHidden]);
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ServicesProvider services={services}>
|
<ServicesProvider services={services}>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,69 @@
|
||||||
|
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 createId = () => Math.floor(Math.random() * 9999) + 1;
|
||||||
|
|
||||||
const useShell = () => {
|
const useShell = () => {
|
||||||
const transport = globalThis?.qt?.webChannelTransport;
|
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)[]) => {
|
const send = (method: string, ...args: (string | number)[]) => {
|
||||||
transport?.send(JSON.stringify({
|
try {
|
||||||
id: createId(),
|
transport?.send(JSON.stringify({
|
||||||
type: 6,
|
id: createId(),
|
||||||
object: 'transport',
|
type: ShellEventType.INVOKE_METHOD,
|
||||||
method: 'onEvent',
|
object: SHELL_EVENT_OBJECT,
|
||||||
args: [method, ...args],
|
method: 'onEvent',
|
||||||
}));
|
args: [method, ...args],
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Shell', 'Failed to send event', e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
active: !!transport,
|
active: !!transport,
|
||||||
send,
|
send,
|
||||||
|
on,
|
||||||
|
off,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ const Settings = () => {
|
||||||
seekTimeDurationSelect,
|
seekTimeDurationSelect,
|
||||||
seekShortTimeDurationSelect,
|
seekShortTimeDurationSelect,
|
||||||
escExitFullscreenToggle,
|
escExitFullscreenToggle,
|
||||||
|
quitOnCloseToggle,
|
||||||
playInExternalPlayerSelect,
|
playInExternalPlayerSelect,
|
||||||
nextVideoPopupDurationSelect,
|
nextVideoPopupDurationSelect,
|
||||||
bingeWatchingToggle,
|
bingeWatchingToggle,
|
||||||
|
|
@ -322,6 +323,19 @@ const Settings = () => {
|
||||||
{...interfaceLanguageSelect}
|
{...interfaceLanguageSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
shell.active &&
|
||||||
|
<div className={styles['option-container']}>
|
||||||
|
<div className={styles['option-name-container']}>
|
||||||
|
<div className={styles['label']}>{ t('SETTINGS_QUIT_ON_CLOSE') }</div>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||||
|
tabIndex={-1}
|
||||||
|
{...quitOnCloseToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div ref={playerSectionRef} className={styles['section-container']}>
|
<div ref={playerSectionRef} className={styles['section-container']}>
|
||||||
<div className={styles['section-title']}>{ t('SETTINGS_NAV_PLAYER') }</div>
|
<div className={styles['section-title']}>{ t('SETTINGS_NAV_PLAYER') }</div>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,23 @@ const useProfileSettingsInputs = (profile) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}), [profile.settings]);
|
}), [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(() => ({
|
const subtitlesLanguageSelect = React.useMemo(() => ({
|
||||||
options: Object.keys(languageNames).map((code) => ({
|
options: Object.keys(languageNames).map((code) => ({
|
||||||
value: code,
|
value: code,
|
||||||
|
|
@ -316,6 +333,7 @@ const useProfileSettingsInputs = (profile) => {
|
||||||
audioLanguageSelect,
|
audioLanguageSelect,
|
||||||
surroundSoundToggle,
|
surroundSoundToggle,
|
||||||
escExitFullscreenToggle,
|
escExitFullscreenToggle,
|
||||||
|
quitOnCloseToggle,
|
||||||
seekTimeDurationSelect,
|
seekTimeDurationSelect,
|
||||||
seekShortTimeDurationSelect,
|
seekShortTimeDurationSelect,
|
||||||
playInExternalPlayerSelect,
|
playInExternalPlayerSelect,
|
||||||
|
|
|
||||||
7
src/types/global.d.ts
vendored
7
src/types/global.d.ts
vendored
|
|
@ -1,7 +1,12 @@
|
||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
|
|
||||||
|
type QtTransportMessage = {
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface QtTransport {
|
interface QtTransport {
|
||||||
send: (message: string) => void,
|
send: (message: string) => void,
|
||||||
|
onmessage: (message: QtTransportMessage) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Qt {
|
interface Qt {
|
||||||
|
|
@ -12,4 +17,4 @@ declare global {
|
||||||
var qt: Qt | undefined;
|
var qt: Qt | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { };
|
export {};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue