Add shortcuts
This commit is contained in:
parent
3cf461c167
commit
7823cee633
7 changed files with 151 additions and 46 deletions
|
@ -7,7 +7,7 @@
|
|||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="application-name" content="Twigs">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" href="/static/icons/icon-192x192.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
<script type="text/javascript" src="/static/js/index.js"></script>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"name": "Pi-helper",
|
||||
"description": "A PWA to facillitate enabling/disabling a Pi-hole",
|
||||
"orientation": "any",
|
||||
"short_name": "Pi-helper",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000",
|
||||
|
@ -95,5 +97,55 @@
|
|||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Enable",
|
||||
"description": "Enable the Pi-hole",
|
||||
"url": "/?enable",
|
||||
"icons": [
|
||||
{
|
||||
"src": "static/icons/shortcut-play.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Disable for 30 seconds",
|
||||
"description": "Enable the Pi-hole",
|
||||
"url": "/?disable=30",
|
||||
"icons": [
|
||||
{
|
||||
"src": "static/icons/shortcut-pause.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Disable for 1 minute",
|
||||
"description": "Enable the Pi-hole",
|
||||
"url": "/?disable=60",
|
||||
"icons": [
|
||||
{
|
||||
"src": "static/icons/shortcut-pause.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Disable for 5 minutes",
|
||||
"description": "Enable the Pi-hole",
|
||||
"url": "/?disable=300",
|
||||
"icons": [
|
||||
{
|
||||
"src": "static/icons/shortcut-pause.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"prebuild": "copyfiles -Ve src/** index.html manifest.webmanifest sw.js **/*.css **/*.png ../dist/public",
|
||||
"prebuild": "copyfiles -Ve src/** index.html manifest.json sw.js **/*.css **/*.png ../dist/public",
|
||||
"build": "tsc --project ./",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
|
|
|
@ -10,17 +10,26 @@ if ('serviceWorker' in navigator) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
function monitorChanges() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
socket = new WebSocket(`${protocol}://${location.host}/`)
|
||||
socket.onopen = (e) => {
|
||||
console.log('socket opened', e)
|
||||
if (window.location.search === '?enable') {
|
||||
enable();
|
||||
} else if (window.location.search.startsWith('?disable')) {
|
||||
const duration = window.location.search.split('=')[1];
|
||||
disable(Number.parseInt(duration));
|
||||
}
|
||||
window.history.replaceState(null, '', window.location.pathname);
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
socket.onmessage = (e) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout!);
|
||||
}
|
||||
const data = JSON.parse(e.data)
|
||||
console.log('message received', data)
|
||||
switch (data.status) {
|
||||
case 'enabled':
|
||||
if (durationInterval) {
|
||||
|
@ -28,12 +37,18 @@ function monitorChanges() {
|
|||
}
|
||||
animateLogo(false)
|
||||
showDisable(false)
|
||||
showEnable(true)
|
||||
timeout = setTimeout(() => {
|
||||
showEnable(true);
|
||||
timeout = null;
|
||||
}, 500)
|
||||
break
|
||||
case 'disabled':
|
||||
animateLogo(false)
|
||||
showEnable(false, false)
|
||||
showDisable(true, data.until)
|
||||
timeout = setTimeout(() => {
|
||||
showDisable(true, data.until)
|
||||
timeout = null;
|
||||
}, 500)
|
||||
break
|
||||
default:
|
||||
console.error('Unhandled status', data)
|
||||
|
@ -80,7 +95,6 @@ function setUnit(unit: string) {
|
|||
}
|
||||
|
||||
function disableCustom() {
|
||||
|
||||
showEnable(false, false)
|
||||
}
|
||||
|
||||
|
@ -97,33 +111,23 @@ function showEnable(show: boolean, showCustom?: boolean) {
|
|||
const enableDiv = document.getElementById('enabled') as HTMLElement
|
||||
const disableCustom = document.getElementById('disable-custom') as HTMLElement
|
||||
if (show) {
|
||||
if (disableCustom.style.opacity === '1') {
|
||||
disableCustom.style.opacity = '0'
|
||||
if (disableCustom.classList.contains('visible')) {
|
||||
disableCustom.classList.replace('visible', 'hidden');
|
||||
setTimeout(() => {
|
||||
disableCustom.style.maxHeight = '0'
|
||||
enableDiv.style.maxHeight = '100vh'
|
||||
}, 250)
|
||||
setTimeout(() => {
|
||||
enableDiv.style.opacity = '1'
|
||||
enableDiv.classList.add('visible')
|
||||
enableDiv.classList.remove('hidden')
|
||||
}, 500)
|
||||
} else {
|
||||
enableDiv.style.maxHeight = '100vh'
|
||||
setTimeout(() => {
|
||||
enableDiv.style.opacity = '1'
|
||||
}, 250)
|
||||
enableDiv.classList.add('visible')
|
||||
enableDiv.classList.remove('hidden')
|
||||
}
|
||||
} else {
|
||||
enableDiv.style.opacity = '0'
|
||||
setTimeout(() => {
|
||||
enableDiv.style.maxHeight = '0'
|
||||
}, 250)
|
||||
enableDiv.classList.replace('visible', 'hidden')
|
||||
if (showCustom) {
|
||||
setTimeout(() => {
|
||||
disableCustom.style.maxHeight = '100vh'
|
||||
disableCustom.classList.add('visible')
|
||||
disableCustom.classList.remove('hidden')
|
||||
}, 250)
|
||||
setTimeout(() => {
|
||||
disableCustom.style.opacity = '1'
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,17 +137,11 @@ let durationInterval: any;
|
|||
function showDisable(show: boolean, timestamp?: number) {
|
||||
const disableDiv = document.getElementById('disabled') as HTMLElement
|
||||
if (show) {
|
||||
disableDiv.style.maxHeight = '100vh'
|
||||
disableDiv.classList.add('visible');
|
||||
disableDiv.classList.remove('hidden');
|
||||
} else {
|
||||
disableDiv.style.opacity = '0'
|
||||
disableDiv.classList.replace('visible', 'hidden');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (show) {
|
||||
disableDiv.style.opacity = '1'
|
||||
} else {
|
||||
disableDiv.style.maxHeight = '0'
|
||||
}
|
||||
}, 250)
|
||||
const duration = document.getElementById('duration') as HTMLElement
|
||||
if (!timestamp) {
|
||||
duration.innerText = ''
|
||||
|
@ -163,7 +161,6 @@ function showDisable(show: boolean, timestamp?: number) {
|
|||
return
|
||||
}
|
||||
let seconds = Math.ceil(difference / 1000)
|
||||
console.log(`${until.getTime()} - ${now.getTime()} = ${seconds} seconds`)
|
||||
let hours = 0
|
||||
let minutes = 0
|
||||
let durationText: string = '';
|
||||
|
@ -171,12 +168,10 @@ function showDisable(show: boolean, timestamp?: number) {
|
|||
hours = Math.floor(seconds / 3600)
|
||||
seconds -= hours * 3600
|
||||
}
|
||||
console.log(`hours: ${hours} seconds: ${seconds}`)
|
||||
if (seconds >= 60) {
|
||||
minutes = Math.floor(seconds / 60)
|
||||
seconds -= minutes * 60
|
||||
}
|
||||
console.log(`minutes: ${minutes} seconds: ${seconds}`)
|
||||
if (hours > 0) {
|
||||
durationText += `${hours.toString().padStart(2, '0')}:`
|
||||
}
|
||||
|
|
|
@ -38,7 +38,8 @@
|
|||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
|
@ -49,7 +50,8 @@ html, body {
|
|||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
body, div {
|
||||
body,
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
|
@ -62,7 +64,8 @@ body {
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
div, div > * {
|
||||
div,
|
||||
div>* {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -82,12 +85,14 @@ button {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
button, p {
|
||||
button,
|
||||
p {
|
||||
margin: 5px 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#enabled, #disabled, #disable-custom {
|
||||
.enabled,
|
||||
.disabled {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
|
@ -131,8 +136,13 @@ button, p {
|
|||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg) }
|
||||
to { transform: rotate(360deg) }
|
||||
from {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
|
||||
.custom * {
|
||||
|
@ -162,3 +172,51 @@ input {
|
|||
margin: 5px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.visible {
|
||||
animation-name: show;
|
||||
animation-fill-mode: both;
|
||||
animation-duration: 500ms;
|
||||
max-height: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
animation-name: hide;
|
||||
animation-fill-mode: forwards;
|
||||
animation-duration: 500ms;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes show {
|
||||
0% {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
max-height: 100vh;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hide {
|
||||
0% {
|
||||
max-height: 100vh;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
max-height: 100vh;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
max-height: 0;
|
||||
}
|
||||
}
|
BIN
client/static/icons/shortcut-pause.png
Normal file
BIN
client/static/icons/shortcut-pause.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
client/static/icons/shortcut-play.png
Normal file
BIN
client/static/icons/shortcut-play.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Loading…
Reference in a new issue