Set up initial configuration for installing
Set up player config for mobile control
This commit is contained in:
parent
551fb6d164
commit
2302dc0581
|
@ -117,6 +117,13 @@ FUNKWHALE_SPA_HTML_CACHE_DURATION = env.int(
|
||||||
FUNKWHALE_EMBED_URL = env(
|
FUNKWHALE_EMBED_URL = env(
|
||||||
"FUNKWHALE_EMBED_URL", default=FUNKWHALE_URL + "/front/embed.html"
|
"FUNKWHALE_EMBED_URL", default=FUNKWHALE_URL + "/front/embed.html"
|
||||||
)
|
)
|
||||||
|
FUNKWHALE_SPA_REWRITE_MANIFEST = env.bool(
|
||||||
|
"FUNKWHALE_SPA_REWRITE_MANIFEST", default=True
|
||||||
|
)
|
||||||
|
FUNKWHALE_SPA_REWRITE_MANIFEST_URL = env.bool(
|
||||||
|
"FUNKWHALE_SPA_REWRITE_MANIFEST_URL", default=None
|
||||||
|
)
|
||||||
|
|
||||||
APP_NAME = "Funkwhale"
|
APP_NAME = "Funkwhale"
|
||||||
|
|
||||||
# XXX: deprecated, see #186
|
# XXX: deprecated, see #186
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import html
|
import html
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import xml.sax.saxutils
|
import xml.sax.saxutils
|
||||||
|
|
||||||
|
@ -9,6 +11,8 @@ from django.core.cache import caches
|
||||||
from django import urls
|
from django import urls
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
|
|
||||||
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
|
|
||||||
from . import preferences
|
from . import preferences
|
||||||
from . import session
|
from . import session
|
||||||
from . import throttling
|
from . import throttling
|
||||||
|
@ -26,6 +30,13 @@ def should_fallback_to_spa(path):
|
||||||
def serve_spa(request):
|
def serve_spa(request):
|
||||||
html = get_spa_html(settings.FUNKWHALE_SPA_HTML_ROOT)
|
html = get_spa_html(settings.FUNKWHALE_SPA_HTML_ROOT)
|
||||||
head, tail = html.split("</head>", 1)
|
head, tail = html.split("</head>", 1)
|
||||||
|
if settings.FUNKWHALE_SPA_REWRITE_MANIFEST:
|
||||||
|
new_url = (
|
||||||
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL
|
||||||
|
or federation_utils.full_url(urls.reverse("api:v1:instance:spa-manifest"))
|
||||||
|
)
|
||||||
|
head = replace_manifest_url(head, new_url)
|
||||||
|
|
||||||
if not preferences.get("common__api_authentication_required"):
|
if not preferences.get("common__api_authentication_required"):
|
||||||
try:
|
try:
|
||||||
request_tags = get_request_head_tags(request) or []
|
request_tags = get_request_head_tags(request) or []
|
||||||
|
@ -66,17 +77,34 @@ def serve_spa(request):
|
||||||
return http.HttpResponse(head + tail)
|
return http.HttpResponse(head + tail)
|
||||||
|
|
||||||
|
|
||||||
|
MANIFEST_LINK_REGEX = re.compile(r"<link .*rel=(?:'|\")?manifest(?:'|\")?.*>")
|
||||||
|
|
||||||
|
|
||||||
|
def replace_manifest_url(head, new_url):
|
||||||
|
replacement = '<link rel=manifest href="{}">'.format(new_url)
|
||||||
|
head = MANIFEST_LINK_REGEX.sub(replacement, head)
|
||||||
|
return head
|
||||||
|
|
||||||
|
|
||||||
def get_spa_html(spa_url):
|
def get_spa_html(spa_url):
|
||||||
|
return get_spa_file(spa_url, "index.html")
|
||||||
|
|
||||||
|
|
||||||
|
def get_spa_file(spa_url, name):
|
||||||
if spa_url.startswith("/"):
|
if spa_url.startswith("/"):
|
||||||
|
# XXX: spa_url is an absolute path to index.html, on the local disk.
|
||||||
|
# However, we may want to access manifest.json or other files as well, so we
|
||||||
|
# strip the filename
|
||||||
|
path = os.path.join(os.path.dirname(spa_url), name)
|
||||||
# we try to open a local file
|
# we try to open a local file
|
||||||
with open(spa_url) as f:
|
with open(path) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
cache_key = "spa-html:{}".format(spa_url)
|
cache_key = "spa-file:{}:{}".format(spa_url, name)
|
||||||
cached = caches["local"].get(cache_key)
|
cached = caches["local"].get(cache_key)
|
||||||
if cached:
|
if cached:
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
response = session.get_session().get(utils.join_url(spa_url, "index.html"),)
|
response = session.get_session().get(utils.join_url(spa_url, name),)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
content = response.text
|
content = response.text
|
||||||
caches["local"].set(cache_key, content, settings.FUNKWHALE_SPA_HTML_CACHE_DURATION)
|
caches["local"].set(cache_key, content, settings.FUNKWHALE_SPA_HTML_CACHE_DURATION)
|
||||||
|
|
|
@ -9,4 +9,5 @@ admin_router.register(r"admin/settings", views.AdminSettings, "admin-settings")
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^nodeinfo/2.0/?$", views.NodeInfo.as_view(), name="nodeinfo-2.0"),
|
url(r"^nodeinfo/2.0/?$", views.NodeInfo.as_view(), name="nodeinfo-2.0"),
|
||||||
url(r"^settings/?$", views.InstanceSettings.as_view(), name="settings"),
|
url(r"^settings/?$", views.InstanceSettings.as_view(), name="settings"),
|
||||||
|
url(r"^spa-manifest.json", views.SpaManifest.as_view(), name="spa-manifest"),
|
||||||
] + admin_router.urls
|
] + admin_router.urls
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from dynamic_preferences.api import serializers
|
from dynamic_preferences.api import serializers
|
||||||
from dynamic_preferences.api import viewsets as preferences_viewsets
|
from dynamic_preferences.api import viewsets as preferences_viewsets
|
||||||
from dynamic_preferences.registries import global_preferences_registry
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from funkwhale_api.common import middleware
|
||||||
|
from funkwhale_api.common import preferences
|
||||||
from funkwhale_api.users.oauth import permissions as oauth_permissions
|
from funkwhale_api.users.oauth import permissions as oauth_permissions
|
||||||
|
|
||||||
from . import nodeinfo
|
from . import nodeinfo
|
||||||
|
@ -39,3 +45,23 @@ class NodeInfo(views.APIView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
data = nodeinfo.get()
|
data = nodeinfo.get()
|
||||||
return Response(data, status=200, content_type=NODEINFO_2_CONTENT_TYPE)
|
return Response(data, status=200, content_type=NODEINFO_2_CONTENT_TYPE)
|
||||||
|
|
||||||
|
|
||||||
|
class SpaManifest(views.APIView):
|
||||||
|
permission_classes = []
|
||||||
|
authentication_classes = []
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
existing_manifest = middleware.get_spa_file(
|
||||||
|
settings.FUNKWHALE_SPA_HTML_ROOT, "manifest.json"
|
||||||
|
)
|
||||||
|
parsed_manifest = json.loads(existing_manifest)
|
||||||
|
parsed_manifest["short_name"] = settings.APP_NAME
|
||||||
|
instance_name = preferences.get("instance__name")
|
||||||
|
if instance_name:
|
||||||
|
parsed_manifest["short_name"] = instance_name
|
||||||
|
parsed_manifest["name"] = instance_name
|
||||||
|
instance_description = preferences.get("instance__short_description")
|
||||||
|
if instance_description:
|
||||||
|
parsed_manifest["description"] = instance_description
|
||||||
|
return Response(parsed_manifest, status=200)
|
||||||
|
|
|
@ -2,6 +2,9 @@ import time
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
|
|
||||||
from funkwhale_api.common import middleware
|
from funkwhale_api.common import middleware
|
||||||
from funkwhale_api.common import throttling
|
from funkwhale_api.common import throttling
|
||||||
|
@ -112,7 +115,7 @@ def test_get_default_head_tags(preferences, settings):
|
||||||
|
|
||||||
|
|
||||||
def test_get_spa_html_from_cache(local_cache):
|
def test_get_spa_html_from_cache(local_cache):
|
||||||
local_cache.set("spa-html:http://test", "hello world")
|
local_cache.set("spa-file:http://test:index.html", "hello world")
|
||||||
|
|
||||||
assert middleware.get_spa_html("http://test") == "hello world"
|
assert middleware.get_spa_html("http://test") == "hello world"
|
||||||
|
|
||||||
|
@ -124,16 +127,16 @@ def test_get_spa_html_from_http(local_cache, r_mock, mocker, settings):
|
||||||
|
|
||||||
assert middleware.get_spa_html("http://test") == "hello world"
|
assert middleware.get_spa_html("http://test") == "hello world"
|
||||||
cache_set.assert_called_once_with(
|
cache_set.assert_called_once_with(
|
||||||
"spa-html:{}".format(url),
|
"spa-file:{}:index.html".format(url),
|
||||||
"hello world",
|
"hello world",
|
||||||
settings.FUNKWHALE_SPA_HTML_CACHE_DURATION,
|
settings.FUNKWHALE_SPA_HTML_CACHE_DURATION,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_spa_html_from_disk(tmpfile):
|
def test_get_spa_html_from_disk(tmp_path):
|
||||||
with open(tmpfile.name, "wb") as f:
|
index = tmp_path / "index.html"
|
||||||
f.write(b"hello world")
|
index.write_bytes(b"hello world")
|
||||||
assert middleware.get_spa_html(tmpfile.name) == "hello world"
|
assert middleware.get_spa_html(str(index)) == "hello world"
|
||||||
|
|
||||||
|
|
||||||
def test_get_route_head_tags(mocker, settings):
|
def test_get_route_head_tags(mocker, settings):
|
||||||
|
@ -225,3 +228,97 @@ def test_throttle_status_middleware_returns_proper_response(mocker):
|
||||||
|
|
||||||
response = m(request)
|
response = m(request)
|
||||||
assert response.status_code == 429
|
assert response.status_code == 429
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"link, new_url, expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"<link rel=manifest href=/manifest.json>",
|
||||||
|
"custom_url",
|
||||||
|
'<link rel=manifest href="custom_url">',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"<link href=/manifest.json rel=manifest>",
|
||||||
|
"custom_url",
|
||||||
|
'<link rel=manifest href="custom_url">',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'<link href="/manifest.json" rel=manifest>',
|
||||||
|
"custom_url",
|
||||||
|
'<link rel=manifest href="custom_url">',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'<link href=/manifest.json rel="manifest">',
|
||||||
|
"custom_url",
|
||||||
|
'<link rel=manifest href="custom_url">',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"<link href='/manifest.json' rel=manifest>",
|
||||||
|
"custom_url",
|
||||||
|
'<link rel=manifest href="custom_url">',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"<link href=/manifest.json rel='manifest'>",
|
||||||
|
"custom_url",
|
||||||
|
'<link rel=manifest href="custom_url">',
|
||||||
|
),
|
||||||
|
# not matching
|
||||||
|
(
|
||||||
|
"<link href=/manifest.json rel=notmanifest>",
|
||||||
|
"custom_url",
|
||||||
|
"<link href=/manifest.json rel=notmanifest>",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_rewrite_manifest_json_url(link, new_url, expected, mocker, settings):
|
||||||
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST = True
|
||||||
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL = new_url
|
||||||
|
spa_html = "<html><head>{}</head></html>".format(link)
|
||||||
|
request = mocker.Mock(path="/")
|
||||||
|
mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
|
||||||
|
mocker.patch.object(
|
||||||
|
middleware, "get_default_head_tags", return_value=[],
|
||||||
|
)
|
||||||
|
response = middleware.serve_spa(request)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
expected_html = "<html><head>{}\n\n</head></html>".format(expected)
|
||||||
|
assert response.content == expected_html.encode()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rewrite_manifest_json_url_rewrite_disabled(mocker, settings):
|
||||||
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST = False
|
||||||
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL = "custom_url"
|
||||||
|
spa_html = "<html><head><link href=/manifest.json rel=manifest></head></html>"
|
||||||
|
request = mocker.Mock(path="/")
|
||||||
|
mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
|
||||||
|
mocker.patch.object(
|
||||||
|
middleware, "get_default_head_tags", return_value=[],
|
||||||
|
)
|
||||||
|
response = middleware.serve_spa(request)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
expected_html = (
|
||||||
|
"<html><head><link href=/manifest.json rel=manifest>\n\n</head></html>"
|
||||||
|
)
|
||||||
|
assert response.content == expected_html.encode()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rewrite_manifest_json_url_rewrite_default_url(mocker, settings):
|
||||||
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST = True
|
||||||
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL = None
|
||||||
|
spa_html = "<html><head><link href=/manifest.json rel=manifest></head></html>"
|
||||||
|
expected_url = federation_utils.full_url(reverse("api:v1:instance:spa-manifest"))
|
||||||
|
request = mocker.Mock(path="/")
|
||||||
|
mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
|
||||||
|
mocker.patch.object(
|
||||||
|
middleware, "get_default_head_tags", return_value=[],
|
||||||
|
)
|
||||||
|
response = middleware.serve_spa(request)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
expected_html = '<html><head><link rel=manifest href="{}">\n\n</head></html>'.format(
|
||||||
|
expected_url
|
||||||
|
)
|
||||||
|
assert response.content == expected_html.encode()
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import json
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,3 +39,25 @@ def test_admin_settings_correct_permission(db, logged_in_api_client, preferences
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert len(response.data) == len(preferences.all())
|
assert len(response.data) == len(preferences.all())
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifest_endpoint(api_client, mocker, preferences, tmp_path, settings):
|
||||||
|
settings.FUNKWHALE_SPA_HTML_ROOT = str(tmp_path / "index.html")
|
||||||
|
preferences["instance__name"] = "Test pod"
|
||||||
|
preferences["instance__short_description"] = "Test description"
|
||||||
|
base_payload = {
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
manifest = tmp_path / "manifest.json"
|
||||||
|
expected = {
|
||||||
|
"foo": "bar",
|
||||||
|
"name": "Test pod",
|
||||||
|
"short_name": "Test pod",
|
||||||
|
"description": "Test description",
|
||||||
|
}
|
||||||
|
manifest.write_bytes(json.dumps(base_payload).encode())
|
||||||
|
|
||||||
|
url = reverse("api:v1:instance:spa-manifest")
|
||||||
|
response = api_client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
|
|
@ -2,13 +2,15 @@
|
||||||
"name": "front",
|
"name": "front",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"description": "Funkwhale front-end",
|
||||||
|
"author": "Eliot Berriot <contact@eliotberriot.com>",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve --port ${VUE_PORT:-8000} --host ${VUE_HOST:-0.0.0.0}",
|
"serve": "vue-cli-service serve --port ${VUE_PORT:-8000} --host ${VUE_HOST:-0.0.0.0}",
|
||||||
"build": "scripts/i18n-compile.sh && vue-cli-service build",
|
"build": "scripts/i18n-compile.sh && vue-cli-service build",
|
||||||
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"lint": "vue-cli-service lint",
|
"lint": "vue-cli-service lint",
|
||||||
"i18n-extract": "scripts/i18n-extract.sh",
|
|
||||||
"i18n-compile": "scripts/i18n-compile.sh",
|
"i18n-compile": "scripts/i18n-compile.sh",
|
||||||
"test:unit": "vue-cli-service test:unit"
|
"i18n-extract": "scripts/i18n-extract.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
|
@ -23,6 +25,7 @@
|
||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"qs": "^6.7.0",
|
"qs": "^6.7.0",
|
||||||
|
"register-service-worker": "^1.6.2",
|
||||||
"sanitize-html": "^1.20.1",
|
"sanitize-html": "^1.20.1",
|
||||||
"showdown": "^1.8.6",
|
"showdown": "^1.8.6",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
|
@ -40,6 +43,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^3.0.0",
|
"@vue/cli-plugin-babel": "^3.0.0",
|
||||||
"@vue/cli-plugin-eslint": "^3.0.0",
|
"@vue/cli-plugin-eslint": "^3.0.0",
|
||||||
|
"@vue/cli-plugin-pwa": "^4.1.2",
|
||||||
"@vue/cli-plugin-unit-mocha": "^3.0.0",
|
"@vue/cli-plugin-unit-mocha": "^3.0.0",
|
||||||
"@vue/cli-service": "^3.0.0",
|
"@vue/cli-service": "^3.0.0",
|
||||||
"@vue/test-utils": "^1.0.0-beta.20",
|
"@vue/test-utils": "^1.0.0-beta.20",
|
||||||
|
@ -104,7 +108,5 @@
|
||||||
"iOS >= 9",
|
"iOS >= 9",
|
||||||
"Android >= 4",
|
"Android >= 4",
|
||||||
"not dead"
|
"not dead"
|
||||||
],
|
]
|
||||||
"author": "Eliot Berriot <contact@eliotberriot.com>",
|
|
||||||
"description": "Funkwhale front-end"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,16 @@
|
||||||
<template>
|
<template>
|
||||||
<sidebar></sidebar>
|
<sidebar></sidebar>
|
||||||
<set-instance-modal @update:show="showSetInstanceModal = $event" :show="showSetInstanceModal"></set-instance-modal>
|
<set-instance-modal @update:show="showSetInstanceModal = $event" :show="showSetInstanceModal"></set-instance-modal>
|
||||||
<service-messages v-if="messages.length > 0"/>
|
<service-messages>
|
||||||
|
<message key="refreshApp" class="large info" v-if="serviceWorker.updateAvailable">
|
||||||
|
<p>
|
||||||
|
<translate translate-context="App/Message/Paragraph">A new version of the app is available.</translate>
|
||||||
|
</p>
|
||||||
|
<button class="ui basic button" @click.stop="updateApp">
|
||||||
|
<translate translate-context="App/Message/Button">Update</translate>
|
||||||
|
</button>
|
||||||
|
</message>
|
||||||
|
</service-messages>
|
||||||
<transition name="queue">
|
<transition name="queue">
|
||||||
<queue @touch-progress="$refs.player.setCurrentTime($event)" v-if="$store.state.ui.queueFocused"></queue>
|
<queue @touch-progress="$refs.player.setCurrentTime($event)" v-if="$store.state.ui.queueFocused"></queue>
|
||||||
</transition>
|
</transition>
|
||||||
|
@ -62,10 +71,23 @@ export default {
|
||||||
bridge: null,
|
bridge: null,
|
||||||
instanceUrl: null,
|
instanceUrl: null,
|
||||||
showShortcutsModal: false,
|
showShortcutsModal: false,
|
||||||
showSetInstanceModal: false
|
showSetInstanceModal: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created () {
|
async created () {
|
||||||
|
|
||||||
|
if (navigator.serviceWorker) {
|
||||||
|
navigator.serviceWorker.addEventListener(
|
||||||
|
'controllerchange', () => {
|
||||||
|
if (this.serviceWorker.refreshing) return;
|
||||||
|
this.$store.commit('ui/serviceWorker', {
|
||||||
|
refreshing: true
|
||||||
|
})
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.openWebsocket()
|
this.openWebsocket()
|
||||||
let self = this
|
let self = this
|
||||||
if (!this.$store.state.ui.selectedLanguage) {
|
if (!this.$store.state.ui.selectedLanguage) {
|
||||||
|
@ -238,6 +260,11 @@ export default {
|
||||||
parts.push(this.$store.state.instance.settings.instance.name.value || 'Funkwhale')
|
parts.push(this.$store.state.instance.settings.instance.name.value || 'Funkwhale')
|
||||||
document.title = parts.join(' – ')
|
document.title = parts.join(' – ')
|
||||||
},
|
},
|
||||||
|
updateApp () {
|
||||||
|
this.$store.commit('ui/serviceWorker', {updateAvailable: false})
|
||||||
|
if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return; }
|
||||||
|
this.serviceWorker.registration.waiting.postMessage('skipWaiting');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
|
@ -246,6 +273,7 @@ export default {
|
||||||
playing: state => state.player.playing,
|
playing: state => state.player.playing,
|
||||||
bufferProgress: state => state.player.bufferProgress,
|
bufferProgress: state => state.player.bufferProgress,
|
||||||
isLoadingAudio: state => state.player.isLoadingAudio,
|
isLoadingAudio: state => state.player.isLoadingAudio,
|
||||||
|
serviceWorker: state => state.ui.serviceWorker,
|
||||||
}),
|
}),
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
hasNext: "queue/hasNext",
|
hasNext: "queue/hasNext",
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<message v-for="message in displayedMessages" :key="String(message.date)" :class="['large', getLevel(message)]">
|
<message v-for="message in displayedMessages" :key="String(message.date)" :class="['large', getLevel(message)]">
|
||||||
<p>{{ message.content }}</p>
|
<p>{{ message.content }}</p>
|
||||||
</message>
|
</message>
|
||||||
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -276,6 +276,15 @@ export default {
|
||||||
if (this.currentTrack) {
|
if (this.currentTrack) {
|
||||||
this.getSound(this.currentTrack)
|
this.getSound(this.currentTrack)
|
||||||
}
|
}
|
||||||
|
// Add controls for notification drawer
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.setActionHandler('play', this.togglePlay);
|
||||||
|
navigator.mediaSession.setActionHandler('pause', this.togglePlay);
|
||||||
|
navigator.mediaSession.setActionHandler('seekforward', this.seekForward);
|
||||||
|
navigator.mediaSession.setActionHandler('seekbackward', this.seekBackward);
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', this.next);
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', this.previous);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
this.dummyAudio.unload()
|
this.dummyAudio.unload()
|
||||||
|
@ -380,6 +389,7 @@ export default {
|
||||||
self.$store.commit('player/resetErrorCount')
|
self.$store.commit('player/resetErrorCount')
|
||||||
self.$store.commit('player/errored', false)
|
self.$store.commit('player/errored', false)
|
||||||
self.$store.commit('player/duration', this.duration())
|
self.$store.commit('player/duration', this.duration())
|
||||||
|
|
||||||
},
|
},
|
||||||
onloaderror: function (sound, error) {
|
onloaderror: function (sound, error) {
|
||||||
self.removeFromCache(this)
|
self.removeFromCache(this)
|
||||||
|
@ -484,6 +494,12 @@ export default {
|
||||||
this.$store.dispatch('player/updateProgress', position)
|
this.$store.dispatch('player/updateProgress', position)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
seekForward () {
|
||||||
|
this.seek (5)
|
||||||
|
},
|
||||||
|
seekBackward () {
|
||||||
|
this.seek (-5)
|
||||||
|
},
|
||||||
observeProgress: function (enable) {
|
observeProgress: function (enable) {
|
||||||
let self = this
|
let self = this
|
||||||
if (enable) {
|
if (enable) {
|
||||||
|
@ -684,6 +700,23 @@ export default {
|
||||||
this.playTimeout = setTimeout(async () => {
|
this.playTimeout = setTimeout(async () => {
|
||||||
await self.loadSound(newValue, oldValue)
|
await self.loadSound(newValue, oldValue)
|
||||||
}, 500);
|
}, 500);
|
||||||
|
// If the session is playing as a PWA, populate the notification
|
||||||
|
// with details from the track
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
title: this.currentTrack.title,
|
||||||
|
artist: this.currentTrack.artist.name,
|
||||||
|
album: this.currentTrack.album.title,
|
||||||
|
artwork: [
|
||||||
|
{ src: this.currentTrack.album.cover.original, sizes: '96x96', type: 'image/png' },
|
||||||
|
{ src: this.currentTrack.album.cover.original, sizes: '128x128', type: 'image/png' },
|
||||||
|
{ src: this.currentTrack.album.cover.original, sizes: '192x192', type: 'image/png' },
|
||||||
|
{ src: this.currentTrack.album.cover.original, sizes: '256x256', type: 'image/png' },
|
||||||
|
{ src: this.currentTrack.album.cover.original, sizes: '384x384', type: 'image/png' },
|
||||||
|
{ src: this.currentTrack.album.cover.original, sizes: '512x512', type: 'image/png' },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
immediate: false
|
immediate: false
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,7 @@ import locales from '@/locales'
|
||||||
|
|
||||||
import filters from '@/filters' // eslint-disable-line
|
import filters from '@/filters' // eslint-disable-line
|
||||||
import globals from '@/components/globals' // eslint-disable-line
|
import globals from '@/components/globals' // eslint-disable-line
|
||||||
|
import './registerServiceWorker'
|
||||||
|
|
||||||
sync(store, router)
|
sync(store, router)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import { register } from 'register-service-worker'
|
||||||
|
|
||||||
|
import store from './store'
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||||
|
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
|
||||||
|
var checkInterval = 1000 * 60 * 60 * 2
|
||||||
|
// var checkInterval = 1000 * 5
|
||||||
|
setInterval(() => {
|
||||||
|
console.log('Checking for service worker update…')
|
||||||
|
registration.update();
|
||||||
|
}, checkInterval);
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// 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.
|
||||||
|
self.addEventListener('message', (e) => {
|
||||||
|
if (!e.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.data) {
|
||||||
|
case 'skipWaiting':
|
||||||
|
self.skipWaiting();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// NOOP
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
workbox.core.clientsClaim();
|
||||||
|
|
||||||
|
// The precaching code provided by Workbox.
|
||||||
|
self.__precacheManifest = [].concat(self.__precacheManifest || []);
|
||||||
|
console.log('Files to be cached by service worker [before filtering]', self.__precacheManifest.length);
|
||||||
|
var excludedUrlsPrefix = [
|
||||||
|
'/js/locale-',
|
||||||
|
'/js/moment-locale-',
|
||||||
|
'/js/admin',
|
||||||
|
'/css/admin',
|
||||||
|
];
|
||||||
|
self.__precacheManifest = self.__precacheManifest.filter((e) => {
|
||||||
|
return !excludedUrlsPrefix.some(prefix => e.url.startsWith(prefix))
|
||||||
|
});
|
||||||
|
console.log('Files to be cached by service worker [after filtering]', self.__precacheManifest.length);
|
||||||
|
// workbox.precaching.suppressWarnings(); // Only used with Vue CLI 3 and Workbox v3.
|
||||||
|
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
|
|
@ -68,6 +68,11 @@ export default {
|
||||||
ordering: "creation_date",
|
ordering: "creation_date",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
serviceWorker: {
|
||||||
|
refreshing: false,
|
||||||
|
registration: null,
|
||||||
|
updateAvailable: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
showInstanceSupportMessage: (state, getters, rootState) => {
|
showInstanceSupportMessage: (state, getters, rootState) => {
|
||||||
|
@ -160,6 +165,10 @@ export default {
|
||||||
orderingDirection: (state, {route, value}) => {
|
orderingDirection: (state, {route, value}) => {
|
||||||
state.routePreferences[route].orderingDirection = value
|
state.routePreferences[route].orderingDirection = value
|
||||||
},
|
},
|
||||||
|
|
||||||
|
serviceWorker: (state, value) => {
|
||||||
|
state.serviceWorker = {...state.serviceWorker, ...value}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
fetchUnreadNotifications ({commit}, payload) {
|
fetchUnreadNotifications ({commit}, payload) {
|
||||||
|
|
|
@ -32,6 +32,46 @@ plugins.push(
|
||||||
module.exports = {
|
module.exports = {
|
||||||
baseUrl: process.env.BASE_URL || '/front/',
|
baseUrl: process.env.BASE_URL || '/front/',
|
||||||
productionSourceMap: false,
|
productionSourceMap: false,
|
||||||
|
// Add settings for manifest file
|
||||||
|
pwa: {
|
||||||
|
name: 'Funkwhale',
|
||||||
|
themeColor: '#f2711c',
|
||||||
|
msTileColor: '#000000',
|
||||||
|
appleMobileWebAppCapable: 'yes',
|
||||||
|
appleMobileWebAppStatusBarStyle: 'black',
|
||||||
|
display: 'minimal-ui',
|
||||||
|
workboxPluginMode: 'InjectManifest',
|
||||||
|
manifestOptions: {
|
||||||
|
start_url: '.',
|
||||||
|
description: 'A social platform to enjoy and share music',
|
||||||
|
scope: "/",
|
||||||
|
categories: ["music"],
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
'src': 'favicon.png',
|
||||||
|
'sizes': '192x192',
|
||||||
|
'type': 'image/png'
|
||||||
|
}, {
|
||||||
|
'src': 'favicon.png',
|
||||||
|
'sizes': '512x512',
|
||||||
|
'type': 'image/png'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
workboxOptions: {
|
||||||
|
importWorkboxFrom: 'local',
|
||||||
|
// swSrc is required in InjectManifest mode.
|
||||||
|
swSrc: 'src/service-worker.js',
|
||||||
|
swDest: 'service-worker.js',
|
||||||
|
},
|
||||||
|
iconPaths: {
|
||||||
|
favicon32: 'favicon.png',
|
||||||
|
favicon16: 'favicon.png',
|
||||||
|
appleTouchIcon: 'favicon.png',
|
||||||
|
maskIcon: 'favicon.png',
|
||||||
|
msTileImage: 'favicon.png'
|
||||||
|
}
|
||||||
|
},
|
||||||
pages: {
|
pages: {
|
||||||
embed: {
|
embed: {
|
||||||
entry: 'src/embed.js',
|
entry: 'src/embed.js',
|
||||||
|
|
635
front/yarn.lock
635
front/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue