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

View file

@ -1,152 +1,152 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"section": { "section": {
"settings": { "settings": {
"title": "Einstellungen", "title": "Einstellungen",
"textColor": "Textfarbe", "textColor": "Textfarbe",
"backgroundColor": "Hintergrundfarbe", "backgroundColor": "Hintergrundfarbe",
"ledPowerOnTest": "LED-Einschalttest", "ledPowerOnTest": "LED-Einschalttest",
"ledFlashOnBlock": "LED blinkt bei neuem Block", "ledFlashOnBlock": "LED blinkt bei neuem Block",
"timePerScreen": "Zeit pro Bildschirm", "timePerScreen": "Zeit pro Bildschirm",
"ledBrightness": "LED-Helligkeit", "ledBrightness": "LED-Helligkeit",
"flMaxBrightness": "Displaybeleuchtung Helligkeit", "flMaxBrightness": "Displaybeleuchtung Helligkeit",
"timezoneOffset": "Zeitzonenoffset", "timezoneOffset": "Zeitzonenoffset",
"timeBetweenPriceUpdates": "Zeit zwischen Preisaktualisierungen", "timeBetweenPriceUpdates": "Zeit zwischen Preisaktualisierungen",
"fullRefreshEvery": "Vollständige Aktualisierung alle", "fullRefreshEvery": "Vollständige Aktualisierung alle",
"mempoolnstance": "Mempool Instance", "mempoolnstance": "Mempool Instance",
"hostnamePrefix": "Hostnamen-Präfix", "hostnamePrefix": "Hostnamen-Präfix",
"StealFocusOnNewBlock": "Steal focus on new block", "StealFocusOnNewBlock": "Steal focus on new block",
"useBigCharsMcap": "Verwende große Zeichen für die Marktkapitalisierung", "useBigCharsMcap": "Verwende große Zeichen für die Marktkapitalisierung",
"useBlkCountdown": "Blocks Countdown zur Halbierung", "useBlkCountdown": "Blocks Countdown zur Halbierung",
"useSatsSymbol": "Sats-Symbol verwenden", "useSatsSymbol": "Sats-Symbol verwenden",
"suffixPrice": "Suffix-Preisformat", "suffixPrice": "Suffix-Preisformat",
"disableLeds": "Alle LED-Effekte deaktivieren", "disableLeds": "Alle LED-Effekte deaktivieren",
"otaUpdates": "OTA updates", "otaUpdates": "OTA updates",
"enableMdns": "mDNS", "enableMdns": "mDNS",
"fetchEuroPrice": "€-Preis abrufen", "fetchEuroPrice": "€-Preis abrufen",
"shortAmountsWarning": "Geringe Beträge können die Lebensdauer der Displays verkürzen", "shortAmountsWarning": "Geringe Beträge können die Lebensdauer der Displays verkürzen",
"tzOffsetHelpText": "Ein Neustart ist erforderlich, um den TZ-Offset anzuwenden.", "tzOffsetHelpText": "Ein Neustart ist erforderlich, um den TZ-Offset anzuwenden.",
"screens": "Bildschirme", "screens": "Bildschirme",
"wifiTxPowerText": "In den meisten Fällen muss dies nicht eingestellt werden.", "wifiTxPowerText": "In den meisten Fällen muss dies nicht eingestellt werden.",
"wifiTxPower": "WiFi-TX-Leistung", "wifiTxPower": "WiFi-TX-Leistung",
"settingsSaved": "Einstellungen gespeichert", "settingsSaved": "Einstellungen gespeichert",
"errorSavingSettings": "Fehler beim Speichern der Einstellungen", "errorSavingSettings": "Fehler beim Speichern der Einstellungen",
"ownDataSource": "BTClock-Datenquelle", "ownDataSource": "BTClock-Datenquelle",
"flAlwaysOn": "Displaybeleuchtung immer an", "flAlwaysOn": "Displaybeleuchtung immer an",
"flEffectDelay": "Displaybeleuchtungeffekt Geschwindigkeit", "flEffectDelay": "Displaybeleuchtungeffekt Geschwindigkeit",
"flFlashOnUpd": "Displaybeleuchting bei neuem Block", "flFlashOnUpd": "Displaybeleuchting bei neuem Block",
"mempoolInstanceHelpText": "Nur wirksam, wenn die BTClock-Datenquelle deaktiviert ist. \nZur Anwendung ist ein Neustart erforderlich.", "mempoolInstanceHelpText": "Nur wirksam, wenn die BTClock-Datenquelle deaktiviert ist. \nZur Anwendung ist ein Neustart erforderlich.",
"luxLightToggle": "Automatisches Umschalten des Frontlichts bei Lux", "luxLightToggle": "Automatisches Umschalten des Frontlichts bei Lux",
"wpTimeout": "WiFi-Konfigurationsportal timeout", "wpTimeout": "WiFi-Konfigurationsportal timeout",
"useNostr": "Nostr-Datenquelle verwenden", "useNostr": "Nostr-Datenquelle verwenden",
"flDisable": "Displaybeleuchtung deaktivieren", "flDisable": "Displaybeleuchtung deaktivieren",
"httpAuthUser": "WebUI-Benutzername", "httpAuthUser": "WebUI-Benutzername",
"httpAuthPass": "WebUI-Passwort", "httpAuthPass": "WebUI-Passwort",
"httpAuthText": "Schützt nur die WebUI mit einem Passwort, nicht API-Aufrufe.", "httpAuthText": "Schützt nur die WebUI mit einem Passwort, nicht API-Aufrufe.",
"currencies": "Währungen", "currencies": "Währungen",
"mowMode": "Mow suffixmodus", "mowMode": "Mow suffixmodus",
"suffixShareDot": "Kompakte Suffix-Notation", "suffixShareDot": "Kompakte Suffix-Notation",
"section": { "section": {
"displaysAndLed": "Anzeigen und LEDs", "displaysAndLed": "Anzeigen und LEDs",
"screenSettings": "Infospezifisch", "screenSettings": "Infospezifisch",
"dataSource": "Datenquelle", "dataSource": "Datenquelle",
"extraFeatures": "Zusätzliche Funktionen", "extraFeatures": "Zusätzliche Funktionen",
"system": "System" "system": "System"
}, },
"ledFlashOnZap": "LED blinkt bei Nostr Zap", "ledFlashOnZap": "LED blinkt bei Nostr Zap",
"flFlashOnZap": "Displaybeleuchting bei Nostr Zap", "flFlashOnZap": "Displaybeleuchting bei Nostr Zap",
"showAll": "Alle anzeigen", "showAll": "Alle anzeigen",
"hideAll": "Alles ausblenden", "hideAll": "Alles ausblenden",
"flOffWhenDark": "Displaybeleuchtung aus, wenn es dunkel ist", "flOffWhenDark": "Displaybeleuchtung aus, wenn es dunkel ist",
"luxLightToggleText": "Zum Deaktivieren auf 0 setzen", "luxLightToggleText": "Zum Deaktivieren auf 0 setzen",
"verticalDesc": "Vrtikale Bildschirmbeschreibung", "verticalDesc": "Vrtikale Bildschirmbeschreibung",
"enableDebugLog": "Debug-Protokoll aktivieren", "enableDebugLog": "Debug-Protokoll aktivieren",
"bitaxeEnabled": "BitAxe-Integration aktivieren", "bitaxeEnabled": "BitAxe-Integration aktivieren",
"miningPoolStats": "Mining-Pool-Statistiken Integration Aktivieren", "miningPoolStats": "Mining-Pool-Statistiken Integration Aktivieren",
"nostrZapNotify": "Nostr Zap-Benachrichtigungen aktivieren", "nostrZapNotify": "Nostr Zap-Benachrichtigungen aktivieren",
"thirdPartySource": "mempool.space/coincap.io Verwenden", "thirdPartySource": "mempool.space/coincap.io Verwenden",
"dataSource": { "dataSource": {
"nostr": "Nostr-Verlag", "nostr": "Nostr-Verlag",
"custom": "Benutzerdefinierter dataquelle" "custom": "Benutzerdefinierter dataquelle"
}, },
"fontName": "Schriftart", "fontName": "Schriftart",
"timeBasedDnd": "Aktivieren Sie den Zeitplan „Bitte nicht stören“.", "timeBasedDnd": "Aktivieren Sie den Zeitplan „Bitte nicht stören“.",
"dndStartHour": "Startstunde", "dndStartHour": "Startstunde",
"dndStartMinute": "Startminute", "dndStartMinute": "Startminute",
"dndEndHour": "Endstunde", "dndEndHour": "Endstunde",
"dndEndMinute": "Schlussminute" "dndEndMinute": "Schlussminute"
}, },
"control": { "control": {
"systemInfo": "Systeminfo", "systemInfo": "Systeminfo",
"version": "Version", "version": "Version",
"buildTime": "Build time", "buildTime": "Build time",
"ledColor": "LED-Farbe", "ledColor": "LED-Farbe",
"turnOff": "Ausschalten", "turnOff": "Ausschalten",
"setColor": "Farbe festlegen", "setColor": "Farbe festlegen",
"showText": "Text anzeigen", "showText": "Text anzeigen",
"text": "Text", "text": "Text",
"title": "Kontrolle", "title": "Kontrolle",
"hostname": "Hostname", "hostname": "Hostname",
"frontlight": "Displaybeleuchtung", "frontlight": "Displaybeleuchtung",
"turnOn": "Einschalten", "turnOn": "Einschalten",
"flashFrontlight": "Blinken" "flashFrontlight": "Blinken"
}, },
"status": { "status": {
"title": "Status", "title": "Status",
"screenCycle": "Bildschirmzyklus", "screenCycle": "Bildschirmzyklus",
"memoryFree": "Speicher frei", "memoryFree": "Speicher frei",
"wsPriceConnection": "WS-Preisverbindung", "wsPriceConnection": "WS-Preisverbindung",
"wsMempoolConnection": "WS {instance}-Verbindung", "wsMempoolConnection": "WS {instance}-Verbindung",
"fetchEuroNote": "If you use \"Fetch € price\" the WS Price connection will show ❌ since it uses another data source.", "fetchEuroNote": "If you use \"Fetch € price\" the WS Price connection will show ❌ since it uses another data source.",
"uptime": "Betriebszeit", "uptime": "Betriebszeit",
"wifiSignalStrength": "WiFi-Signalstärke", "wifiSignalStrength": "WiFi-Signalstärke",
"wsDataConnection": "BTClock-Datenquelle verbindung", "wsDataConnection": "BTClock-Datenquelle verbindung",
"lightSensor": "Lichtsensor", "lightSensor": "Lichtsensor",
"nostrConnection": "Nostr Relay-Verbindung", "nostrConnection": "Nostr Relay-Verbindung",
"doNotDisturb": "Bitte nicht stören", "doNotDisturb": "Bitte nicht stören",
"timeBasedDnd": "Zeitbasierter Zeitplan" "timeBasedDnd": "Zeitbasierter Zeitplan"
}, },
"firmwareUpdater": { "firmwareUpdater": {
"fileUploadSuccess": "Datei erfolgreich hochgeladen, Gerät neu gestartet. WebUI in {countdown} Sekunden neu geladen", "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.", "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", "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.", "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.", "swUpToDate": "Du hast die neueste Version.",
"swUpdateAvailable": "Eine neuere Version ist verfügbar!", "swUpdateAvailable": "Eine neuere Version ist verfügbar!",
"latestVersion": "Letzte Version", "latestVersion": "Letzte Version",
"releaseDate": "Veröffentlichungsdatum", "releaseDate": "Veröffentlichungsdatum",
"viewRelease": "Veröffentlichung anzeigen", "viewRelease": "Veröffentlichung anzeigen",
"autoUpdate": "Update installieren (experimentell)", "autoUpdate": "Update installieren (experimentell)",
"autoUpdateInProgress": "Automatische Aktualisierung läuft, bitte warten..." "autoUpdateInProgress": "Automatische Aktualisierung läuft, bitte warten..."
} }
}, },
"colors": { "colors": {
"black": "Schwarz", "black": "Schwarz",
"white": "Weiss" "white": "Weiss"
}, },
"time": { "time": {
"minutes": "Minuten", "minutes": "Minuten",
"seconds": "Sekunden" "seconds": "Sekunden"
}, },
"restartRequired": "Neustart erforderlich", "restartRequired": "Neustart erforderlich",
"button": { "button": {
"save": "Speichern", "save": "Speichern",
"reset": "Zurücksetzen", "reset": "Zurücksetzen",
"restart": "Neustart", "restart": "Neustart",
"forceFullRefresh": "Vollständige Aktualisierung erzwingen" "forceFullRefresh": "Vollständige Aktualisierung erzwingen"
}, },
"timer": { "timer": {
"running": "läuft", "running": "läuft",
"stopped": "gestoppt" "stopped": "gestoppt"
}, },
"sections": { "sections": {
"control": { "control": {
"keepSameColor": "Gleiche Farbe beibehalten" "keepSameColor": "Gleiche Farbe beibehalten"
} }
}, },
"rssiBar": { "rssiBar": {
"tooltip": "Werte > -67 dBm gelten als gut. > -30 dBm ist erstaunlich" "tooltip": "Werte > -67 dBm gelten als gut. > -30 dBm ist erstaunlich"
}, },
"warning": "Achtung", "warning": "Achtung",
"auto-detect": "Automatische Erkennung" "auto-detect": "Automatische Erkennung"
} }

