From 0d7a163cfcc8f6ecb590ea4860de086522aaf61d Mon Sep 17 00:00:00 2001 From: Andres Cera Date: Wed, 12 Mar 2025 11:41:08 -0500 Subject: [PATCH 1/6] use forms on modem/configurator --- package-lock.json | 105 ++++- package.json | 1 + src/main/shared/ModemConfigurator.svelte | 524 ++++++++++++++++------- 3 files changed, 468 insertions(+), 162 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9309d37a..3fe9a35b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@macfja/svelte-persistent-store": "^2.4.2", "@sveltejs/enhanced-img": "^0.4.1", + "@sveltejs/kit": "^2.19.0", "lodash-es": "^4.17.21", "lodash.template": "^4.5.0", "lucide-svelte": "^0.456.0", @@ -1426,6 +1427,12 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "license": "MIT" + }, "node_modules/@poppinss/macroable": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@poppinss/macroable/-/macroable-1.0.4.tgz", @@ -1814,6 +1821,36 @@ "vite": ">= 5.0.0" } }, + "node_modules/@sveltejs/kit": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.19.0.tgz", + "integrity": "sha512-UTx28Ad4sYsLU//gqkEo5aFOPFBRT2uXCmXTsURqhurDCvzkVwXruJgBcHDaMiK6RKKpYRteDUaXYqZyGPgCXQ==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "import-meta-resolve": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0" + } + }, "node_modules/@sveltejs/vite-plugin-svelte": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", @@ -1920,6 +1957,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2918,6 +2961,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3123,7 +3175,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", - "dev": true, "license": "MIT" }, "node_modules/didyoumean": { @@ -3828,7 +3879,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, "license": "MIT" }, "node_modules/esniff": { @@ -4576,6 +4626,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5169,7 +5229,6 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5450,12 +5509,20 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6440,7 +6507,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, "license": "MIT", "dependencies": { "mri": "^1.1.0" @@ -6516,6 +6582,12 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6725,6 +6797,20 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/sjcl-bit-array": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/sjcl-bit-array/-/sjcl-bit-array-1.0.0.tgz", @@ -7614,6 +7700,15 @@ "license": "MIT", "optional": true }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", diff --git a/package.json b/package.json index adb69dbc..bac3aae1 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "dependencies": { "@macfja/svelte-persistent-store": "^2.4.2", "@sveltejs/enhanced-img": "^0.4.1", + "@sveltejs/kit": "^2.19.0", "lodash-es": "^4.17.21", "lodash.template": "^4.5.0", "lucide-svelte": "^0.456.0", diff --git a/src/main/shared/ModemConfigurator.svelte b/src/main/shared/ModemConfigurator.svelte index 62721bb1..a51d5d0d 100644 --- a/src/main/shared/ModemConfigurator.svelte +++ b/src/main/shared/ModemConfigurator.svelte @@ -5,7 +5,6 @@ import type { Modem, ModemNetworkType } from '$lib/types/socket-messages'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; - import * as Select from '$lib/components/ui/select'; import { Toggle } from '$lib/components/ui/toggle'; import { changeModemSettings, renameSupportedModemNetwork, scanModemNetworks } from '$lib/helpers/NetworkHelper'; @@ -16,186 +15,397 @@ let { deviceId, modem, modemIsScanning } = $props<{ modem: Modem; modemIsScanning: boolean; }>(); + const getSelectedNetwork: () => { value: ModemNetworkType; label: string } = () => { return { value: modem.network_type.active, label: renameSupportedModemNetwork(modem.network_type.active) }; }; const defaultRoamingNetwork = { value: '', label: 'network.modem.automaticRoamingNetwork' }; -const getCurrentModemConfig = () => { - return { - selectedNetwork: getSelectedNetwork(), - autoconfig: modem.config?.autoconfig || false, - apn: modem.config?.apn || '', - username: modem.config?.username || '', - password: modem.config?.password || '', - roaming: modem.config?.roaming || '', - network: - modem.config?.network === '' - ? defaultRoamingNetwork - : { value: modem.config?.network, label: modem.available_networks[modem.config.network].name }, - }; -}; -const saveModemConfig = () => { - if (!modemProperties.roaming) { - modemProperties.network = defaultRoamingNetwork; +// Function to get current modem config - this returns a fresh object each time +const getModemConfig = () => ({ + selectedNetwork: getSelectedNetwork(), + autoconfig: modem.config?.autoconfig || false, + apn: modem.config?.apn || '', + username: modem.config?.username || '', + password: modem.config?.password || '', + roaming: Boolean(modem.config?.roaming), + network: + modem.config?.network === '' + ? defaultRoamingNetwork + : { value: modem.config?.network, label: modem.available_networks[modem.config.network]?.name || '' }, +}); + +// Form state +let formData = $state(getModemConfig()); // Current form state +let savedValues = $state(getModemConfig()); // Last saved values for comparison +let errors = $state>({}); +let localScanningState = $state(false); // Track local scanning state +let justSubmitted = $state(false); // Flag to prevent updates right after form submission + +// Watch for modem changes and update formData if it hasn't been modified +// This ensures the form stays in sync with modem data from parent component +$effect(() => { + // This will execute whenever modem changes (modem is used in getModemConfig) + // Skip the update if we just submitted the form to prevent values from flickering + if (justSubmitted) { + // Reset the flag after a short delay to allow for future updates + setTimeout(() => { + justSubmitted = false; + }, 1000); + return; } - changeModemSettings({ - device: deviceId, - apn: modemProperties.apn, - username: modemProperties.username, - network_type: modemProperties.selectedNetwork.value, - password: modemProperties.password, - autoconfig: modemProperties.autoconfig, - roaming: modemProperties.roaming, - network: modemProperties.network.value, - }); -}; -let modemProperties = $state(getCurrentModemConfig()); -// eslint-disable-next-line unused-imports/no-unused-vars -const resetHotSpotProperties = () => { - modemProperties = getCurrentModemConfig(); -}; + // Only update form if user hasn't made changes + if (!isFormChanged()) { + formData = getModemConfig(); + savedValues = getModemConfig(); + } +}); + +// Validate form data +function validateForm() { + errors = {}; + + if (!formData.selectedNetwork.value) { + errors.selectedNetwork = 'Network type is required'; + } + + if (!formData.autoconfig && !formData.apn) { + errors.apn = 'APN is required when auto-configuration is disabled'; + } + + return Object.keys(errors).length === 0; +} + +// Check if form data has changed compared to saved values +function isFormChanged() { + // We need to compare each property individually to handle Select components properly + // Simple properties + if (formData.autoconfig !== savedValues.autoconfig) return true; + if (formData.apn !== savedValues.apn) return true; + if (formData.username !== savedValues.username) return true; + if (formData.password !== savedValues.password) return true; + if (formData.roaming !== savedValues.roaming) return true; + + // For object properties, compare only the values that matter + // For selectedNetwork, compare the network type value + if (formData.selectedNetwork.value !== savedValues.selectedNetwork.value) return true; + + // For network, compare the network value + if (formData.network.value !== savedValues.network.value) return true; + + // If we got here, nothing has changed + return false; +} + +// Form submission handler +function onSubmit(event: Event) { + // Prevent default form submission to avoid page refresh + event.preventDefault(); + + // Validate form + if (!validateForm()) { + return; + } + + // Set flag to prevent form from resetting due to parent component updates + justSubmitted = true; + + // Create copies of current form data to preserve it after submission + const currentNetwork = { ...formData.network }; + const currentSelectedNetwork = { ...formData.selectedNetwork }; + + // First, make sure the form data is consistent with roaming setting + if (!formData.roaming) { + // If roaming is off, use the default network for submission + const networkForSubmission = defaultRoamingNetwork; + + // Submit with consistent values + changeModemSettings({ + device: deviceId, + apn: formData.apn, + username: formData.username, + network_type: formData.selectedNetwork.value, + password: formData.password, + autoconfig: formData.autoconfig, + roaming: false, // explicitly set false to be clear + network: networkForSubmission.value, + }); + + // Update saved values without modifying current form state + savedValues = { + selectedNetwork: { + value: currentSelectedNetwork.value, + label: currentSelectedNetwork.label, + }, + autoconfig: formData.autoconfig, + apn: formData.apn, + username: formData.username, + password: formData.password, + roaming: false, + network: { + value: networkForSubmission.value, + label: networkForSubmission.label, + }, + }; + + // Note: We're NOT updating formData here to prevent UI reset + } else { + // If roaming is on, use selected network + + // Submit with current values + changeModemSettings({ + device: deviceId, + apn: formData.apn, + username: formData.username, + network_type: formData.selectedNetwork.value, + password: formData.password, + autoconfig: formData.autoconfig, + roaming: true, // explicitly set true to be clear + network: formData.network.value, + }); + + // Update saved values with deep copies to avoid reference issues + // But maintain the original objects to prevent UI reset + savedValues = { + selectedNetwork: { + value: currentSelectedNetwork.value, + label: currentSelectedNetwork.label, + }, + autoconfig: formData.autoconfig, + apn: formData.apn, + username: formData.username, + password: formData.password, + roaming: true, + network: { + value: currentNetwork.value, + label: currentNetwork.label, + }, + }; + } +} + +// Handle scanning networks with proper state management +function handleScanNetworks() { + // Set local scanning state immediately for responsive UI + localScanningState = true; + + // Set flag to prevent form reset while scanning + justSubmitted = true; -const checkChanges = () => { - const defaultValues = getCurrentModemConfig(); - return Object.entries(defaultValues).some(([key, value]) => { - if (key === 'selectedNetwork' || key === 'network') { - if (key === 'network' || !modemProperties.roaming) { - return false; - } - return value.value !== modemProperties[key].value; + // Call the scan function + scanModemNetworks(deviceId); + + // Update the UI to show scanning state before the server responds + // This improves the perceived responsiveness of the UI + setTimeout(() => { + if (!modemIsScanning) { + // If after a short delay, the modemIsScanning prop hasn't updated + // we can assume the request is in progress and keep showing scanning state locally } - return value !== modemProperties[key]; - }); -}; + }, 200); + + // Reset local scanning after a reasonable timeout if server doesn't respond + setTimeout(() => { + localScanningState = false; + // Allow form updates again after scan completes + justSubmitted = false; + }, 10000); +} + +// Reset form handler +function resetForm() { + // Reset form data to saved values + formData = { + selectedNetwork: { + value: savedValues.selectedNetwork.value, + label: savedValues.selectedNetwork.label, + }, + autoconfig: savedValues.autoconfig, + apn: savedValues.apn, + username: savedValues.username, + password: savedValues.password, + roaming: savedValues.roaming, + network: { + value: savedValues.network.value, + label: savedValues.network.label, + }, + }; + errors = {}; +}
-
- - { - if (val) modemProperties.selectedNetwork = val; - }}> - - - - - - {#each modem.network_type.supported as networkType} - {renameSupportedModemNetwork(networkType)} - {/each} - - - -
-
- - {#if modemProperties.roaming} - - {:else} - - {/if} - -
-

- {$_('network.modem.enableRoaming')} -

-
-
-
-
- +
+
+ { - if (val) modemProperties.network = val; + if (val) { + formData.selectedNetwork = { ...val }; // Create a new object to ensure reactivity + errors.selectedNetwork = undefined; + } }}> - - + + - + - - - {#if modem.available_networks} - {#each Object.entries(modem.available_networks) as [key, availableNetwork]} - {#if availableNetwork.availability === 'available'} - - {/if} - {/each} - {/if} + {#each modem.network_type.supported as networkType} + {renameSupportedModemNetwork(networkType)} + {/each} -
- -
-
-
- -
- - {#if modemProperties.autoconfig} - - {:else} - + {#if errors.selectedNetwork} +

{errors.selectedNetwork}

{/if} -
- -
-

- {$_('network.modem.autoapn')} -

-
- {#if !modemProperties.autoconfig} -
- - -
-
- - + +
+ (formData.roaming = val)}> + {#if formData.roaming} + + {:else} + + {/if} + +
+

+ {$_('network.modem.enableRoaming')} +

+
-
- - + + {#if formData.roaming} +
+ + + +
+ +
+ { + if (val) formData.network = { ...val }; // Create a new object to ensure reactivity + }}> + + + + + + + + + {#if modem.available_networks} + {#each Object.entries(modem.available_networks) as [key, availableNetwork]} + {#if availableNetwork.availability === 'available'} + + {/if} + {/each} + {/if} + + + +
+ + +
+ +
+
+
+ {/if} + +
+ (formData.autoconfig = val)}> + {#if formData.autoconfig} + + {:else} + + {/if} + +
+

+ {$_('network.modem.autoapn')} +

+
- {/if} - + {#if !formData.autoconfig} +
+ + (errors.apn = undefined)} /> + {#if errors.apn} +

{errors.apn}

+ {/if} +
+ +
+ + +
+ +
+ + +
+ {/if} + +
+ + +
+
From 58047c4d184c96312c8c2b5c84a86d29d5457d03 Mon Sep 17 00:00:00 2001 From: Andres Cera Date: Wed, 12 Mar 2025 14:31:34 -0500 Subject: [PATCH 2/6] improve the way forms are currently managed and improve reactivity --- src/lib/helpers/PipelineHelper.ts | 26 +- src/locale/ar.json | 2 + src/locale/de.json | 2 + src/locale/en.json | 2 + src/locale/es.json | 2 + src/locale/fr.json | 2 + src/locale/hi.json | 2 + src/locale/ja.json | 2 + src/locale/ko.json | 2 + src/locale/pt-BR.json | 2 + src/locale/zh.json | 2 + src/main/Layout.svelte | 251 +++++++- src/main/Tabs/Settings.svelte | 738 +++++++++++++++-------- src/main/shared/ModemConfigurator.svelte | 258 ++++---- 14 files changed, 907 insertions(+), 386 deletions(-) diff --git a/src/lib/helpers/PipelineHelper.ts b/src/lib/helpers/PipelineHelper.ts index 7b710199..4d4948b6 100644 --- a/src/lib/helpers/PipelineHelper.ts +++ b/src/lib/helpers/PipelineHelper.ts @@ -27,17 +27,31 @@ export type GroupedPipelines = { }; export function parsePipelineName(name: string): PipelineInfo { + // Basic device extraction const deviceMatch = name.match(/^([^/]+)/); + + // Extract encoder (h264 or h265) const encoderMatch = name.match(/(h264|h265)/); - - const formatMatch = name.match(/(?:h264|h265)_([^_]+(?:_[^_\d]+)*)/); + + // Format extraction - comes after h264/h265_ prefix + const formatMatch = name.match(/(?:h264|h265)_([^_]+)/); + + // Extract resolution - typically NNNp format (like 720p, 1080p) const resolutionMatch = name.match(/(\d{3,4}p)/); - const fpsMatch = name.match(/(\d+(?:\.\d+)?)(?:fps)?$/); - + + // Extract framerate - typically pNN format (like p30, p60) + // Handle both underscore separated and inline formats + const fpsMatch = name.match(/p(\d+(?:\.\d+)?)/); + + // Special case for libuvch264 + const isLibUVC = name.includes('libuvch264'); + return { device: deviceMatch ? deviceMatch[0] : null, encoder: encoderMatch ? encoderMatch[0] : null, - format: formatMatch ? formatMatch[1].replace(/_/g, ' ').replace('libuvch264', 'USB-libuvch264') : null, + format: formatMatch + ? (isLibUVC ? 'usb-libuvch264' : formatMatch[1].replace(/_/g, ' ')) + : null, resolution: resolutionMatch ? resolutionMatch[0] : '[Match device resolution]', fps: fpsMatch ? parseFloat(fpsMatch[1]) : '[Match device output]', }; @@ -77,4 +91,4 @@ export const groupPipelinesByDeviceAndFormat = (pipelines: PipelinesMessage): Gr }); return groupedPipelines; -}; +}; \ No newline at end of file diff --git a/src/locale/ar.json b/src/locale/ar.json index 239665a5..a9984d66 100644 --- a/src/locale/ar.json +++ b/src/locale/ar.json @@ -46,6 +46,7 @@ "settings": { "encoderSettings": "إعدادات المرمز", "inputMode": "وضع الإدخال", + "djiCameraMessage": "قد تعمل كاميرات DJI بشكل أفضل باستخدام وضع الإدخال USB-LIBUVCH264", "selectInputMode": "اختر وضع الإدخال", "selectEncodingOutputFormat": "اختر ترميز الإخراج", "selectEncodingFormat": "حدد تنسيق إخراج الترميز", @@ -108,6 +109,7 @@ "enabled": "مفعّل", "connected": "متصل", "disconnected": "غير متصل", + "disconnecting": "جارِ الانفصال", "connecting": "جاري الاتصال", "scanning": "جارٍ المسح" } diff --git a/src/locale/de.json b/src/locale/de.json index 62a7a47f..37d52bef 100644 --- a/src/locale/de.json +++ b/src/locale/de.json @@ -46,6 +46,7 @@ "settings": { "encoderSettings": "Encoder-Einstellungen", "inputMode": "Eingabemodus", + "djiCameraMessage": "DJI-Kameras funktionieren möglicherweise besser im USB-LIBUVCH264-Eingabemodus", "selectInputMode": "Wähle den Eingabemodus", "selectEncodingOutputFormat": "Wähle den Ausgabe-Codec", "selectEncodingFormat": "Kodierungsausgabeformat auswählen", @@ -108,6 +109,7 @@ "enabled": "Aktiviert", "connected": "Verbunden", "disconnected": "Getrennt", + "disconnecting": "Trennen", "connecting": "Verbinde", "scanning": "Wird gescannt" } diff --git a/src/locale/en.json b/src/locale/en.json index f3bb3389..bf62745b 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -46,6 +46,7 @@ "settings": { "encoderSettings": "Encoder Settings", "inputMode": "Input Mode", + "djiCameraMessage": "DJI cameras may work better using the USB-LIBUVCH264 input mode", "selectInputMode": "Select the input mode", "selectEncodingOutputFormat": "Select output codec", "encodingFormat": "Encoding format", @@ -108,6 +109,7 @@ "enabled": "Enabled", "connected": "Connected", "disconnected": "Disconnected", + "disconnecting": "Disconnecting", "connecting": "Connecting", "scanning": "Scanning" diff --git a/src/locale/es.json b/src/locale/es.json index e5ce3c32..73d51c0d 100644 --- a/src/locale/es.json +++ b/src/locale/es.json @@ -45,6 +45,7 @@ "settings": { "encoderSettings": "Configuración del codificador", "inputMode": "Modo de entrada", + "djiCameraMessage": "Las cámaras DJI suelen funcionar mejor usando el modo de entrada USB-LIBUVCH264", "selectInputMode": "Seleccione el modo de Entrada", "selectEncodingOutputFormat": "Seleccione un formato de salida", "encodingFormat": "Formato de codificación", @@ -107,6 +108,7 @@ "connected": "Conectado", "enabled": "Habilitada", "disconnected": "Desconectado", + "disconnecting": "Desconectándose", "connecting": "Conectando", "scanning": "Buscando" } diff --git a/src/locale/fr.json b/src/locale/fr.json index 02db00c5..79ccf2e7 100644 --- a/src/locale/fr.json +++ b/src/locale/fr.json @@ -48,6 +48,7 @@ "settings": { "encoderSettings": "Paramètres de l'encodeur", "inputMode": "Mode d'entrée", + "djiCameraMessage": "Les caméras DJI peuvent mieux fonctionner en utilisant le mode d’entrée USB-LIBUVCH264", "selectInputMode": "Sélectionnez le mode d'entrée", "selectEncodingOutputFormat": "Sélectionnez le codec de sortie", "encodingFormat": "Format de codage", @@ -108,6 +109,7 @@ "registered": "Enregistré", "connected": "Connecté", "disconnected": "Déconnecté", + "disconnecting": "Déconnexion en cours", "connecting": "Connexion en cours", "scanning": "Scan en cours" } diff --git a/src/locale/hi.json b/src/locale/hi.json index 18bbcc84..2a075ba5 100644 --- a/src/locale/hi.json +++ b/src/locale/hi.json @@ -47,6 +47,7 @@ "encoderSettings": "एनकोडर सेटिंग्स", "inputMode": "इनपुट मोड", "selectInputMode": "इनपुट मोड चुनें", + "djiCameraMessage": "DJI कैमरे USB-LIBUVCH264 इनपुट मोड का उपयोग करके बेहतर काम कर सकते हैं", "selectEncodingOutputFormat": "आउटपुट कोडेक चुनें", "selectEncodingFormat": "एनकोडिंग आउटपुट फॉर्मेट चुनें", "encodingResolution": "एनकोडिंग रिज़ॉल्यूशन", @@ -108,6 +109,7 @@ "enabled": "सक्षम", "connected": "जुड़ा हुआ", "disconnected": "डिस्कनेक्ट किया गया", + "disconnecting": "डिसकनेक्ट हो रहा है", "connecting": "कनेक्ट हो रहा है", "scanning": "स्कैनिंग" } diff --git a/src/locale/ja.json b/src/locale/ja.json index 36385226..34acfa8a 100644 --- a/src/locale/ja.json +++ b/src/locale/ja.json @@ -46,6 +46,7 @@ "settings": { "encoderSettings": "エンコーダー設定", "inputMode": "入力モード", + "djiCameraMessage": "DJIカメラはUSB-LIBUVCH264入力モードを使用するとより良く動作する可能性があります", "selectInputMode": "入力モードを選択", "selectEncodingOutputFormat": "出力コーデックを選択", "selectEncodingFormat": "エンコーディング出力フォーマットを選択", @@ -108,6 +109,7 @@ "enabled": "有効", "connected": "接続済み", "disconnected": "切断済み", + "disconnecting": "切断中", "connecting": "接続中", "scanning": "スキャン中" } diff --git a/src/locale/ko.json b/src/locale/ko.json index 84e7f78f..66086338 100644 --- a/src/locale/ko.json +++ b/src/locale/ko.json @@ -46,6 +46,7 @@ "settings": { "encoderSettings": "인코더 설정", "inputMode": "입력 모드", + "djiCameraMessage": "DJI 카메라는 USB-LIBUVCH264 입력 모드를 사용하면 더 잘 작동할 수 있습니다", "selectInputMode": "입력 모드를 선택", "selectEncodingOutputFormat": "출력 코덱을 선택", "selectEncodingFormat": "인코딩 출력 형식 선택", @@ -108,6 +109,7 @@ "enabled": "활성화됨", "connected": "연결됨", "disconnected": "연결 해제됨", + "disconnecting": "연결 해제 중", "connecting": "연결 중", "scanning": "스캔 중" } diff --git a/src/locale/pt-BR.json b/src/locale/pt-BR.json index 52168f4f..6b5b0b9b 100644 --- a/src/locale/pt-BR.json +++ b/src/locale/pt-BR.json @@ -48,6 +48,7 @@ "settings": { "encoderSettings": "Configurações do Codificador", "inputMode": "Modo de Entrada", + "djiCameraMessage": "As câmeras DJI podem funcionar melhor usando o modo de entrada USB-LIBUVCH264", "selectInputMode": "Selecione o modo de entrada", "selectEncodingOutputFormat": "Selecione o codec de saída", "selectEncodingFormat": "Selecione o formato de saída de codificação", @@ -107,6 +108,7 @@ "registered": "Registrado", "connected": "Conectado", "disconnected": "Desconectado", + "disconnecting": "Desconectando", "connecting": "Conectando", "scanning": "Escaneando" diff --git a/src/locale/zh.json b/src/locale/zh.json index 7f76603d..ddac9333 100644 --- a/src/locale/zh.json +++ b/src/locale/zh.json @@ -46,6 +46,7 @@ "settings": { "encoderSettings": "编码器设置", "inputMode": "输入模式", + "djiCameraMessage": "DJI 相机在使用 USB-LIBUVCH264 输入模式时可能表现更好", "selectInputMode": "选择输入模式", "selectEncodingOutputFormat": "选择输出编解码器", "selectEncodingFormat": "选择编码输出格式", @@ -108,6 +109,7 @@ "enabled": "已启用", "connected": "已连接", "disconnected": "已断开", + "disconnecting": "正在断开", "connecting": "连接中", "scanning": "扫描中" diff --git a/src/main/Layout.svelte b/src/main/Layout.svelte index e03960ff..0048ef05 100644 --- a/src/main/Layout.svelte +++ b/src/main/Layout.svelte @@ -8,11 +8,196 @@ import { Toaster } from '$lib/components/ui/sonner'; import UpdatingOverlay from '$lib/components/updating-overlay.svelte'; import { authStatusStore } from '$lib/stores/auth-status'; import { AuthMessages, NotificationsMessages, StatusMessages, sendAuthMessage } from '$lib/stores/websocket-store'; +import { startStreaming as startStreamingFn, stopStreaming as stopStreamingFn } from '$lib/helpers/SystemHelper'; let authStatus = $state(false); let isCheckingAuthStatus = $state(true); let updatingStatus: StatusMessage['updating'] = $state(false); const setupLocaleResult = setupLocale(); + +// Toast tracking system for duplicates +interface ToastInfo { + id: string; + timestamp: number; + duration: number; + notificationKey: string; // Unique key for identifying similar notifications +} +let activeToasts = $state>({}); +// Simple flag to prevent recursive updates +let isUpdatingToasts = false; + +// Function to dismiss all non-persistent toasts +const dismissAllNonPersistentToasts = () => { + // Guard against recursion + if (isUpdatingToasts) return; + + try { + isUpdatingToasts = true; + + // Use Sonner's built-in dismissAll method first which is more reliable + toast.dismiss(); + + // Reset tracking state safely + setTimeout(() => { + activeToasts = {}; + }, 0); + } finally { + isUpdatingToasts = false; + } +}; + +// Override original SystemHelper functions to add toast clearing +const startStreaming = (config: { [key: string]: string | number }) => { + // Guard against infinite updates + if (isUpdatingToasts) { + startStreamingFn(config); + return; + } + + try { + isUpdatingToasts = true; + + // Force clear all toasts completely + toast.dismiss(); + + // Clear all persistent notification timers + Object.values(persistentNotificationTimers).forEach(timer => { + clearTimeout(timer); + }); + + // Reset tracking states safely using setTimeout to avoid reactive updates + setTimeout(() => { + activeToasts = {}; + persistentNotificationTimers = {}; + }, 0); + + // Now call the original function + startStreamingFn(config); + } finally { + // Ensure flag is reset + isUpdatingToasts = false; + } +}; + +const stopStreaming = () => { + // Guard against infinite updates + if (isUpdatingToasts) { + stopStreamingFn(); + return; + } + + try { + isUpdatingToasts = true; + + // Force clear all toasts completely + toast.dismiss(); + + // Clear all persistent notification timers + Object.values(persistentNotificationTimers).forEach(timer => { + clearTimeout(timer); + }); + + // Reset tracking states safely using setTimeout to avoid reactive updates + setTimeout(() => { + activeToasts = {}; + persistentNotificationTimers = {}; + }, 0); + + // Now call the original function + stopStreamingFn(); + } finally { + // Ensure flag is reset + isUpdatingToasts = false; + } +}; + +// Show a toast, extending duration if a duplicate exists +const showToast = (type: NotificationType, name: string, options: any) => { + // Prevent recursive calls that could cause infinite loops + if (isUpdatingToasts) return; + + try { + isUpdatingToasts = true; + + // Generate a message-only key to identify toasts with the same content + const messageKey = options.description; + const now = Date.now(); + + // For persistent notifications, don't create duplicates + if (options.isPersistent) { + // Check if we already have this persistent notification + const existingPersistentToastEntries = Object.entries(activeToasts).filter(([_, toast]) => + toast.notificationKey === messageKey && toast.duration === Infinity + ); + + if (existingPersistentToastEntries.length > 0) { + // We already have this persistent notification showing + // The timer has already been reset in the subscription, so just skip creating a duplicate + return; + } + } + + // Create a unique ID for this toast + const id = `toast-${now}-${Math.random().toString(36).substr(2, 9)}`; + options.id = id; + + // Simplified onDismiss handler + const originalOnDismiss = options.onDismiss; + options.onDismiss = () => { + // Call original onDismiss if it exists + if (originalOnDismiss) originalOnDismiss(); + + // Safely update our tracking + if (activeToasts[id]) { + setTimeout(() => { + const newActiveToasts = { ...activeToasts }; + delete newActiveToasts[id]; + activeToasts = newActiveToasts; + }, 0); + } + }; + + // Display the toast + toast[type](name, options); + + // Track this toast + const toastInfo = { + id, + timestamp: now, + duration: options.duration, + notificationKey: messageKey + }; + + // Use a non-reactive way to update activeToasts to avoid triggering effects + setTimeout(() => { + activeToasts = { ...activeToasts, [id]: toastInfo }; + }, 0); + + // Clean up the toast tracking after it expires (except for persistent toasts) + if (options.duration !== Infinity) { + setTimeout(() => { + try { + // Safely dismiss and remove tracking + toast.dismiss(id); + + setTimeout(() => { + if (activeToasts[id]) { + const newActiveToasts = { ...activeToasts }; + delete newActiveToasts[id]; + activeToasts = newActiveToasts; + } + }, 0); + } catch (e) { + console.error('Error cleaning up toast:', e); + } + }, options.duration + 1000); // Add 1 second buffer + } + } finally { + // Always make sure we reset the flag + isUpdatingToasts = false; + } +}; + StatusMessages.subscribe(status => { updatingStatus = status?.updating && typeof status.updating !== 'boolean' && status.updating.result !== 0; }); @@ -28,9 +213,10 @@ if (auth) { AuthMessages.subscribe(message => { if (message?.success) { isCheckingAuthStatus = false; - toast.success('AUTH', { + showToast('success', 'AUTH', { duration: 5000, description: 'Successfully authenticated', + dismissable: true }); authStatusStore.set(true); } @@ -39,15 +225,76 @@ AuthMessages.subscribe(message => { authStatus = status; }); }); + +// Time after which we'll automatically clear a persistent notification if no new updates arrive +const PERSISTENT_AUTO_CLEAR_TIMEOUT = 5000; // 5 seconds + +// Track timers for auto-clearing persistent notifications +let persistentNotificationTimers = $state>({}); + NotificationsMessages.subscribe(notifications => { notifications?.show?.forEach(notification => { - toast[notification.type as NotificationType](notification.name.toUpperCase(), { + const toastKey = `${notification.type}-${notification.msg}`; + + // If this is a persistent notification, reset/create its auto-clear timer + if (notification.is_persistent) { + // Clear any existing timer for this notification + if (persistentNotificationTimers[toastKey]) { + clearTimeout(persistentNotificationTimers[toastKey]); + } + + // Set a new timer to auto-clear this notification if no new updates arrive + const timerId = window.setTimeout(() => { + // Find any toasts with this key + Object.entries(activeToasts).forEach(([id, toast]) => { + if (toast.notificationKey === notification.msg && toast.duration === Infinity) { + // Auto-clear this toast since no new updates have arrived + toast.dismiss(toast.id); + + // Update our tracking + setTimeout(() => { + const newActiveToasts = { ...activeToasts }; + delete newActiveToasts[id]; + activeToasts = newActiveToasts; + }, 0); + } + }); + + // Remove this timer from tracking + setTimeout(() => { + const newTimers = { ...persistentNotificationTimers }; + delete newTimers[toastKey]; + persistentNotificationTimers = newTimers; + }, 0); + }, PERSISTENT_AUTO_CLEAR_TIMEOUT); + + // Update the timers object + const newTimers = { ...persistentNotificationTimers }; + newTimers[toastKey] = timerId; + persistentNotificationTimers = newTimers; + } + + // Show the toast + showToast(notification.type as NotificationType, notification.name.toUpperCase(), { description: notification.msg, duration: notification.is_persistent ? Infinity : notification.duration * 2500, dismissable: !notification.is_dismissable, + isPersistent: notification.is_persistent }); }); }); + +// TypeScript interface for global window object +declare global { + interface Window { + startStreamingWithNotificationClear: typeof startStreaming; + stopStreamingWithNotificationClear: typeof stopStreaming; + } +} + +// Export our functions to the global scope to make them available to other components +window.startStreamingWithNotificationClear = startStreaming; +window.stopStreamingWithNotificationClear = stopStreaming; {#await setupLocaleResult} diff --git a/src/main/Tabs/Settings.svelte b/src/main/Tabs/Settings.svelte index 0b8de7db..84db87b1 100644 --- a/src/main/Tabs/Settings.svelte +++ b/src/main/Tabs/Settings.svelte @@ -1,6 +1,7 @@
- {#if isStreaming} - - {:else} - - {/if} +
+ {#if isStreaming} + + {:else} + + {/if} +
@@ -271,131 +376,193 @@ const startStreamingWithCurrentConfig = () => { - -
- - { - selectedResolution = { value: undefined, label: undefined }; - selectedFramerate = { value: undefined, label: undefined }; - selectedInputMode = value; - }}> - - - - - - {#if groupedPipelines} - {#each Object.entries(groupedPipelines) as [pipelineKey, _]} - {@const label = pipelineKey.toUpperCase().split(' ')[0]} - - {/each} - {/if} - - - -
+
+ +
+ + { + selectedEncoder = { value: undefined, label: undefined }; + selectedResolution = { value: undefined, label: undefined }; + selectedFramerate = { value: undefined, label: undefined }; + selectedInputMode = value; + + // Auto-select the next level if there's only one option + if (value) { + autoSelectNextOption('inputMode'); + } + }}> + + + + + + {#if groupedPipelines} + {#each Object.entries(groupedPipelines) as [pipelineKey, _]} + {@const label = pipelineKey.toUpperCase().split(' ')[0]} + + {/each} + {/if} + + + + {#if selectedInputMode?.value && selectedInputMode.value.toLowerCase().includes('usb')} +

+ {$_('settings.djiCameraMessage')} +

+ {/if} +
- -
- - (selectedEncoder = value)}> - - - - - - {#if selectedInputMode?.value && groupedPipelines?.[selectedInputMode.value]} - {#each Object.keys(groupedPipelines[selectedInputMode.value]) as encoder} - - {/each} - {/if} - - - -
+ +
+ + { + selectedEncoder = value; + selectedResolution = { value: undefined, label: undefined }; + selectedFramerate = { value: undefined, label: undefined }; + + // Auto-select the next level if there's only one option + if (value) { + autoSelectNextOption('encoder'); + } + }}> + + + + + + {#if selectedInputMode?.value && groupedPipelines?.[selectedInputMode.value]} + {#each Object.keys(groupedPipelines[selectedInputMode.value]) as encoder} + + {/each} + {/if} + + + +
- -
- - { - selectedFramerate = { value: undefined, label: undefined }; - selectedResolution = value; - }}> - - - - - - {#if selectedEncoder?.value && selectedInputMode?.value && groupedPipelines?.[selectedInputMode.value]?.[selectedEncoder.value]} - {@const resolutions = Object.keys(groupedPipelines[selectedInputMode.value][selectedEncoder.value])} - {#each resolutions as resolution} - - {/each} - {/if} - - - -
+ +
+ + { + selectedResolution = value; + selectedFramerate = { value: undefined, label: undefined }; + + // Auto-select the next level if there's only one option + if (value) { + autoSelectNextOption('resolution'); + } + }}> + + + + + + {#if selectedEncoder?.value && selectedInputMode?.value && groupedPipelines?.[selectedInputMode.value]?.[selectedEncoder.value]} + {@const resolutions = Object.keys( + groupedPipelines[selectedInputMode.value][selectedEncoder.value], + )} + {@const sortedResolutions = [...resolutions].sort((a, b) => { + // Put "match device resolution" or similar special options first + if (a.toLowerCase().includes('match') || a.toLowerCase().includes('device')) return -1; + if (b.toLowerCase().includes('match') || b.toLowerCase().includes('device')) return 1; + + // Extract numeric values (like "720" from "720p") + const numA = parseInt(a.match(/\d+/)?.[0] || '0', 10); + const numB = parseInt(b.match(/\d+/)?.[0] || '0', 10); + + // Sort by numeric value + return numA - numB; + })} + {#each sortedResolutions as resolution} + + {/each} + {/if} + + + +
- -
- - (selectedFramerate = value)}> - - - - - - {#if selectedEncoder?.value && selectedInputMode?.value && selectedResolution?.value && groupedPipelines?.[selectedInputMode.value]?.[selectedEncoder.value][selectedResolution.value]} - {@const framerates = - groupedPipelines[selectedInputMode.value][selectedEncoder.value][selectedResolution.value]} - {#each framerates as framerate} - - {/each} - {/if} - - - -
- - { - setTimeout(() => { - selectedBitrate = value[0]; - updateMaxBitrate(); - }); - }} /> - { - selectedBitrate = normalizeValue(selectedBitrate, 2000, 12000, 50); - updateMaxBitrate(); - }}> - {#if isStreaming} -

{$_('settings.changeBitrateNotice')}

+ +
+ + (selectedFramerate = value)}> + + + + + + {#if selectedEncoder?.value && selectedInputMode?.value && selectedResolution?.value && groupedPipelines?.[selectedInputMode.value]?.[selectedEncoder.value][selectedResolution.value]} + {@const framerates = + groupedPipelines[selectedInputMode.value][selectedEncoder.value][selectedResolution.value]} + {@const sortedFramerates = [...framerates].sort((a, b) => { + // Put "match device output" or similar special options first + const fpsA = a.extraction.fps; + const fpsB = b.extraction.fps; + + if (typeof fpsA === 'string' && fpsA.toLowerCase().includes('match')) return -1; + if (typeof fpsB === 'string' && fpsB.toLowerCase().includes('match')) return 1; + + // Convert to numbers for numeric comparison + const numA = typeof fpsA === 'number' ? fpsA : parseFloat(String(fpsA)) || 0; + const numB = typeof fpsB === 'number' ? fpsB : parseFloat(String(fpsB)) || 0; + + // Sort by numeric value + return numA - numB; + })} + {#each sortedFramerates as framerate} + + {/each} + {/if} + + + + + {#if formErrors.pipeline} +

{formErrors.pipeline}

{/if} + +
+ + { + setTimeout(() => { + selectedBitrate = value[0]; + updateMaxBitrate(); + }); + }} /> + { + selectedBitrate = normalizeValue(selectedBitrate, 2000, 12000, 50); + updateMaxBitrate(); + }}> + {#if isStreaming} +

{$_('settings.changeBitrateNotice')}

+ {/if} +
@@ -407,73 +574,86 @@ const startStreamingWithCurrentConfig = () => { - {#if audioCodecs && unparsedPipelines && selectedPipeline && unparsedPipelines[selectedPipeline].asrc} - - (selectedAudioSource = value)}> - - - - - - {#if audioSources} - {#each audioSources as audioSource} - - {/each} - {/if} - - - - {/if} - - {#if audioCodecs && unparsedPipelines && selectedPipeline && unparsedPipelines[selectedPipeline].acodec} - - (selectedAudioCodec = value)}> - - - - - - {#each Object.entries(audioCodecs) as [codec, label]} - - {/each} - - - -
- - (selectedAudioDelay = value[0])} - disabled={isStreaming} - max={2000} - min={-2000} - step={5}> - { - selectedAudioDelay = normalizeValue(selectedAudioDelay, 2000, 12000, 50); - }}> -
- {/if} - - {#if audioCodecs && unparsedPipelines && selectedPipeline && !unparsedPipelines[selectedPipeline].acodec && !unparsedPipelines[selectedPipeline].asrc} -

{$_('settings.noAudioSettingSupport')}

- {/if} - - {#if audioCodecs && unparsedPipelines && !selectedPipeline} -

{$_('settings.audioSettingsMessage')}

- {/if} +
+ {#if audioCodecs && unparsedPipelines && selectedPipeline && unparsedPipelines[selectedPipeline].asrc} +
+ + (selectedAudioSource = value)}> + + + + + + {#if audioSources} + {#each audioSources as audioSource} + + {/each} + {/if} + + + +
+ {/if} + + {#if audioCodecs && unparsedPipelines && selectedPipeline && unparsedPipelines[selectedPipeline].acodec} +
+ + (selectedAudioCodec = value)}> + + + + + + {#each Object.entries(audioCodecs) as [codec, label]} + + {/each} + + + +
+ + (selectedAudioDelay = value[0])} + disabled={isStreaming} + max={2000} + min={-2000} + step={5}> + { + selectedAudioDelay = normalizeValue(selectedAudioDelay, 2000, 12000, 50); + }}> +
+
+ {/if} + + {#if audioCodecs && unparsedPipelines && selectedPipeline && !unparsedPipelines[selectedPipeline].acodec && !unparsedPipelines[selectedPipeline].asrc} +
+

{$_('settings.noAudioSettingSupport')}

+
+ {/if} + + {#if audioCodecs && unparsedPipelines && !selectedPipeline} +
+

{$_('settings.audioSettingsMessage')}

+
+ {/if} +
@@ -483,73 +663,95 @@ const startStreamingWithCurrentConfig = () => { - - value && (selectedRelayServer = value)}> - - - - - - {$_('settings.manualConfiguration')} - {#if relayMessage?.servers} - {#each Object.entries(relayMessage?.servers) as [server, serverInfo]} - - {/each} - {/if} - - - - {#if selectedRelayServer?.value === '-1' || selectedRelayServer?.value === undefined} - - - {/if} - {#if selectedRelayServer?.value !== '-1' && selectedRelayServer?.value !== undefined} - - value && (selectedRelayAccount = value)}> - - - - - - {$_('settings.manualConfiguration')} - {#if relayMessage?.servers} - {#each Object.entries(relayMessage?.accounts) as [account, accountInfo]} - - {/each} - {/if} - - - - {/if} - {#if selectedRelayAccount?.value === '-1' || selectedRelayAccount?.value === undefined} - - - - - {/if} - {#if srtLatency !== undefined} - - (srtLatency = value[0])}> - { - srtLatency = normalizeValue(srtLatency, 2000, 12000, 50); - }}> - {/if} +
+
+ + value && (selectedRelayServer = value)}> + + + + + + {$_('settings.manualConfiguration')} + {#if relayMessage?.servers} + {#each Object.entries(relayMessage?.servers) as [server, serverInfo]} + + {/each} + {/if} + + + +
+ + {#if selectedRelayServer?.value === '-1' || selectedRelayServer?.value === undefined} +
+ + +
+ {/if} + + {#if selectedRelayServer?.value !== '-1' && selectedRelayServer?.value !== undefined} +
+ + value && (selectedRelayAccount = value)}> + + + + + + {$_('settings.manualConfiguration')} + {#if relayMessage?.servers} + {#each Object.entries(relayMessage?.accounts) as [account, accountInfo]} + + {/each} + {/if} + + + +
+ {/if} + + {#if selectedRelayAccount?.value === '-1' || selectedRelayAccount?.value === undefined} +
+ + +
+
+ + +
+ {/if} + + {#if srtLatency !== undefined} +
+ + (srtLatency = value[0])} + disabled={isStreaming}> + { + srtLatency = normalizeValue(srtLatency, 2000, 12000, 50); + }}> +
+ {/if} +
diff --git a/src/main/shared/ModemConfigurator.svelte b/src/main/shared/ModemConfigurator.svelte index a51d5d0d..ce023655 100644 --- a/src/main/shared/ModemConfigurator.svelte +++ b/src/main/shared/ModemConfigurator.svelte @@ -1,5 +1,6 @@ - - - - diff --git a/src/lib/components/icons/aria.svelte b/src/lib/components/icons/aria.svelte deleted file mode 100644 index e0727bbe..00000000 --- a/src/lib/components/icons/aria.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/src/lib/components/icons/github.svelte b/src/lib/components/icons/github.svelte deleted file mode 100644 index 1790637d..00000000 --- a/src/lib/components/icons/github.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/src/lib/components/icons/google.svelte b/src/lib/components/icons/google.svelte deleted file mode 100644 index 1c49e21f..00000000 --- a/src/lib/components/icons/google.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/src/lib/components/icons/index.ts b/src/lib/components/icons/index.ts index 23be5d5e..ed7e4d50 100644 --- a/src/lib/components/icons/index.ts +++ b/src/lib/components/icons/index.ts @@ -1,16 +1,5 @@ -import Apple from './apple.svelte'; -import Aria from './aria.svelte'; -import GitHub from './github.svelte'; -import Google from './google.svelte'; import Hamburger from './hamburger.svelte'; import Logo from './logo.svelte'; -import Npm from './npm.svelte'; -import PayPal from './paypal.svelte'; -import Pnpm from './pnpm.svelte'; -import RadixSvelte from './radix-svelte.svelte'; -import Tailwind from './tailwind.svelte'; -import Twitter from './twitter.svelte'; -import Yarn from './yarn.svelte'; import ArrowRight from 'lucide-svelte/icons/arrow-right'; import Check from 'lucide-svelte/icons/check'; import ChevronLeft from 'lucide-svelte/icons/chevron-left'; @@ -58,23 +47,11 @@ export const Icons = { arrowRight: ArrowRight, help: CircleHelp, pizza: Pizza, - twitter: Twitter, check: Check, copy: Copy, copyDone: ClipboardCheck, sun: SunMedium, moon: Moon, laptop: Laptop, - gitHub: GitHub, - radix: RadixSvelte, - 'Radix Svelte': RadixSvelte, - aria: Aria, - npm: Npm, - yarn: Yarn, - pnpm: Pnpm, - tailwind: Tailwind, - google: Google, - apple: Apple, - paypal: PayPal, Hamburger, }; diff --git a/src/lib/components/icons/npm.svelte b/src/lib/components/icons/npm.svelte deleted file mode 100644 index 64f90e49..00000000 --- a/src/lib/components/icons/npm.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/src/lib/components/icons/paypal.svelte b/src/lib/components/icons/paypal.svelte deleted file mode 100644 index 9a4c2035..00000000 --- a/src/lib/components/icons/paypal.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/src/lib/components/icons/pnpm.svelte b/src/lib/components/icons/pnpm.svelte deleted file mode 100644 index 30dccae1..00000000 --- a/src/lib/components/icons/pnpm.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/src/lib/components/icons/radix-svelte.svelte b/src/lib/components/icons/radix-svelte.svelte deleted file mode 100644 index 5777c5d7..00000000 --- a/src/lib/components/icons/radix-svelte.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/src/lib/components/icons/radix.svelte b/src/lib/components/icons/radix.svelte deleted file mode 100644 index de09cf22..00000000 --- a/src/lib/components/icons/radix.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/src/lib/components/icons/svelte-logo.svelte b/src/lib/components/icons/svelte-logo.svelte deleted file mode 100644 index 12449a53..00000000 --- a/src/lib/components/icons/svelte-logo.svelte +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/src/lib/components/icons/tailwind.svelte b/src/lib/components/icons/tailwind.svelte deleted file mode 100644 index 575b1cd0..00000000 --- a/src/lib/components/icons/tailwind.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/src/lib/components/icons/twitter.svelte b/src/lib/components/icons/twitter.svelte deleted file mode 100644 index a759f76b..00000000 --- a/src/lib/components/icons/twitter.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/src/lib/components/icons/yarn.svelte b/src/lib/components/icons/yarn.svelte deleted file mode 100644 index c4279bd8..00000000 --- a/src/lib/components/icons/yarn.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - From 95e7faf66deb125233e4769288c0c7bb830d85e8 Mon Sep 17 00:00:00 2001 From: Paul Golmann Date: Mon, 10 Mar 2025 21:07:57 +0100 Subject: [PATCH 4/6] Fix labels for networks - Do not assume 2 ethernet ports to be internal. Rock 5B+ only has one and afaik ethernet interface number order is not guaranteed any way. - Handle other numbered interfaces - Handle usb interfaces --- src/lib/helpers/NetworkHelper.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/lib/helpers/NetworkHelper.ts b/src/lib/helpers/NetworkHelper.ts index c71f32c9..82345662 100644 --- a/src/lib/helpers/NetworkHelper.ts +++ b/src/lib/helpers/NetworkHelper.ts @@ -21,22 +21,26 @@ export const networkRenameWithError = (name: string, error?: string) => { } return name; }; + export const networkRename = (name: string) => { - const originalName = name; + let numberSuffix = ''; + const number = name.match(/\d+$/g)?.[0]; + if (number) { + numberSuffix = ` ${Number.parseInt(number) + 1}`; + name = name.slice(0, -number.length).trim(); + } + if (name.startsWith('wl')) { - name = 'WiFI'; + name = 'WiFi'; } else if (name.startsWith('eth') || name.startsWith('en')) { - if (Number.parseInt(name[name.length - 1]) < 2) { - name = 'Ethernet'; - } else { - name = 'ETH-Modem'; - } + name = 'Ethernet'; } else if (name.startsWith('ww')) { name = 'Modem'; + } else if (name.startsWith('usb')) { + name = 'USB'; } - name += ' ' + (Number.parseInt(originalName[originalName.length - 1]) + name === 'ETH-Modem' ? -1 : +1); - return name; + return name + numberSuffix; }; export const getModemNetworkName = (name: string) => { From 6324f335a16498b4ca940cfd0e89a390f432aa0f Mon Sep 17 00:00:00 2001 From: Paul Golmann Date: Mon, 10 Mar 2025 21:36:11 +0100 Subject: [PATCH 5/6] Add favicon, touch icons and webmanifest --- index.html | 7 ++++++- public/vite.svg | 1 - src/assets/apple-touch-icon.png | Bin 0 -> 1944 bytes src/assets/favicon-96x96.png | Bin 0 -> 751 bytes src/assets/favicon.ico | Bin 0 -> 15086 bytes src/assets/favicon.svg | 7 +++++++ src/assets/icon.svg | 23 +++++++++++++++++++++++ src/assets/site.webmanifest | 21 +++++++++++++++++++++ src/assets/web-app-manifest-192x192.png | Bin 0 -> 2239 bytes src/assets/web-app-manifest-512x512.png | Bin 0 -> 9293 bytes 10 files changed, 57 insertions(+), 2 deletions(-) delete mode 100644 public/vite.svg create mode 100644 src/assets/apple-touch-icon.png create mode 100644 src/assets/favicon-96x96.png create mode 100644 src/assets/favicon.ico create mode 100644 src/assets/favicon.svg create mode 100644 src/assets/icon.svg create mode 100644 src/assets/site.webmanifest create mode 100644 src/assets/web-app-manifest-192x192.png create mode 100644 src/assets/web-app-manifest-512x512.png diff --git a/index.html b/index.html index 8b1a8e43..3a52b1df 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,12 @@ - + + + + + + CeraUI diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/apple-touch-icon.png b/src/assets/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9af37f98fbe8ed45c4a60055303e8dfe6238a0b2 GIT binary patch literal 1944 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 literal 0 HcmV?d00001 diff --git a/src/assets/favicon.ico b/src/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b346d1418503e36363271321c0902432fb6cc086 GIT binary patch literal 15086 zcmeHOJ8s)R5S_pQT-rCFnjkJ+r4?_G93f?HK^Xzk>IiOg3pc`niP2_Cvtfyz>)m?Ao58>Mx*lnT;w-w=RnSX zdoS_>4j1r2h*JXBR!mo*OjvFkDniugMF-L4&$fI`iJV_ zt!q!+t2OAm2jsh{a=)dIp?YY@o5t?k2ChjgS7;nHuk^2ww$S)(KkH#zk+6GPhq_L- zm+G2?SZpuVRkFQQ*CfPZd#SF{k?y6m-f6p*F830-+pV^lt%Dv??x*H#-B{c1p_?lG ztk#2HpT1^o9r{w*pU2k0uTNjIwhnzM?ayQD;Mb?GSzCv`l=kOoS_gl`{>^Up!2A8H z(qB>8!wqoa_q174z!WeA_Co=6m&ogJm8mi=AD5UASGqbQ!g}g3(eDn=^p~4I)j3g5 z^f4n|>2gfG)jT22GY|kDNdR1u0GN{ixFP}25fDL;5kcVW^UU#+z}Nz1V(#+)@U7Gk(^K0u#PG$$`1VZ{qS->wE25)Q|FMMxfVm_52?|7 zhE)KE3H6IdFsrdXYobp&)g?|K@@xUqlWMY5o#vU&;IMEG*9w!uq%bK=aKM;hEHDtW zcF1t^;^H|4J~4A=6^D9GfoF{GhL&@Lvx2@Y{{acCKUO*zU3E-ls>C zf0Jz&N0aX^+Yxf4yLuhMUYvE}95};wsbi75MC5)d@;npydyjMBKgFD4POmZUFkr9Z F{sGdJ-SGeb literal 0 HcmV?d00001 diff --git a/src/assets/favicon.svg b/src/assets/favicon.svg new file mode 100644 index 00000000..517327a4 --- /dev/null +++ b/src/assets/favicon.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/assets/icon.svg b/src/assets/icon.svg new file mode 100644 index 00000000..da973675 --- /dev/null +++ b/src/assets/icon.svg @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/src/assets/site.webmanifest b/src/assets/site.webmanifest new file mode 100644 index 00000000..01dc8b61 --- /dev/null +++ b/src/assets/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "CeraUI for BELABOX©", + "short_name": "CeraUI", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/src/assets/web-app-manifest-192x192.png b/src/assets/web-app-manifest-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..a83c4c15d80033a16ac316d3ea42cb2c6c0192d5 GIT binary patch literal 2239 zcmcgu`BPJQ692x(gMCOC5*5NB5CkDyA|NXQnixSvQ3J?*gi&M_H5){MAVZ!CigJjO zfSkrD0o`CgW=4Xb;ZTmCL`Colh{SlIfKkNkWWvPV`5WekKI&6lpRVfqRCk}EZ`)|N zzqBJw|Dg>~YRE^8mR|edt$Rhc+b#c!;hG~-AMfG<9cw%*DB)1nw9E(5 zL->eLpM*_*Yh@#RoxeAIfc)@$Hp$z{nzD*{A|TYg-*@8xG8YIV%3)^>74KN9#8qV&4^#=g$Piq zjI@^jrbZKPwk~)x7n_aH$;Ar7*uWW3`n)nDO~Qfu+Nh@jnnl&h`Aa8&iA5DFNiuI? z1>14^pM^{2SGgliBRgNR9p?|TP?8I`_5gQ2EgDU@(tqri5Dj||N+R=ku3XM)Ml@|6Q} zDp=iSpBAF|)f7)EWDJjTDV7xYl4pa*QRQU9MqpVKP*2ZZBSZPU&3$%~#(XpA5Id=Q zgKGBEQf%#z%OXdPNrO6wWz}#a)KR=#`gw0V{A!oLqg*LSXkU#$O6AsyZK{$P-2)pi zQjmfetns)@;N#w07SJ{1*uZ=ON&kW-Nzy1i#9Rt<6>E-oNf>K!2wAFrZ|umaCgJ%% z%rPQr)w47zmid7q3{p+%VZc}wsyRJFaVrEo55Ah$!H6}1oL*5?&Qx(qAVRLvRk-1@ zULHj0D~u!zeA;*|o`m1=-a~R>^9pG;($~$KQyH*^MF_lyURB;%Lh&5G{Ru5bSxjGr z6ph%s5+)yQU$S21c{I!_A4sV6DO`%qqaa%X)yzc^p;y6P0+Zo7_30G`g*342K9`}N z_|hHagDRynwP7U;E$j@{_+z{lTPh83DNwv^41sEjD2T`Q?YDqY_@Wx#Q^;J0^(Bxw zc1=BHwp4#AmjaJGeW=y6Sy6AVl<^EF|F-g{TbbF<`|aOV`;303>I+@cJ3>ZAs%k`%0`mXG_SCENG+cmdEn@=`=?YMdb^2;%ee>cO67aexR z%JAL$EU!OY)WX!AS-dmyYIitWK2i(DXgI-FrPCSNjYS>1@&<yDq2zIb;I@(}UTP7DmAE zyKTC*_8eeFq(Eq}$x>OOtTfHo7{ixMh%~=z=?$KS1pylFlziG;)Gx}Na$4H)W80Wv zuYA|ri^7I|6XHif-r_yZ3gVCX&$w|wV$JKF+bMCAo3sfILmA3m`^@O+bCpO~He@wb zbnZCY@T`Nl$+hdiG9=><#_O%GRIzJ2$(+P3i%VO`+5zcKo~y(SONDB9|KrM+8CCJR zw6(D(`bsF5%-JJ}!~~zTao=@ZM(MRJOphI$bYnt7@UfJvXzl9%l_`-dsZr0CIN5i! zAVUuI;>FK7GR+lrz0~q2y5^r<%blneEwS^9x#Da+h65GrilS?}I*7|p0^QGOmS@70 ze^#fUiU^Z+Zh93diLfQx49P?6<7&uYd$wbS2IZcLef31=#UkUthPwyjAqlHgGnDJ6 z@_cg~ltY|CV*BswRR)R2fi7u#nfJ_^;M`5UaeB~1#;nI(4tWE18v`d^#QFP(G#frS5jTo1gsxaLg&a;fVnA4W--`DVQ+I}DhF%I_v4 zc&g#D<1P!SApc5Ak{68fPn=t4qCLQO9(n&_4k7gQ{iJ*Z-?1~fDyk5`moFw32f%28 zyVqdD1^_*xd-N@Zkf!tI{|dLoKr-1Z@hpHNg9<$b1nS?^cE)J09sI{|^r>L&M*;!^ Lw)qQb3CI2f`Samx literal 0 HcmV?d00001 diff --git a/src/assets/web-app-manifest-512x512.png b/src/assets/web-app-manifest-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..ac6b0f7c9b35319a1da60359ef9fcf2548b7bf0c GIT binary patch literal 9293 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 E18f?vL;wH) literal 0 HcmV?d00001 From a33f8c3cce11be567910236450872464de140e22 Mon Sep 17 00:00:00 2001 From: Paul Golmann Date: Wed, 12 Mar 2025 21:17:09 +0100 Subject: [PATCH 6/6] new icon --- src/assets/apple-touch-icon.png | Bin 1944 -> 655 bytes src/assets/favicon-96x96.png | Bin 751 -> 370 bytes src/assets/favicon.ico | Bin 15086 -> 630 bytes src/assets/favicon.svg | 17 ++++++++++------- src/assets/icon.svg | 23 ----------------------- src/assets/web-app-manifest-192x192.png | Bin 2239 -> 702 bytes src/assets/web-app-manifest-512x512.png | Bin 9293 -> 1917 bytes 7 files changed, 10 insertions(+), 30 deletions(-) delete mode 100644 src/assets/icon.svg diff --git a/src/assets/apple-touch-icon.png b/src/assets/apple-touch-icon.png index 9af37f98fbe8ed45c4a60055303e8dfe6238a0b2..cc9605814e95e6cbdae2d8d353b564e51ced90a4 100644 GIT binary patch delta 642 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 Ang9R* delta 738 zcmV<80v-MG0`CQo8Gi-<0082ccQ^n500DDSM?wIu&K&6g00OB=L_t(|UhUl5Rl-0N zg<(LMN0Lh#^IhWbK!c3Cy&m>t(pqlYdRiIMcIyXLJ z0OgZ_0T@78KzZ|A0SurlpuBmm00vMNP~JRO00SrsC~uxCfPVp$1#WDN5M-`oe@a@+@a2~xyw@9(+KEbH@Yo%zRmyLF(P`=^ax^a`+mUu*y?_(cY=gkM|$ zYxqS4u!vtw0Dr6aMFg;nA3lI}{LleB;D-(11wUi}Px#>ic*74Bz$1Q`0ABG!1n`XS zK7e<8*8w8nyA2Qp-(`SE`0fHk!*>-RBEFjdQSn^_h>UMOKy-Z50ZPC(8^AI?`imRp zJ{xKrAO6J+QvuxJcOP68eR0D~0H^pd0yxGOe{n;71%H%`ul^KNI=*TE;&*p&l}ixu zJA9P@#6RIr9)S3l_>%@8{x$w&0f^s^KS==M-^R}#fcW?Evj!mkWBhCZh(8QJO90{z z$A*oPYdw!^zrjq zm}T#7WPkc;$03Jo`NX)l|D*pUS>Fmk{2}Mb)qBPV=zi3?Sp}y*;wRxJU4oiHzsvo( z-D-yq|5589N96Ugb8+qtIZ%u5iGZKH0iG0smJIu zfUzgBRD8FWAWKhT5`5PIqz7(gyvs|F6+Fa#Fk1jZQ7{Js2=EX?833Uun1cZXc!;44 zfKU|7!2kk0#83u6C<^9a00ACiC<7oA1#>Wf01q*g0T7CUIT%2IhZxEL2t~mh-{!ro Uf2J>2c>n+a07*qoM6N<$f?3x}HUIzs diff --git a/src/assets/favicon.ico b/src/assets/favicon.ico index b346d1418503e36363271321c0902432fb6cc086..49dfc02bbf2a3b4483b0369d00c776f218bdf171 100644 GIT binary patch literal 630 zcmZQzU}Rus5D;Jh(h3Yy7#JALfLK8R!ao3Hp8{e70|@^Okln?|z|a}s=g!L|#RcT@ zdV0770coHK3<4a?KvF}OKNv{y2l#}z{{R2~$B7FsX3Thyk#WPo;;_0{FHp+C)5S5Q zg0a`HnUTSfr~BLdpJ$WbvTb(po^K-kQBOl{>K$hH86PBX$o$l{2@$DSYtx!jSmgZi z{afZYiYHZ`WiY06FzgK}DaZwy#o+1c=d#Wzp$Utd6oAqSU^lre={g4Fz}$4*&;stP z2i{iKfKoXSH=Uhym663zq-E2YFY%vu7U!M5F8oOHGqcdTa_iiV@5ZtReyg{AXO46) zZCrfe*G;wX*4RszO_v;-WbD}Yf6js1O}dK9DdcR#t-R%Nh?Y4&rzsrzbX zSjVs9QT_VO@W2dNZayByDxom#FJ4XSFFX<|yXaRjGeefy6!VN}9d>QgYv1p^a580;+yc&n zcF{r&w`?lDHOB@dPQ0u)!EkxEBio7PQo?+*W*7)GWCu9TfB3fP%ujKa$feV{1)9=K zlBV5u(ugr>@?mzdoyo S1-@WAepUGEN}wCzegyy>-RIN* literal 15086 zcmeHOJ8s)R5S_pQT-rCFnjkJ+r4?_G93f?HK^Xzk>IiOg3pc`niP2_Cvtfyz>)m?Ao58>Mx*lnT;w-w=RnSX zdoS_>4j1r2h*JXBR!mo*OjvFkDniugMF-L4&$fI`iJV_ zt!q!+t2OAm2jsh{a=)dIp?YY@o5t?k2ChjgS7;nHuk^2ww$S)(KkH#zk+6GPhq_L- zm+G2?SZpuVRkFQQ*CfPZd#SF{k?y6m-f6p*F830-+pV^lt%Dv??x*H#-B{c1p_?lG ztk#2HpT1^o9r{w*pU2k0uTNjIwhnzM?ayQD;Mb?GSzCv`l=kOoS_gl`{>^Up!2A8H z(qB>8!wqoa_q174z!WeA_Co=6m&ogJm8mi=AD5UASGqbQ!g}g3(eDn=^p~4I)j3g5 z^f4n|>2gfG)jT22GY|kDNdR1u0GN{ixFP}25fDL;5kcVW^UU#+z}Nz1V(#+)@U7Gk(^K0u#PG$$`1VZ{qS->wE25)Q|FMMxfVm_52?|7 zhE)KE3H6IdFsrdXYobp&)g?|K@@xUqlWMY5o#vU&;IMEG*9w!uq%bK=aKM;hEHDtW zcF1t^;^H|4J~4A=6^D9GfoF{GhL&@Lvx2@Y{{acCKUO*zU3E-ls>C zf0Jz&N0aX^+Yxf4yLuhMUYvE}95};wsbi75MC5)d@;npydyjMBKgFD4POmZUFkr9Z F{sGdJ-SGeb diff --git a/src/assets/favicon.svg b/src/assets/favicon.svg index 517327a4..6eae9869 100644 --- a/src/assets/favicon.svg +++ b/src/assets/favicon.svg @@ -1,7 +1,10 @@ - - - - - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/src/assets/icon.svg b/src/assets/icon.svg deleted file mode 100644 index da973675..00000000 --- a/src/assets/icon.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/assets/web-app-manifest-192x192.png b/src/assets/web-app-manifest-192x192.png index a83c4c15d80033a16ac316d3ea42cb2c6c0192d5..e4d725eb693badc690e6e11a98e546601e701814 100644 GIT binary patch delta 690 zcmV;j0!{tD5xxbG7=Hu<0001>@^Lf(000kAOjJex|Nr&N&Fh$(*D*NZWonQ;kGTK< z0%}P_K~#7F?VINrt1t{jjgtZ3B?Hg~NGuOP+kd9lEBTNaf<$sMQs7jp954sW zAcwU4`3iL*1Aq-60G_0)=wy(jN-R9XX>MYI2cR%v2NZHR$tpYq5C9(wgP3Wt#R`vb zg3C)kw^hjzu{d>gctnfSN7utf-g%CO-SZEB#u-Y=O4`BytZ%vE}P=N5SMR;RW}lsSIdF|1oj5%tpFZp(`-_o7kHX_ zLVLnvWlfJvKo34z`YExmI{yZ#c#Qk0-ZUt1H43a{1SXq64t6d^f#K{>z8O3CxP1#B z1{^fWfN=%@y#pbCyFdljLNFEmO#;ycpaAFkYXu(vs(*kBxPS||fD8DV>;nAd7YcB6 zfxqetN5WtDl{x$sAL3xIf^#tn40>-P3j){?h`&}Kfe{bxEV!*HOaKgoSW2LUolQSI zL7S7H8-cyUXmWW4Q2T?Xecrt~J;Q5ZPTH(2mMbw!iu65w)DZIYUuxVKs# z%CM>;X@4zv(&Da3`<>QpG0P~J`(mZ#Gyo;m+Y9E@SdjEN|-=-(Y9^H z>II|du+OZOS4iGs`00N*eG?qVaU92S9Otj& YEly#-?_Q`(Y5)KL07*qoM6N<$f*dY9X#fBK literal 2239 zcmcgu`BPJQ692x(gMCOC5*5NB5CkDyA|NXQnixSvQ3J?*gi&M_H5){MAVZ!CigJjO zfSkrD0o`CgW=4Xb;ZTmCL`Colh{SlIfKkNkWWvPV`5WekKI&6lpRVfqRCk}EZ`)|N zzqBJw|Dg>~YRE^8mR|edt$Rhc+b#c!;hG~-AMfG<9cw%*DB)1nw9E(5 zL->eLpM*_*Yh@#RoxeAIfc)@$Hp$z{nzD*{A|TYg-*@8xG8YIV%3)^>74KN9#8qV&4^#=g$Piq zjI@^jrbZKPwk~)x7n_aH$;Ar7*uWW3`n)nDO~Qfu+Nh@jnnl&h`Aa8&iA5DFNiuI? z1>14^pM^{2SGgliBRgNR9p?|TP?8I`_5gQ2EgDU@(tqri5Dj||N+R=ku3XM)Ml@|6Q} zDp=iSpBAF|)f7)EWDJjTDV7xYl4pa*QRQU9MqpVKP*2ZZBSZPU&3$%~#(XpA5Id=Q zgKGBEQf%#z%OXdPNrO6wWz}#a)KR=#`gw0V{A!oLqg*LSXkU#$O6AsyZK{$P-2)pi zQjmfetns)@;N#w07SJ{1*uZ=ON&kW-Nzy1i#9Rt<6>E-oNf>K!2wAFrZ|umaCgJ%% z%rPQr)w47zmid7q3{p+%VZc}wsyRJFaVrEo55Ah$!H6}1oL*5?&Qx(qAVRLvRk-1@ zULHj0D~u!zeA;*|o`m1=-a~R>^9pG;($~$KQyH*^MF_lyURB;%Lh&5G{Ru5bSxjGr z6ph%s5+)yQU$S21c{I!_A4sV6DO`%qqaa%X)yzc^p;y6P0+Zo7_30G`g*342K9`}N z_|hHagDRynwP7U;E$j@{_+z{lTPh83DNwv^41sEjD2T`Q?YDqY_@Wx#Q^;J0^(Bxw zc1=BHwp4#AmjaJGeW=y6Sy6AVl<^EF|F-g{TbbF<`|aOV`;303>I+@cJ3>ZAs%k`%0`mXG_SCENG+cmdEn@=`=?YMdb^2;%ee>cO67aexR z%JAL$EU!OY)WX!AS-dmyYIitWK2i(DXgI-FrPCSNjYS>1@&<yDq2zIb;I@(}UTP7DmAE zyKTC*_8eeFq(Eq}$x>OOtTfHo7{ixMh%~=z=?$KS1pylFlziG;)Gx}Na$4H)W80Wv zuYA|ri^7I|6XHif-r_yZ3gVCX&$w|wV$JKF+bMCAo3sfILmA3m`^@O+bCpO~He@wb zbnZCY@T`Nl$+hdiG9=><#_O%GRIzJ2$(+P3i%VO`+5zcKo~y(SONDB9|KrM+8CCJR zw6(D(`bsF5%-JJ}!~~zTao=@ZM(MRJOphI$bYnt7@UfJvXzl9%l_`-dsZr0CIN5i! zAVUuI;>FK7GR+lrz0~q2y5^r<%blneEwS^9x#Da+h65GrilS?}I*7|p0^QGOmS@70 ze^#fUiU^Z+Zh93diLfQx49P?6<7&uYd$wbS2IZcLef31=#UkUthPwyjAqlHgGnDJ6 z@_cg~ltY|CV*BswRR)R2fi7u#nfJ_^;M`5UaeB~1#;nI(4tWE18v`d^#QFP(G#frS5jTo1gsxaLg&a;fVnA4W--`DVQ+I}DhF%I_v4 zc&g#D<1P!SApc5Ak{68fPn=t4qCLQO9(n&_4k7gQ{iJ*Z-?1~fDyk5`moFw32f%28 zyVqdD1^_*xd-N@Zkf!tI{|dLoKr-1Z@hpHNg9<$b1nS?^cE)J09sI{|^r>L&M*;!^ Lw)qQb3CI2f`Samx diff --git a/src/assets/web-app-manifest-512x512.png b/src/assets/web-app-manifest-512x512.png index ac6b0f7c9b35319a1da60359ef9fcf2548b7bf0c..1c3796243059f5056e7252cb33d4e49503614edb 100644 GIT binary patch literal 1917 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 E18f?vL;wH)