Merge branch 'development' of https://github.com/Stremio/stremio-web into feat/player-default-subtitles

This commit is contained in:
Tim 2022-11-28 10:30:12 +01:00
commit b021f538da
22 changed files with 1974 additions and 61 deletions

BIN
favicons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
images/icon_x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
images/icon_x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

40
manifest.json Normal file
View file

@ -0,0 +1,40 @@
{
"short_name": "Stremio",
"name": "Stremio Web",
"description": "Freedom To Stream",
"icons": [
{
"src": "favicons/favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "images/icon_x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "images/icon_x512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "images/maskable_icon_x192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "maskable"
},
{
"src": "images/maskable_icon_x512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": "https://web.stremio.com",
"scope": "https://web.stremio.com",
"display": "standalone",
"orientation": "natural",
"theme_color": "#2a2843",
"background_color": "#161523"
}

1681
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -14,16 +14,17 @@
"dependencies": {
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "4.0.1",
"@stremio/stremio-colors": "5.0.1",
"@stremio/stremio-core-web": "0.44.6",
"@stremio/stremio-icons": "3.0.5",
"@stremio/stremio-video": "0.0.23",
"@stremio/stremio-icons": "4.0.0",
"@stremio/stremio-video": "0.0.24",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
"classnames": "2.3.1",
"eventemitter3": "4.0.7",
"filter-invalid-dom-props": "2.1.0",
"hat": "0.0.3",
"langs": "^2.0.0",
"lodash.debounce": "4.0.8",
"lodash.intersection": "4.4.0",
@ -62,6 +63,7 @@
"terser-webpack-plugin": "5.2.4",
"webpack": "5.61.0",
"webpack-cli": "4.9.1",
"webpack-dev-server": "4.7.4"
"webpack-dev-server": "4.7.4",
"workbox-webpack-plugin": "^6.5.3"
}
}

View file

@ -7,8 +7,10 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="Stremio">
<link rel="icon" type="image/png" sizes="96x96" href="<%= htmlWebpackPlugin.options.faviconsPath %>/icon-96.png">
<link rel="apple-touch-icon" href="<%= htmlWebpackPlugin.options.faviconsPath %>/icon-96.png" />
<title>Stremio - All you can watch!</title>
<link rel="manifest" href="<%= htmlWebpackPlugin.options.manifestPath %>" />
<meta name="theme-color" content="<%= htmlWebpackPlugin.options.themeColor %>">
<link rel="apple-touch-icon" href="<%= htmlWebpackPlugin.options.imagesPath %>/icon_x192.png">
<title>Stremio - Freedom to Stream</title>
<%= htmlWebpackPlugin.tags.headTags %>
</head>

View file

@ -17,3 +17,12 @@ const App = require('./App');
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(<App />);
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.catch((registrationError) => {
console.error('SW registration failed: ', registrationError);
});
});
}

View file

@ -18,6 +18,7 @@ const ControlBar = ({
duration,
volume,
muted,
playbackSpeed,
subtitlesTracks,
audioTracks,
metaItem,
@ -30,6 +31,7 @@ const ControlBar = ({
onSeekRequested,
onToggleSubtitlesMenu,
onToggleInfoMenu,
onToggleSpeedMenu,
onToggleVideosMenu,
...props
}) => {
@ -42,6 +44,9 @@ const ControlBar = ({
const onInfoButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.infoMenuClosePrevented = true;
}, []);
const onSpeedButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
const onVideosButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.videosMenuClosePrevented = true;
}, []);
@ -86,6 +91,11 @@ const ControlBar = ({
onToggleInfoMenu();
}
}, [onToggleInfoMenu]);
const onSpeedButtonClick = React.useCallback(() => {
if (typeof onToggleSpeedMenu === 'function') {
onToggleSpeedMenu();
}
}, [onToggleSpeedMenu]);
const onVideosButtonClick = React.useCallback(() => {
if (typeof onToggleVideosMenu === 'function') {
onToggleVideosMenu();
@ -145,6 +155,9 @@ const ControlBar = ({
<Icon className={styles['icon']} icon={'ic_more'} />
</Button>
<div className={classnames(styles['control-bar-buttons-menu-container'], { 'open': buttonsMenuOpen })}>
<Button className={classnames(styles['control-bar-button'], { 'disabled': playbackSpeed === null })} tabIndex={-1} onMouseDown={onSpeedButtonMouseDown} onClick={onSpeedButtonClick}>
<Icon className={styles['icon']} icon={'ic_speedometer'} />
</Button>
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_network'} />
</Button>
@ -178,6 +191,7 @@ ControlBar.propTypes = {
duration: PropTypes.number,
volume: PropTypes.number,
muted: PropTypes.bool,
playbackSpeed: PropTypes.number,
subtitlesTracks: PropTypes.array,
audioTracks: PropTypes.array,
metaItem: PropTypes.object,
@ -190,6 +204,7 @@ ControlBar.propTypes = {
onSeekRequested: PropTypes.func,
onToggleSubtitlesMenu: PropTypes.func,
onToggleInfoMenu: PropTypes.func,
onToggleSpeedMenu: PropTypes.func,
onToggleVideosMenu: PropTypes.func
};

View file

@ -14,6 +14,7 @@ const ControlBar = require('./ControlBar');
const InfoMenu = require('./InfoMenu');
const VideosMenu = require('./VideosMenu');
const SubtitlesMenu = require('./SubtitlesMenu');
const SpeedMenu = require('./SpeedMenu');
const Video = require('./Video');
const usePlayer = require('./usePlayer');
const useSettings = require('./useSettings');
@ -40,6 +41,7 @@ const Player = ({ urlParams, queryParams }) => {
const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
const defaultSubtitlesSelected = React.useRef(false);
const [error, setError] = React.useState(null);
@ -54,6 +56,7 @@ const Player = ({ urlParams, queryParams }) => {
buffering: null,
volume: null,
muted: null,
playbackSpeed: null,
audioTracks: [],
selectedAudioTrackId: null,
subtitlesTracks: [],
@ -161,6 +164,9 @@ const Player = ({ urlParams, queryParams }) => {
const onSeekRequested = React.useCallback((time) => {
dispatch({ type: 'setProp', propName: 'time', propValue: time });
}, []);
const onPlaybackSpeedChanged = React.useCallback((rate) => {
dispatch({ type: 'setProp', propName: 'playbackSpeed', propValue: rate });
}, []);
const onSubtitlesTrackSelected = React.useCallback((id) => {
dispatch({ type: 'setProp', propName: 'selectedSubtitlesTrackId', propValue: id });
dispatch({ type: 'setProp', propName: 'selectedExtraSubtitlesTrackId', propValue: null });
@ -202,6 +208,9 @@ const Player = ({ urlParams, queryParams }) => {
if (!event.nativeEvent.infoMenuClosePrevented) {
closeInfoMenu();
}
if (!event.nativeEvent.speedMenuClosePrevented) {
closeSpeedMenu();
}
if (!event.nativeEvent.videosMenuClosePrevented) {
closeVideosMenu();
}
@ -242,7 +251,10 @@ const Player = ({ urlParams, queryParams }) => {
[]
},
autoplay: true,
time: player.libraryItem !== null && player.selected.streamRequest !== null && player.libraryItem.state.video_id === player.selected.streamRequest.id ?
time: player.libraryItem !== null &&
player.selected.streamRequest !== null &&
player.selected.streamRequest.path !== null &&
player.libraryItem.state.video_id === player.selected.streamRequest.path.id ?
player.libraryItem.state.timeOffset
:
0,
@ -344,6 +356,11 @@ const Player = ({ urlParams, queryParams }) => {
closeVideosMenu();
}
}, [player.metaItem]);
React.useEffect(() => {
if (videoState.playbackSpeed === null) {
closeSpeedMenu();
}
}, [videoState.playbackSpeed]);
React.useEffect(() => {
const intervalId = setInterval(() => {
pushToLibrary();
@ -384,7 +401,7 @@ const Player = ({ urlParams, queryParams }) => {
const onKeyDown = (event) => {
switch (event.code) {
case 'Space': {
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.paused !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen&& videoState.paused !== null) {
if (videoState.paused) {
onPlayRequested();
} else {
@ -395,7 +412,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'ArrowRight': {
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) {
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
onSeekRequested(videoState.time + (settings.seekTimeDuration * seekTimeMultiplier));
}
@ -403,7 +420,7 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'ArrowLeft': {
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.time !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.time !== null) {
const seekTimeMultiplier = event.shiftKey ? 3 : 1;
onSeekRequested(videoState.time - (settings.seekTimeDuration * seekTimeMultiplier));
}
@ -411,14 +428,14 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'ArrowUp': {
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume + 5);
}
break;
}
case 'ArrowDown': {
if (!subtitlesMenuOpen && !infoMenuOpen && videoState.volume !== null) {
if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && videoState.volume !== null) {
onVolumeChangeRequested(videoState.volume - 5);
}
@ -426,6 +443,7 @@ const Player = ({ urlParams, queryParams }) => {
}
case 'KeyS': {
closeInfoMenu();
closeSpeedMenu();
closeVideosMenu();
if ((Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) ||
(Array.isArray(videoState.extraSubtitlesTracks) && videoState.extraSubtitlesTracks.length > 0) ||
@ -437,6 +455,7 @@ const Player = ({ urlParams, queryParams }) => {
}
case 'KeyI': {
closeSubtitlesMenu();
closeSpeedMenu();
closeVideosMenu();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
toggleInfoMenu();
@ -444,9 +463,20 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'KeyR': {
closeInfoMenu();
closeSubtitlesMenu();
closeVideosMenu();
if (videoState.playbackSpeed !== null) {
toggleSpeedMenu();
}
break;
}
case 'KeyV': {
closeInfoMenu();
closeSubtitlesMenu();
closeSpeedMenu();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
toggleVideosMenu();
}
@ -456,6 +486,7 @@ const Player = ({ urlParams, queryParams }) => {
case 'Escape': {
closeSubtitlesMenu();
closeInfoMenu();
closeSpeedMenu();
closeVideosMenu();
break;
}
@ -467,7 +498,7 @@ const Player = ({ urlParams, queryParams }) => {
return () => {
window.removeEventListener('keydown', onKeyDown);
};
}, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]);
}, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, videoState.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]);
React.useLayoutEffect(() => {
return () => {
setImmersedDebounced.cancel();
@ -476,7 +507,7 @@ const Player = ({ urlParams, queryParams }) => {
};
}, []);
return (
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen })}
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen && !speedMenuOpen && !videosMenuOpen })}
onMouseDown={onContainerMouseDown}
onMouseMove={onContainerMouseMove}
onMouseOver={onContainerMouseMove}
@ -521,7 +552,7 @@ const Player = ({ urlParams, queryParams }) => {
null
}
{
subtitlesMenuOpen || infoMenuOpen ?
subtitlesMenuOpen || infoMenuOpen || videosMenuOpen || speedMenuOpen ?
<div className={styles['layer']} />
:
null
@ -541,6 +572,7 @@ const Player = ({ urlParams, queryParams }) => {
duration={videoState.duration}
volume={videoState.volume}
muted={videoState.muted}
playbackSpeed={videoState.playbackSpeed}
subtitlesTracks={videoState.subtitlesTracks.concat(videoState.extraSubtitlesTracks)}
audioTracks={videoState.audioTracks}
metaItem={player.metaItem}
@ -553,6 +585,7 @@ const Player = ({ urlParams, queryParams }) => {
onSeekRequested={onSeekRequested}
onToggleSubtitlesMenu={toggleSubtitlesMenu}
onToggleInfoMenu={toggleInfoMenu}
onToggleSpeedMenu={toggleSpeedMenu}
onToggleVideosMenu={toggleVideosMenu}
onMouseMove={onBarMouseMove}
onMouseOver={onBarMouseMove}
@ -595,6 +628,16 @@ const Player = ({ urlParams, queryParams }) => {
:
null
}
{
speedMenuOpen ?
<SpeedMenu
className={classnames(styles['layer'], styles['menu-layer'])}
playbackSpeed={videoState.playbackSpeed}
onPlaybackSpeedChanged={onPlaybackSpeedChanged}
/>
:
null
}
{
videosMenuOpen ?
<VideosMenu

View file

@ -0,0 +1,33 @@
// Copyright (C) 2017-2022 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { Button } = require('stremio/common');
const styles = require('./styles');
const OptionButton = ({ className, value, selected, onSelect }) => {
const onClick = React.useCallback(() => {
if (typeof onSelect === 'function') {
onSelect(value);
}
}, [onSelect, value]);
return (
<Button
className={classnames(className, styles['option'], { 'selected': selected })}
onClick={onClick}
>
<div className={styles['label']}>{ value }x</div>
<div className={styles['icon']} />
</Button>
);
};
OptionButton.propTypes = {
className: PropTypes.string,
value: PropTypes.number,
selected: PropTypes.bool,
onSelect: PropTypes.func,
};
module.exports = OptionButton;

View file

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

View file

@ -0,0 +1,38 @@
// Copyright (C) 2017-2022 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.option {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 1.5em;
&:global(.selected) {
background-color: @color-background;
.icon {
display: block;
}
}
&:hover, &:focus {
background-color: @color-background-light2;
}
.label {
flex: 1;
font-weight: 400;
color: @color-surface-light5-90;
}
.icon {
flex: none;
display: none;
width: 0.5rem;
height: 0.5rem;
border-radius: 100%;
margin-left: 1rem;
background-color: @color-accent3-90;
}
}

View file

@ -0,0 +1,48 @@
// Copyright (C) 2017-2022 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Option = require('./Option');
const styles = require('./styles');
const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
const onOptionSelect = React.useCallback((value) => {
if (typeof onPlaybackSpeedChanged === 'function') {
onPlaybackSpeedChanged(value);
}
}, [onPlaybackSpeedChanged]);
return (
<div className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}>
<div className={styles['title']}>
Playback Speed
</div>
<div className={styles['options-container']}>
{
RATES.map((rate) => (
<Option
className={styles['option']}
key={rate}
value={rate}
selected={rate === playbackSpeed}
onSelect={onOptionSelect}
/>
))
}
</div>
</div>
);
};
SpeedMenu.propTypes = {
className: PropTypes.string,
playbackSpeed: PropTypes.number,
onPlaybackSpeedChanged: PropTypes.func,
};
module.exports = SpeedMenu;

View file

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

View file

@ -0,0 +1,27 @@
// Copyright (C) 2017-2022 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.speed-menu-container {
width: 12rem;
overflow: visible !important;
.title {
flex: none;
align-self: stretch;
max-height: 2.4em;
font-weight: 600;
color: @color-surface-light5-90;
margin: 1rem;
}
.options-container {
flex: 0 1 auto;
max-height: calc(3.2rem * 8);
overflow-y: auto;
.option {
height: 3.2rem;
}
}
}

View file

@ -20,7 +20,7 @@ function Chromecast() {
function onTransportInitError(args) {
console.error(args);
active = false;
error = new Error('Google Cast API not available');
error = new Error('Google Cast API not available', { cause: args });
starting = false;
onStateChanged();
transport = null;

View file

@ -1,6 +1,7 @@
// Copyright (C) 2017-2022 Smart code 203358507
const EventEmitter = require('eventemitter3');
const hat = require('hat');
const MESSAGE_NAMESPACE = 'urn:x-cast:com.stremio';
const CHUNK_SIZE = 20000;
@ -33,7 +34,7 @@ const initialize = () => {
function ChromecastTransport() {
const events = new EventEmitter();
const chunks = [];
const messages = {};
initialize()
.then(() => {
@ -59,26 +60,17 @@ function ChromecastTransport() {
function onMessage(_, message) {
try {
const { chunk, last } = JSON.parse(message);
chunks.push(chunk);
if (!last) {
return;
const { id, chunk, index, length } = JSON.parse(message);
messages[id] = messages[id] || [];
messages[id][index] = chunk;
if (Object.keys(messages[id]).length === length) {
const parsedMessage = JSON.parse(messages[id].join(''));
delete messages[id];
events.emit('message', parsedMessage);
}
} catch (error) {
chunks.splice(0, chunks.length);
events.emit('message-error', error);
return;
}
let parsedMessage;
try {
parsedMessage = JSON.parse(chunks.splice(0, chunks.length).join(''));
} catch (error) {
events.emit('message-error', error);
return;
}
events.emit('message', parsedMessage);
}
function onApplicationStatusChanged(event) {
events.emit(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, event);
@ -165,11 +157,13 @@ function ChromecastTransport() {
const chunk = serializedMessage.slice(start, start + CHUNK_SIZE);
chunks.push(chunk);
}
const id = hat();
return Promise.all(chunks.map((chunk, index) => {
return castSession.sendMessage(MESSAGE_NAMESPACE, {
id,
chunk,
last: index === chunks.length - 1,
index,
length: chunks.length
});
}));
} else {

View file

@ -20,7 +20,7 @@ function Core(args) {
function onTransportError(args) {
console.error(args);
active = false;
error = new Error('Stremio Core Transport initialization failed');
error = new Error('Stremio Core Transport initialization failed', { cause: args });
starting = false;
onStateChanged();
transport = null;

View file

@ -6,8 +6,10 @@ const webpack = require('webpack');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const colors = require('@stremio/stremio-colors');
const pachageJson = require('./package.json');
const COMMIT_HASH = execSync('git rev-parse HEAD').toString().trim();
@ -187,8 +189,18 @@ module.exports = (env, argv) => ({
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['*']
}),
argv.mode === 'production' &&
new WorkboxPlugin.GenerateSW({
maximumFileSizeToCacheInBytes: 20000000,
clientsClaim: true,
skipWaiting: true
}),
new CopyWebpackPlugin({
patterns: [{ from: 'favicons', to: `${COMMIT_HASH}/favicons` }]
patterns: [
{ from: 'favicons', to: `${COMMIT_HASH}/favicons` },
{ from: 'images', to: `${COMMIT_HASH}/images` },
{ from: 'manifest.json', to: `${COMMIT_HASH}/manifest.json` },
]
}),
new MiniCssExtractPlugin({
filename: `${COMMIT_HASH}/styles/[name].css`
@ -197,7 +209,10 @@ module.exports = (env, argv) => ({
template: './src/index.html',
inject: false,
scriptLoading: 'blocking',
faviconsPath: `${COMMIT_HASH}/favicons`
themeColor: colors.background,
faviconsPath: `${COMMIT_HASH}/favicons`,
imagesPath: `${COMMIT_HASH}/images`,
manifestPath: `${COMMIT_HASH}/manifest.json`,
})
]
].filter(Boolean)
});