Support PWA (#437)

* add PWA

* cleanup

* add offline cache
This commit is contained in:
wengtad 2021-05-28 00:48:59 +08:00 committed by GitHub
parent 8e7a17b1bb
commit 39baca4462
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1034 additions and 45 deletions

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@
"fast-levenshtein": "^3.0.0",
"fuse.js": "^6.4.6",
"qs": "^6.9.6",
"register-service-worker": "^1.7.1",
"typeface-roboto": "^1.1.13",
"v-jsoneditor": "^1.4.2",
"vue": "^2.6.11",
@ -30,6 +31,7 @@
"@mdi/font": "^5.9.55",
"@vue/cli-plugin-babel": "^4.5.11",
"@vue/cli-plugin-eslint": "^4.5.11",
"@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-service": "^4.5.12",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
@ -55,16 +57,16 @@
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"prettier": {
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"printWidth": 120
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" version="1.1">
<path d="M 162.083 54.642 C 148.745 68.272, 137.170 80.703, 136.362 82.266 C 133.689 87.435, 133.522 94.130, 135.929 99.573 C 137.122 102.269, 139.070 105.510, 140.258 106.775 L 142.418 109.074 90.974 160.526 L 39.529 211.979 46.999 219.499 L 54.470 227.020 91.235 190.265 L 128 153.510 164.765 190.265 L 201.530 227.020 209 219.500 L 216.470 211.980 179.725 175.225 L 142.980 138.470 150.320 131.178 C 156.858 124.685, 157.808 124.063, 159.001 125.501 C 162.066 129.195, 168.873 132.163, 174.392 132.213 C 183.508 132.295, 186.374 130.174, 212.477 104.038 L 236.454 80.030 231.501 75.001 L 226.548 69.973 209.288 87.212 L 192.027 104.452 187 99.500 L 181.973 94.548 199.212 77.288 L 216.452 60.027 211.500 55 L 206.548 49.973 189.288 67.212 L 172.027 84.452 167 79.500 L 161.973 74.548 179.225 57.275 L 196.477 40.001 191.406 34.930 L 186.335 29.859 162.083 54.642 M 38.429 41.250 C 31.557 49.376, 28.011 62.815, 29.835 73.824 C 31.955 86.615, 34.508 90.093, 61.720 117.253 L 86.520 142.005 101.501 126.999 L 116.482 111.993 79.496 74.996 C 59.154 54.648, 42.210 38, 41.844 38 C 41.478 38, 39.941 39.462, 38.429 41.250" stroke="none" fill="black" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,21 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="apple-touch-icon" sizes="256x256" href="<%= BASE_URL %>favicon.ico">
<link rel="manifest" href='data:application/manifest+json,{"name":"Mealie","icons":[{"src":"<%= BASE_URL %>favicon.ico","sizes":"256x256","type":"image/png"}],"scope":"<%= BASE_URL %>","start_url":"<%= BASE_URL %>","display":"standalone"}'>
<title> Mealie </title>
<!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"> -->
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"> -->
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="description" content="Mealie is a self hosted recipe manager and meal planner.">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title> Mealie </title>
<!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"> -->
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"> -->
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -0,0 +1,82 @@
{
"name": "Mealie",
"short_name": "Mealie",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./img/icons/android-chrome-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./img/icons/apple-touch-icon-60x60.png",
"sizes": "60x60",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-76x76.png",
"sizes": "76x76",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-120x120.png",
"sizes": "120x120",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-180x180.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "./img/icons/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "./img/icons/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "./img/icons/msapplication-icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "./img/icons/mstile-150x150.png",
"sizes": "150x150",
"type": "image/png"
}
],
"start_url": ".",
"display": "standalone",
"background_color": "#FFFFFF",
"theme_color": "#E58325"
}

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@ -9,6 +9,17 @@
</div>
</v-banner>
<GlobalSnackbar />
<v-snackbar v-model="snackWithButtons" bottom left timeout="-1">
{{ snackWithBtnText }}
<template v-slot:action="{ attrs }">
<v-btn text color="primary" v-bind="attrs" @click.stop="refreshApp">
{{ snackBtnText }}
</v-btn>
<v-btn icon class="ml-4" @click="snackWithButtons = false">
<v-icon>{{ $globals.icons.close }}</v-icon>
</v-btn>
</template>
</v-snackbar>
<router-view></router-view>
</v-main>
</v-app>
@ -51,6 +62,29 @@ export default {
this.$store.dispatch("requestSiteSettings");
},
data() {
return {
refreshing: false,
registration: null,
snackBtnText: "",
snackWithBtnText: "",
snackWithButtons: false,
};
},
created() {
// Listen for swUpdated event and display refresh snackbar as required.
document.addEventListener("swUpdated", this.showRefreshUI, { once: true });
// Refresh all open app tabs when a new service worker is installed.
if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (this.refreshing) return;
this.refreshing = true;
window.location.reload();
});
}
},
methods: {
// For Later!
@ -70,6 +104,25 @@ export default {
this.darkModeSystemCheck();
});
},
showRefreshUI(e) {
// Display a snackbar inviting the user to refresh/reload the app due
// to an app update being available.
// The new service worker is installed, but not yet active.
// Store the ServiceWorkerRegistration instance for later use.
this.registration = e.detail;
this.snackBtnText = "Refresh";
this.snackWithBtnText = "New version available!";
this.snackWithButtons = true;
},
refreshApp() {
this.snackWithButtons = false;
// Protect against missing registration.waiting.
if (!this.registration || !this.registration.waiting) {
return;
}
this.registration.waiting.postMessage("skipWaiting");
},
},
};
</script>

