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';
|
||||
|
||||
test('home page has expected h1', async ({ page }) => {
|
||||
test('index page has expected columns control, status, settings', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Control' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Status' })).toBeVisible();
|
||||
});
|
||||
|
|
|
@ -20,7 +20,9 @@ export default ts.config(
|
|||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node }
|
||||
},
|
||||
rules: { 'no-undef': 'off' }
|
||||
rules: {
|
||||
'no-undef': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
|
@ -32,5 +34,10 @@ export default ts.config(
|
|||
svelteConfig
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'svelte/no-at-html-tags': 'off'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,152 +1,152 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"section": {
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"textColor": "Textfarbe",
|
||||
"backgroundColor": "Hintergrundfarbe",
|
||||
"ledPowerOnTest": "LED-Einschalttest",
|
||||
"ledFlashOnBlock": "LED blinkt bei neuem Block",
|
||||
"timePerScreen": "Zeit pro Bildschirm",
|
||||
"ledBrightness": "LED-Helligkeit",
|
||||
"flMaxBrightness": "Displaybeleuchtung Helligkeit",
|
||||
"timezoneOffset": "Zeitzonenoffset",
|
||||
"timeBetweenPriceUpdates": "Zeit zwischen Preisaktualisierungen",
|
||||
"fullRefreshEvery": "Vollständige Aktualisierung alle",
|
||||
"mempoolnstance": "Mempool Instance",
|
||||
"hostnamePrefix": "Hostnamen-Präfix",
|
||||
"StealFocusOnNewBlock": "Steal focus on new block",
|
||||
"useBigCharsMcap": "Verwende große Zeichen für die Marktkapitalisierung",
|
||||
"useBlkCountdown": "Blocks Countdown zur Halbierung",
|
||||
"useSatsSymbol": "Sats-Symbol verwenden",
|
||||
"suffixPrice": "Suffix-Preisformat",
|
||||
"disableLeds": "Alle LED-Effekte deaktivieren",
|
||||
"otaUpdates": "OTA updates",
|
||||
"enableMdns": "mDNS",
|
||||
"fetchEuroPrice": "€-Preis abrufen",
|
||||
"shortAmountsWarning": "Geringe Beträge können die Lebensdauer der Displays verkürzen",
|
||||
"tzOffsetHelpText": "Ein Neustart ist erforderlich, um den TZ-Offset anzuwenden.",
|
||||
"screens": "Bildschirme",
|
||||
"wifiTxPowerText": "In den meisten Fällen muss dies nicht eingestellt werden.",
|
||||
"wifiTxPower": "WiFi-TX-Leistung",
|
||||
"settingsSaved": "Einstellungen gespeichert",
|
||||
"errorSavingSettings": "Fehler beim Speichern der Einstellungen",
|
||||
"ownDataSource": "BTClock-Datenquelle",
|
||||
"flAlwaysOn": "Displaybeleuchtung immer an",
|
||||
"flEffectDelay": "Displaybeleuchtungeffekt Geschwindigkeit",
|
||||
"flFlashOnUpd": "Displaybeleuchting bei neuem Block",
|
||||
"mempoolInstanceHelpText": "Nur wirksam, wenn die BTClock-Datenquelle deaktiviert ist. \nZur Anwendung ist ein Neustart erforderlich.",
|
||||
"luxLightToggle": "Automatisches Umschalten des Frontlichts bei Lux",
|
||||
"wpTimeout": "WiFi-Konfigurationsportal timeout",
|
||||
"useNostr": "Nostr-Datenquelle verwenden",
|
||||
"flDisable": "Displaybeleuchtung deaktivieren",
|
||||
"httpAuthUser": "WebUI-Benutzername",
|
||||
"httpAuthPass": "WebUI-Passwort",
|
||||
"httpAuthText": "Schützt nur die WebUI mit einem Passwort, nicht API-Aufrufe.",
|
||||
"currencies": "Währungen",
|
||||
"mowMode": "Mow suffixmodus",
|
||||
"suffixShareDot": "Kompakte Suffix-Notation",
|
||||
"section": {
|
||||
"displaysAndLed": "Anzeigen und LEDs",
|
||||
"screenSettings": "Infospezifisch",
|
||||
"dataSource": "Datenquelle",
|
||||
"extraFeatures": "Zusätzliche Funktionen",
|
||||
"system": "System"
|
||||
},
|
||||
"ledFlashOnZap": "LED blinkt bei Nostr Zap",
|
||||
"flFlashOnZap": "Displaybeleuchting bei Nostr Zap",
|
||||
"showAll": "Alle anzeigen",
|
||||
"hideAll": "Alles ausblenden",
|
||||
"flOffWhenDark": "Displaybeleuchtung aus, wenn es dunkel ist",
|
||||
"luxLightToggleText": "Zum Deaktivieren auf 0 setzen",
|
||||
"verticalDesc": "Vrtikale Bildschirmbeschreibung",
|
||||
"enableDebugLog": "Debug-Protokoll aktivieren",
|
||||
"bitaxeEnabled": "BitAxe-Integration aktivieren",
|
||||
"miningPoolStats": "Mining-Pool-Statistiken Integration Aktivieren",
|
||||
"nostrZapNotify": "Nostr Zap-Benachrichtigungen aktivieren",
|
||||
"thirdPartySource": "mempool.space/coincap.io Verwenden",
|
||||
"dataSource": {
|
||||
"nostr": "Nostr-Verlag",
|
||||
"custom": "Benutzerdefinierter dataquelle"
|
||||
},
|
||||
"fontName": "Schriftart",
|
||||
"timeBasedDnd": "Aktivieren Sie den Zeitplan „Bitte nicht stören“.",
|
||||
"dndStartHour": "Startstunde",
|
||||
"dndStartMinute": "Startminute",
|
||||
"dndEndHour": "Endstunde",
|
||||
"dndEndMinute": "Schlussminute"
|
||||
},
|
||||
"control": {
|
||||
"systemInfo": "Systeminfo",
|
||||
"version": "Version",
|
||||
"buildTime": "Build time",
|
||||
"ledColor": "LED-Farbe",
|
||||
"turnOff": "Ausschalten",
|
||||
"setColor": "Farbe festlegen",
|
||||
"showText": "Text anzeigen",
|
||||
"text": "Text",
|
||||
"title": "Kontrolle",
|
||||
"hostname": "Hostname",
|
||||
"frontlight": "Displaybeleuchtung",
|
||||
"turnOn": "Einschalten",
|
||||
"flashFrontlight": "Blinken"
|
||||
},
|
||||
"status": {
|
||||
"title": "Status",
|
||||
"screenCycle": "Bildschirmzyklus",
|
||||
"memoryFree": "Speicher frei",
|
||||
"wsPriceConnection": "WS-Preisverbindung",
|
||||
"wsMempoolConnection": "WS {instance}-Verbindung",
|
||||
"fetchEuroNote": "If you use \"Fetch € price\" the WS Price connection will show ❌ since it uses another data source.",
|
||||
"uptime": "Betriebszeit",
|
||||
"wifiSignalStrength": "WiFi-Signalstärke",
|
||||
"wsDataConnection": "BTClock-Datenquelle verbindung",
|
||||
"lightSensor": "Lichtsensor",
|
||||
"nostrConnection": "Nostr Relay-Verbindung",
|
||||
"doNotDisturb": "Bitte nicht stören",
|
||||
"timeBasedDnd": "Zeitbasierter Zeitplan"
|
||||
},
|
||||
"firmwareUpdater": {
|
||||
"fileUploadSuccess": "Datei erfolgreich hochgeladen, Gerät neu gestartet. WebUI in {countdown} Sekunden neu geladen",
|
||||
"fileUploadFailed": "Das Hochladen der Datei ist fehlgeschlagen. \nStellen Sie sicher, dass Sie die richtige Datei ausgewählt haben, und versuchen Sie es erneut.",
|
||||
"uploading": "Hochladen",
|
||||
"firmwareUpdateText": "Wenn Sie die Firmware-Upload-Funktion verwenden, stellen Sie sicher, dass Sie die richtigen Dateien verwenden. \nDas Hochladen der falschen Dateien kann dazu führen, dass das Gerät nicht mehr funktioniert. \nWenn es schief geht, können Sie die Firmware wiederherstellen, indem Sie das vollständige Image hochladen, nachdem Sie das Gerät in den BOOT-Modus versetzt haben.",
|
||||
"swUpToDate": "Du hast die neueste Version.",
|
||||
"swUpdateAvailable": "Eine neuere Version ist verfügbar!",
|
||||
"latestVersion": "Letzte Version",
|
||||
"releaseDate": "Veröffentlichungsdatum",
|
||||
"viewRelease": "Veröffentlichung anzeigen",
|
||||
"autoUpdate": "Update installieren (experimentell)",
|
||||
"autoUpdateInProgress": "Automatische Aktualisierung läuft, bitte warten..."
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"black": "Schwarz",
|
||||
"white": "Weiss"
|
||||
},
|
||||
"time": {
|
||||
"minutes": "Minuten",
|
||||
"seconds": "Sekunden"
|
||||
},
|
||||
"restartRequired": "Neustart erforderlich",
|
||||
"button": {
|
||||
"save": "Speichern",
|
||||
"reset": "Zurücksetzen",
|
||||
"restart": "Neustart",
|
||||
"forceFullRefresh": "Vollständige Aktualisierung erzwingen"
|
||||
},
|
||||
"timer": {
|
||||
"running": "läuft",
|
||||
"stopped": "gestoppt"
|
||||
},
|
||||
"sections": {
|
||||
"control": {
|
||||
"keepSameColor": "Gleiche Farbe beibehalten"
|
||||
}
|
||||
},
|
||||
"rssiBar": {
|
||||
"tooltip": "Werte > -67 dBm gelten als gut. > -30 dBm ist erstaunlich"
|
||||
},
|
||||
"warning": "Achtung",
|
||||
"auto-detect": "Automatische Erkennung"
|
||||
}
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"section": {
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"textColor": "Textfarbe",
|
||||
"backgroundColor": "Hintergrundfarbe",
|
||||
"ledPowerOnTest": "LED-Einschalttest",
|
||||
"ledFlashOnBlock": "LED blinkt bei neuem Block",
|
||||
"timePerScreen": "Zeit pro Bildschirm",
|
||||
"ledBrightness": "LED-Helligkeit",
|
||||
"flMaxBrightness": "Displaybeleuchtung Helligkeit",
|
||||
"timezoneOffset": "Zeitzonenoffset",
|
||||
"timeBetweenPriceUpdates": "Zeit zwischen Preisaktualisierungen",
|
||||
"fullRefreshEvery": "Vollständige Aktualisierung alle",
|
||||
"mempoolnstance": "Mempool Instance",
|
||||
"hostnamePrefix": "Hostnamen-Präfix",
|
||||
"StealFocusOnNewBlock": "Steal focus on new block",
|
||||
"useBigCharsMcap": "Verwende große Zeichen für die Marktkapitalisierung",
|
||||
"useBlkCountdown": "Blocks Countdown zur Halbierung",
|
||||
"useSatsSymbol": "Sats-Symbol verwenden",
|
||||
"suffixPrice": "Suffix-Preisformat",
|
||||
"disableLeds": "Alle LED-Effekte deaktivieren",
|
||||
"otaUpdates": "OTA updates",
|
||||
"enableMdns": "mDNS",
|
||||
"fetchEuroPrice": "€-Preis abrufen",
|
||||
"shortAmountsWarning": "Geringe Beträge können die Lebensdauer der Displays verkürzen",
|
||||
"tzOffsetHelpText": "Ein Neustart ist erforderlich, um den TZ-Offset anzuwenden.",
|
||||
"screens": "Bildschirme",
|
||||
"wifiTxPowerText": "In den meisten Fällen muss dies nicht eingestellt werden.",
|
||||
"wifiTxPower": "WiFi-TX-Leistung",
|
||||
"settingsSaved": "Einstellungen gespeichert",
|
||||
"errorSavingSettings": "Fehler beim Speichern der Einstellungen",
|
||||
"ownDataSource": "BTClock-Datenquelle",
|
||||
"flAlwaysOn": "Displaybeleuchtung immer an",
|
||||
"flEffectDelay": "Displaybeleuchtungeffekt Geschwindigkeit",
|
||||
"flFlashOnUpd": "Displaybeleuchting bei neuem Block",
|
||||
"mempoolInstanceHelpText": "Nur wirksam, wenn die BTClock-Datenquelle deaktiviert ist. \nZur Anwendung ist ein Neustart erforderlich.",
|
||||
"luxLightToggle": "Automatisches Umschalten des Frontlichts bei Lux",
|
||||
"wpTimeout": "WiFi-Konfigurationsportal timeout",
|
||||
"useNostr": "Nostr-Datenquelle verwenden",
|
||||
"flDisable": "Displaybeleuchtung deaktivieren",
|
||||
"httpAuthUser": "WebUI-Benutzername",
|
||||
"httpAuthPass": "WebUI-Passwort",
|
||||
"httpAuthText": "Schützt nur die WebUI mit einem Passwort, nicht API-Aufrufe.",
|
||||
"currencies": "Währungen",
|
||||
"mowMode": "Mow suffixmodus",
|
||||
"suffixShareDot": "Kompakte Suffix-Notation",
|
||||
"section": {
|
||||
"displaysAndLed": "Anzeigen und LEDs",
|
||||
"screenSettings": "Infospezifisch",
|
||||
"dataSource": "Datenquelle",
|
||||
"extraFeatures": "Zusätzliche Funktionen",
|
||||
"system": "System"
|
||||
},
|
||||
"ledFlashOnZap": "LED blinkt bei Nostr Zap",
|
||||
"flFlashOnZap": "Displaybeleuchting bei Nostr Zap",
|
||||
"showAll": "Alle anzeigen",
|
||||
"hideAll": "Alles ausblenden",
|
||||
"flOffWhenDark": "Displaybeleuchtung aus, wenn es dunkel ist",
|
||||
"luxLightToggleText": "Zum Deaktivieren auf 0 setzen",
|
||||
"verticalDesc": "Vrtikale Bildschirmbeschreibung",
|
||||
"enableDebugLog": "Debug-Protokoll aktivieren",
|
||||
"bitaxeEnabled": "BitAxe-Integration aktivieren",
|
||||
"miningPoolStats": "Mining-Pool-Statistiken Integration Aktivieren",
|
||||
"nostrZapNotify": "Nostr Zap-Benachrichtigungen aktivieren",
|
||||
"thirdPartySource": "mempool.space/coincap.io Verwenden",
|
||||
"dataSource": {
|
||||
"nostr": "Nostr-Verlag",
|
||||
"custom": "Benutzerdefinierter dataquelle"
|
||||
},
|
||||
"fontName": "Schriftart",
|
||||
"timeBasedDnd": "Aktivieren Sie den Zeitplan „Bitte nicht stören“.",
|
||||
"dndStartHour": "Startstunde",
|
||||
"dndStartMinute": "Startminute",
|
||||
"dndEndHour": "Endstunde",
|
||||
"dndEndMinute": "Schlussminute"
|
||||
},
|
||||
"control": {
|
||||
"systemInfo": "Systeminfo",
|
||||
"version": "Version",
|
||||
"buildTime": "Build time",
|
||||
"ledColor": "LED-Farbe",
|
||||
"turnOff": "Ausschalten",
|
||||
"setColor": "Farbe festlegen",
|
||||
"showText": "Text anzeigen",
|
||||
"text": "Text",
|
||||
"title": "Kontrolle",
|
||||
"hostname": "Hostname",
|
||||
"frontlight": "Displaybeleuchtung",
|
||||
"turnOn": "Einschalten",
|
||||
"flashFrontlight": "Blinken"
|
||||
},
|
||||
"status": {
|
||||
"title": "Status",
|
||||
"screenCycle": "Bildschirmzyklus",
|
||||
"memoryFree": "Speicher frei",
|
||||
"wsPriceConnection": "WS-Preisverbindung",
|
||||
"wsMempoolConnection": "WS {instance}-Verbindung",
|
||||
"fetchEuroNote": "If you use \"Fetch € price\" the WS Price connection will show ❌ since it uses another data source.",
|
||||
"uptime": "Betriebszeit",
|
||||
"wifiSignalStrength": "WiFi-Signalstärke",
|
||||
"wsDataConnection": "BTClock-Datenquelle verbindung",
|
||||
"lightSensor": "Lichtsensor",
|
||||
"nostrConnection": "Nostr Relay-Verbindung",
|
||||
"doNotDisturb": "Bitte nicht stören",
|
||||
"timeBasedDnd": "Zeitbasierter Zeitplan"
|
||||
},
|
||||
"firmwareUpdater": {
|
||||
"fileUploadSuccess": "Datei erfolgreich hochgeladen, Gerät neu gestartet. WebUI in {countdown} Sekunden neu geladen",
|
||||
"fileUploadFailed": "Das Hochladen der Datei ist fehlgeschlagen. \nStellen Sie sicher, dass Sie die richtige Datei ausgewählt haben, und versuchen Sie es erneut.",
|
||||
"uploading": "Hochladen",
|
||||
"firmwareUpdateText": "Wenn Sie die Firmware-Upload-Funktion verwenden, stellen Sie sicher, dass Sie die richtigen Dateien verwenden. \nDas Hochladen der falschen Dateien kann dazu führen, dass das Gerät nicht mehr funktioniert. \nWenn es schief geht, können Sie die Firmware wiederherstellen, indem Sie das vollständige Image hochladen, nachdem Sie das Gerät in den BOOT-Modus versetzt haben.",
|
||||
"swUpToDate": "Du hast die neueste Version.",
|
||||
"swUpdateAvailable": "Eine neuere Version ist verfügbar!",
|
||||
"latestVersion": "Letzte Version",
|
||||
"releaseDate": "Veröffentlichungsdatum",
|
||||
"viewRelease": "Veröffentlichung anzeigen",
|
||||
"autoUpdate": "Update installieren (experimentell)",
|
||||
"autoUpdateInProgress": "Automatische Aktualisierung läuft, bitte warten..."
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"black": "Schwarz",
|
||||
"white": "Weiss"
|
||||
},
|
||||
"time": {
|
||||
"minutes": "Minuten",
|
||||
"seconds": "Sekunden"
|
||||
},
|
||||
"restartRequired": "Neustart erforderlich",
|
||||
"button": {
|
||||
"save": "Speichern",
|
||||
"reset": "Zurücksetzen",
|
||||
"restart": "Neustart",
|
||||
"forceFullRefresh": "Vollständige Aktualisierung erzwingen"
|
||||
},
|
||||
"timer": {
|
||||
"running": "läuft",
|
||||
"stopped": "gestoppt"
|
||||
},
|
||||
"sections": {
|
||||
"control": {
|
||||
"keepSameColor": "Gleiche Farbe beibehalten"
|
||||
}
|
||||
},
|
||||
"rssiBar": {
|
||||
"tooltip": "Werte > -67 dBm gelten als gut. > -30 dBm ist erstaunlich"
|
||||
},
|
||||
"warning": "Achtung",
|
||||
"auto-detect": "Automatische Erkennung"
|
||||
}
|
||||
|
|
|
@ -171,4 +171,4 @@
|
|||
"auto-detect": "Auto-detect",
|
||||
"on": "on",
|
||||
"off": "off"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,152 +1,152 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from es!",
|
||||
"section": {
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
"textColor": "Color de texto",
|
||||
"backgroundColor": "Color de fondo",
|
||||
"ledBrightness": "Brillo LED",
|
||||
"screens": "Pantallas",
|
||||
"shortAmountsWarning": "Pequeñas cantidades pueden acortar la vida útil de los displays",
|
||||
"fullRefreshEvery": "Actualización completa cada",
|
||||
"timePerScreen": "Tiempo por pantalla",
|
||||
"tzOffsetHelpText": "Es necesario reiniciar para aplicar la compensación.",
|
||||
"timezoneOffset": "Compensación de zona horaria",
|
||||
"StealFocusOnNewBlock": "Presta atención al nuevo bloque",
|
||||
"ledFlashOnBlock": "El LED parpadea con un bloque nuevo",
|
||||
"useBigCharsMcap": "Utilice caracteres grandes para la market cap",
|
||||
"useBlkCountdown": "Cuenta regresiva en bloques",
|
||||
"useSatsSymbol": "Usar símbolo sats",
|
||||
"fetchEuroPrice": "Obtener precio en €",
|
||||
"timeBetweenPriceUpdates": "Tiempo entre actualizaciones de precios",
|
||||
"ledPowerOnTest": "Prueba de encendido del LED",
|
||||
"enableMdns": "mDNS",
|
||||
"hostnamePrefix": "Prefijo de nombre de host",
|
||||
"mempoolnstance": "Instancia de Mempool",
|
||||
"suffixPrice": "Precio con sufijos",
|
||||
"disableLeds": "Desactivar efectos de LED",
|
||||
"otaUpdates": "Actualización por aire",
|
||||
"wifiTxPowerText": "En la mayoría de los casos no es necesario configurar esto.",
|
||||
"settingsSaved": "Configuración guardada",
|
||||
"errorSavingSettings": "Error al guardar la configuración",
|
||||
"ownDataSource": "fuente de datos BTClock",
|
||||
"flMaxBrightness": "Brillo de luz de la pantalla",
|
||||
"flAlwaysOn": "Luz de la pantalla siempre encendida",
|
||||
"flEffectDelay": "Velocidad del efecto de luz de la pantalla",
|
||||
"flFlashOnUpd": "Luz de la pantalla parpadea con un nuevo bloque",
|
||||
"mempoolInstanceHelpText": "Solo es efectivo cuando la fuente de datos BTClock está deshabilitada. \nEs necesario reiniciar para aplicar.",
|
||||
"luxLightToggle": "Cambio automático de luz frontal en lux",
|
||||
"wpTimeout": "Portal de configuración WiFi timeout",
|
||||
"useNostr": "Utilice la fuente de datos Nostr",
|
||||
"flDisable": "Desactivar luz de la pantalla",
|
||||
"httpAuthUser": "Nombre de usuario WebUI",
|
||||
"httpAuthPass": "Contraseña WebUI",
|
||||
"httpAuthText": "Solo la WebUI está protegida con contraseña, no las llamadas API.",
|
||||
"currencies": "Monedas",
|
||||
"mowMode": "Modo de sufijo Mow",
|
||||
"suffixShareDot": "Notación compacta de sufijo",
|
||||
"section": {
|
||||
"displaysAndLed": "Pantallas y LED",
|
||||
"screenSettings": "Específico de la pantalla",
|
||||
"dataSource": "fuente de datos",
|
||||
"extraFeatures": "Funciones adicionales",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"ledFlashOnZap": "LED parpadeante con Nostr Zap",
|
||||
"flFlashOnZap": "Flash de luz frontal con Nostr Zap",
|
||||
"showAll": "Mostrar todo",
|
||||
"hideAll": "Ocultar todo",
|
||||
"flOffWhenDark": "Luz de la pantalla cuando está oscuro",
|
||||
"luxLightToggleText": "Establecer en 0 para desactivar",
|
||||
"verticalDesc": "Descripción de pantalla vertical",
|
||||
"enableDebugLog": "Habilitar registro de depuración",
|
||||
"bitaxeEnabled": "Habilitar la integración de BitAxe",
|
||||
"miningPoolStats": "Habilitar la integración de estadísticas del grupo minero",
|
||||
"nostrZapNotify": "Habilitar notificaciones de Nostr Zap",
|
||||
"thirdPartySource": "Utilice mempool.space/coincap.io",
|
||||
"dataSource": {
|
||||
"nostr": "editorial nostr",
|
||||
"custom": "Punto final personalizado"
|
||||
},
|
||||
"fontName": "Fuente",
|
||||
"timeBasedDnd": "Habilitar el horario de No molestar",
|
||||
"dndStartHour": "Hora de inicio",
|
||||
"dndStartMinute": "Minuto de inicio",
|
||||
"dndEndHour": "Hora final",
|
||||
"dndEndMinute": "Minuto final"
|
||||
},
|
||||
"control": {
|
||||
"turnOff": "Apagar",
|
||||
"setColor": "Establecer el color",
|
||||
"version": "Versión",
|
||||
"ledColor": "color del LED",
|
||||
"systemInfo": "Info del sistema",
|
||||
"showText": "Mostrar texto",
|
||||
"text": "Texto",
|
||||
"title": "Control",
|
||||
"buildTime": "Tiempo de compilación",
|
||||
"hostname": "Nombre del host",
|
||||
"turnOn": "Encender",
|
||||
"frontlight": "Luz de la pantalla",
|
||||
"flashFrontlight": "Luz intermitente"
|
||||
},
|
||||
"status": {
|
||||
"memoryFree": "Memoria RAM libre",
|
||||
"wsPriceConnection": "Conexión WebSocket Precio",
|
||||
"wsMempoolConnection": "Conexión WebSocket {instance}",
|
||||
"screenCycle": "Ciclo de pantalla",
|
||||
"uptime": "Tiempo de funcionamiento",
|
||||
"fetchEuroNote": "Si utiliza \"Obtener precio en €\", la conexión de Precio WS mostrará ❌ ya que utiliza otra fuente de datos.",
|
||||
"title": "Estado",
|
||||
"wifiSignalStrength": "Fuerza de la señal WiFi",
|
||||
"wsDataConnection": "Conexión de fuente de datos BTClock",
|
||||
"lightSensor": "Sensor de luz",
|
||||
"nostrConnection": "Conexión de relé Nostr",
|
||||
"doNotDisturb": "No molestar",
|
||||
"timeBasedDnd": "Horario basado en el tiempo"
|
||||
},
|
||||
"firmwareUpdater": {
|
||||
"fileUploadSuccess": "Archivo cargado exitosamente, reiniciando el dispositivo. Recargando WebUI en {countdown} segundos",
|
||||
"fileUploadFailed": "Error al cargar el archivo. \nAsegúrese de haber seleccionado el archivo correcto e inténtelo nuevamente.",
|
||||
"uploading": "Subiendo",
|
||||
"firmwareUpdateText": "Cuando utilice la función de carga de firmware, asegúrese de utilizar los archivos correctos. \nCargar archivos incorrectos puede provocar que el dispositivo no funcione. \nSi sale mal, puede restaurar el firmware cargando la imagen completa después de configurar el dispositivo en modo BOOT.",
|
||||
"swUpToDate": "Tienes la ultima version.",
|
||||
"swUpdateAvailable": "¡Una nueva versión está disponible!",
|
||||
"latestVersion": "Ultima versión",
|
||||
"releaseDate": "Fecha de lanzamiento",
|
||||
"viewRelease": "Ver lanzamiento",
|
||||
"autoUpdate": "Instalar actualización (experimental)",
|
||||
"autoUpdateInProgress": "Actualización automática en progreso, espere..."
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"save": "Guardar",
|
||||
"reset": "Restaurar",
|
||||
"restart": "Reiniciar",
|
||||
"forceFullRefresh": "Forzar refresco"
|
||||
},
|
||||
"colors": {
|
||||
"black": "Negro",
|
||||
"white": "Blanco"
|
||||
},
|
||||
"restartRequired": "reinicio requerido",
|
||||
"time": {
|
||||
"minutes": "minutos",
|
||||
"seconds": "segundos"
|
||||
},
|
||||
"timer": {
|
||||
"running": "funcionando",
|
||||
"stopped": "detenido"
|
||||
},
|
||||
"sections": {
|
||||
"control": {
|
||||
"keepSameColor": "Mantén el mismo color"
|
||||
}
|
||||
},
|
||||
"rssiBar": {
|
||||
"tooltip": "Se consideran buenos valores > -67 dBm. > -30 dBm es increíble"
|
||||
},
|
||||
"warning": "Aviso",
|
||||
"auto-detect": "Detección automática"
|
||||
}
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from es!",
|
||||
"section": {
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
"textColor": "Color de texto",
|
||||
"backgroundColor": "Color de fondo",
|
||||
"ledBrightness": "Brillo LED",
|
||||
"screens": "Pantallas",
|
||||
"shortAmountsWarning": "Pequeñas cantidades pueden acortar la vida útil de los displays",
|
||||
"fullRefreshEvery": "Actualización completa cada",
|
||||
"timePerScreen": "Tiempo por pantalla",
|
||||
"tzOffsetHelpText": "Es necesario reiniciar para aplicar la compensación.",
|
||||
"timezoneOffset": "Compensación de zona horaria",
|
||||
"StealFocusOnNewBlock": "Presta atención al nuevo bloque",
|
||||
"ledFlashOnBlock": "El LED parpadea con un bloque nuevo",
|
||||
"useBigCharsMcap": "Utilice caracteres grandes para la market cap",
|
||||
"useBlkCountdown": "Cuenta regresiva en bloques",
|
||||
"useSatsSymbol": "Usar símbolo sats",
|
||||
"fetchEuroPrice": "Obtener precio en €",
|
||||
"timeBetweenPriceUpdates": "Tiempo entre actualizaciones de precios",
|
||||
"ledPowerOnTest": "Prueba de encendido del LED",
|
||||
"enableMdns": "mDNS",
|
||||
"hostnamePrefix": "Prefijo de nombre de host",
|
||||
"mempoolnstance": "Instancia de Mempool",
|
||||
"suffixPrice": "Precio con sufijos",
|
||||
"disableLeds": "Desactivar efectos de LED",
|
||||
"otaUpdates": "Actualización por aire",
|
||||
"wifiTxPowerText": "En la mayoría de los casos no es necesario configurar esto.",
|
||||
"settingsSaved": "Configuración guardada",
|
||||
"errorSavingSettings": "Error al guardar la configuración",
|
||||
"ownDataSource": "fuente de datos BTClock",
|
||||
"flMaxBrightness": "Brillo de luz de la pantalla",
|
||||
"flAlwaysOn": "Luz de la pantalla siempre encendida",
|
||||
"flEffectDelay": "Velocidad del efecto de luz de la pantalla",
|
||||
"flFlashOnUpd": "Luz de la pantalla parpadea con un nuevo bloque",
|
||||
"mempoolInstanceHelpText": "Solo es efectivo cuando la fuente de datos BTClock está deshabilitada. \nEs necesario reiniciar para aplicar.",
|
||||
"luxLightToggle": "Cambio automático de luz frontal en lux",
|
||||
"wpTimeout": "Portal de configuración WiFi timeout",
|
||||
"useNostr": "Utilice la fuente de datos Nostr",
|
||||
"flDisable": "Desactivar luz de la pantalla",
|
||||
"httpAuthUser": "Nombre de usuario WebUI",
|
||||
"httpAuthPass": "Contraseña WebUI",
|
||||
"httpAuthText": "Solo la WebUI está protegida con contraseña, no las llamadas API.",
|
||||
"currencies": "Monedas",
|
||||
"mowMode": "Modo de sufijo Mow",
|
||||
"suffixShareDot": "Notación compacta de sufijo",
|
||||
"section": {
|
||||
"displaysAndLed": "Pantallas y LED",
|
||||
"screenSettings": "Específico de la pantalla",
|
||||
"dataSource": "fuente de datos",
|
||||
"extraFeatures": "Funciones adicionales",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"ledFlashOnZap": "LED parpadeante con Nostr Zap",
|
||||
"flFlashOnZap": "Flash de luz frontal con Nostr Zap",
|
||||
"showAll": "Mostrar todo",
|
||||
"hideAll": "Ocultar todo",
|
||||
"flOffWhenDark": "Luz de la pantalla cuando está oscuro",
|
||||
"luxLightToggleText": "Establecer en 0 para desactivar",
|
||||
"verticalDesc": "Descripción de pantalla vertical",
|
||||
"enableDebugLog": "Habilitar registro de depuración",
|
||||
"bitaxeEnabled": "Habilitar la integración de BitAxe",
|
||||
"miningPoolStats": "Habilitar la integración de estadísticas del grupo minero",
|
||||
"nostrZapNotify": "Habilitar notificaciones de Nostr Zap",
|
||||
"thirdPartySource": "Utilice mempool.space/coincap.io",
|
||||
"dataSource": {
|
||||
"nostr": "editorial nostr",
|
||||
"custom": "Punto final personalizado"
|
||||
},
|
||||
"fontName": "Fuente",
|
||||
"timeBasedDnd": "Habilitar el horario de No molestar",
|
||||
"dndStartHour": "Hora de inicio",
|
||||
"dndStartMinute": "Minuto de inicio",
|
||||
"dndEndHour": "Hora final",
|
||||
"dndEndMinute": "Minuto final"
|
||||
},
|
||||
"control": {
|
||||
"turnOff": "Apagar",
|
||||
"setColor": "Establecer el color",
|
||||
"version": "Versión",
|
||||
"ledColor": "color del LED",
|
||||
"systemInfo": "Info del sistema",
|
||||
"showText": "Mostrar texto",
|
||||
"text": "Texto",
|
||||
"title": "Control",
|
||||
"buildTime": "Tiempo de compilación",
|
||||
"hostname": "Nombre del host",
|
||||
"turnOn": "Encender",
|
||||
"frontlight": "Luz de la pantalla",
|
||||
"flashFrontlight": "Luz intermitente"
|
||||
},
|
||||
"status": {
|
||||
"memoryFree": "Memoria RAM libre",
|
||||
"wsPriceConnection": "Conexión WebSocket Precio",
|
||||
"wsMempoolConnection": "Conexión WebSocket {instance}",
|
||||
"screenCycle": "Ciclo de pantalla",
|
||||
"uptime": "Tiempo de funcionamiento",
|
||||
"fetchEuroNote": "Si utiliza \"Obtener precio en €\", la conexión de Precio WS mostrará ❌ ya que utiliza otra fuente de datos.",
|
||||
"title": "Estado",
|
||||
"wifiSignalStrength": "Fuerza de la señal WiFi",
|
||||
"wsDataConnection": "Conexión de fuente de datos BTClock",
|
||||
"lightSensor": "Sensor de luz",
|
||||
"nostrConnection": "Conexión de relé Nostr",
|
||||
"doNotDisturb": "No molestar",
|
||||
"timeBasedDnd": "Horario basado en el tiempo"
|
||||
},
|
||||
"firmwareUpdater": {
|
||||
"fileUploadSuccess": "Archivo cargado exitosamente, reiniciando el dispositivo. Recargando WebUI en {countdown} segundos",
|
||||
"fileUploadFailed": "Error al cargar el archivo. \nAsegúrese de haber seleccionado el archivo correcto e inténtelo nuevamente.",
|
||||
"uploading": "Subiendo",
|
||||
"firmwareUpdateText": "Cuando utilice la función de carga de firmware, asegúrese de utilizar los archivos correctos. \nCargar archivos incorrectos puede provocar que el dispositivo no funcione. \nSi sale mal, puede restaurar el firmware cargando la imagen completa después de configurar el dispositivo en modo BOOT.",
|
||||
"swUpToDate": "Tienes la ultima version.",
|
||||
"swUpdateAvailable": "¡Una nueva versión está disponible!",
|
||||
"latestVersion": "Ultima versión",
|
||||
"releaseDate": "Fecha de lanzamiento",
|
||||
"viewRelease": "Ver lanzamiento",
|
||||
"autoUpdate": "Instalar actualización (experimental)",
|
||||
"autoUpdateInProgress": "Actualización automática en progreso, espere..."
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"save": "Guardar",
|
||||
"reset": "Restaurar",
|
||||
"restart": "Reiniciar",
|
||||
"forceFullRefresh": "Forzar refresco"
|
||||
},
|
||||
"colors": {
|
||||
"black": "Negro",
|
||||
"white": "Blanco"
|
||||
},
|
||||
"restartRequired": "reinicio requerido",
|
||||
"time": {
|
||||
"minutes": "minutos",
|
||||
"seconds": "segundos"
|
||||
},
|
||||
"timer": {
|
||||
"running": "funcionando",
|
||||
"stopped": "detenido"
|
||||
},
|
||||
"sections": {
|
||||
"control": {
|
||||
"keepSameColor": "Mantén el mismo color"
|
||||
}
|
||||
},
|
||||
"rssiBar": {
|
||||
"tooltip": "Se consideran buenos valores > -67 dBm. > -30 dBm es increíble"
|
||||
},
|
||||
"warning": "Aviso",
|
||||
"auto-detect": "Detección automática"
|
||||
}
|
||||
|
|
|
@ -139,4 +139,4 @@
|
|||
},
|
||||
"warning": "Waarschuwing",
|
||||
"auto-detect": "Automatische detectie"
|
||||
}
|
||||
}
|
||||
|
|
20
src/app.css
20
src/app.css
|
@ -1,26 +1,26 @@
|
|||
@import 'tailwindcss';
|
||||
@plugin "daisyui" {
|
||||
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--secondary: #6b7280;
|
||||
--accent: #f59e0b;
|
||||
--primary: #3b82f6;
|
||||
--secondary: #6b7280;
|
||||
--accent: #f59e0b;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html, body {
|
||||
@apply h-full;
|
||||
html,
|
||||
body {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply bg-base-200;
|
||||
@apply bg-base-200;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-base-200 pt-16;
|
||||
}
|
||||
@apply bg-base-200 pt-16;
|
||||
}
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
<html lang="%paraglide.lang%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents" class="h-full">%sveltekit.body%</div>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,112 +1,111 @@
|
|||
import { PUBLIC_BASE_URL } from '$env/static/public';
|
||||
import { baseUrl } from './env';
|
||||
|
||||
/**
|
||||
* Sets custom text to display on the clock
|
||||
*/
|
||||
export const setCustomText = (newText: string) => {
|
||||
return fetch(`${baseUrl}/api/show/text/${newText}`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/show/text/${newText}`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the LED colors
|
||||
*/
|
||||
export const setLEDcolor = (ledStatus: { hex: string }[]) => {
|
||||
return fetch(`${baseUrl}/api/lights/set`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(ledStatus)
|
||||
}).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/lights/set`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(ledStatus)
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns off all LEDs
|
||||
*/
|
||||
export const turnOffLeds = () => {
|
||||
return fetch(`${baseUrl}/api/lights/off`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/lights/off`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Restarts the clock
|
||||
*/
|
||||
export const restartClock = () => {
|
||||
return fetch(`${baseUrl}/api/restart`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/restart`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces a full refresh of the clock
|
||||
*/
|
||||
export const forceFullRefresh = () => {
|
||||
return fetch(`${baseUrl}/api/full_refresh`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/full_refresh`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random color hex code
|
||||
*/
|
||||
export const generateRandomColor = () => {
|
||||
return `#${Math.floor(Math.random() * 16777215)
|
||||
.toString(16)
|
||||
.padStart(6, '0')}`;
|
||||
};
|
||||
return `#${Math.floor(Math.random() * 16777215)
|
||||
.toString(16)
|
||||
.padStart(6, '0')}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the active screen
|
||||
*/
|
||||
export const setActiveScreen = async (screenId: string) => {
|
||||
return fetch(`${baseUrl}/api/show/screen/${screenId}`);
|
||||
}
|
||||
return fetch(`${baseUrl}/api/show/screen/${screenId}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the active currency
|
||||
*/
|
||||
export const setActiveCurrency = async (currency: string) => {
|
||||
return fetch(`${baseUrl}/api/show/currency/${currency}`);
|
||||
}
|
||||
return fetch(`${baseUrl}/api/show/currency/${currency}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns on the frontlight
|
||||
*/
|
||||
export const turnOnFrontlight = () => {
|
||||
return fetch(`${baseUrl}/api/frontlight/on`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/frontlight/on`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Flashes the frontlight
|
||||
*/
|
||||
export const flashFrontlight = () => {
|
||||
return fetch(`${baseUrl}/api/frontlight/flash`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/frontlight/flash`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns off the frontlight
|
||||
*/
|
||||
export const turnOffFrontlight = () => {
|
||||
return fetch(`${baseUrl}/api/frontlight/off`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/frontlight/off`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the timer
|
||||
*/
|
||||
*/
|
||||
export const toggleTimer = (currentStatus: boolean) => (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (currentStatus) {
|
||||
fetch(`${baseUrl}/api/action/pause`);
|
||||
} else {
|
||||
fetch(`${baseUrl}/api/action/timer_restart`);
|
||||
}
|
||||
e.preventDefault();
|
||||
if (currentStatus) {
|
||||
fetch(`${baseUrl}/api/action/pause`);
|
||||
} else {
|
||||
fetch(`${baseUrl}/api/action/timer_restart`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the do not disturb mode
|
||||
*/
|
||||
export const toggleDoNotDisturb = (currentStatus: boolean) => (e: Event) => {
|
||||
e.preventDefault();
|
||||
console.log(currentStatus);
|
||||
if (!currentStatus) {
|
||||
fetch(`${baseUrl}/api/dnd/enable`);
|
||||
} else {
|
||||
fetch(`${baseUrl}/api/dnd/disable`);
|
||||
}
|
||||
};
|
||||
e.preventDefault();
|
||||
console.log(currentStatus);
|
||||
if (!currentStatus) {
|
||||
fetch(`${baseUrl}/api/dnd/enable`);
|
||||
} else {
|
||||
fetch(`${baseUrl}/api/dnd/disable`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -60,10 +60,6 @@
|
|||
// $: if (containerWidth > 0) {
|
||||
// containerHeight = containerWidth * deviceRatio;
|
||||
// }
|
||||
|
||||
const fontSizeSingle = '4.5rem';
|
||||
const fontSizeMedium = '2.0rem';
|
||||
const fontSizeSplit = '1.0rem';
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -190,7 +186,6 @@
|
|||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
|
||||
.vertical-desc .split-text {
|
||||
transform: rotate(270deg);
|
||||
height: 50%;
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
currency,
|
||||
active = false,
|
||||
onClick,
|
||||
...restProps
|
||||
} = $props();
|
||||
let { currency, active = false, onClick, ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="btn join-item {active ? 'btn-primary' : 'btn-outline'} btn-xs"
|
||||
on:click={onClick}
|
||||
{...restProps}
|
||||
<button
|
||||
class="btn join-item {active ? 'btn-primary' : 'btn-outline'} btn-xs"
|
||||
on:click={onClick}
|
||||
{...restProps}
|
||||
>
|
||||
{currency}
|
||||
</button>
|
||||
{currency}
|
||||
</button>
|
||||
|
|
|
@ -1,26 +1,19 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
value = $bindable(''),
|
||||
label = "",
|
||||
placeholder = "",
|
||||
id = "",
|
||||
type = "text",
|
||||
...restProps
|
||||
} = $props();
|
||||
let {
|
||||
value = $bindable(''),
|
||||
label = '',
|
||||
placeholder = '',
|
||||
id = '',
|
||||
type = 'text',
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="form-control w-full">
|
||||
{#if label}
|
||||
<label for={id} class="label">
|
||||
<span class="label-text">{label}</span>
|
||||
</label>
|
||||
{/if}
|
||||
<input
|
||||
type={type}
|
||||
{placeholder}
|
||||
{id}
|
||||
class="input input-bordered w-full"
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
{#if label}
|
||||
<label for={id} class="label">
|
||||
<span class="label-text">{label}</span>
|
||||
</label>
|
||||
{/if}
|
||||
<input {type} {placeholder} {id} class="input input-bordered w-full" bind:value {...restProps} />
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,10 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
checked = $bindable(false),
|
||||
label = "",
|
||||
id = "",
|
||||
...restProps
|
||||
} = $props();
|
||||
let { checked = $bindable(false), label = '', id = '', ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<label class="flex items-center justify-between gap-2 cursor-pointer">
|
||||
{#if label}
|
||||
<span class="label-text text-xs">{label}</span>
|
||||
{/if}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-xs"
|
||||
{id}
|
||||
bind:checked
|
||||
{...restProps}
|
||||
/>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center justify-between gap-2">
|
||||
{#if label}
|
||||
<span class="label-text text-xs">{label}</span>
|
||||
{/if}
|
||||
<input type="checkbox" class="toggle toggle-primary toggle-xs" {id} bind:checked {...restProps} />
|
||||
</label>
|
||||
|
|
|
@ -18,4 +18,4 @@ export { default as CollapsibleSection } from './layout/CollapsibleSection.svelt
|
|||
export { default as ControlSection } from './sections/ControlSection.svelte';
|
||||
export { default as StatusSection } from './sections/StatusSection.svelte';
|
||||
export { default as SettingsSection } from './sections/SettingsSection.svelte';
|
||||
export { default as SystemSection } from './sections/SystemSection.svelte';
|
||||
export { default as SystemSection } from './sections/SystemSection.svelte';
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
title,
|
||||
open = $bindable(false),
|
||||
...restProps
|
||||
} = $props();
|
||||
let { title, open = $bindable(false), ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<div class="collapse collapse-arrow bg-base-200 rounded-lg mb-2" {...restProps}>
|
||||
<input type="checkbox" bind:checked={open} />
|
||||
<div class="collapse-title text-lg font-medium">
|
||||
{title}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-arrow bg-base-200 collapse mb-2 rounded-lg" {...restProps}>
|
||||
<input type="checkbox" bind:checked={open} />
|
||||
<div class="collapse-title text-lg font-medium">
|
||||
{title}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,87 +1,105 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
...restProps
|
||||
} = $props();
|
||||
|
||||
import { setLocale, getLocale } from '$lib/paraglide/runtime';
|
||||
import { locales } from '$lib/paraglide/runtime';
|
||||
import { page } from '$app/stores';
|
||||
let { ...restProps } = $props();
|
||||
|
||||
// Navigation items
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/settings', label: 'Settings' },
|
||||
{ href: '/system', label: 'System' },
|
||||
{ href: '/apidoc', label: 'API' }
|
||||
];
|
||||
import { setLocale, getLocale } from '$lib/paraglide/runtime';
|
||||
import { locales } from '$lib/paraglide/runtime';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
// Helper function to check if a link is active
|
||||
function isActive(href: string) {
|
||||
return $page.url.pathname === href;
|
||||
}
|
||||
// Navigation items
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/settings', label: 'Settings' },
|
||||
{ href: '/system', label: 'System' },
|
||||
{ href: '/apidoc', label: 'API' }
|
||||
];
|
||||
|
||||
const getLocaleName = (locale: string) => {
|
||||
return new Intl.DisplayNames([locale], { type: 'language' }).of(locale)
|
||||
}
|
||||
// Helper function to check if a link is active
|
||||
function isActive(href: string) {
|
||||
return $page.url.pathname === href;
|
||||
}
|
||||
|
||||
const getLanguageName = (locale: string) => {
|
||||
return getLocaleName(locale.split('-')[0])
|
||||
}
|
||||
const getLocaleName = (locale: string) => {
|
||||
return new Intl.DisplayNames([locale], { type: 'language' }).of(locale);
|
||||
};
|
||||
|
||||
const getEmojiFlag = (locale: string) => {
|
||||
const countryCode = locale.split('-')[1];
|
||||
const getLanguageName = (locale: string) => {
|
||||
return getLocaleName(locale.split('-')[0]);
|
||||
};
|
||||
|
||||
if (!countryCode || countryCode === 'US') {
|
||||
return '🇺🇸';
|
||||
}
|
||||
const getEmojiFlag = (locale: string) => {
|
||||
const countryCode = locale.split('-')[1];
|
||||
|
||||
return [...countryCode.toUpperCase()]
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
}
|
||||
if (!countryCode || countryCode === 'US') {
|
||||
return '🇺🇸';
|
||||
}
|
||||
|
||||
// Function to get the current flag
|
||||
const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧';
|
||||
return [...countryCode.toUpperCase()]
|
||||
.map((char) => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
};
|
||||
|
||||
// Function to get the current flag
|
||||
const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧';
|
||||
</script>
|
||||
|
||||
<div class="navbar bg-base-100 fixed top-0 z-50 shadow-sm w-full" {...restProps}>
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul tabindex="-1" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||
{#each navItems as { href, label }}
|
||||
<li>
|
||||
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<a href="/" class="btn btn-ghost text-xl">BTClock</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
{#each navItems as { href, label }}
|
||||
<li>
|
||||
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="dropdown dropdown-end mr-2">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost">
|
||||
<span class="text-sm">{getCurrentFlag()} {getLanguageName(getLocale())}</span>
|
||||
</div>
|
||||
<ul tabindex="-1" class="mt-3 z-[1] p-2 shadow menu dropdown-content bg-base-100 rounded-box w-auto">
|
||||
{#each locales as locale}
|
||||
<li><button onclick={() => setLocale(locale)} class="flex items-center gap-2 text-nowrap">{getEmojiFlag(locale)} {getLanguageName(locale)}</button></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar bg-base-100 fixed top-0 z-50 w-full shadow-sm" {...restProps}>
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h8m-8 6h16"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
|
||||
>
|
||||
{#each navItems as { href, label } (href)}
|
||||
<li>
|
||||
<a {href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<a href="/" class="btn btn-ghost text-xl">BTClock</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
{#each navItems as { href, label } (href)}
|
||||
<li>
|
||||
<a {href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="dropdown dropdown-end mr-2">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost">
|
||||
<span class="text-sm">{getCurrentFlag()} {getLanguageName(getLocale())}</span>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="menu dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-auto p-2 shadow"
|
||||
>
|
||||
{#each locales as locale (locale)}
|
||||
<li>
|
||||
<button onclick={() => setLocale(locale)} class="flex items-center gap-2 text-nowrap"
|
||||
>{getEmojiFlag(locale)} {getLanguageName(locale)}</button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
import { CardContainer, InputField, Toggle } from '$lib/components';
|
||||
import { settings, status } from '$lib/stores';
|
||||
import { onDestroy } from 'svelte';
|
||||
import {
|
||||
setCustomText,
|
||||
setLEDcolor,
|
||||
turnOffLeds,
|
||||
restartClock,
|
||||
import {
|
||||
setCustomText,
|
||||
setLEDcolor,
|
||||
turnOffLeds,
|
||||
restartClock,
|
||||
forceFullRefresh,
|
||||
generateRandomColor,
|
||||
flashFrontlight,
|
||||
|
@ -17,10 +17,10 @@
|
|||
import type { LedStatus } from '$lib/types';
|
||||
|
||||
let ledStatus = $state<LedStatus[]>([
|
||||
{hex: '#000000'},
|
||||
{hex: '#000000'},
|
||||
{hex: '#000000'},
|
||||
{hex: '#000000'}
|
||||
{ hex: '#000000' },
|
||||
{ hex: '#000000' },
|
||||
{ hex: '#000000' },
|
||||
{ hex: '#000000' }
|
||||
]);
|
||||
let customText = $state('');
|
||||
let keepLedsSameColor = $state(false);
|
||||
|
@ -28,7 +28,7 @@
|
|||
const checkSyncLeds = (e: Event) => {
|
||||
if (keepLedsSameColor && e.target instanceof HTMLInputElement) {
|
||||
const targetValue = e.target.value;
|
||||
|
||||
|
||||
ledStatus.forEach((element, i) => {
|
||||
if (ledStatus[i].hex != targetValue) {
|
||||
ledStatus[i].hex = targetValue;
|
||||
|
@ -81,19 +81,21 @@
|
|||
<div class="flex justify-between gap-2">
|
||||
<div class="mb-4 flex flex-wrap gap-2">
|
||||
{#if ledStatus.length > 0}
|
||||
{#each ledStatus as led}
|
||||
{#each ledStatus as led (led)}
|
||||
<input
|
||||
type="color"
|
||||
class="btn btn-square"
|
||||
bind:value={led.hex}
|
||||
onchange={checkSyncLeds}
|
||||
/>
|
||||
{/each}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
<Toggle label={m['sections.control.keepSameColor']()} bind:checked={keepLedsSameColor} />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-secondary" onclick={turnOffLeds}>{m['section.control.turnOff']()}</button>
|
||||
<button class="btn btn-secondary" onclick={turnOffLeds}
|
||||
>{m['section.control.turnOff']()}</button
|
||||
>
|
||||
<button class="btn btn-primary" onclick={() => setLEDcolor(ledStatus)}
|
||||
>{m['section.control.setColor']()}</button
|
||||
>
|
||||
|
@ -102,19 +104,24 @@
|
|||
</div>
|
||||
|
||||
{#if $settings.hasFrontlight && !$settings.flDisable}
|
||||
<div>
|
||||
<h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button class="btn btn-secondary" onclick={() => turnOnFrontlight()}>{m['section.control.turnOn']()}</button>
|
||||
<button class="btn btn-primary" onclick={() => turnOffFrontlight()}>{m['section.control.turnOff']()}</button>
|
||||
<button class="btn btn-accent" onclick={() => flashFrontlight()}>{m['section.control.flashFrontlight']()}</button>
|
||||
<div>
|
||||
<h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-secondary" onclick={() => turnOnFrontlight()}
|
||||
>{m['section.control.turnOn']()}</button
|
||||
>
|
||||
<button class="btn btn-primary" onclick={() => turnOffFrontlight()}
|
||||
>{m['section.control.turnOff']()}</button
|
||||
>
|
||||
<button class="btn btn-accent" onclick={() => flashFrontlight()}
|
||||
>{m['section.control.flashFrontlight']()}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<h3 class="mb-2 font-medium">{m['section.control.title']()}</h3>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
|
||||
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,341 +1,361 @@
|
|||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { CardContainer, Toggle, CollapsibleSection } from '$lib/components';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
let { ...restProps } = $props();
|
||||
|
||||
// Show/hide toggles
|
||||
let showAll = $state(false);
|
||||
let hideAll = $state(false);
|
||||
|
||||
function toggleShowAll() {
|
||||
showAll = true;
|
||||
hideAll = false;
|
||||
}
|
||||
|
||||
function toggleHideAll() {
|
||||
hideAll = true;
|
||||
showAll = false;
|
||||
}
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { CardContainer, Toggle, CollapsibleSection } from '$lib/components';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
let { ...restProps } = $props();
|
||||
|
||||
// Show/hide toggles
|
||||
let showAll = $state(false);
|
||||
let hideAll = $state(false);
|
||||
|
||||
function toggleShowAll() {
|
||||
showAll = true;
|
||||
hideAll = false;
|
||||
}
|
||||
|
||||
function toggleHideAll() {
|
||||
hideAll = true;
|
||||
showAll = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardContainer title={m["section.settings.title"]()} {...restProps}>
|
||||
<div class="flex justify-end gap-2 mb-4">
|
||||
<button class="btn btn-sm" onclick={toggleShowAll}>{m["section.settings.showAll"]()}</button>
|
||||
<button class="btn btn-sm" onclick={toggleHideAll}>{m["section.settings.hideAll"]()}</button>
|
||||
</div>
|
||||
<CardContainer title={m['section.settings.title']()} {...restProps}>
|
||||
<div class="mb-4 flex justify-end gap-2">
|
||||
<button class="btn btn-sm" onclick={toggleShowAll}>{m['section.settings.showAll']()}</button>
|
||||
<button class="btn btn-sm" onclick={toggleHideAll}>{m['section.settings.hideAll']()}</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<CollapsibleSection
|
||||
title={m['section.settings.section.screenSettings']()}
|
||||
open={showAll || !hideAll}
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m['section.settings.StealFocusOnNewBlock']()}
|
||||
bind:checked={$settings.stealFocus}
|
||||
/>
|
||||
<p class="text-xs">
|
||||
When a new block is mined, it will switch focus from the current screen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.section.screenSettings"]()} open={showAll || !hideAll}>
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m["section.settings.StealFocusOnNewBlock"]()}
|
||||
bind:checked={$settings.stealFocus}
|
||||
/>
|
||||
<p class="text-xs">When a new block is mined, it will switch focus from the current screen.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m["section.settings.useBigCharsMcap"]()}
|
||||
bind:checked={$settings.mcapBigChar}
|
||||
/>
|
||||
<p class="text-xs">Use big characters for the market cap screen instead of using a suffix.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m["section.settings.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>
|
||||
<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>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.screens"]()} open={showAll || !hideAll}>
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
{#each $settings.screens as screen}
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={screen.name}
|
||||
checked={screen.enabled}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<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>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.currencies"]()} open={showAll || !hideAll}>
|
||||
<div class="alert alert-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<span>restart required</span>
|
||||
</div>
|
||||
<div class="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="grid gap-4 grid-cols-2">
|
||||
|
||||
{#each $settings.actCurrencies as currency}
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={currency}
|
||||
checked={$settings.actCurrencies.includes(currency)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<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>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.section.displaysAndLed"]()} open={showAll || !hideAll}>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.textColor"]()}</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full">
|
||||
<option>White on Black</option>
|
||||
<option>Black on White</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Font</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full">
|
||||
<option>Oswald</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.timePerScreen"]()}</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" class="input input-bordered w-20" min="1" max="60" value="1" />
|
||||
<span>{m["time.minutes"]()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control flex justify-between">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.fullRefreshEvery"]()}</span>
|
||||
</label>
|
||||
<div class="w-auto input">
|
||||
<input type="number" class="" min="1" max="60" value="60" />
|
||||
<span class="label">{m["time.minutes"]()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control flex justify-between">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.timeBetweenPriceUpdates"]()}</span>
|
||||
</label>
|
||||
<div class="w-auto input">
|
||||
<input type="number" class="" min="1" max="60" value="30" />
|
||||
<span class="label">{m["time.seconds"]()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.ledBrightness"]()}</span>
|
||||
</label>
|
||||
<input type="range" min="0" max="100" class="range" value="50" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m["section.settings.ledPowerOnTest"]()}
|
||||
checked={$settings.ledTestOnPower}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m["section.settings.ledFlashOnBlock"]()}
|
||||
checked={$settings.ledFlashOnUpd}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m["section.settings.disableLeds"]()}
|
||||
checked={$settings.disableLeds}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<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>
|
||||
|
||||
{#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.screens']()} open={showAll || !hideAll}>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{#each $settings.screens as screen (screen.id)}
|
||||
<div class="form-control">
|
||||
<Toggle label={screen.name} checked={screen.enabled} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.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.currencies']()} open={showAll || !hideAll}>
|
||||
<div class="alert alert-warning">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/></svg
|
||||
>
|
||||
<span>restart required</span>
|
||||
</div>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.section.extraFeatures"]()} open={showAll || !hideAll}>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m["section.settings.timeBasedDnd"]()}
|
||||
checked={$settings.dnd.enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{#each $settings.actCurrencies as currency (currency)}
|
||||
<div class="form-control">
|
||||
<Toggle label={currency} checked={$settings.actCurrencies.includes(currency)} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.section.system"]()} open={showAll || !hideAll}>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.timezoneOffset"]()}</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<select class="select select-bordered w-full">
|
||||
<option>Europe/Amsterdam</option>
|
||||
</select>
|
||||
<button class="btn">{m["auto-detect"]()}</button>
|
||||
</div>
|
||||
<p class="text-sm mt-1">{m["section.settings.tzOffsetHelpText"]()}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.hostnamePrefix"]()}</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" value="btclock" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.wpTimeout"]()}</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" class="input input-bordered w-20" min="1" max="900" value="600" />
|
||||
<span>{m["time.seconds"]()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
title={m['section.settings.section.displaysAndLed']()}
|
||||
open={showAll || !hideAll}
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m['section.settings.textColor']()}</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full">
|
||||
<option>White on Black</option>
|
||||
<option>Black on White</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex justify-between mt-6">
|
||||
<button class="btn btn-error">{m["button.reset"]()}</button>
|
||||
<button class="btn btn-primary">{m["button.save"]()}</button>
|
||||
</div>
|
||||
</CardContainer>
|
||||
<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="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 { CardContainer, TabButton, CurrencyButton, Stat, Status } from '$lib/components';
|
||||
import { status, settings } from '$lib/stores';
|
||||
import { setActiveScreen, setActiveCurrency } from '$lib/clockControl';
|
||||
import { setActiveScreen, setActiveCurrency } from '$lib/clockControl';
|
||||
import BTClock from '../BTClock.svelte';
|
||||
import { DataSourceType } from '$lib/types';
|
||||
import { toUptimestring } from '$lib/utils';
|
||||
|
||||
const screens = $settings.screens.map(screen => ({
|
||||
id: screen.id,
|
||||
label: screen.name
|
||||
}));
|
||||
const screens = $settings.screens.map((screen) => ({
|
||||
id: screen.id,
|
||||
label: screen.name
|
||||
}));
|
||||
</script>
|
||||
|
||||
<CardContainer title={m['section.status.title']()}>
|
||||
<div class="space-y-4 mx-auto">
|
||||
<div class="mx-auto space-y-4">
|
||||
<div class="join">
|
||||
{#each screens as screen}
|
||||
<TabButton active={$status.currentScreen === screen.id} onClick={() => setActiveScreen(screen.id)}>
|
||||
{#each screens as screen (screen.id)}
|
||||
<TabButton
|
||||
active={$status.currentScreen === screen.id}
|
||||
onClick={() => setActiveScreen(screen.id)}
|
||||
>
|
||||
{screen.label}
|
||||
</TabButton>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="join flex justify-center">
|
||||
{#each $settings.actCurrencies as currency}
|
||||
<CurrencyButton
|
||||
currency={currency}
|
||||
active={$status.currency === currency}
|
||||
onClick={() => setActiveCurrency(currency)}
|
||||
/>
|
||||
{/each}
|
||||
{#each $settings.actCurrencies as currency (currency)}
|
||||
<CurrencyButton
|
||||
{currency}
|
||||
active={$status.currency === currency}
|
||||
onClick={() => setActiveCurrency(currency)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-center">
|
||||
<div class="w-3/4">
|
||||
<!-- Bitcoin value display showing blocks/price -->
|
||||
<BTClock displays={$status.data} verticalDesc={$settings.verticalDesc} />
|
||||
{$settings.verticalDesc}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-gray-500">
|
||||
{m['section.status.screenCycle']()}: is {$status.timerRunning ? 'running' : 'stopped'}<br />
|
||||
{m['section.status.doNotDisturb']()}: {$status.dnd.enabled ? m['on']() : m['off']()}
|
||||
{m['section.status.doNotDisturb']()}: {$status.dnd.enabled ? m['on']() : m['off']()}
|
||||
<small>
|
||||
{#if $status.dnd?.timeBasedEnabled}
|
||||
{m['section.status.timeBasedDnd']()} ( {$settings.dnd
|
||||
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings
|
||||
.dnd.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
|
||||
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings.dnd
|
||||
.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
|
||||
{/if}
|
||||
</small>
|
||||
</div>
|
||||
|
@ -58,18 +59,28 @@
|
|||
<Status text="Nostr Relay connection" status={$status.nostr ? 'online' : 'offline'} />
|
||||
{/if}
|
||||
{#if $settings.dataSource === DataSourceType.THIRD_PARTY_SOURCE}
|
||||
<Status text={m['section.status.wsPriceConnection']()} status={$status.connectionStatus.price ? 'online' : 'offline'} />
|
||||
<Status text={m['section.status.wsMempoolConnection']({ instance: $settings.mempoolInstance })} status={$status.connectionStatus.blocks ? 'online' : 'offline'} />
|
||||
<Status
|
||||
text={m['section.status.wsPriceConnection']()}
|
||||
status={$status.connectionStatus.price ? 'online' : 'offline'}
|
||||
/>
|
||||
<Status
|
||||
text={m['section.status.wsMempoolConnection']({ instance: $settings.mempoolInstance })}
|
||||
status={$status.connectionStatus.blocks ? 'online' : 'offline'}
|
||||
/>
|
||||
{:else}
|
||||
<Status text={m['section.status.wsDataConnection']()} status={$status.connectionStatus.V2 ? 'online' : 'offline'} />
|
||||
<Status
|
||||
text={m['section.status.wsDataConnection']()}
|
||||
status={$status.connectionStatus.V2 ? 'online' : 'offline'}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="flex justify-center stats shadow mt-4">
|
||||
<Stat title={m['section.status.memoryFree']()} value={`${Math.round($status.espFreeHeap / 1024)} / ${Math.round($status.espHeapSize / 1024)} KiB`} />
|
||||
<Stat title={m['section.status.wifiSignalStrength']()} value={`${$status.rssi} dBm`} />
|
||||
<Stat title={m['section.status.uptime']()} value={`${toUptimestring($status.espUptime)}`} />
|
||||
</div>
|
||||
<div class="stats mt-4 flex justify-center shadow">
|
||||
<Stat
|
||||
title={m['section.status.memoryFree']()}
|
||||
value={`${Math.round($status.espFreeHeap / 1024)} / ${Math.round($status.espHeapSize / 1024)} KiB`}
|
||||
/>
|
||||
<Stat title={m['section.status.wifiSignalStrength']()} value={`${$status.rssi} dBm`} />
|
||||
<Stat title={m['section.status.uptime']()} value={`${toUptimestring($status.espUptime)}`} />
|
||||
</div>
|
||||
</CardContainer>
|
||||
|
|
|
@ -1,102 +1,99 @@
|
|||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { CardContainer } from '$lib/components';
|
||||
import { settings } from '$lib/stores';
|
||||
import {
|
||||
restartClock,
|
||||
forceFullRefresh
|
||||
} from '$lib/clockControl';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { CardContainer } from '$lib/components';
|
||||
import { settings } from '$lib/stores';
|
||||
import { restartClock, forceFullRefresh } from '$lib/clockControl';
|
||||
</script>
|
||||
|
||||
<CardContainer title="System Information">
|
||||
<div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-sm table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">System info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-sm table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">System info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{m['section.control.version']()}</td>
|
||||
<td>{$settings.gitTag}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.buildTime']()}</td>
|
||||
<td>{$settings.lastBuildTime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP</td>
|
||||
<td>{$settings.ip}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HW revision</td>
|
||||
<td>{$settings.hwRev}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.fwCommit']()}</td>
|
||||
<td class="text-xs">{$settings.gitRev}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.hostname']()}</td>
|
||||
<td>{$settings.hostname}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
|
||||
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
|
||||
</div>
|
||||
</div>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{m['section.control.version']()}</td>
|
||||
<td>{$settings.gitTag}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.buildTime']()}</td>
|
||||
<td>{$settings.lastBuildTime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP</td>
|
||||
<td>{$settings.ip}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HW revision</td>
|
||||
<td>{$settings.hwRev}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.fwCommit']()}</td>
|
||||
<td class="text-xs">{$settings.gitRev}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.hostname']()}</td>
|
||||
<td>{$settings.hostname}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
|
||||
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContainer>
|
||||
|
||||
<CardContainer title={m['section.control.firmwareUpdate']()} className="mt-4">
|
||||
<div>
|
||||
<p class="mb-2 text-sm">
|
||||
Latest Version: 3.3.5 - Release Date: 5/2/2025, 12:37:14 AM - <a
|
||||
href="#"
|
||||
class="link link-primary">{m['section.firmwareUpdater.viewRelease']()}</a
|
||||
>
|
||||
</p>
|
||||
<p class="text-success mb-4 text-sm">{m['section.firmwareUpdater.swUpToDate']()}</p>
|
||||
<div>
|
||||
<p class="mb-2 text-sm">
|
||||
Latest Version: 3.3.5 - Release Date: 5/2/2025, 12:37:14 AM - <a
|
||||
href="#"
|
||||
class="link link-primary">{m['section.firmwareUpdater.viewRelease']()}</a
|
||||
>
|
||||
</p>
|
||||
<p class="text-success mb-4 text-sm">{m['section.firmwareUpdater.swUpToDate']()}</p>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label" for="firmwareFile">
|
||||
<span class="label-text">Firmware File (blib_s3_mini_213epd_firmware.bin)</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="file" class="file-input file-input-bordered w-full" id="firmwareFile" />
|
||||
<button class="btn btn-primary">{m['section.control.firmwareUpdate']()}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control mb-4">
|
||||
<label class="label" for="firmwareFile">
|
||||
<span class="label-text">Firmware File (blib_s3_mini_213epd_firmware.bin)</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="file" class="file-input file-input-bordered w-full" id="firmwareFile" />
|
||||
<button class="btn btn-primary">{m['section.control.firmwareUpdate']()}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="webuiFile">
|
||||
<span class="label-text">WebUI File (littlefs_4MB.bin)</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="file" class="file-input file-input-bordered w-full" id="webuiFile" />
|
||||
<button class="btn btn-primary">Update WebUI</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="webuiFile">
|
||||
<span class="label-text">WebUI File (littlefs_4MB.bin)</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="file" class="file-input file-input-bordered w-full" id="webuiFile" />
|
||||
<button class="btn btn-primary">Update WebUI</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/></svg
|
||||
>
|
||||
<span>{m['section.firmwareUpdater.firmwareUpdateText']()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContainer>
|
||||
<div class="alert alert-warning mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/></svg
|
||||
>
|
||||
<span>{m['section.firmwareUpdater.firmwareUpdateText']()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContainer>
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
title,
|
||||
className = "",
|
||||
...restProps
|
||||
} = $props();
|
||||
let { title, className = '', ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl w-full {className}" {...restProps}>
|
||||
<div class="card-body">
|
||||
{#if title}
|
||||
<h2 class="card-title">{title}</h2>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 w-full shadow-xl {className}" {...restProps}>
|
||||
<div class="card-body">
|
||||
{#if title}
|
||||
<h2 class="card-title">{title}</h2>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
title,
|
||||
value,
|
||||
desc = "",
|
||||
icon = "",
|
||||
className = "",
|
||||
...restProps
|
||||
} = $props();
|
||||
let { title, value, desc = '', icon = '', className = '', ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<div class="stat {className}" {...restProps}>
|
||||
{#if icon}
|
||||
<div class="stat-figure text-primary">
|
||||
{@html icon}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="stat-title">{title}</div>
|
||||
<div class="stat-value">{value}</div>
|
||||
{#if desc}
|
||||
<div class="stat-desc">{desc}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if icon}
|
||||
<div class="stat-figure text-primary">
|
||||
{@html icon}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="stat-title">{title}</div>
|
||||
<div class="stat-value">{value}</div>
|
||||
{#if desc}
|
||||
<div class="stat-desc">{desc}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,33 +1,35 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
status = "offline" as "online" | "offline" | "error" | "warning",
|
||||
text,
|
||||
className = "",
|
||||
...restProps
|
||||
} = $props();
|
||||
let {
|
||||
status = 'offline' as 'online' | 'offline' | 'error' | 'warning',
|
||||
text,
|
||||
className = '',
|
||||
...restProps
|
||||
} = $props();
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case "online":
|
||||
return "bg-success";
|
||||
case "offline":
|
||||
return "bg-base-300";
|
||||
case "error":
|
||||
return "bg-error";
|
||||
case "warning":
|
||||
return "bg-warning";
|
||||
default:
|
||||
return "bg-base-300";
|
||||
}
|
||||
};
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return 'bg-success';
|
||||
case 'offline':
|
||||
return 'bg-base-300';
|
||||
case 'error':
|
||||
return 'bg-error';
|
||||
case 'warning':
|
||||
return 'bg-warning';
|
||||
default:
|
||||
return 'bg-base-300';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2 {className}" {...restProps}>
|
||||
<div class="relative flex">
|
||||
<div class="{getStatusColor()} h-3 w-3 rounded-full"></div>
|
||||
{#if status === "online"}
|
||||
<div class="{getStatusColor()} animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-sm">{text}</span>
|
||||
</div>
|
||||
<div class="relative flex">
|
||||
<div class="{getStatusColor()} h-3 w-3 rounded-full"></div>
|
||||
{#if status === 'online'}
|
||||
<div
|
||||
class="{getStatusColor()} absolute inline-flex h-3 w-3 animate-ping rounded-full opacity-75"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-sm">{text}</span>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
active = false,
|
||||
onClick,
|
||||
...restProps
|
||||
} = $props();
|
||||
let { active = false, onClick, ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="btn btn-sm join-item {active ? 'btn-primary' : 'btn-outline'}"
|
||||
on:click={onClick}
|
||||
{...restProps}
|
||||
<button
|
||||
class="btn btn-sm join-item {active ? 'btn-primary' : 'btn-outline'}"
|
||||
on:click={onClick}
|
||||
{...restProps}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
<slot />
|
||||
</button>
|
||||
|
|
|
@ -1,71 +1,86 @@
|
|||
<script lang="ts">
|
||||
type Position =
|
||||
| 'top-start' | 'top-center' | 'top-end'
|
||||
| 'middle-start' | 'middle-center' | 'middle-end'
|
||||
| 'bottom-start' | 'bottom-center' | 'bottom-end';
|
||||
|
||||
type AlertType = 'info' | 'success' | 'warning' | 'error';
|
||||
type Position =
|
||||
| 'top-start'
|
||||
| 'top-center'
|
||||
| 'top-end'
|
||||
| 'middle-start'
|
||||
| 'middle-center'
|
||||
| 'middle-end'
|
||||
| 'bottom-start'
|
||||
| 'bottom-center'
|
||||
| 'bottom-end';
|
||||
|
||||
let props = $props();
|
||||
|
||||
let message = props.message || '';
|
||||
let type = (props.type as AlertType) || 'info';
|
||||
let position = (props.position as Position) || 'bottom-end';
|
||||
let duration = props.duration || 3000;
|
||||
let showClose = props.showClose || false;
|
||||
|
||||
// Create a new object without the known props
|
||||
let restProps = { ...props };
|
||||
delete restProps.message;
|
||||
delete restProps.type;
|
||||
delete restProps.position;
|
||||
delete restProps.duration;
|
||||
delete restProps.showClose;
|
||||
type AlertType = 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
let visible = $state(true);
|
||||
let props = $props();
|
||||
|
||||
// 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;
|
||||
};
|
||||
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;
|
||||
|
||||
// Auto-hide the toast after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
visible = false;
|
||||
}, duration);
|
||||
}
|
||||
// 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;
|
||||
|
||||
const closeToast = () => {
|
||||
visible = false;
|
||||
};
|
||||
let visible = $state(true);
|
||||
|
||||
// Map position prop to DaisyUI classes
|
||||
const getPositionClasses = (pos: Position): string => {
|
||||
const [vertical, horizontal] = pos.split('-');
|
||||
|
||||
let classes = 'toast';
|
||||
|
||||
// Vertical position
|
||||
if (vertical === 'top') classes += ' toast-top';
|
||||
if (vertical === 'middle') classes += ' toast-middle';
|
||||
// bottom is default, no class needed
|
||||
|
||||
// Horizontal position
|
||||
if (horizontal === 'start') classes += ' toast-start';
|
||||
if (horizontal === 'center') classes += ' toast-center';
|
||||
if (horizontal === 'end') classes += ' toast-end';
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
// Auto-hide the toast after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
visible = false;
|
||||
}, duration);
|
||||
}
|
||||
|
||||
const closeToast = () => {
|
||||
visible = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div class={getPositionClasses(position)} {...restProps}>
|
||||
<div class="alert alert-{type}">
|
||||
<span>{message}</span>
|
||||
{#if showClose}
|
||||
<button class="btn btn-circle btn-xs" onclick={closeToast}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class={getPositionClasses(position)} {...restProps}>
|
||||
<div class="alert alert-{type}">
|
||||
<span>{message}</span>
|
||||
{#if showClose}
|
||||
<button class="btn btn-circle btn-xs" onclick={closeToast}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
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 { 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
|
||||
const defaultSettings: Settings = {
|
||||
numScreens: 7,
|
||||
invertedColor: false,
|
||||
timerSeconds: 60,
|
||||
timerRunning: false,
|
||||
minSecPriceUpd: 30,
|
||||
fullRefreshMin: 60,
|
||||
wpTimeout: 600,
|
||||
tzString: "UTC",
|
||||
dataSource: 0,
|
||||
mempoolInstance: "mempool.space",
|
||||
mempoolSecure: true,
|
||||
localPoolEndpoint: "localhost:2019",
|
||||
nostrPubKey: "",
|
||||
nostrRelay: "wss://relay.damus.io",
|
||||
nostrZapNotify: false,
|
||||
nostrZapPubkey: "",
|
||||
ledFlashOnZap: true,
|
||||
fontName: "oswald",
|
||||
availableFonts: ["antonio", "oswald"],
|
||||
customEndpoint: "ws-staging.btclock.dev",
|
||||
customEndpointDisableSSL: false,
|
||||
ledTestOnPower: true,
|
||||
ledFlashOnUpd: true,
|
||||
ledBrightness: 255,
|
||||
stealFocus: false,
|
||||
mcapBigChar: true,
|
||||
mdnsEnabled: true,
|
||||
otaEnabled: true,
|
||||
useSatsSymbol: true,
|
||||
useBlkCountdown: true,
|
||||
suffixPrice: false,
|
||||
disableLeds: false,
|
||||
mowMode: false,
|
||||
verticalDesc: true,
|
||||
suffixShareDot: false,
|
||||
enableDebugLog: false,
|
||||
hostnamePrefix: "btclock",
|
||||
hostname: "btclock",
|
||||
ip: "",
|
||||
txPower: 80,
|
||||
gitReleaseUrl: "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest",
|
||||
bitaxeEnabled: false,
|
||||
bitaxeHostname: "bitaxe1",
|
||||
miningPoolStats: false,
|
||||
miningPoolName: "noderunners",
|
||||
miningPoolUser: "",
|
||||
availablePools: [
|
||||
"ocean",
|
||||
"noderunners",
|
||||
"satoshi_radio",
|
||||
"braiins",
|
||||
"public_pool",
|
||||
"local_public_pool",
|
||||
"gobrrr_pool",
|
||||
"ckpool",
|
||||
"eu_ckpool"
|
||||
],
|
||||
httpAuthEnabled: false,
|
||||
httpAuthUser: "btclock",
|
||||
httpAuthPass: "satoshi",
|
||||
hasFrontlight: false,
|
||||
// Default frontlight settings
|
||||
flDisable: false,
|
||||
flMaxBrightness: 2684,
|
||||
flAlwaysOn: false,
|
||||
flEffectDelay: 50,
|
||||
flFlashOnUpd: true,
|
||||
flFlashOnZap: true,
|
||||
// Default light sensor settings
|
||||
hasLightLevel: false,
|
||||
luxLightToggle: 128,
|
||||
flOffWhenDark: false,
|
||||
hwRev: "",
|
||||
fsRev: "",
|
||||
gitRev: "",
|
||||
gitTag: "",
|
||||
lastBuildTime: "",
|
||||
screens: [
|
||||
{id: 0, name: "Block Height", enabled: true},
|
||||
{id: 3, name: "Time", enabled: false},
|
||||
{id: 4, name: "Halving countdown", enabled: false},
|
||||
{id: 6, name: "Block Fee Rate", enabled: false},
|
||||
{id: 10, name: "Sats per dollar", enabled: true},
|
||||
{id: 20, name: "Ticker", enabled: true},
|
||||
{id: 30, name: "Market Cap", enabled: false}
|
||||
],
|
||||
actCurrencies: ["USD"],
|
||||
availableCurrencies: ["USD", "EUR", "GBP", "JPY", "AUD", "CAD"],
|
||||
poolLogosUrl: "https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main",
|
||||
ceEndpoint: "ws-staging.btclock.dev",
|
||||
ceDisableSSL: false,
|
||||
dnd: {
|
||||
enabled: false,
|
||||
timeBasedEnabled: false,
|
||||
startHour: 23,
|
||||
startMinute: 0,
|
||||
endHour: 7,
|
||||
endMinute: 0
|
||||
}
|
||||
numScreens: 7,
|
||||
invertedColor: false,
|
||||
timerSeconds: 60,
|
||||
timerRunning: false,
|
||||
minSecPriceUpd: 30,
|
||||
fullRefreshMin: 60,
|
||||
wpTimeout: 600,
|
||||
tzString: 'UTC',
|
||||
dataSource: 0,
|
||||
mempoolInstance: 'mempool.space',
|
||||
mempoolSecure: true,
|
||||
localPoolEndpoint: 'localhost:2019',
|
||||
nostrPubKey: '',
|
||||
nostrRelay: 'wss://relay.damus.io',
|
||||
nostrZapNotify: false,
|
||||
nostrZapPubkey: '',
|
||||
ledFlashOnZap: true,
|
||||
fontName: 'oswald',
|
||||
availableFonts: ['antonio', 'oswald'],
|
||||
customEndpoint: 'ws-staging.btclock.dev',
|
||||
customEndpointDisableSSL: false,
|
||||
ledTestOnPower: true,
|
||||
ledFlashOnUpd: true,
|
||||
ledBrightness: 255,
|
||||
stealFocus: false,
|
||||
mcapBigChar: true,
|
||||
mdnsEnabled: true,
|
||||
otaEnabled: true,
|
||||
useSatsSymbol: true,
|
||||
useBlkCountdown: true,
|
||||
suffixPrice: false,
|
||||
disableLeds: false,
|
||||
mowMode: false,
|
||||
verticalDesc: true,
|
||||
suffixShareDot: false,
|
||||
enableDebugLog: false,
|
||||
hostnamePrefix: 'btclock',
|
||||
hostname: 'btclock',
|
||||
ip: '',
|
||||
txPower: 80,
|
||||
gitReleaseUrl: 'https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest',
|
||||
bitaxeEnabled: false,
|
||||
bitaxeHostname: 'bitaxe1',
|
||||
miningPoolStats: false,
|
||||
miningPoolName: 'noderunners',
|
||||
miningPoolUser: '',
|
||||
availablePools: [
|
||||
'ocean',
|
||||
'noderunners',
|
||||
'satoshi_radio',
|
||||
'braiins',
|
||||
'public_pool',
|
||||
'local_public_pool',
|
||||
'gobrrr_pool',
|
||||
'ckpool',
|
||||
'eu_ckpool'
|
||||
],
|
||||
httpAuthEnabled: false,
|
||||
httpAuthUser: 'btclock',
|
||||
httpAuthPass: 'satoshi',
|
||||
hasFrontlight: false,
|
||||
// Default frontlight settings
|
||||
flDisable: false,
|
||||
flMaxBrightness: 2684,
|
||||
flAlwaysOn: false,
|
||||
flEffectDelay: 50,
|
||||
flFlashOnUpd: true,
|
||||
flFlashOnZap: true,
|
||||
// Default light sensor settings
|
||||
hasLightLevel: false,
|
||||
luxLightToggle: 128,
|
||||
flOffWhenDark: false,
|
||||
hwRev: '',
|
||||
fsRev: '',
|
||||
gitRev: '',
|
||||
gitTag: '',
|
||||
lastBuildTime: '',
|
||||
screens: [
|
||||
{ id: 0, name: 'Block Height', enabled: true },
|
||||
{ id: 3, name: 'Time', enabled: false },
|
||||
{ id: 4, name: 'Halving countdown', enabled: false },
|
||||
{ id: 6, name: 'Block Fee Rate', enabled: false },
|
||||
{ id: 10, name: 'Sats per dollar', enabled: true },
|
||||
{ id: 20, name: 'Ticker', enabled: true },
|
||||
{ id: 30, name: 'Market Cap', enabled: false }
|
||||
],
|
||||
actCurrencies: ['USD'],
|
||||
availableCurrencies: ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD'],
|
||||
poolLogosUrl: 'https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main',
|
||||
ceEndpoint: 'ws-staging.btclock.dev',
|
||||
ceDisableSSL: false,
|
||||
dnd: {
|
||||
enabled: false,
|
||||
timeBasedEnabled: false,
|
||||
startHour: 23,
|
||||
startMinute: 0,
|
||||
endHour: 7,
|
||||
endMinute: 0
|
||||
}
|
||||
};
|
||||
|
||||
// Create the Svelte store
|
||||
function createSettingsStore() {
|
||||
const { subscribe, set, update } = writable<Settings>(defaultSettings);
|
||||
const { subscribe, set, update } = writable<Settings>(defaultSettings);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
fetch: async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/settings`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching settings: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
set(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
return defaultSettings;
|
||||
}
|
||||
},
|
||||
update: async (newSettings: Partial<Settings>) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/settings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newSettings),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error updating settings: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Update the local store with the new settings
|
||||
update(currentSettings => ({ ...currentSettings, ...newSettings }));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update settings:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
set: (newSettings: Settings) => set(newSettings),
|
||||
reset: () => set(defaultSettings)
|
||||
};
|
||||
return {
|
||||
subscribe,
|
||||
fetch: async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/settings`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching settings: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
set(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
return defaultSettings;
|
||||
}
|
||||
},
|
||||
update: async (newSettings: Partial<Settings>) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/settings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(newSettings)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error updating settings: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Update the local store with the new settings
|
||||
update((currentSettings) => ({ ...currentSettings, ...newSettings }));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update settings:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
set: (newSettings: Settings) => set(newSettings),
|
||||
reset: () => set(defaultSettings)
|
||||
};
|
||||
}
|
||||
|
||||
export const settings = createSettingsStore();
|
||||
|
||||
// Initialize the store by fetching settings when this module is first imported
|
||||
if (typeof window !== 'undefined') {
|
||||
settings.fetch();
|
||||
}
|
||||
settings.fetch();
|
||||
}
|
||||
|
|
|
@ -3,178 +3,178 @@ import { baseUrl } from '$lib/env';
|
|||
import type { Status } from '$lib/types';
|
||||
// Create a default status object
|
||||
const defaultStatus: Status = {
|
||||
currentScreen: 0,
|
||||
numScreens: 0,
|
||||
timerRunning: false,
|
||||
isOTAUpdating: false,
|
||||
espUptime: 0,
|
||||
espFreeHeap: 0,
|
||||
espHeapSize: 0,
|
||||
connectionStatus: {
|
||||
price: false,
|
||||
blocks: false,
|
||||
V2: false,
|
||||
nostr: false
|
||||
},
|
||||
rssi: 0,
|
||||
currency: "USD",
|
||||
dnd: {
|
||||
enabled: false,
|
||||
timeBasedEnabled: false,
|
||||
startTime: "00:00",
|
||||
endTime: "00:00",
|
||||
active: false
|
||||
},
|
||||
data: [],
|
||||
leds: [
|
||||
{red: 0, green: 0, blue: 0, hex: "#000000"},
|
||||
{red: 0, green: 0, blue: 0, hex: "#000000"},
|
||||
{red: 0, green: 0, blue: 0, hex: "#000000"},
|
||||
{red: 0, green: 0, blue: 0, hex: "#000000"}
|
||||
]
|
||||
currentScreen: 0,
|
||||
numScreens: 0,
|
||||
timerRunning: false,
|
||||
isOTAUpdating: false,
|
||||
espUptime: 0,
|
||||
espFreeHeap: 0,
|
||||
espHeapSize: 0,
|
||||
connectionStatus: {
|
||||
price: false,
|
||||
blocks: false,
|
||||
V2: false,
|
||||
nostr: false
|
||||
},
|
||||
rssi: 0,
|
||||
currency: 'USD',
|
||||
dnd: {
|
||||
enabled: false,
|
||||
timeBasedEnabled: false,
|
||||
startTime: '00:00',
|
||||
endTime: '00:00',
|
||||
active: false
|
||||
},
|
||||
data: [],
|
||||
leds: [
|
||||
{ red: 0, green: 0, blue: 0, hex: '#000000' },
|
||||
{ red: 0, green: 0, blue: 0, hex: '#000000' },
|
||||
{ red: 0, green: 0, blue: 0, hex: '#000000' },
|
||||
{ red: 0, green: 0, blue: 0, hex: '#000000' }
|
||||
]
|
||||
};
|
||||
|
||||
// Create the Svelte store
|
||||
function createStatusStore() {
|
||||
const { subscribe, set, update } = writable<Status>(defaultStatus);
|
||||
let eventSource: EventSource | null = null;
|
||||
const { subscribe, set, update } = writable<Status>(defaultStatus);
|
||||
let eventSource: EventSource | null = null;
|
||||
|
||||
// Clean up function to close SSE connection
|
||||
const cleanup = () => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
};
|
||||
// Clean up function to close SSE connection
|
||||
const cleanup = () => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Create the store object with methods
|
||||
const store = {
|
||||
subscribe,
|
||||
fetch: async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching status: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
set(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch status:', error);
|
||||
return defaultStatus;
|
||||
}
|
||||
},
|
||||
startListening: () => {
|
||||
// Clean up any existing connections first
|
||||
cleanup();
|
||||
|
||||
// Only run in the browser, not during SSR
|
||||
if (typeof window === 'undefined') return;
|
||||
// Create the store object with methods
|
||||
const store = {
|
||||
subscribe,
|
||||
fetch: async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching status: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
set(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch status:', error);
|
||||
return defaultStatus;
|
||||
}
|
||||
},
|
||||
startListening: () => {
|
||||
// Clean up any existing connections first
|
||||
cleanup();
|
||||
|
||||
try {
|
||||
// Create a new EventSource connection
|
||||
eventSource = new EventSource(`${baseUrl}/events`);
|
||||
// Only run in the browser, not during SSR
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Handle status updates
|
||||
eventSource.addEventListener('status', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update(currentStatus => ({ ...currentStatus, ...data }));
|
||||
} catch (error) {
|
||||
console.error('Error processing status event:', error);
|
||||
}
|
||||
});
|
||||
try {
|
||||
// Create a new EventSource connection
|
||||
eventSource = new EventSource(`${baseUrl}/events`);
|
||||
|
||||
// Handle connection status updates
|
||||
eventSource.addEventListener('connection', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update(currentStatus => ({
|
||||
...currentStatus,
|
||||
connectionStatus: {
|
||||
...currentStatus.connectionStatus,
|
||||
...data
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error processing connection event:', error);
|
||||
}
|
||||
});
|
||||
// Handle status updates
|
||||
eventSource.addEventListener('status', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update((currentStatus) => ({ ...currentStatus, ...data }));
|
||||
} catch (error) {
|
||||
console.error('Error processing status event:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle screen updates
|
||||
eventSource.addEventListener('screen', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update(currentStatus => ({
|
||||
...currentStatus,
|
||||
currentScreen: data.screen || currentStatus.currentScreen
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error processing screen event:', error);
|
||||
}
|
||||
});
|
||||
// Handle connection status updates
|
||||
eventSource.addEventListener('connection', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update((currentStatus) => ({
|
||||
...currentStatus,
|
||||
connectionStatus: {
|
||||
...currentStatus.connectionStatus,
|
||||
...data
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error processing connection event:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle generic messages
|
||||
eventSource.onmessage = (event) => {
|
||||
if (event.type === 'message') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update(currentStatus => ({ ...currentStatus, ...data }));
|
||||
} catch (error) {
|
||||
console.error('Error processing message event:', error);
|
||||
}
|
||||
};
|
||||
// Handle screen updates
|
||||
eventSource.addEventListener('screen', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update((currentStatus) => ({
|
||||
...currentStatus,
|
||||
currentScreen: data.screen || currentStatus.currentScreen
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error processing screen event:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('EventSource failed:', error);
|
||||
cleanup();
|
||||
// Attempt to reconnect after a delay
|
||||
setTimeout(() => store.startListening(), 5000);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to setup event source:', error);
|
||||
}
|
||||
},
|
||||
stopListening: cleanup,
|
||||
|
||||
// Function to set the current screen
|
||||
setScreen: async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
// Make the GET request to change the screen
|
||||
const response = await fetch(`${baseUrl}/api/show/screen/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error setting screen: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Update the store with the new screen ID
|
||||
update(currentStatus => ({
|
||||
...currentStatus,
|
||||
currentScreen: id
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to set screen:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return store;
|
||||
// Handle generic messages
|
||||
eventSource.onmessage = (event) => {
|
||||
if (event.type === 'message') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update((currentStatus) => ({ ...currentStatus, ...data }));
|
||||
} catch (error) {
|
||||
console.error('Error processing message event:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle errors
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('EventSource failed:', error);
|
||||
cleanup();
|
||||
// Attempt to reconnect after a delay
|
||||
setTimeout(() => store.startListening(), 5000);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to setup event source:', error);
|
||||
}
|
||||
},
|
||||
stopListening: cleanup,
|
||||
|
||||
// Function to set the current screen
|
||||
setScreen: async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
// Make the GET request to change the screen
|
||||
const response = await fetch(`${baseUrl}/api/show/screen/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error setting screen: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Update the store with the new screen ID
|
||||
update((currentStatus) => ({
|
||||
...currentStatus,
|
||||
currentScreen: id
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to set screen:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
export const status = createStatusStore();
|
||||
|
||||
// Initialize the store by fetching initial status and starting to listen for updates
|
||||
if (typeof window !== 'undefined') {
|
||||
status.fetch().then(() => status.startListening());
|
||||
|
||||
// Clean up the EventSource when the window is unloaded
|
||||
window.addEventListener('beforeunload', () => {
|
||||
status.stopListening();
|
||||
});
|
||||
}
|
||||
status.fetch().then(() => status.startListening());
|
||||
|
||||
// Clean up the EventSource when the window is unloaded
|
||||
window.addEventListener('beforeunload', () => {
|
||||
status.stopListening();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
*/
|
||||
|
||||
export interface LedStatus {
|
||||
hex: string;
|
||||
}
|
||||
hex: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data source types
|
||||
* Data source types
|
||||
*/
|
||||
export enum DataSourceType {
|
||||
BTCLOCK_SOURCE = 0,
|
||||
|
@ -26,31 +26,31 @@ export interface Status {
|
|||
espFreeHeap: number;
|
||||
espHeapSize: number;
|
||||
connectionStatus: {
|
||||
price: boolean;
|
||||
blocks: boolean;
|
||||
V2: boolean;
|
||||
nostr: boolean;
|
||||
price: boolean;
|
||||
blocks: boolean;
|
||||
V2: boolean;
|
||||
nostr: boolean;
|
||||
};
|
||||
rssi: number;
|
||||
currency: string;
|
||||
dnd: {
|
||||
enabled: boolean;
|
||||
timeBasedEnabled: boolean;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
active: boolean;
|
||||
enabled: boolean;
|
||||
timeBasedEnabled: boolean;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
active: boolean;
|
||||
};
|
||||
data: string[];
|
||||
leds: Array<{
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
hex: string;
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
hex: string;
|
||||
}>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// Define the Settings interface based on the API response structure
|
||||
// Define the Settings interface based on the API response structure
|
||||
export interface Settings {
|
||||
numScreens: number;
|
||||
invertedColor: boolean;
|
||||
|
@ -120,9 +120,9 @@ export interface Settings {
|
|||
gitTag: string;
|
||||
lastBuildTime: string;
|
||||
screens: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
actCurrencies: string[];
|
||||
availableCurrencies: string[];
|
||||
|
@ -130,13 +130,12 @@ export interface Settings {
|
|||
ceEndpoint: string;
|
||||
ceDisableSSL: boolean;
|
||||
dnd: {
|
||||
enabled: boolean;
|
||||
timeBasedEnabled: boolean;
|
||||
startHour: number;
|
||||
startMinute: number;
|
||||
endHour: number;
|
||||
endMinute: number;
|
||||
enabled: boolean;
|
||||
timeBasedEnabled: boolean;
|
||||
startHour: number;
|
||||
startMinute: number;
|
||||
endHour: number;
|
||||
endMinute: number;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
export const toTime = (secs: number) => {
|
||||
const hours = Math.floor(secs / (60 * 60));
|
||||
const hours = Math.floor(secs / (60 * 60));
|
||||
|
||||
const divisor_for_minutes = secs % (60 * 60);
|
||||
const minutes = Math.floor(divisor_for_minutes / 60);
|
||||
const divisor_for_minutes = secs % (60 * 60);
|
||||
const minutes = Math.floor(divisor_for_minutes / 60);
|
||||
|
||||
const divisor_for_seconds = divisor_for_minutes % 60;
|
||||
const seconds = Math.ceil(divisor_for_seconds);
|
||||
const divisor_for_seconds = divisor_for_minutes % 60;
|
||||
const seconds = Math.ceil(divisor_for_seconds);
|
||||
|
||||
const obj = {
|
||||
h: hours,
|
||||
m: minutes,
|
||||
s: seconds
|
||||
};
|
||||
return obj;
|
||||
const obj = {
|
||||
h: hours,
|
||||
m: minutes,
|
||||
s: seconds
|
||||
};
|
||||
return obj;
|
||||
};
|
||||
|
||||
export const toUptimestring = (secs: number): string => {
|
||||
const time = toTime(secs);
|
||||
const time = toTime(secs);
|
||||
|
||||
return `${time.h}h ${time.m}m ${time.s}s`;
|
||||
return `${time.h}h ${time.m}m ${time.s}s`;
|
||||
};
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
status.stopListening();
|
||||
}
|
||||
});
|
||||
export const prerender = true;
|
||||
</script>
|
||||
|
||||
<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">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { settings, status } from '$lib/stores';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { ControlSection, StatusSection, SettingsSection } from '$lib/components';
|
||||
|
||||
|
||||
import { ControlSection, StatusSection } from '$lib/components';
|
||||
</script>
|
||||
|
||||
<div class=" mx-auto px-2 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<ControlSection />
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<StatusSection />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<ControlSection />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<StatusSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
let isLoaded = $state(false);
|
||||
let scalarApiReference;
|
||||
let scalarApiReference;
|
||||
|
||||
function initializeScalar() {
|
||||
// @ts-ignore - Scalar is loaded dynamically
|
||||
// @ts-expect-error - Scalar is loaded dynamically
|
||||
if (window.Scalar) {
|
||||
// @ts-ignore - Scalar is loaded dynamically
|
||||
// @ts-expect-error - Scalar is loaded dynamically
|
||||
scalarApiReference = window.Scalar.createApiReference('#app', {
|
||||
url: '/swagger.json',
|
||||
hideDarkModeToggle: true,
|
||||
hideClientButton: true,
|
||||
baseServerURL: baseUrl
|
||||
hideDarkModeToggle: true,
|
||||
hideClientButton: true,
|
||||
baseServerURL: baseUrl
|
||||
});
|
||||
isLoaded = true;
|
||||
} else {
|
||||
|
@ -34,33 +34,33 @@
|
|||
}
|
||||
|
||||
let darkMode = $state(false);
|
||||
let handler: (e: MediaQueryListEvent) => void;
|
||||
let mediaQuery: MediaQueryList;
|
||||
let handler: (e: MediaQueryListEvent) => void;
|
||||
let mediaQuery: MediaQueryList;
|
||||
onMount(() => {
|
||||
loadScalarScript();
|
||||
darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
handler = (e: MediaQueryListEvent) => {
|
||||
darkMode = e.matches;
|
||||
};
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
handler = (e: MediaQueryListEvent) => {
|
||||
darkMode = e.matches;
|
||||
};
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
|
||||
return () => {
|
||||
isLoaded = false;
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (mediaQuery) {
|
||||
mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
if (isLoaded) {
|
||||
document.querySelectorAll('style[data-scalar]').forEach(el => el.remove());
|
||||
onDestroy(() => {
|
||||
if (mediaQuery) {
|
||||
mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
if (isLoaded) {
|
||||
document.querySelectorAll('style[data-scalar]').forEach((el) => el.remove());
|
||||
|
||||
scalarApiReference.destroy();
|
||||
}
|
||||
});
|
||||
scalarApiReference.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { SettingsSection } from '$lib/components';
|
||||
import { SettingsSection } from '$lib/components';
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Settings</h1>
|
||||
<SettingsSection />
|
||||
</div>
|
||||
<h1 class="mb-4 text-2xl font-bold">Settings</h1>
|
||||
<SettingsSection />
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { SystemSection } from '$lib/components';
|
||||
import { SystemSection } from '$lib/components';
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">System Management</h1>
|
||||
<SystemSection />
|
||||
</div>
|
||||
<h1 class="mb-4 text-2xl font-bold">System Management</h1>
|
||||
<SystemSection />
|
||||
</div>
|
||||
|
|
|
@ -3,11 +3,16 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|||
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: { adapter: adapter({
|
||||
fallback: 'index.html',
|
||||
precompress: false,
|
||||
strict: true
|
||||
}) }
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: 'bundle.html',
|
||||
precompress: false,
|
||||
strict: true
|
||||
}),
|
||||
appDir: 'build'
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue