mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-07-28 00:12:56 -04:00
add dark mode and notifications
This commit is contained in:
@@ -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% {
|
||||
|
||||
48
app/frontend/src/components/DarkToggle.svelte
Normal file
48
app/frontend/src/components/DarkToggle.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
36
app/frontend/src/components/Toast.svelte
Normal file
36
app/frontend/src/components/Toast.svelte
Normal 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>
|
||||
22
app/frontend/src/theme.scss
Normal file
22
app/frontend/src/theme.scss
Normal 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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user