Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
funkwhale
funkwhale
Commits
8e937513
Commit
8e937513
authored
Apr 30, 2022
by
Kasper Seweryn
🥞
Committed by
Kasper Seweryn
Jul 03, 2022
Browse files
Add PWA support
parent
47e50905
Changes
17
Expand all
Hide whitespace changes
Inline
Side-by-side
changes/changelog.d/1769.bugfix
0 → 100644
View file @
8e937513
Fixes service worker (#1634)
changes/changelog.d/1769.enhancement
0 → 100644
View file @
8e937513
Handle PWA correctly and provide better cache strategy for album covers (#1721)
front/.eslintrc.js
View file @
8e937513
...
...
@@ -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
'
}
}
front/package.json
View file @
8e937513
...
...
@@ -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"
:
[
"<rootDir>/node_modules/(?!lodash-es/.*)"
],
"moduleNameMapper"
:
{
"^~/(.*)$"
:
"<rootDir>/src/$1"
},
...
...
front/public/service-worker.js
deleted
100644 → 0
View file @
47e50905
/* 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
,
{})
front/src/App.vue
View file @
8e937513
...
...
@@ -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' })
// },
// }
// }
</
script
>
<
template
>
...
...
front/src/init/serviceWorker.ts
View file @
8e937513
import
{
InitModule
}
from
'
~/types
'
import
{
register
}
from
'
register-service-worker
'
import
{
AppModule
}
from
'
~/types
'
import
{
registerSW
}
from
'
virtual:pwa-register
'
import
logger
from
'
~/logging
'
import
Vue
from
'
vue
'
export
const
install
:
InitModule
=
({
store
})
=>
{
if
(
import
.
meta
.
env
.
PROD
)
{
register
(
`
${
import
.
meta
.
env
.
BASE_URL
}
service-worker.js`
,
{
registrationOptions
:
{
scope
:
'
/
'
},
ready
()
{
console
.
log
(
'
App is being served from cache by a service worker.
'
)
},
registered
(
registration
)
{
console
.
log
(
'
Service worker has been registered.
'
)
// check for updates every 2 hours
const
checkInterval
=
1000
*
60
*
60
*
2
// var checkInterval = 1000 * 5
setInterval
(()
=>
{
console
.
log
(
'
Checking for service worker update…
'
)
registration
.
update
()
},
checkInterval
)
store
.
commit
(
'
ui/serviceWorker
'
,
{
registration
:
registration
})
if
(
registration
.
active
)
{
registration
.
active
.
postMessage
({
command
:
'
serverChosen
'
,
serverUrl
:
store
.
state
.
instance
.
instanceUrl
})
}
},
cached
()
{
console
.
log
(
'
Content has been cached for offline use.
'
)
},
updatefound
()
{
console
.
log
(
'
New content is downloading.
'
)
},
updated
(
registration
)
{
console
.
log
(
'
New content is available; please refresh!
'
)
store
.
commit
(
'
ui/serviceWorker
'
,
{
updateAvailable
:
true
,
registration
:
registration
})
},
offline
()
{
console
.
log
(
'
No internet connection found. App is running in offline mode.
'
)
},
error
(
error
)
{
console
.
error
(
'
Error during service worker registration:
'
,
error
)
}
})
}
const
{
$pgettext
}
=
Vue
.
prototype
export
const
install
:
AppModule
=
({
store
})
=>
{
const
updateSW
=
registerSW
({
onRegisterError
()
{
logger
.
default
.
error
(
'
SW install error
'
)
},
onOfflineReady
()
{
logger
.
default
.
info
(
'
Funkwhale is being served from cache by a service worker.
'
)
},
onRegistered
()
{
logger
.
default
.
info
(
'
Service worker has been registered.
'
)
},
onNeedRefresh
()
{
store
.
commit
(
'
ui/addMessage
'
,
{
content
:
$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
:
$pgettext
(
'
App/Message/Paragraph
'
,
'
Update
'
),
class
:
'
primary
'
,
click
:
()
=>
updateSW
()
},
{
text
:
$pgettext
(
'
App/Message/Paragraph
'
,
'
Later
'
),
class
:
'
basic
'
}
]
})
}
})
}
front/src/serviceWorker.ts
0 → 100644
View file @
8e937513
import
{
cleanupOutdatedCaches
,
precacheAndRoute
}
from
'
workbox-precaching
'
import
{
NetworkFirst
,
StaleWhileRevalidate
}
from
'
workbox-strategies
'
import
{
ExpirationPlugin
}
from
'
workbox-expiration
'
import
{
registerRoute
}
from
'
workbox-routing
'
import
{
clientsClaim
}
from
'
workbox-core
'
declare
let
self
:
ServiceWorkerGlobalScope
// NOTE: Clean up outdated caches
// With each new production build, all precached assets
// that were modified are added to the cache. The old versions
// need to be removed manually.
cleanupOutdatedCaches
()
// Let new service worker claim control of already open web pages
// https://developer.chrome.com/docs/workbox/modules/workbox-core/#clients-claim
clientsClaim
()
// Support for an update prompt handled by VitePWA:
// https://vite-plugin-pwa.netlify.app/guide/prompt-for-update.html
self
.
addEventListener
(
'
message
'
,
(
event
)
=>
{
if
(
event
.
data
?.
type
===
'
SKIP_WAITING
'
)
{
return
self
.
skipWaiting
()
}
})
// NOTE: Network-First cache for API calls
// We're using cache only when the user goes offline
registerRoute
(({
url
})
=>
{
if
(
url
.
pathname
.
startsWith
(
'
/api/v1/listen
'
))
return
false
return
url
.
pathname
.
startsWith
(
'
/api/v1
'
)
},
new
NetworkFirst
({
plugins
:
[
// Expire after a week
new
ExpirationPlugin
({
maxAgeSeconds
:
7
*
24
*
3600
})
]
}))
// NOTE: Stale-While-Revalidate cache for album covers
// We're serving from cache if available and making a request
// in the background to update the cache for next request
registerRoute
(({
url
})
=>
{
return
url
.
pathname
.
startsWith
(
'
/media
'
)
},
new
StaleWhileRevalidate
())
// Precache all assets and add routes for them
// https://developer.chrome.com/docs/workbox/reference/workbox-precaching/#method-precacheAndRoute
precacheAndRoute
(
self
.
__WB_MANIFEST
)
front/src/store/instance.js
View file @
8e937513
...
...
@@ -9,12 +9,6 @@ function getDefaultUrl () {
)
}
function
notifyServiceWorker
(
registration
,
message
)
{
if
(
registration
&&
registration
.
active
)
{
registration
.
active
.
postMessage
(
message
)
}
}
export
default
{
namespaced
:
true
,
state
:
{
...
...
@@ -87,7 +81,7 @@ export default {
value
=
value
+
'
/
'
}
state
.
instanceUrl
=
value
notifyServiceWorker
(
state
.
registration
,
{
command
:
'
serverChosen
'
,
serverUrl
:
state
.
instanceUrl
})
// append the URL to the list (and remove existing one if needed)
if
(
value
)
{
const
index
=
state
.
knownInstances
.
indexOf
(
value
)
...
...
front/src/store/ui.js
View file @
8e937513
...
...
@@ -174,11 +174,6 @@ export default {
orderingDirection
:
'
-
'
,
ordering
:
'
creation_date
'
}
},
serviceWorker
:
{
refreshing
:
false
,
registration
:
null
,
updateAvailable
:
false
}
},
getters
:
{
...
...
@@ -310,9 +305,6 @@ export default {
state
.
routePreferences
[
route
].
orderingDirection
=
value
},
serviceWorker
:
(
state
,
value
)
=>
{
state
.
serviceWorker
=
{
...
state
.
serviceWorker
,
...
value
}
},
window
:
(
state
,
value
)
=>
{
state
.
window
=
value
}
...
...
front/src/utils/index.ts
View file @
8e937513
...
...
@@ -19,8 +19,9 @@ export function parseAPIErrors (responseData: APIErrorResponse, parentField?: st
}
const
value
=
responseData
[
field
]
if
(
value
as
string
[])
{
errors
.
push
(...(
value
as
string
[]).
map
(
err
=>
{
if
(
Array
.
isArray
(
value
))
{
const
values
=
value
as
string
[]
errors
.
push
(...
values
.
map
(
err
=>
{
return
err
.
toLocaleLowerCase
().
includes
(
'
this field
'
)
?
`
${
fieldName
}
:
${
err
}
`
:
err
...
...
front/src/utils/time.ts
View file @
8e937513
...
...
@@ -19,7 +19,7 @@ export default {
return
hours
>=
1
?
`
${
hours
}
:
${
pad
(
min
)}
:
${
pad
(
sec
)}
`
:
`
${
pad
(
min
)
}
:
${
pad
(
sec
)}
`
:
`
${
min
}
:
${
pad
(
sec
)}
`
},
durationFormatted
(
v
:
string
)
{
const
duration
=
parseInt
(
v
)
...
...
front/tests/unit/specs/filters/filters.spec.js
View file @
8e937513
import
{
expect
}
from
'
chai
'
import
moment
from
'
moment
'
import
{
truncate
,
ago
,
capitalize
,
year
,
unique
}
from
'
~/filters
'
import
{
truncate
,
ago
,
capitalize
,
year
,
unique
}
from
'
~/
init/
filters
'
describe
(
'
filters
'
,
()
=>
{
describe
(
'
truncate
'
,
()
=>
{
...
...
front/tests/unit/specs/store/queue.spec.js
View file @
8e937513
var
sinon
=
require
(
'
sinon
'
)
import
{
expect
}
from
'
chai
'
import
*
as
_
from
'
lodash-es
'
import
store
from
'
~/store/queue
'
import
{
testAction
}
from
'
../../utils
'
...
...
front/tsconfig.json
View file @
8e937513
...
...
@@ -3,7 +3,7 @@
"baseUrl"
:
"."
,
"module"
:
"ESNext"
,
"target"
:
"ESNext"
,
"lib"
:
[
"DOM"
,
"ESNext"
],
"lib"
:
[
"DOM"
,
"ESNext"
,
"WebWorker"
],
"strict"
:
true
,
"esModuleInterop"
:
true
,
"jsx"
:
"preserve"
,
...
...
front/vite.config.ts
View file @
8e937513
import
{
defineConfig
,
HmrOptions
}
from
'
vite
'
import
{
createVuePlugin
as
Vue2
}
from
'
vite-plugin-vue2
'
import
ScriptSetup
from
'
unplugin-vue2-script-setup/vite
'
// @ts-ignore
import
path
from
'
path
'
import
{
VitePWA
}
from
'
vite-plugin-pwa
'
import
{
resolve
}
from
'
path
'
const
port
=
+
(
process
.
env
.
VUE_PORT
??
8080
)
...
...
@@ -29,6 +28,18 @@ export default defineConfig(() => ({
// https://github.com/antfu/unplugin-vue2-script-setup
ScriptSetup
(),
// https://github.com/antfu/vite-plugin-pwa
VitePWA
({
strategies
:
'
injectManifest
'
,
srcDir
:
'
src
'
,
filename
:
'
serviceWorker.ts
'
,
devOptions
:
{
enabled
:
true
,
type
:
'
module
'
,
navigateFallback
:
'
index.html
'
}
}),
{
name
:
'
fix-fomantic-ui-css
'
,
transform
(
src
,
id
)
{
...
...
@@ -41,7 +52,7 @@ export default defineConfig(() => ({
server
:
{
port
,
hmr
},
resolve
:
{
alias
:
{
'
~
'
:
path
.
resolve
(
__dirname
,
'
./src
'
)
'
~
'
:
resolve
(
__dirname
,
'
./src
'
)
}
},
build
:
{
...
...
front/yarn.lock
View file @
8e937513
This diff is collapsed.
Click to expand it.
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment