Rebuilt the network view in Svelte
This commit is contained in:
24
src/svelte-components/.gitignore
vendored
Normal file
24
src/svelte-components/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
48
src/svelte-components/README.md
Normal file
48
src/svelte-components/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Svelte + TS + Vite
|
||||
|
||||
This template should help get you started developing with Svelte and TypeScript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
||||
|
||||
## Need an official Svelte framework?
|
||||
|
||||
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
|
||||
|
||||
## Technical considerations
|
||||
|
||||
**Why use this over SvelteKit?**
|
||||
|
||||
- It brings its own routing solution which might not be preferable for some users.
|
||||
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
|
||||
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.
|
||||
|
||||
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
|
||||
|
||||
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
|
||||
|
||||
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
|
||||
|
||||
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
|
||||
|
||||
**Why include `.vscode/extensions.json`?**
|
||||
|
||||
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
|
||||
|
||||
**Why enable `allowJs` in the TS template?**
|
||||
|
||||
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
|
||||
|
||||
**Why is HMR not preserving my local component state?**
|
||||
|
||||
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
|
||||
|
||||
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
|
||||
|
||||
```ts
|
||||
// store.ts
|
||||
// An extremely simple external store
|
||||
import { writable } from 'svelte/store'
|
||||
export default writable(0)
|
||||
```
|
||||
13
src/svelte-components/index.html
Normal file
13
src/svelte-components/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Svelte + TS + Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
9507
src/svelte-components/package-lock.json
generated
Normal file
9507
src/svelte-components/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
src/svelte-components/package.json
Normal file
28
src/svelte-components/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "svelte-components",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"postbuild": "smui-theme compile dist/smui.css -i src/theme",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^1.0.0-next.357",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.49",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"node-sass": "^7.0.1",
|
||||
"polyfill-object.fromentries": "^1.0.1",
|
||||
"smui-theme": "^6.0.0-beta.16",
|
||||
"svelte": "^3.48.0",
|
||||
"svelte-check": "^2.8.0",
|
||||
"svelte-material-ui": "^6.0.0-beta.16",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^2.9.13"
|
||||
}
|
||||
}
|
||||
BIN
src/svelte-components/public/favicon.ico
Normal file
BIN
src/svelte-components/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
28
src/svelte-components/public/material-symbols-outlined.css
Normal file
28
src/svelte-components/public/material-symbols-outlined.css
Normal file
@@ -0,0 +1,28 @@
|
||||
/* To browse the icons in material-symbols, see https://marella.me/material-symbols/demo/ */
|
||||
|
||||
@font-face {
|
||||
font-family: "Material Symbols Outlined";
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
font-display: block;
|
||||
src: url("./fonts/material-symbols-outlined.woff2") format("woff2");
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-family: "Material Symbols Outlined";
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-feature-settings: "liga";
|
||||
|
||||
font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 0, "opsz" 48;
|
||||
}
|
||||
204
src/svelte-components/src/components/AdminNetworkView.svelte
Normal file
204
src/svelte-components/src/components/AdminNetworkView.svelte
Normal file
@@ -0,0 +1,204 @@
|
||||
<script lang="ts">
|
||||
import WifiConnectionDialog from "../dialogs/WifiConnectionDialog.svelte";
|
||||
import ChangeHostnameDialog from "../dialogs/ChangeHostnameDialog.svelte";
|
||||
import Paper from "@smui/paper";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import List, { Item, Graphic, Text, Meta } from "@smui/list";
|
||||
import Card from "@smui/card";
|
||||
import { networkInfo } from "../lib/NetworkInfo";
|
||||
import type { WifiNetwork } from "../lib/NetworkInfo";
|
||||
|
||||
let changeHostnameDialog = {
|
||||
open: false,
|
||||
};
|
||||
|
||||
let wifiConnectionDialog = {
|
||||
open: false,
|
||||
network: {} as WifiNetwork,
|
||||
};
|
||||
|
||||
function getWifiStrengthIcon(network: WifiNetwork) {
|
||||
const strength = Math.ceil((Number(network.Quality) / 100) * 4);
|
||||
|
||||
switch (strength) {
|
||||
case 1:
|
||||
return "";
|
||||
|
||||
case 2:
|
||||
return "wifi_1_bar";
|
||||
|
||||
case 3:
|
||||
return "wifi_2_bar";
|
||||
|
||||
case 4:
|
||||
return "wifi";
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeHostname() {
|
||||
changeHostnameDialog = {
|
||||
open: true,
|
||||
};
|
||||
}
|
||||
|
||||
function onNetworkSelected(network: WifiNetwork) {
|
||||
wifiConnectionDialog = {
|
||||
open: true,
|
||||
network,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<WifiConnectionDialog {...wifiConnectionDialog} />
|
||||
<ChangeHostnameDialog {...changeHostnameDialog} />
|
||||
|
||||
<div class="admin-network-view">
|
||||
<h1>Network Info</h1>
|
||||
|
||||
<div class="pure-form pure-form-aligned">
|
||||
<div class="pure-control-group">
|
||||
<label for="hostname">Hostname</label>
|
||||
<Card id="hostname" variant="outlined">
|
||||
<Text id="hostname">
|
||||
{$networkInfo.hostname}
|
||||
</Text>
|
||||
</Card>
|
||||
<Button on:click={onChangeHostname} touch variant="raised">
|
||||
<Label>Change</Label>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-form pure-form-aligned">
|
||||
<div class="pure-control-group">
|
||||
<label for="ip-addresses">IP Addresses</label>
|
||||
<Card id="ip-addresses" variant="outlined">
|
||||
{#each $networkInfo.ipAddresses as ipAddress}
|
||||
<div>
|
||||
<Text id="hostname">
|
||||
{ipAddress}
|
||||
</Text>
|
||||
</div>
|
||||
{/each}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-form pure-form-aligned">
|
||||
<div class="pure-control-group">
|
||||
<label for="wifi">Wi-Fi</label>
|
||||
<div class="wifi-networks">
|
||||
<Card id="wifi" variant="outlined">
|
||||
<List>
|
||||
{#if $networkInfo.wifi.networks.length === 0}
|
||||
<Item class="wifi-network">
|
||||
<Text>Scanning...</Text>
|
||||
</Item>
|
||||
{:else}
|
||||
{#each $networkInfo.wifi.networks as network}
|
||||
<Item
|
||||
class="wifi-network"
|
||||
on:SMUI:action={() => onNetworkSelected(network)}
|
||||
>
|
||||
<Graphic
|
||||
class="strength {$networkInfo.wifi.ssid === network.Name
|
||||
? 'active'
|
||||
: ''}"
|
||||
>
|
||||
<span class="material-symbols-outlined background"
|
||||
>wifi</span
|
||||
>
|
||||
<span class="material-symbols-outlined">
|
||||
{getWifiStrengthIcon(network)}
|
||||
</span>
|
||||
</Graphic>
|
||||
<Text style="margin-right: 20px;">{network.Name}</Text>
|
||||
{#if network.Encryption !== "Open"}
|
||||
<Meta>
|
||||
<span class="material-symbols-outlined lock">lock</span>
|
||||
</Meta>
|
||||
{/if}
|
||||
</Item>
|
||||
{/each}
|
||||
{/if}
|
||||
</List>
|
||||
</Card>
|
||||
<em style="display: block;">
|
||||
Click on a Wi-Fi network to connect or disconnect.
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
$primary: #0078e7;
|
||||
$very-dark: #555;
|
||||
$text: #777;
|
||||
$grey: #bbb;
|
||||
$light: #ddd;
|
||||
|
||||
:global {
|
||||
.admin-network-view {
|
||||
.pure-form-aligned .pure-control-group label {
|
||||
vertical-align: top;
|
||||
font-size: 15pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mdc-card {
|
||||
width: 400px;
|
||||
min-height: 38px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: 20px;
|
||||
margin-right: 20px;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.wifi-networks {
|
||||
display: inline-block;
|
||||
|
||||
.mdc-card {
|
||||
padding: 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.wifi-network {
|
||||
.lock {
|
||||
font-size: 20px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.strength {
|
||||
border-radius: 50%;
|
||||
padding: 3px;
|
||||
background-color: $light;
|
||||
color: $very-dark;
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
background-color: $primary;
|
||||
color: white;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
&.background {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import TextField from "@smui/textfield";
|
||||
import MessageDialog from "./MessageDialog.svelte";
|
||||
import * as api from "../lib/api";
|
||||
|
||||
// https://man7.org/linux/man-pages/man7/hostname.7.html
|
||||
//
|
||||
// Each element of the hostname must be from 1 to 63 characters long
|
||||
// and the entire hostname, including the dots, can be at most 253
|
||||
// characters long. Valid characters for hostnames are ASCII(7)
|
||||
// letters from a to z, the digits from 0 to 9, and the hyphen (-).
|
||||
// A hostname may not start with a hyphen.
|
||||
|
||||
const pattern = /[a-zA-Z0-9][a-zA-Z0-9-]{0,62}/;
|
||||
|
||||
export let open = false;
|
||||
|
||||
let rebooting = false;
|
||||
let redirectTimeout = 45;
|
||||
let hostname = "";
|
||||
|
||||
$: setTimeout(() => {
|
||||
hostname = (hostname.match(pattern) || [""])[0].toLowerCase();
|
||||
}, 0);
|
||||
|
||||
$: if (open) {
|
||||
hostname = "";
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
rebooting = true;
|
||||
await api.PUT("hostname", { hostname });
|
||||
await api.PUT("reboot");
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (0 < redirectTimeout) {
|
||||
redirectTimeout -= 1;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
location.hostname = getRedirectTarget();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function getRedirectTarget() {
|
||||
if (location.hostname.endsWith(".local")) {
|
||||
return `${hostname}.local`;
|
||||
}
|
||||
|
||||
if (location.hostname.endsWith(".lan")) {
|
||||
return `${hostname}.lan`;
|
||||
}
|
||||
|
||||
return hostname;
|
||||
}
|
||||
</script>
|
||||
|
||||
<MessageDialog open={rebooting} title="Rebooting">
|
||||
Rebooting to apply the hostname change...
|
||||
</MessageDialog>
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
scrimClickAction=""
|
||||
aria-labelledby="simple-title"
|
||||
aria-describedby="simple-content"
|
||||
>
|
||||
<Title id="simple-title">Change Hostname</Title>
|
||||
|
||||
<Content id="simple-content">
|
||||
<TextField
|
||||
bind:value={hostname}
|
||||
label="New Hostname"
|
||||
spellcheck="false"
|
||||
variant="filled"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
|
||||
<p>
|
||||
<em>Clicking Confirm will reboot the controller to apply the change.</em>
|
||||
</p>
|
||||
</Content>
|
||||
|
||||
<Actions>
|
||||
<Button>
|
||||
<Label>Cancel</Label>
|
||||
</Button>
|
||||
<Button defaultAction on:click={onConfirm} disabled={hostname.length === 0}>
|
||||
<Label>Confirm</Label>
|
||||
</Button>
|
||||
</Actions>
|
||||
</Dialog>
|
||||
19
src/svelte-components/src/dialogs/MessageDialog.svelte
Normal file
19
src/svelte-components/src/dialogs/MessageDialog.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import Dialog, { Title, Content } from "@smui/dialog";
|
||||
|
||||
export let open: boolean;
|
||||
export let title: string;
|
||||
</script>
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
scrimClickAction=""
|
||||
escapeKeyAction=""
|
||||
aria-labelledby="simple-title"
|
||||
aria-describedby="simple-content"
|
||||
>
|
||||
<Title id="simple-title">{title}</Title>
|
||||
<Content id="simple-content">
|
||||
<slot />
|
||||
</Content>
|
||||
</Dialog>
|
||||
101
src/svelte-components/src/dialogs/WifiConnectionDialog.svelte
Normal file
101
src/svelte-components/src/dialogs/WifiConnectionDialog.svelte
Normal file
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import TextField from "@smui/textfield";
|
||||
import Icon from "@smui/textfield/icon";
|
||||
import HelperText from "@smui/textfield/helper-text";
|
||||
import MessageDialog from "./MessageDialog.svelte";
|
||||
import type { WifiNetwork } from "../lib/NetworkInfo";
|
||||
import * as api from "../lib/api";
|
||||
|
||||
export let open = false;
|
||||
export let network: WifiNetwork;
|
||||
|
||||
let rebooting = false;
|
||||
let needPassword = false;
|
||||
let password = "";
|
||||
let showPassword = false;
|
||||
let connectOrDisconnect: string;
|
||||
let connectToOrDisconnectFrom: string;
|
||||
|
||||
$: needPassword = !network?.active && network?.Encryption !== "Open";
|
||||
|
||||
$: {
|
||||
connectOrDisconnect = network?.active ? "Disconnect" : "Connect";
|
||||
connectToOrDisconnectFrom = network?.active ? "Disconnect from" : "Connect to";
|
||||
}
|
||||
|
||||
$: if (open) {
|
||||
password = "";
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
rebooting = true;
|
||||
|
||||
await api.PUT("network", {
|
||||
wifi: {
|
||||
enabled: !network.active,
|
||||
ssid: network.Name,
|
||||
password,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<MessageDialog open={rebooting} title="Rebooting">
|
||||
Rebooting to apply Wifi changes...
|
||||
</MessageDialog>
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
scrimClickAction=""
|
||||
aria-labelledby="simple-title"
|
||||
aria-describedby="simple-content"
|
||||
>
|
||||
<Title id="simple-title">{connectToOrDisconnectFrom} {network.Name}</Title>
|
||||
|
||||
<Content id="simple-content">
|
||||
{#if needPassword}
|
||||
<TextField
|
||||
bind:value={password}
|
||||
label="Password"
|
||||
spellcheck="false"
|
||||
variant="filled"
|
||||
type={showPassword ? "text" : "password"}
|
||||
style="width: 100%;"
|
||||
>
|
||||
<div
|
||||
slot="trailingIcon"
|
||||
on:click={() => (showPassword = !showPassword)}
|
||||
>
|
||||
<Icon class="material-symbols-outlined">
|
||||
{showPassword ? "password" : "abc"}
|
||||
</Icon>
|
||||
</div>
|
||||
<HelperText persistent slot="helper">
|
||||
Wifi passwords must be 8 to 128 characters
|
||||
</HelperText>
|
||||
</TextField>
|
||||
{/if}
|
||||
|
||||
<p>
|
||||
<em>
|
||||
Clicking {connectOrDisconnect} will reboot the controller to apply the changes.
|
||||
</em>
|
||||
</p>
|
||||
</Content>
|
||||
|
||||
<Actions>
|
||||
<Button>
|
||||
<Label>Cancel</Label>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
defaultAction
|
||||
on:click={onConfirm}
|
||||
disabled={needPassword && (password.length < 8 || password.length > 128)}
|
||||
>
|
||||
<Label>{connectOrDisconnect}</Label>
|
||||
</Button>
|
||||
</Actions>
|
||||
</Dialog>
|
||||
93
src/svelte-components/src/lib/NetworkInfo.ts
Normal file
93
src/svelte-components/src/lib/NetworkInfo.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { readable } from "svelte/store";
|
||||
import * as api from "./api";
|
||||
|
||||
export type WifiNetwork = {
|
||||
Quality: string;
|
||||
Channel: string;
|
||||
Frequency: string;
|
||||
Mode: string;
|
||||
"Bit Rates": string;
|
||||
Name: string;
|
||||
Address: string;
|
||||
Encryption: string;
|
||||
"Signal Level": string;
|
||||
"Noise Level": string;
|
||||
lastSeen: number;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
export type NetworkInfo = {
|
||||
ipAddresses: Array<string>;
|
||||
hostname: string;
|
||||
wifi: {
|
||||
ssid: string;
|
||||
networks: Array<WifiNetwork>;
|
||||
};
|
||||
};
|
||||
|
||||
const empty: NetworkInfo = {
|
||||
ipAddresses: [],
|
||||
hostname: "",
|
||||
wifi: {
|
||||
ssid: "",
|
||||
networks: []
|
||||
}
|
||||
}
|
||||
|
||||
export const networkInfo = readable<NetworkInfo>(empty, (set) => {
|
||||
getNetworkInfo();
|
||||
const networkInfoIntervalId = setInterval(getNetworkInfo, 5000);
|
||||
|
||||
async function getNetworkInfo() {
|
||||
const networksByName: Record<string, WifiNetwork> = {}
|
||||
|
||||
try {
|
||||
const networkInfo: NetworkInfo = await api.GET("network");
|
||||
|
||||
const now = Date.now();
|
||||
for (let network of networkInfo.wifi.networks) {
|
||||
if (network.Name) {
|
||||
network.lastSeen = now;
|
||||
network.active = networkInfo.wifi.ssid === network.Name;
|
||||
networksByName[network.Name] = network;
|
||||
}
|
||||
}
|
||||
|
||||
for (let network of Object.values(networksByName)) {
|
||||
if (network.lastSeen - now > 30000) {
|
||||
delete networksByName[network.Name];
|
||||
}
|
||||
}
|
||||
|
||||
set({
|
||||
ipAddresses: networkInfo.ipAddresses,
|
||||
hostname: networkInfo.hostname,
|
||||
wifi: {
|
||||
ssid: networkInfo.wifi.ssid,
|
||||
networks: Object.values(networksByName).sort((a, b) => {
|
||||
switch (true) {
|
||||
case a.active:
|
||||
return -1;
|
||||
|
||||
case b.active:
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return a.Name.localeCompare(b.Name);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.debug("Failed to fetch network info", error);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(networkInfoIntervalId);
|
||||
}
|
||||
})
|
||||
|
||||
export function init() {
|
||||
return networkInfo.subscribe(() => ({}));
|
||||
}
|
||||
41
src/svelte-components/src/lib/api.ts
Normal file
41
src/svelte-components/src/lib/api.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
type HttpMethod = "GET" | "PUT" | "POST" | "DELETE";
|
||||
|
||||
async function doFetch(method: HttpMethod, url: string, data: any, config: RequestInit) {
|
||||
try {
|
||||
const response = await fetch(`/api/${url}`, {
|
||||
...config,
|
||||
method,
|
||||
cache: "no-cache",
|
||||
body: (typeof data === 'object')
|
||||
? JSON.stringify(data)
|
||||
: undefined,
|
||||
headers: (typeof data === 'object')
|
||||
? {
|
||||
"Content-Type": 'application/json; charset=utf-8'
|
||||
}
|
||||
: {}
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.debug('API Error: ' + url + ': ' + error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(url: string, config: RequestInit = {}) {
|
||||
return doFetch('GET', url, undefined, config);
|
||||
}
|
||||
|
||||
export async function PUT(url: string, data: any = undefined, config: RequestInit = {}) {
|
||||
return doFetch('PUT', url, data, config);
|
||||
}
|
||||
|
||||
export async function POST(url: string, data: any = undefined, config: RequestInit = {}) {
|
||||
return doFetch('POST', url, data, config);
|
||||
}
|
||||
|
||||
export async function DELETE(url: string, config = {}) {
|
||||
return doFetch('DELETE', url, undefined, config);
|
||||
}
|
||||
15
src/svelte-components/src/main.ts
Normal file
15
src/svelte-components/src/main.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'polyfill-object.fromentries';
|
||||
import AdminNetworkView from './components/AdminNetworkView.svelte';
|
||||
import { init as initNetworkInfo } from './lib/NetworkInfo';
|
||||
|
||||
export function create(component: string, target: HTMLElement, props: Record<string, any>) {
|
||||
switch (component) {
|
||||
case "AdminNetworkView":
|
||||
return new AdminNetworkView({ target, props });
|
||||
|
||||
default:
|
||||
throw new Error("Unknown component");
|
||||
}
|
||||
}
|
||||
|
||||
export { initNetworkInfo };
|
||||
22
src/svelte-components/src/theme/_smui-theme.scss
Normal file
22
src/svelte-components/src/theme/_smui-theme.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
@use 'sass:color';
|
||||
|
||||
@use '@material/theme/color-palette';
|
||||
|
||||
// Svelte Colors!
|
||||
@use '@material/theme/index' as theme with (
|
||||
$primary: #0078e7,
|
||||
$secondary: #676778,
|
||||
$surface: #fff,
|
||||
$background: #fff,
|
||||
$error: color-palette.$red-900,
|
||||
$on-surface: #777
|
||||
);
|
||||
|
||||
@use "@material/elevation/mdc-elevation";
|
||||
|
||||
@use "@material/list";
|
||||
@include list.deprecated-core-styles;
|
||||
|
||||
:root {
|
||||
--mdc-theme-text-primary-on-background: #777;
|
||||
}
|
||||
12
src/svelte-components/src/theme/dark/_smui-theme.scss
Normal file
12
src/svelte-components/src/theme/dark/_smui-theme.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@use 'sass:color';
|
||||
|
||||
@use '@material/theme/color-palette';
|
||||
|
||||
// Svelte Colors! (Dark Theme)
|
||||
@use '@material/theme/index' as theme with (
|
||||
$primary: #ff3e00,
|
||||
$secondary: color.scale(#676778, $whiteness: -10%),
|
||||
$surface: color.adjust(color-palette.$grey-900, $blue: +4),
|
||||
$background: #000,
|
||||
$error: color-palette.$red-700
|
||||
);
|
||||
2
src/svelte-components/src/vite-env.d.ts
vendored
Normal file
2
src/svelte-components/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
7
src/svelte-components/svelte.config.js
Normal file
7
src/svelte-components/svelte.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import sveltePreprocess from 'svelte-preprocess'
|
||||
|
||||
export default {
|
||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: sveltePreprocess()
|
||||
}
|
||||
22
src/svelte-components/tsconfig.json
Normal file
22
src/svelte-components/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true,
|
||||
"moduleSuffixes": [".svelte", ""]
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
8
src/svelte-components/tsconfig.node.json
Normal file
8
src/svelte-components/tsconfig.node.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
19
src/svelte-components/vite.config.ts
Normal file
19
src/svelte-components/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
svelte()
|
||||
],
|
||||
build: {
|
||||
target: "chrome60",
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/main.ts'),
|
||||
name: 'SvelteComponents',
|
||||
formats: ['iife'],
|
||||
fileName: () => "index.js"
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user