From 9964f95d5d8bfeb00cac2f5efca639b37907cba4 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:49:09 +0000 Subject: [PATCH] [Remote] Full PWA support, misc bugfixes (#280) - Fix setting remote port properly - Add web worker support (so it can be installed as an "app") - build fixes/removing stray console.log --- .erb/configs/webpack.config.remote.dev.ts | 187 ++++++++-------- .erb/configs/webpack.config.remote.prod.ts | 202 +++++++++--------- src/main/features/core/remote/index.ts | 17 +- src/main/features/core/remote/manifest.json | 17 ++ src/remote/index.ejs | 8 + src/remote/manifest.json | 17 ++ src/remote/service-worker.ts | 48 +++++ src/remote/store/index.ts | 1 - src/remote/worker.js | 0 .../components/general/remote-settings.tsx | 9 +- tsconfig.json | 2 +- 11 files changed, 314 insertions(+), 194 deletions(-) create mode 100644 src/main/features/core/remote/manifest.json create mode 100644 src/remote/manifest.json create mode 100644 src/remote/service-worker.ts create mode 100644 src/remote/worker.js diff --git a/.erb/configs/webpack.config.remote.dev.ts b/.erb/configs/webpack.config.remote.dev.ts index 64332c6e..3313095e 100644 --- a/.erb/configs/webpack.config.remote.dev.ts +++ b/.erb/configs/webpack.config.remote.dev.ts @@ -9,112 +9,119 @@ import checkNodeEnv from '../scripts/check-node-env'; import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; +const { version } = require('../../package.json'); + // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's // at the dev webpack config is not accidentally run in a production environment if (process.env.NODE_ENV === 'production') { - checkNodeEnv('development'); + checkNodeEnv('development'); } -const port = process.env.PORT || 4343; - const configuration: webpack.Configuration = { - devtool: 'inline-source-map', + devtool: 'inline-source-map', - mode: 'development', + mode: 'development', - target: ['web'], + target: ['web'], - entry: [path.join(webpackPaths.srcRemotePath, 'index.tsx')], - - output: { - path: webpackPaths.dllPath, - publicPath: '/', - filename: 'remote.js', - library: { - type: 'umd', + entry: { + remote: path.join(webpackPaths.srcRemotePath, 'index.tsx'), + worker: path.join(webpackPaths.srcRemotePath, 'service-worker.ts'), }, - }, - module: { - rules: [ - { - test: /\.s?css$/, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - modules: true, - sourceMap: true, - importLoaders: 1, + output: { + path: webpackPaths.dllPath, + publicPath: '/', + filename: '[name].js', + library: { + type: 'umd', + }, + }, + + module: { + rules: [ + { + test: /\.s?css$/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + modules: true, + sourceMap: true, + importLoaders: 1, + }, + }, + 'sass-loader', + ], + include: /\.module\.s?(c|a)ss$/, + }, + { + test: /\.s?css$/, + use: ['style-loader', 'css-loader', 'sass-loader'], + exclude: /\.module\.s?(c|a)ss$/, + }, + // Fonts + { + test: /\.(woff|woff2|eot|ttf|otf)$/i, + type: 'asset/resource', + }, + // Images + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', }, - }, - 'sass-loader', ], - include: /\.module\.s?(c|a)ss$/, - }, - { - test: /\.s?css$/, - use: ['style-loader', 'css-loader', 'sass-loader'], - exclude: /\.module\.s?(c|a)ss$/, - }, - // Fonts - { - test: /\.(woff|woff2|eot|ttf|otf)$/i, - type: 'asset/resource', - }, - // Images - { - test: /\.(png|svg|jpg|jpeg|gif)$/i, - type: 'asset/resource', - }, + }, + plugins: [ + new webpack.NoEmitOnErrorsPlugin(), + + /** + * Create global constants which can be configured at compile time. + * + * Useful for allowing different behaviour between development builds and + * release builds + * + * NODE_ENV should be production so that modules do not perform certain + * development checks + * + * By default, use 'development' as NODE_ENV. This can be overriden with + * 'staging', for example, by changing the ENV variables in the npm scripts + */ + new webpack.EnvironmentPlugin({ + NODE_ENV: 'development', + }), + + new webpack.LoaderOptionsPlugin({ + debug: true, + }), + + new HtmlWebpackPlugin({ + filename: path.join('index.html'), + template: path.join(webpackPaths.srcRemotePath, 'index.ejs'), + favicon: path.join(webpackPaths.assetsPath, 'icons', 'favicon.ico'), + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, + }, + isBrowser: true, + env: process.env.NODE_ENV, + isDevelopment: process.env.NODE_ENV !== 'production', + nodeModules: webpackPaths.appNodeModulesPath, + templateParameters: { + version, + prod: false, + }, + }), ], - }, - plugins: [ - new webpack.NoEmitOnErrorsPlugin(), - /** - * Create global constants which can be configured at compile time. - * - * Useful for allowing different behaviour between development builds and - * release builds - * - * NODE_ENV should be production so that modules do not perform certain - * development checks - * - * By default, use 'development' as NODE_ENV. This can be overriden with - * 'staging', for example, by changing the ENV variables in the npm scripts - */ - new webpack.EnvironmentPlugin({ - NODE_ENV: 'development', - }), + node: { + __dirname: false, + __filename: false, + }, - new webpack.LoaderOptionsPlugin({ - debug: true, - }), - - new HtmlWebpackPlugin({ - filename: path.join('index.html'), - template: path.join(webpackPaths.srcRemotePath, 'index.ejs'), - favicon: path.join(webpackPaths.assetsPath, 'icons', 'favicon.ico'), - minify: { - collapseWhitespace: true, - removeAttributeQuotes: true, - removeComments: true, - }, - isBrowser: true, - env: process.env.NODE_ENV, - isDevelopment: process.env.NODE_ENV !== 'production', - nodeModules: webpackPaths.appNodeModulesPath, - }), - ], - - node: { - __dirname: false, - __filename: false, - }, - - watch: true, + watch: true, }; export default merge(baseConfig, configuration); diff --git a/.erb/configs/webpack.config.remote.prod.ts b/.erb/configs/webpack.config.remote.prod.ts index 1dcc61ad..d96dbe5b 100644 --- a/.erb/configs/webpack.config.remote.prod.ts +++ b/.erb/configs/webpack.config.remote.prod.ts @@ -17,116 +17,126 @@ import deleteSourceMaps from '../scripts/delete-source-maps'; import baseConfig from './webpack.config.base'; import webpackPaths from './webpack.paths'; +const { version } = require('../../package.json'); + checkNodeEnv('production'); deleteSourceMaps(); const devtoolsConfig = - process.env.DEBUG_PROD === 'true' - ? { - devtool: 'source-map', - } - : {}; + process.env.DEBUG_PROD === 'true' + ? { + devtool: 'source-map', + } + : {}; const configuration: webpack.Configuration = { - ...devtoolsConfig, + ...devtoolsConfig, - mode: 'production', + mode: 'production', - target: ['web'], + target: ['web'], - entry: [path.join(webpackPaths.srcRemotePath, 'index.tsx')], - - output: { - path: webpackPaths.distRemotePath, - publicPath: './', - filename: 'remote.js', - library: { - type: 'umd', + entry: { + remote: path.join(webpackPaths.srcRemotePath, 'index.tsx'), + worker: path.join(webpackPaths.srcRemotePath, 'service-worker.ts'), }, - }, - module: { - rules: [ - { - test: /\.s?(a|c)ss$/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: 'css-loader', - options: { - modules: true, - sourceMap: true, - importLoaders: 1, + output: { + path: webpackPaths.distRemotePath, + publicPath: './', + filename: '[name].js', + library: { + type: 'umd', + }, + }, + + module: { + rules: [ + { + test: /\.s?(a|c)ss$/, + use: [ + MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + modules: true, + sourceMap: true, + importLoaders: 1, + }, + }, + 'sass-loader', + ], + include: /\.module\.s?(c|a)ss$/, + }, + { + test: /\.s?(a|c)ss$/, + use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], + exclude: /\.module\.s?(c|a)ss$/, + }, + // Fonts + { + test: /\.(woff|woff2|eot|ttf|otf)$/i, + type: 'asset/resource', + }, + // Images + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', }, - }, - 'sass-loader', ], - include: /\.module\.s?(c|a)ss$/, - }, - { - test: /\.s?(a|c)ss$/, - use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], - exclude: /\.module\.s?(c|a)ss$/, - }, - // Fonts - { - test: /\.(woff|woff2|eot|ttf|otf)$/i, - type: 'asset/resource', - }, - // Images - { - test: /\.(png|svg|jpg|jpeg|gif)$/i, - type: 'asset/resource', - }, + }, + + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + }), + new CssMinimizerPlugin(), + ], + }, + + plugins: [ + /** + * Create global constants which can be configured at compile time. + * + * Useful for allowing different behaviour between development builds and + * release builds + * + * NODE_ENV should be production so that modules do not perform certain + * development checks + */ + new webpack.EnvironmentPlugin({ + NODE_ENV: 'production', + DEBUG_PROD: false, + }), + + new MiniCssExtractPlugin({ + filename: 'remote.css', + }), + + new BundleAnalyzerPlugin({ + analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', + }), + + new HtmlWebpackPlugin({ + filename: 'index.html', + template: path.join(webpackPaths.srcRemotePath, 'index.ejs'), + favicon: path.join(webpackPaths.assetsPath, 'icons', 'favicon.ico'), + minify: { + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, + }, + isBrowser: true, + env: process.env.NODE_ENV, + isDevelopment: process.env.NODE_ENV !== 'production', + templateParameters: { + version, + prod: true, + }, + }), ], - }, - - optimization: { - minimize: true, - minimizer: [ - new TerserPlugin({ - parallel: true, - }), - new CssMinimizerPlugin(), - ], - }, - - plugins: [ - /** - * Create global constants which can be configured at compile time. - * - * Useful for allowing different behaviour between development builds and - * release builds - * - * NODE_ENV should be production so that modules do not perform certain - * development checks - */ - new webpack.EnvironmentPlugin({ - NODE_ENV: 'production', - DEBUG_PROD: false, - }), - - new MiniCssExtractPlugin({ - filename: 'remote.css', - }), - - new BundleAnalyzerPlugin({ - analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', - }), - - new HtmlWebpackPlugin({ - filename: 'index.html', - template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), - favicon: path.join(webpackPaths.assetsPath, 'icons', 'favicon.ico'), - minify: { - collapseWhitespace: true, - removeAttributeQuotes: true, - removeComments: true, - }, - isBrowser: false, - isDevelopment: process.env.NODE_ENV !== 'production', - }), - ], }; export default merge(baseConfig, configuration); diff --git a/src/main/features/core/remote/index.ts b/src/main/features/core/remote/index.ts index 2c2b87c0..e5a933de 100644 --- a/src/main/features/core/remote/index.ts +++ b/src/main/features/core/remote/index.ts @@ -6,6 +6,7 @@ import { deflate, gzip } from 'zlib'; import axios from 'axios'; import { app, ipcMain } from 'electron'; import { Server as WsServer, WebSocketServer, WebSocket } from 'ws'; +import manifest from './manifest.json'; import { ClientEvent, ServerEvent } from '../../../../remote/types'; import { PlayerRepeat, SongUpdate } from '../../../../renderer/types'; import { getMainWindow } from '../../../main'; @@ -297,6 +298,12 @@ const enableServer = (config: RemoteConfig): Promise => { await serveFile(req, 'remote', 'js', res); break; } + case '/manifest.json': { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(manifest)); + break; + } case '/credentials': { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); @@ -304,9 +311,13 @@ const enableServer = (config: RemoteConfig): Promise => { break; } default: { - res.statusCode = 404; - res.setHeader('Content-Type', 'text/plain'); - res.end('Not FOund'); + if (req.url?.startsWith('/worker.js')) { + await serveFile(req, 'worker', 'js', res); + } else { + res.statusCode = 404; + res.setHeader('Content-Type', 'text/plain'); + res.end('Not Found'); + } } } } catch (error) { diff --git a/src/main/features/core/remote/manifest.json b/src/main/features/core/remote/manifest.json new file mode 100644 index 00000000..372fc55c --- /dev/null +++ b/src/main/features/core/remote/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "Feishin Remote", + "short_name": "Feishin Remote", + "start_url": "/", + "background_color": "#000100", + "theme_color": "#E7E7E7", + "icons": [ + { + "src": "favicon.ico", + "sizes": "32x32", + "type": "image/png", + "purpose": "maskable any" + } + ], + "display": "standalone", + "orientation": "portrait" +} diff --git a/src/remote/index.ejs b/src/remote/index.ejs index a65faf5e..6271cf7b 100644 --- a/src/remote/index.ejs +++ b/src/remote/index.ejs @@ -6,6 +6,14 @@ Feishin Remote + + diff --git a/src/remote/manifest.json b/src/remote/manifest.json new file mode 100644 index 00000000..5c41ea6e --- /dev/null +++ b/src/remote/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "Feishin Remote", + "short_name": "Feishin Remote", + "start_url": "/", + "background_color": "#FFDCB5", + "theme_color": "#1E003D", + "icons": [ + { + "src": "favicon.ico", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable any" + } + ], + "display": "standalone", + "orientation": "portrait" +} diff --git a/src/remote/service-worker.ts b/src/remote/service-worker.ts new file mode 100644 index 00000000..08dffa56 --- /dev/null +++ b/src/remote/service-worker.ts @@ -0,0 +1,48 @@ +/// + +export type {}; +// eslint-disable-next-line no-undef +declare const self: ServiceWorkerGlobalScope; + +// eslint-disable-next-line no-restricted-globals +const url = new URL(location.toString()); +const version = url.searchParams.get('version'); +const prod = url.searchParams.get('prod') === 'true'; +const cacheName = `Feishin-remote-${version}`; + +const resourcesToCache = ['./', './remote.js', './favicon.ico']; + +if (prod) { + resourcesToCache.push('./remote.css'); +} + +self.addEventListener('install', (e) => { + e.waitUntil( + caches.open(cacheName).then((cache) => { + return cache.addAll(resourcesToCache); + }), + ); +}); + +self.addEventListener('fetch', (e) => { + e.respondWith( + caches.match(e.request).then((response) => { + return response || fetch(e.request); + }), + ); +}); + +self.addEventListener('activate', (e) => { + e.waitUntil( + caches.keys().then((keyList) => { + return Promise.all( + keyList.map((key) => { + if (key !== cacheName) { + return caches.delete(key); + } + return Promise.resolve(); + }), + ); + }), + ); +}); diff --git a/src/remote/store/index.ts b/src/remote/store/index.ts index 2abbb753..dc3c2166 100644 --- a/src/remote/store/index.ts +++ b/src/remote/store/index.ts @@ -194,7 +194,6 @@ export const useRemoteStore = create()( }); }, send: (data: ClientEvent) => { - console.log(data, get().socket); get().socket?.send(JSON.stringify(data)); }, toggleIsDark: () => { diff --git a/src/remote/worker.js b/src/remote/worker.js new file mode 100644 index 00000000..e69de29b diff --git a/src/renderer/features/settings/components/general/remote-settings.tsx b/src/renderer/features/settings/components/general/remote-settings.tsx index 9844d61f..798c52af 100644 --- a/src/renderer/features/settings/components/general/remote-settings.tsx +++ b/src/renderer/features/settings/components/general/remote-settings.tsx @@ -28,24 +28,27 @@ export const RemoteSettings = () => { title: enabled ? 'Error enabling remote' : 'Error disabling remote', }); } - }, 100); + }, 50); const debouncedChangeRemotePort = debounce(async (port: number) => { const errorMsg = await remote!.setRemotePort(port); - if (errorMsg === null) { + if (!errorMsg) { setSettings({ remote: { ...settings, port, }, }); + toast.warn({ + message: 'To have your port change take effect, stop and restart the server', + }); } else { toast.error({ message: errorMsg, title: 'Error setting port', }); } - }); + }, 100); const isHidden = !isElectron(); diff --git a/tsconfig.json b/tsconfig.json index aa8c4e3e..8c5dbfe2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es2021", "module": "commonjs", - "lib": ["dom", "es2021"], + "lib": ["dom", "es2021", "WebWorker"], "baseUrl": "./src", "paths": { "/@/*": ["*"]