stremio-web/src/App/App.js
2026-05-05 04:22:59 +02:00

207 lines
8.1 KiB
JavaScript

// Copyright (C) 2017-2023 Smart code 203358507
require('spatial-navigation-polyfill');
const React = require('react');
const { useTranslation } = require('react-i18next');
const { useCore } = require('stremio/core');
const { Router } = require('stremio-router');
const { Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider, GamepadProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
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 withProtectedRoutes = require('./withProtectedRoutes');
const routerViewsConfig = require('./routerViewsConfig');
const styles = require('./styles');
const RouterWithProtectedRoutes = withProtectedRoutes(Router);
const App = () => {
const core = useCore();
const profile = useProfile();
const { i18n } = useTranslation();
const shell = useShell();
const [gamepadSupportEnabled, setGamepadSupportEnabled] = React.useState(false);
const onPathNotMatch = React.useCallback(() => {
return NotFound;
}, []);
const services = React.useMemo(() => {
return {
shell: new Shell(),
chromecast: new Chromecast(),
keyboardShortcuts: new KeyboardShortcuts(),
dragAndDrop: new DragAndDrop({ core })
};
}, []);
const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false);
const [gamepadModalOpen,, closeGamepadModal, toggleGamepadModal] = useBinaryState(false);
const onShortcut = React.useCallback((name) => {
switch (name) {
case 'shortcuts':
toggleShortcutModal();
break;
case 'gamepadGuide':
toggleGamepadModal();
break;
}
}, [toggleShortcutModal, toggleGamepadModal]);
React.useEffect(() => {
let prevPath = window.location.hash.slice(1);
const onLocationHashChange = () => {
core.transport.analytics({
event: 'LocationPathChanged',
args: { prevPath }
});
prevPath = window.location.hash.slice(1);
};
window.addEventListener('hashchange', onLocationHashChange);
return () => {
window.removeEventListener('hashchange', onLocationHashChange);
};
}, []);
React.useEffect(() => {
const onChromecastStateChange = () => {
if (services.chromecast.active) {
services.chromecast.transport.setOptions({
receiverApplicationId: CONSTANTS.CHROMECAST_RECEIVER_APP_ID,
autoJoinPolicy: chrome.cast.AutoJoinPolicy.PAGE_SCOPED,
resumeSavedSession: false,
language: null,
androidReceiverCompatible: true
});
}
};
services.chromecast.on('stateChanged', onChromecastStateChange);
services.shell.start();
services.chromecast.start();
services.keyboardShortcuts.start();
services.dragAndDrop.start();
window.services = services;
return () => {
services.shell.stop();
services.chromecast.stop();
services.keyboardShortcuts.stop();
services.dragAndDrop.stop();
services.chromecast.off('stateChanged', onChromecastStateChange);
};
}, []);
// Handle shell events
React.useEffect(() => {
const onOpenMedia = (data) => {
try {
const { protocol, hostname, pathname, searchParams } = new URL(data);
if (protocol === CONSTANTS.PROTOCOL) {
if (hostname.length) {
const transportUrl = `https://${hostname}${pathname}`;
window.location.href = `#/addons?addon=${encodeURIComponent(transportUrl)}`;
} else {
window.location.href = `#${pathname}?${searchParams.toString()}`;
}
}
} catch (e) {
console.error('Failed to open media:', e);
}
};
shell.on('open-media', onOpenMedia);
return () => {
shell.off('open-media', onOpenMedia);
};
}, []);
React.useEffect(() => {
if (typeof profile.settings?.interfaceLanguage === 'string') {
i18n.changeLanguage(profile.settings.interfaceLanguage);
}
if (typeof profile.settings?.gamepadSupport === 'boolean') {
setGamepadSupportEnabled(profile.settings.gamepadSupport);
}
if (profile.settings?.quitOnClose && shell.windowClosed) {
shell.send('quit');
}
}, [profile.settings, shell.windowClosed]);
React.useEffect(() => {
const onWindowFocus = () => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullAddonsFromAPI'
}
});
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullUserFromAPI',
args: {}
}
});
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'SyncLibraryWithAPI'
}
});
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'PullNotifications'
}
});
};
onWindowFocus();
window.addEventListener('focus', onWindowFocus);
return () => {
window.removeEventListener('focus', onWindowFocus);
};
}, []);
return (
<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 = withCoreSuspender(App);