From de4bf0a5bbe60bdc941aa6487078dc685e087ad5 Mon Sep 17 00:00:00 2001 From: Kasper Seweryn Date: Sat, 30 Apr 2022 13:25:59 +0000 Subject: [PATCH] Add PWA support --- changes/changelog.d/1769.bugfix | 1 + changes/changelog.d/1769.enhancement | 1 + front/.eslintrc.js | 16 +- front/package.json | 16 +- front/public/service-worker.js | 93 --- front/src/App.vue | 58 -- front/src/init/serviceWorker.ts | 81 +- front/src/serviceWorker.ts | 48 ++ front/src/store/instance.js | 8 +- front/src/store/ui.js | 8 - front/src/utils/index.ts | 5 +- front/src/utils/time.ts | 2 +- .../tests/unit/specs/filters/filters.spec.js | 2 +- front/tests/unit/specs/store/queue.spec.js | 1 + front/tsconfig.json | 2 +- front/vite.config.ts | 19 +- front/yarn.lock | 785 ++++++++++++++++-- 17 files changed, 855 insertions(+), 291 deletions(-) create mode 100644 changes/changelog.d/1769.bugfix create mode 100644 changes/changelog.d/1769.enhancement delete mode 100644 front/public/service-worker.js create mode 100644 front/src/serviceWorker.ts diff --git a/changes/changelog.d/1769.bugfix b/changes/changelog.d/1769.bugfix new file mode 100644 index 000000000..c209cf176 --- /dev/null +++ b/changes/changelog.d/1769.bugfix @@ -0,0 +1 @@ +Fixes service worker (#1634) diff --git a/changes/changelog.d/1769.enhancement b/changes/changelog.d/1769.enhancement new file mode 100644 index 000000000..a212d50ba --- /dev/null +++ b/changes/changelog.d/1769.enhancement @@ -0,0 +1 @@ +Handle PWA correctly and provide better cache strategy for album covers (#1721) diff --git a/front/.eslintrc.js b/front/.eslintrc.js index d350d94ad..9a6053f6f 100644 --- a/front/.eslintrc.js +++ b/front/.eslintrc.js @@ -24,10 +24,22 @@ module.exports = { 'vue/no-v-html': 'off', // TODO: tackle this properly 'vue/no-use-v-if-with-v-for': 'off', - '@typescript-eslint/ban-ts-comment': 'off', + // NOTE: Handled by typescript 'no-undef': 'off', + 'no-unused-vars': 'off', + + // TODO (wvffle): Migrate to VUI + // We're using `// @ts-ignore` in jQuery extensions + // and gettext for vue 2 + '@typescript-eslint/ban-ts-comment': 'off', + // TODO (wvffle): Enable typescript rules later '@typescript-eslint/no-this-alias': 'off', - '@typescript-eslint/no-empty-function': 'off' + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-unused-vars': 'off', + + // TODO (wvffle): Migration to pinia + // Vuex 3 store does not have types defined, hence we use `any` + '@typescript-eslint/no-explicit-any': 'off' } } diff --git a/front/package.json b/front/package.json index c8e44e76b..bb191ffa4 100644 --- a/front/package.json +++ b/front/package.json @@ -50,6 +50,7 @@ "@babel/core": "7.17.12", "@babel/plugin-transform-runtime": "7.17.12", "@babel/preset-env": "7.16.11", + "@types/jest": "27.4.1", "@types/jquery": "3.5.14", "@types/lodash-es": "4.17.6", "@typescript-eslint/eslint-plugin": "5.19.0", @@ -72,12 +73,18 @@ "jest-cli": "27.5.1", "moxios": "0.4.0", "sinon": "13.0.2", + "ts-jest": "27.1.4", "typescript": "4.6.3", "unplugin-vue2-script-setup": "0.10.2", "vite": "2.8.6", + "vite-plugin-pwa": "0.12.0", "vite-plugin-vue2": "1.9.3", "vue-jest": "3.0.7", - "vue-template-compiler": "2.6.14" + "vue-template-compiler": "2.6.14", + "workbox-core": "6.5.3", + "workbox-precaching": "6.5.3", + "workbox-routing": "6.5.3", + "workbox-strategies": "6.5.3" }, "resolutions": { "vue-plyr/plyr": "3.6.12" @@ -132,14 +139,19 @@ ], "jest": { "moduleFileExtensions": [ + "ts", "js", "json", "vue" ], "transform": { ".*\\.(vue)$": "vue-jest", - "^.+\\.js$": "babel-jest" + "^.+\\.js$": "babel-jest", + "^.+\\.ts$": "ts-jest" }, + "transformIgnorePatterns": [ + "/node_modules/(?!lodash-es/.*)" + ], "moduleNameMapper": { "^~/(.*)$": "/src/$1" }, diff --git a/front/public/service-worker.js b/front/public/service-worker.js deleted file mode 100644 index 11f73ea72..000000000 --- a/front/public/service-worker.js +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint no-undef: "off" */ - -// This is the code piece that GenerateSW mode can't provide for us. -// This code listens for the user's confirmation to update the app. -workbox.loadModule('workbox-routing') -workbox.loadModule('workbox-strategies') -workbox.loadModule('workbox-expiration') - -self.addEventListener('message', (e) => { - if (!e.data) { - return - } - console.log('[sw] received message', e.data) - switch (e.data.command) { - case 'skipWaiting': - self.skipWaiting() - break - case 'serverChosen': - self.registerServerRoutes(e.data.serverUrl) - break - default: - // NOOP - break - } -}) -workbox.core.clientsClaim() - -const router = new workbox.routing.Router() -router.addCacheListener() -router.addFetchListener() - -let registeredServerRoutes = [] -self.registerServerRoutes = (serverUrl) => { - console.log('[sw] Setting up API caching for', serverUrl) - registeredServerRoutes.forEach((r) => { - console.log('[sw] Unregistering previous API route...', r) - router.unregisterRoute(r) - }) - if (!serverUrl) { - return - } - const regexReadyServerUrl = serverUrl.replace('.', '\\.') - registeredServerRoutes = [] - const networkFirstPaths = [ - 'api/v1/', - 'media/' - ] - const networkFirstExcludedPaths = [ - 'api/v1/listen' - ] - const strategy = new workbox.strategies.NetworkFirst({ - cacheName: 'api-cache:' + serverUrl, - plugins: [ - new workbox.expiration.Plugin({ - maxAgeSeconds: 24 * 60 * 60 * 7 - }) - ] - }) - const networkFirstRoutes = networkFirstPaths.map((path) => { - const regex = new RegExp(regexReadyServerUrl + path) - return new workbox.routing.RegExpRoute(regex, () => {}) - }) - const matcher = ({ url, event }) => { - for (let index = 0; index < networkFirstExcludedPaths.length; index++) { - const blacklistedPath = networkFirstExcludedPaths[index] - if (url.pathname.startsWith('/' + blacklistedPath)) { - // the path is blacklisted, we don't cache it at all - console.log('[sw] Path is blacklisted, not caching', url.pathname) - return false - } - } - // we call other regex matchers - for (let index = 0; index < networkFirstRoutes.length; index++) { - const route = networkFirstRoutes[index] - const result = route.match({ url, event }) - if (result) { - return result - } - } - return false - } - - const route = new workbox.routing.Route(matcher, strategy) - console.log('[sw] registering new API route...', route) - router.registerRoute(route) - registeredServerRoutes.push(route) -} - -// The precaching code provided by Workbox. -self.__precacheManifest = [].concat(self.__precacheManifest || []) - -// workbox.precaching.suppressWarnings(); // Only used with Vue CLI 3 and Workbox v3. -workbox.precaching.precacheAndRoute(self.__precacheManifest, {}) diff --git a/front/src/App.vue b/front/src/App.vue index 7ec5f741f..dddc33ad3 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -111,64 +111,6 @@ const { width } = useWindowSize() const player = ref() const showShortcutsModal = ref(false) const showSetInstanceModal = ref(false) -// export default { -// computed: { -// ...mapState({ -// serviceWorker: state => state.ui.serviceWorker -// }), -// }, -// watch: { -// 'serviceWorker.updateAvailable': { -// handler (v) { -// if (!v) { -// return -// } -// const self = this -// this.$store.commit('ui/addMessage', { -// content: this.$pgettext('App/Message/Paragraph', 'A new version of the app is available.'), -// date: new Date(), -// key: 'refreshApp', -// displayTime: 0, -// classActions: 'bottom attached opaque', -// actions: [ -// { -// text: this.$pgettext('App/Message/Paragraph', 'Update'), -// class: 'primary', -// click: function () { -// self.updateApp() -// } -// }, -// { -// text: this.$pgettext('App/Message/Paragraph', 'Later'), -// class: 'basic' -// } -// ] -// }) -// }, -// immediate: true -// } -// }, -// async created () { -// if (navigator.serviceWorker) { -// navigator.serviceWorker.addEventListener( -// 'controllerchange', () => { -// if (this.serviceWorker.refreshing) return -// this.$store.commit('ui/serviceWorker', { -// refreshing: true -// }) -// window.location.reload() -// } -// ) -// } -// }, -// methods: { -// updateApp () { -// this.$store.commit('ui/serviceWorker', { updateAvailable: false }) -// if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return } -// this.serviceWorker.registration.waiting.postMessage({ command: 'skipWaiting' }) -// }, -// } -// }