mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-07-26 23:32:11 +00:00
fix: handle web UI updates safely
This commit is contained in:
parent
1c4ec6dac8
commit
07b152bdfc
15 changed files with 326 additions and 260 deletions
|
|
@ -49,7 +49,8 @@
|
|||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#5140f0db28f61b3f80939bed36df93512ca3e841",
|
||||
"url": "0.11.4",
|
||||
"use-long-press": "^3.3.0"
|
||||
"use-long-press": "^3.3.0",
|
||||
"workbox-window": "7.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
|
|
|
|||
|
|
@ -104,6 +104,9 @@ importers:
|
|||
use-long-press:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0(react@18.3.1)
|
||||
workbox-window:
|
||||
specifier: 7.4.1
|
||||
version: 7.4.1
|
||||
devDependencies:
|
||||
'@babel/core':
|
||||
specifier: 7.29.0
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ const ServicesToaster = require('./ServicesToaster');
|
|||
const SearchParamsHandler = require('./SearchParamsHandler');
|
||||
const DeepLinkHandler = require('./DeepLinkHandler');
|
||||
const { default: UpdaterBanner } = require('./UpdaterBanner');
|
||||
const { default: WebUpdateScreen } = require('./WebUpdateScreen');
|
||||
const { default: ShortcutsModal } = require('./ShortcutsModal');
|
||||
const { default: GamepadModal } = require('./GamepadModal');
|
||||
const styles = require('./styles');
|
||||
|
|
@ -199,7 +198,6 @@ const App = () => {
|
|||
<SearchParamsHandler />
|
||||
<DeepLinkHandler />
|
||||
<UpdaterBanner className={styles['updater-banner-container']} />
|
||||
<WebUpdateScreen />
|
||||
<ProtectedRoutes />
|
||||
</DiscordProvider>
|
||||
</FullscreenProvider>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMatch } from 'react-router';
|
||||
import { useBinaryState, usePlatform } from 'stremio/common';
|
||||
import { Button, Transition } from 'stremio/components';
|
||||
import styles from './UpdaterBanner.less';
|
||||
import { UpdateBanner } from 'stremio/components';
|
||||
|
||||
type Props = {
|
||||
className: string,
|
||||
|
|
@ -16,9 +14,9 @@ const UpdaterBanner = ({ className }: Props) => {
|
|||
const [visible, show, hide] = useBinaryState(false);
|
||||
const isPlayer = useMatch('/player/*');
|
||||
|
||||
const onInstallClick = () => {
|
||||
const onInstallClick = useCallback(() => {
|
||||
shell.send('autoupdater-notif-clicked');
|
||||
};
|
||||
}, [shell]);
|
||||
|
||||
useEffect(() => {
|
||||
shell.on('autoupdater-show-notif', show);
|
||||
|
|
@ -29,21 +27,15 @@ const UpdaterBanner = ({ className }: Props) => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Transition when={visible && !isPlayer} name={'slide-up'}>
|
||||
<div className={styles['updater-banner']}>
|
||||
<div className={styles['label']}>
|
||||
{ t('UPDATER_TITLE') }
|
||||
</div>
|
||||
<Button className={styles['button']} onClick={onInstallClick}>
|
||||
{ t('UPDATER_INSTALL_BUTTON') }
|
||||
</Button>
|
||||
<Button className={styles['close']} onClick={hide}>
|
||||
<Icon className={styles['icon']} name={'close'} />
|
||||
</Button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<UpdateBanner
|
||||
className={className}
|
||||
visible={visible && isPlayer === null}
|
||||
label={t('UPDATER_TITLE')}
|
||||
actionLabel={t('UPDATER_INSTALL_BUTTON')}
|
||||
closeLabel={t('BUTTON_CLOSE')}
|
||||
onAction={onInstallClick}
|
||||
onClose={hide}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
|
||||
.web-update-banner {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.web-update-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
@ -30,9 +38,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
100% {
|
||||
|
|
@ -40,13 +48,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes progress-fill {
|
||||
@keyframes progress {
|
||||
0% {
|
||||
transform: scaleX(0);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
transform: translateX(250%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +64,7 @@
|
|||
height: 8rem;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
animation: 1s linear infinite alternate flash;
|
||||
animation: 1s linear infinite alternate pulse;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
@ -76,6 +84,7 @@
|
|||
height: 0.4rem;
|
||||
margin-top: 0.25rem;
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
|
@ -93,12 +102,11 @@
|
|||
.progress-value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background-color: var(--primary-accent-color);
|
||||
transform-origin: left;
|
||||
animation: 2s ease-out forwards progress-fill;
|
||||
animation: 1.2s ease-in-out infinite progress;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 30rem) {
|
||||
|
|
@ -118,4 +126,18 @@
|
|||
width: 15rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
|
||||
.logo {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
width: 100%;
|
||||
opacity: 0.65;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,40 +3,59 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMatch } from 'react-router';
|
||||
import { useServiceWorkerUpdater, useTimeout } from 'stremio/common';
|
||||
import { UpdateBanner } from 'stremio/components';
|
||||
import useServiceWorkerUpdater from './useServiceWorkerUpdater';
|
||||
import styles from './WebUpdateScreen.less';
|
||||
|
||||
const APPLY_DELAY = 2000;
|
||||
|
||||
const WebUpdateScreen = () => {
|
||||
const { t } = useTranslation();
|
||||
const { updateReady, applyUpdate } = useServiceWorkerUpdater();
|
||||
const { state, dismissed, applyUpdate, dismissUpdate } = useServiceWorkerUpdater();
|
||||
const isPlayer = useMatch('/player/*');
|
||||
const timeout = useTimeout(APPLY_DELAY);
|
||||
const visible = updateReady && isPlayer === null;
|
||||
const autoApply = state.status === 'ready' && state.autoApply;
|
||||
const promptVisible =
|
||||
!dismissed &&
|
||||
isPlayer === null &&
|
||||
(state.status === 'ready' || state.status === 'reload-ready' || state.status === 'failed');
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
timeout.start(applyUpdate);
|
||||
|
||||
return timeout.cancel;
|
||||
if (autoApply && isPlayer === null) {
|
||||
applyUpdate();
|
||||
}
|
||||
}, [visible]);
|
||||
}, [applyUpdate, autoApply, isPlayer]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
if (state.status === 'applying') {
|
||||
return (
|
||||
<div
|
||||
className={styles['web-update-screen']}
|
||||
role={'status'}
|
||||
aria-live={'polite'}
|
||||
aria-busy={true}
|
||||
>
|
||||
<img
|
||||
className={styles['logo']}
|
||||
src={require('/assets/images/stremio_symbol.png')}
|
||||
alt={''}
|
||||
/>
|
||||
<div className={styles['title']}>
|
||||
{t('UPDATER_TITLE')}
|
||||
</div>
|
||||
<div className={styles['progress']} aria-hidden={true}>
|
||||
<div className={styles['progress-value']} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles['web-update-screen']}>
|
||||
<img className={styles['logo']} src={require('/assets/images/stremio_symbol.png')} alt={' '} />
|
||||
<div className={styles['title']}>
|
||||
{t('UPDATER_TITLE')}
|
||||
</div>
|
||||
<div className={styles['progress']} aria-hidden={true}>
|
||||
<div className={styles['progress-value']} />
|
||||
</div>
|
||||
</div>
|
||||
<UpdateBanner
|
||||
className={styles['web-update-banner']}
|
||||
visible={promptVisible}
|
||||
label={t('UPDATER_TITLE')}
|
||||
actionLabel={state.status === 'failed' ? t('TRY_AGAIN') : t('RELOAD_UI')}
|
||||
closeLabel={t('BUTTON_CLOSE')}
|
||||
onAction={applyUpdate}
|
||||
onClose={dismissUpdate}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
185
src/App/WebUpdateScreen/useServiceWorkerUpdater.ts
Normal file
185
src/App/WebUpdateScreen/useServiceWorkerUpdater.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
// Copyright (C) 2017-2026 Smart code 203358507
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Workbox, type WorkboxLifecycleEvent, type WorkboxLifecycleWaitingEvent } from 'workbox-window';
|
||||
import { usePlatform } from 'stremio/common';
|
||||
import usePWA from 'stremio/common/usePWA';
|
||||
|
||||
const UPDATE_CHECK_INTERVAL = 30 * 60 * 1000;
|
||||
const APPLY_TIMEOUT = 15 * 1000;
|
||||
|
||||
type UpdaterState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'ready', autoApply: boolean }
|
||||
| { status: 'applying' }
|
||||
| { status: 'reload-ready' }
|
||||
| { status: 'failed' };
|
||||
|
||||
type Runtime = {
|
||||
workbox: Workbox | null,
|
||||
applying: boolean,
|
||||
timeout: ReturnType<typeof setTimeout> | null,
|
||||
checkForUpdate: (() => void) | null,
|
||||
};
|
||||
|
||||
const clearApplyTimeout = (runtime: Runtime) => {
|
||||
if (runtime.timeout !== null) {
|
||||
clearTimeout(runtime.timeout);
|
||||
runtime.timeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
const useServiceWorkerUpdater = () => {
|
||||
const { shell } = usePlatform();
|
||||
const [isIOSPWA, isStandalonePWA] = usePWA();
|
||||
const [state, setState] = useState<UpdaterState>({ status: 'idle' });
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const runtimeRef = useRef<Runtime>({
|
||||
workbox: null,
|
||||
applying: false,
|
||||
timeout: null,
|
||||
checkForUpdate: null,
|
||||
});
|
||||
const appLike = shell.active || Boolean(isIOSPWA) || Boolean(isStandalonePWA);
|
||||
|
||||
const dismissUpdate = useCallback(() => {
|
||||
setDismissed(true);
|
||||
}, []);
|
||||
|
||||
const applyUpdate = useCallback(() => {
|
||||
if (state.status === 'reload-ready') {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const runtime = runtimeRef.current;
|
||||
if (
|
||||
runtime.applying ||
|
||||
runtime.workbox === null ||
|
||||
(state.status !== 'ready' && state.status !== 'failed')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.applying = true;
|
||||
clearApplyTimeout(runtime);
|
||||
setState({ status: 'applying' });
|
||||
runtime.timeout = setTimeout(() => {
|
||||
runtime.timeout = null;
|
||||
runtime.applying = false;
|
||||
setState({ status: 'failed' });
|
||||
}, APPLY_TIMEOUT);
|
||||
runtime.workbox.messageSkipWaiting();
|
||||
}, [state.status]);
|
||||
|
||||
useEffect(() => {
|
||||
const serviceWorkerDisabled = process.env.SERVICE_WORKER_DISABLED as string | boolean | undefined;
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' ||
|
||||
serviceWorkerDisabled === 'true' ||
|
||||
serviceWorkerDisabled === true ||
|
||||
!('serviceWorker' in navigator)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtime = runtimeRef.current;
|
||||
const workbox = new Workbox('service-worker.js');
|
||||
let registered = false;
|
||||
let lastUpdateCheck = Date.now();
|
||||
runtime.workbox = workbox;
|
||||
|
||||
const onWaiting = (event: WorkboxLifecycleWaitingEvent) => {
|
||||
setDismissed(false);
|
||||
setState({
|
||||
status: 'ready',
|
||||
autoApply: appLike && event.wasWaitingBeforeRegister === true,
|
||||
});
|
||||
};
|
||||
const onControlling = (event: WorkboxLifecycleEvent) => {
|
||||
if (!event.isUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearApplyTimeout(runtime);
|
||||
if (runtime.applying) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
setDismissed(false);
|
||||
setState({ status: 'reload-ready' });
|
||||
}
|
||||
};
|
||||
const checkForUpdate = () => {
|
||||
const now = Date.now();
|
||||
if (
|
||||
!registered ||
|
||||
navigator.serviceWorker.controller === null ||
|
||||
now - lastUpdateCheck < UPDATE_CHECK_INTERVAL
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastUpdateCheck = now;
|
||||
workbox.update().catch((error) => {
|
||||
console.error('SW update check failed: ', error);
|
||||
});
|
||||
};
|
||||
runtime.checkForUpdate = checkForUpdate;
|
||||
|
||||
workbox.addEventListener('waiting', onWaiting);
|
||||
workbox.addEventListener('controlling', onControlling);
|
||||
workbox.register()
|
||||
.then(() => {
|
||||
registered = true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('SW registration failed: ', error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
registered = false;
|
||||
workbox.removeEventListener('waiting', onWaiting);
|
||||
workbox.removeEventListener('controlling', onControlling);
|
||||
clearApplyTimeout(runtime);
|
||||
runtime.applying = false;
|
||||
runtime.checkForUpdate = null;
|
||||
if (runtime.workbox === workbox) {
|
||||
runtime.workbox = null;
|
||||
}
|
||||
};
|
||||
}, [appLike]);
|
||||
|
||||
useEffect(() => {
|
||||
const onForeground = () => {
|
||||
setDismissed(false);
|
||||
runtimeRef.current.checkForUpdate?.();
|
||||
};
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
onForeground();
|
||||
}
|
||||
};
|
||||
const onShellVisibilityChange = ({ visible }: { visible?: boolean }) => {
|
||||
if (visible) {
|
||||
onForeground();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
shell.on('win-visibility-changed', onShellVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
shell.off('win-visibility-changed', onShellVisibilityChange);
|
||||
};
|
||||
}, [shell]);
|
||||
|
||||
return {
|
||||
state,
|
||||
dismissed,
|
||||
applyUpdate,
|
||||
dismissUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
export default useServiceWorkerUpdater;
|
||||
|
|
@ -31,7 +31,6 @@ const useTorrent = require('./useTorrent');
|
|||
const useTranslate = require('./useTranslate');
|
||||
const { default: useOrientation } = require('./useOrientation');
|
||||
const { default: useLanguageSorting } = require('./useLanguageSorting');
|
||||
const { registerServiceWorker, useServiceWorkerUpdater } = require('./useServiceWorkerUpdater');
|
||||
|
||||
module.exports = {
|
||||
FileDropProvider,
|
||||
|
|
@ -77,6 +76,4 @@ module.exports = {
|
|||
useTranslate,
|
||||
useOrientation,
|
||||
useLanguageSorting,
|
||||
registerServiceWorker,
|
||||
useServiceWorkerUpdater,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,193 +0,0 @@
|
|||
// Copyright (C) 2017-2026 Smart code 203358507
|
||||
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
type Snapshot = {
|
||||
updateReady: boolean,
|
||||
reloadPending: boolean,
|
||||
};
|
||||
|
||||
const INITIAL_SNAPSHOT: Snapshot = {
|
||||
updateReady: false,
|
||||
reloadPending: false,
|
||||
};
|
||||
|
||||
let snapshot = INITIAL_SNAPSHOT;
|
||||
let registration: ServiceWorkerRegistration | null = null;
|
||||
let waitingWorker: ServiceWorker | null = null;
|
||||
let promptReady = false;
|
||||
let reloadPending = false;
|
||||
let reloading = false;
|
||||
let applyingUpdate = false;
|
||||
let promptOnNextUpdateFound = false;
|
||||
let promptOnNextUpdateFoundTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
const subscribe = (listener: () => void) => {
|
||||
listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
const getSnapshot = () => snapshot;
|
||||
|
||||
const emitSnapshot = () => {
|
||||
const nextSnapshot = {
|
||||
updateReady: promptReady && (waitingWorker !== null || reloadPending),
|
||||
reloadPending,
|
||||
};
|
||||
|
||||
if (snapshot.updateReady !== nextSnapshot.updateReady || snapshot.reloadPending !== nextSnapshot.reloadPending) {
|
||||
snapshot = nextSnapshot;
|
||||
listeners.forEach((listener) => listener());
|
||||
}
|
||||
};
|
||||
|
||||
const clearPromptOnNextUpdateFound = () => {
|
||||
promptOnNextUpdateFound = false;
|
||||
|
||||
if (promptOnNextUpdateFoundTimeout !== null) {
|
||||
clearTimeout(promptOnNextUpdateFoundTimeout);
|
||||
promptOnNextUpdateFoundTimeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
const markPromptOnNextUpdateFound = () => {
|
||||
clearPromptOnNextUpdateFound();
|
||||
promptOnNextUpdateFound = true;
|
||||
promptOnNextUpdateFoundTimeout = setTimeout(clearPromptOnNextUpdateFound, 60000);
|
||||
};
|
||||
|
||||
const consumePromptOnNextUpdateFound = () => {
|
||||
const shouldPrompt = promptOnNextUpdateFound;
|
||||
clearPromptOnNextUpdateFound();
|
||||
|
||||
return shouldPrompt;
|
||||
};
|
||||
|
||||
const reloadPage = () => {
|
||||
if (reloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
reloading = true;
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const setWaitingWorker = (worker: ServiceWorker, shouldPrompt: boolean) => {
|
||||
waitingWorker = worker;
|
||||
promptReady = shouldPrompt || promptReady;
|
||||
emitSnapshot();
|
||||
};
|
||||
|
||||
const trackInstalling = (worker: ServiceWorker | null, shouldPrompt: boolean) => {
|
||||
if (worker === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onStateChange = () => {
|
||||
if (worker.state === 'installed' && navigator.serviceWorker.controller !== null) {
|
||||
setWaitingWorker(worker, shouldPrompt);
|
||||
}
|
||||
};
|
||||
|
||||
worker.addEventListener('statechange', onStateChange);
|
||||
onStateChange();
|
||||
};
|
||||
|
||||
const checkForUpdate = () => {
|
||||
if (registration === null || navigator.serviceWorker.controller === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
markPromptOnNextUpdateFound();
|
||||
registration.update().catch((error) => {
|
||||
clearPromptOnNextUpdateFound();
|
||||
console.error('SW update check failed: ', error);
|
||||
});
|
||||
};
|
||||
|
||||
const showPendingUpdate = () => {
|
||||
if (waitingWorker !== null || reloadPending) {
|
||||
promptReady = true;
|
||||
emitSnapshot();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const applyUpdate = () => {
|
||||
if (reloadPending) {
|
||||
reloadPage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (waitingWorker !== null) {
|
||||
applyingUpdate = true;
|
||||
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
};
|
||||
|
||||
const registerServiceWorker = async () => {
|
||||
let hadController = navigator.serviceWorker.controller !== null;
|
||||
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (!hadController) {
|
||||
hadController = true;
|
||||
return;
|
||||
}
|
||||
|
||||
waitingWorker = null;
|
||||
|
||||
if (applyingUpdate) {
|
||||
reloadPage();
|
||||
return;
|
||||
}
|
||||
|
||||
reloadPending = true;
|
||||
promptReady = false;
|
||||
emitSnapshot();
|
||||
});
|
||||
|
||||
try {
|
||||
registration = await navigator.serviceWorker.register('service-worker.js');
|
||||
|
||||
registration.addEventListener('updatefound', () => {
|
||||
trackInstalling(registration?.installing ?? null, consumePromptOnNextUpdateFound());
|
||||
});
|
||||
|
||||
trackInstalling(registration.installing, true);
|
||||
|
||||
if (registration.waiting !== null && navigator.serviceWorker.controller !== null) {
|
||||
setWaitingWorker(registration.waiting, true);
|
||||
} else {
|
||||
checkForUpdate();
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible' && !showPendingUpdate()) {
|
||||
checkForUpdate();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('SW registration failed: ', error);
|
||||
}
|
||||
};
|
||||
|
||||
const useServiceWorkerUpdater = () => {
|
||||
const updaterSnapshot = useSyncExternalStore(subscribe, getSnapshot);
|
||||
|
||||
return {
|
||||
...updaterSnapshot,
|
||||
applyUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
registerServiceWorker,
|
||||
useServiceWorkerUpdater,
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.updater-banner {
|
||||
.update-banner {
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -43,4 +43,4 @@
|
|||
height: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/components/UpdateBanner/UpdateBanner.tsx
Normal file
37
src/components/UpdateBanner/UpdateBanner.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (C) 2017-2026 Smart code 203358507
|
||||
|
||||
import React from 'react';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import Button from '../Button';
|
||||
import Transition from '../Transition';
|
||||
import styles from './UpdateBanner.less';
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
visible: boolean,
|
||||
label: string,
|
||||
actionLabel: string,
|
||||
closeLabel: string,
|
||||
onAction: () => void,
|
||||
onClose: () => void,
|
||||
};
|
||||
|
||||
const UpdateBanner = ({ className, visible, label, actionLabel, closeLabel, onAction, onClose }: Props) => (
|
||||
<div className={className}>
|
||||
<Transition when={visible} name={'slide-up'}>
|
||||
<div className={styles['update-banner']} role={'status'}>
|
||||
<div className={styles['label']}>
|
||||
{label}
|
||||
</div>
|
||||
<Button className={styles['button']} onClick={onAction}>
|
||||
{actionLabel}
|
||||
</Button>
|
||||
<Button className={styles['close']} title={closeLabel} onClick={onClose}>
|
||||
<Icon className={styles['icon']} name={'close'} />
|
||||
</Button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default UpdateBanner;
|
||||
3
src/components/UpdateBanner/index.ts
Normal file
3
src/components/UpdateBanner/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import UpdateBanner from './UpdateBanner';
|
||||
|
||||
export default UpdateBanner;
|
||||
|
|
@ -29,6 +29,7 @@ import ShortcutsGroup from './ShortcutsGroup';
|
|||
import TextInput from './TextInput';
|
||||
import Toggle from './Toggle';
|
||||
import Transition from './Transition';
|
||||
import UpdateBanner from './UpdateBanner';
|
||||
import Video from './Video';
|
||||
import ActionsGroup from './ActionsGroup';
|
||||
|
||||
|
|
@ -65,6 +66,7 @@ export {
|
|||
TextInput,
|
||||
Toggle,
|
||||
Transition,
|
||||
UpdateBanner,
|
||||
Video,
|
||||
ActionsGroup
|
||||
};
|
||||
|
|
|
|||
12
src/index.js
12
src/index.js
|
|
@ -18,8 +18,9 @@ const i18n = require('i18next');
|
|||
const { initReactI18next } = require('react-i18next');
|
||||
const stremioTranslations = require('stremio-translations');
|
||||
const App = require('./App');
|
||||
const { default: WebUpdateScreen } = require('./App/WebUpdateScreen');
|
||||
const { CoreProvider } = require('./core');
|
||||
const { FileDropProvider, PlatformProvider, registerServiceWorker } = require('./common');
|
||||
const { FileDropProvider, PlatformProvider } = require('./common');
|
||||
|
||||
const translations = Object.fromEntries(Object.entries(stremioTranslations()).map(([key, value]) => [key, {
|
||||
translation: value
|
||||
|
|
@ -48,14 +49,13 @@ root.render(
|
|||
<CoreProvider appInfo={appInfo}>
|
||||
<FileDropProvider>
|
||||
<HashRouter>
|
||||
<App />
|
||||
<>
|
||||
<WebUpdateScreen />
|
||||
<App />
|
||||
</>
|
||||
</HashRouter>
|
||||
</FileDropProvider>
|
||||
</CoreProvider>
|
||||
</PlatformProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && process.env.SERVICE_WORKER_DISABLED !== 'true' && process.env.SERVICE_WORKER_DISABLED !== true && 'serviceWorker' in navigator) {
|
||||
window.addEventListener('load', registerServiceWorker);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ module.exports = (env, argv) => ({
|
|||
new WorkboxPlugin.GenerateSW({
|
||||
maximumFileSizeToCacheInBytes: 20000000,
|
||||
clientsClaim: true,
|
||||
skipWaiting: false
|
||||
skipWaiting: true
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue