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 => { + return bridge.call(['getState'], [model]); + }; + + const dispatch = (action: DispatchAction, model?: string): Promise => { + return bridge.call(['dispatch'], [action, model, location.hash]); + }; + + const encodeStream = (stream: Stream): Promise => { + return bridge.call(['encodeStream'], [stream]); + }; + + const decodeStream = (stream: string): Promise => { + return bridge.call(['decodeStream'], [stream]); + }; + + const analytics = (event: object): Promise => { + return bridge.call(['analytics'], [event, location.hash]); + }; + + return { + init, + getState, + dispatch, + encodeStream, + decodeStream, + analytics, + }; +}; + +export default createTransport; diff --git a/src/core/global.d.ts b/src/core/global.d.ts new file mode 100644 index 000000000..df63d9457 --- /dev/null +++ b/src/core/global.d.ts @@ -0,0 +1,8 @@ +interface Window { + core: CoreTransport | null | undefined, + onCoreEvent: ((event: NewStateEvent | CoreEventEvent) => void) | null; +} + +interface Bridge { + call(action: string[], args: any[]): Promise, +} diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 000000000..0ff9947df --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,7 @@ +import CoreProvider from './CoreProvider'; +import useCore from './useCore'; + +export { + CoreProvider, + useCore, +}; diff --git a/src/core/types.d.ts b/src/core/types.d.ts new file mode 100644 index 000000000..f2ffa00e4 --- /dev/null +++ b/src/core/types.d.ts @@ -0,0 +1,54 @@ +type DispatchAction = { + action: string, + args?: { + model?: string, + action?: string, + args?: any, + } +}; + +type CoreTransport = { + init: (args: object) => Promise, + getState: (model: string) => Promise, + dispatch: (action: DispatchAction, model?: string) => Promise, + encodeStream: (stream: Stream) => Promise, + decodeStream: (stream: string) => Promise, + analytics: (event: object) => Promise, +}; + +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, +}; diff --git a/src/core/useCore.ts b/src/core/useCore.ts new file mode 100644 index 000000000..4c6e4ecb9 --- /dev/null +++ b/src/core/useCore.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react'; +import CoreContext from './CoreContext'; + +const useCore = () => useContext(CoreContext); +export default useCore; diff --git a/src/index.js b/src/index.js index 519d32589..e9061296c 100755 --- a/src/index.js +++ b/src/index.js @@ -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(); +root.render( + + + +); if (process.env.NODE_ENV === 'production' && process.env.SERVICE_WORKER_DISABLED !== 'true' && process.env.SERVICE_WORKER_DISABLED !== true && 'serviceWorker' in navigator) { window.addEventListener('load', () => { diff --git a/src/modules.d.ts b/src/modules.d.ts index 3d5516efd..f260131b8 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -1,3 +1,5 @@ +declare module '@stremio/stremio-core-web/bridge'; + declare module '*.less' { const resource: Record; export = resource; diff --git a/src/routes/Addons/Addons.js b/src/routes/Addons/Addons.js index 86a45374c..671ceef04 100644 --- a/src/routes/Addons/Addons.js +++ b/src/routes/Addons/Addons.js @@ -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); diff --git a/src/routes/Board/StreamingServerWarning/StreamingServerWarning.tsx b/src/routes/Board/StreamingServerWarning/StreamingServerWarning.tsx index 7c9872c8b..c4826094f 100644 --- a/src/routes/Board/StreamingServerWarning/StreamingServerWarning.tsx +++ b/src/routes/Board/StreamingServerWarning/StreamingServerWarning.tsx @@ -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 => { diff --git a/src/routes/Board/useBoard.js b/src/routes/Board/useBoard.js index 30aa1ff3b..c0bb248b0 100644 --- a/src/routes/Board/useBoard.js +++ b/src/routes/Board/useBoard.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'); const useBoard = () => { - const { core } = useServices(); + const core = useCore(); const action = React.useMemo(() => ({ action: 'Load', args: { diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index e181dff5f..cd95fe1a3 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -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); diff --git a/src/routes/Discover/useDiscover.js b/src/routes/Discover/useDiscover.js index 78e41906b..d3f8a48f1 100644 --- a/src/routes/Discover/useDiscover.js +++ b/src/routes/Discover/useDiscover.js @@ -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', diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 1f041c004..f4ed1412a 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -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 ( diff --git a/src/routes/Library/useLibrary.js b/src/routes/Library/useLibrary.js index a7e1aeb68..7ec9f8f5c 100644 --- a/src/routes/Library/useLibrary.js +++ b/src/routes/Library/useLibrary.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'); const useLibrary = (model, urlParams, queryParams) => { - const { core } = useServices(); + const core = useCore(); const loadNextPage = React.useCallback(() => { core.transport.dispatch({ action: 'LibraryWithFilters', diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index 3cdd2d36a..805b2065d 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -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); diff --git a/src/routes/MetaDetails/StreamsList/Stream/Stream.js b/src/routes/MetaDetails/StreamsList/Stream/Stream.js index c556fa1e2..f5de3937e 100644 --- a/src/routes/MetaDetails/StreamsList/Stream/Stream.js +++ b/src/routes/MetaDetails/StreamsList/Stream/Stream.js @@ -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); diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 1ac56ab11..709e635b4 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -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); diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js index 3cc3832d6..b751a625d 100644 --- a/src/routes/MetaDetails/VideosList/VideosList.js +++ b/src/routes/MetaDetails/VideosList/VideosList.js @@ -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(() => { diff --git a/src/routes/Player/OptionsMenu/OptionsMenu.js b/src/routes/Player/OptionsMenu/OptionsMenu.js index cbeeca90e..4123b6e66 100644 --- a/src/routes/Player/OptionsMenu/OptionsMenu.js +++ b/src/routes/Player/OptionsMenu/OptionsMenu.js @@ -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(() => { diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 7a3bd0f12..5a76be3d9 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -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, diff --git a/src/routes/Player/SideDrawer/SideDrawer.tsx b/src/routes/Player/SideDrawer/SideDrawer.tsx index d5b13e11f..05ed63677 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.tsx +++ b/src/routes/Player/SideDrawer/SideDrawer.tsx @@ -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(({ seriesInfo, className, closeSideDrawer, selected, ...props }: Props, ref) => { - const { core } = useServices(); + const core = useCore(); const [season, setSeason] = useState(seriesInfo?.season); const [selectedVideoId, setSelectedVideoId] = useState(null); diff --git a/src/routes/Player/VideosMenu/VideosMenu.js b/src/routes/Player/VideosMenu/VideosMenu.js index 7140ea37f..24eab0467 100644 --- a/src/routes/Player/VideosMenu/VideosMenu.js +++ b/src/routes/Player/VideosMenu/VideosMenu.js @@ -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; diff --git a/src/routes/Player/usePlayer.js b/src/routes/Player/usePlayer.js index 2584517eb..f842db338 100644 --- a/src/routes/Player/usePlayer.js +++ b/src/routes/Player/usePlayer.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 { 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(() => { diff --git a/src/routes/Player/useStatistics.js b/src/routes/Player/useStatistics.js index d8b241a8c..682969181 100644 --- a/src/routes/Player/useStatistics.js +++ b/src/routes/Player/useStatistics.js @@ -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') { diff --git a/src/routes/Search/useSearch.js b/src/routes/Search/useSearch.js index 87ef24cda..436517ec7 100644 --- a/src/routes/Search/useSearch.js +++ b/src/routes/Search/useSearch.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'); -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); diff --git a/src/routes/Settings/General/General.tsx b/src/routes/Settings/General/General.tsx index 49cfdfdd6..238206bbc 100644 --- a/src/routes/Settings/General/General.tsx +++ b/src/routes/Settings/General/General.tsx @@ -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(({ profile }: Props, ref) => { const { t } = useTranslation(); - const { core } = useServices(); + const core = useCore(); const platform = usePlatform(); const toast = useToast(); const [dataExport, loadDataExport] = useDataExport(); diff --git a/src/routes/Settings/General/User/User.tsx b/src/routes/Settings/General/User/User.tsx index 7555e521e..620c41bca 100644 --- a/src/routes/Settings/General/User/User.tsx +++ b/src/routes/Settings/General/User/User.tsx @@ -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 ? diff --git a/src/routes/Settings/General/useDataExport.js b/src/routes/Settings/General/useDataExport.js index 773bcc2bd..22dcb4e3e 100644 --- a/src/routes/Settings/General/useDataExport.js +++ b/src/routes/Settings/General/useDataExport.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 { 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', diff --git a/src/routes/Settings/Interface/useInterfaceOptions.ts b/src/routes/Settings/Interface/useInterfaceOptions.ts index 94243fd5e..2feee8d69 100644 --- a/src/routes/Settings/Interface/useInterfaceOptions.ts +++ b/src/routes/Settings/Interface/useInterfaceOptions.ts @@ -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 }) => ({ diff --git a/src/routes/Settings/Player/usePlayerOptions.ts b/src/routes/Settings/Player/usePlayerOptions.ts index 69d651e64..f9ded1d11 100644 --- a/src/routes/Settings/Player/usePlayerOptions.ts +++ b/src/routes/Settings/Player/usePlayerOptions.ts @@ -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 = 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) => ({ diff --git a/src/routes/Settings/Streaming/URLsManager/useStreamingServerUrls.js b/src/routes/Settings/Streaming/URLsManager/useStreamingServerUrls.js index 401973d4e..03883f942 100644 --- a/src/routes/Settings/Streaming/URLsManager/useStreamingServerUrls.js +++ b/src/routes/Settings/Streaming/URLsManager/useStreamingServerUrls.js @@ -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' }); diff --git a/src/routes/Settings/Streaming/useStreamingOptions.ts b/src/routes/Settings/Streaming/useStreamingOptions.ts index 0e22afc0b..9ed96557b 100644 --- a/src/routes/Settings/Streaming/useStreamingOptions.ts +++ b/src/routes/Settings/Streaming/useStreamingOptions.ts @@ -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 = { }; const useStreamingOptions = (streamingServer: StreamingServer) => { - const { core } = useServices(); + const core = useCore(); const { t } = useTranslation(); // TODO combine those useMemo in one diff --git a/src/services/Core/Core.d.ts b/src/services/Core/Core.d.ts deleted file mode 100644 index ab03f3b1d..000000000 --- a/src/services/Core/Core.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare function Core(): Core; -export = Core; diff --git a/src/services/Core/Core.js b/src/services/Core/Core.js deleted file mode 100644 index db26be78e..000000000 --- a/src/services/Core/Core.js +++ /dev/null @@ -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; diff --git a/src/services/Core/CoreTransport.d.ts b/src/services/Core/CoreTransport.d.ts deleted file mode 100644 index 8a58f7451..000000000 --- a/src/services/Core/CoreTransport.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare function CoreTransport(): CoreTransport; -export = CoreTransport; diff --git a/src/services/Core/CoreTransport.js b/src/services/Core/CoreTransport.js deleted file mode 100644 index c497d75c6..000000000 --- a/src/services/Core/CoreTransport.js +++ /dev/null @@ -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; diff --git a/src/services/Core/globals.d.ts b/src/services/Core/globals.d.ts deleted file mode 100644 index dd7e39e4b..000000000 --- a/src/services/Core/globals.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -type CoreEvent = { - name: string, - args: any[], -}; - -declare global { - interface Window { - onCoreEvent: (event: CoreEvent) => void; - } -} - -export {}; diff --git a/src/services/Core/index.js b/src/services/Core/index.js deleted file mode 100644 index e8ce0b8c8..000000000 --- a/src/services/Core/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const Core = require('./Core'); - -module.exports = Core; diff --git a/src/services/Core/types.d.ts b/src/services/Core/types.d.ts deleted file mode 100644 index 668479e12..000000000 --- a/src/services/Core/types.d.ts +++ /dev/null @@ -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, - getState: (model: string) => Promise, - dispatch: (action: Action, model?: string) => Promise, - decodeStream: (stream: string) => Promise, - encodeStream: (stream: object) => Promise, - analytics: (event: AnalyticsEvent) => Promise, - on: (name: string, listener: () => void) => void, - off: (name: string, listener: () => void) => void, -} - -interface Core { - active: boolean, - transport: CoreTransport, -} diff --git a/src/services/ServicesContext/types.d.ts b/src/services/ServicesContext/types.d.ts index 432155719..45dde7ed9 100644 --- a/src/services/ServicesContext/types.d.ts +++ b/src/services/ServicesContext/types.d.ts @@ -1,5 +1,4 @@ type ServicesContext = { - core: Core, shell: any, chromecast: any, keyboardShortcuts: any, diff --git a/src/services/index.js b/src/services/index.js index 3a3b0128a..17f00719d 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -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, diff --git a/src/types/Stream.d.ts b/src/types/Stream.d.ts index 2400c9725..307b24dc9 100644 --- a/src/types/Stream.d.ts +++ b/src/types/Stream.d.ts @@ -10,8 +10,9 @@ type Stream = { description: string, infoHash?: string, fileIdx?: string, + url?: string, externalUrl?: string, - deepLinks: { + deepLinks?: { player: string, externalPlayer: ExternalPlayerLinks, }, diff --git a/tsconfig.json b/tsconfig.json index f4e9e951b..895625278 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "outDir": "./dist", "module": "nodenext", "moduleResolution": "nodenext", + "types": ["node"], "paths": { "stremio/*": ["./src/*"], },