View file

@ -171,4 +171,4 @@
"auto-detect": "Auto-detect", "auto-detect": "Auto-detect",
"on": "on", "on": "on",
"off": "off" "off": "off"
} }

View file

@ -1,152 +1,152 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from es!", "hello_world": "Hello, {name} from es!",
"section": { "section": {
"settings": { "settings": {
"title": "Configuración", "title": "Configuración",
"textColor": "Color de texto", "textColor": "Color de texto",
"backgroundColor": "Color de fondo", "backgroundColor": "Color de fondo",
"ledBrightness": "Brillo LED", "ledBrightness": "Brillo LED",
"screens": "Pantallas", "screens": "Pantallas",
"shortAmountsWarning": "Pequeñas cantidades pueden acortar la vida útil de los displays", "shortAmountsWarning": "Pequeñas cantidades pueden acortar la vida útil de los displays",
"fullRefreshEvery": "Actualización completa cada", "fullRefreshEvery": "Actualización completa cada",
"timePerScreen": "Tiempo por pantalla", "timePerScreen": "Tiempo por pantalla",
"tzOffsetHelpText": "Es necesario reiniciar para aplicar la compensación.", "tzOffsetHelpText": "Es necesario reiniciar para aplicar la compensación.",
"timezoneOffset": "Compensación de zona horaria", "timezoneOffset": "Compensación de zona horaria",
"StealFocusOnNewBlock": "Presta atención al nuevo bloque", "StealFocusOnNewBlock": "Presta atención al nuevo bloque",
"ledFlashOnBlock": "El LED parpadea con un bloque nuevo", "ledFlashOnBlock": "El LED parpadea con un bloque nuevo",
"useBigCharsMcap": "Utilice caracteres grandes para la market cap", "useBigCharsMcap": "Utilice caracteres grandes para la market cap",
"useBlkCountdown": "Cuenta regresiva en bloques", "useBlkCountdown": "Cuenta regresiva en bloques",
"useSatsSymbol": "Usar símbolo sats", "useSatsSymbol": "Usar símbolo sats",
"fetchEuroPrice": "Obtener precio en €", "fetchEuroPrice": "Obtener precio en €",
"timeBetweenPriceUpdates": "Tiempo entre actualizaciones de precios", "timeBetweenPriceUpdates": "Tiempo entre actualizaciones de precios",
"ledPowerOnTest": "Prueba de encendido del LED", "ledPowerOnTest": "Prueba de encendido del LED",
"enableMdns": "mDNS", "enableMdns": "mDNS",
"hostnamePrefix": "Prefijo de nombre de host", "hostnamePrefix": "Prefijo de nombre de host",
"mempoolnstance": "Instancia de Mempool", "mempoolnstance": "Instancia de Mempool",
"suffixPrice": "Precio con sufijos", "suffixPrice": "Precio con sufijos",
"disableLeds": "Desactivar efectos de LED", "disableLeds": "Desactivar efectos de LED",
"otaUpdates": "Actualización por aire", "otaUpdates": "Actualización por aire",
"wifiTxPowerText": "En la mayoría de los casos no es necesario configurar esto.", "wifiTxPowerText": "En la mayoría de los casos no es necesario configurar esto.",
"settingsSaved": "Configuración guardada", "settingsSaved": "Configuración guardada",
"errorSavingSettings": "Error al guardar la configuración", "errorSavingSettings": "Error al guardar la configuración",
"ownDataSource": "fuente de datos BTClock", "ownDataSource": "fuente de datos BTClock",
"flMaxBrightness": "Brillo de luz de la pantalla", "flMaxBrightness": "Brillo de luz de la pantalla",
"flAlwaysOn": "Luz de la pantalla siempre encendida", "flAlwaysOn": "Luz de la pantalla siempre encendida",
"flEffectDelay": "Velocidad del efecto de luz de la pantalla", "flEffectDelay": "Velocidad del efecto de luz de la pantalla",
"flFlashOnUpd": "Luz de la pantalla parpadea con un nuevo bloque", "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.", "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", "luxLightToggle": "Cambio automático de luz frontal en lux",
"wpTimeout": "Portal de configuración WiFi timeout", "wpTimeout": "Portal de configuración WiFi timeout",
"useNostr": "Utilice la fuente de datos Nostr", "useNostr": "Utilice la fuente de datos Nostr",
"flDisable": "Desactivar luz de la pantalla", "flDisable": "Desactivar luz de la pantalla",
"httpAuthUser": "Nombre de usuario WebUI", "httpAuthUser": "Nombre de usuario WebUI",
"httpAuthPass": "Contraseña WebUI", "httpAuthPass": "Contraseña WebUI",
"httpAuthText": "Solo la WebUI está protegida con contraseña, no las llamadas API.", "httpAuthText": "Solo la WebUI está protegida con contraseña, no las llamadas API.",
"currencies": "Monedas", "currencies": "Monedas",
"mowMode": "Modo de sufijo Mow", "mowMode": "Modo de sufijo Mow",
"suffixShareDot": "Notación compacta de sufijo", "suffixShareDot": "Notación compacta de sufijo",
"section": { "section": {
"displaysAndLed": "Pantallas y LED", "displaysAndLed": "Pantallas y LED",
"screenSettings": "Específico de la pantalla", "screenSettings": "Específico de la pantalla",
"dataSource": "fuente de datos", "dataSource": "fuente de datos",
"extraFeatures": "Funciones adicionales", "extraFeatures": "Funciones adicionales",
"system": "Sistema" "system": "Sistema"
}, },
"ledFlashOnZap": "LED parpadeante con Nostr Zap", "ledFlashOnZap": "LED parpadeante con Nostr Zap",
"flFlashOnZap": "Flash de luz frontal con Nostr Zap", "flFlashOnZap": "Flash de luz frontal con Nostr Zap",
"showAll": "Mostrar todo", "showAll": "Mostrar todo",
"hideAll": "Ocultar todo", "hideAll": "Ocultar todo",
"flOffWhenDark": "Luz de la pantalla cuando está oscuro", "flOffWhenDark": "Luz de la pantalla cuando está oscuro",
"luxLightToggleText": "Establecer en 0 para desactivar", "luxLightToggleText": "Establecer en 0 para desactivar",
"verticalDesc": "Descripción de pantalla vertical", "verticalDesc": "Descripción de pantalla vertical",
"enableDebugLog": "Habilitar registro de depuración", "enableDebugLog": "Habilitar registro de depuración",
"bitaxeEnabled": "Habilitar la integración de BitAxe", "bitaxeEnabled": "Habilitar la integración de BitAxe",
"miningPoolStats": "Habilitar la integración de estadísticas del grupo minero", "miningPoolStats": "Habilitar la integración de estadísticas del grupo minero",
"nostrZapNotify": "Habilitar notificaciones de Nostr Zap", "nostrZapNotify": "Habilitar notificaciones de Nostr Zap",
"thirdPartySource": "Utilice mempool.space/coincap.io", "thirdPartySource": "Utilice mempool.space/coincap.io",
"dataSource": { "dataSource": {
"nostr": "editorial nostr", "nostr": "editorial nostr",
"custom": "Punto final personalizado" "custom": "Punto final personalizado"
}, },
"fontName": "Fuente", "fontName": "Fuente",
"timeBasedDnd": "Habilitar el horario de No molestar", "timeBasedDnd": "Habilitar el horario de No molestar",
"dndStartHour": "Hora de inicio", "dndStartHour": "Hora de inicio",
"dndStartMinute": "Minuto de inicio", "dndStartMinute": "Minuto de inicio",
"dndEndHour": "Hora final", "dndEndHour": "Hora final",
"dndEndMinute": "Minuto final" "dndEndMinute": "Minuto final"
}, },
"control": { "control": {
"turnOff": "Apagar", "turnOff": "Apagar",
"setColor": "Establecer el color", "setColor": "Establecer el color",
"version": "Versión", "version": "Versión",
"ledColor": "color del LED", "ledColor": "color del LED",
"systemInfo": "Info del sistema", "systemInfo": "Info del sistema",
"showText": "Mostrar texto", "showText": "Mostrar texto",
"text": "Texto", "text": "Texto",
"title": "Control", "title": "Control",
"buildTime": "Tiempo de compilación", "buildTime": "Tiempo de compilación",
"hostname": "Nombre del host", "hostname": "Nombre del host",
"turnOn": "Encender", "turnOn": "Encender",
"frontlight": "Luz de la pantalla", "frontlight": "Luz de la pantalla",
"flashFrontlight": "Luz intermitente" "flashFrontlight": "Luz intermitente"
}, },
"status": { "status": {
"memoryFree": "Memoria RAM libre", "memoryFree": "Memoria RAM libre",
"wsPriceConnection": "Conexión WebSocket Precio", "wsPriceConnection": "Conexión WebSocket Precio",
"wsMempoolConnection": "Conexión WebSocket {instance}", "wsMempoolConnection": "Conexión WebSocket {instance}",
"screenCycle": "Ciclo de pantalla", "screenCycle": "Ciclo de pantalla",
"uptime": "Tiempo de funcionamiento", "uptime": "Tiempo de funcionamiento",
"fetchEuroNote": "Si utiliza \"Obtener precio en €\", la conexión de Precio WS mostrará ❌ ya que utiliza otra fuente de datos.", "fetchEuroNote": "Si utiliza \"Obtener precio en €\", la conexión de Precio WS mostrará ❌ ya que utiliza otra fuente de datos.",
"title": "Estado", "title": "Estado",
"wifiSignalStrength": "Fuerza de la señal WiFi", "wifiSignalStrength": "Fuerza de la señal WiFi",
"wsDataConnection": "Conexión de fuente de datos BTClock", "wsDataConnection": "Conexión de fuente de datos BTClock",
"lightSensor": "Sensor de luz", "lightSensor": "Sensor de luz",
"nostrConnection": "Conexión de relé Nostr", "nostrConnection": "Conexión de relé Nostr",
"doNotDisturb": "No molestar", "doNotDisturb": "No molestar",
"timeBasedDnd": "Horario basado en el tiempo" "timeBasedDnd": "Horario basado en el tiempo"
}, },
"firmwareUpdater": { "firmwareUpdater": {
"fileUploadSuccess": "Archivo cargado exitosamente, reiniciando el dispositivo. Recargando WebUI en {countdown} segundos", "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.", "fileUploadFailed": "Error al cargar el archivo. \nAsegúrese de haber seleccionado el archivo correcto e inténtelo nuevamente.",
"uploading": "Subiendo", "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.", "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.", "swUpToDate": "Tienes la ultima version.",
"swUpdateAvailable": "¡Una nueva versión está disponible!", "swUpdateAvailable": "¡Una nueva versión está disponible!",
"latestVersion": "Ultima versión", "latestVersion": "Ultima versión",
"releaseDate": "Fecha de lanzamiento", "releaseDate": "Fecha de lanzamiento",
"viewRelease": "Ver lanzamiento", "viewRelease": "Ver lanzamiento",
"autoUpdate": "Instalar actualización (experimental)", "autoUpdate": "Instalar actualización (experimental)",
"autoUpdateInProgress": "Actualización automática en progreso, espere..." "autoUpdateInProgress": "Actualización automática en progreso, espere..."
} }
}, },
"button": { "button": {
"save": "Guardar", "save": "Guardar",
"reset": "Restaurar", "reset": "Restaurar",
"restart": "Reiniciar", "restart": "Reiniciar",
"forceFullRefresh": "Forzar refresco" "forceFullRefresh": "Forzar refresco"
}, },
"colors": { "colors": {
"black": "Negro", "black": "Negro",
"white": "Blanco" "white": "Blanco"
}, },
"restartRequired": "reinicio requerido", "restartRequired": "reinicio requerido",
"time": { "time": {
"minutes": "minutos", "minutes": "minutos",
"seconds": "segundos" "seconds": "segundos"
}, },
"timer": { "timer": {
"running": "funcionando", "running": "funcionando",
"stopped": "detenido" "stopped": "detenido"
}, },
"sections": { "sections": {
"control": { "control": {
"keepSameColor": "Mantén el mismo color" "keepSameColor": "Mantén el mismo color"
} }
}, },
"rssiBar": { "rssiBar": {
"tooltip": "Se consideran buenos valores > -67 dBm. > -30 dBm es increíble" "tooltip": "Se consideran buenos valores > -67 dBm. > -30 dBm es increíble"
}, },
"warning": "Aviso", "warning": "Aviso",
"auto-detect": "Detección automática" "auto-detect": "Detección automática"
} }

