Compare commits

...

46 commits
872399 ... main

Author SHA1 Message Date
Djuri Baars
033fe09829 Add do not disturb mode
All checks were successful
/ build (push) Successful in 3m26s
/ check-changes (push) Successful in 6s
2024-12-30 02:01:58 +01:00
Djuri Baars
0041ec3d9a Create testing specific vite config, add multi font support, bugfixes
All checks were successful
/ check-changes (push) Successful in 18s
/ build (push) Successful in 3m49s
2024-12-30 00:50:33 +01:00
Djuri Baars
48e585d4ec Update badges in README
All checks were successful
/ check-changes (push) Successful in 18s
/ build (push) Has been skipped
2024-12-29 03:58:08 +01:00
Djuri Baars
1fbddd0e8d Dependency updates, clean up shared test data, create screenshot updater for README
All checks were successful
/ build (push) Successful in 4m36s
/ check-changes (push) Successful in 5s
2024-12-29 03:55:30 +01:00
Djuri Baars
6ae7523d63 Currency selector fix
All checks were successful
/ check-changes (push) Successful in 21s
/ build (push) Successful in 3m34s
2024-12-29 01:29:24 +01:00
Djuri Baars
468e105adf Better feedback when auto-updating and improve handling of disconnects and restarts
All checks were successful
/ build (push) Successful in 4m41s
/ check-changes (push) Successful in 7s
2024-12-28 20:45:10 +01:00
Djuri Baars
4057e18755 Split up settings sections, use new datasource selector
All checks were successful
/ check-changes (push) Successful in 18s
/ build (push) Successful in 3m16s
2024-12-28 17:56:48 +01:00
Djuri Baars
2ce53eb499 Show new WebUI filenames
All checks were successful
/ build (push) Successful in 4m30s
/ check-changes (push) Successful in 18s
2024-12-26 23:59:20 +01:00
Djuri Baars
65b6df5d92 Remove axios as it is not needed
Some checks failed
/ check-changes (push) Successful in 15s
/ build (push) Failing after 3m42s
2024-12-26 16:13:15 +01:00
00b40c4d75 Merge pull request 'Cache playwright browsers for workflow' (#16) from feature/workflow-optimize into main
All checks were successful
/ build (push) Successful in 3m51s
/ check-changes (push) Successful in 16s
Reviewed-on: #16
2024-12-26 15:08:35 +00:00
Djuri Baars
5d03f58801 Cache playwright browsers for workflow
All checks were successful
/ build (pull_request) Successful in 3m31s
/ check-changes (pull_request) Successful in 5s
2024-12-26 16:01:02 +01:00
Djuri Baars
b9c08dec64 Remove unused GitHub reporter
All checks were successful
/ build (push) Successful in 3m56s
/ check-changes (push) Successful in 6s
2024-12-26 15:56:55 +01:00
bd6e938335 Merge pull request 'Migrate renovate config' (#14) from renovate/migrate-config into main
Some checks failed
/ check-changes (push) Successful in 14s
/ build (push) Failing after 3m15s
Reviewed-on: #14
2024-12-26 14:45:39 +00:00
1c43c3ef21 Merge branch 'main' into renovate/migrate-config
Some checks failed
/ check-changes (pull_request) Successful in 15s
/ build (pull_request) Has been cancelled
2024-12-26 14:45:29 +00:00
Djuri Baars
20fba40782 Fix workflow
Some checks failed
/ build (push) Has been cancelled
/ check-changes (push) Has been cancelled
2024-12-26 15:45:12 +01:00
9843706066 Merge branch 'main' into renovate/migrate-config
Some checks failed
/ check-changes (pull_request) Successful in 15s
/ build (pull_request) Failing after 3m38s
2024-12-26 14:40:30 +00:00
Djuri Baars
236a2bb4ae Only release on main
All checks were successful
/ build (push) Successful in 3m27s
/ check-changes (push) Successful in 5s
2024-12-26 15:39:47 +01:00
69bc410d97 Merge pull request 'Update actions/forgejo-release action to v2.5.1' (#13) from renovate/actions-forgejo-release-2.x into main
Some checks failed
/ build (push) Has been cancelled
/ check-changes (push) Has been cancelled
Reviewed-on: #13
2024-12-26 14:34:10 +00:00
Ticktock Depbot
a59de5796f Migrate config renovate.json
All checks were successful
/ check-changes (push) Successful in 14s
/ build (push) Successful in 3m59s
2024-12-26 14:29:21 +00:00
Ticktock Depbot
ad142105f3 Update actions/forgejo-release action to v2.5.1
All checks were successful
/ check-changes (push) Successful in 15s
/ build (push) Successful in 3m34s
2024-12-26 14:29:18 +00:00
2fa44b12f6 Merge pull request 'Update renovate configuration' (#12) from renovate/update-config into main
All checks were successful
/ build (push) Has been skipped
/ check-changes (push) Successful in 5s
Reviewed-on: #12
2024-12-26 14:26:52 +00:00
Djuri Baars
46eb763adb Update renovate configuration
All checks were successful
/ check-changes (push) Successful in 6s
/ build (push) Successful in 3m36s
2024-12-26 15:26:03 +01:00
df5e1d8be8 Merge pull request 'Configure Renovate' (#8) from renovate/configure into main
Some checks failed
/ build (push) Failing after 48s
/ check-changes (push) Successful in 6s
Reviewed-on: #8
2024-12-26 14:22:44 +00:00
Ticktock Depbot
4ae1fc794c Add renovate.json
Some checks failed
/ build (push) Failing after 1m6s
/ check-changes (push) Successful in 17s
2024-12-26 14:22:04 +00:00
Djuri Baars
cc538cf643 Dependency updates and convert npub to hex on paste
Some checks failed
/ check-changes (push) Successful in 16s
/ build (push) Has been cancelled
2024-12-26 01:09:35 +01:00
Djuri Baars
924be8fc2e Fix locale-related bugs and test it with screenshots
All checks were successful
/ check-changes (push) Successful in 5s
/ build (push) Successful in 3m20s
2024-12-20 18:57:36 +01:00
Djuri Baars
23529dbd4b Improve project layout
All checks were successful
/ check-changes (push) Successful in 15s
/ build (push) Successful in 3m38s
2024-12-20 18:19:01 +01:00
Djuri Baars
20c81628f1 More vite improvements
All checks were successful
/ check-changes (push) Successful in 13s
/ build (push) Successful in 3m35s
2024-12-20 17:59:52 +01:00
Djuri Baars
25258b43a7 Settings refactor 2024-12-20 17:56:10 +01:00
Djuri Baars
8a9c013f24 Remove old patches
Some checks failed
/ check-changes (push) Successful in 14s
/ build (push) Failing after 3m17s
2024-12-20 17:18:33 +01:00
Djuri Baars
cefa98148a Updates and cleanup
Some checks failed
/ build (push) Failing after 58s
/ check-changes (push) Successful in 5s
2024-12-20 17:16:50 +01:00
Djuri Baars
551d714cce Remove woff(1) assets, show something when mining pool logo is shown
All checks were successful
/ build (push) Successful in 3m35s
/ check-changes (push) Successful in 14s
2024-12-20 15:22:59 +01:00
Djuri Baars
fd328d4f05 Add more mining pools
All checks were successful
/ check-changes (push) Successful in 14s
/ build (push) Successful in 3m37s
2024-12-20 04:03:35 +01:00
Djuri Baars
dfe703d676 Fix capitalization
All checks were successful
/ check-changes (push) Successful in 14s
/ build (push) Successful in 3m31s
2024-12-20 01:23:45 +01:00
Djuri Baars
a00eb54573 Get available pools from device
All checks were successful
/ build (push) Successful in 3m23s
/ check-changes (push) Successful in 5s
2024-12-20 01:20:15 +01:00
711c625648 bugfix for long preferences key 2024-12-18 21:24:50 -06:00
f458417536 Add mining pool stats enable/disable toggle 2024-12-18 21:24:50 -06:00
0c70c74a1a still untested 2024-12-18 21:24:50 -06:00
2bea761d3c work-in-progress, untested 2024-12-18 21:24:50 -06:00
Djuri Baars
85b9b17506 Add alt tag to bitaxe logo
All checks were successful
/ build (push) Successful in 3m22s
/ check-changes (push) Successful in 5s
2024-12-18 01:28:17 +01:00
Djuri Baars
eff18ba0c3 Add bitaxe icon and modify tests for it
Some checks failed
/ check-changes (push) Successful in 5s
/ build (push) Failing after 53s
2024-12-18 01:24:21 +01:00
Djuri Baars
266a99be96 Add vertical screen description option
All checks were successful
/ build (push) Successful in 4m2s
/ check-changes (push) Successful in 5s
2024-12-18 00:45:26 +01:00
Djuri Baars
653a39d0a3 Improvements for xs screens
All checks were successful
/ check-changes (push) Successful in 5s
/ build (push) Successful in 3m57s
2024-12-12 23:04:13 +01:00
Djuri Baars
68c247f3cc Fix screen selector UI, add screenshot maker
All checks were successful
/ check-changes (push) Successful in 7s
/ build (push) Successful in 3m57s
2024-12-12 19:50:36 +01:00
Djuri Baars
25e91b2086 Dependencies update, add switch for frontlight off when dark
All checks were successful
/ check-changes (push) Successful in 14s
/ build (push) Successful in 3m54s
2024-12-10 14:49:44 +01:00
Djuri Baars
f0fa58b5ea Fix LittleFS image generation
All checks were successful
/ check-changes (push) Successful in 6s
/ build (push) Successful in 3m29s
2024-11-29 00:57:07 +01:00
52 changed files with 2912 additions and 1763 deletions

View file

@ -1,4 +1,9 @@
on: [push]
on:
push:
branches:
- main
pull_request:
jobs:
check-changes:
runs-on: docker
@ -42,7 +47,8 @@ jobs:
path: |
~/.cache/pip
~/node_modules
key: ${{ runner.os }}-pio
~/.cache/ms-playwright
key: ${{ runner.os }}-pio-playwright-${{ hashFiles('**/yarn.lock') }}
- uses: actions/setup-python@v5
with:
python-version: '>=3.10'
@ -62,14 +68,19 @@ jobs:
- name: Run vitest tests
run: yarn 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 }}
@ -81,7 +92,7 @@ jobs:
- name: Check GZipped directory size
run: |
# Set the threshold size in bytes
THRESHOLD=419840
THRESHOLD=410000
# Calculate the total size of files in the directory
DIRECTORY_SIZE=$(du -b -s build_gz | awk '{print $1}')
@ -94,12 +105,14 @@ jobs:
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 419840 output/littlefs.bin
/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: |
@ -107,7 +120,7 @@ jobs:
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.4.0
uses: https://code.forgejo.org/actions/forgejo-release@v2.5.1
with:
url: 'https://git.btclock.dev/'
repo: '${{ github.repository }}'

View file

@ -1,10 +1,11 @@
# BTClock WebUI
[![BTClock CI](https://github.com/btclock/webui/actions/workflows/workflow.yml/badge.svg)](https://github.com/btclock/webui2/actions/workflows/workflow.yml)
[![Latest release](https://git.btclock.dev/btclock/webui/badges/release.svg)](https://git.btclock.dev/btclock/webui/releases/latest)
[![BTClock CI](https://git.btclock.dev/btclock/webui/badges/workflows/build.yaml/badge.svg)](https://git.btclock.dev/btclock/webui/actions?workflow=build.yaml&actor=0&status=0)
The web user-interface for the BTClock, based on Svelte-kit. It uses Bootstrap for the lay-out.
![Screenshot](doc/screenshot.webp)
![Screenshot](doc/screenshot-light.webp)
![Screenshot Dark](doc/screenshot-dark.webp)
## Developing
@ -30,7 +31,11 @@ Make sure the postinstall script is ran, because otherwise the filenames are to
## Deploying
To upload the firmware to the BTClock, you need to GZIP all the files. You can use the python script `gzip_build.py` for that.
To upload the firmware to the BTClock, you need to GZIP all the files. You can use the python script `gzip_build.py` for that:
```bash
python3 gzip_build.py
```
Then you can make a `LittleFS.bin` with mklittlefs:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 71 KiB

BIN
doc/screenshot-light.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

View file

@ -5,14 +5,17 @@
"scripts": {
"dev": "vite dev",
"build": "vite build",
"build:test": "vite build --config vite.config.test.ts",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"postinstall": "patch-package",
"test": "npm run test:integration && npm run test:unit",
"test": "prettier --write . && eslint . && npm run test:integration && npm run test:unit",
"test:integration": "playwright test",
"test:screenshots": "playwright test -c playwright.screenshot.config.ts",
"doc:update-screenshots": "playwright test -c playwright.doc-screenshot.config.ts",
"test:unit": "vitest"
},
"devDependencies": {
@ -32,7 +35,9 @@
"jsdom": "^25.0.0",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.79.3",
"sharp": "^0.33.5",
"svelte": "^4.2.19",
"svelte-check": "^4.0.2",
"svelte-preprocess": "^6.0.2",
@ -40,8 +45,7 @@
"typescript": "^5.5.4",
"typescript-eslint": "^8.7.0",
"vite": "^5.4.7",
"vitest": "^2.1.1",
"vitest-github-actions-reporter": "^0.11.0"
"vitest": "^2.1.1"
},
"type": "module",
"dependencies": {
@ -63,9 +67,7 @@
},
"resolutions": {
"es5-ext": ">=0.10.64",
"undici": ">=5.28.4",
"ws": ">=8.18.0",
"axios": ">=1.7.7",
"micromatch": ">=4.0.8"
},
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"

View file

@ -0,0 +1,30 @@
diff --git a/node_modules/@sveltejs/kit/src/exports/vite/index.js b/node_modules/@sveltejs/kit/src/exports/vite/index.js
index 21bc3d4..eef2db3 100644
--- a/node_modules/@sveltejs/kit/src/exports/vite/index.js
+++ b/node_modules/@sveltejs/kit/src/exports/vite/index.js
@@ -648,9 +648,9 @@ async function kit({ svelte_config }) {
output: {
format: inline ? 'iife' : 'esm',
name: `__sveltekit_${version_hash}.app`,
- entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`,
- chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`,
- assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
+ entryFileNames: ssr ? '[name].js' : `${prefix}/[hash].${ext}`,
+ chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[hash].${ext}`,
+ assetFileNames: `${prefix}/assets/[hash][extname]`,
hoistTransitiveImports: false,
sourcemapIgnoreList,
manualChunks: split ? undefined : () => 'bundle',
@@ -665,9 +665,9 @@ async function kit({ svelte_config }) {
worker: {
rollupOptions: {
output: {
- entryFileNames: `${prefix}/workers/[name]-[hash].js`,
- chunkFileNames: `${prefix}/workers/chunks/[name]-[hash].js`,
- assetFileNames: `${prefix}/workers/assets/[name]-[hash][extname]`,
+ entryFileNames: `${prefix}/workers/[hash].js`,
+ chunkFileNames: `${prefix}/workers/chunks/[hash].js`,
+ assetFileNames: `${prefix}/workers/assets/[hash][extname]`,
hoistTransitiveImports: false
}
}

View file

@ -1,17 +0,0 @@
diff --git a/node_modules/@sveltejs/kit/src/exports/vite/index.js b/node_modules/@sveltejs/kit/src/exports/vite/index.js
index 40fa4c6..738cabf 100644
--- a/node_modules/@sveltejs/kit/src/exports/vite/index.js
+++ b/node_modules/@sveltejs/kit/src/exports/vite/index.js
@@ -655,9 +655,9 @@ async function kit({ svelte_config }) {
input,
output: {
format: 'esm',
- entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`,
- chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`,
- assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
+ entryFileNames: ssr ? '[name].js' : `${prefix}/[hash].${ext}`,
+ chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[hash].${ext}`,
+ assetFileNames: `${prefix}/assets/[hash][extname]`,
hoistTransitiveImports: false,
sourcemapIgnoreList
},

View file

@ -1,17 +0,0 @@
diff --git a/node_modules/@sveltejs/kit/src/exports/vite/index.js b/node_modules/@sveltejs/kit/src/exports/vite/index.js
index e6521e9..f31c28b 100644
--- a/node_modules/@sveltejs/kit/src/exports/vite/index.js
+++ b/node_modules/@sveltejs/kit/src/exports/vite/index.js
@@ -639,9 +639,9 @@ async function kit({ svelte_config }) {
input,
output: {
format: 'esm',
- entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`,
- chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`,
- assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
+ entryFileNames: ssr ? '[name].js' : `${prefix}/[hash].${ext}`,
+ chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[hash].${ext}`,
+ assetFileNames: `${prefix}/assets/[hash][extname]`,
hoistTransitiveImports: false,
sourcemapIgnoreList
},

View file

@ -6,11 +6,11 @@ const config: PlaywrightTestConfig = {
timezoneId: 'Europe/Amsterdam'
},
webServer: {
command: 'npm run build && npm run preview',
command: 'npm run build:test && npm run preview',
port: 4173
},
reporter: process.env.CI ? 'github' : 'list',
testDir: 'tests',
testDir: 'tests/playwright',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};

View file

@ -0,0 +1,27 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
locale: 'en-GB',
timezoneId: 'Europe/Amsterdam'
},
webServer: {
command: 'yarn build && yarn preview',
port: 4173
},
testDir: './tests/doc-screenshots',
outputDir: './test-results/screenshots',
projects: [
{
name: 'Light Mode',
use: {
viewport: { width: 1440, height: 900 },
colorScheme: 'light'
}
},
{
name: 'Dark Mode',
use: { viewport: { width: 1440, height: 900 }, colorScheme: 'dark' }
}
]
});

View file

@ -0,0 +1,67 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
reporter: 'html',
use: {
locale: 'en-GB',
timezoneId: 'Europe/Amsterdam'
},
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: './tests/screenshots',
outputDir: './test-results/screenshots',
projects: [
{
name: 'MacBook Air 13 inch',
use: {
viewport: { width: 1440, height: 900 }
}
},
{
name: 'iPhone 14 Pro',
use: { ...devices['iPhone 14 Pro'] }
},
{
name: 'iPhone 15 Pro Landscape',
use: { ...devices['iPhone 15 Pro Landscape'] }
},
{
name: 'MacBook Pro 14 inch',
use: {
viewport: { width: 1512, height: 982 }
}
},
{
name: 'MacBook Pro 14 inch NL locale',
use: {
viewport: { width: 1512, height: 982 },
locale: 'nl'
}
},
{
name: 'MacBook Pro 14 inch nl-NL locale',
use: {
viewport: { width: 1512, height: 982 },
locale: 'nl-NL'
}
},
{
name: 'MacBook Pro 14 inch Firefox HiDPI',
use: { ...devices['Desktop Firefox HiDPI'], viewport: { width: 1512, height: 982 } }
},
{
name: 'MacBook Pro 14 inch Safari',
use: { ...devices['Desktop Safari'], viewport: { width: 1512, height: 982 } }
},
{
name: 'MacBook Pro 14 inch Safari Dark Mode',
use: {
...devices['Desktop Safari'],
viewport: { width: 1512, height: 982 },
colorScheme: 'dark'
}
}
]
});

14
renovate.json Normal file
View file

@ -0,0 +1,14 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"packageRules": [
{
"matchUpdateTypes": ["major"],
"enabled": false,
"matchPackageNames": ["*"]
}
],
"npm": {
"rangeStrategy": "update-lockfile"
}
}

View file

@ -0,0 +1,11 @@
<script lang="ts">
export let value: unknown;
export let checkValue: unknown = null;
export let width: number = 25;
$: valueToCheck = checkValue === null ? value : checkValue;
</script>
<span class:placeholder={!valueToCheck} class={!valueToCheck ? `w-${width}` : ''}>
{valueToCheck ? value : ''}
</span>

View file

@ -0,0 +1,65 @@
<script lang="ts">
import {
Input,
InputGroup,
InputGroupText,
Label,
FormText,
Col,
Row
} from '@sveltestrap/sveltestrap';
export let id: string;
export let label: string;
export let value: string | number;
export let type: string = 'text';
export let size: string = 'sm';
export let required: boolean = false;
export let min: number | undefined = undefined;
export let max: number | undefined = undefined;
export let step: number | string | undefined = undefined;
export let suffix: string | undefined = undefined;
export let helpText: string | undefined = undefined;
export let disabled: boolean = false;
export let valid: boolean | undefined = undefined;
export let invalid: boolean | undefined = undefined;
export let minlength: string | undefined = undefined;
export let onChange: (() => void) | undefined = undefined;
export let onInput: (() => void) | undefined = undefined;
const onInputHandler = () => {
onInput?.();
};
</script>
<Row>
<Label md={6} for={id} {size}>{label}</Label>
<Col md="6">
<InputGroup {size}>
<Input
{id}
{type}
bind:value
{required}
{min}
{max}
{step}
{disabled}
{valid}
{invalid}
{minlength}
bsSize={size}
on:change={onChange}
on:input={onInputHandler}
spellcheck={type === 'text' ? 'false' : undefined}
/>
{#if suffix}
<InputGroupText>{suffix}</InputGroupText>
{/if}
<slot />
</InputGroup>
{#if helpText}
<FormText>{helpText}</FormText>
{/if}
</Col>
</Row>

View file

@ -0,0 +1,34 @@
<script lang="ts">
import { Input, Label, FormText, Col, Row } from '@sveltestrap/sveltestrap';
export let id: string;
export let label: string;
export let value: string | number;
export let options: Array<[string, string | number]>;
export let size: string = 'sm';
export let helpText: string | undefined = undefined;
export let selectClass: string | undefined = undefined;
export let onChange: (() => void) | undefined = undefined;
</script>
<Row>
<Label md={6} for={id} {size}>{label}</Label>
<Col md="6">
<Input
{id}
type="select"
bind:value
name="select"
bsSize={size}
class={selectClass}
on:change={onChange}
>
{#each options as [key, val]}
<option value={val}>{key}</option>
{/each}
</Input>
{#if helpText}
<FormText>{helpText}</FormText>
{/if}
</Col>
</Row>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import { Input, Col } from '@sveltestrap/sveltestrap';
// Props
export let id: string;
export let checked: boolean;
export let label: string;
export let size: string = 'sm';
export let disabled: boolean = false;
export let col: { [key: string]: string } = { md: '6', xl: '12', xxl: '6' };
</script>
<Col {...col}>
<Input {id} bind:checked type="switch" bsSize={size} {label} {disabled} />
</Col>

View file

@ -0,0 +1,6 @@
export { default as SettingsSwitch } from './SettingsSwitch.svelte';
export { default as SettingsInput } from './SettingsInput.svelte';
export { default as SettingsSelect } from './SettingsSelect.svelte';
export { default as ToggleHeader } from './ToggleHeader.svelte';
export { default as ColorSchemeSwitcher } from './ColorSchemeSwitcher.svelte';
export { default as Placeholder } from './Placeholder.svelte';

View file

@ -0,0 +1,142 @@
<script lang="ts">
import { SettingsInput, SettingsSwitch } from '$lib/components';
import { _ } from 'svelte-i18n';
import { Row, Col, FormGroup, Input, InputGroupText } from '@sveltestrap/sveltestrap';
import ToggleHeader from '../ToggleHeader.svelte';
import { uiSettings } from '$lib/uiSettings';
import { isValidHexPubKey, getPubKey, isValidNpub } from '$lib';
import { createEventDispatcher } from 'svelte';
import { DataSourceType } from '$lib/types/dataSource';
const dispatch = createEventDispatcher();
export let settings;
export let isOpen = false;
const checkValidNostrPubkey = (key: string) => {
$settings[key] = $settings[key].trim();
if (isValidNpub($settings[key])) {
dispatch('showToast', {
color: 'info',
text: $_('section.settings.convertingValidNpub')
});
}
let ret = getPubKey($settings[key]);
if (ret) $settings[key] = ret;
};
</script>
<Row>
<ToggleHeader header={$_('section.settings.section.dataSource')} bind:isOpen defaultOpen={false}>
<Row>
<Col>
<h5>Data Source</h5>
<FormGroup>
<Row>
<Col xs="12" xl="6" class="mb-2">
<Input
type="radio"
id="btclock_source"
name="dataSource"
bind:group={$settings.dataSource}
value={DataSourceType.BTCLOCK_SOURCE}
label={$_('section.settings.dataSource.btclock')}
/>
</Col>
<Col xs="12" xl="6" class="mb-2">
<Input
type="radio"
id="third_party_source"
name="dataSource"
bind:group={$settings.dataSource}
value={DataSourceType.THIRD_PARTY_SOURCE}
label={$_('section.settings.dataSource.thirdParty')}
/>
</Col>
{#if $settings.nostrRelay}
<Col xs="12" xl="6" class="mb-2">
<Input
type="radio"
id="nostr_source"
name="dataSource"
bind:group={$settings.dataSource}
value={DataSourceType.NOSTR_SOURCE}
label={$_('section.settings.dataSource.nostr')}
/>
</Col>
{/if}
<Col xs="12" xl="6" class="mb-2">
<Input
type="radio"
id="custom_source"
name="dataSource"
bind:group={$settings.dataSource}
value={DataSourceType.CUSTOM_SOURCE}
label={$_('section.settings.dataSource.custom')}
/>
</Col>
</Row>
</FormGroup>
</Col>
</Row>
{#if $settings.dataSource === DataSourceType.THIRD_PARTY_SOURCE}
<SettingsInput
id="mempoolInstance"
label={$_('section.settings.mempoolnstance')}
bind:value={$settings.mempoolInstance}
required={true}
size={$uiSettings.inputSize}
>
<InputGroupText>
<Input
type="checkbox"
bind:checked={$settings.mempoolSecure}
bsSize={$uiSettings.inputSize}
/>
HTTPS
</InputGroupText>
</SettingsInput>
{/if}
{#if $settings.dataSource === DataSourceType.NOSTR_SOURCE}
<SettingsInput
id="nostrRelay"
label={$_('section.settings.nostrRelay')}
bind:value={$settings.nostrRelay}
required={true}
size={$uiSettings.inputSize}
/>
<SettingsInput
id="nostrPubKey"
label={$_('section.settings.nostrPubKey')}
bind:value={$settings.nostrPubKey}
required={true}
minlength="64"
invalid={!isValidHexPubKey($settings.nostrPubKey)}
helpText={!isValidHexPubKey($settings.nostrPubKey)
? $_('section.settings.invalidNostrPubkey')
: undefined}
size={$uiSettings.inputSize}
onChange={() => checkValidNostrPubkey('nostrPubKey')}
onInput={() => checkValidNostrPubkey('nostrPubKey')}
/>
{/if}
{#if $settings.dataSource === DataSourceType.CUSTOM_SOURCE}
<SettingsInput
id="ceEndpoint"
label={$_('section.settings.ceEndpoint')}
bind:value={$settings.ceEndpoint}
required={true}
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="ceDisableSSL"
bind:checked={$settings.ceDisableSSL}
label={$_('section.settings.ceDisableSSL')}
size={$uiSettings.inputSize}
/>
{/if}
</ToggleHeader>
</Row>

View file

@ -0,0 +1,202 @@
<script lang="ts">
import { SettingsInput, SettingsSwitch, SettingsSelect } from '$lib/components';
import { _ } from 'svelte-i18n';
import { Row } from '@sveltestrap/sveltestrap';
import ToggleHeader from '../ToggleHeader.svelte';
import { uiSettings } from '$lib/uiSettings';
import { PUBLIC_BASE_URL } from '$lib/config';
export let settings;
export let isOpen = false;
const onFlBrightnessChange = async () => {
await fetch(`${PUBLIC_BASE_URL}/api/frontlight/brightness/${$settings.flMaxBrightness}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
};
const setTextColor = () => {
$settings.invertedColor = !$settings.invertedColor;
};
const textColorOptions: [string, boolean][] = [
[$_('colors.black') + ' on ' + $_('colors.white'), false],
[$_('colors.white') + ' on ' + $_('colors.black'), true]
];
const fontPreferenceOptions: [string, string][] = $settings.availableFonts?.map((font) => [
$_(`fonts.${font}`) !== `fonts.${font}`
? $_(`fonts.${font}`)
: font.charAt(0).toUpperCase() + font.slice(1),
font
]);
</script>
<Row>
<ToggleHeader
header={$_('section.settings.section.displaysAndLed')}
bind:isOpen
defaultOpen={false}
>
<SettingsSelect
id="textColor"
label={$_('section.settings.textColor')}
bind:value={$settings.invertedColor}
options={textColorOptions}
size={$uiSettings.inputSize}
on:change={setTextColor}
/>
<SettingsSelect
id="fontName"
label={$_('section.settings.fontName')}
bind:value={$settings.fontName}
options={fontPreferenceOptions}
size={$uiSettings.inputSize}
/>
<SettingsInput
id="timePerScreen"
label={$_('section.settings.timePerScreen')}
bind:value={$settings.timePerScreen}
type="number"
min={1}
step={1}
required={true}
suffix={$_('time.minutes')}
size={$uiSettings.inputSize}
/>
<SettingsInput
id="fullRefreshMin"
label={$_('section.settings.fullRefreshEvery')}
bind:value={$settings.fullRefreshMin}
type="number"
min={1}
step={1}
required={true}
suffix={$_('time.minutes')}
size={$uiSettings.inputSize}
/>
<SettingsInput
id="minSecPriceUpd"
label={$_('section.settings.timeBetweenPriceUpdates')}
bind:value={$settings.minSecPriceUpd}
type="number"
min={1}
step={1}
suffix={$_('time.seconds')}
helpText={$_('section.settings.shortAmountsWarning')}
size={$uiSettings.inputSize}
/>
<SettingsInput
id="ledBrightness"
label={$_('section.settings.ledBrightness')}
bind:value={$settings.ledBrightness}
type="range"
min={0}
max={255}
step={1}
size={$uiSettings.inputSize}
/>
{#if $settings.hasFrontlight && !$settings.flDisable}
<SettingsInput
id="flMaxBrightness"
label={$_('section.settings.flMaxBrightness')}
bind:value={$settings.flMaxBrightness}
type="range"
min={0}
max={4095}
step={1}
size={$uiSettings.inputSize}
on:change={onFlBrightnessChange}
/>
<SettingsInput
id="flEffectDelay"
label={$_('section.settings.flEffectDelay')}
bind:value={$settings.flEffectDelay}
type="range"
min={5}
max={300}
step={1}
size={$uiSettings.inputSize}
/>
{/if}
{#if !$settings.flDisable && $settings.hasLightLevel}
<SettingsInput
id="luxLightToggle"
label={`${$_('section.settings.luxLightToggle')} (${$settings.luxLightToggle})`}
bind:value={$settings.luxLightToggle}
type="range"
min={0}
max={1000}
step={1}
helpText={$_('section.settings.luxLightToggleText')}
size={$uiSettings.inputSize}
/>
{/if}
<Row>
<SettingsSwitch
id="ledTestOnPower"
bind:checked={$settings.ledTestOnPower}
label={$_('section.settings.ledPowerOnTest')}
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="ledFlashOnUpd"
bind:checked={$settings.ledFlashOnUpd}
label={$_('section.settings.ledFlashOnBlock')}
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="disableLeds"
bind:checked={$settings.disableLeds}
label={$_('section.settings.disableLeds')}
size={$uiSettings.inputSize}
/>
{#if $settings.hasFrontlight}
<SettingsSwitch
id="flDisable"
bind:checked={$settings.flDisable}
label={$_('section.settings.flDisable')}
size={$uiSettings.inputSize}
/>
{/if}
{#if $settings.hasFrontlight && !$settings.flDisable}
<SettingsSwitch
id="flAlwaysOn"
bind:checked={$settings.flAlwaysOn}
label={$_('section.settings.flAlwaysOn')}
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="flFlashOnUpd"
bind:checked={$settings.flFlashOnUpd}
label={$_('section.settings.flFlashOnUpd')}
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="flOffWhenDark"
bind:checked={$settings.flOffWhenDark}
label={$_('section.settings.flOffWhenDark')}
size={$uiSettings.inputSize}
/>
{/if}
</Row>
</ToggleHeader>
</Row>

View file

@ -0,0 +1,248 @@
<script lang="ts">
import { SettingsInput, SettingsSwitch, SettingsSelect } from '$lib/components';
import { _ } from 'svelte-i18n';
import { Row, Button, Col } from '@sveltestrap/sveltestrap';
import ToggleHeader from '../ToggleHeader.svelte';
import { uiSettings } from '$lib/uiSettings';
import { isValidHexPubKey, getPubKey, isValidNpub } from '$lib';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let settings;
export let isOpen = false;
export let miningPoolMap: Map<string, string>;
let validBitaxe = false;
const testBitaxe = async () => {
try {
const response = await fetch(`http://${$settings.bitaxeHostname}/api/system/info`);
if (!response.ok) {
dispatch('showToast', {
color: 'danger',
text: `Failed to connect to BitAxe HTTP error! status: ${response.status}`
});
validBitaxe = false;
throw new Error();
}
const systemInfo = await response.json();
dispatch('showToast', {
color: 'success',
text: `Connected to BitAxe ${systemInfo.ASICModel} (Board version ${systemInfo.boardVersion}) running firmware ${systemInfo.version}.\r\nCurrent hashrate ${Math.round(systemInfo.hashRate)} GH/s`
});
validBitaxe = true;
} catch (error) {
if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
dispatch('showToast', {
color: 'danger',
text: `Failed to connect to BitAxe, make sure you are connected to the same network.`
});
}
console.error('Failed to fetch Bitaxe system info:', error);
validBitaxe = false;
}
};
const checkValidNostrPubkey = (key: string) => {
$settings[key] = $settings[key].trim();
if (isValidNpub($settings[key])) {
dispatch('showToast', {
color: 'info',
text: $_('section.settings.convertingValidNpub')
});
}
let ret = getPubKey($settings[key]);
if (ret) $settings[key] = ret;
};
$: poolOptions = ($settings.availablePools || []).map((pool: string): [string, string] => [
miningPoolMap.get(pool) || pool,
pool
]);
</script>
<Row>
<ToggleHeader
header={$_('section.settings.section.extraFeatures')}
bind:isOpen
defaultOpen={false}
>
<!--- Time based do not disturb settings -->
<SettingsSwitch
id="timeBasedDnd"
label={$_('section.settings.timeBasedDnd')}
bind:checked={$settings.dnd.timeBasedEnabled}
size={$uiSettings.inputSize}
/>
{#if $settings.dnd.timeBasedEnabled}
<Row>
<Col>
<SettingsInput
id="dndStartHour"
type="number"
min="0"
max="23"
label={$_('section.settings.dndStartHour')}
bind:value={$settings.dnd.startHour}
size={$uiSettings.inputSize}
/>
</Col>
<Col>
<SettingsInput
id="dndStartMinute"
type="number"
min="0"
max="59"
label={$_('section.settings.dndStartMinute')}
bind:value={$settings.dnd.startMinute}
size={$uiSettings.inputSize}
/>
</Col>
</Row>
<Row>
<Col>
<SettingsInput
id="dndEndHour"
type="number"
min="0"
max="23"
label={$_('section.settings.dndEndHour')}
bind:value={$settings.dnd.endHour}
size={$uiSettings.inputSize}
/>
</Col>
<Col>
<SettingsInput
id="dndEndMinute"
type="number"
min="0"
max="59"
label={$_('section.settings.dndEndMinute')}
bind:value={$settings.dnd.endMinute}
size={$uiSettings.inputSize}
/>
</Col>
</Row>
{/if}
<!-- BitAxe Settings -->
{#if 'bitaxeEnabled' in $settings}
<Row class="mb-3">
<Col>
<h5>BitAxe</h5>
<SettingsSwitch
id="bitaxeEnabled"
bind:checked={$settings.bitaxeEnabled}
label="{$_('section.settings.bitaxeEnabled')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
col={{ md: '12', xl: '12', xxl: '12' }}
/>
{#if $settings.bitaxeEnabled}
<SettingsInput
id="bitaxeHostname"
label={$_('section.settings.bitaxeHostname')}
bind:value={$settings.bitaxeHostname}
required={true}
valid={validBitaxe}
size={$uiSettings.inputSize}
>
<Button type="button" color="success" on:click={testBitaxe}>
{$_('test', { default: 'Test' })}
</Button>
</SettingsInput>
{/if}
</Col>
</Row>
{/if}
<!-- Mining Pool Settings -->
{#if 'miningPoolStats' in $settings}
<Row class="mb-3">
<Col>
<h5>Mining Pool stats</h5>
<SettingsSwitch
id="miningPoolStats"
bind:checked={$settings.miningPoolStats}
label="{$_('section.settings.miningPoolStats')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
col={{ md: '12', xl: '12', xxl: '12' }}
/>
{#if $settings.miningPoolStats}
<SettingsSelect
id="miningPoolName"
label={$_('section.settings.miningPoolName')}
bind:value={$settings.miningPoolName}
options={poolOptions}
size={$uiSettings.inputSize}
selectClass={$uiSettings.selectClass}
/>
<SettingsInput
id="miningPoolUser"
label={$_('section.settings.miningPoolUser')}
bind:value={$settings.miningPoolUser}
required={true}
size={$uiSettings.inputSize}
/>
{/if}
</Col>
</Row>
{/if}
<!-- Nostr Settings -->
{#if 'nostrZapNotify' in $settings}
<Row class="mb-3">
<Col>
<h5>Nostr</h5>
<SettingsInput
id="nostrRelay"
label={$_('section.settings.nostrRelay')}
bind:value={$settings.nostrRelay}
required={true}
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="nostrZapNotify"
bind:checked={$settings.nostrZapNotify}
label="{$_('section.settings.nostrZapNotify')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
col={{ md: '12', xl: '12', xxl: '12' }}
/>
{#if $settings.nostrZapNotify}
<Row>
<SettingsSwitch
id="ledFlashOnZap"
bind:checked={$settings.ledFlashOnZap}
label={$_('section.settings.ledFlashOnZap')}
size={$uiSettings.inputSize}
/>
{#if $settings.hasFrontlight && !$settings.flDisable}
<SettingsSwitch
id="flFlashOnZap"
bind:checked={$settings.flFlashOnZap}
label={$_('section.settings.flFlashOnZap')}
size={$uiSettings.inputSize}
/>
{/if}
</Row>
<SettingsInput
id="nostrZapPubkey"
label={$_('section.settings.nostrZapPubkey')}
bind:value={$settings.nostrZapPubkey}
required={true}
minlength="64"
invalid={!isValidHexPubKey($settings.nostrZapPubkey)}
helpText={!isValidHexPubKey($settings.nostrZapPubkey)
? $_('section.settings.invalidNostrPubkey')
: undefined}
size={$uiSettings.inputSize}
onChange={() => checkValidNostrPubkey('nostrZapPubkey')}
onInput={() => checkValidNostrPubkey('nostrZapPubkey')}
/>
{/if}
</Col>
</Row>
{/if}
</ToggleHeader>
</Row>

View file

@ -0,0 +1,125 @@
<script lang="ts">
import { SettingsSwitch } from '$lib/components';
import { _ } from 'svelte-i18n';
import { Row, Col } from '@sveltestrap/sveltestrap';
import ToggleHeader from '../ToggleHeader.svelte';
import { uiSettings } from '$lib/uiSettings';
export let settings;
export let isOpen = false;
</script>
<Row>
<ToggleHeader
header={$_('section.settings.section.screenSettings')}
bind:isOpen
defaultOpen={true}
>
<Row>
<SettingsSwitch
id="stealFocus"
bind:checked={$settings.stealFocus}
label={$_('section.settings.StealFocusOnNewBlock')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/>
<SettingsSwitch
id="mcapBigChar"
bind:checked={$settings.mcapBigChar}
label={$_('section.settings.useBigCharsMcap')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/>
<SettingsSwitch
id="useBlkCountdown"
bind:checked={$settings.useBlkCountdown}
label={$_('section.settings.useBlkCountdown')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/>
<SettingsSwitch
id="useSatsSymbol"
bind:checked={$settings.useSatsSymbol}
label={$_('section.settings.useSatsSymbol')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/>
<SettingsSwitch
id="suffixPrice"
bind:checked={$settings.suffixPrice}
label={$_('section.settings.suffixPrice')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/>
<SettingsSwitch
id="mowMode"
bind:checked={$settings.mowMode}
label={$_('section.settings.mowMode')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
disabled={!$settings.suffixPrice}
/>
<SettingsSwitch
id="suffixShareDot"
bind:checked={$settings.suffixShareDot}
label={$_('section.settings.suffixShareDot')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
disabled={!$settings.suffixPrice}
/>
<SettingsSwitch
id="verticalDesc"
bind:checked={$settings.verticalDesc}
label={$_('section.settings.verticalDesc')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/>
{#if !$settings.actCurrencies}
<SettingsSwitch
id="fetchEurPrice"
bind:checked={$settings.fetchEurPrice}
label="{$_('section.settings.fetchEuroPrice')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/>
{/if}
</Row>
<Row>
<h5>{$_('section.settings.screens')}</h5>
{#if $settings.screens}
{#each $settings.screens as s}
<SettingsSwitch
id="screens_{s.id}"
bind:checked={s.enabled}
label={s.name}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/>
{/each}
{/if}
</Row>
{#if $settings.actCurrencies && $settings.useNostr !== true}
<Row>
<h5>{$_('section.settings.currencies')}</h5>
<small>{$_('restartRequired')}</small>
{#if $settings.availableCurrencies}
{#each $settings.availableCurrencies as c}
<Col md="6" xl="12" xxl="6">
<div class="form-check form-control-{$uiSettings.inputSize}">
<input
id="currency_{c}"
bind:group={$settings.actCurrencies}
value={c}
type="checkbox"
class="form-check-input"
/>
<label class="form-check-label" for="currency_{c}">{c}</label>
</div>
</Col>
{/each}
{/if}
</Row>
{/if}
</ToggleHeader>
</Row>

View file

@ -0,0 +1,113 @@
<script lang="ts">
import { SettingsInput, SettingsSwitch } from '$lib/components';
import { _ } from 'svelte-i18n';
import { Row, Button } from '@sveltestrap/sveltestrap';
import ToggleHeader from '../ToggleHeader.svelte';
import { uiSettings } from '$lib/uiSettings';
import EyeIcon from 'svelte-bootstrap-icons/lib/Eye.svelte';
import EyeSlashIcon from 'svelte-bootstrap-icons/lib/EyeSlash.svelte';
export let settings;
export let isOpen = false;
let showPassword = false;
const getTzOffsetFromSystem = () => {
const dt = new Date();
let diffTZ = dt.getTimezoneOffset();
$settings.tzOffset = diffTZ * -1;
};
</script>
<Row>
<ToggleHeader header={$_('section.settings.section.system')} bind:isOpen defaultOpen={false}>
<SettingsInput
id="tzOffset"
label={$_('section.settings.timezoneOffset')}
bind:value={$settings.tzOffset}
type="number"
step={1}
required={true}
suffix={$_('time.minutes')}
helpText={$_('section.settings.tzOffsetHelpText')}
size={$uiSettings.inputSize}
>
<Button type="button" color="info" on:click={getTzOffsetFromSystem}>
{$_('auto-detect')}
</Button>
</SettingsInput>
{#if $settings.httpAuthEnabled}
<SettingsInput
id="httpAuthUser"
label={$_('section.settings.httpAuthUser')}
bind:value={$settings.httpAuthUser}
required={true}
size={$uiSettings.inputSize}
/>
<SettingsInput
id="httpAuthPass"
label={$_('section.settings.httpAuthPass')}
bind:value={$settings.httpAuthPass}
type={showPassword ? 'text' : 'password'}
required={true}
size={$uiSettings.inputSize}
>
<Button
type="button"
on:click={() => (showPassword = !showPassword)}
color={showPassword ? 'success' : 'danger'}
>
{#if !showPassword}<EyeIcon />{:else}<EyeSlashIcon />{/if}
</Button>
</SettingsInput>
{/if}
<SettingsInput
id="hostnamePrefix"
label={$_('section.settings.hostnamePrefix')}
bind:value={$settings.hostnamePrefix}
required={true}
size={$uiSettings.inputSize}
/>
<SettingsInput
id="wpTimeout"
label={$_('section.settings.wpTimeout')}
bind:value={$settings.wpTimeout}
type="number"
min={1}
step={1}
required={true}
suffix={$_('time.seconds')}
size={$uiSettings.inputSize}
/>
<Row>
<SettingsSwitch
id="otaEnabled"
bind:checked={$settings.otaEnabled}
label="{$_('section.settings.otaUpdates')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="mdnsEnabled"
bind:checked={$settings.mdnsEnabled}
label="{$_('section.settings.enableMdns')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="httpAuthEnabled"
bind:checked={$settings.httpAuthEnabled}
label="{$_('section.settings.httpAuthEnabled')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="enableDebugLog"
bind:checked={$settings.enableDebugLog}
label="{$_('section.settings.enableDebugLog')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/>
</Row>
</ToggleHeader>
</Row>

View file

@ -0,0 +1,5 @@
export { default as ScreenSpecificSettings } from './ScreenSpecificSettings.svelte';
export { default as DisplaySettings } from './DisplaySettings.svelte';
export { default as DataSourceSettings } from './DataSourceSettings.svelte';
export { default as ExtraFeaturesSettings } from './ExtraFeaturesSettings.svelte';
export { default as SystemSettings } from './SystemSettings.svelte';

0
src/lib/i18n/en.json Normal file
View file

View file

@ -8,11 +8,23 @@ register('nl', () => import('../locales/nl.json'));
register('es', () => import('../locales/es.json'));
register('de', () => import('../locales/de.json'));
const getInitialLocale = () => {
if (!browser) return defaultLocale;
// Check localStorage first
const storedLocale = localStorage.getItem('locale');
if (storedLocale) return storedLocale;
// Get browser locale and normalize it
const browserLocale = window.navigator.language;
const normalizedLocale = browserLocale.split('-')[0].toLowerCase();
// Check if we support this locale
const supportedLocales = ['en', 'nl', 'es', 'de'];
return supportedLocales.includes(normalizedLocale) ? normalizedLocale : defaultLocale;
};
init({
fallbackLocale: defaultLocale,
initialLocale: browser
? browser && localStorage.getItem('locale')
? localStorage.getItem('locale')
: window.navigator.language.slice(0, 2)
: defaultLocale
initialLocale: getInitialLocale()
});

View file

@ -30,7 +30,7 @@
"wifiTxPower": "WiFi-TX-Leistung",
"settingsSaved": "Einstellungen gespeichert",
"errorSavingSettings": "Fehler beim Speichern der Einstellungen",
"ownDataSource": "BTClock-Datenquelle verwenden",
"ownDataSource": "BTClock-Datenquelle",
"flAlwaysOn": "Displaybeleuchtung immer an",
"flEffectDelay": "Displaybeleuchtungeffekt Geschwindigkeit",
"flFlashOnUpd": "Displaybeleuchting bei neuem Block",
@ -55,7 +55,25 @@
"ledFlashOnZap": "LED blinkt bei Nostr Zap",
"flFlashOnZap": "Displaybeleuchting bei Nostr Zap",
"showAll": "Alle anzeigen",
"hideAll": "Alles ausblenden"
"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",
@ -83,7 +101,9 @@
"wifiSignalStrength": "WiFi-Signalstärke",
"wsDataConnection": "BTClock-Datenquelle verbindung",
"lightSensor": "Lichtsensor",
"nostrConnection": "Nostr Relay-Verbindung"
"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",
@ -95,7 +115,8 @@
"latestVersion": "Letzte Version",
"releaseDate": "Veröffentlichungsdatum",
"viewRelease": "Veröffentlichung anzeigen",
"autoUpdate": "Update installieren (experimentell)"
"autoUpdate": "Update installieren (experimentell)",
"autoUpdateInProgress": "Automatische Aktualisierung läuft, bitte warten..."
}
},
"colors": {

View file

@ -29,7 +29,7 @@
"wifiTxPower": "WiFi TX power",
"settingsSaved": "Settings saved",
"errorSavingSettings": "Error saving settings",
"ownDataSource": "Use BTClock data source",
"ownDataSource": "BTClock data source",
"flMaxBrightness": "Frontlight brightness",
"flAlwaysOn": "Frontlight always on",
"flEffectDelay": "Frontlight effect speed",
@ -40,10 +40,13 @@
"nostrPubKey": "Nostr source pubkey",
"nostrZapKey": "Nostr zap pubkey",
"nostrRelay": "Nostr Relay",
"nostrZapNotify": "Nostr Zap Notifications",
"nostrZapNotify": "Enable Nostr Zap Notifications",
"useNostr": "Use Nostr data source",
"bitaxeHostname": "BitAxe hostname or IP",
"bitaxeEnabled": "Enable BitAxe",
"bitaxeEnabled": "Enable BitAxe-integration",
"miningPoolStats": "Enable Mining Pool Stats integration",
"miningPoolName": "Mining Pool",
"miningPoolUser": "Mining Pool username or api key",
"nostrZapPubkey": "Nostr Zap pubkey",
"invalidNostrPubkey": "Invalid Nostr pubkey, note that your pubkey does NOT start with npub.",
"convertingValidNpub": "Converting valid npub to pubkey",
@ -53,7 +56,7 @@
"httpAuthPass": "WebUI Password",
"httpAuthText": "Only password-protects WebUI, not API-calls.",
"currencies": "Currencies",
"stagingSource": "Use Staging data source (for development)",
"customSource": "Use custom data source endpoint",
"useNostrTooltip": "Very experimental and unstable. Nostr data source is not required for Nostr Zap notifications.",
"mowMode": "Mow Suffix Mode",
"suffixShareDot": "Suffix compact notation",
@ -67,7 +70,27 @@
"ledFlashOnZap": "LED flash on Nostr Zap",
"flFlashOnZap": "Frontlight flash on Nostr Zap",
"showAll": "Show all",
"hideAll": "Hide all"
"hideAll": "Hide all",
"flOffWhenDark": "Frontlight off when dark",
"luxLightToggleText": "Set to 0 to disable",
"verticalDesc": "Use vertical screen description",
"enableDebugLog": "Enable Debug-log",
"dataSource": {
"label": "Data Source",
"btclock": "BTClock Data Source",
"thirdParty": "mempool.space/coincap.io",
"nostr": "Nostr publisher",
"custom": "Custom Endpoint"
},
"thirdPartySource": "Use mempool.space/coincap.io",
"ceDisableSSL": "Disable SSL",
"ceEndpoint": "Endpoint hostname",
"fontName": "Font",
"timeBasedDnd": "Enable Do Not Disturb time schedule",
"dndStartHour": "Start hour",
"dndStartMinute": "Start minute",
"dndEndHour": "End hour",
"dndEndMinute": "End minute"
},
"control": {
"systemInfo": "System info",
@ -97,7 +120,9 @@
"wifiSignalStrength": "WiFi Signal strength",
"wsDataConnection": "BTClock data-source connection",
"lightSensor": "Light sensor",
"nostrConnection": "Nostr Relay connection"
"nostrConnection": "Nostr Relay connection",
"doNotDisturb": "Do not disturb",
"timeBasedDnd": "Time-based schedule"
},
"firmwareUpdater": {
"fileUploadFailed": "File upload failed. Make sure you have selected the correct file and try again.",
@ -109,7 +134,8 @@
"latestVersion": "Latest Version",
"releaseDate": "Release Date",
"viewRelease": "View Release",
"autoUpdate": "Install update (experimental)"
"autoUpdate": "Install update (experimental)",
"autoUpdateInProgress": "Auto-update in progress, please wait..."
}
},
"colors": {

View file

@ -28,7 +28,7 @@
"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": "Utilice la fuente de datos BTClock",
"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",
@ -54,7 +54,25 @@
"ledFlashOnZap": "LED parpadeante con Nostr Zap",
"flFlashOnZap": "Flash de luz frontal con Nostr Zap",
"showAll": "Mostrar todo",
"hideAll": "Ocultar 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",
@ -82,7 +100,9 @@
"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"
"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",
@ -94,7 +114,8 @@
"latestVersion": "Ultima versión",
"releaseDate": "Fecha de lanzamiento",
"viewRelease": "Ver lanzamiento",
"autoUpdate": "Instalar actualización (experimental)"
"autoUpdate": "Instalar actualización (experimental)",
"autoUpdateInProgress": "Actualización automática en progreso, espere..."
}
},
"button": {

View file

@ -55,7 +55,16 @@
"ledFlashOnZap": "Knipper LED bij Nostr Zap",
"flFlashOnZap": "Knipper displaylicht bij Nostr Zap",
"showAll": "Toon alles",
"hideAll": "Alles verbergen"
"hideAll": "Alles verbergen",
"flOffWhenDark": "Displaylicht uit als het donker is",
"luxLightToggleText": "Stel in op 0 om uit te schakelen",
"verticalDesc": "Verticale schermbeschrijving",
"fontName": "Lettertype",
"timeBasedDnd": "Schakel het tijdschema Niet storen in",
"dndStartHour": "Begin uur",
"dndStartMinute": "Beginminuut",
"dndEndHour": "Eind uur",
"dndEndMinute": "Einde minuut"
},
"control": {
"systemInfo": "Systeeminformatie",
@ -82,7 +91,9 @@
"wifiSignalStrength": "WiFi signaalsterkte",
"wsDataConnection": "BTClock-gegevensbron verbinding",
"lightSensor": "Licht sensor",
"nostrConnection": "Nostr Relay-verbinding"
"nostrConnection": "Nostr Relay-verbinding",
"doNotDisturb": "Niet storen",
"timeBasedDnd": "Op tijd gebaseerd schema"
},
"firmwareUpdater": {
"fileUploadSuccess": "Bestand geüpload, apparaat herstart. WebUI opnieuw geladen over {countdown} seconden",
@ -94,7 +105,8 @@
"latestVersion": "Laatste versie",
"releaseDate": "Datum van publicatie",
"viewRelease": "Bekijk publicatie",
"autoUpdate": "Update installeren (experimenteel)"
"autoUpdate": "Update installeren (experimenteel)",
"autoUpdateInProgress": "Automatische update wordt uitgevoerd. Even geduld a.u.b...."
}
},
"colors": {

View file

@ -1,11 +1,23 @@
@use '@fontsource/ubuntu/scss/mixins' as Ubuntu;
@use '@fontsource/antonio/scss/mixins' as Antonio;
@import '../node_modules/bootstrap/scss/functions';
@import '../node_modules/bootstrap/scss/variables';
@import '../node_modules/bootstrap/scss/variables-dark';
//@import "@fontsource/antonio/latin-400.css";
@import '@fontsource/ubuntu/latin-400.css';
//@import '@fontsource/oswald/latin-400.css';
@import '@fontsource/antonio/latin-400.css';
@include Ubuntu.faces(
$subsets: latin,
$weights: 400,
$formats: 'woff2',
$directory: '@fontsource/ubuntu/files'
);
@include Antonio.faces(
$subsets: latin,
$weights: 400,
$formats: 'woff2',
$directory: '@fontsource/antonio/files'
);
@import './satsymbol';
@ -14,6 +26,8 @@ $font-family-base: 'Ubuntu';
$font-size-base: 0.9rem;
$input-font-size-sm: $font-size-base * 0.875;
@import '../node_modules/bootstrap/scss/variables';
@import '../node_modules/bootstrap/scss/variables-dark';
// $border-radius: .675rem;
@import '../node_modules/bootstrap/scss/mixins';
@ -39,10 +53,46 @@ $input-font-size-sm: $font-size-base * 0.875;
@import '../node_modules/bootstrap/scss/tooltip';
@import '../node_modules/bootstrap/scss/toasts';
@import '../node_modules/bootstrap/scss/alert';
@import '../node_modules/bootstrap/scss/placeholders';
@import '../node_modules/bootstrap/scss/spinners';
@import '../node_modules/bootstrap/scss/helpers';
@import '../node_modules/bootstrap/scss/utilities/api';
/* Default state (xs) - sticky */
.sticky-xs-top {
position: sticky;
top: 0;
z-index: 1020;
}
@media (max-width: 576px) {
main {
margin-top: 25px;
}
}
/* Remove sticky behavior for larger screens */
@media (min-width: 576px) {
.sticky-xs-top {
position: relative;
}
}
@include color-mode(dark) {
.navbar {
--bs-navbar-color: $light;
background-color: $dark;
}
}
@include color-mode(light) {
.navbar {
--bs-navbar-color: $dark;
background-color: $light;
}
}
nav {
margin-bottom: 15px;
}
@ -53,6 +103,23 @@ nav {
.btn-group-sm .btn {
font-size: 0.8rem;
// text-overflow: ellipsis;
// white-space: nowrap;
// overflow: hidden;
// width: 4rem;
}
.btn-group-sm {
display: flex !important;
flex-wrap: wrap !important;
gap: 0.25rem !important;
}
/* Remove the border radius override that Bootstrap applies */
.btn-group-sm > .btn {
border-radius: 0.25rem !important;
margin: 0 !important;
position: relative !important;
}
#customText {
@ -63,7 +130,7 @@ nav {
.btclock {
background: #000;
display: flex;
font-size: calc(3vw + 3vh);
font-size: calc(2vw + 2vh);
font-family: 'Antonio', sans-serif;
font-weight: 400;
padding: 10px;
@ -104,19 +171,25 @@ nav {
flex-direction: column; /* Stack the text and line vertically */
align-items: center;
justify-content: space-around; /* Distribute items with space between */
padding: 10px;
padding: 5px;
}
.splitText div:first-child::after {
&.verticalDesc > .splitText:first-child {
.textcontainer {
transform: rotate(-90deg);
}
}
.splitText .textcontainer :first-child::after {
display: block;
content: '';
margin-top: 0px;
border-bottom: 2px solid;
margin-bottom: 3px;
// margin-bottom: 3px;
}
.splitText {
font-size: calc(0.5vw + 1vh);
font-size: calc(0.3vw + 1vh);
.top-text,
.bottom-text {
@ -242,3 +315,38 @@ nav {
input[type='number'] {
text-align: right;
}
.lightMode .bitaxelogo {
filter: brightness(0) saturate(100%);
}
.connection-lost-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.75);
z-index: 1050;
display: flex;
justify-content: center;
align-items: center;
.overlay-content {
background-color: rgba(255, 255, 255, 0.75);
padding: 0.5rem;
border-radius: 0.5rem;
text-align: center;
i {
font-size: 1rem;
color: $danger;
margin-bottom: 1rem;
}
h4 {
margin-bottom: 0.5rem;
}
}
}

View file

@ -1,8 +1,6 @@
@font-face {
font-family: 'Satoshi Symbol';
src:
url('/fonts/Satoshi_Symbol.woff2') format('woff2'),
url('/fonts/Satoshi_Symbol.woff') format('woff');
src: url('/fonts/Satoshi_Symbol.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;

View file

@ -0,0 +1,6 @@
export enum DataSourceType {
BTCLOCK_SOURCE = 0,
THIRD_PARTY_SOURCE = 1,
NOSTR_SOURCE = 2,
CUSTOM_SOURCE = 3
}

View file

@ -12,10 +12,12 @@
NavbarBrand,
NavbarToggler
} from '@sveltestrap/sveltestrap';
import { _ } from 'svelte-i18n';
import { page } from '$app/stores';
import { locale, locales, isLoading } from 'svelte-i18n';
import ColorSchemeSwitcher from '../components/ColorSchemeSwitcher.svelte';
import { ColorSchemeSwitcher } from '$lib/components';
import { derived } from 'svelte/store';
export const setLocale = (lang: string) => () => {
locale.set(lang);
@ -38,19 +40,20 @@
return flagMap[lowercaseCode];
} else {
// Return null for unsupported language codes
return null;
return flagMap['en'];
}
};
let languageNames = {};
locale.subscribe(() => {
if ($locale) {
let newLanguageNames = new Intl.DisplayNames([$locale], { type: 'language' });
const currentLocale = derived(locale, ($locale) => $locale || 'en');
for (let l of $locales) {
languageNames[l] = newLanguageNames.of(l);
}
locale.subscribe(() => {
const localeToUse = $locale || 'en';
let newLanguageNames = new Intl.DisplayNames([localeToUse], { type: 'language' });
for (let l of $locales) {
languageNames[l] = newLanguageNames.of(l) || l;
}
});
@ -61,8 +64,23 @@
};
</script>
<Navbar expand="md">
<NavbarBrand>&#8383;TClock</NavbarBrand>
<Navbar expand="md" sticky="xs-top" theme="auto">
<NavbarBrand class="d-none d-sm-block">&#8383;TClock</NavbarBrand>
<Nav class="d-md-none" pills>
<NavItem>
<NavLink href="#control" active>{$_('section.control.title', { default: 'Control' })}</NavLink
>
</NavItem>
<NavItem>
<NavLink href="#status">{$_('section.status.title', { default: 'Status' })}</NavLink>
</NavItem>
<NavItem>
<NavLink class="nav-link" href="#settings"
>{$_('section.settings.title', { default: 'Settings' })}</NavLink
>
</NavItem>
</Nav>
<NavbarToggler on:click={toggle} />
<Collapse {isOpen} navbar expand="sm">
@ -79,7 +97,10 @@
</Nav>
{#if !$isLoading}
<Dropdown id="nav-language-dropdown" inNavbar class="me-3">
<DropdownToggle nav caret>{getFlagEmoji($locale)} {languageNames[$locale]}</DropdownToggle>
<DropdownToggle nav caret
>{getFlagEmoji($currentLocale)}
{languageNames[$currentLocale] || 'English'}</DropdownToggle
>
<DropdownMenu end>
{#each $locales as locale}
<DropdownItem on:click={setLocale(locale)}
@ -94,4 +115,6 @@
</Navbar>
<!-- +layout.svelte -->
<slot />
<main>
<slot />
</main>

View file

@ -6,10 +6,15 @@ import { locale, waitLocale } from 'svelte-i18n';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async () => {
if (browser && localStorage.getItem('locale')) {
locale.set(localStorage.getItem('locale'));
} else if (browser) {
locale.set(window.navigator.language);
if (browser) {
if (localStorage.getItem('locale')) {
locale.set(localStorage.getItem('locale'));
} else {
// Normalize the browser locale
const browserLocale = window.navigator.language.split('-')[0].toLowerCase();
const supportedLocales = ['en', 'nl', 'es', 'de'];
locale.set(supportedLocales.includes(browserLocale) ? browserLocale : 'en');
}
}
await waitLocale();
};

View file

@ -3,6 +3,7 @@
import { screenSize, updateScreenSize } from '$lib/screen';
import { Container, Row, Toast, ToastBody } from '@sveltestrap/sveltestrap';
import { replaceState } from '$app/navigation';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
@ -13,15 +14,10 @@
let settings = writable({
fgColor: '0',
bgColor: '0'
bgColor: '0',
isLoaded: false
});
// let uiSettings = writable({
// inputSize: 'sm',
// selectClass: '',
// btnSize: 'lg'
// });
let status = writable({
data: ['L', 'O', 'A', 'D', 'I', 'N', 'G'],
espFreeHeap: 0,
@ -30,48 +26,126 @@
price: false,
blocks: false
},
leds: []
leds: [],
isUpdating: false
});
const fetchStatusData = () => {
fetch(`${PUBLIC_BASE_URL}/api/status`, { credentials: 'same-origin' })
.then((res) => res.json())
.then((data) => {
status.set(data);
});
const fetchStatusData = async () => {
const res = await fetch(`${PUBLIC_BASE_URL}/api/status`, { credentials: 'same-origin' });
if (!res.ok) {
console.error('Error fetching status data:', res.statusText);
return false;
}
const data = await res.json();
status.set(data);
return true;
};
const fetchSettingsData = () => {
fetch(PUBLIC_BASE_URL + `/api/settings`, { credentials: 'same-origin' })
.then((res) => res.json())
.then((data) => {
data.fgColor = String(data.fgColor);
data.bgColor = String(data.bgColor);
data.timePerScreen = data.timerSeconds / 60;
const fetchSettingsData = async () => {
const res = await fetch(PUBLIC_BASE_URL + `/api/settings`, { credentials: 'same-origin' });
if (data.fgColor > 65535) {
data.fgColor = '65535';
}
if (!res.ok) {
console.error('Error fetching settings data:', res.statusText);
return;
}
if (data.bgColor > 65535) {
data.bgColor = '65535';
}
settings.set(data);
});
const data = await res.json();
data.fgColor = String(data.fgColor);
data.bgColor = String(data.bgColor);
data.timePerScreen = data.timerSeconds / 60;
if (data.fgColor > 65535) {
data.fgColor = '65535';
}
if (data.bgColor > 65535) {
data.bgColor = '65535';
}
settings.set(data);
};
onMount(() => {
fetchSettingsData();
fetchStatusData();
let sections: (HTMLElement | null)[];
let observer: IntersectionObserver;
const SM_BREAKPOINT = 576;
const evtSource = new EventSource(`${PUBLIC_BASE_URL}/events`);
const setupObserver = () => {
if (window.innerWidth < SM_BREAKPOINT) {
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const id = entry.target.id;
replaceState(`#${id}`);
evtSource.addEventListener('status', (e) => {
let dataObj = JSON.parse(e.data);
status.set(dataObj);
});
// Update nav pills
document.querySelectorAll('.nav-link').forEach((link) => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${id}`) {
link.classList.add('active');
}
});
}
});
},
{
threshold: 0.25 // Trigger when section is 50% visible
}
);
sections = ['control', 'status', 'settings'].map((id) => document.getElementById(id));
sections.forEach((section) => observer.observe(section!));
}
};
onMount(async () => {
setupObserver();
const connectEventSource = () => {
const evtSource = new EventSource(`${PUBLIC_BASE_URL}/events`);
evtSource.addEventListener('status', (e) => {
let dataObj = JSON.parse(e.data);
status.update((s) => ({ ...s, isUpdating: true }));
status.set(dataObj);
});
evtSource.addEventListener('message', (e) => {
if (e.data == 'closing') {
console.log('EventSource closing');
status.update((s) => ({ ...s, isUpdating: false }));
evtSource.close(); // Close the current connection
setTimeout(connectEventSource, 5000);
}
});
evtSource.addEventListener('error', (e) => {
console.error('EventSource failed:', e);
status.update((s) => ({ ...s, isUpdating: false }));
evtSource.close(); // Close the current connection
setTimeout(connectEventSource, 1000);
});
};
try {
await fetchSettingsData();
if (await fetchStatusData()) {
settings.update((s) => ({ ...s, isLoaded: true }));
connectEventSource();
}
} catch (error) {
console.log('Error fetching data:', error);
}
function handleResize() {
if (observer) {
observer.disconnect();
}
setupObserver();
updateScreenSize();
}
@ -123,9 +197,11 @@
</svelte:head>
<Container fluid>
<Row>
<Row class="placeholder-glow">
<Control bind:settings on:showToast={showToast} bind:status lg="3" xxl="4"></Control>
<Status bind:settings bind:status lg="6" xxl="4"></Status>
<Settings bind:settings on:showToast={showToast} on:formReset={fetchSettingsData} lg="3" xxl="4"
></Settings>
</Row>

View file

@ -17,6 +17,7 @@
} from '@sveltestrap/sveltestrap';
import FirmwareUpdater from './FirmwareUpdater.svelte';
import { uiSettings } from '$lib/uiSettings';
import { Placeholder } from '$lib/components';
export let settings = {};
@ -105,8 +106,8 @@
export let xxl = xl;
</script>
<Col {xs} {sm} {md} {lg} {xl} {xxl}>
<Card>
<Col {xs} {sm} {md} {lg} {xl} {xxl} class="mb-4 mb-xl-0">
<Card id="control">
<CardHeader>
<CardTitle>{$_('section.control.title', { default: 'Control' })}</CardTitle>
</CardHeader>
@ -214,15 +215,16 @@
</li>
{/if}
<li>
{$_('section.control.buildTime')}: {new Date(
$settings.lastBuildTime * 1000
).toLocaleString()}
{$_('section.control.buildTime')}: <Placeholder
value={new Date($settings.lastBuildTime * 1000).toLocaleString()}
checkValue={$settings.lastBuildTime}
/>
</li>
<li>IP: {$settings.ip}</li>
<li>HW revision: {$settings.hwRev}</li>
<li>{$_('section.control.fwCommit')}: {$settings.gitRev}</li>
<li>WebUI commit: {$settings.fsRev}</li>
<li>{$_('section.control.hostname')}: {$settings.hostname}</li>
<li>IP: <Placeholder value={$settings.ip} /></li>
<li>HW revision: <Placeholder value={$settings.hwRev} /></li>
<li>{$_('section.control.fwCommit')}: <Placeholder value={$settings.gitRev} /></li>
<li>WebUI commit: <Placeholder value={$settings.fsRev} /></li>
<li>{$_('section.control.hostname')}: <Placeholder value={$settings.hostname} /></li>
</ul>
<Row>
<Col class="d-flex justify-content-end">
@ -239,7 +241,7 @@
{#if $settings.otaEnabled}
<hr />
<h3>{$_('section.control.firmwareUpdate')}</h3>
<FirmwareUpdater on:showToast bind:settings />
<FirmwareUpdater on:showToast bind:settings bind:status />
{/if}
</CardBody>
</Card>

View file

@ -4,11 +4,12 @@
import { _ } from 'svelte-i18n';
import { writable } from 'svelte/store';
import { Progress, Alert, Button } from '@sveltestrap/sveltestrap';
import HourglassSplitIcon from 'svelte-bootstrap-icons/lib/HourglassSplit.svelte';
const dispatch = createEventDispatcher();
export let settings = { hwRev: '' };
export let status = writable({ isOTAUpdating: false });
let currentVersion: string = $settings.gitTag; // Replace with your current version
let latestVersion: string = '';
@ -112,6 +113,25 @@
return binaryFilename;
};
const getWebUiBinaryName = () => {
let webuiFilename = '';
switch ($settings.hwRev) {
case 'REV_V8_EPD_2_13':
webuiFilename = 'littlefs_16MB.bin';
break;
case 'REV_B_EPD_2_13':
webuiFilename = 'littlefs_8MB.bin';
break;
case 'REV_A_EPD_2_13':
webuiFilename = 'littlefs_4MB.bin';
break;
default:
webuiFilename = 'Unsupported hardware, unable to determine WebUI binary filename';
}
return webuiFilename;
};
const onAutoUpdate = async (e: Event) => {
e.preventDefault();
@ -188,8 +208,12 @@
)}: {releaseDate} -
<a href={releaseUrl} target="_blank">{$_('section.firmwareUpdater.viewRelease')}</a><br />
{#if isNewerVersionAvailable}
{$_('section.firmwareUpdater.swUpdateAvailable')} -
<a href="/" on:click={onAutoUpdate}>{$_('section.firmwareUpdater.autoUpdate')}</a>.
{#if !$status.isOTAUpdating}
{$_('section.firmwareUpdater.swUpdateAvailable')} -
<a href="/" on:click={onAutoUpdate}>{$_('section.firmwareUpdater.autoUpdate')}</a>.
{:else}
<HourglassSplitIcon /> {$_('section.firmwareUpdater.autoUpdateInProgress')}
{/if}
{:else}
{$_('section.firmwareUpdater.swUpToDate')}
{/if}
@ -199,57 +223,59 @@
{:else}
<p>Loading...</p>
{/if}
<section class="row row-cols-lg-auto align-items-end">
<div class="col-12">
<label for="firmwareFile" class="form-label">Firmware file ({getFirmwareBinaryName()})</label>
<input
type="file"
id="firmwareFile"
on:change={(e) => handleFileChange(e, (file) => (firmwareUploadFile = file))}
name="update"
class="form-control"
accept=".bin"
/>
</div>
<div class="flex-fill">
<Button block on:click={uploadFirmwareFile} color="primary" disabled={!firmwareUploadFile}
>Update firmware</Button
{#if !$status.isOTAUpdating}
<section class="row row-cols-lg-auto align-items-end">
<div class="col flex-fill">
<label for="firmwareFile" class="form-label">Firmware file ({getFirmwareBinaryName()})</label>
<input
type="file"
id="firmwareFile"
on:change={(e) => handleFileChange(e, (file) => (firmwareUploadFile = file))}
name="update"
class="form-control"
accept=".bin"
/>
</div>
<div class="flex-fill">
<Button block on:click={uploadFirmwareFile} color="primary" disabled={!firmwareUploadFile}
>Update firmware</Button
>
</div>
<div class="col flex-fill">
<label for="webuiFile" class="form-label">WebUI file ({getWebUiBinaryName()})</label>
<input
type="file"
id="webuiFile"
name="update"
class="form-control"
placeholder="littlefs.bin"
on:change={(e) => handleFileChange(e, (file) => (firmwareWebUiFile = file))}
accept=".bin"
/>
</div>
<div class="flex-fill">
<Button block on:click={uploadWebUiFile} color="secondary" disabled={!firmwareWebUiFile}
>Update WebUI</Button
>
</div>
</section>
{#if firmwareUploadProgress > 0}
<Progress striped value={firmwareUploadProgress} class="progress" id="firmwareUploadProgress"
>{$_('section.firmwareUpdater.uploading')}... {firmwareUploadProgress}%</Progress
>
</div>
<div class="col mt-2">
<label for="webuiFile" class="form-label">WebUI file (littlefs.bin)</label>
<input
type="file"
id="webuiFile"
name="update"
class="form-control"
placeholder="littlefs.bin"
on:change={(e) => handleFileChange(e, (file) => (firmwareWebUiFile = file))}
accept=".bin"
/>
</div>
<div class="flex-fill">
<Button block on:click={uploadWebUiFile} color="secondary" disabled={!firmwareWebUiFile}
>Update WebUI</Button
>
</div>
</section>
{#if firmwareUploadProgress > 0}
<Progress striped value={firmwareUploadProgress} class="progress" id="firmwareUploadProgress"
>{$_('section.firmwareUpdater.uploading')}... {firmwareUploadProgress}%</Progress
>
{/if}
{#if firmwareUploadSuccess}
<Alert color="success" class="firmwareUploadStatusAlert"
>{$_('section.firmwareUpdater.fileUploadSuccess', { values: { countdown: $countdown } })}
</Alert>
{/if}
{/if}
{#if firmwareUploadSuccess}
<Alert color="success" class="firmwareUploadStatusAlert"
>{$_('section.firmwareUpdater.fileUploadSuccess', { values: { countdown: $countdown } })}
</Alert>
{/if}
{#if firmwareUploadError}
<Alert color="danger" class="firmwareUploadStatusAlert"
>{$_('section.firmwareUpdater.fileUploadFailed')}</Alert
{#if firmwareUploadError}
<Alert color="danger" class="firmwareUploadStatusAlert"
>{$_('section.firmwareUpdater.fileUploadFailed')}</Alert
>
{/if}
<small
>⚠️ <strong>{$_('warning')}</strong>: {$_('section.firmwareUpdater.firmwareUpdateText')}</small
>
{/if}
<small
>⚠️ <strong>{$_('warning')}</strong>: {$_('section.firmwareUpdater.firmwareUpdateText')}</small
>

View file

@ -9,7 +9,7 @@
};
export let className = 'btclock-wrapper';
export let verticalDesc = false;
// Define the currency symbols as constants
const CURRENCY_USD = '$';
const CURRENCY_EUR = '[';
@ -44,21 +44,22 @@
</script>
<div class={className} id={className}>
<div class="btclock">
<div class={'btclock' + (verticalDesc ? ' verticalDesc' : '')}>
{#each status.data as char}
{#if isSplitText(char)}
<div class="splitText">
{#if char.split('/').length}
<span class="top-text">{char.split('/')[0]}</span>
<hr />
<span class="bottom-text">{char.split('/')[1]}</span>
{/if}
<div class="textcontainer">
{#if char.split('/').length}
<span class="top-text">{char.split('/')[0]}</span>
<span class="bottom-text">{char.split('/')[1]}</span>
{/if}
</div>
<!-- {#each char.split('/') as part}
<div class="flex-items">{part}</div>
{/each} -->
</div>
{:else if char.startsWith('mdi')}
<div class="digit icon">
<div class={'digit icon' + (char.endsWith('bitaxe') ? ' icon-img' : '')}>
{#if char.endsWith('rocket')}
<RocketIcon></RocketIcon>
{/if}
@ -68,6 +69,12 @@
{#if char.endsWith('bolt')}
<ZapIcon></ZapIcon>
{/if}
{#if char.endsWith('bitaxe')}
<img src="/bitaxe.webp" class="bitaxelogo" alt="BitAxe logo" />
{/if}
{#if char.endsWith('miningpool')}
<span class="pool-logo">Mining Pool Logo</span>
{/if}
</div>
{:else if char === 'STS'}
<div class="digit sats">S</div>
@ -82,8 +89,26 @@
</div>
</div>
<style>
<style lang="scss">
.icon {
fill: currentColor;
}
.btclock-wrapper .btclock .icon.icon-img {
// padding: 0 15px;
aspect-ratio: 1;
width: calc(100 / 7);
img {
max-width: 95%;
}
}
.bitaxelogo {
transform: rotate(-90deg);
}
.pool-logo {
font-size: 0.75rem;
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
Row
} from '@sveltestrap/sveltestrap';
import Rendered from './Rendered.svelte';
import { DataSourceType } from '$lib/types/dataSource';
export let settings;
export let status: writable<object>;
@ -74,7 +75,7 @@
});
settings.subscribe((value: object) => {
lightMode = value.bgColor > value.fgColor;
lightMode = !value.invertedColor;
if (value.screens) buttonChunks = chunkArray(value.screens, 5);
});
@ -96,6 +97,16 @@
}
};
const toggleDoNotDisturb = (currentStatus: boolean) => (e: Event) => {
e.preventDefault();
console.log(currentStatus);
if (!currentStatus) {
fetch(`${PUBLIC_BASE_URL}/api/dnd/enable`);
} else {
fetch(`${PUBLIC_BASE_URL}/api/dnd/disable`);
}
};
export let xs = 12;
export let sm = xs;
export let md = sm;
@ -104,17 +115,35 @@
export let xxl = xl;
</script>
<Col {xs} {sm} {md} {lg} {xl} {xxl}>
<Card>
<Col {xs} {sm} {md} {lg} {xl} {xxl} class="mb-4 mb-xl-0">
<Card id="status">
<CardHeader>
<CardTitle>{$_('section.status.title', { default: 'Status' })}</CardTitle>
</CardHeader>
<CardBody>
{#if $settings.screens}
<div class=" d-block d-sm-none mx-auto text-center">
{#each buttonChunks as chunk}
<ButtonGroup size="sm" class="mx-auto mb-1">
{#each chunk as s}
{#if $settings.isLoaded === false}
<div class="d-flex align-items-center">
<strong role="status">Loading...</strong>
<div class="spinner-border ms-auto" aria-hidden="true"></div>
</div>
{:else}
{#if $settings.screens}
<div class=" d-block d-sm-none mx-auto text-center">
{#each buttonChunks as chunk}
<ButtonGroup size="sm" class="mx-auto mb-1">
{#each chunk as s}
<Button
color="outline-primary"
active={$status.currentScreen == s.id}
on:click={setScreen(s.id)}>{s.name}</Button
>
{/each}
</ButtonGroup>
{/each}
</div>
<div class="d-flex justify-content-center d-none d-sm-flex">
<ButtonGroup size="sm">
{#each $settings.screens as s}
<Button
color="outline-primary"
active={$status.currentScreen == s.id}
@ -122,144 +151,169 @@
>
{/each}
</ButtonGroup>
{/each}
</div>
<div class="d-flex justify-content-center d-none d-sm-flex">
<ButtonGroup size="sm">
{#each $settings.screens as s}
<Button
color="outline-primary"
active={$status.currentScreen == s.id}
on:click={setScreen(s.id)}>{s.name}</Button
>
{/each}
</ButtonGroup>
</div>
{#if $settings.actCurrencies && $settings.ownDataSource}
<div class="d-flex justify-content-center d-sm-flex mt-2">
<ButtonGroup size="sm">
{#each $settings.actCurrencies as c}
<Button
color="outline-success"
active={$status.currency == c}
on:click={setCurrency(c)}>{c}</Button
>
{/each}
</ButtonGroup>
</div>
{/if}
<hr />
{#if $status.data}
<section class={lightMode ? 'lightMode' : 'darkMode'}>
<Rendered status={$status} className="btclock-wrapper"></Rendered>
</section>
{$_('section.status.screenCycle')}:
<a
id="timerStatusText"
href={'#'}
style="cursor: pointer"
tabindex="0"
role="button"
aria-pressed="false"
on:click={toggleTimer($status.timerRunning)}
>{#if $status.timerRunning}&#9205; {$_('timer.running')}{:else}&#9208; {$_(
'timer.stopped'
)}{/if}</a
>
{/if}
{/if}
<hr />
{#if !$settings.disableLeds}
<Row class="justify-content-evenly">
{#if $status.leds}
{#each $status.leds as led}
<Col>
<Input
type="color"
id="ledColorPicker"
bind:value={led.hex}
class="mx-auto"
disabled
/>
</Col>
{/each}
{#if $settings.actCurrencies && ($settings.dataSource == DataSourceType.BTCLOCK_SOURCE || $settings.dataSource == DataSourceType.CUSTOM_SOURCE)}
<div class="d-flex justify-content-center d-sm-flex mt-2">
<ButtonGroup size="sm">
{#each $settings.actCurrencies as c}
<Button
color="outline-success"
active={$status.currency == c}
on:click={setCurrency(c)}>{c}</Button
>
{/each}
</ButtonGroup>
</div>
{/if}
</Row>
<hr />
{/if}
<Progress striped value={memoryFreePercent}>{memoryFreePercent}%</Progress>
<div class="d-flex justify-content-between">
<div>{$_('section.status.memoryFree')}</div>
<div>
{Math.round($status.espFreeHeap / 1024)} / {Math.round($status.espHeapSize / 1024)} KiB
</div>
</div>
<hr />
{#if $settings.hasLightLevel}
{$_('section.status.lightSensor')}: {Number(Math.round($status.lightLevel))} lux
<hr />
{/if}
<Progress striped id="rssiBar" color={wifiStrengthColor} value={rssiPercent}
>{rssiPercent}%</Progress
>
<Tooltip target="rssiBar" placement="bottom">{$_('rssiBar.tooltip')}</Tooltip>
<hr />
{#if $status.data}
<section class={lightMode ? 'lightMode' : 'darkMode'} style="position: relative;">
{#if $status.isUpdating === false && ($status.isFake ?? false) === false}
<div class="connection-lost-overlay">
<div class="overlay-content">
<i class="bi bi-wifi-off"></i>
<h4>Lost connection</h4>
<p>Trying to reconnect...</p>
</div>
</div>
{/if}
<Rendered
status={$status}
className="btclock-wrapper"
verticalDesc={$settings.verticalDesc}
></Rendered>
</section>
{$_('section.status.screenCycle')}:
<a
id="timerStatusText"
href={'#'}
style="cursor: pointer"
tabindex="0"
role="button"
aria-pressed="false"
on:click={toggleTimer($status.timerRunning)}
>{#if $status.timerRunning}&#9205; {$_('timer.running')}{:else}&#9208; {$_(
'timer.stopped'
)}{/if}</a
><br />
<div class="d-flex justify-content-between">
<div>{$_('section.status.wifiSignalStrength')}</div>
<div>
{$status.rssi} dBm
</div>
</div>
<hr />
{$_('section.status.uptime')}: {toUptimestring($status.espUptime)}
<br />
<p>
{#if $settings.useNostr || $settings.nostrZapNotify}
{$_('section.status.nostrConnection')}:
<span>
{#if $status.connectionStatus && $status.connectionStatus.nostr}
&#9989;
{:else}
&#10060;
{/if}
</span>
{$_('section.status.doNotDisturb')}:
<a
id="dndStatusText"
href={'#'}
style="cursor: pointer"
tabindex="0"
role="button"
aria-pressed="false"
on:click={toggleDoNotDisturb($status.dnd?.enabled)}
>
{#if $status.dnd?.active}&#9205; {$_('on')}{:else}&#9208; {$_('off')}{/if}</a
>
<small>
{#if $status.dnd?.timeBasedEnabled}
{$_('section.status.timeBasedDnd')} ( {$settings.dnd
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings
.dnd.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
{/if}
</small>
{/if}
{/if}
{#if !$settings.useNostr}
{#if !$settings.ownDataSource}
{$_('section.status.wsPriceConnection')}:
<hr />
{#if !$settings.disableLeds}
<Row class="justify-content-evenly">
{#if $status.leds}
{#each $status.leds as led}
<Col>
<Input
type="color"
id="ledColorPicker"
bind:value={led.hex}
class="mx-auto"
disabled
/>
</Col>
{/each}
{/if}
</Row>
<hr />
{/if}
<Progress striped value={memoryFreePercent}>{memoryFreePercent}%</Progress>
<div class="d-flex justify-content-between">
<div>{$_('section.status.memoryFree')}</div>
<div>
{Math.round($status.espFreeHeap / 1024)} / {Math.round($status.espHeapSize / 1024)} KiB
</div>
</div>
<hr />
{#if $settings.hasLightLevel}
{$_('section.status.lightSensor')}: {Number(Math.round($status.lightLevel))} lux
<hr />
{/if}
<Progress striped id="rssiBar" color={wifiStrengthColor} value={rssiPercent}
>{rssiPercent}%</Progress
>
<Tooltip target="rssiBar" placement="bottom">{$_('rssiBar.tooltip')}</Tooltip>
<div class="d-flex justify-content-between">
<div>{$_('section.status.wifiSignalStrength')}</div>
<div>
{$status.rssi} dBm
</div>
</div>
<hr />
{$_('section.status.uptime')}: {toUptimestring($status.espUptime)}
<br />
<p>
{#if $settings.dataSource == DataSourceType.NOSTR_SOURCE || $settings.nostrZapNotify}
{$_('section.status.nostrConnection')}:
<span>
{#if $status.connectionStatus && $status.connectionStatus.price}
&#9989;
{:else}
&#10060;
{/if}
</span>
-
{$_('section.status.wsMempoolConnection', {
values: { instance: $settings.mempoolInstance }
})}:
<span>
{#if $status.connectionStatus && $status.connectionStatus.blocks}
&#9989;
{:else}
&#10060;
{/if}
</span><br />
{:else}
{$_('section.status.wsDataConnection')}:
<span>
{#if $status.connectionStatus && $status.connectionStatus.V2}
{#if $status.connectionStatus && $status.connectionStatus.nostr}
&#9989;
{:else}
&#10060;
{/if}
</span>
{/if}
{/if}
{#if $settings.fetchEurPrice}
<small>{$_('section.status.fetchEuroNote')}</small>
{/if}
</p>
{#if $settings.dataSource != DataSourceType.NOSTR_SOURCE}
{#if $settings.dataSource == DataSourceType.THIRD_PARTY_SOURCE}
{$_('section.status.wsPriceConnection')}:
<span>
{#if $status.connectionStatus && $status.connectionStatus.price}
&#9989;
{:else}
&#10060;
{/if}
</span>
-
{$_('section.status.wsMempoolConnection', {
values: { instance: $settings.mempoolInstance }
})}:
<span>
{#if $status.connectionStatus && $status.connectionStatus.blocks}
&#9989;
{:else}
&#10060;
{/if}
</span><br />
{:else}
{$_('section.status.wsDataConnection')}:
<span>
{#if $status.connectionStatus && $status.connectionStatus.V2}
&#9989;
{:else}
&#10060;
{/if}
</span>
{/if}
{/if}
{#if $settings.fetchEurPrice}
<small>{$_('section.status.fetchEuroNote')}</small>
{/if}
</p>
{/if}
</CardBody>
</Card>
</Col>
<style lang="scss">
</style>

BIN
static/bitaxe.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

View file

@ -0,0 +1,70 @@
import { test, expect } from '@playwright/test';
import { initMock, settingsJson, statusJson } from '../shared';
import sharp from 'sharp';
test.beforeEach(initMock);
// Define the translations for the headings
const headings = {
en: {
control: 'Control',
status: 'Status',
settings: 'Settings',
language: 'English'
},
de: {
control: 'Steuerung',
status: 'Status',
settings: 'Einstellungen',
language: 'Deutsch'
},
nl: {
control: 'Besturing',
status: 'Status',
settings: 'Instellingen',
language: 'Nederlands'
},
es: {
control: 'Control',
status: 'Estado',
settings: 'Ajustes',
language: 'Español'
}
};
test('capture screenshots across devices', async ({ page }, testInfo) => {
// Get the locale from the browser or default to 'en'
const locale = testInfo.project.use?.locale?.split('-')[0].toLowerCase() || 'en';
const translations = headings[locale] || headings.en;
statusJson.isUpdating = true;
// Set the color scheme
if (testInfo.project.use?.colorScheme === 'dark') {
settingsJson.invertedColor = true;
} else {
settingsJson.invertedColor = false;
}
await page.goto('/');
await expect(page.getByRole('heading', { name: translations.control })).toBeVisible();
await expect(page.getByRole('heading', { name: translations.status })).toBeVisible();
await expect(page.getByRole('heading', { name: translations.settings })).toBeVisible();
if (await page.locator('#nav-language-dropdown').isVisible()) {
await expect(page.getByRole('link', { name: translations.language })).toBeVisible();
}
const screenshot = await page.screenshot({
fullPage: true
});
await sharp(screenshot)
.toFormat('webp', {
quality: 95,
nearLossless: true
})
.toFile(
`./doc/screenshot-${test.info().project.use.colorScheme?.toLowerCase().replace(' ', '_')}.webp`
);
});

View file

@ -1,114 +1,7 @@
import { expect, test } from '@playwright/test';
import { initMock, settingsJson, statusJson } from '../shared';
const statusJson = {
currentScreen: 0,
numScreens: 7,
timerRunning: true,
espUptime: 4479,
espFreeHeap: 58508,
espHeapSize: 342108,
connectionStatus: { price: true, blocks: true },
rssi: -66,
data: ['BLOCK/HEIGHT', '8', '1', '8', '0', '2', '6'],
rendered: ['BLOCK/HEIGHT', '8', '1', '8', '0', '2', '6'],
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' }
]
};
const settingsJson = {
numScreens: 7,
fgColor: 415029,
bgColor: 0,
timerSeconds: 1800,
timerRunning: true,
minSecPriceUpd: 30,
fullRefreshMin: 60,
wpTimeout: 600,
tzOffset: 0,
useBitcoinNode: false,
mempoolInstance: 'mempool.space',
ledTestOnPower: true,
ledFlashOnUpd: true,
ledBrightness: 128,
stealFocus: true,
mcapBigChar: true,
mdnsEnabled: true,
otaEnabled: true,
fetchEurPrice: false,
hostnamePrefix: 'btclock',
hostname: 'btclock-d60b14',
ip: '192.168.20.231',
txPower: 78,
gitRev: '25d8b92bcbc8938417c140355ea3ba99ff9eb4b7',
gitTag: '3.1.9',
bitaxeEnabled: false,
bitaxeHostname: 'bitaxe1',
nostrZapNotify: true,
hwRev: 'REV_A_EPD_2_13',
fsRev: '4c5d9616212b27e3f05c35370f0befcf2c5a04b2',
nostrZapPubkey: 'b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422',
lastBuildTime: '1700666677',
screens: [
{ id: 0, name: 'Block Height', enabled: true },
{ id: 1, name: 'Sats per dollar', enabled: true },
{ id: 2, name: 'Ticker', enabled: true },
{ id: 3, name: 'Time', enabled: true },
{ id: 4, name: 'Halving countdown', enabled: true },
{ id: 5, name: 'Market Cap', enabled: true }
]
};
test.beforeEach(async ({ page }) => {
await page.route('*/**/api/status', async (route) => {
await route.fulfill({ json: statusJson });
});
await page.route('*/**/api/show/screen/1', async (route) => {
//if (route.request().url().includes('*/**/api/show/screen/1')) {
statusJson.currentScreen = 1;
statusJson.data = ['MSCW/TIME', ' ', ' ', '2', '6', '4', '4'];
statusJson.rendered = statusJson.data;
//}
await route.fulfill({ json: statusJson });
});
await page.route('*/**/api/show/screen/2', async (route) => {
statusJson.currentScreen = 2;
statusJson.data = ['BTC/USD', '$', '3', '7', '8', '2', '4'];
statusJson.rendered = statusJson.data;
await route.fulfill({ json: statusJson });
});
await page.route('*/**/api/show/screen/4', async (route) => {
statusJson.currentScreen = 4;
statusJson.data = ['BIT/COIN', 'HALV/ING', '0/YRS', '149/DAYS', '8/HRS', '30/MINS', 'TO/GO'];
statusJson.rendered = statusJson.data;
await route.fulfill({ json: statusJson });
});
await page.route('*/**/api/settings', async (route) => {
await route.fulfill({ json: settingsJson });
});
await page.route('**/events', (route) => {
const newStatus = statusJson;
newStatus.data = ['BLOCK/HEIGHT', '8', '0', '0', '8', '1', '5'];
// Respond with a custom SSE message
route.fulfill({
status: 200,
contentType: 'text/event-stream',
json: `${JSON.stringify(newStatus)}\n\n`
});
});
});
test.beforeEach(initMock);
test('index page has expected columns control, status, settings', async ({ page }) => {
await page.goto('/');
@ -181,6 +74,8 @@ test('time values can not be zero or negative', async ({ page }) => {
});
test('info message when fetch eur price is enabled', async ({ page }) => {
delete (settingsJson as { actCurrencies?: string[] }).actCurrencies;
await page.goto('/');
await page.getByRole('button', { name: 'Show all' }).click();
@ -237,7 +132,7 @@ test('screens should be able to change', async ({ page }) => {
await page.getByRole('button', { name: 'Sats per Dollar' }).click();
const response = await responsePromise;
expect(response.url()).toContain('api/show/screen/1');
expect(response.url()).toContain('api/show/screen/10');
});
test('parse all types of EPD content correctly', async ({ page }) => {

View file

@ -0,0 +1,132 @@
import { test, expect } from '@playwright/test';
import { initMock, settingsJson, statusJson } from '../shared';
test.beforeEach(initMock);
// Define the translations for the headings
const headings = {
en: {
control: 'Control',
status: 'Status',
settings: 'Settings',
language: 'English'
},
de: {
control: 'Steuerung',
status: 'Status',
settings: 'Einstellungen',
language: 'Deutsch'
},
nl: {
control: 'Besturing',
status: 'Status',
settings: 'Instellingen',
language: 'Nederlands'
},
es: {
control: 'Control',
status: 'Estado',
settings: 'Ajustes',
language: 'Español'
}
};
test('capture screenshots across devices', async ({ page }, testInfo) => {
// Get the locale from the browser or default to 'en'
const locale = testInfo.project.use?.locale?.split('-')[0].toLowerCase() || 'en';
const translations = headings[locale] || headings.en;
await page.goto('/');
await expect(page.getByRole('heading', { name: translations.control })).toBeVisible();
await expect(page.getByRole('heading', { name: translations.status })).toBeVisible();
await expect(page.getByRole('heading', { name: translations.settings })).toBeVisible();
if (await page.locator('#nav-language-dropdown').isVisible()) {
await expect(page.getByRole('link', { name: translations.language })).toBeVisible();
}
const screenshot = await page.screenshot({
path: `./test-results/screenshots/default-${test.info().project.name.toLowerCase().replace(' ', '_')}.png`
});
await testInfo.attach(`default`, {
body: screenshot,
contentType: 'image/png'
});
});
test('capture screenshots across devices with bitaxe screens', async ({ page }, testInfo) => {
const locale = testInfo.project.use?.locale?.split('-')[0].toLowerCase() || 'en';
const translations = headings[locale] || headings.en;
settingsJson.screens = [
{
id: 0,
name: 'Block Height',
enabled: true
},
{
id: 3,
name: 'Time',
enabled: true
},
{
id: 4,
name: 'Halving countdown',
enabled: true
},
{
id: 6,
name: 'Block Fee Rate',
enabled: true
},
{
id: 10,
name: 'Sats per dollar',
enabled: true
},
{
id: 20,
name: 'Ticker',
enabled: true
},
{
id: 30,
name: 'Market Cap',
enabled: true
},
{
id: 80,
name: 'BitAxe Hashrate',
enabled: true
},
{
id: 81,
name: 'BitAxe Best Difficulty',
enabled: true
}
];
statusJson.data = ['mdi:bitaxe', '', 'mdi:pickaxe', '6', '3', '7', 'GH/S'];
statusJson.rendered = ['mdi:bitaxe', '', 'mdi:pickaxe', '6', '3', '7', 'GH/S'];
await page.goto('/');
await expect(page.getByRole('heading', { name: translations.control })).toBeVisible();
await expect(page.getByRole('heading', { name: translations.status })).toBeVisible();
await expect(page.getByRole('heading', { name: translations.settings })).toBeVisible();
if (await page.locator('#nav-language-dropdown').isVisible()) {
await expect(page.getByRole('link', { name: translations.language })).toBeVisible();
}
await page.screenshot({
path: `./test-results/screenshots/bitaxe-${test.info().project.name.toLowerCase().replace(' ', '_')}.png`
});
await testInfo.attach(`bitaxe`, {
path: `./test-results/screenshots/bitaxe-${test.info().project.name.toLowerCase().replace(' ', '_')}.png`,
contentType: 'image/png'
});
});

218
tests/shared.ts Normal file
View file

@ -0,0 +1,218 @@
export const statusJson = {
currentScreen: 20,
numScreens: 7,
timerRunning: true,
isOTAUpdating: false,
espUptime: 4479,
espFreeHeap: 58508,
espHeapSize: 342108,
connectionStatus: {
price: false,
blocks: false,
V2: true,
nostr: true
},
rssi: -66,
data: ['BLOCK/HEIGHT', '8', '7', '6', '5', '4', '3'],
currency: 'USD',
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' }
],
isUpdating: true,
isFake: true,
dnd: {
enabled: true,
timeBasedEnabled: true,
startTime: '23:00',
endTime: '7:00',
active: true
}
};
export const settingsJson = {
numScreens: 7,
timerSeconds: 1800,
timerRunning: true,
minSecPriceUpd: 30,
fullRefreshMin: 60,
wpTimeout: 600,
tzOffset: 0,
dataSource: 0,
mempoolInstance: 'mempool.space',
ledTestOnPower: true,
ledFlashOnUpd: true,
ledBrightness: 128,
stealFocus: true,
mcapBigChar: true,
mdnsEnabled: true,
otaEnabled: true,
fetchEurPrice: false,
hostnamePrefix: 'btclock',
hostname: 'btclock-d60b14',
ip: '192.168.20.231',
txPower: 78,
gitRev: '25d8b92bcbc8938417c140355ea3ba99ff9eb4b7',
gitTag: '3.2.23',
bitaxeEnabled: false,
bitaxeHostname: 'bitaxe1',
miningPoolStats: false,
miningPoolName: 'ocean',
miningPoolUser: '38Qkkei3SuF1Eo45BaYmRHUneRD54yyTFy',
nostrZapNotify: true,
hwRev: 'REV_A_EPD_2_13',
fsRev: '64e518bf58f89749753167a8b6826e10bb6455c5',
nostrZapPubkey: 'b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422',
lastBuildTime: Math.round(new Date().getTime() / 1000),
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', 'EUR'],
availableCurrencies: ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD'],
availablePools: [
'ocean',
'noderunners',
'satoshi_radio',
'braiins',
'public_pool',
'gobrrr_pool',
'ckpool',
'eu_ckpool'
],
dnd: {
enabled: false,
timeBasedEnabled: true,
startHour: 23,
startMinute: 0,
endHour: 7,
endMinute: 0
},
availableFonts: ['antonio', 'oswald'],
invertedColor: false,
isLoaded: true,
isFake: true
};
export const latestReleaseFake = {
id: 782,
tag_name: '3.2.24',
target_commitish: '',
name: '3.2.24',
body: '',
url: 'https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/782',
html_url: 'https://git.btclock.dev/btclock/btclock_v3/releases/tag/3.2.24',
tarball_url: 'https://git.btclock.dev/btclock/btclock_v3/archive/3.2.24.tar.gz',
zipball_url: 'https://git.btclock.dev/btclock/btclock_v3/archive/3.2.24.zip',
hide_archive_links: false,
upload_url: 'https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/782/assets',
draft: false,
prerelease: false,
created_at: '2024-12-28T17:48:05Z',
published_at: '2024-12-28T17:48:05Z',
author: {},
assets: [],
archive_download_count: {
zip: 0,
tar_gz: 0
}
};
export const initMock = async ({ page }) => {
await page.route('*/**/api/status', async (route) => {
await route.fulfill({ json: statusJson });
});
await page.route('*/**/api/show/screen/10', async (route) => {
//if (route.request().url().includes('*/**/api/show/screen/1')) {
statusJson.currentScreen = 1;
statusJson.data = ['MSCW/TIME', ' ', ' ', '2', '6', '4', '4'];
await route.fulfill({ json: statusJson });
});
await page.route('*/**/api/show/screen/20', async (route) => {
statusJson.currentScreen = 2;
statusJson.data = ['BTC/USD', '$', '3', '7', '8', '2', '4'];
await route.fulfill({ json: statusJson });
});
await page.route('*/**/api/show/screen/4', async (route) => {
statusJson.currentScreen = 4;
statusJson.data = ['BIT/COIN', 'HALV/ING', '0/YRS', '149/DAYS', '8/HRS', '30/MINS', 'TO/GO'];
await route.fulfill({ json: statusJson });
});
await page.route('*/**/api/settings', async (route) => {
await route.fulfill({ json: settingsJson });
});
await page.route('**/events', async (route) => {
const newStatus = statusJson;
newStatus.data = ['BLOCK/HEIGHT', '8', '0', '0', '8', '1', '5'];
newStatus.isUpdating = true;
// Format the SSE message correctly
const sseMessage = `data: ${JSON.stringify(newStatus)}\n\n`;
// Create a readable stream for SSE
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(sseMessage));
// Keep the connection open
// controller.close(); // Don't close if you want to send more events
}
});
await route.fulfill({
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
},
body: stream
});
});
await page.route('**/api/v1/repos/btclock/btclock_v3/releases/latest', async (route) => {
await route.fulfill({ json: latestReleaseFake });
});
};

18
vite.config.test.ts Normal file
View file

@ -0,0 +1,18 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
build: {
sourcemap: true,
minify: false,
rollupOptions: {
output: {
manualChunks: undefined // Disable code splitting
}
}
},
test: {
include: ['tests/**/*.{test,spec}.{js,ts}']
}
});

View file

@ -1,6 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
import GithubActionsReporter from 'vitest-github-actions-reporter';
import { defineConfig } from 'vite';
// import { visualizer } from 'rollup-plugin-visualizer';
import * as fs from 'fs';
import * as path from 'path';
@ -65,21 +65,43 @@ export default defineConfig({
}
}
}
// visualizer({
// emitFile: true,
// filename: "stats.html",
// })
],
build: {
minify: true,
minify: 'esbuild',
cssCodeSplit: false,
chunkSizeWarningLimit: 550,
rollupOptions: {
output: {
manualChunks: () => 'app',
assetFileNames: '[name][extname]'
// assetFileNames: '[hash][extname]',
entryFileNames: `[hash][extname]`,
chunkFileNames: `[hash][extname]`,
assetFileNames: `[hash][extname]`,
preserveModules: false,
manualChunks: () => {
return 'app';
}
}
}
},
css: {
preprocessorOptions: {
scss: {
quietDeps: true,
silenceDeprecations: ['import']
}
}
},
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
globals: true,
environment: 'jsdom',
reporters: process.env.GITHUB_ACTIONS ? ['default', new GithubActionsReporter()] : 'default'
environment: 'jsdom'
},
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
}
});

940
yarn.lock

File diff suppressed because it is too large Load diff