Merge pull request #1258 from Stremio/refactor/core-logic
Some checks are pending
Build / build (push) Waiting to run

Dev: Move core logic to a provider
This commit is contained in:
Tim 2026-05-05 12:13:36 +02:00 committed by GitHub
commit 230be72e94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 471 additions and 496 deletions

View file

@ -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",

View file

@ -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==}

View file

@ -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 (
<React.StrictMode>
<ServicesProvider services={services}>
{
initialized ?
services.core.error instanceof Error ?
<ErrorDialog className={styles['error-container']} />
:
<PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<FileDropProvider className={styles['file-drop-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>
</FileDropProvider>
</TooltipProvider>
</ToastProvider>
</PlatformProvider>
:
<div className={styles['loader-container']} />
}
</ServicesProvider>
</React.StrictMode>
<ServicesProvider services={services}>
<PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<FileDropProvider className={styles['file-drop-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>
</FileDropProvider>
</TooltipProvider>
</ToastProvider>
</PlatformProvider>
</ServicesProvider>
);
};
module.exports = App;
module.exports = withCoreSuspender(App);

View file

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

View file

@ -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();

View file

@ -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);
};
}, []);

View file

@ -241,11 +241,6 @@ html {
width: 100%;
height: 100%;
}
.loader-container, .error-container {
width: 100%;
height: 100%;
}
}
}
}

View file

@ -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({});

View file

@ -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;

View file

@ -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;

View file

@ -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) => {

View file

@ -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);

View file