View file

@ -139,4 +139,4 @@
}, },
"warning": "Waarschuwing", "warning": "Waarschuwing",
"auto-detect": "Automatische detectie" "auto-detect": "Automatische detectie"
} }

View file

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

View file

@ -2,11 +2,10 @@
<html lang="%paraglide.lang%"> <html lang="%paraglide.lang%">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents" class="h-full">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View file

@ -1,112 +1,111 @@
import { PUBLIC_BASE_URL } from '$env/static/public';
import { baseUrl } from './env'; import { baseUrl } from './env';
/** /**
* Sets custom text to display on the clock * Sets custom text to display on the clock
*/ */
export const setCustomText = (newText: string) => { 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 * Updates the LED colors
*/ */
export const setLEDcolor = (ledStatus: { hex: string }[]) => { export const setLEDcolor = (ledStatus: { hex: string }[]) => {
return fetch(`${baseUrl}/api/lights/set`, { return fetch(`${baseUrl}/api/lights/set`, {
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
method: 'PATCH', method: 'PATCH',
body: JSON.stringify(ledStatus) body: JSON.stringify(ledStatus)
}).catch(() => {}); }).catch(() => {});
}; };
/** /**
* Turns off all LEDs * Turns off all LEDs
*/ */
export const turnOffLeds = () => { export const turnOffLeds = () => {
return fetch(`${baseUrl}/api/lights/off`).catch(() => {}); return fetch(`${baseUrl}/api/lights/off`).catch(() => {});
}; };
/** /**
* Restarts the clock * Restarts the clock
*/ */
export const restartClock = () => { export const restartClock = () => {
return fetch(`${baseUrl}/api/restart`).catch(() => {}); return fetch(`${baseUrl}/api/restart`).catch(() => {});
}; };
/** /**
* Forces a full refresh of the clock * Forces a full refresh of the clock
*/ */
export const forceFullRefresh = () => { export const forceFullRefresh = () => {
return fetch(`${baseUrl}/api/full_refresh`).catch(() => {}); return fetch(`${baseUrl}/api/full_refresh`).catch(() => {});
}; };
/** /**
* Generates a random color hex code * Generates a random color hex code
*/ */
export const generateRandomColor = () => { export const generateRandomColor = () => {
return `#${Math.floor(Math.random() * 16777215) return `#${Math.floor(Math.random() * 16777215)
.toString(16) .toString(16)
.padStart(6, '0')}`; .padStart(6, '0')}`;
}; };
/** /**
* Sets the active screen * Sets the active screen
*/ */
export const setActiveScreen = async (screenId: string) => { export const setActiveScreen = async (screenId: string) => {
return fetch(`${baseUrl}/api/show/screen/${screenId}`); return fetch(`${baseUrl}/api/show/screen/${screenId}`);
} };
/** /**
* Sets the active currency * Sets the active currency
*/ */
export const setActiveCurrency = async (currency: string) => { export const setActiveCurrency = async (currency: string) => {
return fetch(`${baseUrl}/api/show/currency/${currency}`); return fetch(`${baseUrl}/api/show/currency/${currency}`);
} };
/** /**
* Turns on the frontlight * Turns on the frontlight
*/ */
export const turnOnFrontlight = () => { export const turnOnFrontlight = () => {
return fetch(`${baseUrl}/api/frontlight/on`).catch(() => {}); return fetch(`${baseUrl}/api/frontlight/on`).catch(() => {});
}; };
/** /**
* Flashes the frontlight * Flashes the frontlight
*/ */
export const flashFrontlight = () => { export const flashFrontlight = () => {
return fetch(`${baseUrl}/api/frontlight/flash`).catch(() => {}); return fetch(`${baseUrl}/api/frontlight/flash`).catch(() => {});
}; };
/** /**
* Turns off the frontlight * Turns off the frontlight
*/ */
export const turnOffFrontlight = () => { export const turnOffFrontlight = () => {
return fetch(`${baseUrl}/api/frontlight/off`).catch(() => {}); return fetch(`${baseUrl}/api/frontlight/off`).catch(() => {});
}; };
/** /**
* Toggles the timer * Toggles the timer
*/ */
export const toggleTimer = (currentStatus: boolean) => (e: Event) => { export const toggleTimer = (currentStatus: boolean) => (e: Event) => {
e.preventDefault(); e.preventDefault();
if (currentStatus) { if (currentStatus) {
fetch(`${baseUrl}/api/action/pause`); fetch(`${baseUrl}/api/action/pause`);
} else { } else {
fetch(`${baseUrl}/api/action/timer_restart`); fetch(`${baseUrl}/api/action/timer_restart`);
} }
}; };
/** /**
* Toggles the do not disturb mode * Toggles the do not disturb mode
*/ */
export const toggleDoNotDisturb = (currentStatus: boolean) => (e: Event) => { export const toggleDoNotDisturb = (currentStatus: boolean) => (e: Event) => {
e.preventDefault(); e.preventDefault();
console.log(currentStatus); console.log(currentStatus);
if (!currentStatus) { if (!currentStatus) {
fetch(`${baseUrl}/api/dnd/enable`); fetch(`${baseUrl}/api/dnd/enable`);
} else { } else {
fetch(`${baseUrl}/api/dnd/disable`); fetch(`${baseUrl}/api/dnd/disable`);
} }
}; };

View file

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

View file

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

View file

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

View file

