Set time and time zone
This commit is contained in:
859
src/svelte-components/package-lock.json
generated
859
src/svelte-components/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,12 @@
|
||||
"node-sass": "^7.0.1",
|
||||
"polyfill-object.fromentries": "^1.0.1",
|
||||
"smui-theme": "^6.0.0-beta.16",
|
||||
"string.prototype.matchall": "^4.0.7",
|
||||
"svelte": "^3.48.0",
|
||||
"svelte-check": "^2.8.0",
|
||||
"svelte-material-ui": "^6.0.0-beta.16",
|
||||
"svelte-preprocess": "^4.10.7",
|
||||
"svelte-tiny-virtual-list": "^2.0.5",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^2.9.13"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import ProbeDialog from "$dialogs/ProbeDialog.svelte";
|
||||
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
|
||||
import UploadDialog from "$dialogs/UploadDialog.svelte";
|
||||
import SetTimeDialog from "./SetTimeDialog.svelte";
|
||||
|
||||
const HomeMachineDialogProps = writable<HomeMachineDialogPropsType>();
|
||||
type HomeMachineDialogPropsType = {
|
||||
@@ -29,6 +30,11 @@
|
||||
onComplete: () => void;
|
||||
};
|
||||
|
||||
const SetTimeDialogProps = writable<SetTimeDialogPropsType>();
|
||||
type SetTimeDialogPropsType = {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
export function showDialog(
|
||||
dialog: "HomeMachine",
|
||||
props: Omit<HomeMachineDialogPropsType, "open">
|
||||
@@ -49,6 +55,11 @@
|
||||
props: Omit<UploadDialogPropsType, "open">
|
||||
);
|
||||
|
||||
export function showDialog(
|
||||
dialog: "SetTime",
|
||||
props: Omit<SetTimeDialogPropsType, "open">
|
||||
);
|
||||
|
||||
export function showDialog(dialog: string, props: any) {
|
||||
switch (dialog) {
|
||||
case "HomeMachine":
|
||||
@@ -67,6 +78,10 @@
|
||||
UploadDialogProps.set({ ...props, open: true });
|
||||
break;
|
||||
|
||||
case "SetTime":
|
||||
SetTimeDialogProps.set({ ...props, open: true });
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown dialog '${dialog}`);
|
||||
}
|
||||
@@ -77,3 +92,4 @@
|
||||
<ProbeDialog {...$ProbeDialogProps} />
|
||||
<ScreenRotationDialog {...$ScreenRotationDialogProps} />
|
||||
<UploadDialog {...$UploadDialogProps} />
|
||||
<SetTimeDialog {...$SetTimeDialogProps} />
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
<script type="ts" context="module">
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
<script type="ts">
|
||||
import DimensionInput from "$components/DimensionInput.svelte";
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import { waitForChange } from "$lib/StoreHelpers";
|
||||
import { ControllerMethods } from "$lib/RegisterControllerMethods";
|
||||
import { Config } from "$lib/ConfigStore";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import {
|
||||
probingActive,
|
||||
probeContacted,
|
||||
probingComplete,
|
||||
probingFailed,
|
||||
probingStarted,
|
||||
} from "$lib/ControllerState";
|
||||
|
||||
type Step =
|
||||
| "None"
|
||||
@@ -19,49 +32,8 @@
|
||||
};
|
||||
|
||||
const cancelled = writable(false);
|
||||
const probingActive = writable(false);
|
||||
const probeContacted = writable(false);
|
||||
const probingStarted = writable(false);
|
||||
const probingFailed = writable(false);
|
||||
const probingComplete = writable(false);
|
||||
const userAcknowledged = writable(false);
|
||||
|
||||
export function handleControllerStateUpdate(state: Record<string, any>) {
|
||||
if (!get(probingActive)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case state.pw === 0:
|
||||
probeContacted.set(true);
|
||||
break;
|
||||
|
||||
case state.log?.msg === "Switch not found":
|
||||
probingFailed.set(true);
|
||||
break;
|
||||
|
||||
case state.cycle !== "idle":
|
||||
probingStarted.set(true);
|
||||
break;
|
||||
|
||||
case state.cycle === "idle":
|
||||
if (get(probingStarted)) {
|
||||
probingStarted.set(false);
|
||||
probingComplete.set(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="ts">
|
||||
import DimensionInput from "$components/DimensionInput.svelte";
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import { waitForChange } from "$lib/StoreHelpers";
|
||||
import { ControllerMethods } from "$lib/RegisterControllerMethods";
|
||||
import { Config } from "$lib/ConfigStore";
|
||||
|
||||
const cutterDiameterOptions = [
|
||||
{ value: 0.5, label: '1/2 "', metric: false },
|
||||
{ value: 0.25, label: '1/4 "', metric: false },
|
||||
|
||||
240
src/svelte-components/src/dialogs/SetTimeDialog.svelte
Normal file
240
src/svelte-components/src/dialogs/SetTimeDialog.svelte
Normal file
@@ -0,0 +1,240 @@
|
||||
<script lang="ts">
|
||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||
import Button, { Label } from "@smui/button";
|
||||
import TextField from "@smui/textfield";
|
||||
import CircularProgress from "@smui/circular-progress";
|
||||
import VirtualList from "svelte-tiny-virtual-list";
|
||||
import * as api from "$lib/api";
|
||||
|
||||
const itemHeight = 35;
|
||||
|
||||
type Timezone = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export let open = false;
|
||||
let value = "";
|
||||
let wasOpen = false;
|
||||
let loading = true;
|
||||
let timezones: Timezone[] = [];
|
||||
let currentTimezoneIndex: number;
|
||||
let selectedTimezoneIndex: number;
|
||||
let networkTimeSynchronized: boolean;
|
||||
|
||||
$: if (open != wasOpen) {
|
||||
if (!wasOpen) {
|
||||
loadData();
|
||||
}
|
||||
|
||||
wasOpen = open;
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading = true;
|
||||
|
||||
const result = await api.GET("time");
|
||||
|
||||
parseTimezones(result.timezones);
|
||||
parseTimeinfo(result.timeinfo);
|
||||
value = getDateTimeValueString();
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function parseTimeinfo(str: string) {
|
||||
const matches = Array.from(str.matchAll(/\s*([^:]+):\s+(.+)/gm));
|
||||
|
||||
let currentTimezoneValue;
|
||||
for (const match of matches) {
|
||||
let [, label, value] = match;
|
||||
|
||||
switch (label) {
|
||||
case "Time zone":
|
||||
currentTimezoneValue = value.split(" ")[0];
|
||||
break;
|
||||
|
||||
case "NTP synchronized":
|
||||
networkTimeSynchronized = value === "yes";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
currentTimezoneIndex = timezones.findIndex(
|
||||
(tz) => tz.value === currentTimezoneValue
|
||||
);
|
||||
selectedTimezoneIndex = currentTimezoneIndex;
|
||||
}
|
||||
|
||||
function parseTimezones(str: string) {
|
||||
const matches = Array.from(str.matchAll(/\s*(\S+)\s*/gm));
|
||||
|
||||
timezones = [];
|
||||
for (let [, value] of matches) {
|
||||
timezones.push({
|
||||
label: value.replace(/_/g, " "),
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort alphabetically, but with the current timezone at the top of the list
|
||||
timezones.sort((a, b) => {
|
||||
switch (true) {
|
||||
case a.value === "UTC":
|
||||
return -1;
|
||||
|
||||
case b.value === "UTC":
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return a.value.localeCompare(b.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getDateTimeValueString() {
|
||||
const date = new Date();
|
||||
|
||||
const year = date.getFullYear().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const hour = date.getHours().toString().padStart(2, "0");
|
||||
const minute = date.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day}T${hour}:${minute}:00`;
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
const date = new Date(value);
|
||||
const year = date.getFullYear().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const hour = date.getHours().toString().padStart(2, "0");
|
||||
const minute = date.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
await api.PUT("time", {
|
||||
datetime: `${year}-${month}-${day} ${hour}:${minute}:00`,
|
||||
timezone: timezones[selectedTimezoneIndex].value,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog
|
||||
bind:open
|
||||
scrimClickAction=""
|
||||
aria-labelledby="set-time-dialog-title"
|
||||
aria-describedby="set-time-dialog-content"
|
||||
>
|
||||
<Title id="set-time-dialog-title">Adjust Clock & Timezone</Title>
|
||||
|
||||
<Content id="set-time-dialog-content">
|
||||
{#if loading}
|
||||
<div style="display: flex; justify-content: center">
|
||||
<CircularProgress style="height: 32px; width: 32px;" indeterminate />
|
||||
</div>
|
||||
{:else}
|
||||
{#if networkTimeSynchronized}
|
||||
<p>
|
||||
Because this controller is connected to the internet, the time is
|
||||
managed automatically, and cannot be manually set.
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
Because this controller is not connected to the internet, you can
|
||||
manually set the time.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Note: any time the controller is turned off, the time will need to be
|
||||
reset. If you connect the controller to the internet, the time will be
|
||||
managed automatically.
|
||||
</p>
|
||||
|
||||
<Label>Date & Time</Label>
|
||||
<TextField
|
||||
bind:value
|
||||
label="Time"
|
||||
type="datetime-local"
|
||||
variant="filled"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<p>
|
||||
To display your local time correctly, the controller must know what
|
||||
timezone it is in.
|
||||
</p>
|
||||
|
||||
<div class="timezones-container" style="margin-top: 20px;">
|
||||
<Label>Select your timezone</Label>
|
||||
<VirtualList
|
||||
width="100%"
|
||||
height={itemHeight * 6}
|
||||
itemCount={timezones.length}
|
||||
itemSize={itemHeight}
|
||||
scrollToIndex={currentTimezoneIndex}
|
||||
scrollToAlignment="center"
|
||||
>
|
||||
<div
|
||||
slot="item"
|
||||
let:index
|
||||
let:style
|
||||
{style}
|
||||
class="timezone"
|
||||
class:selected={index === selectedTimezoneIndex}
|
||||
on:click={() => (selectedTimezoneIndex = index)}
|
||||
>
|
||||
{timezones[index].label}
|
||||
</div>
|
||||
</VirtualList>
|
||||
</div>
|
||||
{/if}
|
||||
</Content>
|
||||
|
||||
<Actions>
|
||||
<Button>
|
||||
<Label>Cancel</Label>
|
||||
</Button>
|
||||
<Button
|
||||
defaultAction
|
||||
disabled={selectedTimezoneIndex === -1}
|
||||
on:click={onConfirm}
|
||||
>
|
||||
<Label>Confirm</Label>
|
||||
</Button>
|
||||
</Actions>
|
||||
</Dialog>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass:color";
|
||||
|
||||
$primary: #0078e7;
|
||||
$very-dark: #555;
|
||||
$text: #777;
|
||||
$grey: #bbb;
|
||||
$light: #ddd;
|
||||
|
||||
.timezones-container {
|
||||
:global {
|
||||
.virtual-list-wrapper {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
.timezone {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding-left: 10px;
|
||||
|
||||
&.selected {
|
||||
color: $primary;
|
||||
background-color: color.adjust($primary, $lightness: 50%);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
src/svelte-components/src/lib/ControllerState.ts
Normal file
36
src/svelte-components/src/lib/ControllerState.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { get, writable } from "svelte/store";
|
||||
import { processNetworkInfo } from "./NetworkInfo";
|
||||
|
||||
export const networkInfo = writable({});
|
||||
|
||||
export const probingActive = writable(false);
|
||||
export const probeContacted = writable(false);
|
||||
export const probingStarted = writable(false);
|
||||
export const probingFailed = writable(false);
|
||||
export const probingComplete = writable(false);
|
||||
|
||||
export function handleControllerStateUpdate(state: Record<string, any>) {
|
||||
if (state.networkInfo) {
|
||||
processNetworkInfo(state.networkInfo);
|
||||
delete state.networkInfo;
|
||||
}
|
||||
|
||||
if (get(probingActive)) {
|
||||
if (state.pw === 0) {
|
||||
probeContacted.set(true);
|
||||
}
|
||||
|
||||
if (state.log?.msg === "Switch not found") {
|
||||
probingFailed.set(true);
|
||||
}
|
||||
|
||||
if (state.cycle !== "idle") {
|
||||
probingStarted.set(true);
|
||||
}
|
||||
|
||||
if (state.cycle === "idle" && get(probingStarted)) {
|
||||
probingStarted.set(false);
|
||||
probingComplete.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { readable } from "svelte/store";
|
||||
import * as api from "$lib/api";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export type WifiNetwork = {
|
||||
Quality: string;
|
||||
@@ -34,60 +33,43 @@ const empty: NetworkInfo = {
|
||||
}
|
||||
}
|
||||
|
||||
export const networkInfo = readable<NetworkInfo>(empty, (set) => {
|
||||
getNetworkInfo();
|
||||
const networkInfoIntervalId = setInterval(getNetworkInfo, 5000);
|
||||
export const networkInfo = writable<NetworkInfo>(empty);
|
||||
|
||||
async function getNetworkInfo() {
|
||||
const networksByName: Record<string, WifiNetwork> = {}
|
||||
export function processNetworkInfo(rawNetworkInfo: NetworkInfo) {
|
||||
const now = Date.now();
|
||||
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);
|
||||
for (let network of rawNetworkInfo.wifi.networks) {
|
||||
if (network.Name) {
|
||||
network.lastSeen = now;
|
||||
network.active = rawNetworkInfo.wifi.ssid === network.Name;
|
||||
networksByName[network.Name] = network;
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(networkInfoIntervalId);
|
||||
for (let network of Object.values(networksByName)) {
|
||||
if (network.lastSeen - now > 30000) {
|
||||
delete networksByName[network.Name];
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export function init() {
|
||||
return networkInfo.subscribe(() => ({}));
|
||||
networkInfo.set({
|
||||
ipAddresses: rawNetworkInfo.ipAddresses,
|
||||
hostname: rawNetworkInfo.hostname,
|
||||
wifi: {
|
||||
ssid: rawNetworkInfo.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);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'polyfill-object.fromentries';
|
||||
import matchAll from "string.prototype.matchall";
|
||||
|
||||
matchAll.shim();
|
||||
|
||||
import AdminNetworkView from '$components/AdminNetworkView.svelte';
|
||||
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
|
||||
import Devmode from "$components/Devmode.svelte";
|
||||
import { handleConfigUpdate } from '$lib/ConfigStore';
|
||||
import { init as initNetworkInfo } from '$lib/NetworkInfo';
|
||||
import { handleControllerStateUpdate } from "$dialogs/ProbeDialog.svelte";
|
||||
import { handleControllerStateUpdate } from "$lib/ControllerState";
|
||||
import { registerControllerMethods } from "$lib/RegisterControllerMethods";
|
||||
|
||||
export function createComponent(component: string, target: HTMLElement, props: Record<string, any>) {
|
||||
@@ -17,7 +19,7 @@ export function createComponent(component: string, target: HTMLElement, props: R
|
||||
return new DialogHost({ target, props });
|
||||
|
||||
case "Devmode":
|
||||
return new Devmode({target, props});
|
||||
return new Devmode({ target, props });
|
||||
|
||||
default:
|
||||
throw new Error("Unknown component");
|
||||
@@ -25,7 +27,6 @@ export function createComponent(component: string, target: HTMLElement, props: R
|
||||
}
|
||||
|
||||
export {
|
||||
initNetworkInfo,
|
||||
showDialog,
|
||||
handleControllerStateUpdate,
|
||||
handleConfigUpdate,
|
||||
|
||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
minify: false,
|
||||
target: "chrome60",
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/main.ts'),
|
||||
|
||||
Reference in New Issue
Block a user