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="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="application-name" content="Twigs">
|
<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="apple-touch-icon" href="/static/icons/icon-192x192.png">
|
||||||
<link rel="stylesheet" href="/static/css/style.css" />
|
<link rel="stylesheet" href="/static/css/style.css" />
|
||||||
<script type="text/javascript" src="/static/js/index.js"></script>
|
<script type="text/javascript" src="/static/js/index.js"></script>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "Pi-helper",
|
"name": "Pi-helper",
|
||||||
|
"description": "A PWA to facillitate enabling/disabling a Pi-hole",
|
||||||
|
"orientation": "any",
|
||||||
"short_name": "Pi-helper",
|
"short_name": "Pi-helper",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
"background_color": "#000000",
|
"background_color": "#000000",
|
||||||
|
@ -95,5 +97,55 @@
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"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": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"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 ./",
|
"build": "tsc --project ./",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,17 +10,26 @@ if ('serviceWorker' in navigator) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function monitorChanges() {
|
function monitorChanges() {
|
||||||
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
socket = new WebSocket(`${protocol}://${location.host}/`)
|
socket = new WebSocket(`${protocol}://${location.host}/`)
|
||||||
socket.onopen = (e) => {
|
socket.onopen = (e) => {
|
||||||
console.log('socket opened', 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) => {
|
socket.onmessage = (e) => {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout!);
|
||||||
|
}
|
||||||
const data = JSON.parse(e.data)
|
const data = JSON.parse(e.data)
|
||||||
console.log('message received', data)
|
|
||||||
switch (data.status) {
|
switch (data.status) {
|
||||||
case 'enabled':
|
case 'enabled':
|
||||||
if (durationInterval) {
|
if (durationInterval) {
|
||||||
|
@ -28,12 +37,18 @@ function monitorChanges() {
|
||||||
}
|
}
|
||||||
animateLogo(false)
|
animateLogo(false)
|
||||||
showDisable(false)
|
showDisable(false)
|
||||||
showEnable(true)
|
timeout = setTimeout(() => {
|
||||||
|
showEnable(true);
|
||||||
|
timeout = null;
|
||||||
|
}, 500)
|
||||||
break
|
break
|
||||||
case 'disabled':
|
case 'disabled':
|
||||||
animateLogo(false)
|
animateLogo(false)
|
||||||
showEnable(false, false)
|
showEnable(false, false)
|
||||||
showDisable(true, data.until)
|
timeout = setTimeout(() => {
|
||||||
|
showDisable(true, data.until)
|
||||||
|
timeout = null;
|
||||||
|
}, 500)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
console.error('Unhandled status', data)
|
console.error('Unhandled status', data)
|
||||||
|
@ -80,7 +95,6 @@ function setUnit(unit: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableCustom() {
|
function disableCustom() {
|
||||||
|
|
||||||
showEnable(false, false)
|
showEnable(false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,33 +111,23 @@ function showEnable(show: boolean, showCustom?: boolean) {
|
||||||
const enableDiv = document.getElementById('enabled') as HTMLElement
|
const enableDiv = document.getElementById('enabled') as HTMLElement
|
||||||
const disableCustom = document.getElementById('disable-custom') as HTMLElement
|
const disableCustom = document.getElementById('disable-custom') as HTMLElement
|
||||||
if (show) {
|
if (show) {
|
||||||
if (disableCustom.style.opacity === '1') {
|
if (disableCustom.classList.contains('visible')) {
|
||||||
disableCustom.style.opacity = '0'
|
disableCustom.classList.replace('visible', 'hidden');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
disableCustom.style.maxHeight = '0'
|
enableDiv.classList.add('visible')
|
||||||
enableDiv.style.maxHeight = '100vh'
|
enableDiv.classList.remove('hidden')
|
||||||
}, 250)
|
|
||||||
setTimeout(() => {
|
|
||||||
enableDiv.style.opacity = '1'
|
|
||||||
}, 500)
|
}, 500)
|
||||||
} else {
|
} else {
|
||||||
enableDiv.style.maxHeight = '100vh'
|
enableDiv.classList.add('visible')
|
||||||
setTimeout(() => {
|
enableDiv.classList.remove('hidden')
|
||||||
enableDiv.style.opacity = '1'
|
|
||||||
}, 250)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enableDiv.style.opacity = '0'
|
enableDiv.classList.replace('visible', 'hidden')
|
||||||
setTimeout(() => {
|
|
||||||
enableDiv.style.maxHeight = '0'
|
|
||||||
}, 250)
|
|
||||||
if (showCustom) {
|
if (showCustom) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
disableCustom.style.maxHeight = '100vh'
|
disableCustom.classList.add('visible')
|
||||||
|
disableCustom.classList.remove('hidden')
|
||||||
}, 250)
|
}, 250)
|
||||||
setTimeout(() => {
|
|
||||||
disableCustom.style.opacity = '1'
|
|
||||||
}, 500)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,17 +137,11 @@ let durationInterval: any;
|
||||||
function showDisable(show: boolean, timestamp?: number) {
|
function showDisable(show: boolean, timestamp?: number) {
|
||||||
const disableDiv = document.getElementById('disabled') as HTMLElement
|
const disableDiv = document.getElementById('disabled') as HTMLElement
|
||||||
if (show) {
|
if (show) {
|
||||||
disableDiv.style.maxHeight = '100vh'
|
disableDiv.classList.add('visible');
|
||||||
|
disableDiv.classList.remove('hidden');
|
||||||
} else {
|
} 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
|
const duration = document.getElementById('duration') as HTMLElement
|
||||||
if (!timestamp) {
|
if (!timestamp) {
|
||||||
duration.innerText = ''
|
duration.innerText = ''
|
||||||
|
@ -163,7 +161,6 @@ function showDisable(show: boolean, timestamp?: number) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let seconds = Math.ceil(difference / 1000)
|
let seconds = Math.ceil(difference / 1000)
|
||||||
console.log(`${until.getTime()} - ${now.getTime()} = ${seconds} seconds`)
|
|
||||||
let hours = 0
|
let hours = 0
|
||||||
let minutes = 0
|
let minutes = 0
|
||||||
let durationText: string = '';
|
let durationText: string = '';
|
||||||
|
@ -171,12 +168,10 @@ function showDisable(show: boolean, timestamp?: number) {
|
||||||
hours = Math.floor(seconds / 3600)
|
hours = Math.floor(seconds / 3600)
|
||||||
seconds -= hours * 3600
|
seconds -= hours * 3600
|
||||||
}
|
}
|
||||||
console.log(`hours: ${hours} seconds: ${seconds}`)
|
|
||||||
if (seconds >= 60) {
|
if (seconds >= 60) {
|
||||||
minutes = Math.floor(seconds / 60)
|
minutes = Math.floor(seconds / 60)
|
||||||
seconds -= minutes * 60
|
seconds -= minutes * 60
|
||||||
}
|
}
|
||||||
console.log(`minutes: ${minutes} seconds: ${seconds}`)
|
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
durationText += `${hours.toString().padStart(2, '0')}:`
|
durationText += `${hours.toString().padStart(2, '0')}:`
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,8 @@
|
||||||
transition: all 0.25s ease;
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
|
body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -49,7 +50,8 @@ html, body {
|
||||||
color: var(--color-foreground);
|
color: var(--color-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
body, div {
|
body,
|
||||||
|
div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
@ -62,7 +64,8 @@ body {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div, div > * {
|
div,
|
||||||
|
div>* {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,12 +85,14 @@ button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, p {
|
button,
|
||||||
|
p {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#enabled, #disabled, #disable-custom {
|
.enabled,
|
||||||
|
.disabled {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -131,8 +136,13 @@ button, p {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from { transform: rotate(0deg) }
|
from {
|
||||||
to { transform: rotate(360deg) }
|
transform: rotate(0deg)
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom * {
|
.custom * {
|
||||||
|
@ -162,3 +172,51 @@ input {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
padding: 10px;
|
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