@ -1,21 +1,10 @@
<script lang="ts"> <script lang="ts">
let { let { checked = $bindable(false), label = '', id = '', ...restProps } = $props();
checked = $bindable(false),
label = "",
id = "",
...restProps
} = $props();
</script> </script>
<label class="flex items-center justify-between gap-2 cursor-pointer"> <label class="flex cursor-pointer items-center justify-between gap-2">
{#if label} {#if label}
<span class="label-text text-xs">{label}</span> <span class="label-text text-xs">{label}</span>
{/if} {/if}
<input <input type="checkbox" class="toggle toggle-primary toggle-xs" {id} bind:checked {...restProps} />
type="checkbox" </label>
class="toggle toggle-primary toggle-xs"
{id}
bind:checked
{...restProps}
/>
</label>

View file

@ -18,4 +18,4 @@ export { default as CollapsibleSection } from './layout/CollapsibleSection.svelt
export { default as ControlSection } from './sections/ControlSection.svelte'; export { default as ControlSection } from './sections/ControlSection.svelte';
export { default as StatusSection } from './sections/StatusSection.svelte'; export { default as StatusSection } from './sections/StatusSection.svelte';
export { default as SettingsSection } from './sections/SettingsSection.svelte'; export { default as SettingsSection } from './sections/SettingsSection.svelte';
export { default as SystemSection } from './sections/SystemSection.svelte'; export { default as SystemSection } from './sections/SystemSection.svelte';

View file

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

View file

@ -1,87 +1,105 @@
<script lang="ts"> <script lang="ts">
let { let { ...restProps } = $props();
...restProps
} = $props();
import { setLocale, getLocale } from '$lib/paraglide/runtime';
import { locales } from '$lib/paraglide/runtime';
import { page } from '$app/stores';
// Navigation items import { setLocale, getLocale } from '$lib/paraglide/runtime';
const navItems = [ import { locales } from '$lib/paraglide/runtime';
{ href: '/', label: 'Home' }, import { page } from '$app/stores';
{ href: '/settings', label: 'Settings' },
{ href: '/system', label: 'System' },
{ href: '/apidoc', label: 'API' }
];
// Helper function to check if a link is active // Navigation items
function isActive(href: string) { const navItems = [
return $page.url.pathname === href; { href: '/', label: 'Home' },
} { href: '/settings', label: 'Settings' },
{ href: '/system', label: 'System' },
{ href: '/apidoc', label: 'API' }
];
const getLocaleName = (locale: string) => { // Helper function to check if a link is active
return new Intl.DisplayNames([locale], { type: 'language' }).of(locale) function isActive(href: string) {
} return $page.url.pathname === href;
}
const getLanguageName = (locale: string) => { const getLocaleName = (locale: string) => {
return getLocaleName(locale.split('-')[0]) return new Intl.DisplayNames([locale], { type: 'language' }).of(locale);
} };
const getEmojiFlag = (locale: string) => { const getLanguageName = (locale: string) => {
const countryCode = locale.split('-')[1]; return getLocaleName(locale.split('-')[0]);
};
if (!countryCode || countryCode === 'US') { const getEmojiFlag = (locale: string) => {
return '🇺🇸'; const countryCode = locale.split('-')[1];
}
return [...countryCode.toUpperCase()] if (!countryCode || countryCode === 'US') {
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0))) return '🇺🇸';
.join(''); }
}
// Function to get the current flag return [...countryCode.toUpperCase()]
const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧'; .map((char) => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join('');
};
// Function to get the current flag
const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧';
</script> </script>
<div class="navbar bg-base-100 fixed top-0 z-50 shadow-sm w-full" {...restProps}> <div class="navbar bg-base-100 fixed top-0 z-50 w-full shadow-sm" {...restProps}>
<div class="navbar-start"> <div class="navbar-start">
<div class="dropdown"> <div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden"> <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"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /> xmlns="http://www.w3.org/2000/svg"
</svg> class="h-5 w-5"
</div> fill="none"
<ul tabindex="-1" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"> viewBox="0 0 24 24"
{#each navItems as { href, label }} stroke="currentColor"
<li> >
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a> <path
</li> stroke-linecap="round"
{/each} stroke-linejoin="round"
</ul> stroke-width="2"
</div> d="M4 6h16M4 12h8m-8 6h16"
<a href="/" class="btn btn-ghost text-xl">BTClock</a> />
</div> </svg>
<div class="navbar-center hidden lg:flex"> </div>
<ul class="menu menu-horizontal px-1"> <ul
{#each navItems as { href, label }} tabindex="-1"
<li> class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a> >
</li> {#each navItems as { href, label } (href)}
{/each} <li>
</ul> <a {href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
</div> </li>
<div class="navbar-end"> {/each}
<div class="dropdown dropdown-end mr-2"> </ul>
<div tabindex="0" role="button" class="btn btn-ghost"> </div>
<span class="text-sm">{getCurrentFlag()} {getLanguageName(getLocale())}</span> <a href="/" class="btn btn-ghost text-xl">BTClock</a>
</div> </div>
<ul tabindex="-1" class="mt-3 z-[1] p-2 shadow menu dropdown-content bg-base-100 rounded-box w-auto"> <div class="navbar-center hidden lg:flex">
{#each locales as locale} <ul class="menu menu-horizontal px-1">
<li><button onclick={() => setLocale(locale)} class="flex items-center gap-2 text-nowrap">{getEmojiFlag(locale)} {getLanguageName(locale)}</button></li> {#each navItems as { href, label } (href)}
{/each} <li>
</ul> <a {href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
</div> </li>
{/each}
</div> </ul>
</div> </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

@ -3,11 +3,11 @@
import { CardContainer, InputField, Toggle } from '$lib/components'; import { CardContainer, InputField, Toggle } from '$lib/components';
import { settings, status } from '$lib/stores'; import { settings, status } from '$lib/stores';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { import {
setCustomText, setCustomText,
setLEDcolor, setLEDcolor,
turnOffLeds, turnOffLeds,
restartClock, restartClock,
forceFullRefresh, forceFullRefresh,
generateRandomColor, generateRandomColor,
flashFrontlight, flashFrontlight,
@ -17,10 +17,10 @@
import type { LedStatus } from '$lib/types'; import type { LedStatus } from '$lib/types';
let ledStatus = $state<LedStatus[]>([ let ledStatus = $state<LedStatus[]>([
{hex: '#000000'}, { hex: '#000000' },
{hex: '#000000'}, { hex: '#000000' },
{hex: '#000000'}, { hex: '#000000' },
{hex: '#000000'} { hex: '#000000' }
]); ]);
let customText = $state(''); let customText = $state('');
let keepLedsSameColor = $state(false); let keepLedsSameColor = $state(false);
@ -28,7 +28,7 @@
const checkSyncLeds = (e: Event) => { const checkSyncLeds = (e: Event) => {
if (keepLedsSameColor && e.target instanceof HTMLInputElement) { if (keepLedsSameColor && e.target instanceof HTMLInputElement) {
const targetValue = e.target.value; const targetValue = e.target.value;
ledStatus.forEach((element, i) => { ledStatus.forEach((element, i) => {
if (ledStatus[i].hex != targetValue) { if (ledStatus[i].hex != targetValue) {
ledStatus[i].hex = targetValue; ledStatus[i].hex = targetValue;
@ -81,19 +81,21 @@
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="mb-4 flex flex-wrap gap-2"> <div class="mb-4 flex flex-wrap gap-2">
{#if ledStatus.length > 0} {#if ledStatus.length > 0}
{#each ledStatus as led} {#each ledStatus as led (led)}
<input <input
type="color" type="color"
class="btn btn-square" class="btn btn-square"
bind:value={led.hex} bind:value={led.hex}
onchange={checkSyncLeds} onchange={checkSyncLeds}
/> />
{/each} {/each}
{/if} {/if}
<Toggle label={m['sections.control.keepSameColor']()} bind:checked={keepLedsSameColor} /> <Toggle label={m['sections.control.keepSameColor']()} bind:checked={keepLedsSameColor} />
</div> </div>
<div class="flex gap-2"> <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)} <button class="btn btn-primary" onclick={() => setLEDcolor(ledStatus)}
>{m['section.control.setColor']()}</button >{m['section.control.setColor']()}</button
> >
@ -102,19 +104,24 @@
</div> </div>
{#if $settings.hasFrontlight && !$settings.flDisable} {#if $settings.hasFrontlight && !$settings.flDisable}
<div> <div>
<h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3> <h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3>
<div class="flex gap-2 justify-end"> <div class="flex justify-end gap-2">
<button class="btn btn-secondary" onclick={() => turnOnFrontlight()}>{m['section.control.turnOn']()}</button> <button class="btn btn-secondary" onclick={() => turnOnFrontlight()}
<button class="btn btn-primary" onclick={() => turnOffFrontlight()}>{m['section.control.turnOff']()}</button> >{m['section.control.turnOn']()}</button
<button class="btn btn-accent" onclick={() => flashFrontlight()}>{m['section.control.flashFrontlight']()}</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>
</div>
{/if} {/if}
<div> <div>
<h3 class="mb-2 font-medium">{m['section.control.title']()}</h3> <div class="flex justify-end gap-2">
<div class="flex gap-2 justify-end">
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button> <button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button> <button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
</div> </div>

View file

@ -1,341 +1,361 @@
<script lang="ts"> <script lang="ts">
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { CardContainer, Toggle, CollapsibleSection } from '$lib/components'; import { CardContainer, Toggle, CollapsibleSection } from '$lib/components';
import { settings } from '$lib/stores'; import { settings } from '$lib/stores';
let { ...restProps } = $props(); let { ...restProps } = $props();
// Show/hide toggles // Show/hide toggles
let showAll = $state(false); let showAll = $state(false);
let hideAll = $state(false); let hideAll = $state(false);
function toggleShowAll() { function toggleShowAll() {
showAll = true; showAll = true;
hideAll = false; hideAll = false;
} }
function toggleHideAll() { function toggleHideAll() {
hideAll = true; hideAll = true;
showAll = false; showAll = false;
} }
</script> </script>
<CardContainer title={m["section.settings.title"]()} {...restProps}> <CardContainer title={m['section.settings.title']()} {...restProps}>
<div class="flex justify-end gap-2 mb-4"> <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={toggleShowAll}>{m['section.settings.showAll']()}</button>
<button class="btn btn-sm" onclick={toggleHideAll}>{m["section.settings.hideAll"]()}</button> <button class="btn btn-sm" onclick={toggleHideAll}>{m['section.settings.hideAll']()}</button>
</div> </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="form-control">
<div class="grid gap-4 grid-cols-2"> <Toggle
<div class="form-control"> label={m['section.settings.useBigCharsMcap']()}
<Toggle bind:checked={$settings.mcapBigChar}
label={m["section.settings.StealFocusOnNewBlock"]()} />
bind:checked={$settings.stealFocus} <p class="text-xs">
/> Use big characters for the market cap screen instead of using a suffix.
<p class="text-xs">When a new block is mined, it will switch focus from the current screen.</p> </p>
</div> </div>
<div class="form-control">
<Toggle
label={m["section.settings.useBigCharsMcap"]()}
bind:checked={$settings.mcapBigChar}
/>
<p class="text-xs">Use big characters for the market cap screen instead of using a suffix.</p>
</div>
<div class="form-control">
<Toggle
label={m["section.settings.useBlkCountdown"]()}
bind:checked={$settings.useBlkCountdown}
/>
<p class="text-xs">When enabled it count down blocks instead of years/monts/days/hours/minutes.</p>
</div>
<div class="form-control">
<Toggle
label={m["section.settings.useSatsSymbol"]()}
bind:checked={$settings.useSatsSymbol}
/>
<p class="text-xs">Prefix satoshi amounts with the sats symbol.</p>
</div>
<div class="form-control">
<Toggle
label={m["section.settings.suffixPrice"]()}
bind:checked={$settings.suffixPrice}
/>
<p class="text-xs">Always use a suffix for the ticker screen.</p>
</div>
<div class="form-control">
<Toggle
label={m["section.settings.verticalDesc"]()}
bind:checked={$settings.verticalDesc}
/>
<p class="text-xs">Rotate the description of the screen 90 degrees.</p>
</div>
</div>
</CollapsibleSection>
<CollapsibleSection title={m["section.settings.screens"]()} open={showAll || !hideAll}> <div class="form-control">
<div class="grid gap-4 grid-cols-2"> <Toggle
{#each $settings.screens as screen} label={m['section.settings.useBlkCountdown']()}
<div class="form-control"> bind:checked={$settings.useBlkCountdown}
<Toggle />
label={screen.name} <p class="text-xs">
checked={screen.enabled} When enabled it count down blocks instead of years/monts/days/hours/minutes.
/> </p>
</div> </div>
{/each}
</div>
</CollapsibleSection>
<CollapsibleSection title={m["section.settings.currencies"]()} open={showAll || !hideAll}> <div class="form-control">
<div class="alert alert-warning"> <Toggle
<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> label={m['section.settings.useSatsSymbol']()}
<span>restart required</span> bind:checked={$settings.useSatsSymbol}
</div> />
<p class="text-xs">Prefix satoshi amounts with the sats symbol.</p>
</div>
<div class="grid gap-4 grid-cols-2"> <div class="form-control">
<Toggle
{#each $settings.actCurrencies as currency} label={m['section.settings.suffixPrice']()}
<div class="form-control"> bind:checked={$settings.suffixPrice}
<Toggle />
label={currency} <p class="text-xs">Always use a suffix for the ticker screen.</p>
checked={$settings.actCurrencies.includes(currency)} </div>
/>
</div>
{/each}
</div>
</CollapsibleSection>
<CollapsibleSection title={m["section.settings.section.displaysAndLed"]()} open={showAll || !hideAll}> <div class="form-control">
<div class="grid gap-4"> <Toggle
<div class="form-control"> label={m['section.settings.verticalDesc']()}
<label class="label"> bind:checked={$settings.verticalDesc}
<span class="label-text">{m["section.settings.textColor"]()}</span> />
</label> <p class="text-xs">Rotate the description of the screen 90 degrees.</p>
<select class="select select-bordered w-full"> </div>
<option>White on Black</option> </div>
<option>Black on White</option> </CollapsibleSection>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Font</span>
</label>
<select class="select select-bordered w-full">
<option>Oswald</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{m["section.settings.timePerScreen"]()}</span>
</label>
<div class="flex items-center gap-2">
<input type="number" class="input input-bordered w-20" min="1" max="60" value="1" />
<span>{m["time.minutes"]()}</span>
</div>
</div>
<div class="form-control flex justify-between">
<label class="label">
<span class="label-text">{m["section.settings.fullRefreshEvery"]()}</span>
</label>
<div class="w-auto input">
<input type="number" class="" min="1" max="60" value="60" />
<span class="label">{m["time.minutes"]()}</span>
</div>
</div>
<div class="form-control flex justify-between">
<label class="label">
<span class="label-text">{m["section.settings.timeBetweenPriceUpdates"]()}</span>
</label>
<div class="w-auto input">
<input type="number" class="" min="1" max="60" value="30" />
<span class="label">{m["time.seconds"]()}</span>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{m["section.settings.ledBrightness"]()}</span>
</label>
<input type="range" min="0" max="100" class="range" value="50" />
</div>
<div class="form-control">
<Toggle
label={m["section.settings.ledPowerOnTest"]()}
checked={$settings.ledTestOnPower}
/>
</div>
<div class="form-control">
<Toggle
label={m["section.settings.ledFlashOnBlock"]()}
checked={$settings.ledFlashOnUpd}
/>
</div>
<div class="form-control">
<Toggle
label={m["section.settings.disableLeds"]()}
checked={$settings.disableLeds}
/>
</div>
</div>
</CollapsibleSection>
{#if $settings.hasFrontlight} <CollapsibleSection title={m['section.settings.screens']()} open={showAll || !hideAll}>
<CollapsibleSection title="Frontlight Settings" open={showAll || !hideAll}> <div class="grid grid-cols-2 gap-4">
<div class="grid gap-4"> {#each $settings.screens as screen (screen.id)}
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label={screen.name} checked={screen.enabled} />
label="Disable Frontlight" </div>
checked={$settings.flDisable} {/each}
/> </div>
</div> </CollapsibleSection>
<div class="form-control">
<Toggle
label="Always On"
checked={$settings.flAlwaysOn}
/>
</div>
<div class="form-control">
<Toggle
label="Flash on Updates"
checked={$settings.flFlashOnUpd}
/>
</div>
<div class="form-control">
<Toggle
label="Flash on Zaps"
checked={$settings.flFlashOnZap}
/>
</div>
{#if $settings.hasLightLevel}
<div class="form-control">
<Toggle
label="Turn Off in Dark"
checked={$settings.flOffWhenDark}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Light Level Threshold</span>
</label>
<input type="range" min="0" max="255" class="range" value={$settings.luxLightToggle} />
</div>
{/if}
<div class="form-control">
<label class="label">
<span class="label-text">Maximum Brightness</span>
</label>
<input type="range" min="0" max="4095" class="range" value={$settings.flMaxBrightness} />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Effect Delay (ms)</span>
</label>
<input type="number" class="input input-bordered w-20" min="10" max="1000" value={$settings.flEffectDelay} />
</div>
</div>
</CollapsibleSection>
{/if}
<CollapsibleSection title={m["section.settings.section.dataSource"]()} open={showAll || !hideAll}> <CollapsibleSection title={m['section.settings.currencies']()} open={showAll || !hideAll}>
<div class="grid gap-4"> <div class="alert alert-warning">
<div class="form-control"> <svg
<label class="label"> xmlns="http://www.w3.org/2000/svg"
<span class="label-text">{m["section.settings.dataSource.label"]()}</span> class="h-6 w-6 shrink-0 stroke-current"
</label> fill="none"
<select class="select select-bordered w-full"> viewBox="0 0 24 24"
<option value="btclock">{m["section.settings.dataSource.btclock"]()}</option> ><path
<option value="thirdparty">{m["section.settings.dataSource.thirdParty"]()}</option> stroke-linecap="round"
<option value="nostr">{m["section.settings.dataSource.nostr"]()}</option> stroke-linejoin="round"
<option value="custom">{m["section.settings.dataSource.custom"]()}</option> stroke-width="2"
</select> 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"
</div> /></svg
>
<div class="form-control"> <span>restart required</span>
<label class="label"> </div>
<span class="label-text">{m["section.settings.mempoolnstance"]()}</span>
</label>
<input type="text" class="input input-bordered w-full" value="mempool.space/coinlcp.io" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{m["section.settings.ceEndpoint"]()}</span>
</label>
<input type="text" class="input input-bordered w-full" placeholder="Custom Endpoint URL" />
</div>
</div>
</CollapsibleSection>
<CollapsibleSection title={m["section.settings.section.extraFeatures"]()} open={showAll || !hideAll}> <div class="grid grid-cols-2 gap-4">
<div class="grid gap-4"> {#each $settings.actCurrencies as currency (currency)}
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label={currency} checked={$settings.actCurrencies.includes(currency)} />
label={m["section.settings.timeBasedDnd"]()} </div>
checked={$settings.dnd.enabled} {/each}
/> </div>
</div> </CollapsibleSection>
</div>
</CollapsibleSection>
<CollapsibleSection title={m["section.settings.section.system"]()} open={showAll || !hideAll}> <CollapsibleSection
<div class="grid gap-4"> title={m['section.settings.section.displaysAndLed']()}
<div class="form-control"> open={showAll || !hideAll}
<label class="label"> >
<span class="label-text">{m["section.settings.timezoneOffset"]()}</span> <div class="grid gap-4">
</label> <div class="form-control">
<div class="flex items-center gap-2"> <label class="label">
<select class="select select-bordered w-full"> <span class="label-text">{m['section.settings.textColor']()}</span>
<option>Europe/Amsterdam</option> </label>
</select> <select class="select select-bordered w-full">
<button class="btn">{m["auto-detect"]()}</button> <option>White on Black</option>
</div> <option>Black on White</option>
<p class="text-sm mt-1">{m["section.settings.tzOffsetHelpText"]()}</p> </select>
</div> </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="form-control">
<div class="flex justify-between mt-6"> <label class="label">
<button class="btn btn-error">{m["button.reset"]()}</button> <span class="label-text">Font</span>
<button class="btn btn-primary">{m["button.save"]()}</button> </label>
</div> <select class="select select-bordered w-full">
</CardContainer> <option>Oswald</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.timePerScreen']()}</span>
</label>
<div class="flex items-center gap-2">
<input type="number" class="input input-bordered w-20" min="1" max="60" value="1" />
<span>{m['time.minutes']()}</span>
</div>
</div>
<div class="form-control flex justify-between">
<label class="label">
<span class="label-text">{m['section.settings.fullRefreshEvery']()}</span>
</label>
<div class="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 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.ledBrightness']()}</span>
</label>
<input type="range" min="0" max="100" class="range" value="50" />
</div>
<div class="form-control">
<Toggle
label={m['section.settings.ledPowerOnTest']()}
checked={$settings.ledTestOnPower}
/>
</div>
<div class="form-control">
<Toggle
label={m['section.settings.ledFlashOnBlock']()}
checked={$settings.ledFlashOnUpd}
/>
</div>
<div class="form-control">
<Toggle label={m['section.settings.disableLeds']()} checked={$settings.disableLeds} />
</div>
</div>
</CollapsibleSection>
{#if $settings.hasFrontlight}
<CollapsibleSection title="Frontlight Settings" open={showAll || !hideAll}>
<div class="grid gap-4">
<div class="form-control">
<Toggle label="Disable Frontlight" checked={$settings.flDisable} />
</div>
<div class="form-control">
<Toggle label="Always On" checked={$settings.flAlwaysOn} />
</div>
<div class="form-control">
<Toggle label="Flash on Updates" checked={$settings.flFlashOnUpd} />
</div>
<div class="form-control">
<Toggle label="Flash on Zaps" checked={$settings.flFlashOnZap} />
</div>
{#if $settings.hasLightLevel}
<div class="form-control">
<Toggle label="Turn Off in Dark" checked={$settings.flOffWhenDark} />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Light Level Threshold</span>
</label>
<input
type="range"
min="0"
max="255"
class="range"
value={$settings.luxLightToggle}
/>
</div>
{/if}
<div class="form-control">
<label class="label">
<span class="label-text">Maximum Brightness</span>
</label>
<input
type="range"
min="0"
max="4095"
class="range"
value={$settings.flMaxBrightness}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Effect Delay (ms)</span>
</label>
<input
type="number"
class="input input-bordered w-20"
min="10"
max="1000"
value={$settings.flEffectDelay}
/>
</div>
</div>
</CollapsibleSection>
{/if}
<CollapsibleSection
title={m['section.settings.section.dataSource']()}
open={showAll || !hideAll}
>
<div class="grid gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.dataSource.label']()}</span>
</label>
<select class="select select-bordered w-full">
<option value="btclock">{m['section.settings.dataSource.btclock']()}</option>
<option value="thirdparty">{m['section.settings.dataSource.thirdParty']()}</option>
<option value="nostr">{m['section.settings.dataSource.nostr']()}</option>
<option value="custom">{m['section.settings.dataSource.custom']()}</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.mempoolnstance']()}</span>
</label>
<input type="text" class="input input-bordered w-full" value="mempool.space/coinlcp.io" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.ceEndpoint']()}</span>
</label>
<input
type="text"
class="input input-bordered w-full"
placeholder="Custom Endpoint URL"
/>
</div>
</div>
</CollapsibleSection>
<CollapsibleSection
title={m['section.settings.section.extraFeatures']()}
open={showAll || !hideAll}
>
<div class="grid gap-4">
<div class="form-control">
<Toggle label={m['section.settings.timeBasedDnd']()} checked={$settings.dnd.enabled} />
</div>
</div>
</CollapsibleSection>
<CollapsibleSection title={m['section.settings.section.system']()} open={showAll || !hideAll}>
<div class="grid gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.timezoneOffset']()}</span>
</label>
<div class="flex items-center gap-2">
<select class="select select-bordered w-full">
<option>Europe/Amsterdam</option>
</select>
<button class="btn">{m['auto-detect']()}</button>
</div>
<p class="mt-1 text-sm">{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="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,53 +2,54 @@
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { CardContainer, TabButton, CurrencyButton, Stat, Status } from '$lib/components'; import { CardContainer, TabButton, CurrencyButton, Stat, Status } from '$lib/components';
import { status, settings } from '$lib/stores'; import { status, settings } from '$lib/stores';
import { setActiveScreen, setActiveCurrency } from '$lib/clockControl'; import { setActiveScreen, setActiveCurrency } from '$lib/clockControl';
import BTClock from '../BTClock.svelte'; import BTClock from '../BTClock.svelte';
import { DataSourceType } from '$lib/types'; import { DataSourceType } from '$lib/types';
import { toUptimestring } from '$lib/utils'; import { toUptimestring } from '$lib/utils';
const screens = $settings.screens.map(screen => ({ const screens = $settings.screens.map((screen) => ({
id: screen.id, id: screen.id,
label: screen.name label: screen.name
})); }));
</script> </script>
<CardContainer title={m['section.status.title']()}> <CardContainer title={m['section.status.title']()}>
<div class="space-y-4 mx-auto"> <div class="mx-auto space-y-4">
<div class="join"> <div class="join">
{#each screens as screen} {#each screens as screen (screen.id)}
<TabButton active={$status.currentScreen === screen.id} onClick={() => setActiveScreen(screen.id)}> <TabButton
active={$status.currentScreen === screen.id}
onClick={() => setActiveScreen(screen.id)}
>
{screen.label} {screen.label}
</TabButton> </TabButton>
{/each} {/each}
</div> </div>
<div class="join flex justify-center"> <div class="join flex justify-center">
{#each $settings.actCurrencies as currency} {#each $settings.actCurrencies as currency (currency)}
<CurrencyButton <CurrencyButton
currency={currency} {currency}
active={$status.currency === currency} active={$status.currency === currency}
onClick={() => setActiveCurrency(currency)} onClick={() => setActiveCurrency(currency)}
/> />
{/each} {/each}
</div> </div>
<div class="mt-8 flex justify-center"> <div class="mt-8 flex justify-center">
<div class="w-3/4"> <div class="w-3/4">
<!-- Bitcoin value display showing blocks/price -->
<BTClock displays={$status.data} verticalDesc={$settings.verticalDesc} /> <BTClock displays={$status.data} verticalDesc={$settings.verticalDesc} />
{$settings.verticalDesc}
</div> </div>
</div> </div>
<div class="text-center text-sm text-gray-500"> <div class="text-center text-sm text-gray-500">
{m['section.status.screenCycle']()}: is {$status.timerRunning ? 'running' : 'stopped'}<br /> {m['section.status.screenCycle']()}: is {$status.timerRunning ? 'running' : 'stopped'}<br />
{m['section.status.doNotDisturb']()}: {$status.dnd.enabled ? m['on']() : m['off']()} {m['section.status.doNotDisturb']()}: {$status.dnd.enabled ? m['on']() : m['off']()}
<small> <small>
{#if $status.dnd?.timeBasedEnabled} {#if $status.dnd?.timeBasedEnabled}
{m['section.status.timeBasedDnd']()} ( {$settings.dnd {m['section.status.timeBasedDnd']()} ( {$settings.dnd
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings .startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings.dnd
.dnd.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} ) .endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
{/if} {/if}
</small> </small>
</div> </div>
@ -58,18 +59,28 @@
<Status text="Nostr Relay connection" status={$status.nostr ? 'online' : 'offline'} /> <Status text="Nostr Relay connection" status={$status.nostr ? 'online' : 'offline'} />
{/if} {/if}
{#if $settings.dataSource === DataSourceType.THIRD_PARTY_SOURCE} {#if $settings.dataSource === DataSourceType.THIRD_PARTY_SOURCE}
<Status text={m['section.status.wsPriceConnection']()} status={$status.connectionStatus.price ? 'online' : 'offline'} /> <Status
<Status text={m['section.status.wsMempoolConnection']({ instance: $settings.mempoolInstance })} status={$status.connectionStatus.blocks ? 'online' : 'offline'} /> 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} {: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} {/if}
</div> </div>
</div> </div>
<div class="flex justify-center stats shadow mt-4"> <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
<Stat title={m['section.status.wifiSignalStrength']()} value={`${$status.rssi} dBm`} /> title={m['section.status.memoryFree']()}
<Stat title={m['section.status.uptime']()} value={`${toUptimestring($status.espUptime)}`} /> value={`${Math.round($status.espFreeHeap / 1024)} / ${Math.round($status.espHeapSize / 1024)} KiB`}
</div> />
<Stat title={m['section.status.wifiSignalStrength']()} value={`${$status.rssi} dBm`} />
<Stat title={m['section.status.uptime']()} value={`${toUptimestring($status.espUptime)}`} />
</div>
</CardContainer> </CardContainer>

View file

@ -1,102 +1,99 @@
<script lang="ts"> <script lang="ts">
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { CardContainer } from '$lib/components'; import { CardContainer } from '$lib/components';
import { settings } from '$lib/stores'; import { settings } from '$lib/stores';
import { import { restartClock, forceFullRefresh } from '$lib/clockControl';
restartClock,
forceFullRefresh
} from '$lib/clockControl';
</script> </script>
<CardContainer title="System Information"> <CardContainer title="System Information">
<div> <div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table-sm table"> <table class="table-sm table">
<thead> <thead>
<tr> <tr>
<th colspan="2">System info</th> <th colspan="2">System info</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{m['section.control.version']()}</td> <td>{m['section.control.version']()}</td>
<td>{$settings.gitTag}</td> <td>{$settings.gitTag}</td>
</tr> </tr>
<tr> <tr>
<td>{m['section.control.buildTime']()}</td> <td>{m['section.control.buildTime']()}</td>
<td>{$settings.lastBuildTime}</td> <td>{$settings.lastBuildTime}</td>
</tr> </tr>
<tr> <tr>
<td>IP</td> <td>IP</td>
<td>{$settings.ip}</td> <td>{$settings.ip}</td>
</tr> </tr>
<tr> <tr>
<td>HW revision</td> <td>HW revision</td>
<td>{$settings.hwRev}</td> <td>{$settings.hwRev}</td>
</tr> </tr>
<tr> <tr>
<td>{m['section.control.fwCommit']()}</td> <td>{m['section.control.fwCommit']()}</td>
<td class="text-xs">{$settings.gitRev}</td> <td class="text-xs">{$settings.gitRev}</td>
</tr> </tr>
<tr> <tr>
<td>{m['section.control.hostname']()}</td> <td>{m['section.control.hostname']()}</td>
<td>{$settings.hostname}</td> <td>{$settings.hostname}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="mt-4 flex gap-2"> <div class="mt-4 flex gap-2">
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button> <button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button> <button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
</div> </div>
</div> </div>
</CardContainer> </CardContainer>
<CardContainer title={m['section.control.firmwareUpdate']()} className="mt-4"> <CardContainer title={m['section.control.firmwareUpdate']()} className="mt-4">
<div> <div>
<p class="mb-2 text-sm"> <p class="mb-2 text-sm">
Latest Version: 3.3.5 - Release Date: 5/2/2025, 12:37:14 AM - <a Latest Version: 3.3.5 - Release Date: 5/2/2025, 12:37:14 AM - <a
href="#" href="#"
class="link link-primary">{m['section.firmwareUpdater.viewRelease']()}</a class="link link-primary">{m['section.firmwareUpdater.viewRelease']()}</a
> >
</p> </p>
<p class="text-success mb-4 text-sm">{m['section.firmwareUpdater.swUpToDate']()}</p> <p class="text-success mb-4 text-sm">{m['section.firmwareUpdater.swUpToDate']()}</p>
<div class="form-control mb-4"> <div class="form-control mb-4">
<label class="label" for="firmwareFile"> <label class="label" for="firmwareFile">
<span class="label-text">Firmware File (blib_s3_mini_213epd_firmware.bin)</span> <span class="label-text">Firmware File (blib_s3_mini_213epd_firmware.bin)</span>
</label> </label>
<div class="flex gap-2"> <div class="flex gap-2">
<input type="file" class="file-input file-input-bordered w-full" id="firmwareFile" /> <input type="file" class="file-input file-input-bordered w-full" id="firmwareFile" />
<button class="btn btn-primary">{m['section.control.firmwareUpdate']()}</button> <button class="btn btn-primary">{m['section.control.firmwareUpdate']()}</button>
</div> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label" for="webuiFile"> <label class="label" for="webuiFile">
<span class="label-text">WebUI File (littlefs_4MB.bin)</span> <span class="label-text">WebUI File (littlefs_4MB.bin)</span>
</label> </label>
<div class="flex gap-2"> <div class="flex gap-2">
<input type="file" class="file-input file-input-bordered w-full" id="webuiFile" /> <input type="file" class="file-input file-input-bordered w-full" id="webuiFile" />
<button class="btn btn-primary">Update WebUI</button> <button class="btn btn-primary">Update WebUI</button>
</div> </div>
</div> </div>
<div class="alert alert-warning mt-4"> <div class="alert alert-warning mt-4">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current" class="h-6 w-6 shrink-0 stroke-current"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
><path ><path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" 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" 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 /></svg
> >
<span>{m['section.firmwareUpdater.firmwareUpdateText']()}</span> <span>{m['section.firmwareUpdater.firmwareUpdateText']()}</span>
</div> </div>
</div> </div>
</CardContainer> </CardContainer>

View file

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

View file

@ -1,23 +1,16 @@
<script lang="ts"> <script lang="ts">
let { let { title, value, desc = '', icon = '', className = '', ...restProps } = $props();
title,
value,
desc = "",
icon = "",
className = "",
...restProps
} = $props();
</script> </script>
<div class="stat {className}" {...restProps}> <div class="stat {className}" {...restProps}>
{#if icon} {#if icon}
<div class="stat-figure text-primary"> <div class="stat-figure text-primary">
{@html icon} {@html icon}
</div> </div>
{/if} {/if}
<div class="stat-title">{title}</div> <div class="stat-title">{title}</div>
<div class="stat-value">{value}</div> <div class="stat-value">{value}</div>
{#if desc} {#if desc}
<div class="stat-desc">{desc}</div> <div class="stat-desc">{desc}</div>
{/if} {/if}
</div> </div>

View file

@ -1,33 +1,35 @@
<script lang="ts"> <script lang="ts">
let { let {
status = "offline" as "online" | "offline" | "error" | "warning", status = 'offline' as 'online' | 'offline' | 'error' | 'warning',
text, text,
className = "", className = '',
...restProps ...restProps
} = $props(); } = $props();
const getStatusColor = () => { const getStatusColor = () => {
switch (status) { switch (status) {
case "online": case 'online':
return "bg-success"; return 'bg-success';
case "offline": case 'offline':
return "bg-base-300"; return 'bg-base-300';
case "error": case 'error':
return "bg-error"; return 'bg-error';
case "warning": case 'warning':
return "bg-warning"; return 'bg-warning';
default: default:
return "bg-base-300"; return 'bg-base-300';
} }
}; };
</script> </script>
<div class="flex items-center gap-2 {className}" {...restProps}> <div class="flex items-center gap-2 {className}" {...restProps}>
<div class="relative flex"> <div class="relative flex">
<div class="{getStatusColor()} h-3 w-3 rounded-full"></div> <div class="{getStatusColor()} h-3 w-3 rounded-full"></div>
{#if status === "online"} {#if status === 'online'}
<div class="{getStatusColor()} animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75"></div> <div
{/if} class="{getStatusColor()} absolute inline-flex h-3 w-3 animate-ping rounded-full opacity-75"
</div> ></div>
<span class="text-sm">{text}</span> {/if}
</div> </div>
<span class="text-sm">{text}</span>
</div>

View file

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

View file

@ -1,71 +1,86 @@
<script lang="ts"> <script lang="ts">
type Position = type Position =
| 'top-start' | 'top-center' | 'top-end' | 'top-start'
| 'middle-start' | 'middle-center' | 'middle-end' | 'top-center'
| 'bottom-start' | 'bottom-center' | 'bottom-end'; | 'top-end'
| 'middle-start'
type AlertType = 'info' | 'success' | 'warning' | 'error'; | 'middle-center'
| 'middle-end'
| 'bottom-start'
| 'bottom-center'
| 'bottom-end';
let props = $props(); type AlertType = 'info' | 'success' | 'warning' | 'error';
let message = props.message || '';
let type = (props.type as AlertType) || 'info';
let position = (props.position as Position) || 'bottom-end';
let duration = props.duration || 3000;
let showClose = props.showClose || false;
// Create a new object without the known props
let restProps = { ...props };
delete restProps.message;
delete restProps.type;
delete restProps.position;
delete restProps.duration;
delete restProps.showClose;
let visible = $state(true); let props = $props();
// Map position prop to DaisyUI classes let message = props.message || '';
const getPositionClasses = (pos: Position): string => { let type = (props.type as AlertType) || 'info';
const [vertical, horizontal] = pos.split('-'); let position = (props.position as Position) || 'bottom-end';
let duration = props.duration || 3000;
let classes = 'toast'; let showClose = props.showClose || false;
// Vertical position
if (vertical === 'top') classes += ' toast-top';
if (vertical === 'middle') classes += ' toast-middle';
// bottom is default, no class needed
// Horizontal position
if (horizontal === 'start') classes += ' toast-start';
if (horizontal === 'center') classes += ' toast-center';
if (horizontal === 'end') classes += ' toast-end';
return classes;
};
// Auto-hide the toast after duration // Create a new object without the known props
if (duration > 0) { let restProps = { ...props };
setTimeout(() => { delete restProps.message;
visible = false; delete restProps.type;
}, duration); delete restProps.position;
} delete restProps.duration;
delete restProps.showClose;
const closeToast = () => { let visible = $state(true);
visible = false;
}; // Map position prop to DaisyUI classes
const getPositionClasses = (pos: Position): string => {
const [vertical, horizontal] = pos.split('-');
let classes = 'toast';
// Vertical position
if (vertical === 'top') classes += ' toast-top';
if (vertical === 'middle') classes += ' toast-middle';
// bottom is default, no class needed
// Horizontal position
if (horizontal === 'start') classes += ' toast-start';
if (horizontal === 'center') classes += ' toast-center';
if (horizontal === 'end') classes += ' toast-end';
return classes;
};
// Auto-hide the toast after duration
if (duration > 0) {
setTimeout(() => {
visible = false;
}, duration);
}
const closeToast = () => {
visible = false;
};
</script> </script>
{#if visible} {#if visible}
<div class={getPositionClasses(position)} {...restProps}> <div class={getPositionClasses(position)} {...restProps}>
<div class="alert alert-{type}"> <div class="alert alert-{type}">
<span>{message}</span> <span>{message}</span>
{#if showClose} {#if showClose}
<button class="btn btn-circle btn-xs" onclick={closeToast}> <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"> <svg
<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" /> xmlns="http://www.w3.org/2000/svg"
</svg> class="h-4 w-4"
</button> viewBox="0 0 20 20"
{/if} fill="currentColor"
</div> >
</div> <path
{/if} 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

@ -1,3 +1,3 @@
import { PUBLIC_BASE_URL } from '$env/static/public'; import { PUBLIC_BASE_URL } from '$env/static/public';
export const baseUrl = PUBLIC_BASE_URL; export const baseUrl = PUBLIC_BASE_URL;

View file

@ -1,2 +1,2 @@
export { settings, type Settings } from './settings'; export { settings, type Settings } from './settings';
export { status, type Status } from './status'; export { status, type Status } from './status';

View file

@ -4,158 +4,158 @@ import type { Settings } from '$lib/types';
// Create a default settings object // Create a default settings object
const defaultSettings: Settings = { const defaultSettings: Settings = {
numScreens: 7, numScreens: 7,
invertedColor: false, invertedColor: false,
timerSeconds: 60, timerSeconds: 60,
timerRunning: false, timerRunning: false,
minSecPriceUpd: 30, minSecPriceUpd: 30,
fullRefreshMin: 60, fullRefreshMin: 60,
wpTimeout: 600, wpTimeout: 600,
tzString: "UTC", tzString: 'UTC',
dataSource: 0, dataSource: 0,
mempoolInstance: "mempool.space", mempoolInstance: 'mempool.space',
mempoolSecure: true, mempoolSecure: true,
localPoolEndpoint: "localhost:2019", localPoolEndpoint: 'localhost:2019',
nostrPubKey: "", nostrPubKey: '',
nostrRelay: "wss://relay.damus.io", nostrRelay: 'wss://relay.damus.io',
nostrZapNotify: false, nostrZapNotify: false,
nostrZapPubkey: "", nostrZapPubkey: '',
ledFlashOnZap: true, ledFlashOnZap: true,
fontName: "oswald", fontName: 'oswald',
availableFonts: ["antonio", "oswald"], availableFonts: ['antonio', 'oswald'],
customEndpoint: "ws-staging.btclock.dev", customEndpoint: 'ws-staging.btclock.dev',
customEndpointDisableSSL: false, customEndpointDisableSSL: false,
ledTestOnPower: true, ledTestOnPower: true,
ledFlashOnUpd: true, ledFlashOnUpd: true,
ledBrightness: 255, ledBrightness: 255,
stealFocus: false, stealFocus: false,
mcapBigChar: true, mcapBigChar: true,
mdnsEnabled: true, mdnsEnabled: true,
otaEnabled: true, otaEnabled: true,
useSatsSymbol: true, useSatsSymbol: true,
useBlkCountdown: true, useBlkCountdown: true,
suffixPrice: false, suffixPrice: false,
disableLeds: false, disableLeds: false,
mowMode: false, mowMode: false,
verticalDesc: true, verticalDesc: true,
suffixShareDot: false, suffixShareDot: false,
enableDebugLog: false, enableDebugLog: false,
hostnamePrefix: "btclock", hostnamePrefix: 'btclock',
hostname: "btclock", hostname: 'btclock',
ip: "", ip: '',
txPower: 80, txPower: 80,
gitReleaseUrl: "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest", gitReleaseUrl: 'https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest',
bitaxeEnabled: false, bitaxeEnabled: false,
bitaxeHostname: "bitaxe1", bitaxeHostname: 'bitaxe1',
miningPoolStats: false, miningPoolStats: false,
miningPoolName: "noderunners", miningPoolName: 'noderunners',
miningPoolUser: "", miningPoolUser: '',
availablePools: [ availablePools: [
"ocean", 'ocean',
"noderunners", 'noderunners',
"satoshi_radio", 'satoshi_radio',
"braiins", 'braiins',
"public_pool", 'public_pool',
"local_public_pool", 'local_public_pool',
"gobrrr_pool", 'gobrrr_pool',
"ckpool", 'ckpool',
"eu_ckpool" 'eu_ckpool'
], ],
httpAuthEnabled: false, httpAuthEnabled: false,
httpAuthUser: "btclock", httpAuthUser: 'btclock',
httpAuthPass: "satoshi", httpAuthPass: 'satoshi',
hasFrontlight: false, hasFrontlight: false,
// Default frontlight settings // Default frontlight settings
flDisable: false, flDisable: false,
flMaxBrightness: 2684, flMaxBrightness: 2684,
flAlwaysOn: false, flAlwaysOn: false,
flEffectDelay: 50, flEffectDelay: 50,
flFlashOnUpd: true, flFlashOnUpd: true,
flFlashOnZap: true, flFlashOnZap: true,
// Default light sensor settings // Default light sensor settings
hasLightLevel: false, hasLightLevel: false,
luxLightToggle: 128, luxLightToggle: 128,
flOffWhenDark: false, flOffWhenDark: false,
hwRev: "", hwRev: '',
fsRev: "", fsRev: '',
gitRev: "", gitRev: '',
gitTag: "", gitTag: '',
lastBuildTime: "", lastBuildTime: '',
screens: [ screens: [
{id: 0, name: "Block Height", enabled: true}, { id: 0, name: 'Block Height', enabled: true },
{id: 3, name: "Time", enabled: false}, { id: 3, name: 'Time', enabled: false },
{id: 4, name: "Halving countdown", enabled: false}, { id: 4, name: 'Halving countdown', enabled: false },
{id: 6, name: "Block Fee Rate", enabled: false}, { id: 6, name: 'Block Fee Rate', enabled: false },
{id: 10, name: "Sats per dollar", enabled: true}, { id: 10, name: 'Sats per dollar', enabled: true },
{id: 20, name: "Ticker", enabled: true}, { id: 20, name: 'Ticker', enabled: true },
{id: 30, name: "Market Cap", enabled: false} { id: 30, name: 'Market Cap', enabled: false }
], ],
actCurrencies: ["USD"], actCurrencies: ['USD'],
availableCurrencies: ["USD", "EUR", "GBP", "JPY", "AUD", "CAD"], availableCurrencies: ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD'],
poolLogosUrl: "https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main", poolLogosUrl: 'https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main',
ceEndpoint: "ws-staging.btclock.dev", ceEndpoint: 'ws-staging.btclock.dev',
ceDisableSSL: false, ceDisableSSL: false,
dnd: { dnd: {
enabled: false, enabled: false,
timeBasedEnabled: false, timeBasedEnabled: false,
startHour: 23, startHour: 23,
startMinute: 0, startMinute: 0,
endHour: 7, endHour: 7,
endMinute: 0 endMinute: 0
} }
}; };
// Create the Svelte store // Create the Svelte store
function createSettingsStore() { function createSettingsStore() {
const { subscribe, set, update } = writable<Settings>(defaultSettings); const { subscribe, set, update } = writable<Settings>(defaultSettings);
return { return {
subscribe, subscribe,
fetch: async () => { fetch: async () => {
try { try {
const response = await fetch(`${baseUrl}/api/settings`); const response = await fetch(`${baseUrl}/api/settings`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Error fetching settings: ${response.statusText}`); throw new Error(`Error fetching settings: ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
set(data); set(data);
return data; return data;
} catch (error) { } catch (error) {
console.error('Failed to fetch settings:', error); console.error('Failed to fetch settings:', error);
return defaultSettings; return defaultSettings;
} }
}, },
update: async (newSettings: Partial<Settings>) => { update: async (newSettings: Partial<Settings>) => {
try { try {
const response = await fetch(`${baseUrl}/api/settings`, { const response = await fetch(`${baseUrl}/api/settings`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify(newSettings), body: JSON.stringify(newSettings)
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Error updating settings: ${response.statusText}`); throw new Error(`Error updating settings: ${response.statusText}`);
} }
// Update the local store with the new settings // Update the local store with the new settings
update(currentSettings => ({ ...currentSettings, ...newSettings })); update((currentSettings) => ({ ...currentSettings, ...newSettings }));
return true; return true;
} catch (error) { } catch (error) {
console.error('Failed to update settings:', error); console.error('Failed to update settings:', error);
return false; return false;
} }
}, },
set: (newSettings: Settings) => set(newSettings), set: (newSettings: Settings) => set(newSettings),
reset: () => set(defaultSettings) reset: () => set(defaultSettings)
}; };
} }
export const settings = createSettingsStore(); export const settings = createSettingsStore();
// Initialize the store by fetching settings when this module is first imported // Initialize the store by fetching settings when this module is first imported
if (typeof window !== 'undefined') { 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'; import type { Status } from '$lib/types';
// Create a default status object // Create a default status object
const defaultStatus: Status = { const defaultStatus: Status = {
currentScreen: 0, currentScreen: 0,
numScreens: 0, numScreens: 0,
timerRunning: false, timerRunning: false,
isOTAUpdating: false, isOTAUpdating: false,
espUptime: 0, espUptime: 0,
espFreeHeap: 0, espFreeHeap: 0,
espHeapSize: 0, espHeapSize: 0,
connectionStatus: { connectionStatus: {
price: false, price: false,
blocks: false, blocks: false,
V2: false, V2: false,
nostr: false nostr: false
}, },
rssi: 0, rssi: 0,
currency: "USD", currency: 'USD',
dnd: { dnd: {
enabled: false, enabled: false,
timeBasedEnabled: false, timeBasedEnabled: false,
startTime: "00:00", startTime: '00:00',
endTime: "00:00", endTime: '00:00',
active: false active: false
}, },
data: [], data: [],
leds: [ 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' },
{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 // Create the Svelte store
function createStatusStore() { function createStatusStore() {
const { subscribe, set, update } = writable<Status>(defaultStatus); const { subscribe, set, update } = writable<Status>(defaultStatus);
let eventSource: EventSource | null = null; let eventSource: EventSource | null = null;
// Clean up function to close SSE connection // Clean up function to close SSE connection
const cleanup = () => { const cleanup = () => {
if (eventSource) { if (eventSource) {
eventSource.close(); eventSource.close();
eventSource = null; eventSource = null;
} }
}; };
// Create the store object with methods // Create the store object with methods
const store = { const store = {
subscribe, subscribe,
fetch: async () => { fetch: async () => {
try { try {
const response = await fetch(`${baseUrl}/api/status`); const response = await fetch(`${baseUrl}/api/status`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Error fetching status: ${response.statusText}`); throw new Error(`Error fetching status: ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
set(data); set(data);
return data; return data;
} catch (error) { } catch (error) {
console.error('Failed to fetch status:', error); console.error('Failed to fetch status:', error);
return defaultStatus; return defaultStatus;
} }
}, },
startListening: () => { startListening: () => {
// Clean up any existing connections first // Clean up any existing connections first
cleanup(); cleanup();
// Only run in the browser, not during SSR
if (typeof window === 'undefined') return;
try { // Only run in the browser, not during SSR
// Create a new EventSource connection if (typeof window === 'undefined') return;
eventSource = new EventSource(`${baseUrl}/events`);
// Handle status updates try {
eventSource.addEventListener('status', (event) => { // Create a new EventSource connection
try { eventSource = new EventSource(`${baseUrl}/events`);
const data = JSON.parse(event.data);
update(currentStatus => ({ ...currentStatus, ...data }));
} catch (error) {
console.error('Error processing status event:', error);
}
});
// Handle connection status updates // Handle status updates
eventSource.addEventListener('connection', (event) => { eventSource.addEventListener('status', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
update(currentStatus => ({ update((currentStatus) => ({ ...currentStatus, ...data }));
...currentStatus, } catch (error) {
connectionStatus: { console.error('Error processing status event:', error);
...currentStatus.connectionStatus, }
...data });
}
}));
} catch (error) {
console.error('Error processing connection event:', error);
}
});
// Handle screen updates // Handle connection status updates
eventSource.addEventListener('screen', (event) => { eventSource.addEventListener('connection', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
update(currentStatus => ({ update((currentStatus) => ({
...currentStatus, ...currentStatus,
currentScreen: data.screen || currentStatus.currentScreen connectionStatus: {
})); ...currentStatus.connectionStatus,
} catch (error) { ...data
console.error('Error processing screen event:', error); }
} }));
}); } catch (error) {
console.error('Error processing connection event:', error);
}
});
// Handle generic messages // Handle screen updates
eventSource.onmessage = (event) => { eventSource.addEventListener('screen', (event) => {
if (event.type === 'message') { try {
return; const data = JSON.parse(event.data);
} update((currentStatus) => ({
try { ...currentStatus,
const data = JSON.parse(event.data); currentScreen: data.screen || currentStatus.currentScreen
update(currentStatus => ({ ...currentStatus, ...data })); }));
} catch (error) { } catch (error) {
console.error('Error processing message event:', error); console.error('Error processing screen event:', error);
} }
}; });
// Handle errors // Handle generic messages
eventSource.onerror = (error) => { eventSource.onmessage = (event) => {
console.error('EventSource failed:', error); if (event.type === 'message') {
cleanup(); return;
// Attempt to reconnect after a delay }
setTimeout(() => store.startListening(), 5000); try {
}; const data = JSON.parse(event.data);
} catch (error) { update((currentStatus) => ({ ...currentStatus, ...data }));
console.error('Failed to setup event source:', error); } catch (error) {
} console.error('Error processing message event:', error);
}, }
stopListening: cleanup, };
// Function to set the current screen // Handle errors
setScreen: async (id: number): Promise<boolean> => { eventSource.onerror = (error) => {
try { console.error('EventSource failed:', error);
// Make the GET request to change the screen cleanup();
const response = await fetch(`${baseUrl}/api/show/screen/${id}`); // Attempt to reconnect after a delay
setTimeout(() => store.startListening(), 5000);
if (!response.ok) { };
throw new Error(`Error setting screen: ${response.statusText}`); } catch (error) {
} console.error('Failed to setup event source:', error);
}
// Update the store with the new screen ID },
update(currentStatus => ({ stopListening: cleanup,
...currentStatus,
currentScreen: id // Function to set the current screen
})); setScreen: async (id: number): Promise<boolean> => {
try {
return true; // Make the GET request to change the screen
} catch (error) { const response = await fetch(`${baseUrl}/api/show/screen/${id}`);
console.error('Failed to set screen:', error);
return false; if (!response.ok) {
} throw new Error(`Error setting screen: ${response.statusText}`);
} }
};
// Update the store with the new screen ID
return store; update((currentStatus) => ({
...currentStatus,
currentScreen: id
}));
return true;
} catch (error) {
console.error('Failed to set screen:', error);
return false;
}
}
};
return store;
} }
export const status = createStatusStore(); export const status = createStatusStore();
// Initialize the store by fetching initial status and starting to listen for updates // Initialize the store by fetching initial status and starting to listen for updates
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
status.fetch().then(() => status.startListening()); status.fetch().then(() => status.startListening());
// Clean up the EventSource when the window is unloaded // Clean up the EventSource when the window is unloaded
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
status.stopListening(); status.stopListening();
}); });
} }

View file

@ -3,11 +3,11 @@
*/ */
export interface LedStatus { export interface LedStatus {
hex: string; hex: string;
} }
/** /**
* Data source types * Data source types
*/ */
export enum DataSourceType { export enum DataSourceType {
BTCLOCK_SOURCE = 0, BTCLOCK_SOURCE = 0,
@ -26,31 +26,31 @@ export interface Status {
espFreeHeap: number; espFreeHeap: number;
espHeapSize: number; espHeapSize: number;
connectionStatus: { connectionStatus: {
price: boolean; price: boolean;
blocks: boolean; blocks: boolean;
V2: boolean; V2: boolean;
nostr: boolean; nostr: boolean;
}; };
rssi: number; rssi: number;
currency: string; currency: string;
dnd: { dnd: {
enabled: boolean; enabled: boolean;
timeBasedEnabled: boolean; timeBasedEnabled: boolean;
startTime: string; startTime: string;
endTime: string; endTime: string;
active: boolean; active: boolean;
}; };
data: string[]; data: string[];
leds: Array<{ leds: Array<{
red: number; red: number;
green: number; green: number;
blue: number; blue: number;
hex: string; hex: string;
}>; }>;
[key: string]: unknown; [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 { export interface Settings {
numScreens: number; numScreens: number;
invertedColor: boolean; invertedColor: boolean;
@ -120,9 +120,9 @@ export interface Settings {
gitTag: string; gitTag: string;
lastBuildTime: string; lastBuildTime: string;
screens: Array<{ screens: Array<{
id: number; id: number;
name: string; name: string;
enabled: boolean; enabled: boolean;
}>; }>;
actCurrencies: string[]; actCurrencies: string[];
availableCurrencies: string[]; availableCurrencies: string[];
@ -130,13 +130,12 @@ export interface Settings {
ceEndpoint: string; ceEndpoint: string;
ceDisableSSL: boolean; ceDisableSSL: boolean;
dnd: { dnd: {
enabled: boolean; enabled: boolean;
timeBasedEnabled: boolean; timeBasedEnabled: boolean;
startHour: number; startHour: number;
startMinute: number; startMinute: number;
endHour: number; endHour: number;
endMinute: number; endMinute: number;
}; };
[key: string]: unknown; [key: string]: unknown;
} }

View file

@ -1,22 +1,22 @@
export const toTime = (secs: number) => { 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 divisor_for_minutes = secs % (60 * 60);
const minutes = Math.floor(divisor_for_minutes / 60); const minutes = Math.floor(divisor_for_minutes / 60);
const divisor_for_seconds = divisor_for_minutes % 60; const divisor_for_seconds = divisor_for_minutes % 60;
const seconds = Math.ceil(divisor_for_seconds); const seconds = Math.ceil(divisor_for_seconds);
const obj = { const obj = {
h: hours, h: hours,
m: minutes, m: minutes,
s: seconds s: seconds
}; };
return obj; return obj;
}; };
export const toUptimestring = (secs: number): string => { 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(); status.stopListening();
} }
}); });
export const prerender = true;
</script> </script>
<Navbar /> <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"> <script lang="ts">
import { m } from '$lib/paraglide/messages'; import { ControlSection, StatusSection } from '$lib/components';
import { settings, status } from '$lib/stores';
import { onMount, onDestroy } from 'svelte';
import { ControlSection, StatusSection, SettingsSection } from '$lib/components';
</script> </script>
<div class=" mx-auto px-2 py-4"> <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 class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
<div> <div>
<ControlSection /> <ControlSection />
</div> </div>
<div class="col-span-2">
<StatusSection />
</div>
</div> <div class="col-span-2">
<StatusSection />
</div>
</div>
</div> </div>

View file

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

View file

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

View file

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

View file

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