feat: Lint fixes, add forgejo workflow and e2e tests
Some checks failed
/ check-changes (push) Successful in 7s
/ build (push) Failing after 1m18s

This commit is contained in:
Djuri 2025-05-03 18:45:32 +02:00
parent af2f593fb8
commit 5917713b0d
Signed by: djuri
GPG key ID: 61B9B2DDE5AA3AC1
39 changed files with 1666 additions and 1506 deletions

View file

@ -0,0 +1,132 @@
on:
push:
branches:
- main
pull_request:
jobs:
check-changes:
runs-on: docker
outputs:
all_changed_and_modified_files_count: ${{ steps.changed-files.outputs.all_changed_and_modified_files_count }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get changed files count
id: changed-files
uses: tj-actions/changed-files@v45
with:
files_ignore: 'doc/**,README.md,Dockerfile,.*'
files_ignore_separator: ','
- name: Print changed files count
run: >
echo "Changed files count: ${{
steps.changed-files.outputs.all_changed_and_modified_files_count }}"
build:
needs: check-changes
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:js-22.04
if: ${{ needs.check-changes.outputs.all_changed_and_modified_files_count >= 1 }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
token: ${{ secrets.GH_TOKEN }}
node-version: lts/*
cache: yarn
cache-dependency-path: '**/yarn.lock'
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/node_modules
~/.cache/ms-playwright
key: ${{ runner.os }}-pio-playwright-${{ hashFiles('**/yarn.lock') }}
- name: Get current date
id: dateAndTime
run: echo "dateAndTime=$(date +'%Y-%m-%d-%H:%M')" >> $GITHUB_OUTPUT
- name: Install mklittlefs
run: >
git clone https://github.com/earlephilhower/mklittlefs.git /tmp/mklittlefs &&
cd /tmp/mklittlefs &&
git submodule update --init &&
make dist
- name: Install yarn
run: pnpm
- name: Run linter
run: pnpm lint
- name: Run vitest tests
run: pnpm vitest run
- name: Install Playwright Browsers
if: steps.cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Build WebUI
run: yarn build
# The following steps only run on push to main
- name: Get current block
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
id: getBlockHeight
run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
- name: Write block height to file
env:
BLOCK_HEIGHT: ${{ steps.getBlockHeight.outputs.blockHeight }}
run: mkdir -p output && echo "$BLOCK_HEIGHT" > output/version.txt
- name: gzip build for LittleFS
run: find dist -type f ! -name ".*" -exec sh -c 'mkdir -p "build_gz/$(dirname "${1#dist/}")" && gzip -k "$1" -c > "build_gz/${1#dist/}".gz' _ {} \;
- name: Write git rev to file
run: echo "$GITHUB_SHA" > build_gz/fs_hash.txt && echo "$GITHUB_SHA" > output/commit.txt
- name: Check GZipped directory size
run: |
# Set the threshold size in bytes
THRESHOLD=410000
# Calculate the total size of files in the directory
DIRECTORY_SIZE=$(du -b -s build_gz | awk '{print $1}')
# Fail the workflow if the size exceeds the threshold
if [ "$DIRECTORY_SIZE" -gt "$THRESHOLD" ]; then
echo "Directory size exceeds the threshold of $THRESHOLD bytes"
exit 1
else
echo "Directory size is within the threshold $DIRECTORY_SIZE"
fi
- name: Create tarball
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: tar czf webui.tgz --strip-components=1 dist
- name: Build LittleFS
run: |
set -e
/tmp/mklittlefs/mklittlefs -c build_gz -s 410000 output/littlefs.bin
- name: Upload artifacts
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
path: |
webui.tgz
output/littlefs.bin
- name: Create release
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: https://code.forgejo.org/actions/forgejo-release@v2.6.0
with:
url: 'https://git.btclock.dev/'
repo: '${{ github.repository }}'
direction: upload
tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
sha: '${{ github.sha }}'
release-dir: output
token: ${{ secrets.TOKEN }}
override: false
verbose: false
release-notes-assistant: false

View file

@ -1,6 +1,7 @@
import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
test('index page has expected columns control, status, settings', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Control' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Status' })).toBeVisible();
});

View file

@ -20,7 +20,9 @@ export default ts.config(
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: { 'no-undef': 'off' }
rules: {
'no-undef': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
@ -32,5 +34,10 @@ export default ts.config(
svelteConfig
}
}
},
{
rules: {
'svelte/no-at-html-tags': 'off'
}
}
);

