Added PWA and service-worker
18
main.go
|
@ -240,5 +240,23 @@ func initRouter(config models.WrapperrConfig) *gin.Engine {
|
|||
c.Data(http.StatusOK, "text/plain", TXTfile)
|
||||
})
|
||||
|
||||
// Static endpoint for service-worker
|
||||
router.GET("/service-worker.js", func(c *gin.Context) {
|
||||
JSfile, err := os.ReadFile("./web/js/service-worker.js")
|
||||
if err != nil {
|
||||
fmt.Println("Reading service-worker threw error trying to open the file. Error: " + err.Error())
|
||||
}
|
||||
c.Data(http.StatusOK, "text/javascript", JSfile)
|
||||
})
|
||||
|
||||
// Static endpoint for manifest
|
||||
router.GET("/manifest.json", func(c *gin.Context) {
|
||||
JSONfile, err := os.ReadFile("./web/json/manifest.json")
|
||||
if err != nil {
|
||||
fmt.Println("Reading manifest threw error trying to open the file. Error: " + err.Error())
|
||||
}
|
||||
c.Data(http.StatusOK, "text/json", JSONfile)
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
BIN
web/assets/logos/logo-1024x1024.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
web/assets/logos/logo-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
web/assets/logos/logo-512x512.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
web/assets/logos/logo-72x72.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
web/assets/logos/logo-mono-512x512.png
Normal file
After Width: | Height: | Size: 35 KiB |
31
web/assets/logos/logo-mono.svg
Normal file
After Width: | Height: | Size: 82 KiB |
42
web/assets/logos/logo.svg
Normal file
|
@ -0,0 +1,42 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F2F2F2;}
|
||||
.st1{fill:#FFBD55;}
|
||||
.st2{fill:#DEDEDE;}
|
||||
.st3{fill:#F2AB44;}
|
||||
.st4{fill:#D3D3D3;}
|
||||
.st5{fill:#FFC878;}
|
||||
.st6{fill:#FFFFFF;}
|
||||
.st7{fill:#D4D4D4;}
|
||||
</style>
|
||||
<path class="st0" d="M278.6,308.5c-96.3,0-226.9,2.4-222.3,40.4s74,42.7,56,66c-18,23.3,28,59.3,114,55.3s151.3-14.3,154-47.7 c1.7-20.9,27.9-31.6,45.3-42.9c24.7-16,30-48-12-60S278.6,308.5,278.6,308.5z"/>
|
||||
<g id="BACKGROUND">
|
||||
</g>
|
||||
<g id="OBJECTS">
|
||||
<polygon class="st1" points="401.9,359 222.8,422.5 222.8,258 401.9,194.5 "/>
|
||||
<polygon class="st0" points="328.9,384.8 295.8,396.6 295.8,232.1 328.9,220.4 "/>
|
||||
<polygon class="st2" points="401.9,194.5 401.9,217.7 222.7,281.3 222.7,258 "/>
|
||||
<polygon class="st1" points="432,183.8 222.8,258 222.8,180.2 432,106 "/>
|
||||
<polygon class="st0" points="346.7,214.1 308,227.8 308,149.9 346.7,136.2 "/>
|
||||
<polygon class="st3" points="106.2,359 222.8,422.5 222.8,258 106.2,194.5 "/>
|
||||
<polygon class="st4" points="153.7,384.8 175.2,396.6 175.2,232.1 153.7,220.4 "/>
|
||||
<polygon class="st2" points="222.7,258 222.7,281.7 106.2,218.2 106.2,194.5 "/>
|
||||
<polygon class="st3" points="86.5,183.8 222.8,258 222.8,180.2 86.5,106 "/>
|
||||
<polygon class="st4" points="142.1,214.1 167.2,227.8 167.2,149.9 142.1,136.2 "/>
|
||||
<polygon class="st5" points="432,106 346.7,136.2 308,149.9 222.8,180.2 167.3,149.9 142,136.2 86.5,106 175.9,78.6 175.9,78.6 210.5,68 261.3,52.5 314.7,69.2 350.5,80.4 "/>
|
||||
<polygon class="st6" points="346.7,136.2 308,149.9 249.9,118.6 223.8,104.5 175.9,78.6 210.5,68 257.3,91.5 257.3,91.5 285,105.3 "/>
|
||||
<polygon class="st6" points="350.5,80.4 285,105.3 249.9,118.6 167.3,149.9 142,136.2 223.8,104.5 257.3,91.5 314.7,69.2 "/>
|
||||
<path class="st7" d="M246.3,110.2c0,0,8.7-79.8,47.4-69.8s0,49.7,0,49.7s6.8-31-6.8-32.4C273.2,56.4,253.1,84.6,246.3,110.2z"/>
|
||||
<path class="st7" d="M323.4,117l-23.3,16.4c0,0-34.7-6.8-53.8-23.3l21.4-9.9C267.7,100.3,296.4,115.8,323.4,117z"/>
|
||||
<path class="st0" d="M246.3,110.2c0,0,84.8-54.7,100.4-26.5s2.7,38.3-6.4,41.5c-9.1,3.2-40.2,8.2-40.2,8.2s14.5-31.4-1.7-39 c-3.6-1.7-7.9-1.5-11.7,0L246.3,110.2z"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M169.2,117l23.3,16.4c0,0,34.7-6.8,53.8-23.3l-21.4-9.9C224.8,100.3,196.1,115.8,169.2,117z"/>
|
||||
</g>
|
||||
<path class="st0" d="M246.3,110.2c0,0-8.7-79.8-47.4-69.8s0,49.7,0,49.7s-6.8-31,6.8-32.4C219.4,56.4,239.4,84.6,246.3,110.2z"/>
|
||||
</g>
|
||||
<path class="st7" d="M246.3,110.2c0,0-84.8-54.7-100.4-26.5c-15.5,28.3-2.7,38.3,6.4,41.5c9.1,3.2,40.2,8.2,40.2,8.2 s-14.5-31.4,1.7-39c3.6-1.7,7.9-1.5,11.7,0L246.3,110.2z"/>
|
||||
</g>
|
||||
<g id="DESIGNED_BY_FREEPIK">
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
|
@ -11,7 +11,9 @@
|
|||
<link rel="stylesheet" type="text/css" href="./assets/css/wrapped.css" />
|
||||
<link rel="shortcut icon" href="./assets/img/favicons/favicon.ico" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="./js/service-worker.js"></script>
|
||||
<script src="./js/functions.js"></script>
|
||||
<script src="./js/index.js"></script>
|
||||
<script src="./js/get_stats.js"></script>
|
||||
|
|
175
web/js/service-worker.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
console.log("Service-worker loaded.");
|
||||
|
||||
// Incrementing OFFLINE_VERSION will kick off the install event and force
|
||||
// previously cached resources to be updated from the network.
|
||||
const OFFLINE_VERSION = 1;
|
||||
const CACHE_NAME = 'wrapperr-cache';
|
||||
// Customize this with a different URL if needed.
|
||||
const urlsToCache = [
|
||||
'/',
|
||||
'./manifest.json',
|
||||
'./assets/favicons/favicon.ico'
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil((async () => {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
// Setting {cache: 'reload'} in the new request will ensure that the response
|
||||
// isn't fulfilled from the HTTP cache; i.e., it will be from the network.
|
||||
for(var i = 0; i < urlsToCache.length; i++) {
|
||||
await cache.add(new Request(urlsToCache[i], {cache: 'reload'}));
|
||||
}
|
||||
})());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil((async () => {
|
||||
// Enable navigation preload if it's supported.
|
||||
// See https://developers.google.com/web/updates/2017/02/navigation-preload
|
||||
if ('navigationPreload' in self.registration) {
|
||||
await self.registration.navigationPreload.enable();
|
||||
}
|
||||
})());
|
||||
|
||||
// Tell the active service worker to take control of the page immediately.
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// We only want to call event.respondWith() if this is a navigation request
|
||||
// for an HTML page.
|
||||
if (event.request.mode === 'navigate') {
|
||||
event.respondWith((async () => {
|
||||
try {
|
||||
// First, try to use the navigation preload response if it's supported.
|
||||
const preloadResponse = await event.preloadResponse;
|
||||
if (preloadResponse) {
|
||||
return preloadResponse;
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(event.request);
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
// catch is only triggered if an exception is thrown, which is likely
|
||||
// due to a network error.
|
||||
// If fetch() returns a valid HTTP response with a response code in
|
||||
// the 4xx or 5xx range, the catch() will NOT be called.
|
||||
console.log('Fetch failed; returning offline page instead.', error);
|
||||
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedResponse = await cache.match(OFFLINE_URL);
|
||||
return cachedResponse;
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
// If our if() condition is false, then this fetch handler won't intercept the
|
||||
// request. If there are any other fetch handlers registered, they will get a
|
||||
// chance to call event.respondWith(). If no fetch handlers call
|
||||
// event.respondWith(), the request will be handled by the browser as if there
|
||||
// were no service worker involvement.
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclose', event => {
|
||||
|
||||
try {
|
||||
|
||||
const notification = event.notification;
|
||||
const primaryKey = notification.data.primaryKey;
|
||||
|
||||
console.log('Closed notification: ' + primaryKey);
|
||||
|
||||
} catch(e) {
|
||||
console.log("Failed to click notfication. Error: " + e)
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', event => {
|
||||
|
||||
try {
|
||||
|
||||
const notification = event.notification;
|
||||
const primaryKey = notification.data.primaryKey;
|
||||
const url = notification.data.url;
|
||||
const action = event.action;
|
||||
|
||||
if (action === 'close') {
|
||||
notification.close();
|
||||
} else {
|
||||
event.waitUntil(clients.openWindow(self.location.origin + url));
|
||||
notification.close();
|
||||
}
|
||||
|
||||
console.log('Clicked notification: ' + primaryKey);
|
||||
|
||||
} catch(e) {
|
||||
console.log("Failed to click notfication. Error: " + e)
|
||||
}
|
||||
|
||||
// TODO 5.3 - close all notifications when one is clicked
|
||||
|
||||
});
|
||||
|
||||
self.addEventListener('push', function(event) {
|
||||
|
||||
if (!(self.Notification && self.Notification.permission === "granted")) {
|
||||
console.log("Notification permission not given.")
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Pushing notification.")
|
||||
|
||||
try {
|
||||
|
||||
let jsonData = event.data?.json() ?? {
|
||||
category: "general",
|
||||
title: "Error",
|
||||
body: "An error occured"
|
||||
};
|
||||
|
||||
console.log("JSON: " + JSON.stringify(jsonData));
|
||||
|
||||
let url;
|
||||
let action;
|
||||
|
||||
if(jsonData.category == "achievement") {
|
||||
url = "/achievements"
|
||||
action = "Check out"
|
||||
} else if(jsonData.category == "news") {
|
||||
url = "/news"
|
||||
action = "Read"
|
||||
} else {
|
||||
url = "/"
|
||||
action = "Visit"
|
||||
}
|
||||
|
||||
const options = {
|
||||
body: jsonData.body,
|
||||
icon: '/assets/logos/logo-384x384.png',
|
||||
badge: '/assets/logos/logo-mono-96x96.png',
|
||||
vibrate: [100, 50, 100],
|
||||
data: {
|
||||
dateOfArrival: Date.now(),
|
||||
primaryKey: 1,
|
||||
url: url
|
||||
},
|
||||
actions: [
|
||||
{action: 'explore', title: action,
|
||||
icon: '/assets/check.svg'
|
||||
},
|
||||
{action: 'close', title: 'Close',
|
||||
icon: '/assets/x.svg'
|
||||
},
|
||||
],
|
||||
tag: 'Message'
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(jsonData.title, options)
|
||||
);
|
||||
|
||||
} catch(e) {
|
||||
console.log("Failed to push notfication. Error: " + e)
|
||||
}
|
||||
|
||||
});
|
64
web/json/manifest.json
Normal file
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"lang": "en",
|
||||
"dir": "ltr",
|
||||
"name": "Wrapperr",
|
||||
"short_name": "Wrapperr",
|
||||
"description": "nicely wrapped Plex statistics.",
|
||||
"theme_color": "#F9C74F",
|
||||
"background_color": "#F9844A",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/logos/logo.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/logos/logo-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/logos/logo-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/logos/logo-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/logos/logo-1024x1024.png",
|
||||
"sizes": "1024x1024",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable any"
|
||||
},
|
||||
{
|
||||
"src": "/assets/logos/logo-mono-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any monochrome"
|
||||
},
|
||||
{
|
||||
"src": "/assets/logos/logo-mono.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any monochrome"
|
||||
}
|
||||
],
|
||||
"splash_pages": null,
|
||||
"id": "7b46f4ad-dd2a-4446-80e7-a2c2765eedbc",
|
||||
"categories": [
|
||||
"games",
|
||||
"lifestyle"
|
||||
]
|
||||
}
|