Merge pull request #1271 from Stremio/refactor/shell-logic

Dev: Move shell logic to provider
This commit is contained in:
Tim 2026-05-15 14:13:22 +02:00 committed by GitHub
commit 24eabde85f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 226 additions and 391 deletions

View file

@ -5,9 +5,9 @@ const React = require('react');
const { useTranslation } = require('react-i18next');
const { useCore } = require('stremio/core');
const { Router } = require('stremio-router');
const { Shell, Chromecast, ServicesProvider, GamepadProvider } = require('stremio/services');
const { Chromecast, ServicesProvider, GamepadProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
const { FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender, onFileDrop } = require('stremio/common');
const { FullscreenProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useBinaryState, useProfile, withCoreSuspender, onFileDrop, usePlatform } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
const DeepLinkHandler = require('./DeepLinkHandler');
const SearchParamsHandler = require('./SearchParamsHandler');
@ -24,14 +24,13 @@ const App = () => {
const core = useCore();
const profile = useProfile();
const { i18n } = useTranslation();
const shell = useShell();
const { shell } = usePlatform();
const [gamepadSupportEnabled, setGamepadSupportEnabled] = React.useState(false);
const onPathNotMatch = React.useCallback(() => {
return NotFound;
}, []);
const services = React.useMemo(() => {
return {
shell: new Shell(),
chromecast: new Chromecast(),
};
}, []);
@ -99,11 +98,9 @@ const App = () => {
}
};
services.chromecast.on('stateChanged', onChromecastStateChange);
services.shell.start();
services.chromecast.start();
window.services = services;
return () => {
services.shell.stop();
services.chromecast.stop();
services.chromecast.off('stateChanged', onChromecastStateChange);
};
@ -143,10 +140,10 @@ const App = () => {
setGamepadSupportEnabled(profile.settings.gamepadSupport);
}
if (profile.settings?.quitOnClose && shell.windowClosed) {
if (profile.settings?.quitOnClose && shell.state.windowClosed) {
shell.send('quit');
}
}, [profile.settings, shell.windowClosed]);
}, [profile.settings, shell.state.windowClosed]);
React.useEffect(() => {
const onWindowFocus = () => {
@ -187,33 +184,31 @@ const App = () => {
return (
<ServicesProvider services={services}>
<PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<GamepadProvider enabled={gamepadSupportEnabled} onGuide={toggleGamepadModal}>
<ShortcutsProvider onShortcut={onShortcut}>
<FullscreenProvider>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
{
gamepadModalOpen && <GamepadModal onClose={closeGamepadModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</FullscreenProvider>
</ShortcutsProvider>
</GamepadProvider>
</TooltipProvider>
</ToastProvider>
</PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<GamepadProvider enabled={gamepadSupportEnabled} onGuide={toggleGamepadModal}>
<ShortcutsProvider onShortcut={onShortcut}>
<FullscreenProvider>
{
shortcutModalOpen && <ShortcutsModal onClose={closeShortcutsModal}/>
}
{
gamepadModalOpen && <GamepadModal onClose={closeGamepadModal}/>
}
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</FullscreenProvider>
</ShortcutsProvider>
</GamepadProvider>
</TooltipProvider>
</ToastProvider>
</ServicesProvider>
);
};

View file

@ -1,8 +1,7 @@
import React, { useEffect } from 'react';
import Icon from '@stremio/stremio-icons/react';
import { useTranslation } from 'react-i18next';
import { useServices } from 'stremio/services';
import { useBinaryState, useShell } from 'stremio/common';
import { useBinaryState, usePlatform } from 'stremio/common';
import { Button, Transition } from 'stremio/components';
import styles from './UpdaterBanner.less';
@ -12,19 +11,18 @@ type Props = {
const UpdaterBanner = ({ className }: Props) => {
const { t } = useTranslation();
const { shell } = useServices();
const shellTransport = useShell();
const { shell } = usePlatform();
const [visible, show, hide] = useBinaryState(false);
const onInstallClick = () => {
shellTransport.send('autoupdater-notif-clicked');
shell.send('autoupdater-notif-clicked');
};
useEffect(() => {
shell.transport && shell.transport.on('autoupdater-show-notif', show);
shell.on('autoupdater-show-notif', show);
return () => {
shell.transport && shell.transport.off('autoupdater-show-notif', show);
shell.off('autoupdater-show-notif', show);
};
}, []);

View file

@ -4,8 +4,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { withCoreSuspender } from '../CoreSuspender';
import onShortcut from '../Shortcuts/onShortcut';
import useSettings from '../useSettings';
import useShell, { type WindowVisibility } from '../useShell';
import FullscreenContext, { type FullscreenContextValue } from './FullscreenContext';
import { usePlatform } from '../Platform';
type Props = {
children: React.ReactNode,
@ -15,7 +15,7 @@ const hasWebkitFullscreen = typeof HTMLVideoElement !== 'undefined' &&
typeof HTMLVideoElement.prototype.webkitEnterFullscreen === 'function';
const FullscreenProvider = ({ children }: Props) => {
const shell = useShell();
const { shell } = usePlatform();
const [settings] = useSettings();
const escExitFullscreen = settings.escExitFullscreen;

View file

@ -1,10 +1,12 @@
import React, { createContext, useContext } from 'react';
import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
import { name, isMobile } from './device';
import useShell from './shell/useShell';
interface PlatformContext {
name: string;
isMobile: boolean;
shell: Shell;
openExternal: (url: string) => void;
}
@ -15,6 +17,8 @@ type Props = {
};
const PlatformProvider = ({ children }: Props) => {
const shell = useShell();
const openExternal = (url: string) => {
try {
const { hostname } = new URL(url);
@ -30,7 +34,7 @@ const PlatformProvider = ({ children }: Props) => {
};
return (
<PlatformContext.Provider value={{ openExternal, name, isMobile }}>
<PlatformContext.Provider value={{ openExternal, shell, name, isMobile }}>
{children}
</PlatformContext.Provider>
);

View file

@ -3,7 +3,6 @@ type QtTransportMessage = {
};
interface QtTransport {
send: (message: string) => void,
onmessage: (message: QtTransportMessage) => void,
}
@ -22,8 +21,8 @@ interface Chrome {
}
declare global {
var qt: Qt | undefined;
var qt: Qt;
var chrome: Chrome | undefined;
}
export { };
export {};

27
src/common/Platform/shell/shell.d.ts vendored Normal file
View file

@ -0,0 +1,27 @@
type WindowVisibility = {
visible: boolean;
visibility: number;
isFullscreen: boolean;
};
type WindowState = {
state: number;
};
type MediaStatus = {
paused: boolean;
};
interface Shell {
active: boolean,
state: ShellState,
on: (name: string, listener: (arg: any) => void) => void;
off: (name: string, listener: (arg: any) => void) => void;
send: (method: string, ...args: (string | number | object)[]) => void;
}
type ShellState = {
version: string | null;
windowClosed: boolean;
windowHidden: boolean;
};

View file

@ -0,0 +1,124 @@
import { useEffect, useState } from 'react';
import EventEmitter from 'eventemitter3';
const IPC = globalThis?.chrome?.webview;
const LEGACY_IPC = globalThis?.qt?.webChannelTransport;
if (LEGACY_IPC) LEGACY_IPC.onmessage = () => { /* empty */ };
const events = new EventEmitter();
enum ShellEventType {
SIGNAL = 1,
INIT = 3,
INVOKE_METHOD = 6,
}
type ShellEvent = {
id: number;
type: ShellEventType;
};
type ShellEventInit = ShellEvent & {
data: {
transport: {
properties: string[][],
}
};
};
type ShellEventSignal = ShellEvent & {
args: string[];
};
type ShellMessage = {
data: string;
};
const useShell = (): Shell => {
const [state, setState] = useState<ShellState>({
version: null,
windowClosed: false,
windowHidden: false,
});
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 | object)[]) => {
try {
IPC?.postMessage(JSON.stringify({
id: 0,
type: ShellEventType.INVOKE_METHOD,
args: [method, ...args],
}));
} catch (e) {
console.error('Shell', 'Failed to send event', e);
}
};
useEffect(() => {
const onWindowVisibilityChanged = (data: WindowVisibility) => {
setState((state) => ({
...state,
windowClosed: data.visible === false && data.visibility === 0,
}));
};
const onWindowStateChanged = (data: WindowState) => {
setState((state) => ({
...state,
windowHidden: 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(() => {
IPC?.postMessage(JSON.stringify({
id: 0,
type: ShellEventType.INIT,
}));
const onMessage = (message: ShellMessage) => {
try {
const event = JSON.parse(message.data) as ShellEvent;
if (event.type === ShellEventType.INIT) {
const { data } = event as ShellEventInit;
const [, [,,, version]] = data.transport.properties;
setState((state) => ({ ...state, version }));
send('app-ready');
}
if (event.type === ShellEventType.SIGNAL) {
const { args } = event as ShellEventSignal;
const [methodName, methodArg] = args;
events.emit(methodName, methodArg);
}
} catch (e) {
console.error('Shell', 'Failed to handle event', e);
}
};
IPC?.addEventListener('message', onMessage);
return () => IPC?.removeEventListener('message', onMessage);
}, []);
return {
active: !!IPC,
send,
on,
off,
state,
};
};
export default useShell;

View file

@ -22,7 +22,6 @@ 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 { default: useTimeout } = require('./useTimeout');
const { default: usePlayUrl } = require('./usePlayUrl');
@ -63,7 +62,6 @@ module.exports = {
useOnScrollToBottom,
useProfile,
useSettings,
useShell,
useStreamingServer,
useTimeout,
usePlayUrl,

View file

@ -1,109 +0,0 @@
import { useEffect, useState } from 'react';
import EventEmitter from 'eventemitter3';
const SHELL_EVENT_OBJECT = 'transport';
const transport = globalThis?.chrome?.webview;
const events = new EventEmitter();
enum ShellEventType {
SIGNAL = 1,
INVOKE_METHOD = 6,
}
type ShellEvent = {
id: number;
type: ShellEventType;
object: string;
args: string[];
};
export type WindowVisibility = {
visible: boolean;
visibility: number;
isFullscreen: boolean;
};
export type WindowState = {
state: number;
};
export type MediaStatus = {
paused: boolean;
};
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);
};
const off = (name: string, listener: (arg: any) => void) => {
events.off(name, listener);
};
const send = (method: string, ...args: (string | number | object)[]) => {
try {
transport?.postMessage(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);
}
};
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;
const onMessage = ({ data }: { data: string }) => {
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);
}
};
transport.addEventListener('message', onMessage);
return () => transport.removeEventListener('message', onMessage);
}, []);
return {
active: !!transport,
send,
on,
off,
windowClosed,
windowHidden,
};
};
export default useShell;

View file

@ -18,7 +18,7 @@ const { initReactI18next } = require('react-i18next');
const stremioTranslations = require('stremio-translations');
const App = require('./App');
const { CoreProvider } = require('./core');
const { FileDropProvider } = require('./common');
const { FileDropProvider, PlatformProvider } = require('./common');
const translations = Object.fromEntries(Object.entries(stremioTranslations()).map(([key, value]) => [key, {
translation: value
@ -42,11 +42,13 @@ const appInfo = {
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(
<CoreProvider appInfo={appInfo}>
<FileDropProvider>
<App />
</FileDropProvider>
</CoreProvider>
<PlatformProvider>
<CoreProvider appInfo={appInfo}>
<FileDropProvider>
<App />
</FileDropProvider>
</CoreProvider>
</PlatformProvider>
);
if (process.env.NODE_ENV === 'production' && process.env.SERVICE_WORKER_DISABLED !== 'true' && process.env.SERVICE_WORKER_DISABLED !== true && 'serviceWorker' in navigator) {

View file

@ -5,12 +5,12 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const debounce = require('lodash.debounce');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { usePlatform } = require('stremio/common');
const { Slider } = require('stremio/components');
const styles = require('./styles');
const VolumeSlider = ({ className, volume, onVolumeChangeRequested, muted }) => {
const { shell } = useServices();
const { shell } = usePlatform();
const disabled = volume === null || isNaN(volume);
const routeFocused = useRouteFocused();
const [slidingVolume, setSlidingVolume] = React.useState(null);

View file

@ -10,7 +10,7 @@ const { useRouteFocused } = require('stremio-router');
const { useCore } = require('stremio/core');
const { useServices, useGamepad } = require('stremio/services');
const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation');
const { useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, useShell, usePlatform, onShortcut } = require('stremio/common');
const { useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, usePlatform, onShortcut } = require('stremio/common');
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
@ -42,7 +42,6 @@ const Player = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
const services = useServices();
const core = useCore();
const shell = useShell();
const gamepad = useGamepad();
const forceTranscoding = React.useMemo(() => {
return queryParams.has('forceTranscoding');
@ -424,7 +423,7 @@ const Player = ({ urlParams, queryParams }) => {
seriesInfo: player.seriesInfo,
}, {
chromecastTransport: services.chromecast.active ? services.chromecast.transport : null,
shellTransport: services.shell.active ? services.shell.transport : null,
shellTransport: platform.shell.active ? platform.shell : null,
});
}
}, [streamingServer.baseUrl, player.selected, player.stream, streamSubtitles, forceTranscoding, casting]);
@ -536,10 +535,10 @@ const Player = ({ urlParams, queryParams }) => {
}, []);
React.useEffect(() => {
if (settings.pauseOnMinimize && (shell.windowClosed || shell.windowHidden)) {
if (settings.pauseOnMinimize && (platform.shell.state.windowClosed || platform.shell.state.windowHidden)) {
onPauseRequested();
}
}, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]);
}, [settings.pauseOnMinimize, platform.shell.state.windowClosed, platform.shell.state.windowHidden]);
useMediaSession(video.state, player, fullscreen, onPlayRequested, onPauseRequested, onNextVideoRequested);
@ -565,8 +564,8 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
};
shell.on('media-key', onMediaKey);
return () => shell.off('media-key', onMediaKey);
platform.shell.on('media-key', onMediaKey);
return () => platform.shell.off('media-key', onMediaKey);
}, [video.state.paused, player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]);
onShortcut('seekForward', (combo) => {

View file

@ -1,6 +1,5 @@
import { useEffect } from 'react';
import { useShell } from 'stremio/common';
import { MediaStatus } from 'stremio/common/useShell';
import { usePlatform } from 'stremio/common';
const useMediaSession = (
videoState: VideoState,
@ -10,7 +9,7 @@ const useMediaSession = (
onPauseRequested: () => void,
onNextVideoRequested: () => void,
) => {
const shell = useShell();
const { shell } = usePlatform();
useEffect(() => {
if (!('audioSession' in navigator)) return;

View file

@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useServices } from 'stremio/services';
import { usePlatform } from 'stremio/common';
import { Option, Section } from '../components';
import styles from './Info.less';
@ -9,7 +9,7 @@ type Props = {
};
const Info = ({ streamingServer }: Props) => {
const { shell } = useServices();
const { shell } = usePlatform();
const { t } = useTranslation();
const settings = useMemo(() => (
@ -38,10 +38,10 @@ const Info = ({ streamingServer }: Props) => {
</Option>
}
{
typeof shell?.transport?.props?.shellVersion === 'string' &&
typeof shell.state.version === 'string' &&
<Option label={t('SETTINGS_SHELL_VERSION')}>
<div className={styles['label']}>
{shell.transport.props.shellVersion}
{shell.state.version}
</div>
</Option>
}

View file

@ -1,5 +1,5 @@
import React, { forwardRef } from 'react';
import { useServices } from 'stremio/services';
import { usePlatform } from 'stremio/common';
import { MultiselectMenu, Toggle } from 'stremio/components';
import { Section, Option } from '../components';
import useInterfaceOptions from './useInterfaceOptions';
@ -9,7 +9,7 @@ type Props = {
};
const Interface = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
const { shell } = useServices();
const { shell } = usePlatform();
const {
interfaceLanguageSelect,

View file

@ -1,10 +1,9 @@
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useServices } from 'stremio/services';
import { usePlatform } from 'stremio/common';
import { Button } from 'stremio/components';
import { SECTIONS } from '../constants';
import { usePlatform } from 'stremio/common';
import styles from './Menu.less';
type Props = {
@ -15,7 +14,7 @@ type Props = {
const Menu = ({ selected, streamingServer, onSelect }: Props) => {
const { t } = useTranslation();
const { shell } = useServices();
const { shell } = usePlatform();
const platform = usePlatform();
const settings = useMemo(() => (
@ -55,9 +54,9 @@ const Menu = ({ selected, streamingServer, onSelect }: Props) => {
</div>
}
{
typeof shell?.transport?.props?.shellVersion === 'string' &&
<div className={styles['version-info-label']} title={shell.transport.props.shellVersion}>
{t('SETTINGS_SHELL_VERSION')}: {shell.transport.props.shellVersion}
typeof shell.state.version === 'string' &&
<div className={styles['version-info-label']} title={shell.state.version}>
{t('SETTINGS_SHELL_VERSION')}: {shell.state.version}
</div>
}
</div>

View file

@ -1,16 +1,15 @@
import React, { forwardRef } from 'react';
import { ColorInput, MultiselectMenu, Toggle } from 'stremio/components';
import { useServices } from 'stremio/services';
import { usePlatform } from 'stremio/common';
import { Category, Option, Section } from '../components';
import usePlayerOptions from './usePlayerOptions';
import { usePlatform } from 'stremio/common';
type Props = {
profile: Profile,
};
const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
const { shell } = useServices();
const { shell } = usePlatform();
const platform = usePlatform();
const {

View file

@ -1,4 +1,3 @@
type ServicesContext = {
shell: any,
chromecast: any,
};

View file

@ -1,11 +0,0 @@
type ShellTransportProps = {
shellVersion: string,
};
type ShellTransport = {
props: ShellTransportProps,
};
interface ShellService {
transport: ShellTransport,
}

View file

@ -1,88 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const EventEmitter = require('eventemitter3');
const ShellTransport = require('./ShellTransport');
function Shell() {
let active = false;
let error = null;
let starting = false;
let transport = null;
const events = new EventEmitter();
function onStateChanged() {
events.emit('stateChanged');
}
Object.defineProperties(this, {
active: {
configurable: false,
enumerable: true,
get: function() {
return active;
}
},
error: {
configurable: false,
enumerable: true,
get: function() {
return error;
}
},
starting: {
configurable: false,
enumerable: true,
get: function() {
return starting;
}
},
transport: {
configurable: false,
enumerable: true,
get: function() {
return transport;
}
}
});
this.start = function() {
if (active || error instanceof Error || starting) {
return;
}
active = false;
starting = true;
try {
transport = new ShellTransport();
active = true;
error = null;
starting = false;
onStateChanged();
} catch (e) {
console.error(e);
active = false;
error = new Error('Failed to initialize shell transport', { cause: e });
starting = false;
onStateChanged();
transport = null;
}
onStateChanged();
};
this.stop = function() {
active = false;
error = null;
starting = false;
onStateChanged();
};
this.on = function(name, listener) {
events.on(name, listener);
};
this.off = function(name, listener) {
events.off(name, listener);
};
}
module.exports = Shell;

View file

@ -1,92 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const EventEmitter = require('eventemitter3');
const QtMsgTypes = {
signal: 1,
propertyUpdate: 2,
init: 3,
idle: 4,
debug: 5,
invokeMethod: 6,
connectToSignal: 7,
disconnectFromSignal: 8,
setProperty: 9,
response: 10,
};
const QtObjId = 'transport'; // the ID of our transport object
function ShellTransport() {
const events = new EventEmitter();
this.props = {};
// eslint-disable-next-line @typescript-eslint/no-this-alias
const shell = this;
const transport = window.qt && window.qt.webChannelTransport;
if (!transport) throw 'no viable transport found (qt.webChannelTransport)';
let id = 0;
function send(msg) {
msg.id = id++;
transport.send(JSON.stringify(msg));
}
transport.onmessage = function (message) {
const msg = JSON.parse(message.data);
if (msg.id === 0) {
const obj = msg.data[QtObjId];
obj.properties.slice(1).forEach(function (prop) {
shell.props[prop[1]] = prop[3];
});
if (typeof shell.props.shellVersion === 'string') {
shell.shellVersionArr = (
shell.props.shellVersion.match(/(\d+)\.(\d+)\.(\d+)/) || []
)
.slice(1, 4)
.map(Number);
}
events.emit('received-props', shell.props);
obj.signals.forEach(function (sig) {
send({
type: QtMsgTypes.connectToSignal,
object: QtObjId,
signal: sig[1],
});
});
const onEvent = obj.methods.filter(function (x) {
return x[0] === 'onEvent';
})[0];
shell.send = function (ev, args) {
send({
type: QtMsgTypes.invokeMethod,
object: QtObjId,
method: onEvent[1],
args: [ev, args || {}],
});
};
shell.send('app-ready', {}); // signal that we're ready to take events
}
if (msg.object === QtObjId && msg.type === QtMsgTypes.signal)
events.emit(msg.args[0], msg.args[1]);
};
send({ type: QtMsgTypes.init });
this.on = function(name, listener) {
events.on(name, listener);
};
this.off = function(name, listener) {
events.off(name, listener);
};
this.removeAllListeners = function() {
events.removeAllListeners();
};
}
module.exports = ShellTransport;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Shell = require('./Shell');
module.exports = Shell;

View file

@ -3,13 +3,11 @@
const Chromecast = require('./Chromecast');
const { ServicesProvider, useServices } = require('./ServicesContext');
const { GamepadProvider, useGamepad } = require('./GamepadContext');
const Shell = require('./Shell');
module.exports = {
Chromecast,
ServicesProvider,
useServices,
Shell,
GamepadProvider,
useGamepad,
};