755
frontend/package-lock.json
generated
@ -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"
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 32 KiB |
BIN
frontend/public/img/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
frontend/public/img/icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/img/icons/android-chrome-maskable-192x192.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
frontend/public/img/icons/android-chrome-maskable-512x512.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/img/icons/apple-touch-icon-120x120.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/public/img/icons/apple-touch-icon-152x152.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
frontend/public/img/icons/apple-touch-icon-180x180.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
frontend/public/img/icons/apple-touch-icon-60x60.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/public/img/icons/apple-touch-icon-76x76.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
frontend/public/img/icons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
frontend/public/img/icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
frontend/public/img/icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/public/img/icons/msapplication-icon-144x144.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
frontend/public/img/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 10 KiB |
3
frontend/public/img/icons/safari-pinned-tab.svg
Normal 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 |
@ -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>
|
82
frontend/public/manifest.json
Normal 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"
|
||||
}
|
2
frontend/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
@ -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>
|
||||
|
@ -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);
|
||||
|
39
frontend/src/registerServiceWorker.js
Normal 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
@ -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, {});
|
@ -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",
|
||||
},
|
||||
},
|
||||
};
|
||||
|