add dark mode and notifications

This commit is contained in:
Maxi Quoß
2022-02-26 22:04:35 +01:00
parent bc6539e9e7
commit 4e0912c283
7 changed files with 278 additions and 98 deletions

View File

@@ -2,11 +2,18 @@
import { onMount } from 'svelte';
import store from './store.js';
import Navbar from "./components/Navbar.svelte";
import DeviceCard from "./components/DeviceCard.svelte"
import DeviceCard from "./components/DeviceCard.svelte";
import Toast from "./components/Toast.svelte";
let visitors = 0;
let devices = [];
let settings = {}
let toast = {
title: "",
message: "",
color: "",
show: false
}
onMount(() => {
store.subscribe(currentMessage => {
@@ -32,9 +39,9 @@
// set device status
if (currentMessage.message.up == true) {
setUp(currentMessage.message.id);
setUp(currentMessage.message);
} else {
setDown(currentMessage.message.id);
setDown(currentMessage.message);
}
} else if (currentMessage.type == "wake") {
// set device waking
@@ -71,10 +78,13 @@
})
})
function setUp(id) {
const dot = document.querySelector(`#dot-${id}`)
const spinner = document.querySelector(`#spinner-${id}`);
function setUp(device) {
const dot = document.querySelector(`#dot-${device.id}`)
const spinner = document.querySelector(`#spinner-${device.id}`);
if (dot) {
if (dot.classList.contains("danger")) {
showToast(device.name, "Device is up!", "success")
}
dot.style.animation = "none";
dot.offsetWidth;
if (!spinner.classList.contains("d-none")) {
@@ -89,10 +99,13 @@
}
}
function setDown(id) {
const dot = document.querySelector(`#dot-${id}`)
const spinner = document.querySelector(`#spinner-${id}`);
function setDown(device) {
const dot = document.querySelector(`#dot-${device.id}`)
const spinner = document.querySelector(`#spinner-${device.id}`);
if (dot) {
if (dot.classList.contains("success")) {
showToast(device.name, "Device is down!", "danger")
}
dot.style.animation = "none";
dot.offsetWidth;
if (!spinner.classList.contains("d-none")) {
@@ -114,6 +127,19 @@
spinner.classList.remove("d-none");
}
function showToast(title, message, color) {
if (settings.notifications === false) {
return
}
toast.title = title;
toast.message = message;
toast.color = color;
toast.show = true;
setTimeout(() => {
toast.show = false;
}, 4000)
}
function compare(a, b) {
if (a.name < b.name){
return -1;
@@ -135,14 +161,86 @@
{/each}
</div>
</div>
<Toast toast={toast} />
</main>
<style lang="scss" global>
@import "variables.scss";
@import "./variables.scss";
@import "../node_modules/bootstrap/scss/bootstrap";
@import "../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "../node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
@import "../node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
@import "theme.scss";
body {
color: var(--color-text);
background-color: var(--color-bg);
}
.modal-content {
background-color: var(--bg-lighter);
}
.modal-header {
border-bottom: 1px solid var(--color-text);
.btn-close {
background: var(--svg-close);
}
}
.modal-footer {
border-top: 1px solid var(--color-text);
}
.btn, button {
&.btn-light {
color: var(--color-text);
background-color: var(--bg-lighter);
border-width: 0px;
}
&.btn-outline-success {
border-color: $success;
&:hover {
background-color: $success;
}
}
&.btn-outline-danger {
border-color: $danger;
&:hover {
background-color: $danger;
}
}
}
.form-control{
&:focus {
border-color: inherit;
box-shadow: none;
}
}
.callout {
padding: 1rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
border-left-width: 0.25rem;
border-radius: 0.25rem;
&.callout-info {
background-color: var(--info-dark-transparent);
border: 1px solid var(--info-dark-transparent);
border-left: 5px solid var(--info);
}
&.callout-danger {
background-color: var(--danger-dark-transparent);
border: 1px solid var(--danger-dark-transparent);
border-left: 5px solid var(--danger);
}
}
@keyframes on-pulse {
0% {

View File

@@ -0,0 +1,48 @@
<script>
const preferesDark = window.matchMedia("(prefers-color-scheme: dark)");
preferesDark.addEventListener("change", e => {
setTheme(e.matches ? "dark" : "light");
})
if (localStorage.getItem("data-theme") === null) {
setTheme(preferesDark.matches ? "dark" : "light");
} else {
setTheme(localStorage.getItem("data-theme"));
}
function setTheme(color) {
if (color == "system") {
document.documentElement.setAttribute("data-theme", preferesDark ? "dark" : "light");
localStorage.setItem("data-theme", preferesDark ? "dark" : "light");
} else {
document.documentElement.setAttribute("data-theme", color);
localStorage.setItem("data-theme", color);
}
}
</script>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle px-3 me-2 py-2" type="button" id="darkModeButton" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-palette me-2"></i>Theme
</button>
<ul class="dropdown-menu" aria-labelledby="darkModeButton">
<li>
<button class="dropdown-item" on:click={() => setTheme("dark")}>
<i class="fa-solid fa-moon me-2"></i>Dark
</button>
</li>
<li>
<button class="dropdown-item" on:click={() => setTheme("light")}>
<i class="fa-solid fa-sun me-2"></i>Light
</button>
</li>
<li>
<button class="dropdown-item" on:click={() => setTheme("system")}>
<i class="fa-solid fa-desktop me-2"></i>System
</button>
</li>
</ul>
</div>

View File

@@ -69,11 +69,11 @@
</script>
<div id="device-col-{device.id}" class="col-xs-12 col-sm-6 col-md-4 col-lg-3 g-4">
<div class="card border-0 p-3 pt-2" >
<div class="card border-0 p-3 pt-2">
<div class="card-body">
<div class="row">
<div class="col-auto me-auto">
<div id="spinner-{device.id}" class="spinner-border color-warning d-none" role="status"></div>
<div id="spinner-{device.id}" class="spinner-border warning d-none" role="status"></div>
<div class="hover" on:click="{() => wake(device.id)}" role="button">
{#if device.up === true || device.up === false}
<i id="dot-{device.id}" class="fa-solid fa-power-off fa-2x {device.up ? 'success' : 'danger'}"></i>
@@ -157,7 +157,7 @@
<div class="input-group mb-2">
<input type="text" id="{device.id}-custom-port" class="form-control" placeholder="Name" aria-label="Name" aria-describedby="button-addon2" bind:value={customPort.name}>
<input type="number" min="1" max="65535" class="form-control" placeholder="Port" aria-label="Port" aria-describedby="button-addon2" bind:value={customPort.number} on:input={validatePort}>
<button class="btn btn-outline-secondary" type="button" id="button-addon2" on:click="{updatePort}">Update Port</button>
<button class="btn btn-secondary" type="button" id="button-addon2" on:click="{updatePort}">Update Port</button>
</div>
<div class="callout callout-info">
<p class="mb-0">Ports must be between 1 and 65535. Enter the same port with a differen name to change it. Leave name empty to delete port.</p>
@@ -187,6 +187,7 @@
</pre>
<p class="mb-0">Read more about <a href="https://linux.die.net/man/5/crontab" target="_blank">valid syntax here</a> or <a href="https://crontab.guru/" target="_blank">generate</a> it. Expressions starting with "@..." are not supported.</p>
</div>
<!-- TODO: add shutdown command -->
</form>
</div>
<div class="modal-footer justify-content-between">
@@ -199,42 +200,28 @@
</div>
<style lang="scss">
@import "../variables.scss";
button {
&.btn-outline-success {
border-color: $success;
&:hover {
background-color: $success;
}
}
&.btn-outline-danger {
border-color: $danger;
&:hover {
background-color: $danger;
}
}
.success {
color: var(--success);
}
i {
&.success {
color: $success;
}
.warning {
color: var(--warning);
}
&.danger {
color: $danger;
}
.danger {
color: var(--danger);
}
pre {
white-space: pre-line;
background-color: $light;
background-color: var(--color-bg);
padding: 1em;
border-radius: 1em;
}
.card {
border-radius: 2em;
background-color: var(--bg-lighter);
}
.spinner-border {
@@ -259,23 +246,9 @@
.fa-circle {
font-size: 0.8em;
color: $success;
}
.color-warning {
color: $warning;
}
.callout {
padding: 1rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
border: 1px solid #e9ecef;
border-left-width: 0.25rem;
border-radius: 0.25rem;
&.callout-info {
border-left-color: $info;
}
.list-group-item {
color: var(--color-text);
}
</style>

View File

@@ -1,5 +1,6 @@
<script>
import store from '../store.js';
import DarkToggle from "./DarkToggle.svelte";
export let visitors;
export let settings;
@@ -74,20 +75,21 @@
</script>
<nav class="navbar navbar-expand-sm navbar-light bg-light">
<nav class="navbar navbar-expand-sm">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<div class="navbar-brand" href="/">
<img src="/favicon.png" alt="Logo" width="24" height="24" class="me-2">
UpSnap
</a>
</div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<span class="ms-auto d-flex">
<DarkToggle />
<div class="dropdown">
<button class="btn btn-light dropdown-toggle px-3 me-2 py-2 " type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
More
<button class="btn btn-light dropdown-toggle px-3 me-2 py-2" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-wrench me-2"></i>More
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li>
@@ -102,10 +104,8 @@
</li>
</ul>
</div>
<div class="card border-0">
<div class="card-body py-2 px-3">
{visitors} {visitors === 1 ? "Visitor" : "Visitors"}<i class="ms-1 fa-solid {visitors === 1 ? "fa-user" : "fa-user-group"} text-muted"></i>
</div>
<div class="btn btn-light px-3 me-2 py-2 pe-none">
<i class="me-2 fa-solid {visitors === 1 ? "fa-user" : "fa-user-group"}"></i>{visitors} {visitors === 1 ? "Visitor" : "Visitors"}
</div>
</span>
</div>
@@ -157,7 +157,7 @@
<h5 class="fw-bold">Network discovery</h5>
{#if !settings.discovery}
<div class="callout callout-danger">
<p class="my-0">To enable this option, please enter your network address above.</p>
<p class="my-0">To enable this option, please enter your network address in the settings.</p>
</div>
{/if}
<button id="btnScan" class="btn btn-secondary" type="button" on:click={scanNetwork} disabled={!settings.discovery}>
@@ -212,7 +212,7 @@
<div class="col-sm-8">
<div class="mb-3">
<label for="inputNetworkDiscovery" class="form-label">Network discovery address</label>
<input type="text" class="form-control" id="inputNetworkDiscovery" placeholder="192.168.1.0/24" bind:value="{settings.discovery}" pattern="^([01]?\d\d?|2[0-4]\d|25[0-5])(?:\.(?:[01]?\d\d?|2[0-4]\d|25[0-5])){'{'}2{'}'}(?:\.(?:0))(?:/[0-2]\d|/3[0-2])$" required>
<input type="text" class="form-control" id="inputNetworkDiscovery" placeholder="192.168.1.0/24" bind:value="{settings.discovery}" pattern="^([01]?\d\d?|2[0-4]\d|25[0-5])(?:\.(?:[01]?\d\d?|2[0-4]\d|25[0-5])){'{'}2{'}'}(?:\.(?:0))(?:/[0-2]\d|/3[0-2])$">
</div>
</div>
<div class="col-sm-4">
@@ -257,39 +257,34 @@
</div>
</div>
<style lang="scss">
@import "../variables.scss";
<style lang="scss" global>
.navbar-brand {
color: var(--color-text);
}
.dropdown-menu {
background-color: var(--bg-lighter);
li:first-child>button {
border-radius: 0.5rem 0.5rem 0rem 0rem;
}
li:last-child>button {
border-radius: 0rem 0rem 0.5rem 0.5rem;
}
li:only-child>button {
border-radius: 0.5rem;
}
}
.dropdown-item {
&:active {
color: #1e2125;
background-color: #e9ecef;
}
}
color: var(--color-text);
background-color: var(--bg-lighter);
.card {
border-radius: 1em;
}
.btn-light {
background-color: white;
border-radius: 1em;
}
.callout {
padding: 1rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
border: 1px solid #e9ecef;
border-left-width: 0.25rem;
border-radius: 0.25rem;
&.callout-info {
border-left-color: $info;
}
&.callout-danger {
border-left-color: $danger;
&:hover {
color: var(--color-bg);
background-color: var(--color-text);
}
}
</style>

View File

@@ -0,0 +1,36 @@
<script>
export let toast;
</script>
<div class="position-fixed bottom-0 end-0 p-3 toast-container">
<div class="toast fade {toast.show ? 'show' : 'hide'} {toast.color}-bg" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong id="toast-title" class="me-auto">{toast.title}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{toast.message}
</div>
</div>
</div>
<style lang="scss">
@import "../variables.scss";
.toast-container {
z-index: 11;
border-radius: $toast-border-radius;
div>.toast-body {
color: $dark-lighter;
}
}
.success-bg {
background-color: var(--success);
}
.danger-bg {
background-color: var(--danger);
}
</style>

View File

@@ -0,0 +1,22 @@
:root {
--success: #{$success};
--warning: #{$warning};
--danger: #{$danger};
--danger-dark-transparent: #{$danger-dark-transparent};
--info: #{$info};
--info-dark-transparent: #{$info-dark-transparent};
}
html[data-theme="light"] {
--color-bg: #{$light};
--color-text: #{$dark};
--bg-lighter: #{$light-darker};
--svg-close: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
}
html[data-theme="dark"] {
--color-bg: #{$dark};
--color-text: #{$light};
--bg-lighter: #{$dark-lighter};
--svg-close: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
}

View File

@@ -1,21 +1,29 @@
// colors
$light: #ecedf3;
$light: #f6f6fb;
$light-darker: #e2e4eb;
$success: #06d6a0;
$danger: #ef476f;
$danger-dark-transparent: #ef476f1f;
$warning: #ffd166;
$info: #118ab2;
$info: #1177b2;
$info-dark-transparent: #1177b21f;
$dark: #131316;
$dark-lighter: #25252b;
// bootstrap
$body-bg: $light;
$list-group-border-width: 0px;
$list-group-item-padding-x: 0px;
$list-group-item-padding-y: 0px;
$list-group-bg: transparent;
$modal-content-border-radius: 2em;
$modal-inner-padding: 1.5rem;
$btn-border-radius: 0.5rem;
$btn-border-radius: 0.7rem;
$dropdown-border-radius: 1em;
$dropdown-border-width: 0px;
$dropdown-min-width: 8rem;
$toast-border-radius: 0.7rem;
$toast-box-shadow: none;
$toast-border-width: 0px;
// font awesome
$fa-font-path: "webfonts";