View file

@ -1,152 +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"
"$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"
}

View file

@ -1,152 +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"
"$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"
}

View file

@ -1,26 +1,26 @@
@import 'tailwindcss';
@plugin "daisyui" {
}
:root {
--primary: #3b82f6;
--secondary: #6b7280;
--accent: #f59e0b;
--primary: #3b82f6;
--secondary: #6b7280;
--accent: #f59e0b;
}
html {
scroll-behavior: smooth;
scroll-behavior: smooth;
}
html, body {
@apply h-full;
html,
body {
@apply h-full;
}
html {
@apply bg-base-200;
@apply bg-base-200;
}
body {
@apply bg-base-200 pt-16;
@apply bg-base-200 pt-16;
}

View file

@ -2,11 +2,10 @@
<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>
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -1,112 +1,111 @@
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(() => {});
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(() => {});
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(() => {});
return fetch(`${baseUrl}/api/lights/off`).catch(() => {});
};
/**
* Restarts the clock
*/
export const restartClock = () => {
return fetch(`${baseUrl}/api/restart`).catch(() => {});
return fetch(`${baseUrl}/api/restart`).catch(() => {});
};
/**
* Forces a full refresh of the clock
*/
export const forceFullRefresh = () => {
return fetch(`${baseUrl}/api/full_refresh`).catch(() => {});
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')}`;
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}`);
}
return fetch(`${baseUrl}/api/show/screen/${screenId}`);
};
/**
* Sets the active currency
*/
export const setActiveCurrency = async (currency: string) => {
return fetch(`${baseUrl}/api/show/currency/${currency}`);
}
return fetch(`${baseUrl}/api/show/currency/${currency}`);
};
/**
* Turns on the frontlight
*/
export const turnOnFrontlight = () => {
return fetch(`${baseUrl}/api/frontlight/on`).catch(() => {});
return fetch(`${baseUrl}/api/frontlight/on`).catch(() => {});
};
/**
* Flashes the frontlight
*/
export const flashFrontlight = () => {
return fetch(`${baseUrl}/api/frontlight/flash`).catch(() => {});
return fetch(`${baseUrl}/api/frontlight/flash`).catch(() => {});
};
/**
* Turns off the frontlight
*/
export const turnOffFrontlight = () => {
return fetch(`${baseUrl}/api/frontlight/off`).catch(() => {});
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`);
}
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`);
}
e.preventDefault();
console.log(currentStatus);
if (!currentStatus) {
fetch(`${baseUrl}/api/dnd/enable`);
} else {
fetch(`${baseUrl}/api/dnd/disable`);
}
};

View file

@ -60,10 +60,6 @@
// $: if (containerWidth > 0) {
// containerHeight = containerWidth * deviceRatio;
// }
const fontSizeSingle = '4.5rem';
const fontSizeMedium = '2.0rem';
const fontSizeSplit = '1.0rem';
</script>
<div
@ -190,7 +186,6 @@
font-family: var(--font-family);
}
.vertical-desc .split-text {
transform: rotate(270deg);
height: 50%;

View file

@ -1,16 +1,11 @@
<script lang="ts">
let {
currency,
active = false,
onClick,
...restProps
} = $props();
let { currency, active = false, onClick, ...restProps } = $props();
</script>
<button
class="btn join-item {active ? 'btn-primary' : 'btn-outline'} btn-xs"
on:click={onClick}
{...restProps}
class="btn join-item {active ? 'btn-primary' : 'btn-outline'} btn-xs"
on:click={onClick}
{...restProps}
>
{currency}
{currency}
</button>

View file

@ -1,26 +1,19 @@
<script lang="ts">
let {
value = $bindable(''),
label = "",
placeholder = "",
id = "",
type = "text",
...restProps
} = $props();
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}
/>
{#if label}
<label for={id} class="label">
<span class="label-text">{label}</span>
</label>
{/if}
<input {type} {placeholder} {id} class="input input-bordered w-full" bind:value {...restProps} />
</div>

View file

@ -1,21 +1,10 @@
<script lang="ts">
let {
checked = $bindable(false),
label = "",
id = "",
...restProps
} = $props();
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 class="flex cursor-pointer items-center justify-between gap-2">
{#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>

View file

@ -1,17 +1,13 @@
<script lang="ts">
let {
title,
open = $bindable(false),
...restProps
} = $props();
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 class="collapse-arrow bg-base-200 collapse mb-2 rounded-lg" {...restProps}>
<input type="checkbox" bind:checked={open} />
<div class="collapse-title text-lg font-medium">
{title}
</div>
<div class="collapse-content">
<slot />
</div>
</div>

View file

@ -1,87 +1,105 @@
<script lang="ts">
let {
...restProps
} = $props();
let { ...restProps } = $props();
import { setLocale, getLocale } from '$lib/paraglide/runtime';
import { locales } from '$lib/paraglide/runtime';
import { page } from '$app/stores';
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' }
];
// 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;
}
// 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 getLocaleName = (locale: string) => {
return new Intl.DisplayNames([locale], { type: 'language' }).of(locale);
};
const getLanguageName = (locale: string) => {
return getLocaleName(locale.split('-')[0])
}
const getLanguageName = (locale: string) => {
return getLocaleName(locale.split('-')[0]);
};
const getEmojiFlag = (locale: string) => {
const countryCode = locale.split('-')[1];
const getEmojiFlag = (locale: string) => {
const countryCode = locale.split('-')[1];
if (!countryCode || countryCode === 'US') {
return '🇺🇸';
}
if (!countryCode || countryCode === 'US') {
return '🇺🇸';
}
return [...countryCode.toUpperCase()]
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
}
return [...countryCode.toUpperCase()]
.map((char) => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
};
// Function to get the current flag
const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧';
// 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 class="navbar bg-base-100 fixed top-0 z-50 w-full shadow-sm" {...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 bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
>
{#each navItems as { href, label } (href)}
<li>
<a {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 } (href)}
<li>
<a {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="menu dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-auto p-2 shadow"
>
{#each locales as locale (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>

View file

@ -17,10 +17,10 @@
import type { LedStatus } from '$lib/types';
let ledStatus = $state<LedStatus[]>([
{hex: '#000000'},
{hex: '#000000'},
{hex: '#000000'},
{hex: '#000000'}
{ hex: '#000000' },
{ hex: '#000000' },
{ hex: '#000000' },
{ hex: '#000000' }
]);
let customText = $state('');
let keepLedsSameColor = $state(false);
@ -81,19 +81,21 @@
<div class="flex justify-between gap-2">
<div class="mb-4 flex flex-wrap gap-2">
{#if ledStatus.length > 0}
{#each ledStatus as led}
{#each ledStatus as led (led)}
<input
type="color"
class="btn btn-square"
bind:value={led.hex}
onchange={checkSyncLeds}
/>
{/each}
/>
{/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-secondary" onclick={turnOffLeds}
>{m['section.control.turnOff']()}</button
>
<button class="btn btn-primary" onclick={() => setLEDcolor(ledStatus)}
>{m['section.control.setColor']()}</button
>
@ -102,19 +104,24 @@
</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>
<h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3>
<div class="flex justify-end gap-2">
<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>
</div>
{/if}
<div>
<h3 class="mb-2 font-medium">{m['section.control.title']()}</h3>
<div class="flex gap-2 justify-end">
<div class="flex justify-end gap-2">
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
</div>

View file

@ -1,341 +1,361 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages';
import { CardContainer, Toggle, CollapsibleSection } from '$lib/components';
import { settings } from '$lib/stores';
import { m } from '$lib/paraglide/messages';
import { CardContainer, Toggle, CollapsibleSection } from '$lib/components';
import { settings } from '$lib/stores';
let { ...restProps } = $props();
let { ...restProps } = $props();
// Show/hide toggles
let showAll = $state(false);
let hideAll = $state(false);
// Show/hide toggles
let showAll = $state(false);
let hideAll = $state(false);
function toggleShowAll() {
showAll = true;
hideAll = false;
}
function toggleShowAll() {
showAll = true;
hideAll = false;
}
function toggleHideAll() {
hideAll = true;
showAll = 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>
<CardContainer title={m['section.settings.title']()} {...restProps}>
<div class="mb-4 flex justify-end gap-2">
<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">
<div class="grid grid-cols-2 gap-4">
<CollapsibleSection
title={m['section.settings.section.screenSettings']()}
open={showAll || !hideAll}
>
<div class="grid grid-cols-2 gap-4">
<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>
<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.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.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.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.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>
<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 grid-cols-2 gap-4">
{#each $settings.screens as screen (screen.id)}
<div class="form-control">
<Toggle label={screen.name} checked={screen.enabled} />
</div>
{/each}
</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="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>restart required</span>
</div>
<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 grid-cols-2 gap-4">
{#each $settings.actCurrencies as currency (currency)}
<div class="form-control">
<Toggle label={currency} checked={$settings.actCurrencies.includes(currency)} />
</div>
{/each}
</div>
</CollapsibleSection>
<div class="grid gap-4 grid-cols-2">
<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>
{#each $settings.actCurrencies as currency}
<div class="form-control">
<Toggle
label={currency}
checked={$settings.actCurrencies.includes(currency)}
/>
</div>
{/each}
<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>
</CollapsibleSection>
<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>
<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 flex justify-between">
<label class="label">
<span class="label-text">{m['section.settings.fullRefreshEvery']()}</span>
</label>
<div class="input w-auto">
<input type="number" class="" min="1" max="60" value="60" />
<span class="label">{m['time.minutes']()}</span>
</div>
</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 flex justify-between">
<label class="label">
<span class="label-text">{m['section.settings.timeBetweenPriceUpdates']()}</span>
</label>
<div class="input w-auto">
<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.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">
<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 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">
<Toggle
label={m['section.settings.ledPowerOnTest']()}
checked={$settings.ledTestOnPower}
/>
</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">
<Toggle
label={m['section.settings.ledFlashOnBlock']()}
checked={$settings.ledFlashOnUpd}
/>
</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.disableLeds']()} checked={$settings.disableLeds} />
</div>
</div>
</CollapsibleSection>
<div class="form-control">
<Toggle
label={m["section.settings.ledPowerOnTest"]()}
checked={$settings.ledTestOnPower}
/>
</div>
{#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={m["section.settings.ledFlashOnBlock"]()}
checked={$settings.ledFlashOnUpd}
/>
</div>
<div class="form-control">
<Toggle label="Always On" checked={$settings.flAlwaysOn} />
</div>
<div class="form-control">
<Toggle
label={m["section.settings.disableLeds"]()}
checked={$settings.disableLeds}
/>
</div>
</div>
</CollapsibleSection>
<div class="form-control">
<Toggle label="Flash on Updates" checked={$settings.flFlashOnUpd} />
</div>
{#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="Flash on Zaps" checked={$settings.flFlashOnZap} />
</div>
<div class="form-control">
<Toggle
label="Always On"
checked={$settings.flAlwaysOn}
/>
</div>
{#if $settings.hasLightLevel}
<div class="form-control">
<Toggle label="Turn Off in Dark" checked={$settings.flOffWhenDark} />
</div>
<div class="form-control">
<Toggle
label="Flash on Updates"
checked={$settings.flFlashOnUpd}
/>
</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">
<Toggle
label="Flash on Zaps"
checked={$settings.flFlashOnZap}
/>
</div>
<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>
{#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">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}
<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}
<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">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">{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">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}
<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.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>
<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>
<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>
<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="mt-1 text-sm">{m['section.settings.tzOffsetHelpText']()}</p>
</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>
<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>
<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>
<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="mt-6 flex justify-between">
<button class="btn btn-error">{m['button.reset']()}</button>
<button class="btn btn-primary">{m['button.save']()}</button>
</div>
</CardContainer>

View file

@ -2,42 +2,43 @@
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 { 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
}));
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="mx-auto space-y-4">
<div class="join">
{#each screens as screen}
<TabButton active={$status.currentScreen === screen.id} onClick={() => setActiveScreen(screen.id)}>
{#each screens as screen (screen.id)}
<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}
{#each $settings.actCurrencies as currency (currency)}
<CurrencyButton
{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>
@ -47,8 +48,8 @@
<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')} )
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings.dnd
.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
{/if}
</small>
</div>
@ -58,18 +59,28 @@
<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'} />
<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'} />
<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>
<div class="stats mt-4 flex justify-center shadow">
<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>

View file

@ -1,102 +1,99 @@
<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';
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>
<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>
<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>
<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 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="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>
<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>

View file

@ -1,16 +1,12 @@
<script lang="ts">
let {
title,
className = "",
...restProps
} = $props();
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 class="card bg-base-100 w-full shadow-xl {className}" {...restProps}>
<div class="card-body">
{#if title}
<h2 class="card-title">{title}</h2>
{/if}
<slot />
</div>
</div>

View file

@ -1,23 +1,16 @@
<script lang="ts">
let {
title,
value,
desc = "",
icon = "",
className = "",
...restProps
} = $props();
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}
{#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>

View file

@ -1,33 +1,35 @@
<script lang="ts">
let {
status = "offline" as "online" | "offline" | "error" | "warning",
text,
className = "",
...restProps
} = $props();
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";
}
};
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 class="relative flex">
<div class="{getStatusColor()} h-3 w-3 rounded-full"></div>
{#if status === 'online'}
<div
class="{getStatusColor()} absolute inline-flex h-3 w-3 animate-ping rounded-full opacity-75"
></div>
{/if}
</div>
<span class="text-sm">{text}</span>
</div>

View file

@ -1,15 +1,11 @@
<script lang="ts">
let {
active = false,
onClick,
...restProps
} = $props();
let { active = false, onClick, ...restProps } = $props();
</script>
<button
class="btn btn-sm join-item {active ? 'btn-primary' : 'btn-outline'}"
on:click={onClick}
{...restProps}
class="btn btn-sm join-item {active ? 'btn-primary' : 'btn-outline'}"
on:click={onClick}
{...restProps}
>
<slot />
<slot />
</button>

View file

@ -1,71 +1,86 @@
<script lang="ts">
type Position =
| 'top-start' | 'top-center' | 'top-end'
| 'middle-start' | 'middle-center' | 'middle-end'
| 'bottom-start' | 'bottom-center' | 'bottom-end';
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';
type AlertType = 'info' | 'success' | 'warning' | 'error';
let props = $props();
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;
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;
// 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);
let visible = $state(true);
// Map position prop to DaisyUI classes
const getPositionClasses = (pos: Position): string => {
const [vertical, horizontal] = pos.split('-');
// Map position prop to DaisyUI classes
const getPositionClasses = (pos: Position): string => {
const [vertical, horizontal] = pos.split('-');
let classes = 'toast';
let classes = 'toast';
// Vertical position
if (vertical === 'top') classes += ' toast-top';
if (vertical === 'middle') classes += ' toast-middle';
// bottom is default, no class needed
// 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';
// Horizontal position
if (horizontal === 'start') classes += ' toast-start';
if (horizontal === 'center') classes += ' toast-center';
if (horizontal === 'end') classes += ' toast-end';
return classes;
};
return classes;
};
// Auto-hide the toast after duration
if (duration > 0) {
setTimeout(() => {
visible = false;
}, duration);
}
// Auto-hide the toast after duration
if (duration > 0) {
setTimeout(() => {
visible = false;
}, duration);
}
const closeToast = () => {
visible = false;
};
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>
<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}

View file

@ -4,158 +4,158 @@ 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
}
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);
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),
});
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}`);
}
if (!response.ok) {
throw new Error(`Error updating settings: ${response.statusText}`);
}
// Update the local store with the new settings
update(currentSettings => ({ ...currentSettings, ...newSettings }));
// 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)
};
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();
settings.fetch();
}

