feat: Lint fixes, add forgejo workflow and e2e tests
This commit is contained in:
parent
af2f593fb8
commit
5917713b0d
39 changed files with 1666 additions and 1506 deletions
132
.forgejo/workflows/build.yaml
Normal file
132
.forgejo/workflows/build.yaml
Normal 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
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,4 +171,4 @@
|
||||||
"auto-detect": "Auto-detect",
|
"auto-detect": "Auto-detect",
|
||||||
"on": "on",
|
"on": "on",
|
||||||
"off": "off"
|
"off": "off"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,4 +139,4 @@
|
||||||
},
|
},
|
||||||
"warning": "Waarschuwing",
|
"warning": "Waarschuwing",
|
||||||
"auto-detect": "Automatische detectie"
|
"auto-detect": "Automatische detectie"
|
||||||
}
|
}
|
||||||
|
|
20
src/app.css
20
src/app.css
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
status.stopListening();
|
status.stopListening();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
export const prerender = true;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
2
src/routes/+layout.ts
Normal file
2
src/routes/+layout.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const prerender = true;
|
||||||
|
export const ssr = false;
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue