From 98ad7d1432a972b7d668221d7ed0a74098e8daff Mon Sep 17 00:00:00 2001 From: Djuri Baars Date: Sun, 4 May 2025 02:12:17 +0200 Subject: [PATCH] feat: Most settings implemented --- .gitignore | 2 + eslint.config.js | 3 +- messages/de-DE.json | 6 + messages/en-US.json | 9 +- messages/es-ES.json | 6 + messages/nl-NL.json | 6 + package.json | 3 +- pnpm-lock.yaml | 82 +++ src/app.css | 2 + src/lib/clockControl.ts | 96 +++ src/lib/components/BTClock.svelte | 3 - src/lib/components/form/NumericInput.svelte | 75 ++ src/lib/components/form/Radio.svelte | 67 ++ src/lib/components/form/RadioGroup.svelte | 76 ++ src/lib/components/form/RangeSlider.svelte | 134 ++++ src/lib/components/form/Select.svelte | 90 +++ .../components/form/SettingsInputField.svelte | 70 ++ .../components/form/TimezoneSelector.svelte | 51 ++ src/lib/components/form/Toggle.svelte | 40 +- src/lib/components/form/ToggleWrapper.svelte | 26 + src/lib/components/index.ts | 10 + .../layout/CollapsibleSection.svelte | 2 +- src/lib/components/layout/Navbar.svelte | 52 +- .../sections/FirmwareUpdateSection.svelte | 259 +++++++ .../sections/SettingsSection.svelte | 668 +++++++++++------- .../components/sections/StatusSection.svelte | 154 ++-- .../components/sections/SystemSection.svelte | 62 +- src/lib/components/ui/Alert.svelte | 65 ++ src/lib/components/ui/CardContainer.svelte | 4 +- src/lib/stores/firmware.ts | 82 +++ src/lib/stores/index.ts | 1 + src/lib/stores/settings.ts | 30 +- src/lib/stores/status.ts | 1 + src/lib/types.ts | 3 + src/lib/utils.ts | 80 +++ src/lib/utils/index.ts | 46 ++ src/routes/+layout.svelte | 8 +- src/routes/+layout.ts | 2 +- src/routes/settings/+page.svelte | 13 +- src/routes/system/+page.svelte | 7 +- src/safelist.txt | 1 + 41 files changed, 1976 insertions(+), 421 deletions(-) create mode 100644 src/lib/components/form/NumericInput.svelte create mode 100644 src/lib/components/form/Radio.svelte create mode 100644 src/lib/components/form/RadioGroup.svelte create mode 100644 src/lib/components/form/RangeSlider.svelte create mode 100644 src/lib/components/form/Select.svelte create mode 100644 src/lib/components/form/SettingsInputField.svelte create mode 100644 src/lib/components/form/TimezoneSelector.svelte create mode 100644 src/lib/components/form/ToggleWrapper.svelte create mode 100644 src/lib/components/sections/FirmwareUpdateSection.svelte create mode 100644 src/lib/components/ui/Alert.svelte create mode 100644 src/lib/stores/firmware.ts create mode 100644 src/lib/utils/index.ts create mode 100644 src/safelist.txt diff --git a/.gitignore b/.gitignore index 11ad1db..be88562 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ vite.config.ts.timestamp-* # Paraglide src/lib/paraglide +build +build_gz \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 4fa0657..8547ae7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -37,7 +37,8 @@ export default ts.config( }, { rules: { - 'svelte/no-at-html-tags': 'off' + 'svelte/no-at-html-tags': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off' } } ); diff --git a/messages/de-DE.json b/messages/de-DE.json index 32a8ab8..f640e50 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -118,6 +118,12 @@ "viewRelease": "Veröffentlichung anzeigen", "autoUpdate": "Update installieren (experimentell)", "autoUpdateInProgress": "Automatische Aktualisierung läuft, bitte warten..." + }, + "system": { + "firmwareOutdated": "WebUI und Firmware sind nicht synchronisiert. Dies kann zu unerwartetem Verhalten führen." + }, + "home": { + "title": "Start" } }, "colors": { diff --git a/messages/en-US.json b/messages/en-US.json index 41f093d..c005ad7 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -92,7 +92,8 @@ "dndStartHour": "Start hour", "dndStartMinute": "Start minute", "dndEndHour": "End hour", - "dndEndMinute": "End minute" + "dndEndMinute": "End minute", + "localPoolEndpoint": "Local Pool Endpoint" }, "control": { "systemInfo": "System info", @@ -138,6 +139,12 @@ "viewRelease": "View Release", "autoUpdate": "Install update (experimental)", "autoUpdateInProgress": "Auto-update in progress, please wait..." + }, + "system": { + "firmwareOutdated": "WebUI and firmware are out of sync. This might cause unexpected behavior." + }, + "home": { + "title": "Home" } }, "colors": { diff --git a/messages/es-ES.json b/messages/es-ES.json index 2937974..be6e04c 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -118,6 +118,12 @@ "viewRelease": "Ver lanzamiento", "autoUpdate": "Instalar actualización (experimental)", "autoUpdateInProgress": "Actualización automática en progreso, espere..." + }, + "system": { + "firmwareOutdated": "La interfaz web y el firmware no están sincronizados. Esto podría causar un comportamiento inesperado." + }, + "home": { + "title": "Inicio" } }, "button": { diff --git a/messages/nl-NL.json b/messages/nl-NL.json index ebad607..6183472 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -108,6 +108,12 @@ "viewRelease": "Bekijk publicatie", "autoUpdate": "Update installeren (experimenteel)", "autoUpdateInProgress": "Automatische update wordt uitgevoerd. Even geduld a.u.b...." + }, + "system": { + "firmwareOutdated": "WebUI en firmware zijn niet gesynchroniseerd. Dit kan onverwacht gedrag veroorzaken." + }, + "home": { + "title": "Overzicht" } }, "colors": { diff --git a/package.json b/package.json index 29ea8d7..4a861c9 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "@fontsource-variable/oswald": "^5.2.5", "@fontsource/ubuntu": "^5.2.5", "@inlang/paraglide-js": "^2.0.0", - "daisyui": "^5.0.35" + "daisyui": "^5.0.35", + "nostr-tools": "^2.12.0" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ed79a1..34cdac2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: daisyui: specifier: ^5.0.35 version: 5.0.35 + nostr-tools: + specifier: ^2.12.0 + version: 2.12.0(typescript@5.8.3) devDependencies: '@eslint/compat': specifier: ^1.2.5 @@ -414,6 +417,23 @@ packages: resolution: {integrity: sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==} engines: {node: '>=18'} + '@noble/ciphers@0.5.3': + resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==} + + '@noble/curves@1.1.0': + resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} + + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + + '@noble/hashes@1.3.1': + resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==} + engines: {node: '>= 16'} + + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -534,6 +554,15 @@ packages: cpu: [x64] os: [win32] + '@scure/base@1.1.1': + resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} + + '@scure/bip32@1.3.1': + resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==} + + '@scure/bip39@1.2.1': + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + '@sinclair/typebox@0.31.28': resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} @@ -1569,6 +1598,17 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + nostr-tools@2.12.0: + resolution: {integrity: sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + nostr-wasm@0.1.0: + resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} + nwsapi@2.2.20: resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} @@ -2473,6 +2513,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@noble/ciphers@0.5.3': {} + + '@noble/curves@1.1.0': + dependencies: + '@noble/hashes': 1.3.1 + + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + + '@noble/hashes@1.3.1': {} + + '@noble/hashes@1.3.2': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2551,6 +2605,19 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.1': optional: true + '@scure/base@1.1.1': {} + + '@scure/bip32@1.3.1': + dependencies: + '@noble/curves': 1.1.0 + '@noble/hashes': 1.3.1 + '@scure/base': 1.1.1 + + '@scure/bip39@1.2.1': + dependencies: + '@noble/hashes': 1.3.1 + '@scure/base': 1.1.1 + '@sinclair/typebox@0.31.28': {} '@sqlite.org/sqlite-wasm@3.48.0-build4': {} @@ -3589,6 +3656,21 @@ snapshots: negotiator@1.0.0: {} + nostr-tools@2.12.0(typescript@5.8.3): + dependencies: + '@noble/ciphers': 0.5.3 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.1 + '@scure/base': 1.1.1 + '@scure/bip32': 1.3.1 + '@scure/bip39': 1.2.1 + optionalDependencies: + nostr-wasm: 0.1.0 + typescript: 5.8.3 + + nostr-wasm@0.1.0: + optional: true + nwsapi@2.2.20: {} object-assign@4.1.1: {} diff --git a/src/app.css b/src/app.css index 64d0745..5f0947d 100644 --- a/src/app.css +++ b/src/app.css @@ -1,3 +1,5 @@ +@source "./safelist.txt"; + @import 'tailwindcss'; @plugin "daisyui" { } diff --git a/src/lib/clockControl.ts b/src/lib/clockControl.ts index 29a965e..c15678f 100644 --- a/src/lib/clockControl.ts +++ b/src/lib/clockControl.ts @@ -109,3 +109,99 @@ export const toggleDoNotDisturb = (currentStatus: boolean) => (e: Event) => { fetch(`${baseUrl}/api/dnd/disable`); } }; + +/** + * Uploads a file to a specified endpoint with progress, success and error callbacks + * @param file The file to upload + * @param endpoint The endpoint to upload to + * @param callbacks Optional callbacks for various events during upload + * @returns A promise that resolves when the upload is complete or fails + */ +export const uploadFile = async ( + file: File | null, + endpoint: string, + callbacks?: { + onProgress?: (progress: number) => void; + onSuccess?: () => void; + onError?: (error?: Error | string | unknown) => void; + } +) => { + if (!file) return; + + const formData = new FormData(); + formData.append('file', file); + + try { + const xhr = new XMLHttpRequest(); + xhr.open('POST', endpoint); + + xhr.upload.onprogress = (event: ProgressEvent) => { + if (event.lengthComputable) { + const progress = Math.round((event.loaded * 100) / event.total); + callbacks?.onProgress?.(progress); + } + }; + + xhr.onload = () => { + if (xhr.status === 200 && xhr.responseText !== 'FAIL') { + callbacks?.onSuccess?.(); + } else { + callbacks?.onError?.(xhr.responseText); + } + }; + + xhr.onerror = () => { + callbacks?.onError?.('Network error'); + }; + + xhr.send(formData); + } catch (error) { + callbacks?.onError?.(error); + console.error(error); + } +}; + +export const uploadFirmwareFile = ( + firmwareUploadFile: File, + callbacks?: { + onProgress?: (progress: number) => void; + onSuccess?: () => void; + onError?: (error?: Error | string | unknown) => void; + } +) => { + uploadFile(firmwareUploadFile, `${baseUrl}/upload/firmware`, callbacks); +}; + +export const uploadWebUiFile = ( + firmwareWebUiFile: File, + callbacks?: { + onProgress?: (progress: number) => void; + onSuccess?: () => void; + onError?: (error?: Error | string | unknown) => void; + } +) => { + uploadFile(firmwareWebUiFile, `${baseUrl}/upload/webui`, callbacks); +}; + +/** + * Automatically updates the firmware + */ +export const autoUpdate = async (callbacks?: { + onSuccess?: (message: string) => void; + onError?: (error?: Error | string | unknown) => void; +}) => { + try { + const response = await fetch(`${baseUrl}/api/firmware/auto_update`); + const data = await response.json(); + const message = data.msg; + + if (!response.ok) { + callbacks?.onError?.(message); + } else { + callbacks?.onSuccess?.(message); + } + } catch (error) { + callbacks?.onError?.(error); + console.error('Error during auto-update:', error); + } +}; diff --git a/src/lib/components/BTClock.svelte b/src/lib/components/BTClock.svelte index 2a25bb8..c1a8951 100644 --- a/src/lib/components/BTClock.svelte +++ b/src/lib/components/BTClock.svelte @@ -1,7 +1,4 @@ + +
+ + +
+ + + {#if unit} + {unit} + {/if} +
+
diff --git a/src/lib/components/form/Radio.svelte b/src/lib/components/form/Radio.svelte new file mode 100644 index 0000000..7ba8842 --- /dev/null +++ b/src/lib/components/form/Radio.svelte @@ -0,0 +1,67 @@ + + +
+ + {#if label} + + {/if} +
diff --git a/src/lib/components/form/RadioGroup.svelte b/src/lib/components/form/RadioGroup.svelte new file mode 100644 index 0000000..66651a7 --- /dev/null +++ b/src/lib/components/form/RadioGroup.svelte @@ -0,0 +1,76 @@ + + +
+ {#if legendLabel} + {legendLabel} + {/if} + + {#if description} +

{description}

+ {/if} + +
+ {#each options as option} + + {/each} +
+
diff --git a/src/lib/components/form/RangeSlider.svelte b/src/lib/components/form/RangeSlider.svelte new file mode 100644 index 0000000..c427b16 --- /dev/null +++ b/src/lib/components/form/RangeSlider.svelte @@ -0,0 +1,134 @@ + + +
+
+ + + {#if showValue && !showBubble} +  ({formattedValue()}) + {/if} +
+ +
+ + + {#if showBubble} +
+ {formattedValue()} +
+ {/if} + + {#if showTicks} +
+ {#each getTickPositions() as { value: tickValue, percent }} +
+
+ {tickValue} +
+
+ {/each} +
+ {/if} +
+
diff --git a/src/lib/components/form/Select.svelte b/src/lib/components/form/Select.svelte new file mode 100644 index 0000000..fd69bd9 --- /dev/null +++ b/src/lib/components/form/Select.svelte @@ -0,0 +1,90 @@ + + +
+ {#if label} + + {/if} + + +
diff --git a/src/lib/components/form/SettingsInputField.svelte b/src/lib/components/form/SettingsInputField.svelte new file mode 100644 index 0000000..c968d00 --- /dev/null +++ b/src/lib/components/form/SettingsInputField.svelte @@ -0,0 +1,70 @@ + + +
+ {#if label} + + {/if} + + +
diff --git a/src/lib/components/form/TimezoneSelector.svelte b/src/lib/components/form/TimezoneSelector.svelte new file mode 100644 index 0000000..1f8cddd --- /dev/null +++ b/src/lib/components/form/TimezoneSelector.svelte @@ -0,0 +1,51 @@ + + +
+ +
+
+ + +
+
+ {#if helpText} +

{helpText}

+ {/if} +
diff --git a/src/lib/components/form/Toggle.svelte b/src/lib/components/form/Toggle.svelte index 187377c..0625e30 100644 --- a/src/lib/components/form/Toggle.svelte +++ b/src/lib/components/form/Toggle.svelte @@ -1,10 +1,42 @@ -