View File

@ -8,6 +8,7 @@ import { globals } from "@/utils/globals";
import i18n from "./i18n";
import "@mdi/font/css/materialdesignicons.css";
import "typeface-roboto/index.css";
import './registerServiceWorker'
Vue.config.productionTip = false;
Vue.use(VueRouter);

View File

@ -0,0 +1,39 @@
/* eslint-disable no-console */
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log("Service worker is active.");
},
registered(registration) {
console.log("Service worker has been registered.");
// Routinely check for app updates by testing for a new service worker.
setInterval(() => {
registration.update();
}, 1000 * 60 * 60); // hourly checks
},
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.");
// Add a custom event and dispatch it.
// Used to display of a 'refresh' banner following a service worker update.
// Set the event payload to the service worker registration object.
document.dispatchEvent(new CustomEvent("swUpdated", { detail: registration }));
},
offline() {
console.log("No internet connection found. App is running in offline mode.");
},
error(error) {
console.error("Error during service worker registration:", error);
},
});
}

75
frontend/src/sw.js Normal file
View File

@ -0,0 +1,75 @@
/* eslint-disable no-undef, no-underscore-dangle, no-restricted-globals */
self.addEventListener("install", event => {
event.waitUntil(preLoad());
});
var preLoad = async () => {
console.log("Installing web app");
const cache = await caches.open("offline");
console.log("caching index and important routes");
return await cache.addAll(["/", "/recipes/all"]);
};
self.addEventListener("fetch", event => {
event.respondWith(
checkResponse(event.request).catch(() => {
return returnFromCache(event.request);
})
);
event.waitUntil(addToCache(event.request));
});
var checkResponse = request => {
return new Promise(function(fulfill, reject) {
fetch(request).then(function(response) {
if (response.status !== 404) {
fulfill(response);
} else {
reject();
}
}, reject);
});
};
var addToCache = async request => {
const cache = await caches.open("offline");
const response = await fetch(request);
console.log(response.url + " was cached");
return await cache.put(request, response);
};
var returnFromCache = async request => {
const cache = await caches.open("offline");
const matching = await cache.match(request);
if (!matching || matching.status == 404) {
return cache.match("offline.html");
} else {
return matching;
}
};
// 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(); // Vue CLI 4 and Workbox v4, else
// workbox.clientsClaim(); // Vue CLI 3 and Workbox v3.
// 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, {});

View File

@ -1,4 +1,5 @@
const path = require("path");
const manifestJSON = require("./public/manifest.json");
module.exports = {
transpileDependencies: ["vuetify"],
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
@ -26,4 +27,17 @@ module.exports = {
},
},
},
pwa: {
name: manifestJSON.short_name,
themeColor: manifestJSON.theme_color,
msTileColor: manifestJSON.background_color,
appleMobileWebAppCapable: "yes",
appleMobileWebAppStatusBarStyle: "black",
workboxPluginMode: "InjectManifest",
workboxOptions: {
swSrc: "./src/sw.js",
swDest: "service-worker.js",
},
},
};