Initial commit
This commit is contained in:
commit
af2f593fb8
66 changed files with 8735 additions and 0 deletions
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
test-results
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# Paraglide
|
||||||
|
src/lib/paraglide
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
6
.prettierignore
Normal file
6
.prettierignore
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Package Managers
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
bun.lock
|
||||||
|
bun.lockb
|
15
.prettierrc
Normal file
15
.prettierrc
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
38
README.md
Normal file
38
README.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a new project in the current directory
|
||||||
|
npx sv create
|
||||||
|
|
||||||
|
# create a new project in my-app
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To create a production version of your app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
6
e2e/demo.test.ts
Normal file
6
e2e/demo.test.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test('home page has expected h1', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.locator('h1')).toBeVisible();
|
||||||
|
});
|
36
eslint.config.js
Normal file
36
eslint.config.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import { includeIgnoreFile } from '@eslint/compat';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import globals from 'globals';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
import svelteConfig from './svelte.config.js';
|
||||||
|
|
||||||
|
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||||
|
|
||||||
|
export default ts.config(
|
||||||
|
includeIgnoreFile(gitignorePath),
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs.recommended,
|
||||||
|
prettier,
|
||||||
|
...svelte.configs.prettier,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser, ...globals.node }
|
||||||
|
},
|
||||||
|
rules: { 'no-undef': 'off' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
extraFileExtensions: ['.svelte'],
|
||||||
|
parser: ts.parser,
|
||||||
|
svelteConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
152
messages/de-DE.json
Normal file
152
messages/de-DE.json
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"section": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Einstellungen",
|
||||||
|
"textColor": "Textfarbe",
|
||||||
|
"backgroundColor": "Hintergrundfarbe",
|
||||||
|
"ledPowerOnTest": "LED-Einschalttest",
|
||||||
|
"ledFlashOnBlock": "LED blinkt bei neuem Block",
|
||||||
|
"timePerScreen": "Zeit pro Bildschirm",
|
||||||
|
"ledBrightness": "LED-Helligkeit",
|
||||||
|
"flMaxBrightness": "Displaybeleuchtung Helligkeit",
|
||||||
|
"timezoneOffset": "Zeitzonenoffset",
|
||||||
|
"timeBetweenPriceUpdates": "Zeit zwischen Preisaktualisierungen",
|
||||||
|
"fullRefreshEvery": "Vollständige Aktualisierung alle",
|
||||||
|
"mempoolnstance": "Mempool Instance",
|
||||||
|
"hostnamePrefix": "Hostnamen-Präfix",
|
||||||
|
"StealFocusOnNewBlock": "Steal focus on new block",
|
||||||
|
"useBigCharsMcap": "Verwende große Zeichen für die Marktkapitalisierung",
|
||||||
|
"useBlkCountdown": "Blocks Countdown zur Halbierung",
|
||||||
|
"useSatsSymbol": "Sats-Symbol verwenden",
|
||||||
|
"suffixPrice": "Suffix-Preisformat",
|
||||||
|
"disableLeds": "Alle LED-Effekte deaktivieren",
|
||||||
|
"otaUpdates": "OTA updates",
|
||||||
|
"enableMdns": "mDNS",
|
||||||
|
"fetchEuroPrice": "€-Preis abrufen",
|
||||||
|
"shortAmountsWarning": "Geringe Beträge können die Lebensdauer der Displays verkürzen",
|
||||||
|
"tzOffsetHelpText": "Ein Neustart ist erforderlich, um den TZ-Offset anzuwenden.",
|
||||||
|
"screens": "Bildschirme",
|
||||||
|
"wifiTxPowerText": "In den meisten Fällen muss dies nicht eingestellt werden.",
|
||||||
|
"wifiTxPower": "WiFi-TX-Leistung",
|
||||||
|
"settingsSaved": "Einstellungen gespeichert",
|
||||||
|
"errorSavingSettings": "Fehler beim Speichern der Einstellungen",
|
||||||
|
"ownDataSource": "BTClock-Datenquelle",
|
||||||
|
"flAlwaysOn": "Displaybeleuchtung immer an",
|
||||||
|
"flEffectDelay": "Displaybeleuchtungeffekt Geschwindigkeit",
|
||||||
|
"flFlashOnUpd": "Displaybeleuchting bei neuem Block",
|
||||||
|
"mempoolInstanceHelpText": "Nur wirksam, wenn die BTClock-Datenquelle deaktiviert ist. \nZur Anwendung ist ein Neustart erforderlich.",
|
||||||
|
"luxLightToggle": "Automatisches Umschalten des Frontlichts bei Lux",
|
||||||
|
"wpTimeout": "WiFi-Konfigurationsportal timeout",
|
||||||
|
"useNostr": "Nostr-Datenquelle verwenden",
|
||||||
|
"flDisable": "Displaybeleuchtung deaktivieren",
|
||||||
|
"httpAuthUser": "WebUI-Benutzername",
|
||||||
|
"httpAuthPass": "WebUI-Passwort",
|
||||||
|
"httpAuthText": "Schützt nur die WebUI mit einem Passwort, nicht API-Aufrufe.",
|
||||||
|
"currencies": "Währungen",
|
||||||
|
"mowMode": "Mow suffixmodus",
|
||||||
|
"suffixShareDot": "Kompakte Suffix-Notation",
|
||||||
|
"section": {
|
||||||
|
"displaysAndLed": "Anzeigen und LEDs",
|
||||||
|
"screenSettings": "Infospezifisch",
|
||||||
|
"dataSource": "Datenquelle",
|
||||||
|
"extraFeatures": "Zusätzliche Funktionen",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
|
"ledFlashOnZap": "LED blinkt bei Nostr Zap",
|
||||||
|
"flFlashOnZap": "Displaybeleuchting bei Nostr Zap",
|
||||||
|
"showAll": "Alle anzeigen",
|
||||||
|
"hideAll": "Alles ausblenden",
|
||||||
|
"flOffWhenDark": "Displaybeleuchtung aus, wenn es dunkel ist",
|
||||||
|
"luxLightToggleText": "Zum Deaktivieren auf 0 setzen",
|
||||||
|
"verticalDesc": "Vrtikale Bildschirmbeschreibung",
|
||||||
|
"enableDebugLog": "Debug-Protokoll aktivieren",
|
||||||
|
"bitaxeEnabled": "BitAxe-Integration aktivieren",
|
||||||
|
"miningPoolStats": "Mining-Pool-Statistiken Integration Aktivieren",
|
||||||
|
"nostrZapNotify": "Nostr Zap-Benachrichtigungen aktivieren",
|
||||||
|
"thirdPartySource": "mempool.space/coincap.io Verwenden",
|
||||||
|
"dataSource": {
|
||||||
|
"nostr": "Nostr-Verlag",
|
||||||
|
"custom": "Benutzerdefinierter dataquelle"
|
||||||
|
},
|
||||||
|
"fontName": "Schriftart",
|
||||||
|
"timeBasedDnd": "Aktivieren Sie den Zeitplan „Bitte nicht stören“.",
|
||||||
|
"dndStartHour": "Startstunde",
|
||||||
|
"dndStartMinute": "Startminute",
|
||||||
|
"dndEndHour": "Endstunde",
|
||||||
|
"dndEndMinute": "Schlussminute"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"systemInfo": "Systeminfo",
|
||||||
|
"version": "Version",
|
||||||
|
"buildTime": "Build time",
|
||||||
|
"ledColor": "LED-Farbe",
|
||||||
|
"turnOff": "Ausschalten",
|
||||||
|
"setColor": "Farbe festlegen",
|
||||||
|
"showText": "Text anzeigen",
|
||||||
|
"text": "Text",
|
||||||
|
"title": "Kontrolle",
|
||||||
|
"hostname": "Hostname",
|
||||||
|
"frontlight": "Displaybeleuchtung",
|
||||||
|
"turnOn": "Einschalten",
|
||||||
|
"flashFrontlight": "Blinken"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"title": "Status",
|
||||||
|
"screenCycle": "Bildschirmzyklus",
|
||||||
|
"memoryFree": "Speicher frei",
|
||||||
|
"wsPriceConnection": "WS-Preisverbindung",
|
||||||
|
"wsMempoolConnection": "WS {instance}-Verbindung",
|
||||||
|
"fetchEuroNote": "If you use \"Fetch € price\" the WS Price connection will show ❌ since it uses another data source.",
|
||||||
|
"uptime": "Betriebszeit",
|
||||||
|
"wifiSignalStrength": "WiFi-Signalstärke",
|
||||||
|
"wsDataConnection": "BTClock-Datenquelle verbindung",
|
||||||
|
"lightSensor": "Lichtsensor",
|
||||||
|
"nostrConnection": "Nostr Relay-Verbindung",
|
||||||
|
"doNotDisturb": "Bitte nicht stören",
|
||||||
|
"timeBasedDnd": "Zeitbasierter Zeitplan"
|
||||||
|
},
|
||||||
|
"firmwareUpdater": {
|
||||||
|
"fileUploadSuccess": "Datei erfolgreich hochgeladen, Gerät neu gestartet. WebUI in {countdown} Sekunden neu geladen",
|
||||||
|
"fileUploadFailed": "Das Hochladen der Datei ist fehlgeschlagen. \nStellen Sie sicher, dass Sie die richtige Datei ausgewählt haben, und versuchen Sie es erneut.",
|
||||||
|
"uploading": "Hochladen",
|
||||||
|
"firmwareUpdateText": "Wenn Sie die Firmware-Upload-Funktion verwenden, stellen Sie sicher, dass Sie die richtigen Dateien verwenden. \nDas Hochladen der falschen Dateien kann dazu führen, dass das Gerät nicht mehr funktioniert. \nWenn es schief geht, können Sie die Firmware wiederherstellen, indem Sie das vollständige Image hochladen, nachdem Sie das Gerät in den BOOT-Modus versetzt haben.",
|
||||||
|
"swUpToDate": "Du hast die neueste Version.",
|
||||||
|
"swUpdateAvailable": "Eine neuere Version ist verfügbar!",
|
||||||
|
"latestVersion": "Letzte Version",
|
||||||
|
"releaseDate": "Veröffentlichungsdatum",
|
||||||
|
"viewRelease": "Veröffentlichung anzeigen",
|
||||||
|
"autoUpdate": "Update installieren (experimentell)",
|
||||||
|
"autoUpdateInProgress": "Automatische Aktualisierung läuft, bitte warten..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"black": "Schwarz",
|
||||||
|
"white": "Weiss"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"minutes": "Minuten",
|
||||||
|
"seconds": "Sekunden"
|
||||||
|
},
|
||||||
|
"restartRequired": "Neustart erforderlich",
|
||||||
|
"button": {
|
||||||
|
"save": "Speichern",
|
||||||
|
"reset": "Zurücksetzen",
|
||||||
|
"restart": "Neustart",
|
||||||
|
"forceFullRefresh": "Vollständige Aktualisierung erzwingen"
|
||||||
|
},
|
||||||
|
"timer": {
|
||||||
|
"running": "läuft",
|
||||||
|
"stopped": "gestoppt"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"control": {
|
||||||
|
"keepSameColor": "Gleiche Farbe beibehalten"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rssiBar": {
|
||||||
|
"tooltip": "Werte > -67 dBm gelten als gut. > -30 dBm ist erstaunlich"
|
||||||
|
},
|
||||||
|
"warning": "Achtung",
|
||||||
|
"auto-detect": "Automatische Erkennung"
|
||||||
|
}
|
153
messages/de.json
Normal file
153
messages/de.json
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"hello_world": "Hello, {name} from de!",
|
||||||
|
"section": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Einstellungen",
|
||||||
|
"textColor": "Textfarbe",
|
||||||
|
"backgroundColor": "Hintergrundfarbe",
|
||||||
|
"ledPowerOnTest": "LED-Einschalttest",
|
||||||
|
"ledFlashOnBlock": "LED blinkt bei neuem Block",
|
||||||
|
"timePerScreen": "Zeit pro Bildschirm",
|
||||||
|
"ledBrightness": "LED-Helligkeit",
|
||||||
|
"flMaxBrightness": "Displaybeleuchtung Helligkeit",
|
||||||
|
"timezoneOffset": "Zeitzonenoffset",
|
||||||
|
"timeBetweenPriceUpdates": "Zeit zwischen Preisaktualisierungen",
|
||||||
|
"fullRefreshEvery": "Vollständige Aktualisierung alle",
|
||||||
|
"mempoolnstance": "Mempool Instance",
|
||||||
|
"hostnamePrefix": "Hostnamen-Präfix",
|
||||||
|
"StealFocusOnNewBlock": "Steal focus on new block",
|
||||||
|
"useBigCharsMcap": "Verwende große Zeichen für die Marktkapitalisierung",
|
||||||
|
"useBlkCountdown": "Blocks Countdown zur Halbierung",
|
||||||
|
"useSatsSymbol": "Sats-Symbol verwenden",
|
||||||
|
"suffixPrice": "Suffix-Preisformat",
|
||||||
|
"disableLeds": "Alle LED-Effekte deaktivieren",
|
||||||
|
"otaUpdates": "OTA updates",
|
||||||
|
"enableMdns": "mDNS",
|
||||||
|
"fetchEuroPrice": "€-Preis abrufen",
|
||||||
|
"shortAmountsWarning": "Geringe Beträge können die Lebensdauer der Displays verkürzen",
|
||||||
|
"tzOffsetHelpText": "Ein Neustart ist erforderlich, um den TZ-Offset anzuwenden.",
|
||||||
|
"screens": "Bildschirme",
|
||||||
|
"wifiTxPowerText": "In den meisten Fällen muss dies nicht eingestellt werden.",
|
||||||
|
"wifiTxPower": "WiFi-TX-Leistung",
|
||||||
|
"settingsSaved": "Einstellungen gespeichert",
|
||||||
|
"errorSavingSettings": "Fehler beim Speichern der Einstellungen",
|
||||||
|
"ownDataSource": "BTClock-Datenquelle",
|
||||||
|
"flAlwaysOn": "Displaybeleuchtung immer an",
|
||||||
|
"flEffectDelay": "Displaybeleuchtungeffekt Geschwindigkeit",
|
||||||
|
"flFlashOnUpd": "Displaybeleuchting bei neuem Block",
|
||||||
|
"mempoolInstanceHelpText": "Nur wirksam, wenn die BTClock-Datenquelle deaktiviert ist. \nZur Anwendung ist ein Neustart erforderlich.",
|
||||||
|
"luxLightToggle": "Automatisches Umschalten des Frontlichts bei Lux",
|
||||||
|
"wpTimeout": "WiFi-Konfigurationsportal timeout",
|
||||||
|
"useNostr": "Nostr-Datenquelle verwenden",
|
||||||
|
"flDisable": "Displaybeleuchtung deaktivieren",
|
||||||
|
"httpAuthUser": "WebUI-Benutzername",
|
||||||
|
"httpAuthPass": "WebUI-Passwort",
|
||||||
|
"httpAuthText": "Schützt nur die WebUI mit einem Passwort, nicht API-Aufrufe.",
|
||||||
|
"currencies": "Währungen",
|
||||||
|
"mowMode": "Mow suffixmodus",
|
||||||
|
"suffixShareDot": "Kompakte Suffix-Notation",
|
||||||
|
"section": {
|
||||||
|
"displaysAndLed": "Anzeigen und LEDs",
|
||||||
|
"screenSettings": "Infospezifisch",
|
||||||
|
"dataSource": "Datenquelle",
|
||||||
|
"extraFeatures": "Zusätzliche Funktionen",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
|
"ledFlashOnZap": "LED blinkt bei Nostr Zap",
|
||||||
|
"flFlashOnZap": "Displaybeleuchting bei Nostr Zap",
|
||||||
|
"showAll": "Alle anzeigen",
|
||||||
|
"hideAll": "Alles ausblenden",
|
||||||
|
"flOffWhenDark": "Displaybeleuchtung aus, wenn es dunkel ist",
|
||||||
|
"luxLightToggleText": "Zum Deaktivieren auf 0 setzen",
|
||||||
|
"verticalDesc": "Vrtikale Bildschirmbeschreibung",
|
||||||
|
"enableDebugLog": "Debug-Protokoll aktivieren",
|
||||||
|
"bitaxeEnabled": "BitAxe-Integration aktivieren",
|
||||||
|
"miningPoolStats": "Mining-Pool-Statistiken Integration Aktivieren",
|
||||||
|
"nostrZapNotify": "Nostr Zap-Benachrichtigungen aktivieren",
|
||||||
|
"thirdPartySource": "mempool.space/coincap.io Verwenden",
|
||||||
|
"dataSource": {
|
||||||
|
"nostr": "Nostr-Verlag",
|
||||||
|
"custom": "Benutzerdefinierter dataquelle"
|
||||||
|
},
|
||||||
|
"fontName": "Schriftart",
|
||||||
|
"timeBasedDnd": "Aktivieren Sie den Zeitplan „Bitte nicht stören“.",
|
||||||
|
"dndStartHour": "Startstunde",
|
||||||
|
"dndStartMinute": "Startminute",
|
||||||
|
"dndEndHour": "Endstunde",
|
||||||
|
"dndEndMinute": "Schlussminute"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"systemInfo": "Systeminfo",
|
||||||
|
"version": "Version",
|
||||||
|
"buildTime": "Build time",
|
||||||
|
"ledColor": "LED-Farbe",
|
||||||
|
"turnOff": "Ausschalten",
|
||||||
|
"setColor": "Farbe festlegen",
|
||||||
|
"showText": "Text anzeigen",
|
||||||
|
"text": "Text",
|
||||||
|
"title": "Kontrolle",
|
||||||
|
"hostname": "Hostname",
|
||||||
|
"frontlight": "Displaybeleuchtung",
|
||||||
|
"turnOn": "Einschalten",
|
||||||
|
"flashFrontlight": "Blinken"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"title": "Status",
|
||||||
|
"screenCycle": "Bildschirmzyklus",
|
||||||
|
"memoryFree": "Speicher frei",
|
||||||
|
"wsPriceConnection": "WS-Preisverbindung",
|
||||||
|
"wsMempoolConnection": "WS {instance}-Verbindung",
|
||||||
|
"fetchEuroNote": "If you use \"Fetch € price\" the WS Price connection will show ❌ since it uses another data source.",
|
||||||
|
"uptime": "Betriebszeit",
|
||||||
|
"wifiSignalStrength": "WiFi-Signalstärke",
|
||||||
|
"wsDataConnection": "BTClock-Datenquelle verbindung",
|
||||||
|
"lightSensor": "Lichtsensor",
|
||||||
|
"nostrConnection": "Nostr Relay-Verbindung",
|
||||||
|
"doNotDisturb": "Bitte nicht stören",
|
||||||
|
"timeBasedDnd": "Zeitbasierter Zeitplan"
|
||||||
|
},
|
||||||
|
"firmwareUpdater": {
|
||||||
|
"fileUploadSuccess": "Datei erfolgreich hochgeladen, Gerät neu gestartet. WebUI in {countdown} Sekunden neu geladen",
|
||||||
|
"fileUploadFailed": "Das Hochladen der Datei ist fehlgeschlagen. \nStellen Sie sicher, dass Sie die richtige Datei ausgewählt haben, und versuchen Sie es erneut.",
|
||||||
|
"uploading": "Hochladen",
|
||||||
|
"firmwareUpdateText": "Wenn Sie die Firmware-Upload-Funktion verwenden, stellen Sie sicher, dass Sie die richtigen Dateien verwenden. \nDas Hochladen der falschen Dateien kann dazu führen, dass das Gerät nicht mehr funktioniert. \nWenn es schief geht, können Sie die Firmware wiederherstellen, indem Sie das vollständige Image hochladen, nachdem Sie das Gerät in den BOOT-Modus versetzt haben.",
|
||||||
|
"swUpToDate": "Du hast die neueste Version.",
|
||||||
|
"swUpdateAvailable": "Eine neuere Version ist verfügbar!",
|
||||||
|
"latestVersion": "Letzte Version",
|
||||||
|
"releaseDate": "Veröffentlichungsdatum",
|
||||||
|
"viewRelease": "Veröffentlichung anzeigen",
|
||||||
|
"autoUpdate": "Update installieren (experimentell)",
|
||||||
|
"autoUpdateInProgress": "Automatische Aktualisierung läuft, bitte warten..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"black": "Schwarz",
|
||||||
|
"white": "Weiss"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"minutes": "Minuten",
|
||||||
|
"seconds": "Sekunden"
|
||||||
|
},
|
||||||
|
"restartRequired": "Neustart erforderlich",
|
||||||
|
"button": {
|
||||||
|
"save": "Speichern",
|
||||||
|
"reset": "Zurücksetzen",
|
||||||
|
"restart": "Neustart",
|
||||||
|
"forceFullRefresh": "Vollständige Aktualisierung erzwingen"
|
||||||
|
},
|
||||||
|
"timer": {
|
||||||
|
"running": "läuft",
|
||||||
|
"stopped": "gestoppt"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"control": {
|
||||||
|
"keepSameColor": "Gleiche Farbe beibehalten"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rssiBar": {
|
||||||
|
"tooltip": "Werte > -67 dBm gelten als gut. > -30 dBm ist erstaunlich"
|
||||||
|
},
|
||||||
|
"warning": "Achtung",
|
||||||
|
"auto-detect": "Automatische Erkennung"
|
||||||
|
}
|
174
messages/en-US.json
Normal file
174
messages/en-US.json
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"hello_world": "Hello, {name} from en!",
|
||||||
|
"section": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"textColor": "Text color",
|
||||||
|
"backgroundColor": "Background color",
|
||||||
|
"ledPowerOnTest": "LED power-on test",
|
||||||
|
"ledFlashOnBlock": "LED flash on new block",
|
||||||
|
"timePerScreen": "Time per screen",
|
||||||
|
"ledBrightness": "LED brightness",
|
||||||
|
"timezoneOffset": "Timezone offset",
|
||||||
|
"timeBetweenPriceUpdates": "Time between price updates",
|
||||||
|
"fullRefreshEvery": "Full refresh every",
|
||||||
|
"mempoolnstance": "Mempool Instance",
|
||||||
|
"hostnamePrefix": "Hostname prefix",
|
||||||
|
"StealFocusOnNewBlock": "Steal focus on new block",
|
||||||
|
"useBigCharsMcap": "Use big characters for market cap",
|
||||||
|
"useBlkCountdown": "Blocks countdown for halving",
|
||||||
|
"useSatsSymbol": "Use sats symbol",
|
||||||
|
"suffixPrice": "Suffix price format",
|
||||||
|
"disableLeds": "Disable all LEDs effects",
|
||||||
|
"otaUpdates": "OTA updates",
|
||||||
|
"enableMdns": "mDNS",
|
||||||
|
"fetchEuroPrice": "Fetch € price",
|
||||||
|
"shortAmountsWarning": "Short amounts might shorten lifespan of the displays",
|
||||||
|
"tzOffsetHelpText": "A restart is required to apply TZ offset.",
|
||||||
|
"screens": "Screens",
|
||||||
|
"wifiTxPowerText": "In most cases this does not need to be set.",
|
||||||
|
"wifiTxPower": "WiFi TX power",
|
||||||
|
"settingsSaved": "Settings saved",
|
||||||
|
"errorSavingSettings": "Error saving settings",
|
||||||
|
"ownDataSource": "BTClock data source",
|
||||||
|
"flMaxBrightness": "Frontlight brightness",
|
||||||
|
"flAlwaysOn": "Frontlight always on",
|
||||||
|
"flEffectDelay": "Frontlight effect speed",
|
||||||
|
"flFlashOnUpd": "Frontlight flash on new block",
|
||||||
|
"mempoolInstanceHelpText": "Only effective when BTClock data-source is disabled. A restart is required to apply.",
|
||||||
|
"luxLightToggle": "Auto toggle frontlight at lux",
|
||||||
|
"wpTimeout": "WiFi-config portal timeout",
|
||||||
|
"nostrPubKey": "Nostr source pubkey",
|
||||||
|
"nostrZapKey": "Nostr zap pubkey",
|
||||||
|
"nostrRelay": "Nostr Relay",
|
||||||
|
"nostrZapNotify": "Enable Nostr Zap Notifications",
|
||||||
|
"useNostr": "Use Nostr data source",
|
||||||
|
"bitaxeHostname": "BitAxe hostname or IP",
|
||||||
|
"bitaxeEnabled": "Enable BitAxe-integration",
|
||||||
|
"miningPoolStats": "Enable Mining Pool Stats integration",
|
||||||
|
"miningPoolName": "Mining Pool",
|
||||||
|
"miningPoolUser": "Mining Pool username or api key",
|
||||||
|
"nostrZapPubkey": "Nostr Zap pubkey",
|
||||||
|
"invalidNostrPubkey": "Invalid Nostr pubkey, note that your pubkey does NOT start with npub.",
|
||||||
|
"convertingValidNpub": "Converting valid npub to pubkey",
|
||||||
|
"flDisable": "Disable frontlight",
|
||||||
|
"httpAuthEnabled": "Require authentication for WebUI",
|
||||||
|
"httpAuthUser": "WebUI Username",
|
||||||
|
"httpAuthPass": "WebUI Password",
|
||||||
|
"httpAuthText": "Only password-protects WebUI, not API-calls.",
|
||||||
|
"currencies": "Currencies",
|
||||||
|
"customSource": "Use custom data source endpoint",
|
||||||
|
"useNostrTooltip": "Very experimental and unstable. Nostr data source is not required for Nostr Zap notifications.",
|
||||||
|
"mowMode": "Mow Suffix Mode",
|
||||||
|
"suffixShareDot": "Suffix compact notation",
|
||||||
|
"section": {
|
||||||
|
"displaysAndLed": "Displays and LEDs",
|
||||||
|
"screenSettings": "Screen specific",
|
||||||
|
"dataSource": "Data source",
|
||||||
|
"extraFeatures": "Extra features",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
|
"ledFlashOnZap": "LED flash on Nostr Zap",
|
||||||
|
"flFlashOnZap": "Frontlight flash on Nostr Zap",
|
||||||
|
"showAll": "Show all",
|
||||||
|
"hideAll": "Hide all",
|
||||||
|
"flOffWhenDark": "Frontlight off when dark",
|
||||||
|
"luxLightToggleText": "Set to 0 to disable",
|
||||||
|
"verticalDesc": "Use vertical screen description",
|
||||||
|
"enableDebugLog": "Enable Debug-log",
|
||||||
|
"dataSource": {
|
||||||
|
"label": "Data Source",
|
||||||
|
"btclock": "BTClock Data Source",
|
||||||
|
"thirdParty": "mempool.space/Kraken",
|
||||||
|
"nostr": "Nostr publisher",
|
||||||
|
"custom": "Custom Endpoint"
|
||||||
|
},
|
||||||
|
"thirdPartySource": "Use mempool.space/coincap.io",
|
||||||
|
"ceDisableSSL": "Disable SSL",
|
||||||
|
"ceEndpoint": "Endpoint hostname",
|
||||||
|
"fontName": "Font",
|
||||||
|
"timeBasedDnd": "Enable Do Not Disturb time schedule",
|
||||||
|
"dndStartHour": "Start hour",
|
||||||
|
"dndStartMinute": "Start minute",
|
||||||
|
"dndEndHour": "End hour",
|
||||||
|
"dndEndMinute": "End minute"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"systemInfo": "System info",
|
||||||
|
"version": "Version",
|
||||||
|
"buildTime": "Build time",
|
||||||
|
"ledColor": "LED color",
|
||||||
|
"turnOff": "Turn off",
|
||||||
|
"setColor": "Set color",
|
||||||
|
"showText": "Show text",
|
||||||
|
"text": "Text",
|
||||||
|
"title": "Control",
|
||||||
|
"hostname": "Hostname",
|
||||||
|
"frontlight": "Frontlight",
|
||||||
|
"turnOn": "Turn on",
|
||||||
|
"flashFrontlight": "Flash",
|
||||||
|
"firmwareUpdate": "Firmware update",
|
||||||
|
"fwCommit": "Firmware commit"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"title": "Status",
|
||||||
|
"screenCycle": "Screen cycle",
|
||||||
|
"memoryFree": "Memory free",
|
||||||
|
"wsPriceConnection": "WS Price connection",
|
||||||
|
"wsMempoolConnection": "WS {instance} connection",
|
||||||
|
"fetchEuroNote": "If you use \"Fetch € price\" the WS Price connection will show ❌ since it uses another data source.",
|
||||||
|
"uptime": "Uptime",
|
||||||
|
"wifiSignalStrength": "WiFi Signal strength",
|
||||||
|
"wsDataConnection": "BTClock data-source connection",
|
||||||
|
"lightSensor": "Light sensor",
|
||||||
|
"nostrConnection": "Nostr Relay connection",
|
||||||
|
"doNotDisturb": "Do not disturb",
|
||||||
|
"timeBasedDnd": "Time-based schedule"
|
||||||
|
},
|
||||||
|
"firmwareUpdater": {
|
||||||
|
"fileUploadFailed": "File upload failed. Make sure you have selected the correct file and try again.",
|
||||||
|
"fileUploadSuccess": "File uploaded successfully, restarting device and reloading WebUI in {countdown} seconds",
|
||||||
|
"uploading": "Uploading",
|
||||||
|
"firmwareUpdateText": "When you use the firmware upload functionality, make sure you use the correct files. Uploading the wrong files can result in a non-working device. If it goes wrong, you can restore firmware by uploading the full image after setting the device in BOOT-mode.",
|
||||||
|
"swUpdateAvailable": "A newer version is available!",
|
||||||
|
"swUpToDate": "You are up to date.",
|
||||||
|
"latestVersion": "Latest Version",
|
||||||
|
"releaseDate": "Release Date",
|
||||||
|
"viewRelease": "View Release",
|
||||||
|
"autoUpdate": "Install update (experimental)",
|
||||||
|
"autoUpdateInProgress": "Auto-update in progress, please wait..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"black": "Black",
|
||||||
|
"white": "White"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"minutes": "minutes",
|
||||||
|
"seconds": "seconds"
|
||||||
|
},
|
||||||
|
"restartRequired": "restart required",
|
||||||
|
"button": {
|
||||||
|
"save": "Save",
|
||||||
|
"reset": "Reset",
|
||||||
|
"restart": "Restart",
|
||||||
|
"forceFullRefresh": "Force full refresh"
|
||||||
|
},
|
||||||
|
"timer": {
|
||||||
|
"running": "running",
|
||||||
|
"stopped": "stopped"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"control": {
|
||||||
|
"keepSameColor": "Keep same color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rssiBar": {
|
||||||
|
"tooltip": "Values > -67 dBm are considered good. > -30 dBm is amazing"
|
||||||
|
},
|
||||||
|
"warning": "Warning",
|
||||||
|
"auto-detect": "Auto-detect",
|
||||||
|
"on": "on",
|
||||||
|
"off": "off"
|
||||||
|
}
|
172
messages/en.json
Normal file
172
messages/en.json
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"hello_world": "Hello, {name} from en!",
|
||||||
|
"section": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"textColor": "Text color",
|
||||||
|
"backgroundColor": "Background color",
|
||||||
|
"ledPowerOnTest": "LED power-on test",
|
||||||
|
"ledFlashOnBlock": "LED flash on new block",
|
||||||
|
"timePerScreen": "Time per screen",
|
||||||
|
"ledBrightness": "LED brightness",
|
||||||
|
"timezoneOffset": "Timezone offset",
|
||||||
|
"timeBetweenPriceUpdates": "Time between price updates",
|
||||||
|
"fullRefreshEvery": "Full refresh every",
|
||||||
|
"mempoolnstance": "Mempool Instance",
|
||||||
|
"hostnamePrefix": "Hostname prefix",
|
||||||
|
"StealFocusOnNewBlock": "Steal focus on new block",
|
||||||
|
"useBigCharsMcap": "Use big characters for market cap",
|
||||||
|
"useBlkCountdown": "Blocks countdown for halving",
|
||||||
|
"useSatsSymbol": "Use sats symbol",
|
||||||
|
"suffixPrice": "Suffix price format",
|
||||||
|
"disableLeds": "Disable all LEDs effects",
|
||||||
|
"otaUpdates": "OTA updates",
|
||||||
|
"enableMdns": "mDNS",
|
||||||
|
"fetchEuroPrice": "Fetch € price",
|
||||||
|
"shortAmountsWarning": "Short amounts might shorten lifespan of the displays",
|
||||||
|
"tzOffsetHelpText": "A restart is required to apply TZ offset.",
|
||||||
|
"screens": "Screens",
|
||||||
|
"wifiTxPowerText": "In most cases this does not need to be set.",
|
||||||
|
"wifiTxPower": "WiFi TX power",
|
||||||
|
"settingsSaved": "Settings saved",
|
||||||
|
"errorSavingSettings": "Error saving settings",
|
||||||
|
"ownDataSource": "BTClock data source",
|
||||||
|
"flMaxBrightness": "Frontlight brightness",
|
||||||
|
"flAlwaysOn": "Frontlight always on",
|
||||||
|
"flEffectDelay": "Frontlight effect speed",
|
||||||
|
"flFlashOnUpd": "Frontlight flash on new block",
|
||||||
|
"mempoolInstanceHelpText": "Only effective when BTClock data-source is disabled. A restart is required to apply.",
|
||||||
|
"luxLightToggle": "Auto toggle frontlight at lux",
|
||||||
|
"wpTimeout": "WiFi-config portal timeout",
|
||||||
|
"nostrPubKey": "Nostr source pubkey",
|
||||||
|
"nostrZapKey": "Nostr zap pubkey",
|
||||||
|
"nostrRelay": "Nostr Relay",
|
||||||
|
"nostrZapNotify": "Enable Nostr Zap Notifications",
|
||||||
|
"useNostr": "Use Nostr data source",
|
||||||
|
"bitaxeHostname": "BitAxe hostname or IP",
|
||||||
|
"bitaxeEnabled": "Enable BitAxe-integration",
|
||||||
|
"miningPoolStats": "Enable Mining Pool Stats integration",
|
||||||
|
"miningPoolName": "Mining Pool",
|
||||||
|
"miningPoolUser": "Mining Pool username or api key",
|
||||||
|
"nostrZapPubkey": "Nostr Zap pubkey",
|
||||||
|
"invalidNostrPubkey": "Invalid Nostr pubkey, note that your pubkey does NOT start with npub.",
|
||||||
|
"convertingValidNpub": "Converting valid npub to pubkey",
|
||||||
|
"flDisable": "Disable frontlight",
|
||||||
|
"httpAuthEnabled": "Require authentication for WebUI",
|
||||||
|
"httpAuthUser": "WebUI Username",
|
||||||
|
"httpAuthPass": "WebUI Password",
|
||||||
|
"httpAuthText": "Only password-protects WebUI, not API-calls.",
|
||||||
|
"currencies": "Currencies",
|
||||||
|
"customSource": "Use custom data source endpoint",
|
||||||
|
"useNostrTooltip": "Very experimental and unstable. Nostr data source is not required for Nostr Zap notifications.",
|
||||||
|
"mowMode": "Mow Suffix Mode",
|
||||||
|
"suffixShareDot": "Suffix compact notation",
|
||||||
|
"section": {
|
||||||
|
"displaysAndLed": "Displays and LEDs",
|
||||||
|
"screenSettings": "Screen specific",
|
||||||
|
"dataSource": "Data source",
|
||||||
|
"extraFeatures": "Extra features",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
|
"ledFlashOnZap": "LED flash on Nostr Zap",
|
||||||
|
"flFlashOnZap": "Frontlight flash on Nostr Zap",
|
||||||
|
"showAll": "Show all",
|
||||||
|
"hideAll": "Hide all",
|
||||||
|
"flOffWhenDark": "Frontlight off when dark",
|
||||||
|
"luxLightToggleText": "Set to 0 to disable",
|
||||||
|
"verticalDesc": "Use vertical screen description",
|
||||||
|
"enableDebugLog": "Enable Debug-log",
|
||||||
|
"dataSource": {
|
||||||
|
"label": "Data Source",
|
||||||
|
"btclock": "BTClock Data Source",
|
||||||
|
"thirdParty": "mempool.space/Kraken",
|
||||||
|
"nostr": "Nostr publisher",
|
||||||
|
"custom": "Custom Endpoint"
|
||||||
|
},
|
||||||
|
"thirdPartySource": "Use mempool.space/coincap.io",
|
||||||
|
"ceDisableSSL": "Disable SSL",
|
||||||
|
"ceEndpoint": "Endpoint hostname",
|
||||||
|
"fontName": "Font",
|
||||||
|
"timeBasedDnd": "Enable Do Not Disturb time schedule",
|
||||||
|
"dndStartHour": "Start hour",
|
||||||
|
"dndStartMinute": "Start minute",
|
||||||
|
"dndEndHour": "End hour",
|
||||||
|
"dndEndMinute": "End minute"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"systemInfo": "System info",
|
||||||
|
"version": "Version",
|
||||||
|
"buildTime": "Build time",
|
||||||
|
"ledColor": "LED color",
|
||||||
|
"turnOff": "Turn off",
|
||||||
|
"setColor": "Set color",
|
||||||
|
"showText": "Show text",
|
||||||
|
"text": "Text",
|
||||||
|
"title": "Control",
|
||||||
|
"hostname": "Hostname",
|
||||||
|
"frontlight": "Frontlight",
|
||||||
|
"turnOn": "Turn on",
|
||||||
|
"flashFrontlight": "Flash",
|
||||||
|
"firmwareUpdate": "Firmware update",
|
||||||
|
"fwCommit": "Firmware commit"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"title": "Status",
|
||||||
|
"screenCycle": "Screen cycle",
|
||||||
|
"memoryFree": "Memory free",
|
||||||
|
"wsPriceConnection": "WS Price connection",
|
||||||
|
"wsMempoolConnection": "WS {instance} connection",
|
||||||
|
"fetchEuroNote": "If you use \"Fetch € price\" the WS Price connection will show ❌ since it uses another data source.",
|
||||||
|
"uptime": "Uptime",
|
||||||
|
"wifiSignalStrength": "WiFi Signal strength",
|
||||||
|
"wsDataConnection": "BTClock data-source connection",
|
||||||
|
"lightSensor": "Light sensor",
|
||||||
|
"nostrConnection": "Nostr Relay connection",
|
||||||
|
"doNotDisturb": "Do not disturb",
|
||||||
|
"timeBasedDnd": "Time-based schedule"
|
||||||
|
},
|
||||||
|
"firmwareUpdater": {
|
||||||
|
"fileUploadFailed": "File upload failed. Make sure you have selected the correct file and try again.",
|
||||||
|
"fileUploadSuccess": "File uploaded successfully, restarting device and reloading WebUI in {countdown} seconds",
|
||||||
|
"uploading": "Uploading",
|
||||||
|
"firmwareUpdateText": "When you use the firmware upload functionality, make sure you use the correct files. Uploading the wrong files can result in a non-working device. If it goes wrong, you can restore firmware by uploading the full image after setting the device in BOOT-mode.",
|
||||||
|
"swUpdateAvailable": "A newer version is available!",
|
||||||
|
"swUpToDate": "You are up to date.",
|
||||||
|
"latestVersion": "Latest Version",
|
||||||
|
"releaseDate": "Release Date",
|
||||||
|
"viewRelease": "View Release",
|
||||||
|
"autoUpdate": "Install update (experimental)",
|
||||||
|
"autoUpdateInProgress": "Auto-update in progress, please wait..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"black": "Black",
|
||||||
|
"white": "White"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"minutes": "minutes",
|
||||||
|
"seconds": "seconds"
|
||||||
|
},
|
||||||
|
"restartRequired": "restart required",
|
||||||
|
"button": {
|
||||||
|
"save": "Save",
|
||||||
|
"reset": "Reset",
|
||||||
|
"restart": "Restart",
|
||||||
|
"forceFullRefresh": "Force full refresh"
|
||||||
|
},
|
||||||
|
"timer": {
|
||||||
|
"running": "running",
|
||||||
|
"stopped": "stopped"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"control": {
|
||||||
|
"keepSameColor": "Keep same color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rssiBar": {
|
||||||
|
"tooltip": "Values > -67 dBm are considered good. > -30 dBm is amazing"
|
||||||
|
},
|
||||||
|
"warning": "Warning",
|
||||||
|
"auto-detect": "Auto-detect"
|
||||||
|
}
|
152
messages/es-ES.json
Normal file
152
messages/es-ES.json
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"hello_world": "Hello, {name} from es!",
|
||||||
|
"section": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Configuración",
|
||||||
|
"textColor": "Color de texto",
|
||||||
|
"backgroundColor": "Color de fondo",
|
||||||
|
"ledBrightness": "Brillo LED",
|
||||||
|
"screens": "Pantallas",
|
||||||
|
"shortAmountsWarning": "Pequeñas cantidades pueden acortar la vida útil de los displays",
|
||||||
|
"fullRefreshEvery": "Actualización completa cada",
|
||||||
|
"timePerScreen": "Tiempo por pantalla",
|
||||||
|
"tzOffsetHelpText": "Es necesario reiniciar para aplicar la compensación.",
|
||||||
|
"timezoneOffset": "Compensación de zona horaria",
|
||||||
|
"StealFocusOnNewBlock": "Presta atención al nuevo bloque",
|
||||||
|
"ledFlashOnBlock": "El LED parpadea con un bloque nuevo",
|
||||||
|
"useBigCharsMcap": "Utilice caracteres grandes para la market cap",
|
||||||
|
"useBlkCountdown": "Cuenta regresiva en bloques",
|
||||||
|
"useSatsSymbol": "Usar símbolo sats",
|
||||||
|
"fetchEuroPrice": "Obtener precio en €",
|
||||||
|
"timeBetweenPriceUpdates": "Tiempo entre actualizaciones de precios",
|
||||||
|
"ledPowerOnTest": "Prueba de encendido del LED",
|
||||||
|
"enableMdns": "mDNS",
|
||||||
|
"hostnamePrefix": "Prefijo de nombre de host",
|
||||||
|
"mempoolnstance": "Instancia de Mempool",
|
||||||
|
"suffixPrice": "Precio con sufijos",
|
||||||
|
"disableLeds": "Desactivar efectos de LED",
|
||||||
|
"otaUpdates": "Actualización por aire",
|
||||||
|
"wifiTxPowerText": "En la mayoría de los casos no es necesario configurar esto.",
|
||||||
|
"settingsSaved": "Configuración guardada",
|
||||||
|
"errorSavingSettings": "Error al guardar la configuración",
|
||||||
|
"ownDataSource": "fuente de datos BTClock",
|
||||||
|
"flMaxBrightness": "Brillo de luz de la pantalla",
|
||||||
|
"flAlwaysOn": "Luz de la pantalla siempre encendida",
|
||||||
|
"flEffectDelay": "Velocidad del efecto de luz de la pantalla",
|
||||||
|
"flFlashOnUpd": "Luz de la pantalla parpadea con un nuevo bloque",
|
||||||
|
"mempoolInstanceHelpText": "Solo es efectivo cuando la fuente de datos BTClock está deshabilitada. \nEs necesario reiniciar para aplicar.",
|
||||||
|
"luxLightToggle": "Cambio automático de luz frontal en lux",
|
||||||
|
"wpTimeout": "Portal de configuración WiFi timeout",
|
||||||
|
"useNostr": "Utilice la fuente de datos Nostr",
|
||||||
|
"flDisable": "Desactivar luz de la pantalla",
|
||||||
|
"httpAuthUser": "Nombre de usuario WebUI",
|
||||||
|
"httpAuthPass": "Contraseña WebUI",
|
||||||
|
"httpAuthText": "Solo la WebUI está protegida con contraseña, no las llamadas API.",
|
||||||
|
"currencies": "Monedas",
|
||||||
|
"mowMode": "Modo de sufijo Mow",
|
||||||
|
"suffixShareDot": "Notación compacta de sufijo",
|
||||||
|
"section": {
|
||||||
|
"displaysAndLed": "Pantallas y LED",
|
||||||
|
"screenSettings": "Específico de la pantalla",
|
||||||
|
"dataSource": "fuente de datos",
|
||||||
|
"extraFeatures": "Funciones adicionales",
|
||||||
|
"system": "Sistema"
|
||||||
|
},
|
||||||
|
"ledFlashOnZap": "LED parpadeante con Nostr Zap",
|
||||||
|
"flFlashOnZap": "Flash de luz frontal con Nostr Zap",
|
||||||
|
"showAll": "Mostrar todo",
|
||||||
|
"hideAll": "Ocultar todo",
|
||||||
|
"flOffWhenDark": "Luz de la pantalla cuando está oscuro",
|
||||||
|
"luxLightToggleText": "Establecer en 0 para desactivar",
|
||||||
|
"verticalDesc": "Descripción de pantalla vertical",
|
||||||
|
"enableDebugLog": "Habilitar registro de depuración",
|
||||||
|
"bitaxeEnabled": "Habilitar la integración de BitAxe",
|
||||||
|
"miningPoolStats": "Habilitar la integración de estadísticas del grupo minero",
|
||||||
|
"nostrZapNotify": "Habilitar notificaciones de Nostr Zap",
|
||||||
|
"thirdPartySource": "Utilice mempool.space/coincap.io",
|
||||||
|
"dataSource": {
|
||||||
|
"nostr": "editorial nostr",
|
||||||
|
"custom": "Punto final personalizado"
|
||||||
|
},
|
||||||
|
"fontName": "Fuente",
|
||||||
|
"timeBasedDnd": "Habilitar el horario de No molestar",
|
||||||
|
"dndStartHour": "Hora de inicio",
|
||||||
|
"dndStartMinute": "Minuto de inicio",
|
||||||
|
"dndEndHour": "Hora final",
|
||||||
|
"dndEndMinute": "Minuto final"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"turnOff": "Apagar",
|
||||||
|
"setColor": "Establecer el color",
|
||||||
|
"version": "Versión",
|
||||||
|
"ledColor": "color del LED",
|
||||||
|
"systemInfo": "Info del sistema",
|
||||||
|
"showText": "Mostrar texto",
|
||||||
|
"text": "Texto",
|
||||||
|
"title": "Control",
|
||||||
|
"buildTime": "Tiempo de compilación",
|
||||||
|
"hostname": "Nombre del host",
|
||||||
|
"turnOn": "Encender",
|
||||||
|
"frontlight": "Luz de la pantalla",
|
||||||
|
"flashFrontlight": "Luz intermitente"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"memoryFree": "Memoria RAM libre",
|
||||||
|
"wsPriceConnection": "Conexión WebSocket Precio",
|
||||||
|
"wsMempoolConnection": "Conexión WebSocket {instance}",
|
||||||
|
"screenCycle": "Ciclo de pantalla",
|
||||||
|
"uptime": "Tiempo de funcionamiento",
|
||||||
|
"fetchEuroNote": "Si utiliza \"Obtener precio en €\", la conexión de Precio WS mostrará ❌ ya que utiliza otra fuente de datos.",
|
||||||
|
"title": "Estado",
|
||||||
|
"wifiSignalStrength": "Fuerza de la señal WiFi",
|
||||||
|
"wsDataConnection": "Conexión de fuente de datos BTClock",
|
||||||
|
"lightSensor": "Sensor de luz",
|
||||||
|
"nostrConnection": "Conexión de relé Nostr",
|
||||||
|
"doNotDisturb": "No molestar",
|
||||||
|
"timeBasedDnd": "Horario basado en el tiempo"
|
||||||
|
},
|
||||||
|
"firmwareUpdater": {
|
||||||
|
"fileUploadSuccess": "Archivo cargado exitosamente, reiniciando el dispositivo. Recargando WebUI en {countdown} segundos",
|
||||||
|
"fileUploadFailed": "Error al cargar el archivo. \nAsegúrese de haber seleccionado el archivo correcto e inténtelo nuevamente.",
|
||||||
|
"uploading": "Subiendo",
|
||||||
|
"firmwareUpdateText": "Cuando utilice la función de carga de firmware, asegúrese de utilizar los archivos correctos. \nCargar archivos incorrectos puede provocar que el dispositivo no funcione. \nSi sale mal, puede restaurar el firmware cargando la imagen completa después de configurar el dispositivo en modo BOOT.",
|
||||||
|
"swUpToDate": "Tienes la ultima version.",
|
||||||
|
"swUpdateAvailable": "¡Una nueva versión está disponible!",
|
||||||
|
"latestVersion": "Ultima versión",
|
||||||
|
"releaseDate": "Fecha de lanzamiento",
|
||||||
|
"viewRelease": "Ver lanzamiento",
|
||||||
|
"autoUpdate": "Instalar actualización (experimental)",
|
||||||
|
"autoUpdateInProgress": "Actualización automática en progreso, espere..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"save": "Guardar",
|
||||||
|
"reset": "Restaurar",
|
||||||
|
"restart": "Reiniciar",
|
||||||
|
"forceFullRefresh": "Forzar refresco"
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"black": "Negro",
|
||||||
|
"white": "Blanco"
|
||||||
|
},
|
||||||
|
"restartRequired": "reinicio requerido",
|
||||||
|
"time": {
|
||||||
|
"minutes": "minutos",
|
||||||
|
"seconds": "segundos"
|
||||||
|
},
|
||||||
|
"timer": {
|
||||||
|
"running": "funcionando",
|
||||||
|
"stopped": "detenido"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"control": {
|
||||||
|
"keepSameColor": "Mantén el mismo color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rssiBar": {
|
||||||
|
"tooltip": "Se consideran buenos valores > -67 dBm. > -30 dBm es increíble"
|
||||||
|
},
|
||||||
|
"warning": "Aviso",
|
||||||
|
"auto-detect": "Detección automática"
|
||||||
|
}
|
152
messages/es.json
Normal file
152
messages/es.json
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"hello_world": "Hello, {name} from es!",
|
||||||
|
"section": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Configuración",
|
||||||
|
"textColor": "Color de texto",
|
||||||
|
"backgroundColor": "Color de fondo",
|
||||||
|
"ledBrightness": "Brillo LED",
|
||||||
|
"screens": "Pantallas",
|
||||||
|
"shortAmountsWarning": "Pequeñas cantidades pueden acortar la vida útil de los displays",
|
||||||
|
"fullRefreshEvery": "Actualización completa cada",
|
||||||
|
"timePerScreen": "Tiempo por pantalla",
|
||||||
|
"tzOffsetHelpText": "Es necesario reiniciar para aplicar la compensación.",
|
||||||
|
"timezoneOffset": "Compensación de zona horaria",
|
||||||
|
"StealFocusOnNewBlock": "Presta atención al nuevo bloque",
|
||||||
|
"ledFlashOnBlock": "El LED parpadea con un bloque nuevo",
|
||||||
|
"useBigCharsMcap": "Utilice caracteres grandes para la market cap",
|
||||||
|
"useBlkCountdown": "Cuenta regresiva en bloques",
|
||||||
|
"useSatsSymbol": "Usar símbolo sats",
|
||||||
|
"fetchEuroPrice": "Obtener precio en €",
|
||||||
|
"timeBetweenPriceUpdates": "Tiempo entre actualizaciones de precios",
|
||||||
|
"ledPowerOnTest": "Prueba de encendido del LED",
|
||||||
|
"enableMdns": "mDNS",
|
||||||
|
"hostnamePrefix": "Prefijo de nombre de host",
|
||||||
|
"mempoolnstance": "Instancia de Mempool",
|
||||||
|
"suffixPrice": "Precio con sufijos",
|
||||||
|
"disableLeds": "Desactivar efectos de LED",
|
||||||
|
"otaUpdates": "Actualización por aire",
|
||||||
|
"wifiTxPowerText": "En la mayoría de los casos no es necesario configurar esto.",
|
||||||
|
"settingsSaved": "Configuración guardada",
|
||||||
|
"errorSavingSettings": "Error al guardar la configuración",
|
||||||
|
"ownDataSource": "fuente de datos BTClock",
|
||||||
|
"flMaxBrightness": "Brillo de luz de la pantalla",
|
||||||
|
"flAlwaysOn": "Luz de la pantalla siempre encendida",
|
||||||
|
"flEffectDelay": "Velocidad del efecto de luz de la pantalla",
|
||||||
|
"flFlashOnUpd": "Luz de la pantalla parpadea con un nuevo bloque",
|
||||||
|
"mempoolInstanceHelpText": "Solo es efectivo cuando la fuente de datos BTClock está deshabilitada. \nEs necesario reiniciar para aplicar.",
|
||||||
|
"luxLightToggle": "Cambio automático de luz frontal en lux",
|
||||||
|
"wpTimeout": "Portal de configuración WiFi timeout",
|
||||||
|
"useNostr": "Utilice la fuente de datos Nostr",
|
||||||
|
"flDisable": "Desactivar luz de la pantalla",
|
||||||
|
"httpAuthUser": "Nombre de usuario WebUI",
|
||||||
|
"httpAuthPass": "Contraseña WebUI",
|
||||||
|
"httpAuthText": "Solo la WebUI está protegida con contraseña, no las llamadas API.",
|
||||||
|
"currencies": "Monedas",
|
||||||
|
"mowMode": "Modo de sufijo Mow",
|
||||||
|
"suffixShareDot": "Notación compacta de sufijo",
|
||||||
|
"section": {
|
||||||
|
"displaysAndLed": "Pantallas y LED",
|
||||||
|
"screenSettings": "Específico de la pantalla",
|
||||||
|
"dataSource": "fuente de datos",
|
||||||
|
"extraFeatures": "Funciones adicionales",
|
||||||
|
"system": "Sistema"
|
||||||
|
},
|
||||||
|
"ledFlashOnZap": "LED parpadeante con Nostr Zap",
|
||||||
|
"flFlashOnZap": "Flash de luz frontal con Nostr Zap",
|
||||||
|
"showAll": "Mostrar todo",
|
||||||
|
"hideAll": "Ocultar todo",
|
||||||
|
"flOffWhenDark": "Luz de la pantalla cuando está oscuro",
|
||||||
|
"luxLightToggleText": "Establecer en 0 para desactivar",
|
||||||
|
"verticalDesc": "Descripción de pantalla vertical",
|
||||||
|
"enableDebugLog": "Habilitar registro de depuración",
|
||||||
|
"bitaxeEnabled": "Habilitar la integración de BitAxe",
|
||||||
|
"miningPoolStats": "Habilitar la integración de estadísticas del grupo minero",
|
||||||
|
"nostrZapNotify": "Habilitar notificaciones de Nostr Zap",
|
||||||
|
"thirdPartySource": "Utilice mempool.space/coincap.io",
|
||||||
|
"dataSource": {
|
||||||
|
"nostr": "editorial nostr",
|
||||||
|
"custom": "Punto final personalizado"
|
||||||
|
},
|
||||||
|
"fontName": "Fuente",
|
||||||
|
"timeBasedDnd": "Habilitar el horario de No molestar",
|
||||||
|
"dndStartHour": "Hora de inicio",
|
||||||
|
"dndStartMinute": "Minuto de inicio",
|
||||||
|
"dndEndHour": "Hora final",
|
||||||
|
"dndEndMinute": "Minuto final"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"turnOff": "Apagar",
|
||||||
|
"setColor": "Establecer el color",
|
||||||
|
"version": "Versión",
|
||||||
|
"ledColor": "color del LED",
|
||||||
|
"systemInfo": "Info del sistema",
|
||||||
|
"showText": "Mostrar texto",
|
||||||
|
"text": "Texto",
|
||||||
|
"title": "Control",
|
||||||
|
"buildTime": "Tiempo de compilación",
|
||||||
|
"hostname": "Nombre del host",
|
||||||
|
"turnOn": "Encender",
|
||||||
|
"frontlight": "Luz de la pantalla",
|
||||||
|
"flashFrontlight": "Luz intermitente"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"memoryFree": "Memoria RAM libre",
|
||||||
|
"wsPriceConnection": "Conexión WebSocket Precio",
|
||||||
|
"wsMempoolConnection": "Conexión WebSocket {instance}",
|
||||||
|
"screenCycle": "Ciclo de pantalla",
|
||||||
|
"uptime": "Tiempo de funcionamiento",
|
||||||
|
"fetchEuroNote": "Si utiliza \"Obtener precio en €\", la conexión de Precio WS mostrará ❌ ya que utiliza otra fuente de datos.",
|
||||||
|
"title": "Estado",
|
||||||
|
"wifiSignalStrength": "Fuerza de la señal WiFi",
|
||||||
|
"wsDataConnection": "Conexión de fuente de datos BTClock",
|
||||||
|
"lightSensor": "Sensor de luz",
|
||||||
|
"nostrConnection": "Conexión de relé Nostr",
|
||||||
|
"doNotDisturb": "No molestar",
|
||||||
|
"timeBasedDnd": "Horario basado en el tiempo"
|
||||||
|
},
|
||||||
|
"firmwareUpdater": {
|
||||||
|
"fileUploadSuccess": "Archivo cargado exitosamente, reiniciando el dispositivo. Recargando WebUI en {countdown} segundos",
|
||||||
|
"fileUploadFailed": "Error al cargar el archivo. \nAsegúrese de haber seleccionado el archivo correcto e inténtelo nuevamente.",
|
||||||
|
"uploading": "Subiendo",
|
||||||
|
"firmwareUpdateText": "Cuando utilice la función de carga de firmware, asegúrese de utilizar los archivos correctos. \nCargar archivos incorrectos puede provocar que el dispositivo no funcione. \nSi sale mal, puede restaurar el firmware cargando la imagen completa después de configurar el dispositivo en modo BOOT.",
|
||||||
|
"swUpToDate": "Tienes la ultima version.",
|
||||||
|
"swUpdateAvailable": "¡Una nueva versión está disponible!",
|
||||||
|
"latestVersion": "Ultima versión",
|
||||||
|
"releaseDate": "Fecha de lanzamiento",
|
||||||
|
"viewRelease": "Ver lanzamiento",
|
||||||
|
"autoUpdate": "Instalar actualización (experimental)",
|
||||||
|
"autoUpdateInProgress": "Actualización automática en progreso, espere..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"save": "Guardar",
|
||||||
|
"reset": "Restaurar",
|
||||||
|
"restart": "Reiniciar",
|
||||||
|
"forceFullRefresh": "Forzar refresco"
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"black": "Negro",
|
||||||
|
"white": "Blanco"
|
||||||
|
},
|
||||||
|
"restartRequired": "reinicio requerido",
|
||||||
|
"time": {
|
||||||
|
"minutes": "minutos",
|
||||||
|
"seconds": "segundos"
|
||||||
|
},
|
||||||
|
"timer": {
|
||||||
|
"running": "funcionando",
|
||||||
|
"stopped": "detenido"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"control": {
|
||||||
|
"keepSameColor": "Mantén el mismo color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rssiBar": {
|
||||||
|
"tooltip": "Se consideran buenos valores > -67 dBm. > -30 dBm es increíble"
|
||||||
|
},
|
||||||
|
"warning": "Aviso",
|
||||||
|
"auto-detect": "Detección automática"
|
||||||
|
}
|
142
messages/nl-NL.json
Normal file
142
messages/nl-NL.json
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"section": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Instellingen",
|
||||||
|
"textColor": "Tekstkleur",
|
||||||
|
"backgroundColor": "Achtergrondkleur",
|
||||||
|
"timeBetweenPriceUpdates": "Tijd tussen prijs updates",
|
||||||
|
"timezoneOffset": "Tijdzone afwijking",
|
||||||
|
"ledBrightness": "LED helderheid",
|
||||||
|
"timePerScreen": "Tijd per scherm",
|
||||||
|
"fullRefreshEvery": "Volledig verversen elke",
|
||||||
|
"shortAmountsWarning": "Lage waardes verkorten mogelijk levensduur schermen",
|
||||||
|
"tzOffsetHelpText": "Herstart nodig voor toepassen afwijking.",
|
||||||
|
"enableMdns": "mDNS",
|
||||||
|
"ledPowerOnTest": "LED test bij aanzetten",
|
||||||
|
"StealFocusOnNewBlock": "Pak aandacht bij nieuw blok",
|
||||||
|
"ledFlashOnBlock": "Knipper led bij nieuw blok",
|
||||||
|
"useBigCharsMcap": "Gebruik grote tekens bij market cap",
|
||||||
|
"useBlkCountdown": "Blocks aftellen voor halving",
|
||||||
|
"useSatsSymbol": "Gebruik sats symbol",
|
||||||
|
"fetchEuroPrice": "Toon € prijs",
|
||||||
|
"screens": "Schermen",
|
||||||
|
"hostnamePrefix": "Hostnaam voorvoegsel",
|
||||||
|
"mempoolnstance": "Mempool instantie",
|
||||||
|
"suffixPrice": "Achtervoegsel prijs formaat",
|
||||||
|
"disableLeds": "Alle LEDs effecten uit",
|
||||||
|
"otaUpdates": "OTA updates",
|
||||||
|
"wifiTxPower": "WiFi TX power",
|
||||||
|
"wifiTxPowerText": "Meestal hoeft dit niet aangepast te worden.",
|
||||||
|
"settingsSaved": "Instellingen opgeslagen",
|
||||||
|
"errorSavingSettings": "Fout bij opslaan instellingen",
|
||||||
|
"ownDataSource": "BTClock-gegevensbron gebruiken",
|
||||||
|
"flMaxBrightness": "Displaylicht helderheid",
|
||||||
|
"flAlwaysOn": "Displaylicht altijd aan",
|
||||||
|
"flEffectDelay": "Displaylicht effect snelheid",
|
||||||
|
"flFlashOnUpd": "Knipper displaylicht bij nieuw blok",
|
||||||
|
"mempoolInstanceHelpText": "Alleen effectief als de BTClock-gegevensbron is uitgeschakeld. \nOm toe te passen is een herstart nodig.",
|
||||||
|
"luxLightToggle": "Schakelen displaylicht op lux",
|
||||||
|
"wpTimeout": "WiFi-config-portal timeout",
|
||||||
|
"useNostr": "Gebruik Nostr-gegevensbron",
|
||||||
|
"flDisable": "Schakel Displaylicht uit",
|
||||||
|
"httpAuthUser": "WebUI-gebruikersnaam",
|
||||||
|
"httpAuthPass": "WebUI-wachtwoord",
|
||||||
|
"httpAuthText": "Beveiligd enkel WebUI, niet de API.",
|
||||||
|
"currencies": "Valuta's",
|
||||||
|
"mowMode": "Mow achtervoegsel",
|
||||||
|
"suffixShareDot": "Achtervoegsel compacte notatie",
|
||||||
|
"section": {
|
||||||
|
"displaysAndLed": "Displays en LED's",
|
||||||
|
"screenSettings": "Schermspecifiek",
|
||||||
|
"dataSource": "Gegevensbron",
|
||||||
|
"extraFeatures": "Extra functies",
|
||||||
|
"system": "Systeem"
|
||||||
|
},
|
||||||
|
"ledFlashOnZap": "Knipper LED bij Nostr Zap",
|
||||||
|
"flFlashOnZap": "Knipper displaylicht bij Nostr Zap",
|
||||||
|
"showAll": "Toon alles",
|
||||||
|
"hideAll": "Alles verbergen",
|
||||||
|
"flOffWhenDark": "Displaylicht uit als het donker is",
|
||||||
|
"luxLightToggleText": "Stel in op 0 om uit te schakelen",
|
||||||
|
"verticalDesc": "Verticale schermbeschrijving",
|
||||||
|
"fontName": "Lettertype",
|
||||||
|
"timeBasedDnd": "Schakel het tijdschema Niet storen in",
|
||||||
|
"dndStartHour": "Begin uur",
|
||||||
|
"dndStartMinute": "Beginminuut",
|
||||||
|
"dndEndHour": "Eind uur",
|
||||||
|
"dndEndMinute": "Einde minuut"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"systemInfo": "Systeeminformatie",
|
||||||
|
"version": "Versie",
|
||||||
|
"buildTime": "Bouwtijd",
|
||||||
|
"setColor": "Kleur instellen",
|
||||||
|
"turnOff": "Uitzetten",
|
||||||
|
"ledColor": "LED kleur",
|
||||||
|
"showText": "Toon tekst",
|
||||||
|
"text": "Tekst",
|
||||||
|
"title": "Besturing",
|
||||||
|
"frontlight": "Displaylicht",
|
||||||
|
"turnOn": "Aanzetten",
|
||||||
|
"flashFrontlight": "Knipper"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"title": "Status",
|
||||||
|
"memoryFree": "Geheugen vrij",
|
||||||
|
"screenCycle": "Scherm cyclus",
|
||||||
|
"wsPriceConnection": "WS Prijs verbinding",
|
||||||
|
"wsMempoolConnection": "WS {instance} verbinding",
|
||||||
|
"fetchEuroNote": "Wanneer je \"Toon € prijs\" aanzet, zal de prijsverbinding als ❌ verbroken getoond worden vanwege het gebruik van een andere bron.",
|
||||||
|
"uptime": "Uptime",
|
||||||
|
"wifiSignalStrength": "WiFi signaalsterkte",
|
||||||
|
"wsDataConnection": "BTClock-gegevensbron verbinding",
|
||||||
|
"lightSensor": "Licht sensor",
|
||||||
|
"nostrConnection": "Nostr Relay-verbinding",
|
||||||
|
"doNotDisturb": "Niet storen",
|
||||||
|
"timeBasedDnd": "Op tijd gebaseerd schema"
|
||||||
|
},
|
||||||
|
"firmwareUpdater": {
|
||||||
|
"fileUploadSuccess": "Bestand geüpload, apparaat herstart. WebUI opnieuw geladen over {countdown} seconden",
|
||||||
|
"fileUploadFailed": "Bestandsupload mislukt. \nZorg ervoor dat het juiste bestand is geselecteerd en probeer het opnieuw.",
|
||||||
|
"uploading": "Uploaden",
|
||||||
|
"firmwareUpdateText": "Zorg bij het gebruiken van de firmware upload dat de juiste bestanden gebruikt worden. \nHet uploaden van de verkeerde bestanden kan resulteren in een niet-werkend apparaat. \nAls het misgaat, kunt u de firmware herstellen door de volledige afbeelding te uploaden nadat u het apparaat in de BOOT-modus hebt gezet.",
|
||||||
|
"swUpToDate": "Je hebt de nieuwste versie.",
|
||||||
|
"swUpdateAvailable": "Een nieuwere versie is beschikbaar!",
|
||||||
|
"latestVersion": "Laatste versie",
|
||||||
|
"releaseDate": "Datum van publicatie",
|
||||||
|
"viewRelease": "Bekijk publicatie",
|
||||||
|
"autoUpdate": "Update installeren (experimenteel)",
|
||||||
|
"autoUpdateInProgress": "Automatische update wordt uitgevoerd. Even geduld a.u.b...."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"black": "Zwart",
|
||||||
|
"white": "Wit"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"minutes": "minuten",
|
||||||
|
"seconds": "seconden"
|
||||||
|
},
|
||||||
|
"restartRequired": "herstart nodig",
|
||||||
|
"button": {
|
||||||
|
"save": "Opslaan",
|
||||||
|
"reset": "Herstel",
|
||||||
|
"restart": "Herstart",
|
||||||
|
"forceFullRefresh": "Forceer scherm verversen"
|
||||||
|
},
|
||||||
|
"timer": {
|
||||||
|
"running": "actief",
|
||||||
|
"stopped": "gestopt"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"control": {
|
||||||
|
"keepSameColor": "Behoud zelfde kleur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rssiBar": {
|
||||||
|
"tooltip": "Waarden > -67 dBm zijn goed. > -30 dBm is verbazingwekkend"
|
||||||
|
},
|
||||||
|
"warning": "Waarschuwing",
|
||||||
|
"auto-detect": "Automatische detectie"
|
||||||
|
}
|
142
messages/nl.json
Normal file
142
messages/nl.json
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
|
"section": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Instellingen",
|
||||||
|
"textColor": "Tekstkleur",
|
||||||
|
"backgroundColor": "Achtergrondkleur",
|
||||||
|
"timeBetweenPriceUpdates": "Tijd tussen prijs updates",
|
||||||
|
"timezoneOffset": "Tijdzone afwijking",
|
||||||
|
"ledBrightness": "LED helderheid",
|
||||||
|
"timePerScreen": "Tijd per scherm",
|
||||||
|
"fullRefreshEvery": "Volledig verversen elke",
|
||||||
|
"shortAmountsWarning": "Lage waardes verkorten mogelijk levensduur schermen",
|
||||||
|
"tzOffsetHelpText": "Herstart nodig voor toepassen afwijking.",
|
||||||
|
"enableMdns": "mDNS",
|
||||||
|
"ledPowerOnTest": "LED test bij aanzetten",
|
||||||
|
"StealFocusOnNewBlock": "Pak aandacht bij nieuw blok",
|
||||||
|
"ledFlashOnBlock": "Knipper led bij nieuw blok",
|
||||||
|
"useBigCharsMcap": "Gebruik grote tekens bij market cap",
|
||||||
|
"useBlkCountdown": "Blocks aftellen voor halving",
|
||||||
|
"useSatsSymbol": "Gebruik sats symbol",
|
||||||
|
"fetchEuroPrice": "Toon € prijs",
|
||||||
|
"screens": "Schermen",
|
||||||
|
"hostnamePrefix": "Hostnaam voorvoegsel",
|
||||||
|
"mempoolnstance": "Mempool instantie",
|
||||||
|
"suffixPrice": "Achtervoegsel prijs formaat",
|
||||||
|
"disableLeds": "Alle LEDs effecten uit",
|
||||||
|
"otaUpdates": "OTA updates",
|
||||||
|
"wifiTxPower": "WiFi TX power",
|
||||||
|
"wifiTxPowerText": "Meestal hoeft dit niet aangepast te worden.",
|
||||||
|
"settingsSaved": "Instellingen opgeslagen",
|
||||||
|
"errorSavingSettings": "Fout bij opslaan instellingen",
|
||||||
|
"ownDataSource": "BTClock-gegevensbron gebruiken",
|
||||||
|
"flMaxBrightness": "Displaylicht helderheid",
|
||||||
|
"flAlwaysOn": "Displaylicht altijd aan",
|
||||||
|
"flEffectDelay": "Displaylicht effect snelheid",
|
||||||
|
"flFlashOnUpd": "Knipper displaylicht bij nieuw blok",
|
||||||
|
"mempoolInstanceHelpText": "Alleen effectief als de BTClock-gegevensbron is uitgeschakeld. \nOm toe te passen is een herstart nodig.",
|
||||||
|
"luxLightToggle": "Schakelen displaylicht op lux",
|
||||||
|
"wpTimeout": "WiFi-config-portal timeout",
|
||||||
|
"useNostr": "Gebruik Nostr-gegevensbron",
|
||||||
|
"flDisable": "Schakel Displaylicht uit",
|
||||||
|
"httpAuthUser": "WebUI-gebruikersnaam",
|
||||||
|
"httpAuthPass": "WebUI-wachtwoord",
|
||||||
|
"httpAuthText": "Beveiligd enkel WebUI, niet de API.",
|
||||||
|
"currencies": "Valuta's",
|
||||||
|
"mowMode": "Mow achtervoegsel",
|
||||||
|
"suffixShareDot": "Achtervoegsel compacte notatie",
|
||||||
|
"section": {
|
||||||
|
"displaysAndLed": "Displays en LED's",
|
||||||
|
"screenSettings": "Schermspecifiek",
|
||||||
|
"dataSource": "Gegevensbron",
|
||||||
|
"extraFeatures": "Extra functies",
|
||||||
|
"system": "Systeem"
|
||||||
|
},
|
||||||
|
"ledFlashOnZap": "Knipper LED bij Nostr Zap",
|
||||||
|
"flFlashOnZap": "Knipper displaylicht bij Nostr Zap",
|
||||||
|
"showAll": "Toon alles",
|
||||||
|
"hideAll": "Alles verbergen",
|
||||||
|
"flOffWhenDark": "Displaylicht uit als het donker is",
|
||||||
|
"luxLightToggleText": "Stel in op 0 om uit te schakelen",
|
||||||
|
"verticalDesc": "Verticale schermbeschrijving",
|
||||||
|
"fontName": "Lettertype",
|
||||||
|
"timeBasedDnd": "Schakel het tijdschema Niet storen in",
|
||||||
|
"dndStartHour": "Begin uur",
|
||||||
|
"dndStartMinute": "Beginminuut",
|
||||||
|
"dndEndHour": "Eind uur",
|
||||||
|
"dndEndMinute": "Einde minuut"
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"systemInfo": "Systeeminformatie",
|
||||||
|
"version": "Versie",
|
||||||
|
"buildTime": "Bouwtijd",
|
||||||
|
"setColor": "Kleur instellen",
|
||||||
|
"turnOff": "Uitzetten",
|
||||||
|
"ledColor": "LED kleur",
|
||||||
|
"showText": "Toon tekst",
|
||||||
|
"text": "Tekst",
|
||||||
|
"title": "Besturing",
|
||||||
|
"frontlight": "Displaylicht",
|
||||||
|
"turnOn": "Aanzetten",
|
||||||
|
"flashFrontlight": "Knipper"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"title": "Status",
|
||||||
|
"memoryFree": "Geheugen vrij",
|
||||||
|
"screenCycle": "Scherm cyclus",
|
||||||
|
"wsPriceConnection": "WS Prijs verbinding",
|
||||||
|
"wsMempoolConnection": "WS {instance} verbinding",
|
||||||
|
"fetchEuroNote": "Wanneer je \"Toon € prijs\" aanzet, zal de prijsverbinding als ❌ verbroken getoond worden vanwege het gebruik van een andere bron.",
|
||||||
|
"uptime": "Uptime",
|
||||||
|
"wifiSignalStrength": "WiFi signaalsterkte",
|
||||||
|
"wsDataConnection": "BTClock-gegevensbron verbinding",
|
||||||
|
"lightSensor": "Licht sensor",
|
||||||
|
"nostrConnection": "Nostr Relay-verbinding",
|
||||||
|
"doNotDisturb": "Niet storen",
|
||||||
|
"timeBasedDnd": "Op tijd gebaseerd schema"
|
||||||
|
},
|
||||||
|
"firmwareUpdater": {
|
||||||
|
"fileUploadSuccess": "Bestand geüpload, apparaat herstart. WebUI opnieuw geladen over {countdown} seconden",
|
||||||
|
"fileUploadFailed": "Bestandsupload mislukt. \nZorg ervoor dat het juiste bestand is geselecteerd en probeer het opnieuw.",
|
||||||
|
"uploading": "Uploaden",
|
||||||
|
"firmwareUpdateText": "Zorg bij het gebruiken van de firmware upload dat de juiste bestanden gebruikt worden. \nHet uploaden van de verkeerde bestanden kan resulteren in een niet-werkend apparaat. \nAls het misgaat, kunt u de firmware herstellen door de volledige afbeelding te uploaden nadat u het apparaat in de BOOT-modus hebt gezet.",
|
||||||
|
"swUpToDate": "Je hebt de nieuwste versie.",
|
||||||
|
"swUpdateAvailable": "Een nieuwere versie is beschikbaar!",
|
||||||
|
"latestVersion": "Laatste versie",
|
||||||
|
"releaseDate": "Datum van publicatie",
|
||||||
|
"viewRelease": "Bekijk publicatie",
|
||||||
|
"autoUpdate": "Update installeren (experimenteel)",
|
||||||
|
"autoUpdateInProgress": "Automatische update wordt uitgevoerd. Even geduld a.u.b...."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"black": "Zwart",
|
||||||
|
"white": "Wit"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"minutes": "minuten",
|
||||||
|
"seconds": "seconden"
|
||||||
|
},
|
||||||
|
"restartRequired": "herstart nodig",
|
||||||
|
"button": {
|
||||||
|
"save": "Opslaan",
|
||||||
|
"reset": "Herstel",
|
||||||
|
"restart": "Herstart",
|
||||||
|
"forceFullRefresh": "Forceer scherm verversen"
|
||||||
|
},
|
||||||
|
"timer": {
|
||||||
|
"running": "actief",
|
||||||
|
"stopped": "gestopt"
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"control": {
|
||||||
|
"keepSameColor": "Behoud zelfde kleur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rssiBar": {
|
||||||
|
"tooltip": "Waarden > -67 dBm zijn goed. > -30 dBm is verbazingwekkend"
|
||||||
|
},
|
||||||
|
"warning": "Waarschuwing",
|
||||||
|
"auto-detect": "Automatische detectie"
|
||||||
|
}
|
59
package.json
Normal file
59
package.json
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"name": "btclock-webui-tailwind",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"test:unit": "vitest",
|
||||||
|
"test": "npm run test:unit -- --run && npm run test:e2e",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.2.5",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@playwright/test": "^1.49.1",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
|
"@sveltejs/kit": "^2.16.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/svelte": "^5.2.4",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"typescript-eslint": "^8.20.0",
|
||||||
|
"vite": "^6.2.6",
|
||||||
|
"vitest": "^3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource-variable/oswald": "^5.2.5",
|
||||||
|
"@fontsource/ubuntu": "^5.2.5",
|
||||||
|
"@inlang/paraglide-js": "^2.0.0",
|
||||||
|
"daisyui": "^5.0.35"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
],
|
||||||
|
"patchedDependencies": {
|
||||||
|
"@sveltejs/kit": "patches/@sveltejs__kit.patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
patches/@sveltejs__kit.patch
Normal file
29
patches/@sveltejs__kit.patch
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
diff --git a/src/exports/vite/index.js b/src/exports/vite/index.js
|
||||||
|
index c2e445e865d2fd1f31f28638fa314c725dc53674..26fa6519264be7bb475f9a9ec4de097f58da4103 100644
|
||||||
|
--- a/src/exports/vite/index.js
|
||||||
|
+++ b/src/exports/vite/index.js
|
||||||
|
@@ -670,9 +670,9 @@ Tips:
|
||||||
|
output: {
|
||||||
|
format: inline ? 'iife' : 'esm',
|
||||||
|
name: `__sveltekit_${version_hash}.app`,
|
||||||
|
- entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`,
|
||||||
|
- chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[hash].${ext}`,
|
||||||
|
- assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
|
||||||
|
+ entryFileNames: ssr ? '[name].js' : `${prefix}/[hash].${ext}`,
|
||||||
|
+ chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/c[hash].${ext}`,
|
||||||
|
+ assetFileNames: `${prefix}/a[hash][extname]`,
|
||||||
|
hoistTransitiveImports: false,
|
||||||
|
sourcemapIgnoreList,
|
||||||
|
manualChunks: split ? undefined : () => 'bundle',
|
||||||
|
@@ -699,9 +699,9 @@ Tips:
|
||||||
|
worker: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
- entryFileNames: `${prefix}/workers/[name]-[hash].js`,
|
||||||
|
+ entryFileNames: `${prefix}/workers/[hash].js`,
|
||||||
|
chunkFileNames: `${prefix}/workers/chunks/[hash].js`,
|
||||||
|
- assetFileNames: `${prefix}/workers/assets/[name]-[hash][extname]`,
|
||||||
|
+ assetFileNames: `${prefix}/workers/assets/[hash][extname]`,
|
||||||
|
hoistTransitiveImports: false
|
||||||
|
}
|
||||||
|
}
|
9
playwright.config.ts
Normal file
9
playwright.config.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run build && npm run preview',
|
||||||
|
port: 4173
|
||||||
|
},
|
||||||
|
testDir: 'e2e'
|
||||||
|
});
|
4157
pnpm-lock.yaml
generated
Normal file
4157
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
1
project.inlang/.gitignore
vendored
Normal file
1
project.inlang/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
cache
|
1
project.inlang/project_id
Normal file
1
project.inlang/project_id
Normal file
|
@ -0,0 +1 @@
|
||||||
|
HdrJCBg8oujVtKYW00
|
12
project.inlang/settings.json
Normal file
12
project.inlang/settings.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://inlang.com/schema/project-settings",
|
||||||
|
"modules": [
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
|
||||||
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
|
||||||
|
],
|
||||||
|
"plugin.inlang.messageFormat": {
|
||||||
|
"pathPattern": "./messages/{locale}.json"
|
||||||
|
},
|
||||||
|
"baseLocale": "en-US",
|
||||||
|
"locales": ["en-US", "es-ES", "nl-NL", "de-DE"]
|
||||||
|
}
|
26
src/app.css
Normal file
26
src/app.css
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
@import 'tailwindcss';
|
||||||
|
@plugin "daisyui" {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #3b82f6;
|
||||||
|
--secondary: #6b7280;
|
||||||
|
--accent: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
@apply bg-base-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-base-200 pt-16;
|
||||||
|
}
|
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
12
src/app.html
Normal file
12
src/app.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="%paraglide.lang%">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents" class="h-full">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
src/demo.spec.ts
Normal file
7
src/demo.spec.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('sum test', () => {
|
||||||
|
it('adds 1 + 2 to equal 3', () => {
|
||||||
|
expect(1 + 2).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
13
src/hooks.server.ts
Normal file
13
src/hooks.server.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
import { paraglideMiddleware } from '$lib/paraglide/server';
|
||||||
|
|
||||||
|
const handleParaglide: Handle = ({ event, resolve }) =>
|
||||||
|
paraglideMiddleware(event.request, ({ request, locale }) => {
|
||||||
|
event.request = request;
|
||||||
|
|
||||||
|
return resolve(event, {
|
||||||
|
transformPageChunk: ({ html }) => html.replace('%paraglide.lang%', locale)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const handle: Handle = handleParaglide;
|
3
src/hooks.ts
Normal file
3
src/hooks.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { deLocalizeUrl } from '$lib/paraglide/runtime';
|
||||||
|
|
||||||
|
export const reroute = (request) => deLocalizeUrl(request.url).pathname;
|
112
src/lib/clockControl.ts
Normal file
112
src/lib/clockControl.ts
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { PUBLIC_BASE_URL } from '$env/static/public';
|
||||||
|
import { baseUrl } from './env';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets custom text to display on the clock
|
||||||
|
*/
|
||||||
|
export const setCustomText = (newText: string) => {
|
||||||
|
return fetch(`${baseUrl}/api/show/text/${newText}`).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the LED colors
|
||||||
|
*/
|
||||||
|
export const setLEDcolor = (ledStatus: { hex: string }[]) => {
|
||||||
|
return fetch(`${baseUrl}/api/lights/set`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(ledStatus)
|
||||||
|
}).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns off all LEDs
|
||||||
|
*/
|
||||||
|
export const turnOffLeds = () => {
|
||||||
|
return fetch(`${baseUrl}/api/lights/off`).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restarts the clock
|
||||||
|
*/
|
||||||
|
export const restartClock = () => {
|
||||||
|
return fetch(`${baseUrl}/api/restart`).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces a full refresh of the clock
|
||||||
|
*/
|
||||||
|
export const forceFullRefresh = () => {
|
||||||
|
return fetch(`${baseUrl}/api/full_refresh`).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random color hex code
|
||||||
|
*/
|
||||||
|
export const generateRandomColor = () => {
|
||||||
|
return `#${Math.floor(Math.random() * 16777215)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(6, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active screen
|
||||||
|
*/
|
||||||
|
export const setActiveScreen = async (screenId: string) => {
|
||||||
|
return fetch(`${baseUrl}/api/show/screen/${screenId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active currency
|
||||||
|
*/
|
||||||
|
export const setActiveCurrency = async (currency: string) => {
|
||||||
|
return fetch(`${baseUrl}/api/show/currency/${currency}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns on the frontlight
|
||||||
|
*/
|
||||||
|
export const turnOnFrontlight = () => {
|
||||||
|
return fetch(`${baseUrl}/api/frontlight/on`).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flashes the frontlight
|
||||||
|
*/
|
||||||
|
export const flashFrontlight = () => {
|
||||||
|
return fetch(`${baseUrl}/api/frontlight/flash`).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns off the frontlight
|
||||||
|
*/
|
||||||
|
export const turnOffFrontlight = () => {
|
||||||
|
return fetch(`${baseUrl}/api/frontlight/off`).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the timer
|
||||||
|
*/
|
||||||
|
export const toggleTimer = (currentStatus: boolean) => (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentStatus) {
|
||||||
|
fetch(`${baseUrl}/api/action/pause`);
|
||||||
|
} else {
|
||||||
|
fetch(`${baseUrl}/api/action/timer_restart`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the do not disturb mode
|
||||||
|
*/
|
||||||
|
export const toggleDoNotDisturb = (currentStatus: boolean) => (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log(currentStatus);
|
||||||
|
if (!currentStatus) {
|
||||||
|
fetch(`${baseUrl}/api/dnd/enable`);
|
||||||
|
} else {
|
||||||
|
fetch(`${baseUrl}/api/dnd/disable`);
|
||||||
|
}
|
||||||
|
};
|
382
src/lib/components/BTClock.svelte
Normal file
382
src/lib/components/BTClock.svelte
Normal file
|
@ -0,0 +1,382 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import '@fontsource-variable/oswald'; // Import the Oswald variable font
|
||||||
|
import '@fontsource/ubuntu';
|
||||||
|
|
||||||
|
type DisplayMode = 'single' | 'medium' | 'split';
|
||||||
|
type DisplayTheme = 'light' | 'dark'; // New type for display theme
|
||||||
|
|
||||||
|
// Props for the component
|
||||||
|
export let displays = ['B', 'T', 'C', 'L', 'O', 'C', 'K'];
|
||||||
|
export let mode: DisplayMode | DisplayMode[] = 'single'; // 'single', 'medium', 'split', or an array of modes
|
||||||
|
export let mixedMode = false; // Whether to use mixed modes for displays
|
||||||
|
export let theme: DisplayTheme = 'dark'; // New prop for display theme (default: dark)
|
||||||
|
export let containerRadius = '0.5rem'; // New prop for container border radius
|
||||||
|
export let displayRadius = '6px'; // New prop for display border radius
|
||||||
|
export let splitTextPadding = '2px'; // New prop for padding between split text and separator
|
||||||
|
export let primaryColor = 'gold'; // Primary color (default: blue)
|
||||||
|
export let frameBorderColor = '#000000'; // Frame border color (default: same as primary)
|
||||||
|
export let screwColor = 'orange'; // Screw color (default: same as primary)
|
||||||
|
export let verticalDesc = false;
|
||||||
|
|
||||||
|
// For responsive sizing
|
||||||
|
let containerWidth = 0;
|
||||||
|
// let containerHeight = 0;
|
||||||
|
let containerElement: HTMLDivElement;
|
||||||
|
let initialWidth = 0;
|
||||||
|
|
||||||
|
// Device dimensions
|
||||||
|
const deviceWidth = 224; // mm
|
||||||
|
const deviceHeight = 85; // mm
|
||||||
|
// const deviceRatio = deviceHeight / deviceWidth; // Overall device aspect ratio
|
||||||
|
|
||||||
|
// Display dimensions
|
||||||
|
// const displayAspectRatio = 122 / 250; // Width to height ratio of each display
|
||||||
|
|
||||||
|
// Function to get the display mode for a specific index
|
||||||
|
function getDisplayMode(index: number): DisplayMode {
|
||||||
|
if (displays[index].length > 1) {
|
||||||
|
if (displays[index].includes('/')) {
|
||||||
|
return 'split';
|
||||||
|
} else {
|
||||||
|
return 'medium';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'single';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get the content parts for split text
|
||||||
|
function getSplitParts(content: string): [string, string] {
|
||||||
|
const parts = content.split('/');
|
||||||
|
return [parts[0] || '', parts[1] || ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store initial width on first render
|
||||||
|
$: if (containerWidth > 0 && initialWidth === 0) {
|
||||||
|
initialWidth = containerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Calculate container height based on width
|
||||||
|
// $: if (containerWidth > 0) {
|
||||||
|
// containerHeight = containerWidth * deviceRatio;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const fontSizeSingle = '4.5rem';
|
||||||
|
const fontSizeMedium = '2.0rem';
|
||||||
|
const fontSizeSplit = '1.0rem';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="btclock-container"
|
||||||
|
bind:this={containerElement}
|
||||||
|
bind:clientWidth={containerWidth}
|
||||||
|
style="aspect-ratio: {deviceWidth} / {deviceHeight};
|
||||||
|
border-radius: {containerRadius};
|
||||||
|
border-color: {frameBorderColor};"
|
||||||
|
>
|
||||||
|
<div class="screw-top-left" style="background-color: {screwColor};"></div>
|
||||||
|
<div class="screw-top-right" style="background-color: {screwColor};"></div>
|
||||||
|
<div class="screw-bottom-left" style="background-color: {screwColor};"></div>
|
||||||
|
<div class="screw-bottom-right" style="background-color: {screwColor};"></div>
|
||||||
|
|
||||||
|
<div class="displays-row">
|
||||||
|
{#each displays as display, i (i)}
|
||||||
|
<div class="btclock-display-wrapper">
|
||||||
|
<div
|
||||||
|
class="btclock-display {theme}"
|
||||||
|
style="border-radius: {displayRadius};
|
||||||
|
border-color: {primaryColor};"
|
||||||
|
>
|
||||||
|
<div class="display-content {verticalDesc ? 'vertical-desc' : ''}">
|
||||||
|
{#if getDisplayMode(i) === 'single'}
|
||||||
|
<div class="single-char">{display}</div>
|
||||||
|
{:else if getDisplayMode(i) === 'medium'}
|
||||||
|
<div class="medium-chars">{display}</div>
|
||||||
|
{:else if getDisplayMode(i) === 'split'}
|
||||||
|
{@const [topText, bottomText] = getSplitParts(display)}
|
||||||
|
<div class="split-text" style="--split-text-padding: {splitTextPadding};">
|
||||||
|
<div class="top-text">{topText}</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="bottom-text">{bottomText}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btclock-label">BTClock</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--font-family: 'Oswald Variable', sans-serif;
|
||||||
|
--primary-color: #0000ff;
|
||||||
|
--frame-color: white;
|
||||||
|
--display-dark-bg: black;
|
||||||
|
--display-light-bg: white;
|
||||||
|
--display-dark-text: white;
|
||||||
|
--display-light-text: black;
|
||||||
|
--label-color: rgba(0, 0, 0, 0.5);
|
||||||
|
--divider-light-color: #333;
|
||||||
|
--divider-dark-color: #fff;
|
||||||
|
|
||||||
|
/* Font weights */
|
||||||
|
--font-weight-regular: 400;
|
||||||
|
--font-weight-medium: 400;
|
||||||
|
--font-weight-semibold: 400;
|
||||||
|
--font-weight-bold: 400;
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
--border-width: 2px;
|
||||||
|
--gap-size: 2px;
|
||||||
|
--screw-size: 6px;
|
||||||
|
--label-font-size: 0.4rem;
|
||||||
|
|
||||||
|
/* Font sizes */
|
||||||
|
--font-size-single: 4cqw;
|
||||||
|
--font-size-medium: 1.5cqw;
|
||||||
|
--font-size-split: 1cqw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screw base style */
|
||||||
|
.screw-top-left,
|
||||||
|
.screw-top-right,
|
||||||
|
.screw-bottom-left,
|
||||||
|
.screw-bottom-right {
|
||||||
|
position: absolute;
|
||||||
|
width: var(--screw-size);
|
||||||
|
height: var(--screw-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 3;
|
||||||
|
/* Background color set via inline style */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display text base style */
|
||||||
|
.single-char,
|
||||||
|
.medium-chars {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: var(--font-weight-medium); /* Set all font weights to medium */
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-char {
|
||||||
|
font-size: var(--font-size-single);
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium-chars {
|
||||||
|
font-size: var(--font-size-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btclock-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--frame-color);
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
border: var(--border-width) solid; /* Color set via inline style */
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
/* Maintain the overall device aspect ratio */
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.vertical-desc .split-text {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-desc .split-text .divider {
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.displays-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--gap-size);
|
||||||
|
width: 100%;
|
||||||
|
height: 70%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btclock-display-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0; /* Prevent flex items from overflowing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.screw-top-left {
|
||||||
|
top: var(--screw-size);
|
||||||
|
left: var(--screw-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screw-top-right {
|
||||||
|
top: var(--screw-size);
|
||||||
|
right: var(--screw-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screw-bottom-left {
|
||||||
|
bottom: var(--screw-size);
|
||||||
|
left: var(--screw-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screw-bottom-right {
|
||||||
|
bottom: var(--screw-size);
|
||||||
|
right: var(--screw-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BTClock label */
|
||||||
|
.btclock-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: var(--label-font-size);
|
||||||
|
color: gold;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'Ubuntu', sans-serif;
|
||||||
|
font-style: italic;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btclock-display {
|
||||||
|
border: 1px solid; /* Color set via inline style */
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
/* Ensure the display maintains its proportions */
|
||||||
|
aspect-ratio: 122 / 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme variants */
|
||||||
|
.btclock-display.dark {
|
||||||
|
background-color: var(--display-dark-bg);
|
||||||
|
color: var(--display-dark-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btclock-display.dark .divider {
|
||||||
|
background-color: var(--divider-dark-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btclock-display.light {
|
||||||
|
background-color: var(--display-light-bg);
|
||||||
|
color: var(--display-light-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btclock-display.light .divider {
|
||||||
|
background-color: var(--divider-light-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-content {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-weight: var(--font-weight-medium); /* Set all font weights to medium */
|
||||||
|
padding: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-text .top-text,
|
||||||
|
.split-text .bottom-text {
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: var(--font-weight-medium); /* Set all font weights to medium */
|
||||||
|
font-size: var(--font-size-split);
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
/* Remove flex: 1 to prevent flexbox from overriding the padding */
|
||||||
|
height: calc(50% - var(--split-text-padding, 2px) - 0.5px);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-text .divider {
|
||||||
|
width: 90%;
|
||||||
|
height: 1px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screw size adjustments based on container width */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.btclock-container {
|
||||||
|
padding: 0.4rem 0.2rem;
|
||||||
|
border-width: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.displays-row {
|
||||||
|
gap: var(--gap-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-content {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.btclock-container {
|
||||||
|
padding: 0.3rem 0.15rem;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.displays-row {
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-content {
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure displays grow properly on larger screens */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.btclock-label {
|
||||||
|
font-size: 1cqw;
|
||||||
|
bottom: 0.35cqh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.displays-row {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btclock-display {
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
16
src/lib/components/form/CurrencyButton.svelte
Normal file
16
src/lib/components/form/CurrencyButton.svelte
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
currency,
|
||||||
|
active = false,
|
||||||
|
onClick,
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn join-item {active ? 'btn-primary' : 'btn-outline'} btn-xs"
|
||||||
|
on:click={onClick}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{currency}
|
||||||
|
</button>
|
26
src/lib/components/form/InputField.svelte
Normal file
26
src/lib/components/form/InputField.svelte
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
value = $bindable(''),
|
||||||
|
label = "",
|
||||||
|
placeholder = "",
|
||||||
|
id = "",
|
||||||
|
type = "text",
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-control w-full">
|
||||||
|
{#if label}
|
||||||
|
<label for={id} class="label">
|
||||||
|
<span class="label-text">{label}</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
{placeholder}
|
||||||
|
{id}
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</div>
|
21
src/lib/components/form/Toggle.svelte
Normal file
21
src/lib/components/form/Toggle.svelte
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
checked = $bindable(false),
|
||||||
|
label = "",
|
||||||
|
id = "",
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label class="flex items-center justify-between gap-2 cursor-pointer">
|
||||||
|
{#if label}
|
||||||
|
<span class="label-text text-xs">{label}</span>
|
||||||
|
{/if}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-primary toggle-xs"
|
||||||
|
{id}
|
||||||
|
bind:checked
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
</label>
|
21
src/lib/components/index.ts
Normal file
21
src/lib/components/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// UI Components
|
||||||
|
export { default as CardContainer } from './ui/CardContainer.svelte';
|
||||||
|
export { default as TabButton } from './ui/TabButton.svelte';
|
||||||
|
export { default as Toast } from './ui/Toast.svelte';
|
||||||
|
export { default as Stat } from './ui/Stat.svelte';
|
||||||
|
export { default as Status } from './ui/Status.svelte';
|
||||||
|
|
||||||
|
// Form Components
|
||||||
|
export { default as InputField } from './form/InputField.svelte';
|
||||||
|
export { default as Toggle } from './form/Toggle.svelte';
|
||||||
|
export { default as CurrencyButton } from './form/CurrencyButton.svelte';
|
||||||
|
|
||||||
|
// Layout Components
|
||||||
|
export { default as Navbar } from './layout/Navbar.svelte';
|
||||||
|
export { default as CollapsibleSection } from './layout/CollapsibleSection.svelte';
|
||||||
|
|
||||||
|
// Section Components
|
||||||
|
export { default as ControlSection } from './sections/ControlSection.svelte';
|
||||||
|
export { default as StatusSection } from './sections/StatusSection.svelte';
|
||||||
|
export { default as SettingsSection } from './sections/SettingsSection.svelte';
|
||||||
|
export { default as SystemSection } from './sections/SystemSection.svelte';
|
17
src/lib/components/layout/CollapsibleSection.svelte
Normal file
17
src/lib/components/layout/CollapsibleSection.svelte
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
open = $bindable(false),
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow bg-base-200 rounded-lg mb-2" {...restProps}>
|
||||||
|
<input type="checkbox" bind:checked={open} />
|
||||||
|
<div class="collapse-title text-lg font-medium">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
87
src/lib/components/layout/Navbar.svelte
Normal file
87
src/lib/components/layout/Navbar.svelte
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
import { setLocale, getLocale } from '$lib/paraglide/runtime';
|
||||||
|
import { locales } from '$lib/paraglide/runtime';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
// Navigation items
|
||||||
|
const navItems = [
|
||||||
|
{ href: '/', label: 'Home' },
|
||||||
|
{ href: '/settings', label: 'Settings' },
|
||||||
|
{ href: '/system', label: 'System' },
|
||||||
|
{ href: '/apidoc', label: 'API' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper function to check if a link is active
|
||||||
|
function isActive(href: string) {
|
||||||
|
return $page.url.pathname === href;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLocaleName = (locale: string) => {
|
||||||
|
return new Intl.DisplayNames([locale], { type: 'language' }).of(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLanguageName = (locale: string) => {
|
||||||
|
return getLocaleName(locale.split('-')[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEmojiFlag = (locale: string) => {
|
||||||
|
const countryCode = locale.split('-')[1];
|
||||||
|
|
||||||
|
if (!countryCode || countryCode === 'US') {
|
||||||
|
return '🇺🇸';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...countryCode.toUpperCase()]
|
||||||
|
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get the current flag
|
||||||
|
const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="navbar bg-base-100 fixed top-0 z-50 shadow-sm w-full" {...restProps}>
|
||||||
|
<div class="navbar-start">
|
||||||
|
<div class="dropdown">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<ul tabindex="-1" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
|
{#each navItems as { href, label }}
|
||||||
|
<li>
|
||||||
|
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a href="/" class="btn btn-ghost text-xl">BTClock</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-center hidden lg:flex">
|
||||||
|
<ul class="menu menu-horizontal px-1">
|
||||||
|
{#each navItems as { href, label }}
|
||||||
|
<li>
|
||||||
|
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-end">
|
||||||
|
<div class="dropdown dropdown-end mr-2">
|
||||||
|
<div tabindex="0" role="button" class="btn btn-ghost">
|
||||||
|
<span class="text-sm">{getCurrentFlag()} {getLanguageName(getLocale())}</span>
|
||||||
|
</div>
|
||||||
|
<ul tabindex="-1" class="mt-3 z-[1] p-2 shadow menu dropdown-content bg-base-100 rounded-box w-auto">
|
||||||
|
{#each locales as locale}
|
||||||
|
<li><button onclick={() => setLocale(locale)} class="flex items-center gap-2 text-nowrap">{getEmojiFlag(locale)} {getLanguageName(locale)}</button></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
123
src/lib/components/sections/ControlSection.svelte
Normal file
123
src/lib/components/sections/ControlSection.svelte
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import { CardContainer, InputField, Toggle } from '$lib/components';
|
||||||
|
import { settings, status } from '$lib/stores';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import {
|
||||||
|
setCustomText,
|
||||||
|
setLEDcolor,
|
||||||
|
turnOffLeds,
|
||||||
|
restartClock,
|
||||||
|
forceFullRefresh,
|
||||||
|
generateRandomColor,
|
||||||
|
flashFrontlight,
|
||||||
|
turnOnFrontlight,
|
||||||
|
turnOffFrontlight
|
||||||
|
} from '$lib/clockControl';
|
||||||
|
import type { LedStatus } from '$lib/types';
|
||||||
|
|
||||||
|
let ledStatus = $state<LedStatus[]>([
|
||||||
|
{hex: '#000000'},
|
||||||
|
{hex: '#000000'},
|
||||||
|
{hex: '#000000'},
|
||||||
|
{hex: '#000000'}
|
||||||
|
]);
|
||||||
|
let customText = $state('');
|
||||||
|
let keepLedsSameColor = $state(false);
|
||||||
|
|
||||||
|
const checkSyncLeds = (e: Event) => {
|
||||||
|
if (keepLedsSameColor && e.target instanceof HTMLInputElement) {
|
||||||
|
const targetValue = e.target.value;
|
||||||
|
|
||||||
|
ledStatus.forEach((element, i) => {
|
||||||
|
if (ledStatus[i].hex != targetValue) {
|
||||||
|
ledStatus[i].hex = targetValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let firstLedDataSubscription = () => {};
|
||||||
|
|
||||||
|
firstLedDataSubscription = status.subscribe(async (val) => {
|
||||||
|
if (val && val.leds) {
|
||||||
|
ledStatus = val.leds.map((obj) => ({ ['hex']: obj['hex'] }));
|
||||||
|
|
||||||
|
for (let led of ledStatus) {
|
||||||
|
if (led['hex'] == '#000000') {
|
||||||
|
led['hex'] = generateRandomColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firstLedDataSubscription();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(firstLedDataSubscription);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CardContainer title={m['section.control.title']()}>
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="customText">
|
||||||
|
<span class="label-text">{m['section.control.text']()}</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<InputField
|
||||||
|
id="customText"
|
||||||
|
maxLength="7"
|
||||||
|
bind:value={customText}
|
||||||
|
placeholder={m['section.control.text']()}
|
||||||
|
style="text-transform: uppercase;"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-primary" onclick={() => setCustomText(customText)}
|
||||||
|
>{m['section.control.showText']()}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<h3 class="mb-2 font-medium">{m['section.control.ledColor']()}</h3>
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<div class="mb-4 flex flex-wrap gap-2">
|
||||||
|
{#if ledStatus.length > 0}
|
||||||
|
{#each ledStatus as led}
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
class="btn btn-square"
|
||||||
|
bind:value={led.hex}
|
||||||
|
onchange={checkSyncLeds}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
<Toggle label={m['sections.control.keepSameColor']()} bind:checked={keepLedsSameColor} />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="btn btn-secondary" onclick={turnOffLeds}>{m['section.control.turnOff']()}</button>
|
||||||
|
<button class="btn btn-primary" onclick={() => setLEDcolor(ledStatus)}
|
||||||
|
>{m['section.control.setColor']()}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $settings.hasFrontlight && !$settings.flDisable}
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3>
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<button class="btn btn-secondary" onclick={() => turnOnFrontlight()}>{m['section.control.turnOn']()}</button>
|
||||||
|
<button class="btn btn-primary" onclick={() => turnOffFrontlight()}>{m['section.control.turnOff']()}</button>
|
||||||
|
<button class="btn btn-accent" onclick={() => flashFrontlight()}>{m['section.control.flashFrontlight']()}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="mb-2 font-medium">{m['section.control.title']()}</h3>
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
|
||||||
|
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContainer>
|
341
src/lib/components/sections/SettingsSection.svelte
Normal file
341
src/lib/components/sections/SettingsSection.svelte
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import { CardContainer, Toggle, CollapsibleSection } from '$lib/components';
|
||||||
|
import { settings } from '$lib/stores';
|
||||||
|
|
||||||
|
let { ...restProps } = $props();
|
||||||
|
|
||||||
|
// Show/hide toggles
|
||||||
|
let showAll = $state(false);
|
||||||
|
let hideAll = $state(false);
|
||||||
|
|
||||||
|
function toggleShowAll() {
|
||||||
|
showAll = true;
|
||||||
|
hideAll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleHideAll() {
|
||||||
|
hideAll = true;
|
||||||
|
showAll = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CardContainer title={m["section.settings.title"]()} {...restProps}>
|
||||||
|
<div class="flex justify-end gap-2 mb-4">
|
||||||
|
<button class="btn btn-sm" onclick={toggleShowAll}>{m["section.settings.showAll"]()}</button>
|
||||||
|
<button class="btn btn-sm" onclick={toggleHideAll}>{m["section.settings.hideAll"]()}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 grid-cols-2">
|
||||||
|
|
||||||
|
<CollapsibleSection title={m["section.settings.section.screenSettings"]()} open={showAll || !hideAll}>
|
||||||
|
<div class="grid gap-4 grid-cols-2">
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.StealFocusOnNewBlock"]()}
|
||||||
|
bind:checked={$settings.stealFocus}
|
||||||
|
/>
|
||||||
|
<p class="text-xs">When a new block is mined, it will switch focus from the current screen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.useBigCharsMcap"]()}
|
||||||
|
bind:checked={$settings.mcapBigChar}
|
||||||
|
/>
|
||||||
|
<p class="text-xs">Use big characters for the market cap screen instead of using a suffix.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.useBlkCountdown"]()}
|
||||||
|
bind:checked={$settings.useBlkCountdown}
|
||||||
|
/>
|
||||||
|
<p class="text-xs">When enabled it count down blocks instead of years/monts/days/hours/minutes.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.useSatsSymbol"]()}
|
||||||
|
bind:checked={$settings.useSatsSymbol}
|
||||||
|
/>
|
||||||
|
<p class="text-xs">Prefix satoshi amounts with the sats symbol.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.suffixPrice"]()}
|
||||||
|
bind:checked={$settings.suffixPrice}
|
||||||
|
/>
|
||||||
|
<p class="text-xs">Always use a suffix for the ticker screen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.verticalDesc"]()}
|
||||||
|
bind:checked={$settings.verticalDesc}
|
||||||
|
/>
|
||||||
|
<p class="text-xs">Rotate the description of the screen 90 degrees.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
<CollapsibleSection title={m["section.settings.screens"]()} open={showAll || !hideAll}>
|
||||||
|
<div class="grid gap-4 grid-cols-2">
|
||||||
|
{#each $settings.screens as screen}
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={screen.name}
|
||||||
|
checked={screen.enabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
<CollapsibleSection title={m["section.settings.currencies"]()} open={showAll || !hideAll}>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||||
|
<span>restart required</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 grid-cols-2">
|
||||||
|
|
||||||
|
{#each $settings.actCurrencies as currency}
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={currency}
|
||||||
|
checked={$settings.actCurrencies.includes(currency)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
<CollapsibleSection title={m["section.settings.section.displaysAndLed"]()} open={showAll || !hideAll}>
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.textColor"]()}</span>
|
||||||
|
</label>
|
||||||
|
<select class="select select-bordered w-full">
|
||||||
|
<option>White on Black</option>
|
||||||
|
<option>Black on White</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Font</span>
|
||||||
|
</label>
|
||||||
|
<select class="select select-bordered w-full">
|
||||||
|
<option>Oswald</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.timePerScreen"]()}</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="number" class="input input-bordered w-20" min="1" max="60" value="1" />
|
||||||
|
<span>{m["time.minutes"]()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control flex justify-between">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.fullRefreshEvery"]()}</span>
|
||||||
|
</label>
|
||||||
|
<div class="w-auto input">
|
||||||
|
<input type="number" class="" min="1" max="60" value="60" />
|
||||||
|
<span class="label">{m["time.minutes"]()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control flex justify-between">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.timeBetweenPriceUpdates"]()}</span>
|
||||||
|
</label>
|
||||||
|
<div class="w-auto input">
|
||||||
|
<input type="number" class="" min="1" max="60" value="30" />
|
||||||
|
<span class="label">{m["time.seconds"]()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.ledBrightness"]()}</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" min="0" max="100" class="range" value="50" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.ledPowerOnTest"]()}
|
||||||
|
checked={$settings.ledTestOnPower}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.ledFlashOnBlock"]()}
|
||||||
|
checked={$settings.ledFlashOnUpd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.disableLeds"]()}
|
||||||
|
checked={$settings.disableLeds}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
{#if $settings.hasFrontlight}
|
||||||
|
<CollapsibleSection title="Frontlight Settings" open={showAll || !hideAll}>
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label="Disable Frontlight"
|
||||||
|
checked={$settings.flDisable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label="Always On"
|
||||||
|
checked={$settings.flAlwaysOn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label="Flash on Updates"
|
||||||
|
checked={$settings.flFlashOnUpd}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label="Flash on Zaps"
|
||||||
|
checked={$settings.flFlashOnZap}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $settings.hasLightLevel}
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label="Turn Off in Dark"
|
||||||
|
checked={$settings.flOffWhenDark}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Light Level Threshold</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" min="0" max="255" class="range" value={$settings.luxLightToggle} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Maximum Brightness</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" min="0" max="4095" class="range" value={$settings.flMaxBrightness} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">Effect Delay (ms)</span>
|
||||||
|
</label>
|
||||||
|
<input type="number" class="input input-bordered w-20" min="10" max="1000" value={$settings.flEffectDelay} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<CollapsibleSection title={m["section.settings.section.dataSource"]()} open={showAll || !hideAll}>
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.dataSource.label"]()}</span>
|
||||||
|
</label>
|
||||||
|
<select class="select select-bordered w-full">
|
||||||
|
<option value="btclock">{m["section.settings.dataSource.btclock"]()}</option>
|
||||||
|
<option value="thirdparty">{m["section.settings.dataSource.thirdParty"]()}</option>
|
||||||
|
<option value="nostr">{m["section.settings.dataSource.nostr"]()}</option>
|
||||||
|
<option value="custom">{m["section.settings.dataSource.custom"]()}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.mempoolnstance"]()}</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="input input-bordered w-full" value="mempool.space/coinlcp.io" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.ceEndpoint"]()}</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="input input-bordered w-full" placeholder="Custom Endpoint URL" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
<CollapsibleSection title={m["section.settings.section.extraFeatures"]()} open={showAll || !hideAll}>
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<Toggle
|
||||||
|
label={m["section.settings.timeBasedDnd"]()}
|
||||||
|
checked={$settings.dnd.enabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
<CollapsibleSection title={m["section.settings.section.system"]()} open={showAll || !hideAll}>
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.timezoneOffset"]()}</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<select class="select select-bordered w-full">
|
||||||
|
<option>Europe/Amsterdam</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn">{m["auto-detect"]()}</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm mt-1">{m["section.settings.tzOffsetHelpText"]()}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.hostnamePrefix"]()}</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="input input-bordered w-full" value="btclock" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{m["section.settings.wpTimeout"]()}</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="number" class="input input-bordered w-20" min="1" max="900" value="600" />
|
||||||
|
<span>{m["time.seconds"]()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mt-6">
|
||||||
|
<button class="btn btn-error">{m["button.reset"]()}</button>
|
||||||
|
<button class="btn btn-primary">{m["button.save"]()}</button>
|
||||||
|
</div>
|
||||||
|
</CardContainer>
|
75
src/lib/components/sections/StatusSection.svelte
Normal file
75
src/lib/components/sections/StatusSection.svelte
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import { CardContainer, TabButton, CurrencyButton, Stat, Status } from '$lib/components';
|
||||||
|
import { status, settings } from '$lib/stores';
|
||||||
|
import { setActiveScreen, setActiveCurrency } from '$lib/clockControl';
|
||||||
|
import BTClock from '../BTClock.svelte';
|
||||||
|
import { DataSourceType } from '$lib/types';
|
||||||
|
import { toUptimestring } from '$lib/utils';
|
||||||
|
|
||||||
|
const screens = $settings.screens.map(screen => ({
|
||||||
|
id: screen.id,
|
||||||
|
label: screen.name
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CardContainer title={m['section.status.title']()}>
|
||||||
|
<div class="space-y-4 mx-auto">
|
||||||
|
<div class="join">
|
||||||
|
{#each screens as screen}
|
||||||
|
<TabButton active={$status.currentScreen === screen.id} onClick={() => setActiveScreen(screen.id)}>
|
||||||
|
{screen.label}
|
||||||
|
</TabButton>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="join flex justify-center">
|
||||||
|
{#each $settings.actCurrencies as currency}
|
||||||
|
<CurrencyButton
|
||||||
|
currency={currency}
|
||||||
|
active={$status.currency === currency}
|
||||||
|
onClick={() => setActiveCurrency(currency)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 flex justify-center">
|
||||||
|
<div class="w-3/4">
|
||||||
|
<!-- Bitcoin value display showing blocks/price -->
|
||||||
|
<BTClock displays={$status.data} verticalDesc={$settings.verticalDesc} />
|
||||||
|
{$settings.verticalDesc}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center text-sm text-gray-500">
|
||||||
|
{m['section.status.screenCycle']()}: is {$status.timerRunning ? 'running' : 'stopped'}<br />
|
||||||
|
{m['section.status.doNotDisturb']()}: {$status.dnd.enabled ? m['on']() : m['off']()}
|
||||||
|
<small>
|
||||||
|
{#if $status.dnd?.timeBasedEnabled}
|
||||||
|
{m['section.status.timeBasedDnd']()} ( {$settings.dnd
|
||||||
|
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings
|
||||||
|
.dnd.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
|
||||||
|
{/if}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
{#if $settings.dataSource === DataSourceType.NOSTR_SOURCE || $settings.nostrZapNotify}
|
||||||
|
<Status text="Nostr Relay connection" status={$status.nostr ? 'online' : 'offline'} />
|
||||||
|
{/if}
|
||||||
|
{#if $settings.dataSource === DataSourceType.THIRD_PARTY_SOURCE}
|
||||||
|
<Status text={m['section.status.wsPriceConnection']()} status={$status.connectionStatus.price ? 'online' : 'offline'} />
|
||||||
|
<Status text={m['section.status.wsMempoolConnection']({ instance: $settings.mempoolInstance })} status={$status.connectionStatus.blocks ? 'online' : 'offline'} />
|
||||||
|
{:else}
|
||||||
|
<Status text={m['section.status.wsDataConnection']()} status={$status.connectionStatus.V2 ? 'online' : 'offline'} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center stats shadow mt-4">
|
||||||
|
<Stat title={m['section.status.memoryFree']()} value={`${Math.round($status.espFreeHeap / 1024)} / ${Math.round($status.espHeapSize / 1024)} KiB`} />
|
||||||
|
<Stat title={m['section.status.wifiSignalStrength']()} value={`${$status.rssi} dBm`} />
|
||||||
|
<Stat title={m['section.status.uptime']()} value={`${toUptimestring($status.espUptime)}`} />
|
||||||
|
</div>
|
||||||
|
</CardContainer>
|
102
src/lib/components/sections/SystemSection.svelte
Normal file
102
src/lib/components/sections/SystemSection.svelte
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import { CardContainer } from '$lib/components';
|
||||||
|
import { settings } from '$lib/stores';
|
||||||
|
import {
|
||||||
|
restartClock,
|
||||||
|
forceFullRefresh
|
||||||
|
} from '$lib/clockControl';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CardContainer title="System Information">
|
||||||
|
<div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table-sm table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">System info</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{m['section.control.version']()}</td>
|
||||||
|
<td>{$settings.gitTag}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{m['section.control.buildTime']()}</td>
|
||||||
|
<td>{$settings.lastBuildTime}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>IP</td>
|
||||||
|
<td>{$settings.ip}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>HW revision</td>
|
||||||
|
<td>{$settings.hwRev}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{m['section.control.fwCommit']()}</td>
|
||||||
|
<td class="text-xs">{$settings.gitRev}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{m['section.control.hostname']()}</td>
|
||||||
|
<td>{$settings.hostname}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex gap-2">
|
||||||
|
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
|
||||||
|
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContainer>
|
||||||
|
|
||||||
|
<CardContainer title={m['section.control.firmwareUpdate']()} className="mt-4">
|
||||||
|
<div>
|
||||||
|
<p class="mb-2 text-sm">
|
||||||
|
Latest Version: 3.3.5 - Release Date: 5/2/2025, 12:37:14 AM - <a
|
||||||
|
href="#"
|
||||||
|
class="link link-primary">{m['section.firmwareUpdater.viewRelease']()}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p class="text-success mb-4 text-sm">{m['section.firmwareUpdater.swUpToDate']()}</p>
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label class="label" for="firmwareFile">
|
||||||
|
<span class="label-text">Firmware File (blib_s3_mini_213epd_firmware.bin)</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="file" class="file-input file-input-bordered w-full" id="firmwareFile" />
|
||||||
|
<button class="btn btn-primary">{m['section.control.firmwareUpdate']()}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="webuiFile">
|
||||||
|
<span class="label-text">WebUI File (littlefs_4MB.bin)</span>
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="file" class="file-input file-input-bordered w-full" id="webuiFile" />
|
||||||
|
<button class="btn btn-primary">Update WebUI</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning mt-4">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
<span>{m['section.firmwareUpdater.firmwareUpdateText']()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContainer>
|
16
src/lib/components/ui/CardContainer.svelte
Normal file
16
src/lib/components/ui/CardContainer.svelte
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
className = "",
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-xl w-full {className}" {...restProps}>
|
||||||
|
<div class="card-body">
|
||||||
|
{#if title}
|
||||||
|
<h2 class="card-title">{title}</h2>
|
||||||
|
{/if}
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
23
src/lib/components/ui/Stat.svelte
Normal file
23
src/lib/components/ui/Stat.svelte
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
desc = "",
|
||||||
|
icon = "",
|
||||||
|
className = "",
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="stat {className}" {...restProps}>
|
||||||
|
{#if icon}
|
||||||
|
<div class="stat-figure text-primary">
|
||||||
|
{@html icon}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="stat-title">{title}</div>
|
||||||
|
<div class="stat-value">{value}</div>
|
||||||
|
{#if desc}
|
||||||
|
<div class="stat-desc">{desc}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
33
src/lib/components/ui/Status.svelte
Normal file
33
src/lib/components/ui/Status.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
status = "offline" as "online" | "offline" | "error" | "warning",
|
||||||
|
text,
|
||||||
|
className = "",
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const getStatusColor = () => {
|
||||||
|
switch (status) {
|
||||||
|
case "online":
|
||||||
|
return "bg-success";
|
||||||
|
case "offline":
|
||||||
|
return "bg-base-300";
|
||||||
|
case "error":
|
||||||
|
return "bg-error";
|
||||||
|
case "warning":
|
||||||
|
return "bg-warning";
|
||||||
|
default:
|
||||||
|
return "bg-base-300";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 {className}" {...restProps}>
|
||||||
|
<div class="relative flex">
|
||||||
|
<div class="{getStatusColor()} h-3 w-3 rounded-full"></div>
|
||||||
|
{#if status === "online"}
|
||||||
|
<div class="{getStatusColor()} animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm">{text}</span>
|
||||||
|
</div>
|
15
src/lib/components/ui/TabButton.svelte
Normal file
15
src/lib/components/ui/TabButton.svelte
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
active = false,
|
||||||
|
onClick,
|
||||||
|
...restProps
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-sm join-item {active ? 'btn-primary' : 'btn-outline'}"
|
||||||
|
on:click={onClick}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
71
src/lib/components/ui/Toast.svelte
Normal file
71
src/lib/components/ui/Toast.svelte
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts">
|
||||||
|
type Position =
|
||||||
|
| 'top-start' | 'top-center' | 'top-end'
|
||||||
|
| 'middle-start' | 'middle-center' | 'middle-end'
|
||||||
|
| 'bottom-start' | 'bottom-center' | 'bottom-end';
|
||||||
|
|
||||||
|
type AlertType = 'info' | 'success' | 'warning' | 'error';
|
||||||
|
|
||||||
|
let props = $props();
|
||||||
|
|
||||||
|
let message = props.message || '';
|
||||||
|
let type = (props.type as AlertType) || 'info';
|
||||||
|
let position = (props.position as Position) || 'bottom-end';
|
||||||
|
let duration = props.duration || 3000;
|
||||||
|
let showClose = props.showClose || false;
|
||||||
|
|
||||||
|
// Create a new object without the known props
|
||||||
|
let restProps = { ...props };
|
||||||
|
delete restProps.message;
|
||||||
|
delete restProps.type;
|
||||||
|
delete restProps.position;
|
||||||
|
delete restProps.duration;
|
||||||
|
delete restProps.showClose;
|
||||||
|
|
||||||
|
let visible = $state(true);
|
||||||
|
|
||||||
|
// Map position prop to DaisyUI classes
|
||||||
|
const getPositionClasses = (pos: Position): string => {
|
||||||
|
const [vertical, horizontal] = pos.split('-');
|
||||||
|
|
||||||
|
let classes = 'toast';
|
||||||
|
|
||||||
|
// Vertical position
|
||||||
|
if (vertical === 'top') classes += ' toast-top';
|
||||||
|
if (vertical === 'middle') classes += ' toast-middle';
|
||||||
|
// bottom is default, no class needed
|
||||||
|
|
||||||
|
// Horizontal position
|
||||||
|
if (horizontal === 'start') classes += ' toast-start';
|
||||||
|
if (horizontal === 'center') classes += ' toast-center';
|
||||||
|
if (horizontal === 'end') classes += ' toast-end';
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-hide the toast after duration
|
||||||
|
if (duration > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
visible = false;
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeToast = () => {
|
||||||
|
visible = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div class={getPositionClasses(position)} {...restProps}>
|
||||||
|
<div class="alert alert-{type}">
|
||||||
|
<span>{message}</span>
|
||||||
|
{#if showClose}
|
||||||
|
<button class="btn btn-circle btn-xs" onclick={closeToast}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
3
src/lib/env.ts
Normal file
3
src/lib/env.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { PUBLIC_BASE_URL } from '$env/static/public';
|
||||||
|
|
||||||
|
export const baseUrl = PUBLIC_BASE_URL;
|
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
2
src/lib/stores/index.ts
Normal file
2
src/lib/stores/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { settings, type Settings } from './settings';
|
||||||
|
export { status, type Status } from './status';
|
161
src/lib/stores/settings.ts
Normal file
161
src/lib/stores/settings.ts
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { baseUrl } from '$lib/env';
|
||||||
|
import type { Settings } from '$lib/types';
|
||||||
|
|
||||||
|
// Create a default settings object
|
||||||
|
const defaultSettings: Settings = {
|
||||||
|
numScreens: 7,
|
||||||
|
invertedColor: false,
|
||||||
|
timerSeconds: 60,
|
||||||
|
timerRunning: false,
|
||||||
|
minSecPriceUpd: 30,
|
||||||
|
fullRefreshMin: 60,
|
||||||
|
wpTimeout: 600,
|
||||||
|
tzString: "UTC",
|
||||||
|
dataSource: 0,
|
||||||
|
mempoolInstance: "mempool.space",
|
||||||
|
mempoolSecure: true,
|
||||||
|
localPoolEndpoint: "localhost:2019",
|
||||||
|
nostrPubKey: "",
|
||||||
|
nostrRelay: "wss://relay.damus.io",
|
||||||
|
nostrZapNotify: false,
|
||||||
|
nostrZapPubkey: "",
|
||||||
|
ledFlashOnZap: true,
|
||||||
|
fontName: "oswald",
|
||||||
|
availableFonts: ["antonio", "oswald"],
|
||||||
|
customEndpoint: "ws-staging.btclock.dev",
|
||||||
|
customEndpointDisableSSL: false,
|
||||||
|
ledTestOnPower: true,
|
||||||
|
ledFlashOnUpd: true,
|
||||||
|
ledBrightness: 255,
|
||||||
|
stealFocus: false,
|
||||||
|
mcapBigChar: true,
|
||||||
|
mdnsEnabled: true,
|
||||||
|
otaEnabled: true,
|
||||||
|
useSatsSymbol: true,
|
||||||
|
useBlkCountdown: true,
|
||||||
|
suffixPrice: false,
|
||||||
|
disableLeds: false,
|
||||||
|
mowMode: false,
|
||||||
|
verticalDesc: true,
|
||||||
|
suffixShareDot: false,
|
||||||
|
enableDebugLog: false,
|
||||||
|
hostnamePrefix: "btclock",
|
||||||
|
hostname: "btclock",
|
||||||
|
ip: "",
|
||||||
|
txPower: 80,
|
||||||
|
gitReleaseUrl: "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest",
|
||||||
|
bitaxeEnabled: false,
|
||||||
|
bitaxeHostname: "bitaxe1",
|
||||||
|
miningPoolStats: false,
|
||||||
|
miningPoolName: "noderunners",
|
||||||
|
miningPoolUser: "",
|
||||||
|
availablePools: [
|
||||||
|
"ocean",
|
||||||
|
"noderunners",
|
||||||
|
"satoshi_radio",
|
||||||
|
"braiins",
|
||||||
|
"public_pool",
|
||||||
|
"local_public_pool",
|
||||||
|
"gobrrr_pool",
|
||||||
|
"ckpool",
|
||||||
|
"eu_ckpool"
|
||||||
|
],
|
||||||
|
httpAuthEnabled: false,
|
||||||
|
httpAuthUser: "btclock",
|
||||||
|
httpAuthPass: "satoshi",
|
||||||
|
hasFrontlight: false,
|
||||||
|
// Default frontlight settings
|
||||||
|
flDisable: false,
|
||||||
|
flMaxBrightness: 2684,
|
||||||
|
flAlwaysOn: false,
|
||||||
|
flEffectDelay: 50,
|
||||||
|
flFlashOnUpd: true,
|
||||||
|
flFlashOnZap: true,
|
||||||
|
// Default light sensor settings
|
||||||
|
hasLightLevel: false,
|
||||||
|
luxLightToggle: 128,
|
||||||
|
flOffWhenDark: false,
|
||||||
|
hwRev: "",
|
||||||
|
fsRev: "",
|
||||||
|
gitRev: "",
|
||||||
|
gitTag: "",
|
||||||
|
lastBuildTime: "",
|
||||||
|
screens: [
|
||||||
|
{id: 0, name: "Block Height", enabled: true},
|
||||||
|
{id: 3, name: "Time", enabled: false},
|
||||||
|
{id: 4, name: "Halving countdown", enabled: false},
|
||||||
|
{id: 6, name: "Block Fee Rate", enabled: false},
|
||||||
|
{id: 10, name: "Sats per dollar", enabled: true},
|
||||||
|
{id: 20, name: "Ticker", enabled: true},
|
||||||
|
{id: 30, name: "Market Cap", enabled: false}
|
||||||
|
],
|
||||||
|
actCurrencies: ["USD"],
|
||||||
|
availableCurrencies: ["USD", "EUR", "GBP", "JPY", "AUD", "CAD"],
|
||||||
|
poolLogosUrl: "https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main",
|
||||||
|
ceEndpoint: "ws-staging.btclock.dev",
|
||||||
|
ceDisableSSL: false,
|
||||||
|
dnd: {
|
||||||
|
enabled: false,
|
||||||
|
timeBasedEnabled: false,
|
||||||
|
startHour: 23,
|
||||||
|
startMinute: 0,
|
||||||
|
endHour: 7,
|
||||||
|
endMinute: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the Svelte store
|
||||||
|
function createSettingsStore() {
|
||||||
|
const { subscribe, set, update } = writable<Settings>(defaultSettings);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
fetch: async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/api/settings`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error fetching settings: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
set(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch settings:', error);
|
||||||
|
return defaultSettings;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: async (newSettings: Partial<Settings>) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/api/settings`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newSettings),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error updating settings: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the local store with the new settings
|
||||||
|
update(currentSettings => ({ ...currentSettings, ...newSettings }));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update settings:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: (newSettings: Settings) => set(newSettings),
|
||||||
|
reset: () => set(defaultSettings)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settings = createSettingsStore();
|
||||||
|
|
||||||
|
// Initialize the store by fetching settings when this module is first imported
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
settings.fetch();
|
||||||
|
}
|
180
src/lib/stores/status.ts
Normal file
180
src/lib/stores/status.ts
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { baseUrl } from '$lib/env';
|
||||||
|
import type { Status } from '$lib/types';
|
||||||
|
// Create a default status object
|
||||||
|
const defaultStatus: Status = {
|
||||||
|
currentScreen: 0,
|
||||||
|
numScreens: 0,
|
||||||
|
timerRunning: false,
|
||||||
|
isOTAUpdating: false,
|
||||||
|
espUptime: 0,
|
||||||
|
espFreeHeap: 0,
|
||||||
|
espHeapSize: 0,
|
||||||
|
connectionStatus: {
|
||||||
|
price: false,
|
||||||
|
blocks: false,
|
||||||
|
V2: false,
|
||||||
|
nostr: false
|
||||||
|
},
|
||||||
|
rssi: 0,
|
||||||
|
currency: "USD",
|
||||||
|
dnd: {
|
||||||
|
enabled: false,
|
||||||
|
timeBasedEnabled: false,
|
||||||
|
startTime: "00:00",
|
||||||
|
endTime: "00:00",
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
data: [],
|
||||||
|
leds: [
|
||||||
|
{red: 0, green: 0, blue: 0, hex: "#000000"},
|
||||||
|
{red: 0, green: 0, blue: 0, hex: "#000000"},
|
||||||
|
{red: 0, green: 0, blue: 0, hex: "#000000"},
|
||||||
|
{red: 0, green: 0, blue: 0, hex: "#000000"}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the Svelte store
|
||||||
|
function createStatusStore() {
|
||||||
|
const { subscribe, set, update } = writable<Status>(defaultStatus);
|
||||||
|
let eventSource: EventSource | null = null;
|
||||||
|
|
||||||
|
// Clean up function to close SSE connection
|
||||||
|
const cleanup = () => {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
eventSource = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the store object with methods
|
||||||
|
const store = {
|
||||||
|
subscribe,
|
||||||
|
fetch: async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/api/status`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error fetching status: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
set(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch status:', error);
|
||||||
|
return defaultStatus;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startListening: () => {
|
||||||
|
// Clean up any existing connections first
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
// Only run in the browser, not during SSR
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a new EventSource connection
|
||||||
|
eventSource = new EventSource(`${baseUrl}/events`);
|
||||||
|
|
||||||
|
// Handle status updates
|
||||||
|
eventSource.addEventListener('status', (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
update(currentStatus => ({ ...currentStatus, ...data }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing status event:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle connection status updates
|
||||||
|
eventSource.addEventListener('connection', (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
update(currentStatus => ({
|
||||||
|
...currentStatus,
|
||||||
|
connectionStatus: {
|
||||||
|
...currentStatus.connectionStatus,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing connection event:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle screen updates
|
||||||
|
eventSource.addEventListener('screen', (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
update(currentStatus => ({
|
||||||
|
...currentStatus,
|
||||||
|
currentScreen: data.screen || currentStatus.currentScreen
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing screen event:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle generic messages
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
if (event.type === 'message') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
update(currentStatus => ({ ...currentStatus, ...data }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing message event:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error('EventSource failed:', error);
|
||||||
|
cleanup();
|
||||||
|
// Attempt to reconnect after a delay
|
||||||
|
setTimeout(() => store.startListening(), 5000);
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to setup event source:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopListening: cleanup,
|
||||||
|
|
||||||
|
// Function to set the current screen
|
||||||
|
setScreen: async (id: number): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
// Make the GET request to change the screen
|
||||||
|
const response = await fetch(`${baseUrl}/api/show/screen/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error setting screen: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the store with the new screen ID
|
||||||
|
update(currentStatus => ({
|
||||||
|
...currentStatus,
|
||||||
|
currentScreen: id
|
||||||
|
}));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set screen:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const status = createStatusStore();
|
||||||
|
|
||||||
|
// Initialize the store by fetching initial status and starting to listen for updates
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
status.fetch().then(() => status.startListening());
|
||||||
|
|
||||||
|
// Clean up the EventSource when the window is unloaded
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
status.stopListening();
|
||||||
|
});
|
||||||
|
}
|
142
src/lib/types.ts
Normal file
142
src/lib/types.ts
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
/**
|
||||||
|
* Types for the BTClock application
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LedStatus {
|
||||||
|
hex: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data source types
|
||||||
|
*/
|
||||||
|
export enum DataSourceType {
|
||||||
|
BTCLOCK_SOURCE = 0,
|
||||||
|
THIRD_PARTY_SOURCE = 1,
|
||||||
|
NOSTR_SOURCE = 2,
|
||||||
|
CUSTOM_SOURCE = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the Status interface based on the API response structure
|
||||||
|
export interface Status {
|
||||||
|
currentScreen: number;
|
||||||
|
numScreens: number;
|
||||||
|
timerRunning: boolean;
|
||||||
|
isOTAUpdating: boolean;
|
||||||
|
espUptime: number;
|
||||||
|
espFreeHeap: number;
|
||||||
|
espHeapSize: number;
|
||||||
|
connectionStatus: {
|
||||||
|
price: boolean;
|
||||||
|
blocks: boolean;
|
||||||
|
V2: boolean;
|
||||||
|
nostr: boolean;
|
||||||
|
};
|
||||||
|
rssi: number;
|
||||||
|
currency: string;
|
||||||
|
dnd: {
|
||||||
|
enabled: boolean;
|
||||||
|
timeBasedEnabled: boolean;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
data: string[];
|
||||||
|
leds: Array<{
|
||||||
|
red: number;
|
||||||
|
green: number;
|
||||||
|
blue: number;
|
||||||
|
hex: string;
|
||||||
|
}>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the Settings interface based on the API response structure
|
||||||
|
export interface Settings {
|
||||||
|
numScreens: number;
|
||||||
|
invertedColor: boolean;
|
||||||
|
timerSeconds: number;
|
||||||
|
timerRunning: boolean;
|
||||||
|
minSecPriceUpd: number;
|
||||||
|
fullRefreshMin: number;
|
||||||
|
wpTimeout: number;
|
||||||
|
tzString: string;
|
||||||
|
dataSource: number;
|
||||||
|
mempoolInstance: string;
|
||||||
|
mempoolSecure: boolean;
|
||||||
|
localPoolEndpoint: string;
|
||||||
|
nostrPubKey: string;
|
||||||
|
nostrRelay: string;
|
||||||
|
nostrZapNotify: boolean;
|
||||||
|
nostrZapPubkey: string;
|
||||||
|
ledFlashOnZap: boolean;
|
||||||
|
fontName: string;
|
||||||
|
availableFonts: string[];
|
||||||
|
customEndpoint: string;
|
||||||
|
customEndpointDisableSSL: boolean;
|
||||||
|
ledTestOnPower: boolean;
|
||||||
|
ledFlashOnUpd: boolean;
|
||||||
|
ledBrightness: number;
|
||||||
|
stealFocus: boolean;
|
||||||
|
mcapBigChar: boolean;
|
||||||
|
mdnsEnabled: boolean;
|
||||||
|
otaEnabled: boolean;
|
||||||
|
useSatsSymbol: boolean;
|
||||||
|
useBlkCountdown: boolean;
|
||||||
|
suffixPrice: boolean;
|
||||||
|
disableLeds: boolean;
|
||||||
|
mowMode: boolean;
|
||||||
|
verticalDesc: boolean;
|
||||||
|
suffixShareDot: boolean;
|
||||||
|
enableDebugLog: boolean;
|
||||||
|
hostnamePrefix: string;
|
||||||
|
hostname: string;
|
||||||
|
ip: string;
|
||||||
|
txPower: number;
|
||||||
|
gitReleaseUrl: string;
|
||||||
|
bitaxeEnabled: boolean;
|
||||||
|
bitaxeHostname: string;
|
||||||
|
miningPoolStats: boolean;
|
||||||
|
miningPoolName: string;
|
||||||
|
miningPoolUser: string;
|
||||||
|
availablePools: string[];
|
||||||
|
httpAuthEnabled: boolean;
|
||||||
|
httpAuthUser: string;
|
||||||
|
httpAuthPass: string;
|
||||||
|
hasFrontlight?: boolean;
|
||||||
|
// Frontlight settings
|
||||||
|
flDisable?: boolean;
|
||||||
|
flMaxBrightness?: number;
|
||||||
|
flAlwaysOn?: boolean;
|
||||||
|
flEffectDelay?: number;
|
||||||
|
flFlashOnUpd?: boolean;
|
||||||
|
flFlashOnZap?: boolean;
|
||||||
|
// Light sensor settings
|
||||||
|
hasLightLevel?: boolean;
|
||||||
|
luxLightToggle?: number;
|
||||||
|
flOffWhenDark?: boolean;
|
||||||
|
hwRev: string;
|
||||||
|
fsRev: string;
|
||||||
|
gitRev: string;
|
||||||
|
gitTag: string;
|
||||||
|
lastBuildTime: string;
|
||||||
|
screens: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}>;
|
||||||
|
actCurrencies: string[];
|
||||||
|
availableCurrencies: string[];
|
||||||
|
poolLogosUrl: string;
|
||||||
|
ceEndpoint: string;
|
||||||
|
ceDisableSSL: boolean;
|
||||||
|
dnd: {
|
||||||
|
enabled: boolean;
|
||||||
|
timeBasedEnabled: boolean;
|
||||||
|
startHour: number;
|
||||||
|
startMinute: number;
|
||||||
|
endHour: number;
|
||||||
|
endMinute: number;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
22
src/lib/utils.ts
Normal file
22
src/lib/utils.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export const toTime = (secs: number) => {
|
||||||
|
const hours = Math.floor(secs / (60 * 60));
|
||||||
|
|
||||||
|
const divisor_for_minutes = secs % (60 * 60);
|
||||||
|
const minutes = Math.floor(divisor_for_minutes / 60);
|
||||||
|
|
||||||
|
const divisor_for_seconds = divisor_for_minutes % 60;
|
||||||
|
const seconds = Math.ceil(divisor_for_seconds);
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
h: hours,
|
||||||
|
m: minutes,
|
||||||
|
s: seconds
|
||||||
|
};
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toUptimestring = (secs: number): string => {
|
||||||
|
const time = toTime(secs);
|
||||||
|
|
||||||
|
return `${time.h}h ${time.m}m ${time.s}s`;
|
||||||
|
};
|
35
src/routes/+layout.svelte
Normal file
35
src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import '../app.css';
|
||||||
|
import { Navbar } from '$lib/components';
|
||||||
|
import { settings, status } from '$lib/stores';
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
let unsubscribeSettings: () => void;
|
||||||
|
let unsubscribeStatus: () => void;
|
||||||
|
|
||||||
|
// Initialize stores when page loads
|
||||||
|
onMount(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('Initializing stores');
|
||||||
|
unsubscribeSettings = settings.subscribe(() => {});
|
||||||
|
unsubscribeStatus = status.subscribe(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up on component destroy
|
||||||
|
onDestroy(() => {
|
||||||
|
if (unsubscribeSettings) unsubscribeSettings();
|
||||||
|
if (unsubscribeStatus) unsubscribeStatus();
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
status.stopListening();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export const prerender = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main class="bg-base-200">
|
||||||
|
{@render children()}
|
||||||
|
</main>
|
21
src/routes/+page.svelte
Normal file
21
src/routes/+page.svelte
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
|
import { settings, status } from '$lib/stores';
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { ControlSection, StatusSection, SettingsSection } from '$lib/components';
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class=" mx-auto px-2 py-4">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<ControlSection />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2">
|
||||||
|
<StatusSection />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
68
src/routes/apidoc/+page.svelte
Normal file
68
src/routes/apidoc/+page.svelte
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { baseUrl } from '$lib/env';
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
let isLoaded = $state(false);
|
||||||
|
let scalarApiReference;
|
||||||
|
|
||||||
|
function initializeScalar() {
|
||||||
|
// @ts-ignore - Scalar is loaded dynamically
|
||||||
|
if (window.Scalar) {
|
||||||
|
// @ts-ignore - Scalar is loaded dynamically
|
||||||
|
scalarApiReference = window.Scalar.createApiReference('#app', {
|
||||||
|
url: '/swagger.json',
|
||||||
|
hideDarkModeToggle: true,
|
||||||
|
hideClientButton: true,
|
||||||
|
baseServerURL: baseUrl
|
||||||
|
});
|
||||||
|
isLoaded = true;
|
||||||
|
} else {
|
||||||
|
setTimeout(initializeScalar, 100); // Check again in 100ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadScalarScript() {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/@scalar/api-reference';
|
||||||
|
script.onload = () => {
|
||||||
|
initializeScalar();
|
||||||
|
};
|
||||||
|
script.onerror = (error) => {
|
||||||
|
console.error('Failed to load Scalar API Reference:', error);
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
let darkMode = $state(false);
|
||||||
|
let handler: (e: MediaQueryListEvent) => void;
|
||||||
|
let mediaQuery: MediaQueryList;
|
||||||
|
onMount(() => {
|
||||||
|
loadScalarScript();
|
||||||
|
darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
handler = (e: MediaQueryListEvent) => {
|
||||||
|
darkMode = e.matches;
|
||||||
|
};
|
||||||
|
mediaQuery.addEventListener('change', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isLoaded = false;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (mediaQuery) {
|
||||||
|
mediaQuery.removeEventListener('change', handler);
|
||||||
|
}
|
||||||
|
if (isLoaded) {
|
||||||
|
document.querySelectorAll('style[data-scalar]').forEach(el => el.remove());
|
||||||
|
|
||||||
|
scalarApiReference.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<div id="app" class="w-full" class:dark-mode={darkMode}></div>
|
||||||
|
</div>
|
0
src/routes/convert/+page.svelte
Normal file
0
src/routes/convert/+page.svelte
Normal file
11
src/routes/page.svelte.test.ts
Normal file
11
src/routes/page.svelte.test.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { describe, test, expect } from 'vitest';
|
||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
|
import { render, screen } from '@testing-library/svelte';
|
||||||
|
import Page from './+page.svelte';
|
||||||
|
|
||||||
|
describe('/+page.svelte', () => {
|
||||||
|
test('should render h1', () => {
|
||||||
|
render(Page);
|
||||||
|
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
8
src/routes/settings/+page.svelte
Normal file
8
src/routes/settings/+page.svelte
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { SettingsSection } from '$lib/components';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-4">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Settings</h1>
|
||||||
|
<SettingsSection />
|
||||||
|
</div>
|
8
src/routes/system/+page.svelte
Normal file
8
src/routes/system/+page.svelte
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { SystemSection } from '$lib/components';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-4">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">System Management</h1>
|
||||||
|
<SystemSection />
|
||||||
|
</div>
|
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
457
static/swagger.json
Normal file
457
static/swagger.json
Normal file
|
@ -0,0 +1,457 @@
|
||||||
|
{
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"title": "BTClock API",
|
||||||
|
"version": "3.0",
|
||||||
|
"description": "BTClock V3 API"
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "/api/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/status": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["system"],
|
||||||
|
"summary": "Get current status",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/system_status": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["system"],
|
||||||
|
"summary": "Get system status",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/settings": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["system"],
|
||||||
|
"summary": "Get current settings",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ArrayOfLeds"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": ["system"],
|
||||||
|
"summary": "Save current settings",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"tags": ["system"],
|
||||||
|
"summary": "Save current settings",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/action/pause": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["timer"],
|
||||||
|
"summary": "Pause screen rotation",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/action/timer_restart": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["timer"],
|
||||||
|
"summary": "Restart screen rotation",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/show/screen/{id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["screens"],
|
||||||
|
"summary": "Set screen to show",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"description": "ID of screen to show"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/show/text/{text}": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["screens"],
|
||||||
|
"summary": "Set text to show",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "text",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "text"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"description": "Text to show"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/show/custom": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["screens"],
|
||||||
|
"summary": "Set text to show (advanced)",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CustomText"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/full_refresh": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["system"],
|
||||||
|
"summary": "Force full refresh of all displays",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/lights": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["lights"],
|
||||||
|
"summary": "Get LEDs status",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ArrayOfLeds"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/lights/set": {
|
||||||
|
"patch": {
|
||||||
|
"tags": ["lights"],
|
||||||
|
"summary": "Set individual LEDs",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ArrayOfLedsInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "succesful operation"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "invalid colors or wrong amount of LEDs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/lights/color/{color}": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["lights"],
|
||||||
|
"summary": "Turn on LEDs with specific color",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "color",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "FFCC00"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"description": "Color in RGB hex"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/lights/off": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["lights"],
|
||||||
|
"summary": "Turn LEDs off",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/restart": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["system"],
|
||||||
|
"summary": "Restart BTClock",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "successful operation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"RgbColorValues": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"red": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 255,
|
||||||
|
"example": 255
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 255,
|
||||||
|
"example": 204
|
||||||
|
},
|
||||||
|
"blue": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 255,
|
||||||
|
"example": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RgbColorHex": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hex": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^#(?:[0-9a-fA-F]{3}){1,2}$",
|
||||||
|
"example": "#FFCC00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RgbColorValueAndHex": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RgbColorValues"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RgbColorHex"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RgbColorValueOrHex": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RgbColorValues"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RgbColorHex"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ArrayOfLeds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/RgbColorValueAndHex"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ArrayOfLedsInput": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/RgbColorValueOrHex"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Settings": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fetchEurPrice": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Fetch EUR price instead of USD"
|
||||||
|
},
|
||||||
|
"fgColor": {
|
||||||
|
"type": "string",
|
||||||
|
"default": 16777215,
|
||||||
|
"description": "ePaper foreground (text) color"
|
||||||
|
},
|
||||||
|
"bgColor": {
|
||||||
|
"type": "string",
|
||||||
|
"default": 0,
|
||||||
|
"description": "ePaper background color"
|
||||||
|
},
|
||||||
|
"ledTestOnPower": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Do LED test on power-on"
|
||||||
|
},
|
||||||
|
"ledFlashOnUpd": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Flash LEDs on new block"
|
||||||
|
},
|
||||||
|
"mdnsEnabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Enable mDNS"
|
||||||
|
},
|
||||||
|
"otaEnabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Enable over-the-air updates"
|
||||||
|
},
|
||||||
|
"stealFocus": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Steal focus on new block"
|
||||||
|
},
|
||||||
|
"mcapBigChar": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Use big characters for market cap screen"
|
||||||
|
},
|
||||||
|
"mempoolInstance": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "mempool.space",
|
||||||
|
"description": "Mempool.space instance to connect to"
|
||||||
|
},
|
||||||
|
"ledBrightness": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 128,
|
||||||
|
"description": "Brightness of LEDs"
|
||||||
|
},
|
||||||
|
"fullRefreshMin": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 60,
|
||||||
|
"description": "Full refresh time of ePaper displays in minutes"
|
||||||
|
},
|
||||||
|
"screen[0]": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"screen[1]": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"screen[2]": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"screen[3]": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"screen[4]": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"screen[5]": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"screen[6]": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"tzOffset": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 60,
|
||||||
|
"description": "Timezone offset in minutes"
|
||||||
|
},
|
||||||
|
"minSecPriceUpd": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 30,
|
||||||
|
"description": "Minimum time between price updates in seconds"
|
||||||
|
},
|
||||||
|
"timePerScreen": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 30,
|
||||||
|
"description": "Time between screens when rotating in minutes"
|
||||||
|
},
|
||||||
|
"txPower": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "WiFi Tx Power"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CustomText": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"minItems": 7,
|
||||||
|
"maxItems": 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
316
static/swagger.yml
Normal file
316
static/swagger.yml
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: BTClock API
|
||||||
|
version: '3.0'
|
||||||
|
description: BTClock V3 API
|
||||||
|
servers:
|
||||||
|
- url: /api/
|
||||||
|
paths:
|
||||||
|
/status:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- system
|
||||||
|
summary: Get current status
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/system_status:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- system
|
||||||
|
summary: Get system status
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/settings:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- system
|
||||||
|
summary: Get current settings
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ArrayOfLeds'
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- system
|
||||||
|
summary: Save current settings
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Settings'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
patch:
|
||||||
|
tags:
|
||||||
|
- system
|
||||||
|
summary: Save current settings
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Settings'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/action/pause:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- timer
|
||||||
|
summary: Pause screen rotation
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/action/timer_restart:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- timer
|
||||||
|
summary: Restart screen rotation
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/show/screen/{id}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- screens
|
||||||
|
summary: Set screen to show
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: id
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
required: true
|
||||||
|
description: ID of screen to show
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/show/text/{text}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- screens
|
||||||
|
summary: Set text to show
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: text
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
default: text
|
||||||
|
required: true
|
||||||
|
description: Text to show
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/show/custom:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- screens
|
||||||
|
summary: Set text to show (advanced)
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CustomText'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/full_refresh:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- system
|
||||||
|
summary: Force full refresh of all displays
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/lights:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- lights
|
||||||
|
summary: Get LEDs status
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ArrayOfLeds'
|
||||||
|
/lights/set:
|
||||||
|
patch:
|
||||||
|
tags:
|
||||||
|
- lights
|
||||||
|
summary: Set individual LEDs
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ArrayOfLedsInput'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: succesful operation
|
||||||
|
'400':
|
||||||
|
description: invalid colors or wrong amount of LEDs
|
||||||
|
/lights/color/{color}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- lights
|
||||||
|
summary: Turn on LEDs with specific color
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: color
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
default: FFCC00
|
||||||
|
required: true
|
||||||
|
description: Color in RGB hex
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/lights/off:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- lights
|
||||||
|
summary: Turn LEDs off
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
/restart:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- system
|
||||||
|
summary: Restart BTClock
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
RgbColorValues:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
red:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
maximum: 255
|
||||||
|
example: 255
|
||||||
|
green:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
maximum: 255
|
||||||
|
example: 204
|
||||||
|
blue:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
maximum: 255
|
||||||
|
example: 0
|
||||||
|
RgbColorHex:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
hex:
|
||||||
|
type: string
|
||||||
|
pattern: ^#(?:[0-9a-fA-F]{3}){1,2}$
|
||||||
|
example: '#FFCC00'
|
||||||
|
RgbColorValueAndHex:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/RgbColorValues'
|
||||||
|
- $ref: '#/components/schemas/RgbColorHex'
|
||||||
|
RgbColorValueOrHex:
|
||||||
|
oneOf:
|
||||||
|
- $ref: '#/components/schemas/RgbColorValues'
|
||||||
|
- $ref: '#/components/schemas/RgbColorHex'
|
||||||
|
ArrayOfLeds:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RgbColorValueAndHex'
|
||||||
|
ArrayOfLedsInput:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RgbColorValueOrHex'
|
||||||
|
Settings:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
fetchEurPrice:
|
||||||
|
type: boolean
|
||||||
|
description: Fetch EUR price instead of USD
|
||||||
|
fgColor:
|
||||||
|
type: string
|
||||||
|
default: 16777215
|
||||||
|
description: ePaper foreground (text) color
|
||||||
|
bgColor:
|
||||||
|
type: string
|
||||||
|
default: 0
|
||||||
|
description: ePaper background color
|
||||||
|
ledTestOnPower:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: Do LED test on power-on
|
||||||
|
ledFlashOnUpd:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: Flash LEDs on new block
|
||||||
|
mdnsEnabled:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: Enable mDNS
|
||||||
|
otaEnabled:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: Enable over-the-air updates
|
||||||
|
stealFocus:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: Steal focus on new block
|
||||||
|
mcapBigChar:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: Use big characters for market cap screen
|
||||||
|
mempoolInstance:
|
||||||
|
type: string
|
||||||
|
default: mempool.space
|
||||||
|
description: Mempool.space instance to connect to
|
||||||
|
ledBrightness:
|
||||||
|
type: integer
|
||||||
|
default: 128
|
||||||
|
description: Brightness of LEDs
|
||||||
|
fullRefreshMin:
|
||||||
|
type: integer
|
||||||
|
default: 60
|
||||||
|
description: Full refresh time of ePaper displays in minutes
|
||||||
|
screen[0]:
|
||||||
|
type: boolean
|
||||||
|
screen[1]:
|
||||||
|
type: boolean
|
||||||
|
screen[2]:
|
||||||
|
type: boolean
|
||||||
|
screen[3]:
|
||||||
|
type: boolean
|
||||||
|
screen[4]:
|
||||||
|
type: boolean
|
||||||
|
screen[5]:
|
||||||
|
type: boolean
|
||||||
|
screen[6]:
|
||||||
|
type: boolean
|
||||||
|
tzOffset:
|
||||||
|
type: integer
|
||||||
|
default: 60
|
||||||
|
description: Timezone offset in minutes
|
||||||
|
minSecPriceUpd:
|
||||||
|
type: integer
|
||||||
|
default: 30
|
||||||
|
description: Minimum time between price updates in seconds
|
||||||
|
timePerScreen:
|
||||||
|
type: integer
|
||||||
|
default: 30
|
||||||
|
description: Time between screens when rotating in minutes
|
||||||
|
txPower:
|
||||||
|
type: integer
|
||||||
|
description: WiFi Tx Power
|
||||||
|
CustomText:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
minItems: 7
|
||||||
|
maxItems: 7
|
13
svelte.config.js
Normal file
13
svelte.config.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
kit: { adapter: adapter({
|
||||||
|
fallback: 'index.html',
|
||||||
|
precompress: false,
|
||||||
|
strict: true
|
||||||
|
}) }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
59
vite.config.ts
Normal file
59
vite.config.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { paraglideVitePlugin } from '@inlang/paraglide-js';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { svelteTesting } from '@testing-library/svelte/vite';
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
sveltekit(),
|
||||||
|
paraglideVitePlugin({
|
||||||
|
project: './project.inlang',
|
||||||
|
outdir: './src/lib/paraglide'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
minify: 'esbuild',
|
||||||
|
cssCodeSplit: false,
|
||||||
|
chunkSizeWarningLimit: 550,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// assetFileNames: '[hash][extname]',
|
||||||
|
entryFileNames: `[hash][extname]`,
|
||||||
|
chunkFileNames: `[hash][extname]`,
|
||||||
|
assetFileNames: `[hash][extname]`,
|
||||||
|
preserveModules: false,
|
||||||
|
|
||||||
|
manualChunks: () => {
|
||||||
|
return 'app';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
workspace: [
|
||||||
|
{
|
||||||
|
extends: './vite.config.ts',
|
||||||
|
plugins: [svelteTesting()],
|
||||||
|
test: {
|
||||||
|
name: 'client',
|
||||||
|
environment: 'jsdom',
|
||||||
|
clearMocks: true,
|
||||||
|
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||||
|
exclude: ['src/lib/server/**'],
|
||||||
|
setupFiles: ['./vitest-setup-client.ts']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
extends: './vite.config.ts',
|
||||||
|
test: {
|
||||||
|
name: 'server',
|
||||||
|
environment: 'node',
|
||||||
|
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||||
|
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
18
vitest-setup-client.ts
Normal file
18
vitest-setup-client.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// required for svelte5 + jsdom as jsdom does not support matchMedia
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
enumerable: true,
|
||||||
|
value: vi.fn().mockImplementation((query) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn()
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
// add more mocks here if you need them
|
Loading…
Add table
Add a link
Reference in a new issue