View file

@ -3,178 +3,178 @@ 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"}
]
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;
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;
}
};
// 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();
// 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;
// Only run in the browser, not during SSR
if (typeof window === 'undefined') return;
try {
// Create a new EventSource connection
eventSource = new EventSource(`${baseUrl}/events`);
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 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 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 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 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,
// 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}`);
// 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}`);
}
if (!response.ok) {
throw new Error(`Error setting screen: ${response.statusText}`);
}
// Update the store with the new screen ID
update(currentStatus => ({
...currentStatus,
currentScreen: id
}));
// 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 true;
} catch (error) {
console.error('Failed to set screen:', error);
return false;
}
}
};
return store;
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());
status.fetch().then(() => status.startListening());
// Clean up the EventSource when the window is unloaded
window.addEventListener('beforeunload', () => {
status.stopListening();
});
// Clean up the EventSource when the window is unloaded
window.addEventListener('beforeunload', () => {
status.stopListening();
});
}

View file

@ -3,7 +3,7 @@
*/
export interface LedStatus {
hex: string;
hex: string;
}
/**
@ -26,31 +26,31 @@ export interface Status {
espFreeHeap: number;
espHeapSize: number;
connectionStatus: {
price: boolean;
blocks: boolean;
V2: boolean;
nostr: boolean;
price: boolean;
blocks: boolean;
V2: boolean;
nostr: boolean;
};
rssi: number;
currency: string;
dnd: {
enabled: boolean;
timeBasedEnabled: boolean;
startTime: string;
endTime: string;
active: boolean;
enabled: boolean;
timeBasedEnabled: boolean;
startTime: string;
endTime: string;
active: boolean;
};
data: string[];
leds: Array<{
red: number;
green: number;
blue: number;
hex: string;
red: number;
green: number;
blue: number;
hex: string;
}>;
[key: string]: unknown;
}
}
// Define the Settings interface based on the API response structure
// Define the Settings interface based on the API response structure
export interface Settings {
numScreens: number;
invertedColor: boolean;
@ -120,9 +120,9 @@ export interface Settings {
gitTag: string;
lastBuildTime: string;
screens: Array<{
id: number;
name: string;
enabled: boolean;
id: number;
name: string;
enabled: boolean;
}>;
actCurrencies: string[];
availableCurrencies: string[];
@ -130,13 +130,12 @@ export interface Settings {
ceEndpoint: string;
ceDisableSSL: boolean;
dnd: {
enabled: boolean;
timeBasedEnabled: boolean;
startHour: number;
startMinute: number;
endHour: number;
endMinute: number;
enabled: boolean;
timeBasedEnabled: boolean;
startHour: number;
startMinute: number;
endHour: number;
endMinute: number;
};
[key: string]: unknown;
}
}

View file

@ -1,22 +1,22 @@
export const toTime = (secs: number) => {
const hours = Math.floor(secs / (60 * 60));
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_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 divisor_for_seconds = divisor_for_minutes % 60;
const seconds = Math.ceil(divisor_for_seconds);
const obj = {
h: hours,
m: minutes,
s: seconds
};
return obj;
const obj = {
h: hours,
m: minutes,
s: seconds
};
return obj;
};
export const toUptimestring = (secs: number): string => {
const time = toTime(secs);
const time = toTime(secs);
return `${time.h}h ${time.m}m ${time.s}s`;
return `${time.h}h ${time.m}m ${time.s}s`;
};

View file

@ -25,7 +25,6 @@
status.stopListening();
}
});
export const prerender = true;
</script>
<Navbar />

2
src/routes/+layout.ts Normal file
View file

@ -0,0 +1,2 @@
export const prerender = true;
export const ssr = false;

View file

@ -1,21 +1,15 @@
<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';
import { ControlSection, StatusSection } 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="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
<div>
<ControlSection />
</div>
<div class="col-span-2">
<StatusSection />
</div>
</div>
<div class="col-span-2">
<StatusSection />
</div>
</div>
</div>

View file

@ -3,17 +3,17 @@
import { onMount, onDestroy } from 'svelte';
let isLoaded = $state(false);
let scalarApiReference;
let scalarApiReference;
function initializeScalar() {
// @ts-ignore - Scalar is loaded dynamically
// @ts-expect-error - Scalar is loaded dynamically
if (window.Scalar) {
// @ts-ignore - Scalar is loaded dynamically
// @ts-expect-error - Scalar is loaded dynamically
scalarApiReference = window.Scalar.createApiReference('#app', {
url: '/swagger.json',
hideDarkModeToggle: true,
hideClientButton: true,
baseServerURL: baseUrl
hideDarkModeToggle: true,
hideClientButton: true,
baseServerURL: baseUrl
});
isLoaded = true;
} else {
@ -34,33 +34,33 @@
}
let darkMode = $state(false);
let handler: (e: MediaQueryListEvent) => void;
let mediaQuery: MediaQueryList;
let handler: (e: MediaQueryListEvent) => void;
let mediaQuery: MediaQueryList;
onMount(() => {
loadScalarScript();
darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
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);
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());
onDestroy(() => {
if (mediaQuery) {
mediaQuery.removeEventListener('change', handler);
}
if (isLoaded) {
document.querySelectorAll('style[data-scalar]').forEach((el) => el.remove());
scalarApiReference.destroy();
}
});
scalarApiReference.destroy();
}
});
</script>
<div class="relative">

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { SettingsSection } from '$lib/components';
import { SettingsSection } from '$lib/components';
</script>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Settings</h1>
<SettingsSection />
<h1 class="mb-4 text-2xl font-bold">Settings</h1>
<SettingsSection />
</div>

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { SystemSection } from '$lib/components';
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 />
<h1 class="mb-4 text-2xl font-bold">System Management</h1>
<SystemSection />
</div>

View file

@ -3,11 +3,16 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
kit: { adapter: adapter({
fallback: 'index.html',
precompress: false,
strict: true
}) }
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'bundle.html',
precompress: false,
strict: true
}),
appDir: 'build'
}
};
export default config;