@ -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(() => {

View file

@ -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();

View file

@ -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({

View file

@ -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;

View file

@ -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<RatingInfo>) => {
const { core } = useServices();
const core = useCore();
const setRating = useCallback((status: Rating) => {
core.transport.dispatch({

View file

@ -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();

View file

@ -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',

View file

@ -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(() => {

View file

@ -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();

15
src/core/CoreContext.ts Normal file
View file

@ -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<CoreContext>({} as CoreContext);
export default CoreContext;

99
src/core/CoreProvider.tsx Normal file
View file

@ -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<Error | null>();
const stateListeners = useRef<CoreStateListener[]>([]);
const eventListeners = useRef<CoreEventListener[]>([]);
const errorListeners = useRef<CoreErrorListener[]>([]);
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 (
<CoreContext.Provider value={{ transport, on, off }}>
{ error && !initialized && <Error /> }
{ initialized && !error && props.children }
</CoreContext.Provider>
);
};
export default Core;

View file

@ -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 (
<div className={classnames(className, styles['error-container'])}>
<div className={styles['error-container']}>
<Image
className={styles['error-image']}
src={require('/assets/images/empty.png')}
@ -44,10 +46,5 @@ const ErrorDialog = ({ className }) => {
);
};
ErrorDialog.displayName = 'ErrorDialog';
export default Error;
ErrorDialog.propTypes = {
className: PropTypes.string
};
module.exports = ErrorDialog;

4
src/core/Error/index.ts Normal file
View file

@ -0,0 +1,4 @@
// Copyright (C) 2017-2023 Smart code 203358507
import Error from './Error';
export default Error;

View file

@ -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;

View file

@ -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<void> => {
return bridge.call(['init'], [args]);
};
const getState = (model: string): Promise<object> => {
return bridge.call(['getState'], [model]);
};
const dispatch = (action: DispatchAction, model?: string): Promise<void> => {
return bridge.call(['dispatch'], [action, model, location.hash]);
};
const encodeStream = (stream: Stream): Promise<string> => {
return bridge.call(['encodeStream'], [stream]);
};
const decodeStream = (stream: string): Promise<Stream> => {
return bridge.call(['decodeStream'], [stream]);
};
const analytics = (event: object): Promise<void> => {
return bridge.call(['analytics'], [event, location.hash]);
};
return {
init,
getState,
dispatch,
encodeStream,
decodeStream,
analytics,
};
};
export default createTransport;

8
src/core/global.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
interface Window {
core: CoreTransport | null | undefined,
onCoreEvent: ((event: NewStateEvent | CoreEventEvent) => void) | null;
}
interface Bridge {
call(action: string[], args: any[]): Promise<any>,
}

7
src/core/index.ts Normal file
View file

@ -0,0 +1,7 @@
import CoreProvider from './CoreProvider';
import useCore from './useCore';
export {
CoreProvider,
useCore,
};

54
src/core/types.d.ts vendored Normal file
View file

@ -0,0 +1,54 @@
type DispatchAction = {
action: string,
args?: {
model?: string,
action?: string,
args?: any,
}
};
type CoreTransport = {
init: (args: object) => Promise<void>,
getState: (model: string) => Promise<object>,
dispatch: (action: DispatchAction, model?: string) => Promise<void>,
encodeStream: (stream: Stream) => Promise<string>,
decodeStream: (stream: string) => Promise<Stream>,
analytics: (event: object) => Promise<void>,
};
type CoreStateListener = (models: string[]) => void;
type CoreEventListener = (name: string, data: object) => void;
type CoreErrorListener = (source: CoreEvent, error: CoreEventError) => void;
type CoreListener = CoreStateListener | CoreEventListener | CoreErrorListener;
type CoreListenerType = 'state' | 'event' | 'error';
type NewStateEvent = {
name: 'NewState',
args: string[],
};
type CoreEvent = {
event: 'UserPulledFromAPI' | 'UserLibraryMissing' | 'UserAuthenticated' | 'UserAddonsLocked' |
'LibraryItemsPulledFromAPI' | 'LibraryItemsPushedToStorage' | 'LibrarySyncWithAPIPlanned',
args: object,
};
type CoreEventError = {
code: number,
type: string,
message: string,
};
type CoreError = {
event: 'Error',
args: {
source: CoreEvent,
error: CoreEventError,
},
};
type CoreEventEvent = {
name: 'CoreEvent',
args: CoreEvent | CoreError,
};

5
src/core/useCore.ts Normal file
View file

@ -0,0 +1,5 @@
import { useContext } from 'react';
import CoreContext from './CoreContext';
const useCore = () => useContext(CoreContext);
export default useCore;

View file

@ -17,6 +17,7 @@ const i18n = require('i18next');
const { initReactI18next } = require('react-i18next');
const stremioTranslations = require('stremio-translations');
const App = require('./App');
const { CoreProvider } = require('./core');
const translations = Object.fromEntries(Object.entries(stremioTranslations()).map(([key, value]) => [key, {
translation: value
@ -33,8 +34,17 @@ i18n
}
});
const appInfo = {
appVersion: process.env.VERSION,
shellVersion: null
};
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<App />);
root.render(
<CoreProvider appInfo={appInfo}>
<App />
</CoreProvider>
);
if (process.env.NODE_ENV === 'production' && process.env.SERVICE_WORKER_DISABLED !== 'true' && process.env.SERVICE_WORKER_DISABLED !== true && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {

2
src/modules.d.ts vendored
View file

@ -1,3 +1,5 @@
declare module '@stremio/stremio-core-web/bridge';
declare module '*.less' {
const resource: Record<string, string>;
export = resource;

View file

@ -5,9 +5,9 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { useCore } = require('stremio/core');
const { usePlatform, useBinaryState, withCoreSuspender } = require('stremio/common');
const { AddonDetailsModal, Button, Image, MainNavBars, ModalDialog, SearchBar, SharePrompt, TextInput, MultiselectMenu } = require('stremio/components');
const { useServices } = require('stremio/services');
const useToast = require('stremio/common/Toast/useToast');
const Addon = require('./Addon');
const useInstalledAddons = require('./useInstalledAddons');
@ -20,7 +20,7 @@ const { AddonPlaceholder } = require('./AddonPlaceholder');
const Addons = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
const platform = usePlatform();
const { core } = useServices();
const core = useCore();
const toast = useToast();
const installedAddons = useInstalledAddons(urlParams);
const remoteAddons = useRemoteAddons(urlParams);

View file

@ -3,8 +3,8 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import { useServices } from 'stremio/services';
import { Button } from 'stremio/components';
import { useCore } from 'stremio/core';
import useProfile from 'stremio/common/useProfile';
import { withCoreSuspender } from 'stremio/common/CoreSuspender';
import styles from './StreamingServerWarning.less';
@ -15,7 +15,7 @@ type Props = {
const StreamingServerWarning = ({ className }: Props) => {
const { t } = useTranslation();
const { core } = useServices();
const core = useCore();
const profile = useProfile();
const createDismissalDate = (months: number, years = 0): Date => {

View file

@ -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');
const useBoard = () => {
const { core } = useServices();
const core = useCore();
const action = React.useMemo(() => ({
action: 'Load',
args: {

View file

@ -5,7 +5,7 @@ const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { useServices } = require('stremio/services');
const { useCore } = require('stremio/core');
const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender } = require('stremio/common');
const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem, MetaPreview, ModalDialog, MultiselectMenu } = require('stremio/components');
const useDiscover = require('./useDiscover');
@ -16,7 +16,7 @@ const SCROLL_TO_BOTTOM_THRESHOLD = 400;
const Discover = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
const { core } = useServices();
const core = useCore();
const [discover, loadNextPage] = useDiscover(urlParams, queryParams);
const [selectInputs, hasNextPage] = useSelectableInputs(discover);
const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false);

View file

@ -2,7 +2,7 @@
const React = require('react');
const UrlUtils = require('url');
const { useServices } = require('stremio/services');
const { useCore } = require('stremio/core');
const { useModelState } = require('stremio/common');
const map = (discover) => ({
@ -23,7 +23,7 @@ const map = (discover) => ({
});
const useDiscover = (urlParams, queryParams) => {
const { core } = useServices();
const core = useCore();
const loadNextPage = React.useCallback(() => {
core.transport.dispatch({
action: 'CatalogWithFilters',

View file

@ -6,7 +6,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { Modal, useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { useCore } = require('stremio/core');
const { useBinaryState } = require('stremio/common');
const { Button, Image, Checkbox } = require('stremio/components');
const CredentialsTextInput = require('./CredentialsTextInput');
@ -20,7 +20,7 @@ const SIGNUP_FORM = 'signup';
const LOGIN_FORM = 'login';
const Intro = ({ queryParams }) => {
const { core } = useServices();
const core = useCore();
const { t } = useTranslation();
const routeFocused = useRouteFocused();
const [startFacebookLogin, stopFacebookLogin] = useFacebookLogin();
@ -268,27 +268,24 @@ const Intro = ({ queryParams }) => {
}
}, [state.form, routeFocused]);
React.useEffect(() => {
const onCoreEvent = ({ event, args }) => {
switch (event) {
case 'UserAuthenticated': {
closeLoaderModal();
if (routeFocused) {
window.location = '#/';
}
break;
}
case 'Error': {
if (args.source.event === 'UserAuthenticated') {
closeLoaderModal();
}
break;
const onCoreEvent = (name) => {
if (name === 'UserAuthenticated') {
closeLoaderModal();
if (routeFocused) {
window.location = '#/';
}
}
};
core.transport.on('CoreEvent', onCoreEvent);
const onCoreError = (source) => {
if (source.event === 'UserAuthenticated') {
closeLoaderModal();
}
};
core.on('event', onCoreEvent);
core.on('error', onCoreError);
return () => {
core.transport.off('CoreEvent', onCoreEvent);
core.off('event', onCoreEvent);
core.off('error', onCoreError);
};
}, [routeFocused]);
return (

View file

@ -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');
const useLibrary = (model, urlParams, queryParams) => {
const { core } = useServices();
const core = useCore();
const loadNextPage = React.useCallback(() => {
core.transport.dispatch({
action: 'LibraryWithFilters',

View file

@ -4,7 +4,7 @@ const React = require('react');
const { useTranslation } = require('react-i18next');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useServices } = require('stremio/services');
const { useCore } = require('stremio/core');
const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation');
const { withCoreSuspender } = require('stremio/common');
const { VerticalNavBar, HorizontalNavBar, DelayedRenderer, Image, MetaPreview, ModalDialog } = require('stremio/components');
@ -18,7 +18,7 @@ const styles = require('./styles');
const MetaDetails = ({ urlParams, queryParams }) => {
const contentRef = React.useRef(null);
const { t } = useTranslation();
const { core } = useServices();
const core = useCore();
const metaDetails = useMetaDetails(urlParams);
const [season, setSeason] = useSeason(urlParams, queryParams);
const [tabs, metaExtension, clearMetaExtension] = useMetaExtensionTabs(metaDetails.metaExtensions);

View file

@ -5,9 +5,9 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { t } = require('i18next');
const { useCore } = require('stremio/core');
const { useProfile, usePlatform, useToast, useBinaryState } = require('stremio/common');
const { Button, Image, Popup } = require('stremio/components');
const { useServices } = require('stremio/services');
const { useRouteFocused } = require('stremio-router');
const StreamPlaceholder = require('./StreamPlaceholder');
const styles = require('./styles');
@ -16,7 +16,7 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
const profile = useProfile();
const toast = useToast();
const platform = usePlatform();
const { core } = useServices();
const core = useCore();
const routeFocused = useRouteFocused();
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);

View file

@ -6,7 +6,7 @@ const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { Button, Image, MultiselectMenu } = require('stremio/components');
const { useServices } = require('stremio/services');
const { useCore } = require('stremio/core');
const Stream = require('./Stream');
const styles = require('./styles');
const { usePlatform, useProfile } = require('stremio/common');
@ -16,7 +16,7 @@ const ALL_ADDONS_KEY = 'ALL';
const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => {
const { t } = useTranslation();
const { core } = useServices();
const core = useCore();
const platform = usePlatform();
const profile = useProfile();
const streamsContainerRef = React.useRef(null);

View file

@ -4,7 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { t } = require('i18next');
const { useServices } = require('stremio/services');
const { useCore } = require('stremio/core');
const { useProfile } = require('stremio/common');
const { Image, SearchBar, Toggle, Video } = require('stremio/components');
const SeasonsBar = require('./SeasonsBar');
@ -12,7 +12,7 @@ const { default: EpisodePicker } = require('../EpisodePicker');
const styles = require('./styles');
const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, selectedVideoId, toggleNotifications }) => {
const { core } = useServices();
const core = useCore();
const profile = useProfile();
const showNotificationsToggle = React.useMemo(() => {

View file

@ -4,14 +4,14 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const { useCore } = require('stremio/core');
const { usePlatform, useToast } = require('stremio/common');
const { useServices } = require('stremio/services');
const Option = require('./Option');
const styles = require('./styles');
const OptionsMenu = React.memo(React.forwardRef(({ className, stream, playbackDevices, extraSubtitlesTracks, selectedExtraSubtitlesTrackId }, ref) => {
const { t } = useTranslation();
const { core } = useServices();
const core = useCore();
const platform = usePlatform();
const toast = useToast();
const [streamingUrl, downloadUrl, magnetUrl] = React.useMemo(() => {

View file

@ -7,6 +7,7 @@ const debounce = require('lodash.debounce');
const langs = require('langs');
const { useTranslation } = require('react-i18next');
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');
@ -40,6 +41,7 @@ const GAMEPAD_HANDLER_ID = 'player';
const Player = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
const services = useServices();
const core = useCore();
const shell = useShell();
const gamepad = useGamepad();
const forceTranscoding = React.useMemo(() => {
@ -506,19 +508,19 @@ const Player = ({ urlParams, queryParams }) => {
);
}
};
const onCoreEvent = ({ event }) => {
if (event === 'PlayingOnDevice') {
const onCoreEvent = (name) => {
if (name === 'PlayingOnDevice') {
playingOnExternalDevice.current = true;
onPauseRequested();
}
};
services.chromecast.on('stateChanged', onChromecastServiceStateChange);
services.core.transport.on('CoreEvent', onCoreEvent);
core.on('event', onCoreEvent);
onChromecastServiceStateChange();
return () => {
toast.removeFilter(toastFilter);
services.chromecast.off('stateChanged', onChromecastServiceStateChange);
services.core.transport.off('CoreEvent', onCoreEvent);
core.off('event', onCoreEvent);
if (services.chromecast.active) {
services.chromecast.transport.off(
cast.framework.CastContextEventType.CAST_STATE_CHANGED,

View file

@ -3,7 +3,7 @@
import React, { useMemo, useCallback, useState, forwardRef, memo } from 'react';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
import { useServices } from 'stremio/services';
import { useCore } from 'stremio/core';
import { CONSTANTS } from 'stremio/common';
import { MetaPreview, Video } from 'stremio/components';
import SeasonsBar from 'stremio/routes/MetaDetails/VideosList/SeasonsBar';
@ -19,7 +19,7 @@ type Props = {
};
const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, className, closeSideDrawer, selected, ...props }: Props, ref) => {
const { core } = useServices();
const core = useCore();
const [season, setSeason] = useState<number>(seriesInfo?.season);
const [selectedVideoId, setSelectedVideoId] = useState<string | null>(null);

View file

@ -3,12 +3,12 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useServices } = require('stremio/services');
const { useCore } = require('stremio/core');
const { Video } = require('stremio/components');
const styles = require('./styles');
const VideosMenu = ({ className, metaItem, seriesInfo }) => {
const { core } = useServices();
const core = useCore();
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.videosMenuClosePrevented = true;

View file

@ -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 { useModelState, useCoreSuspender } = require('stremio/common');
const map = (player) => ({
@ -33,7 +33,7 @@ const map = (player) => ({
});
const usePlayer = (urlParams) => {
const { core } = useServices();
const core = useCore();
const { decodeStream } = useCoreSuspender();
const stream = decodeStream(urlParams.stream);
const action = React.useMemo(() => {

View file

@ -1,10 +1,10 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const { useServices } = require('stremio/services');
const { useCore } = require('stremio/core');
const useStatistics = (player, streamingServer) => {
const { core } = useServices();
const core = useCore();
const stream = React.useMemo(() => {
if (player.stream?.type === 'Ready') {

View file

@ -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');
const { useServices } = require('stremio/services');
const useSearch = (queryParams) => {
const { core } = useServices();
const core = useCore();
// TODO: refactor this to be in stremio-core-web
// React.useEffect(() => {
// let timerId = setTimeout(emitSearchEvent, 500);

View file

@ -1,7 +1,7 @@
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCore } from 'stremio/core';
import { Button } from 'stremio/components';
import { useServices } from 'stremio/services';
import { usePlatform, useToast } from 'stremio/common';
import { Section, Option, Link } from '../components';
import User from './User';
@ -14,7 +14,7 @@ type Props = {
const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
const { t } = useTranslation();
const { core } = useServices();
const core = useCore();
const platform = usePlatform();
const toast = useToast();
const [dataExport, loadDataExport] = useDataExport();

View file

@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useServices } from 'stremio/services';
import { useCore } from 'stremio/core';
import { Link } from '../../components';
import styles from './User.less';
@ -10,7 +10,7 @@ type Props = {
const User = ({ profile }: Props) => {
const { t } = useTranslation();
const { core } = useServices();
const core = useCore();
const avatar = useMemo(() => (
!profile.auth ?

View file

@ -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 { useModelState } = require('stremio/common');
const map = (dataExport) => ({
@ -13,7 +13,7 @@ const map = (dataExport) => ({
});
const useDataExport = () => {
const { core } = useServices();
const core = useCore();
const loadDataExport = React.useCallback(() => {
core.transport.dispatch({
action: 'Load',

View file

@ -1,9 +1,9 @@
import { useMemo } from 'react';
import { useCore } from 'stremio/core';
import { interfaceLanguages, useLanguageSorting } from 'stremio/common';
import { useServices } from 'stremio/services';
const useInterfaceOptions = (profile: Profile) => {
const { core } = useServices();
const core = useCore();
const interfaceLanguageOptions = useMemo(() =>
interfaceLanguages.map(({ name, codes }) => ({

View file

@ -1,13 +1,13 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCore } from 'stremio/core';
import { CONSTANTS, languageNames, useLanguageSorting, usePlatform } from 'stremio/common';
import { useServices } from 'stremio/services';
const LANGUAGES_NAMES: Record<string, string> = languageNames;
const usePlayerOptions = (profile: Profile) => {
const { t } = useTranslation();
const { core } = useServices();
const core = useCore();
const platform = usePlatform();
const languageOptions = useMemo(() => Object.keys(LANGUAGES_NAMES).map((code) => ({

View file

@ -1,12 +1,12 @@
// Copyright (C) 2017-2024 Smart code 203358507
import { useCallback } from 'react';
import { useCore } from 'stremio/core';
import { useModelState, useToast } from 'stremio/common';
import useProfile from 'stremio/common/useProfile';
import { useServices } from 'stremio/services';
const useStreamingServerUrls = () => {
const { core } = useServices();
const core = useCore();
const profile = useProfile();
const toast = useToast();
const ctx = useModelState({ model: 'ctx' });

View file

@ -3,7 +3,7 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { deepEqual } from 'fast-equals';
import { useServices } from 'stremio/services';
import { useCore } from 'stremio/core';
const CACHE_SIZES = [0, 2147483648, 5368709120, 10737418240, null];
@ -62,7 +62,7 @@ const TORRENT_PROFILES: Record<string, TorrentProfile> = {
};
const useStreamingOptions = (streamingServer: StreamingServer) => {
const { core } = useServices();
const core = useCore();
const { t } = useTranslation();
// TODO combine those useMemo in one

View file

@ -1,2 +0,0 @@
declare function Core(): Core;
export = Core;

View file

@ -1,92 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const EventEmitter = require('eventemitter3');
const CoreTransport = require('./CoreTransport');
function Core(args) {
let active = false;
let error = null;
let starting = false;
let transport = null;
const events = new EventEmitter();
function onTransportInit() {
active = true;
error = null;
starting = false;
onStateChanged();
}
function onTransportError(args) {
console.error(args);
active = false;
error = new Error('Stremio Core Transport initialization failed', { cause: args });
starting = false;
onStateChanged();
transport = null;
}
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;
}
starting = true;
transport = new CoreTransport(args);
transport.on('init', onTransportInit);
transport.on('error', onTransportError);
onStateChanged();
};
this.stop = function() {
active = false;
error = null;
starting = false;
onStateChanged();
if (transport !== null) {
transport.removeAllListeners();
transport = null;
}
};
this.on = function(name, listener) {
events.on(name, listener);
};
this.off = function(name, listener) {
events.off(name, listener);
};
}
module.exports = Core;

View file

@ -1,2 +0,0 @@
declare function CoreTransport(): CoreTransport;
export = CoreTransport;

View file

@ -1,60 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const EventEmitter = require('eventemitter3');
const Bridge = require('@stremio/stremio-core-web/bridge');
function CoreTransport(args) {
const events = new EventEmitter();
const worker = new Worker(`${process.env.COMMIT_HASH}/scripts/worker.js`);
const bridge = new Bridge(window, worker);
window.onCoreEvent = ({ name, args }) => {
try {
events.emit(name, args);
} catch (error) {
console.error('CoreTransport', error);
}
};
bridge.call(['init'], [args])
.then(() => {
try {
events.emit('init');
} catch (error) {
console.error('CoreTransport', error);
}
})
.catch((error) => {
events.emit('error', error);
});
this.on = function(name, listener) {
events.on(name, listener);
};
this.off = function(name, listener) {
events.off(name, listener);
};
this.removeAllListeners = function() {
events.removeAllListeners();
};
this.getState = async function(field) {
return bridge.call(['getState'], [field]);
};
this.getDebugState = async function() {
return bridge.call(['getDebugState'], []);
};
this.dispatch = async function(action, field) {
return bridge.call(['dispatch'], [action, field, location.hash]);
};
this.analytics = async function(event) {
return bridge.call(['analytics'], [event, location.hash]);
};
this.decodeStream = async function(stream) {
return bridge.call(['decodeStream'], [stream]);
};
this.encodeStream = async function(stream) {
return bridge.call(['encodeStream'], [stream]);
};
}
module.exports = CoreTransport;

View file

@ -1,12 +0,0 @@
type CoreEvent = {
name: string,
args: any[],
};
declare global {
interface Window {
onCoreEvent: (event: CoreEvent) => void;
}
}
export {};

View file

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

View file

@ -1,29 +0,0 @@
type Action = {
action: string,
args?: {
model?: string,
action?: string,
args?: any,
}
};
type AnalyticsEvent = {
event: string,
args: object,
};
interface CoreTransport {
start: (args: object) => Promise<void>,
getState: (model: string) => Promise<object>,
dispatch: (action: Action, model?: string) => Promise<void>,
decodeStream: (stream: string) => Promise<Stream>,
encodeStream: (stream: object) => Promise<string>,
analytics: (event: AnalyticsEvent) => Promise<void>,
on: (name: string, listener: () => void) => void,
off: (name: string, listener: () => void) => void,
}
interface Core {
active: boolean,
transport: CoreTransport,
}

View file

@ -1,5 +1,4 @@
type ServicesContext = {
core: Core,
shell: any,
chromecast: any,
keyboardShortcuts: any,

View file

@ -1,7 +1,6 @@
// Copyright (C) 2017-2023 Smart code 203358507
const Chromecast = require('./Chromecast');
const Core = require('./Core');
const DragAndDrop = require('./DragAndDrop');
const KeyboardShortcuts = require('./KeyboardShortcuts');
const { ServicesProvider, useServices } = require('./ServicesContext');
@ -10,7 +9,6 @@ const Shell = require('./Shell');
module.exports = {
Chromecast,
Core,
DragAndDrop,
KeyboardShortcuts,
ServicesProvider,

View file

@ -10,8 +10,9 @@ type Stream = {
description: string,
infoHash?: string,
fileIdx?: string,
url?: string,
externalUrl?: string,
deepLinks: {
deepLinks?: {
player: string,
externalPlayer: ExternalPlayerLinks,
},

View file

@ -6,6 +6,7 @@
"outDir": "./dist",
"module": "nodenext",
"moduleResolution": "nodenext",
"types": ["node"],
"paths": {
"stremio/*": ["./src/*"],
},