diff --git a/package.json b/package.json
index 59a211c74..dd73785af 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"@types/hat": "^0.0.4",
"@types/lodash.throttle": "^4.1.9",
"@types/magnet-uri": "^5.1.5",
+ "@types/node": "^25.6.0",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
"babel-loader": "10.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 62ee2ef5d..28201c156 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -123,6 +123,9 @@ importers:
'@types/lodash.throttle':
specifier: ^4.1.9
version: 4.1.9
+ '@types/node':
+ specifier: ^25.6.0
+ version: 25.6.0
'@types/magnet-uri':
specifier: ^5.1.5
version: 5.1.5
@@ -1494,6 +1497,9 @@ packages:
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
+ '@types/node-forge@1.3.14':
+ resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
+
'@types/node@25.6.0':
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
diff --git a/src/App/App.js b/src/App/App.js
index 375c1f5db..85697ddca 100644
--- a/src/App/App.js
+++ b/src/App/App.js
@@ -3,24 +3,26 @@
require('spatial-navigation-polyfill');
const React = require('react');
const { useTranslation } = require('react-i18next');
+const { useCore } = require('stremio/core');
const { Router } = require('stremio-router');
-const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider, GamepadProvider } = require('stremio/services');
+const { Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider, GamepadProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
-const { FileDropProvider, FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common');
+const { FileDropProvider, FullscreenProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useShell, useBinaryState, useProfile, withCoreSuspender } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
const DeepLinkHandler = require('./DeepLinkHandler');
const SearchParamsHandler = require('./SearchParamsHandler');
const { default: UpdaterBanner } = require('./UpdaterBanner');
const { default: ShortcutsModal } = require('./ShortcutsModal');
const { default: GamepadModal } = require('./GamepadModal');
-const ErrorDialog = require('./ErrorDialog');
const withProtectedRoutes = require('./withProtectedRoutes');
const routerViewsConfig = require('./routerViewsConfig');
const styles = require('./styles');
-const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router));
+const RouterWithProtectedRoutes = withProtectedRoutes(Router);
const App = () => {
+ const core = useCore();
+ const profile = useProfile();
const { i18n } = useTranslation();
const shell = useShell();
const [gamepadSupportEnabled, setGamepadSupportEnabled] = React.useState(false);
@@ -28,19 +30,13 @@ const App = () => {
return NotFound;
}, []);
const services = React.useMemo(() => {
- const core = new Core({
- appVersion: process.env.VERSION,
- shellVersion: null
- });
return {
- core,
shell: new Shell(),
chromecast: new Chromecast(),
keyboardShortcuts: new KeyboardShortcuts(),
dragAndDrop: new DragAndDrop({ core })
};
}, []);
- const [initialized, setInitialized] = React.useState(false);
const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false);
const [gamepadModalOpen,, closeGamepadModal, toggleGamepadModal] = useBinaryState(false);
@@ -58,12 +54,10 @@ const App = () => {
React.useEffect(() => {
let prevPath = window.location.hash.slice(1);
const onLocationHashChange = () => {
- if (services.core.active) {
- services.core.transport.analytics({
- event: 'LocationPathChanged',
- args: { prevPath }
- });
- }
+ core.transport.analytics({
+ event: 'LocationPathChanged',
+ args: { prevPath }
+ });
prevPath = window.location.hash.slice(1);
};
window.addEventListener('hashchange', onLocationHashChange);
@@ -71,19 +65,8 @@ const App = () => {
window.removeEventListener('hashchange', onLocationHashChange);
};
}, []);
+
React.useEffect(() => {
- const onCoreStateChanged = () => {
- setInitialized(
- (services.core.active || services.core.error instanceof Error) &&
- (services.shell.active || services.shell.error instanceof Error)
- );
- };
- const onShellStateChanged = () => {
- setInitialized(
- (services.core.active || services.core.error instanceof Error) &&
- (services.shell.active || services.shell.error instanceof Error)
- );
- };
const onChromecastStateChange = () => {
if (services.chromecast.active) {
services.chromecast.transport.setOptions({
@@ -95,23 +78,17 @@ const App = () => {
});
}
};
- services.core.on('stateChanged', onCoreStateChanged);
- services.shell.on('stateChanged', onShellStateChanged);
services.chromecast.on('stateChanged', onChromecastStateChange);
- services.core.start();
services.shell.start();
services.chromecast.start();
services.keyboardShortcuts.start();
services.dragAndDrop.start();
window.services = services;
return () => {
- services.core.stop();
services.shell.stop();
services.chromecast.stop();
services.keyboardShortcuts.stop();
services.dragAndDrop.stop();
- services.core.off('stateChanged', onCoreStateChanged);
- services.shell.off('stateChanged', onShellStateChanged);
services.chromecast.off('stateChanged', onChromecastStateChange);
};
}, []);
@@ -142,124 +119,89 @@ const App = () => {
}, []);
React.useEffect(() => {
- const onCoreEvent = ({ event, args }) => {
- switch (event) {
- case 'SettingsUpdated': {
- if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') {
- i18n.changeLanguage(args.settings.interfaceLanguage);
- }
+ if (typeof profile.settings?.interfaceLanguage === 'string') {
+ i18n.changeLanguage(profile.settings.interfaceLanguage);
+ }
- if (args?.settings?.gamepadSupport !== undefined) {
- setGamepadSupportEnabled(args.settings.gamepadSupport);
- }
+ if (typeof profile.settings?.gamepadSupport === 'boolean') {
+ setGamepadSupportEnabled(profile.settings.gamepadSupport);
+ }
- if (args?.settings?.quitOnClose && shell.windowClosed) {
- shell.send('quit');
- }
+ if (profile.settings?.quitOnClose && shell.windowClosed) {
+ shell.send('quit');
+ }
+ }, [profile.settings, shell.windowClosed]);
- break;
- }
- }
- };
- const onCtxState = (state) => {
- if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') {
- i18n.changeLanguage(state.profile.settings.interfaceLanguage);
- }
-
- if (typeof state.profile.settings.gamepadSupport === 'boolean') {
- setGamepadSupportEnabled(state.profile.settings.gamepadSupport);
- }
-
- if (state?.profile?.settings?.quitOnClose && shell.windowClosed) {
- shell.send('quit');
- }
- };
+ React.useEffect(() => {
const onWindowFocus = () => {
- services.core.transport.dispatch({
+ core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullAddonsFromAPI'
}
});
- services.core.transport.dispatch({
+ core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullUserFromAPI',
args: {}
}
});
- services.core.transport.dispatch({
+ core.transport.dispatch({
action: 'Ctx',
args: {
action: 'SyncLibraryWithAPI'
}
});
- services.core.transport.dispatch({
+ core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullNotifications'
}
});
};
- if (services.core.active) {
- onWindowFocus();
- window.addEventListener('focus', onWindowFocus);
- services.core.transport.on('CoreEvent', onCoreEvent);
- services.core.transport
- .getState('ctx')
- .then(onCtxState)
- .catch(console.error);
- }
+
+ onWindowFocus();
+ window.addEventListener('focus', onWindowFocus);
+
return () => {
- if (services.core.active) {
- window.removeEventListener('focus', onWindowFocus);
- services.core.transport.off('CoreEvent', onCoreEvent);
- }
+ window.removeEventListener('focus', onWindowFocus);
};
- }, [initialized, shell.windowClosed]);
+ }, []);
+
return (
-
-
- {
- initialized ?
- services.core.error instanceof Error ?
-
- :
-
-
-
-
-
-
-
- {
- shortcutModalOpen &&
- }
- {
- gamepadModalOpen &&
- }
-
-
-
-
-
-
-
-
-
-
-
-
- :
-
- }
-
-
+
+
+
+
+
+
+
+
+ {
+ shortcutModalOpen &&
+ }
+ {
+ gamepadModalOpen &&
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
-module.exports = App;
+module.exports = withCoreSuspender(App);
diff --git a/src/App/ErrorDialog/index.js b/src/App/ErrorDialog/index.js
deleted file mode 100644
index 6a25fdea2..000000000
--- a/src/App/ErrorDialog/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright (C) 2017-2023 Smart code 203358507
-
-const ErrorDialog = require('./ErrorDialog');
-
-module.exports = ErrorDialog;
diff --git a/src/App/SearchParamsHandler.js b/src/App/SearchParamsHandler.js
index 86f10d0c4..591c0cf60 100644
--- a/src/App/SearchParamsHandler.js
+++ b/src/App/SearchParamsHandler.js
@@ -2,11 +2,11 @@
const React = require('react');
const { deepEqual } = require('fast-equals');
+const { useCore } = require('stremio/core');
const { withCoreSuspender, useProfile, useToast } = require('stremio/common');
-const { useServices } = require('stremio/services');
const SearchParamsHandler = () => {
- const { core } = useServices();
+ const core = useCore();
const profile = useProfile();
const toast = useToast();
diff --git a/src/App/ServicesToaster.js b/src/App/ServicesToaster.js
index 7ca7e2914..6338fba00 100644
--- a/src/App/ServicesToaster.js
+++ b/src/App/ServicesToaster.js
@@ -2,38 +2,16 @@
const React = require('react');
const { useServices } = require('stremio/services');
+const { useCore } = require('stremio/core');
const { useToast } = require('stremio/common');
const ServicesToaster = () => {
- const { core, dragAndDrop } = useServices();
+ const { dragAndDrop } = useServices();
+ const core = useCore();
const toast = useToast();
React.useEffect(() => {
- const onCoreEvent = ({ event, args }) => {
- switch (event) {
- case 'Error': {
- if (args.source.event === 'UserPulledFromAPI' && args.source.args.uid === null) {
- break;
- }
-
- if (args.source.event === 'LibrarySyncWithAPIPlanned' && args.source.args.uid === null) {
- break;
- }
-
- if (args.error.type === 'Other' && args.error.code === 3 && args.source.event === 'AddonInstalled' && args.source.args.transport_url.startsWith('https://www.strem.io/trakt/addon')) {
- break;
- }
-
- toast.show({
- type: 'error',
- title: args.source.event,
- message: args.error.message,
- timeout: 4000,
- dataset: {
- type: 'CoreEvent'
- }
- });
- break;
- }
+ const onCoreEvent = (name, data) => {
+ switch (name) {
case 'TorrentParsed': {
toast.show({
type: 'success',
@@ -53,13 +31,28 @@ const ServicesToaster = () => {
case 'PlayingOnDevice': {
toast.show({
type: 'success',
- title: `Stream opened in ${args.device}`,
+ title: `Stream opened in ${data.device}`,
timeout: 4000
});
break;
}
}
};
+ const onCoreError = (source, error) => {
+ if (source.event === 'UserPulledFromAPI' && source.args.uid === null) return;
+ if (source.event === 'LibrarySyncWithAPIPlanned' && source.args.uid === null) return;
+ if (error.type === 'Other' && error.code === 3 && source.event === 'AddonInstalled' && source.args.transport_url.startsWith('https://www.strem.io/trakt/addon')) return;
+
+ toast.show({
+ type: 'error',
+ title: source.event,
+ message: error.message,
+ timeout: 4000,
+ dataset: {
+ type: 'CoreEvent'
+ }
+ });
+ };
const onDragAndDropError = (error) => {
toast.show({
type: 'error',
@@ -68,10 +61,12 @@ const ServicesToaster = () => {
timeout: 4000
});
};
- core.transport.on('CoreEvent', onCoreEvent);
+ core.on('event', onCoreEvent);
+ core.on('error', onCoreError);
dragAndDrop.on('error', onDragAndDropError);
return () => {
- core.transport.off('CoreEvent', onCoreEvent);
+ core.off('event', onCoreEvent);
+ core.off('error', onCoreError);
dragAndDrop.off('error', onDragAndDropError);
};
}, []);
diff --git a/src/App/styles.less b/src/App/styles.less
index f86057cd0..dc6aa512f 100644
--- a/src/App/styles.less
+++ b/src/App/styles.less
@@ -241,11 +241,6 @@ html {
width: 100%;
height: 100%;
}
-
- .loader-container, .error-container {
- width: 100%;
- height: 100%;
- }
}
}
}
diff --git a/src/common/CoreSuspender.js b/src/common/CoreSuspender.js
index d225d5411..02f61b5b6 100644
--- a/src/common/CoreSuspender.js
+++ b/src/common/CoreSuspender.js
@@ -1,7 +1,7 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
-const { useServices } = require('stremio/services');
+const { useCore } = require('stremio/core');
const CoreSuspenderContext = React.createContext(null);
@@ -40,7 +40,7 @@ const useCoreSuspender = () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
const withCoreSuspender = (Component, Fallback = () => { }) => {
return function withCoreSuspender(props) {
- const { core } = useServices();
+ const core = useCore();
const parentSuspender = useCoreSuspender();
const [render, setRender] = React.useState(parentSuspender === null);
const statesRef = React.useRef({});
diff --git a/src/common/useModelState.js b/src/common/useModelState.js
index da637b8ff..f9914553c 100644
--- a/src/common/useModelState.js
+++ b/src/common/useModelState.js
@@ -4,12 +4,12 @@ const React = require('react');
const throttle = require('lodash.throttle');
const { deepEqual } = require('fast-equals');
const intersection = require('lodash.intersection');
+const { useCore } = require('stremio/core');
const { useCoreSuspender } = require('stremio/common/CoreSuspender');
const { useRouteFocused } = require('stremio-router');
-const { useServices } = require('stremio/services');
const useModelState = ({ action, ...args }) => {
- const { core } = useServices();
+ const core = useCore();
const routeFocused = useRouteFocused();
const mountedRef = React.useRef(false);
const [model, timeout, map, deps] = React.useMemo(() => {
@@ -25,24 +25,21 @@ const useModelState = ({ action, ...args }) => {
},
undefined,
() => {
- if (typeof map === 'function') {
- return map(getState(model));
- } else {
- return getState(model);
- }
+ const state = getState(model);
+ return typeof map === 'function' ? map(state) : state;
}
);
- React.useInsertionEffect(() => {
+ React.useEffect(() => {
if (action) {
core.transport.dispatch(action, model);
}
}, [action]);
- React.useInsertionEffect(() => {
+ React.useEffect(() => {
return () => {
core.transport.dispatch({ action: 'Unload' }, model);
};
}, []);
- React.useInsertionEffect(() => {
+ React.useEffect(() => {
const onNewState = async (models) => {
if (models.indexOf(model) === -1 && (!Array.isArray(deps) || intersection(deps, models).length === 0)) {
return;
@@ -57,17 +54,17 @@ const useModelState = ({ action, ...args }) => {
};
const onNewStateThrottled = throttle(onNewState, timeout);
if (routeFocused) {
- core.transport.on('NewState', onNewStateThrottled);
+ core.on('state', onNewStateThrottled);
if (mountedRef.current) {
onNewState([model]);
}
}
return () => {
onNewStateThrottled.cancel();
- core.transport.off('NewState', onNewStateThrottled);
+ core.off('state', onNewStateThrottled);
};
}, [routeFocused]);
- React.useInsertionEffect(() => {
+ React.useEffect(() => {
mountedRef.current = true;
}, []);
return state;
diff --git a/src/common/usePlayUrl.ts b/src/common/usePlayUrl.ts
index 49fe386ed..2ac0ac1ff 100644
--- a/src/common/usePlayUrl.ts
+++ b/src/common/usePlayUrl.ts
@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import magnet from 'magnet-uri';
-import { useServices } from 'stremio/services';
+import { useCore } from 'stremio/core';
import useToast from 'stremio/common/Toast/useToast';
import useTorrent from 'stremio/common/useTorrent';
import useStreamingServer from 'stremio/common/useStreamingServer';
@@ -8,7 +8,7 @@ import useStreamingServer from 'stremio/common/useStreamingServer';
const HTTP_REGEX = /^https?:\/\/.+/i;
const usePlayUrl = () => {
- const { core } = useServices();
+ const core = useCore();
const toast = useToast();
const { createTorrentFromMagnet } = useTorrent();
const streamingServer = useStreamingServer();
@@ -24,7 +24,11 @@ const usePlayUrl = () => {
timeout: 3000
});
try {
- const encoded = await core.transport.encodeStream({ url: trimmed });
+ const encoded = await core.transport.encodeStream({
+ name: '',
+ description: '',
+ url: trimmed,
+ });
if (typeof encoded === 'string') {
window.location.hash = `#/player/${encodeURIComponent(encoded)}`;
return true;
diff --git a/src/common/useSettings.ts b/src/common/useSettings.ts
index 16dc2cd2f..5f19a387f 100644
--- a/src/common/useSettings.ts
+++ b/src/common/useSettings.ts
@@ -1,11 +1,11 @@
// Copyright (C) 2017-2025 Smart code 203358507
import { useCallback } from 'react';
-import { useServices } from 'stremio/services';
import useProfile from './useProfile';
+import { useCore } from 'stremio/core';
const useSettings = (): [Settings, (settings: Settings) => void] => {
- const { core } = useServices();
+ const core = useCore();
const profile = useProfile();
const updateSettings = useCallback((settings: Settings) => {
diff --git a/src/common/useTorrent.js b/src/common/useTorrent.js
index 2527154a2..4a9401091 100644
--- a/src/common/useTorrent.js
+++ b/src/common/useTorrent.js
@@ -2,14 +2,14 @@
const React = require('react');
const magnet = require('magnet-uri');
-const { useServices } = require('stremio/services');
+const { useCore } = require('stremio/core');
const useToast = require('stremio/common/Toast/useToast');
const useStreamingServer = require('stremio/common/useStreamingServer');
const CREATE_TORRENT_TIMEOUT = 20000;
const useTorrent = () => {
- const { core } = useServices();
+ const core = useCore();
const streamingServer = useStreamingServer();
const toast = useToast();
const createTorrentTimeout = React.useRef(null);
diff --git a/src/components/AddonDetailsModal/AddonDetailsModal.js b/src/components/AddonDetailsModal/AddonDetailsModal.js
index a89a68b77..7a89b704d 100644
--- a/src/components/AddonDetailsModal/AddonDetailsModal.js
+++ b/src/components/AddonDetailsModal/AddonDetailsModal.js
@@ -3,10 +3,10 @@
const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
+const { useCore } = require('stremio/core');
const ModalDialog = require('stremio/components/ModalDialog');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const { usePlatform } = require('stremio/common/Platform');
-const { useServices } = require('stremio/services');
const AddonDetailsWithRemoteAndLocalAddon = withRemoteAndLocalAddon(require('./AddonDetails'));
const useAddonDetails = require('./useAddonDetails');
const styles = require('./styles');
@@ -45,7 +45,7 @@ function withRemoteAndLocalAddon(AddonDetails) {
const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
const { t } = useTranslation();
- const { core } = useServices();
+ const core = useCore();
const platform = usePlatform();
const addonDetails = useAddonDetails(transportUrl);
const modalButtons = React.useMemo(() => {
diff --git a/src/components/ContinueWatchingItem/ContinueWatchingItem.js b/src/components/ContinueWatchingItem/ContinueWatchingItem.js
index 8e56179df..4716972c3 100644
--- a/src/components/ContinueWatchingItem/ContinueWatchingItem.js
+++ b/src/components/ContinueWatchingItem/ContinueWatchingItem.js
@@ -2,11 +2,11 @@
const React = require('react');
const PropTypes = require('prop-types');
-const { useServices } = require('stremio/services');
+const { useCore } = require('stremio/core');
const LibItem = require('stremio/components/LibItem');
const ContinueWatchingItem = ({ _id, notifications, ...props }) => {
- const { core } = useServices();
+ const core = useCore();
const onDismissClick = React.useCallback((event) => {
event.preventDefault();
diff --git a/src/components/EventModal/useEvents.js b/src/components/EventModal/useEvents.js
index f5efaa79e..37f0748a6 100644
--- a/src/components/EventModal/useEvents.js
+++ b/src/components/EventModal/useEvents.js
@@ -1,14 +1,14 @@
// Copyright (C) 2017-2023 Smart code 203358507
+const { useCore } = require('stremio/core');
const useModelState = require('stremio/common/useModelState');
-const { useServices } = require('stremio/services');
const map = (ctx) => ({
...ctx.events,
});
const useEvents = () => {
- const { core } = useServices();
+ const core = useCore();
const pullEvents = () => {
core.transport.dispatch({
diff --git a/src/components/LibItem/LibItem.js b/src/components/LibItem/LibItem.js
index 28769ddcf..1d5e5c96f 100644
--- a/src/components/LibItem/LibItem.js
+++ b/src/components/LibItem/LibItem.js
@@ -1,14 +1,13 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
-const { useServices } = require('stremio/services');
const PropTypes = require('prop-types');
+const { useCore } = require('stremio/core');
const MetaItem = require('stremio/components/MetaItem');
const { t } = require('i18next');
const LibItem = ({ _id, removable, notifications, watched, ...props }) => {
-
- const { core } = useServices();
+ const core = useCore();
const newVideos = React.useMemo(() => {
const count = notifications.items?.[_id]?.length ?? 0;
diff --git a/src/components/MetaPreview/Ratings/useRating.ts b/src/components/MetaPreview/Ratings/useRating.ts
index 286ad7284..da07ed831 100644
--- a/src/components/MetaPreview/Ratings/useRating.ts
+++ b/src/components/MetaPreview/Ratings/useRating.ts
@@ -1,10 +1,10 @@
// Copyright (C) 2017-2025 Smart code 203358507
import { useMemo, useCallback } from 'react';
-import { useServices } from 'stremio/services';
+import { useCore } from 'stremio/core';
const useRating = (ratingInfo?: Loadable) => {
- const { core } = useServices();
+ const core = useCore();
const setRating = useCallback((status: Rating) => {
core.transport.dispatch({
diff --git a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js
index 6615e5b76..d973f6b54 100644
--- a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js
+++ b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js
@@ -5,7 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
-const { useServices } = require('stremio/services');
+const { useCore } = require('stremio/core');
const { Button } = require('stremio/components');
const { useFullscreen } = require('stremio/common/Fullscreen');
const useProfile = require('stremio/common/useProfile');
@@ -18,7 +18,7 @@ const styles = require('./styles');
const NavMenuContent = ({ onClick }) => {
const { t } = useTranslation();
- const { core } = useServices();
+ const core = useCore();
const profile = useProfile();
const streamingServer = useStreamingServer();
const { handlePlayUrl } = usePlayUrl();
diff --git a/src/components/NavBar/HorizontalNavBar/SearchBar/useLocalSearch.js b/src/components/NavBar/HorizontalNavBar/SearchBar/useLocalSearch.js
index 6f5ce3868..dd867db84 100644
--- a/src/components/NavBar/HorizontalNavBar/SearchBar/useLocalSearch.js
+++ b/src/components/NavBar/HorizontalNavBar/SearchBar/useLocalSearch.js
@@ -1,11 +1,11 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
-const { useServices } = require('stremio/services');
+const { useCore } = require('stremio/core');
const useModelState = require('stremio/common/useModelState');
const useLocalSearch = () => {
- const { core } = useServices();
+ const core = useCore();
const action = React.useMemo(() => ({
action: 'Load',
diff --git a/src/components/NavBar/HorizontalNavBar/SearchBar/useSearchHistory.js b/src/components/NavBar/HorizontalNavBar/SearchBar/useSearchHistory.js
index 99c6f4479..b3dd19ddc 100644
--- a/src/components/NavBar/HorizontalNavBar/SearchBar/useSearchHistory.js
+++ b/src/components/NavBar/HorizontalNavBar/SearchBar/useSearchHistory.js
@@ -1,11 +1,11 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
+const { useCore } = require('stremio/core');
const useModelState = require('stremio/common/useModelState');
-const { useServices } = require('stremio/services');
const useSearchHistory = () => {
- const { core } = useServices();
+ const core = useCore();
const { searchHistory: items } = useModelState({ model: 'ctx' });
const clear = React.useCallback(() => {
diff --git a/src/components/SharePrompt/SharePrompt.js b/src/components/SharePrompt/SharePrompt.js
index af4393b8a..21963c9aa 100644
--- a/src/components/SharePrompt/SharePrompt.js
+++ b/src/components/SharePrompt/SharePrompt.js
@@ -6,7 +6,7 @@ const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router');
-const { useServices } = require('stremio/services');
+const { useCore } = require('stremio/core');
const { Button } = require('stremio/components');
const { default: TextInput } = require('stremio/components/TextInput');
const useToast = require('stremio/common/Toast/useToast');
@@ -14,7 +14,7 @@ const styles = require('./styles');
const SharePrompt = ({ className, url }) => {
const { t } = useTranslation();
- const { core } = useServices();
+ const core = useCore();
const toast = useToast();
const inputRef = React.useRef(null);
const routeFocused = useRouteFocused();
diff --git a/src/core/CoreContext.ts b/src/core/CoreContext.ts
new file mode 100644
index 000000000..427b79cec
--- /dev/null
+++ b/src/core/CoreContext.ts
@@ -0,0 +1,15 @@
+import { createContext } from 'react';
+
+interface CoreContext {
+ transport: CoreTransport;
+ on(name: 'state', listener: CoreStateListener): void;
+ on(name: 'event', listener: CoreEventListener): void;
+ on(name: 'error', listener: CoreErrorListener): void;
+ off(name: 'state', listener: CoreStateListener): void;
+ off(name: 'event', listener: CoreEventListener): void;
+ off(name: 'error', listener: CoreErrorListener): void;
+}
+
+const CoreContext = createContext({} as CoreContext);
+
+export default CoreContext;
diff --git a/src/core/CoreProvider.tsx b/src/core/CoreProvider.tsx
new file mode 100644
index 000000000..67dce4a4e
--- /dev/null
+++ b/src/core/CoreProvider.tsx
@@ -0,0 +1,99 @@
+import React, { useEffect, useRef, useState } from 'react';
+import CoreContext from './CoreContext';
+import createTransport from './createTransport';
+import Error from './Error';
+
+type Props = {
+ appInfo: object,
+ children: React.ReactNode,
+};
+
+const Core = (props: Props) => {
+ const transport = createTransport();
+ const [initialized, setInitialized] = useState(false);
+ const [error, setError] = useState();
+
+ const stateListeners = useRef([]);
+ const eventListeners = useRef([]);
+ const errorListeners = useRef([]);
+
+ const on = (name: CoreListenerType, listener: CoreListener) => {
+ if (name === 'state') stateListeners.current = [...stateListeners.current, listener as CoreStateListener];
+ if (name === 'event') eventListeners.current = [...eventListeners.current, listener as CoreEventListener];
+ if (name === 'error') errorListeners.current = [...errorListeners.current, listener as CoreErrorListener];
+ };
+
+ const off = (name: CoreListenerType, listener: CoreListener) => {
+ if (name === 'state') stateListeners.current = stateListeners.current.filter((l) => l !== listener);
+ if (name === 'event') eventListeners.current = eventListeners.current.filter((l) => l !== listener);
+ if (name === 'error') errorListeners.current = errorListeners.current.filter((l) => l !== listener);
+ };
+
+ useEffect(() => {
+ const onCoreEvent = ({ name, args }: NewStateEvent | CoreEventEvent) => {
+ switch (name) {
+ case 'NewState':
+ stateListeners.current.forEach((listener) => listener(args));
+ break;
+
+ case 'CoreEvent': {
+ switch (args.event) {
+ case 'Error': {
+ const { source, error } = args.args;
+ errorListeners.current.forEach((listener) => listener(
+ source,
+ error,
+ ));
+ break;
+ }
+ default:
+ eventListeners.current.forEach((listener) => listener(
+ args.event,
+ args.args,
+ ));
+ break;
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ };
+
+ if (!window.core) {
+ transport
+ .init(props.appInfo)
+ .then(() => {
+ window.core = transport;
+ window.onCoreEvent = onCoreEvent;
+ setInitialized(true);
+ setError(null);
+ })
+ .catch((e: Error) => {
+ console.error('Failed to initialize core:', e);
+ setInitialized(false);
+ setError(e);
+ });
+ }
+
+ return () => {
+ stateListeners.current = [];
+ eventListeners.current = [];
+ errorListeners.current = [];
+ setInitialized(false);
+ setError(null);
+ window.onCoreEvent = null;
+ window.core = null;
+ };
+ }, []);
+
+ return (
+
+ { error && !initialized && }
+ { initialized && !error && props.children }
+
+ );
+};
+
+export default Core;
diff --git a/src/App/ErrorDialog/ErrorDialog.js b/src/core/Error/Error.tsx
similarity index 72%
rename from src/App/ErrorDialog/ErrorDialog.js
rename to src/core/Error/Error.tsx
index eac9b34ac..36c9d1811 100644
--- a/src/App/ErrorDialog/ErrorDialog.js
+++ b/src/core/Error/Error.tsx
@@ -1,25 +1,27 @@
// Copyright (C) 2017-2023 Smart code 203358507
-const React = require('react');
-const { useTranslation } = require('react-i18next');
-const PropTypes = require('prop-types');
-const classnames = require('classnames');
-const { Image, Button } = require('stremio/components');
-const styles = require('./styles');
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import Image from 'stremio/components/Image';
+import Button from 'stremio/components/Button';
+import styles from './styles.less';
-const ErrorDialog = ({ className }) => {
+const Error = () => {
const { t } = useTranslation();
const [dataCleared, setDataCleared] = React.useState(false);
+
const reload = React.useCallback(() => {
window.location.reload();
}, []);
+
const clearData = React.useCallback(() => {
window.localStorage.clear();
setDataCleared(true);
}, []);
+
return (
-
+
{
);
};
-ErrorDialog.displayName = 'ErrorDialog';
+export default Error;
-ErrorDialog.propTypes = {
- className: PropTypes.string
-};
-
-module.exports = ErrorDialog;
diff --git a/src/core/Error/index.ts b/src/core/Error/index.ts
new file mode 100644
index 000000000..2b6c32580
--- /dev/null
+++ b/src/core/Error/index.ts
@@ -0,0 +1,4 @@
+// Copyright (C) 2017-2023 Smart code 203358507
+
+import Error from './Error';
+export default Error;
diff --git a/src/App/ErrorDialog/styles.less b/src/core/Error/styles.less
similarity index 97%
rename from src/App/ErrorDialog/styles.less
rename to src/core/Error/styles.less
index c3f7813d4..a3932533b 100644
--- a/src/App/ErrorDialog/styles.less
+++ b/src/core/Error/styles.less
@@ -3,6 +3,9 @@
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.error-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
display: flex;
flex-direction: column;
align-items: center;
diff --git a/src/core/createTransport.ts b/src/core/createTransport.ts
new file mode 100644
index 000000000..8ac77803a
--- /dev/null
+++ b/src/core/createTransport.ts
@@ -0,0 +1,41 @@
+import Bridge from '@stremio/stremio-core-web/bridge';
+
+const worker = new Worker(`${process.env.COMMIT_HASH}/scripts/worker.js`);
+const bridge = new Bridge(window, worker);
+
+const createTransport = (): CoreTransport => {
+ const init = async (args: object): Promise => {
+ return bridge.call(['init'], [args]);
+ };
+
+ const getState = (model: string): Promise