Compare commits

...

39 commits

Author SHA1 Message Date
ed6bb5ccca still untested 2024-12-16 17:38:53 -06:00
908c750c79 work-in-progress, untested 2024-12-16 17:32:03 -06:00
Djuri Baars
653a39d0a3 Improvements for xs screens 2024-12-12 23:04:13 +01:00
Djuri Baars
68c247f3cc Fix screen selector UI, add screenshot maker 2024-12-12 19:50:36 +01:00
Djuri Baars
25e91b2086 Dependencies update, add switch for frontlight off when dark 2024-12-10 14:49:44 +01:00
Djuri Baars
f0fa58b5ea Fix LittleFS image generation 2024-11-29 00:57:07 +01:00
Djuri Baars
b8ed628bf5 Fix formatting 2024-11-29 00:13:43 +01:00
Djuri Baars
00af5f6521 Dependency updates and small fixes 2024-11-29 00:10:33 +01:00
Djuri Baars
51cce2ee9f Add color mode switcher 2024-11-28 23:30:14 +01:00
Djuri Baars
de99a221d6 Fix tests 2024-11-28 17:49:38 +01:00
Djuri Baars
93482b3be2 Dependency updates, increase fs allowance, split up settings section and add settings 2024-11-28 17:40:10 +01:00
Djuri Baars
d74e9dab60 Add Mow suffix mode setting 2024-11-27 10:03:32 +01:00
Djuri Baars
da3c70285d Add Forgejo action
Fix forgejo workflow

Fix workflow

Fix workflow python version

Fix workflow container

Forgejo js image

use upload artifacts fork
2024-11-26 17:35:26 +01:00
Djuri Baars
5066032a55 Change release checker endpoint, update dependencies 2024-11-25 23:58:21 +01:00
Djuri Baars
5346938159 New patch 2024-11-05 13:15:22 +01:00
Djuri Baars
c8e68faf69 New patch 2024-11-05 13:15:06 +01:00
Djuri Baars
eeeb0ee62c Dependency updates 2024-11-05 13:05:46 +01:00
Djuri Baars
384b4317c4 Dependency updates 2024-10-23 00:30:52 +02:00
Djuri Baars
9867988a09 Dependency updates, add clarification for Nostr Datasource 2024-10-01 20:34:41 +02:00
Djuri Baars
6f0e343429 Dependency updates 2024-09-25 00:53:52 +02:00
Djuri Baars
95aa9d67d1 Update dependencies, add v8 firmware support 2024-09-21 15:40:13 +02:00
Djuri Baars
7d82b1e1a9 Fix currency converter in WebUI 2024-09-18 01:49:18 +02:00
Djuri Baars
761c7f2991 Dependency updates 2024-09-16 21:22:34 +02:00
Djuri Baars
1447917955 Add currency converter, fix for display light toggler 2024-09-16 21:15:56 +02:00
Djuri Baars
6c40b54273 Add auto-update functionality 2024-09-11 20:16:55 +02:00
Djuri Baars
1c2d8dcdd0 Bugfix and dependency updates 2024-09-11 02:26:10 +02:00
Djuri Baars
1fa62ca88d Minor dependency updates 2024-09-09 14:51:20 +02:00
Djuri Baars
2fffb3ef02 Add staging source toggle 2024-09-05 13:42:16 +02:00
Djuri Baars
3d69570099 Improve multi-currency support 2024-09-05 13:10:58 +02:00
Djuri Baars
3342e6a532
Merge pull request #15 from btclock/dependabot/npm_and_yarn/vitest/ui-2.0.5 2024-09-05 04:22:44 +03:00
Djuri Baars
97519a1ae7
Merge pull request #14 from btclock/dependabot/npm_and_yarn/vite-5.4.3 2024-09-05 04:22:07 +03:00
Djuri Baars
0843fae200
Merge pull request #17 from btclock/dependabot/npm_and_yarn/sveltejs/kit-2.5.26 2024-09-05 04:21:49 +03:00
Djuri Baars
87b53165bf
Merge pull request #16 from btclock/dependabot/npm_and_yarn/sass-1.78.0 2024-09-05 04:21:37 +03:00
Djuri Baars
53a242582c
Merge pull request #13 from btclock/dependabot/npm_and_yarn/tslib-2.7.0 2024-09-05 04:20:49 +03:00
dependabot[bot]
b5d384023b
Bump @sveltejs/kit from 2.5.25 to 2.5.26
Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.5.25 to 2.5.26.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.5.26/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-05 01:15:21 +00:00
dependabot[bot]
af81b14b86
Bump sass from 1.77.8 to 1.78.0
Bumps [sass](https://github.com/sass/dart-sass) from 1.77.8 to 1.78.0.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.77.8...1.78.0)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 01:25:08 +00:00
dependabot[bot]
80d1211f91
Bump @vitest/ui from 0.34.7 to 2.0.5
Bumps [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) from 0.34.7 to 2.0.5.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.0.5/packages/ui)

---
updated-dependencies:
- dependency-name: "@vitest/ui"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 01:25:01 +00:00
dependabot[bot]
eafe9d6341
Bump vite from 5.4.2 to 5.4.3
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.2 to 5.4.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 01:24:36 +00:00
dependabot[bot]
1d5efa42e8
Bump tslib from 2.6.3 to 2.7.0
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.6.3 to 2.7.0.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/v2.6.3...v2.7.0)

---
updated-dependencies:
- dependency-name: tslib
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-04 01:24:27 +00:00
31 changed files with 2780 additions and 3163 deletions

View file

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

View file

@ -10,3 +10,6 @@ updates:
schedule: schedule:
interval: 'daily' interval: 'daily'
versioning-strategy: 'increase-if-necessary' versioning-strategy: 'increase-if-necessary'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']

View file

@ -30,7 +30,11 @@ Make sure the postinstall script is ran, because otherwise the filenames are to
## Deploying ## 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: Then you can make a `LittleFS.bin` with mklittlefs:

View file

@ -13,6 +13,7 @@
"postinstall": "patch-package", "postinstall": "patch-package",
"test": "npm run test:integration && npm run test:unit", "test": "npm run test:integration && npm run test:unit",
"test:integration": "playwright test", "test:integration": "playwright test",
"test:screenshots": "playwright test -c playwright.screenshot.config.ts",
"test:unit": "vitest" "test:unit": "vitest"
}, },
"devDependencies": { "devDependencies": {
@ -23,41 +24,43 @@
"@sveltejs/vite-plugin-svelte": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.1",
"@testing-library/svelte": "^5.2.1", "@testing-library/svelte": "^5.2.1",
"@types/swagger-ui": "^3.52.4", "@types/swagger-ui": "^3.52.4",
"@typescript-eslint/eslint-plugin": "^8.4.0", "@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.4.0", "@typescript-eslint/parser": "^8.7.0",
"@vitest/ui": "^0.34.6", "@vitest/ui": "^2.0.5",
"eslint": "^9.0.0", "eslint": "^9.11.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0", "eslint-plugin-svelte": "^2.36.0",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"prettier": "^3.1.1", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.2.6",
"sass": "^1.69.5", "sass": "^1.79.3",
"svelte": "^4.2.7", "svelte": "^4.2.19",
"svelte-check": "^3.6.0", "svelte-check": "^4.0.2",
"svelte-preprocess": "^5.1.1", "svelte-preprocess": "^6.0.2",
"tslib": "^2.4.1", "tslib": "^2.7.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.7.0",
"vite": "^5.4.2", "vite": "^5.4.7",
"vitest": "^2.0.5", "vitest": "^2.1.1",
"vitest-github-actions-reporter": "^0.11.0" "vitest-github-actions-reporter": "^0.11.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/antonio": "^5.0.17", "@fontsource/antonio": "^5.1.0",
"@fontsource/oswald": "^5.0.17", "@fontsource/oswald": "^5.1.0",
"@fontsource/ubuntu": "^5.0.8", "@fontsource/ubuntu": "^5.1.0",
"@noble/secp256k1": "^2.1.0", "@noble/secp256k1": "^2.1.0",
"@playwright/test": "^1.46.0", "@playwright/test": "^1.46.0",
"@popperjs/core": "^2.11.8",
"@sveltestrap/sveltestrap": "^6.2.7", "@sveltestrap/sveltestrap": "^6.2.7",
"@testing-library/jest-dom": "^6.5.0", "@testing-library/jest-dom": "^6.5.0",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"msgpack-es": "^0.0.5",
"nostr-tools": "^2.7.1", "nostr-tools": "^2.7.1",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"svelte-i18n": "^4.0.0", "svelte-bootstrap-icons": "^3.1.1",
"swagger-ui": "^5.10.0" "svelte-i18n": "^4.0.0"
}, },
"resolutions": { "resolutions": {
"es5-ext": ">=0.10.64", "es5-ext": ">=0.10.64",

View file

@ -1,8 +1,8 @@
diff --git a/node_modules/@sveltejs/kit/src/exports/vite/index.js b/node_modules/@sveltejs/kit/src/exports/vite/index.js diff --git a/node_modules/@sveltejs/kit/src/exports/vite/index.js b/node_modules/@sveltejs/kit/src/exports/vite/index.js
index 2280025..cfbcfa9 100644 index 40fa4c6..738cabf 100644
--- a/node_modules/@sveltejs/kit/src/exports/vite/index.js --- a/node_modules/@sveltejs/kit/src/exports/vite/index.js
+++ b/node_modules/@sveltejs/kit/src/exports/vite/index.js +++ b/node_modules/@sveltejs/kit/src/exports/vite/index.js
@@ -621,9 +621,9 @@ async function kit({ svelte_config }) { @@ -655,9 +655,9 @@ async function kit({ svelte_config }) {
input, input,
output: { output: {
format: 'esm', format: 'esm',

View file

@ -0,0 +1,17 @@
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

@ -10,7 +10,7 @@ const config: PlaywrightTestConfig = {
port: 4173 port: 4173
}, },
reporter: process.env.CI ? 'github' : 'list', reporter: process.env.CI ? 'github' : 'list',
testDir: 'tests', testDir: 'tests/playwright',
testMatch: /(.+\.)?(test|spec)\.[jt]s/ testMatch: /(.+\.)?(test|spec)\.[jt]s/
}; };

View file

@ -0,0 +1,51 @@
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',
use: {
viewport: { width: 1512, height: 982 }
}
},
{
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 } }
}
]
});

View file

@ -0,0 +1,53 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from '@sveltestrap/sveltestrap';
type Theme = 'light' | 'dark' | 'auto';
let theme: Theme = 'auto';
// Set the theme based on user selection and store it in localStorage
function setTheme(newTheme: Theme) {
theme = newTheme;
localStorage.setItem('theme', newTheme);
applyTheme(newTheme);
}
// Apply the selected theme to the document
function applyTheme(selectedTheme: Theme) {
if (selectedTheme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-bs-theme', prefersDark ? 'dark' : 'light');
} else {
document.documentElement.setAttribute('data-bs-theme', selectedTheme);
}
}
// On component mount, check localStorage and apply the saved theme
onMount(() => {
const savedTheme = (localStorage.getItem('theme') as Theme) || 'auto';
applyTheme(savedTheme);
theme = savedTheme;
// Listen for changes in the system color scheme preference
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => {
if (theme === 'auto') {
applyTheme('auto');
}
});
});
</script>
<Dropdown inNavbar>
<DropdownToggle nav caret>
{theme === 'auto' ? '🌗' : theme === 'dark' ? '🌙' : '☀️'}
</DropdownToggle>
<DropdownMenu end>
<DropdownItem active={theme === 'light'} on:click={() => setTheme('light')}
>☀️ Light</DropdownItem
>
<DropdownItem active={theme === 'dark'} on:click={() => setTheme('dark')}>🌙 Dark</DropdownItem>
<DropdownItem active={theme === 'auto'} on:click={() => setTheme('auto')}>🌗 Auto</DropdownItem>
</DropdownMenu>
</Dropdown>

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { Fade } from '@sveltestrap/sveltestrap';
import CaretRightFill from 'svelte-bootstrap-icons/lib/CaretRightFill.svelte';
import CaretDownFill from 'svelte-bootstrap-icons/lib/CaretDownFill.svelte';
export let header;
export let defaultOpen = false;
export let isOpen = defaultOpen;
</script>
<h4 style="cursor: pointer">
<span
role="link"
on:click={() => (isOpen = !isOpen)}
tabindex="0"
on:keypress={() => (isOpen = !isOpen)}
>
{#if isOpen}
<CaretDownFill></CaretDownFill>
{:else}
<CaretRightFill></CaretRightFill>
{/if}
{header}
</span>
</h4>
<Fade {isOpen}>
<slot></slot>
</Fade>

View file

@ -1,13 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-eye"
viewBox="0 0 16 16"
>
<path
d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z"
/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0" />
</svg>

Before

Width:  |  Height:  |  Size: 530 B

View file

@ -1,18 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-eye-slash"
viewBox="0 0 16 16"
>
<path
d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7 7 0 0 0-2.79.588l.77.771A6 6 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755q-.247.248-.517.486z"
/>
<path
d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829"
/>
<path
d="M3.35 5.47q-.27.24-.518.487A13 13 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7 7 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 814 B

View file

@ -42,7 +42,22 @@
"httpAuthUser": "WebUI-Benutzername", "httpAuthUser": "WebUI-Benutzername",
"httpAuthPass": "WebUI-Passwort", "httpAuthPass": "WebUI-Passwort",
"httpAuthText": "Schützt nur die WebUI mit einem Passwort, nicht API-Aufrufe.", "httpAuthText": "Schützt nur die WebUI mit einem Passwort, nicht API-Aufrufe.",
"currencies": "Währungen" "currencies": "Währungen",
"mowMode": "Mow suffixmodus",
"suffixShareDot": "Kompakte Suffix-Notation",
"section": {
"displaysAndLed": "Anzeigen und LEDs",
"screenSettings": "Infospezifisch",
"dataSource": "Datenquelle",
"extraFeatures": "Zusätzliche Funktionen",
"system": "System"
},
"ledFlashOnZap": "LED blinkt bei Nostr Zap",
"flFlashOnZap": "Displaybeleuchting bei Nostr Zap",
"showAll": "Alle anzeigen",
"hideAll": "Alles ausblenden",
"flOffWhenDark": "Displaybeleuchtung aus, wenn es dunkel ist",
"luxLightToggleText": "Zum Deaktivieren auf 0 setzen"
}, },
"control": { "control": {
"systemInfo": "Systeminfo", "systemInfo": "Systeminfo",
@ -81,7 +96,8 @@
"swUpdateAvailable": "Eine neuere Version ist verfügbar!", "swUpdateAvailable": "Eine neuere Version ist verfügbar!",
"latestVersion": "Letzte Version", "latestVersion": "Letzte Version",
"releaseDate": "Veröffentlichungsdatum", "releaseDate": "Veröffentlichungsdatum",
"viewRelease": "Veröffentlichung anzeigen" "viewRelease": "Veröffentlichung anzeigen",
"autoUpdate": "Update installieren (experimentell)"
} }
}, },
"colors": { "colors": {

View file

@ -41,9 +41,11 @@
"nostrZapKey": "Nostr zap pubkey", "nostrZapKey": "Nostr zap pubkey",
"nostrRelay": "Nostr Relay", "nostrRelay": "Nostr Relay",
"nostrZapNotify": "Nostr Zap Notifications", "nostrZapNotify": "Nostr Zap Notifications",
"useNostr": "Use Nostr datasource", "useNostr": "Use Nostr data source",
"bitaxeHostname": "BitAxe hostname or IP", "bitaxeHostname": "BitAxe hostname or IP",
"bitaxeEnabled": "Enable BitAxe", "bitaxeEnabled": "Enable BitAxe",
"miningPoolName": "Mining Pool",
"miningPoolUser": "Mining Pool username or api key",
"nostrZapPubkey": "Nostr Zap pubkey", "nostrZapPubkey": "Nostr Zap pubkey",
"invalidNostrPubkey": "Invalid Nostr pubkey, note that your pubkey does NOT start with npub.", "invalidNostrPubkey": "Invalid Nostr pubkey, note that your pubkey does NOT start with npub.",
"convertingValidNpub": "Converting valid npub to pubkey", "convertingValidNpub": "Converting valid npub to pubkey",
@ -52,7 +54,24 @@
"httpAuthUser": "WebUI Username", "httpAuthUser": "WebUI Username",
"httpAuthPass": "WebUI Password", "httpAuthPass": "WebUI Password",
"httpAuthText": "Only password-protects WebUI, not API-calls.", "httpAuthText": "Only password-protects WebUI, not API-calls.",
"currencies": "Currencies" "currencies": "Currencies",
"stagingSource": "Use Staging data source (for development)",
"useNostrTooltip": "Very experimental and unstable. Nostr data source is not required for Nostr Zap notifications.",
"mowMode": "Mow Suffix Mode",
"suffixShareDot": "Suffix compact notation",
"section": {
"displaysAndLed": "Displays and LEDs",
"screenSettings": "Screen specific",
"dataSource": "Data source",
"extraFeatures": "Extra features",
"system": "System"
},
"ledFlashOnZap": "LED flash on Nostr Zap",
"flFlashOnZap": "Frontlight flash on Nostr Zap",
"showAll": "Show all",
"hideAll": "Hide all",
"flOffWhenDark": "Frontlight off when dark",
"luxLightToggleText": "Set to 0 to disable"
}, },
"control": { "control": {
"systemInfo": "System info", "systemInfo": "System info",
@ -93,7 +112,8 @@
"swUpToDate": "You are up to date.", "swUpToDate": "You are up to date.",
"latestVersion": "Latest Version", "latestVersion": "Latest Version",
"releaseDate": "Release Date", "releaseDate": "Release Date",
"viewRelease": "View Release" "viewRelease": "View Release",
"autoUpdate": "Install update (experimental)"
} }
}, },
"colors": { "colors": {

View file

@ -41,7 +41,22 @@
"httpAuthUser": "Nombre de usuario WebUI", "httpAuthUser": "Nombre de usuario WebUI",
"httpAuthPass": "Contraseña WebUI", "httpAuthPass": "Contraseña WebUI",
"httpAuthText": "Solo la WebUI está protegida con contraseña, no las llamadas API.", "httpAuthText": "Solo la WebUI está protegida con contraseña, no las llamadas API.",
"currencies": "Monedas" "currencies": "Monedas",
"mowMode": "Modo de sufijo Mow",
"suffixShareDot": "Notación compacta de sufijo",
"section": {
"displaysAndLed": "Pantallas y LED",
"screenSettings": "Específico de la pantalla",
"dataSource": "fuente de datos",
"extraFeatures": "Funciones adicionales",
"system": "Sistema"
},
"ledFlashOnZap": "LED parpadeante con Nostr Zap",
"flFlashOnZap": "Flash de luz frontal con Nostr Zap",
"showAll": "Mostrar todo",
"hideAll": "Ocultar todo",
"flOffWhenDark": "Luz de la pantalla cuando está oscuro",
"luxLightToggleText": "Establecer en 0 para desactivar"
}, },
"control": { "control": {
"turnOff": "Apagar", "turnOff": "Apagar",
@ -80,7 +95,8 @@
"swUpdateAvailable": "¡Una nueva versión está disponible!", "swUpdateAvailable": "¡Una nueva versión está disponible!",
"latestVersion": "Ultima versión", "latestVersion": "Ultima versión",
"releaseDate": "Fecha de lanzamiento", "releaseDate": "Fecha de lanzamiento",
"viewRelease": "Ver lanzamiento" "viewRelease": "Ver lanzamiento",
"autoUpdate": "Instalar actualización (experimental)"
} }
}, },
"button": { "button": {

View file

@ -42,7 +42,22 @@
"httpAuthUser": "WebUI-gebruikersnaam", "httpAuthUser": "WebUI-gebruikersnaam",
"httpAuthPass": "WebUI-wachtwoord", "httpAuthPass": "WebUI-wachtwoord",
"httpAuthText": "Beveiligd enkel WebUI, niet de API.", "httpAuthText": "Beveiligd enkel WebUI, niet de API.",
"currencies": "Valuta's" "currencies": "Valuta's",
"mowMode": "Mow achtervoegsel",
"suffixShareDot": "Achtervoegsel compacte notatie",
"section": {
"displaysAndLed": "Displays en LED's",
"screenSettings": "Schermspecifiek",
"dataSource": "Gegevensbron",
"extraFeatures": "Extra functies",
"system": "Systeem"
},
"ledFlashOnZap": "Knipper LED bij Nostr Zap",
"flFlashOnZap": "Knipper displaylicht bij Nostr Zap",
"showAll": "Toon alles",
"hideAll": "Alles verbergen",
"flOffWhenDark": "Displaylicht uit als het donker is",
"luxLightToggleText": "Stel in op 0 om uit te schakelen"
}, },
"control": { "control": {
"systemInfo": "Systeeminformatie", "systemInfo": "Systeeminformatie",
@ -80,7 +95,8 @@
"swUpdateAvailable": "Een nieuwere versie is beschikbaar!", "swUpdateAvailable": "Een nieuwere versie is beschikbaar!",
"latestVersion": "Laatste versie", "latestVersion": "Laatste versie",
"releaseDate": "Datum van publicatie", "releaseDate": "Datum van publicatie",
"viewRelease": "Bekijk publicatie" "viewRelease": "Bekijk publicatie",
"autoUpdate": "Update installeren (experimenteel)"
} }
}, },
"colors": { "colors": {

View file

@ -1,6 +1,4 @@
@import '../node_modules/bootstrap/scss/functions'; @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/antonio/latin-400.css";
@import '@fontsource/ubuntu/latin-400.css'; @import '@fontsource/ubuntu/latin-400.css';
@ -9,11 +7,13 @@
@import './satsymbol'; @import './satsymbol';
$color-mode-type: media-query; $color-mode-type: data;
$font-family-base: 'Ubuntu'; $font-family-base: 'Ubuntu';
$font-size-base: 0.9rem; $font-size-base: 0.9rem;
$input-font-size-sm: $font-size-base * 0.875; $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; // $border-radius: .675rem;
@import '../node_modules/bootstrap/scss/mixins'; @import '../node_modules/bootstrap/scss/mixins';
@ -28,7 +28,7 @@ $input-font-size-sm: $font-size-base * 0.875;
@import '../node_modules/bootstrap/scss/forms'; @import '../node_modules/bootstrap/scss/forms';
@import '../node_modules/bootstrap/scss/buttons'; @import '../node_modules/bootstrap/scss/buttons';
@import '../node_modules/bootstrap/scss/button-group'; @import '../node_modules/bootstrap/scss/button-group';
@import '../node_modules/bootstrap/scss/pagination'; //@import '../node_modules/bootstrap/scss/pagination';
@import '../node_modules/bootstrap/scss/dropdown'; @import '../node_modules/bootstrap/scss/dropdown';
@ -43,6 +43,40 @@ $input-font-size-sm: $font-size-base * 0.875;
@import '../node_modules/bootstrap/scss/helpers'; @import '../node_modules/bootstrap/scss/helpers';
@import '../node_modules/bootstrap/scss/utilities/api'; @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 { nav {
margin-bottom: 15px; margin-bottom: 15px;
} }
@ -53,6 +87,23 @@ nav {
.btn-group-sm .btn { .btn-group-sm .btn {
font-size: 0.8rem; 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 { #customText {
@ -63,7 +114,7 @@ nav {
.btclock { .btclock {
background: #000; background: #000;
display: flex; display: flex;
font-size: calc(3vw + 3vh); font-size: calc(2vw + 2vh);
font-family: 'Antonio', sans-serif; font-family: 'Antonio', sans-serif;
font-weight: 400; font-weight: 400;
padding: 10px; padding: 10px;
@ -104,7 +155,7 @@ nav {
flex-direction: column; /* Stack the text and line vertically */ flex-direction: column; /* Stack the text and line vertically */
align-items: center; align-items: center;
justify-content: space-around; /* Distribute items with space between */ justify-content: space-around; /* Distribute items with space between */
padding: 10px; padding: 5px;
} }
.splitText div:first-child::after { .splitText div:first-child::after {
@ -116,7 +167,7 @@ nav {
} }
.splitText { .splitText {
font-size: calc(0.5vw + 1vh); font-size: calc(0.3vw + 1vh);
.top-text, .top-text,
.bottom-text { .bottom-text {
@ -228,3 +279,17 @@ nav {
#firmwareUploadProgress { #firmwareUploadProgress {
@extend .my-2; @extend .my-2;
} }
.sats {
font-family: 'Satoshi Symbol';
}
.currencyCode {
width: 20%;
text-align: center;
display: inline-block;
}
input[type='number'] {
text-align: right;
}

View file

@ -9,11 +9,14 @@
NavItem, NavItem,
NavLink, NavLink,
Navbar, Navbar,
NavbarBrand NavbarBrand,
NavbarToggler
} from '@sveltestrap/sveltestrap'; } from '@sveltestrap/sveltestrap';
import { _ } from 'svelte-i18n';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { locale, locales, isLoading } from 'svelte-i18n'; import { locale, locales, isLoading } from 'svelte-i18n';
import ColorSchemeSwitcher from '../components/ColorSchemeSwitcher.svelte';
export const setLocale = (lang: string) => () => { export const setLocale = (lang: string) => () => {
locale.set(lang); locale.set(lang);
@ -36,7 +39,7 @@
return flagMap[lowercaseCode]; return flagMap[lowercaseCode];
} else { } else {
// Return null for unsupported language codes // Return null for unsupported language codes
return null; return flagMap['en'];
} }
}; };
@ -51,21 +54,47 @@
} }
} }
}); });
let isOpen = false;
const toggle = () => {
isOpen = !isOpen;
};
</script> </script>
<Navbar expand="md"> <Navbar expand="md" sticky="xs-top" theme="auto">
<NavbarBrand>&#8383;TClock</NavbarBrand> <NavbarBrand class="d-none d-sm-block">&#8383;TClock</NavbarBrand>
<Collapse navbar expand="md"> <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">
<Nav class="me-auto" navbar> <Nav class="me-auto" navbar>
<NavItem> <NavItem>
<NavLink href="/" active={$page.url.pathname === '/'}>Home</NavLink> <NavLink href="/" active={$page.url.pathname === '/'}>Home</NavLink>
</NavItem> </NavItem>
<NavItem>
<NavLink href="/convert" active={$page.url.pathname === '/convert'}>Convert</NavLink>
</NavItem>
<NavItem> <NavItem>
<NavLink href="/api" active={$page.url.pathname === '/api'}>API</NavLink> <NavLink href="/api" active={$page.url.pathname === '/api'}>API</NavLink>
</NavItem> </NavItem>
</Nav> </Nav>
{#if !$isLoading} {#if !$isLoading}
<Dropdown id="nav-language-dropdown" inNavbar> <Dropdown id="nav-language-dropdown" inNavbar class="me-3">
<DropdownToggle nav caret>{getFlagEmoji($locale)} {languageNames[$locale]}</DropdownToggle> <DropdownToggle nav caret>{getFlagEmoji($locale)} {languageNames[$locale]}</DropdownToggle>
<DropdownMenu end> <DropdownMenu end>
{#each $locales as locale} {#each $locales as locale}
@ -76,8 +105,11 @@
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
{/if} {/if}
<ColorSchemeSwitcher></ColorSchemeSwitcher>
</Collapse> </Collapse>
</Navbar> </Navbar>
<!-- +layout.svelte --> <!-- +layout.svelte -->
<slot /> <main>
<slot />
</main>

View file

@ -3,6 +3,7 @@
import { screenSize, updateScreenSize } from '$lib/screen'; import { screenSize, updateScreenSize } from '$lib/screen';
import { Container, Row, Toast, ToastBody } from '@sveltestrap/sveltestrap'; import { Container, Row, Toast, ToastBody } from '@sveltestrap/sveltestrap';
import { replaceState } from '$app/navigation';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
@ -12,15 +13,10 @@
import { uiSettings } from '$lib/uiSettings'; import { uiSettings } from '$lib/uiSettings';
let settings = writable({ let settings = writable({
fgColor: '0' fgColor: '0',
bgColor: '0'
}); });
// let uiSettings = writable({
// inputSize: 'sm',
// selectClass: '',
// btnSize: 'lg'
// });
let status = writable({ let status = writable({
data: ['L', 'O', 'A', 'D', 'I', 'N', 'G'], data: ['L', 'O', 'A', 'D', 'I', 'N', 'G'],
espFreeHeap: 0, espFreeHeap: 0,
@ -59,7 +55,43 @@
}); });
}; };
let sections: (HTMLElement | null)[];
let observer: IntersectionObserver;
const SM_BREAKPOINT = 576;
const setupObserver = () => {
if (window.innerWidth < SM_BREAKPOINT) {
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const id = entry.target.id;
replaceState(`#${id}`);
// 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(() => { onMount(() => {
setupObserver();
fetchSettingsData(); fetchSettingsData();
fetchStatusData(); fetchStatusData();
@ -71,6 +103,11 @@
}); });
function handleResize() { function handleResize() {
if (observer) {
observer.disconnect();
}
setupObserver();
updateScreenSize(); updateScreenSize();
} }
@ -123,8 +160,10 @@
<Container fluid> <Container fluid>
<Row> <Row>
<Control bind:settings bind:status lg="3" xxl="4"></Control> <Control bind:settings on:showToast={showToast} bind:status lg="3" xxl="4"></Control>
<Status bind:settings bind:status lg="6" xxl="4"></Status> <Status bind:settings bind:status lg="6" xxl="4"></Status>
<Settings bind:settings on:showToast={showToast} on:formReset={fetchSettingsData} lg="3" xxl="4" <Settings bind:settings on:showToast={showToast} on:formReset={fetchSettingsData} lg="3" xxl="4"
></Settings> ></Settings>
</Row> </Row>

View file

@ -105,8 +105,8 @@
export let xxl = xl; export let xxl = xl;
</script> </script>
<Col {xs} {sm} {md} {lg} {xl} {xxl}> <Col {xs} {sm} {md} {lg} {xl} {xxl} class="mb-4 mb-xl-0">
<Card> <Card id="control">
<CardHeader> <CardHeader>
<CardTitle>{$_('section.control.title', { default: 'Control' })}</CardTitle> <CardTitle>{$_('section.control.title', { default: 'Control' })}</CardTitle>
</CardHeader> </CardHeader>
@ -239,7 +239,7 @@
{#if $settings.otaEnabled} {#if $settings.otaEnabled}
<hr /> <hr />
<h3>{$_('section.control.firmwareUpdate')}</h3> <h3>{$_('section.control.firmwareUpdate')}</h3>
<FirmwareUpdater bind:settings /> <FirmwareUpdater on:showToast bind:settings />
{/if} {/if}
</CardBody> </CardBody>
</Card> </Card>

View file

@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_BASE_URL } from '$lib/config'; import { PUBLIC_BASE_URL } from '$lib/config';
import { onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { Progress, Alert, Button } from '@sveltestrap/sveltestrap'; import { Progress, Alert, Button } from '@sveltestrap/sveltestrap';
const dispatch = createEventDispatcher();
export let settings = { hwRev: '' }; export let settings = { hwRev: '' };
let currentVersion: string = $settings.gitTag; // Replace with your current version let currentVersion: string = $settings.gitTag; // Replace with your current version
@ -91,6 +93,9 @@
const getFirmwareBinaryName = () => { const getFirmwareBinaryName = () => {
let binaryFilename = ''; let binaryFilename = '';
switch ($settings.hwRev) { switch ($settings.hwRev) {
case 'REV_V8_EPD_2_13':
binaryFilename = 'btclock_rev_v8_213epd_firmware.bin';
break;
case 'REV_B_EPD_2_13': case 'REV_B_EPD_2_13':
binaryFilename = 'btclock_rev_b_213epd_firmware.bin'; binaryFilename = 'btclock_rev_b_213epd_firmware.bin';
break; break;
@ -107,10 +112,40 @@
return binaryFilename; return binaryFilename;
}; };
const onAutoUpdate = async (e: Event) => {
e.preventDefault();
try {
const response = await fetch(`${PUBLIC_BASE_URL}/api/firmware/auto_update`);
if (!response.ok) {
let msg = (await response.json()).msg;
dispatch('showToast', {
color: 'danger',
text: msg
});
} else {
let msg = (await response.json()).msg;
dispatch('showToast', {
color: 'info',
text: msg
});
}
} catch (error) {
dispatch('showToast', {
color: 'danger',
text: error
});
console.error('Error fetching latest version:', error);
}
};
onMount(async () => { onMount(async () => {
try { try {
const response = await fetch( const response = await fetch(
'https://api.github.com/repos/btclock/btclock_v3/releases/latest' 'https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest'
); );
if (!response.ok) { if (!response.ok) {
@ -153,7 +188,8 @@
)}: {releaseDate} - )}: {releaseDate} -
<a href={releaseUrl} target="_blank">{$_('section.firmwareUpdater.viewRelease')}</a><br /> <a href={releaseUrl} target="_blank">{$_('section.firmwareUpdater.viewRelease')}</a><br />
{#if isNewerVersionAvailable} {#if isNewerVersionAvailable}
{$_('section.firmwareUpdater.swUpdateAvailable')} {$_('section.firmwareUpdater.swUpdateAvailable')} -
<a href="/" on:click={onAutoUpdate}>{$_('section.firmwareUpdater.autoUpdate')}</a>.
{:else} {:else}
{$_('section.firmwareUpdater.swUpToDate')} {$_('section.firmwareUpdater.swUpToDate')}
{/if} {/if}
@ -164,7 +200,7 @@
<p>Loading...</p> <p>Loading...</p>
{/if} {/if}
<section class="row row-cols-lg-auto align-items-end"> <section class="row row-cols-lg-auto align-items-end">
<div class="col-12"> <div class="col flex-fill">
<label for="firmwareFile" class="form-label">Firmware file ({getFirmwareBinaryName()})</label> <label for="firmwareFile" class="form-label">Firmware file ({getFirmwareBinaryName()})</label>
<input <input
type="file" type="file"
@ -180,7 +216,7 @@
>Update firmware</Button >Update firmware</Button
> >
</div> </div>
<div class="col mt-2"> <div class="col flex-fill">
<label for="webuiFile" class="form-label">WebUI file (littlefs.bin)</label> <label for="webuiFile" class="form-label">WebUI file (littlefs.bin)</label>
<input <input
type="file" type="file"

View file

@ -18,10 +18,15 @@
InputGroup, InputGroup,
InputGroupText, InputGroupText,
Label, Label,
Tooltip,
Row Row
} from '@sveltestrap/sveltestrap'; } from '@sveltestrap/sveltestrap';
import EyeIcon from '../icons/EyeIcon.svelte';
import EyeSlashIcon from '../icons/EyeSlashIcon.svelte'; import EyeIcon from 'svelte-bootstrap-icons/lib/Eye.svelte';
import EyeSlashIcon from 'svelte-bootstrap-icons/lib/EyeSlash.svelte';
import { derived } from 'svelte/store';
import ToggleHeader from '../components/ToggleHeader.svelte';
export let settings; export let settings;
@ -160,57 +165,238 @@
let showPassword = false; let showPassword = false;
let textColor = '0';
const colorStore = derived(settings, ($settings) => ({
fgColor: $settings.fgColor,
bgColor: $settings.bgColor
}));
// $: {
// if ($colorStore) {
// console.log('Settings model changed:', $colorStore);
// if ($colorStore.fgColor < $colorStore.bgColor)
// textColor = "0";
// else
// textColor = "1"; // 65535
// }
// }
colorStore.subscribe(() => {
if ($colorStore) {
if ($colorStore.fgColor < $colorStore.bgColor) textColor = '0';
else textColor = '1'; // 65535
}
});
const setTextColor = () => {
console.log(textColor);
if (textColor == '1') {
$settings.fgColor = 65535;
$settings.bgColor = 0;
} else {
$settings.fgColor = 0;
$settings.bgColor = 65535;
}
};
const showAll = (show: boolean) => {
screenSettingsIsOpen = show;
displaysAndLedIsOpen = show;
dataSourceIsOpen = show;
extraFeaturesIsOpen = show;
systemIsOpen = show;
};
export let xs = 12; export let xs = 12;
export let sm = xs; export let sm = xs;
export let md = sm; export let md = sm;
export let lg = md; export let lg = md;
export let xl = lg; export let xl = lg;
export let xxl = xl; export let xxl = xl;
let screenSettingsIsOpen: boolean,
displaysAndLedIsOpen: boolean,
dataSourceIsOpen: boolean,
extraFeaturesIsOpen: boolean,
systemIsOpen: boolean;
</script> </script>
<Col {xs} {sm} {md} {lg} {xl} {xxl}> <Col {xs} {sm} {md} {lg} {xl} {xxl} class="mb-4 mb-xl-0">
<Card> <Card id="settings">
<CardHeader> <CardHeader>
<div class="float-end">
<small
><button
on:click={() => {
showAll(true);
}}
type="button">{$_('section.settings.showAll')}</button
>
|
<button
type="button"
on:click={() => {
showAll(false);
}}>{$_('section.settings.hideAll')}</button
></small
>
</div>
<CardTitle>{$_('section.settings.title', { default: 'Settings' })}</CardTitle> <CardTitle>{$_('section.settings.title', { default: 'Settings' })}</CardTitle>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<Form on:submit={onSave}> <Form on:submit={onSave} class="clearfix">
<Row> <Row>
<Label md={6} for="fgColor" size={$uiSettings.inputSize} <ToggleHeader
header={$_('section.settings.section.screenSettings')}
defaultOpen={true}
isOpen={screenSettingsIsOpen}
>
<Row>
<Col md="6" xl="12" xxl="6">
<Input
id="stealFocus"
bind:checked={$settings.stealFocus}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.StealFocusOnNewBlock')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="mcapBigChar"
bind:checked={$settings.mcapBigChar}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.useBigCharsMcap')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="useBlkCountdown"
bind:checked={$settings.useBlkCountdown}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.useBlkCountdown')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="useSatsSymbol"
bind:checked={$settings.useSatsSymbol}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.useSatsSymbol')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="suffixPrice"
bind:checked={$settings.suffixPrice}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.suffixPrice')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
disabled={!$settings.suffixPrice}
id="mowMode"
bind:checked={$settings.mowMode}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.mowMode')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
disabled={!$settings.suffixPrice}
id="suffixShareDot"
bind:checked={$settings.suffixShareDot}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.suffixShareDot')}
/>
</Col>
{#if !$settings.actCurrencies}
<Col md="6" xl="12" xxl="6">
<Input
id="fetchEurPrice"
bind:checked={$settings.fetchEurPrice}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.fetchEuroPrice')} ({$_('restartRequired')})"
/>
</Col>
{/if}
</Row>
<Row>
<h5>{$_('section.settings.screens')}</h5>
{#if $settings.screens}
{#each $settings.screens as s}
<Col md="6" xl="12" xxl="6">
<Input
id="screens_{s.id}"
bind:checked={s.enabled}
type="switch"
bsSize={$uiSettings.inputSize}
label={s.name}
/>
</Col>
{/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"
bsSize={$uiSettings.inputSize}
label={c}
/>
<label class="form-check-label" for="currency_{c}">{c}</label>
</div>
</Col>
{/each}
{/if}
</Row>
{/if}
</ToggleHeader>
</Row><Row>
<ToggleHeader
header={$_('section.settings.section.displaysAndLed')}
isOpen={displaysAndLedIsOpen}
>
<Row>
<Label md={6} for="textColor" size={$uiSettings.inputSize}
>{$_('section.settings.textColor', { default: 'Text color' })}</Label >{$_('section.settings.textColor', { default: 'Text color' })}</Label
> >
<Col md="6"> <Col md="6">
<Input <Input
type="select" type="select"
bind:value={$settings.fgColor} bind:value={textColor}
name="select" name="select"
id="fgColor" id="textColor"
on:change={setTextColor}
bsSize={$uiSettings.inputSize} bsSize={$uiSettings.inputSize}
class={$uiSettings.selectClass} class={$uiSettings.selectClass}
> >
<option value="0">{$_('colors.black')}</option> <option value="0">{$_('colors.black')} on {$_('colors.white')}</option>
<option value="65535">{$_('colors.white')}</option> <option value="1">{$_('colors.white')} on {$_('colors.black')}</option>
</Input>
</Col>
</Row>
<Row>
<Label md={6} for="bgColor" size={$uiSettings.inputSize}
>{$_('section.settings.backgroundColor')}</Label
>
<Col md="6">
<Input
type="select"
bind:value={$settings.bgColor}
name="select"
id="bgColor"
bsSize={$uiSettings.inputSize}
class={$uiSettings.selectClass}
>
<option value="0">{$_('colors.black')}</option>
<option value="65535">{$_('colors.white')}</option>
</Input> </Input>
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Label md={6} for="timePerScreen" size={$uiSettings.inputSize} <Label md={6} for="timePerScreen" size={$uiSettings.inputSize}
>{$_('section.settings.timePerScreen')}</Label >{$_('section.settings.timePerScreen')}</Label
@ -265,28 +451,6 @@
<FormText>{$_('section.settings.shortAmountsWarning')}</FormText> <FormText>{$_('section.settings.shortAmountsWarning')}</FormText>
</Col> </Col>
</Row> </Row>
<Row>
<Label md={6} for="tzOffset" size={$uiSettings.inputSize}
>{$_('section.settings.timezoneOffset')}</Label
>
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type="number"
step="1"
name="tzOffset"
id="tzOffset"
required
bind:value={$settings.tzOffset}
/>
<InputGroupText>{$_('time.minutes')}</InputGroupText>
<Button type="button" color="info" on:click={getTzOffsetFromSystem}
>{$_('auto-detect')}</Button
>
</InputGroup>
<FormText>{$_('section.settings.tzOffsetHelpText')}</FormText>
</Col>
</Row>
<Row> <Row>
<Label md={6} for="ledBrightness" size={$uiSettings.inputSize} <Label md={6} for="ledBrightness" size={$uiSettings.inputSize}
>{$_('section.settings.ledBrightness')}</Label >{$_('section.settings.ledBrightness')}</Label
@ -338,7 +502,7 @@
</Col> </Col>
</Row> </Row>
{/if} {/if}
{#if $settings.hasLightLevel} {#if !$settings.flDisable && $settings.hasLightLevel}
<Row> <Row>
<Label md={6} for="luxLightToggle" size={$uiSettings.inputSize} <Label md={6} for="luxLightToggle" size={$uiSettings.inputSize}
>{$_('section.settings.luxLightToggle')} ({$settings.luxLightToggle})</Label >{$_('section.settings.luxLightToggle')} ({$settings.luxLightToggle})</Label
@ -353,9 +517,156 @@
max={1000} max={1000}
step={1} step={1}
/> />
<FormText>{$_('section.settings.luxLightToggleText')}</FormText>
</Col> </Col>
</Row> </Row>
{/if} {/if}
<Row>
<Col md="6" xl="12" xxl="6">
<Input
id="ledTestOnPower"
bind:checked={$settings.ledTestOnPower}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.ledPowerOnTest')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="ledFlashOnUpd"
bind:checked={$settings.ledFlashOnUpd}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.ledFlashOnBlock')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="disableLeds"
bind:checked={$settings.disableLeds}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.disableLeds')}
/>
</Col>
{#if $settings.hasFrontlight}
<Col md="6" xl="12" xxl="6">
<Input
id="flDisable"
bind:checked={$settings.flDisable}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flDisable')}
/>
</Col>
{/if}
{#if $settings.hasFrontlight && !$settings.flDisable}
<Col md="6" xl="12" xxl="6">
<Input
id="flAlwaysOn"
bind:checked={$settings.flAlwaysOn}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flAlwaysOn')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="flFlashOnUpd"
bind:checked={$settings.flFlashOnUpd}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flFlashOnUpd')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="flOffWhenDark"
bind:checked={$settings.flOffWhenDark}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flOffWhenDark')}
/>
</Col>
{/if}
</Row>
</ToggleHeader>
</Row><Row>
<ToggleHeader
header={$_('section.settings.section.dataSource')}
isOpen={dataSourceIsOpen}
>
<Row>
<Label md={6} for="mempoolInstance" size="sm"
>{$_('section.settings.mempoolnstance')}</Label
>
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type="text"
bind:value={$settings.mempoolInstance}
name="mempoolInstance"
id="mempoolInstance"
disabled={$settings.ownDataSource}
bsSize="sm"
required
></Input>
<InputGroupText>
<Input
addon
type="checkbox"
bind:checked={$settings.mempoolSecure}
disabled={$settings.ownDataSource}
bsSize={$uiSettings.inputSize}
/>
HTTPS
</InputGroupText>
</InputGroup>
<FormText>{$_('section.settings.mempoolInstanceHelpText')}</FormText>
</Col>
</Row>
<Row>
<Col md="6" xl="12" xxl="6">
<Input
id="ownDataSource"
bind:checked={$settings.ownDataSource}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.ownDataSource')} ({$_('restartRequired')})"
/>
</Col>
{#if $settings.nostrRelay}
<Col md="6" xl="12" xxl="6">
<Input
id="useNostr"
bind:checked={$settings.useNostr}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.useNostr')} ({$_('restartRequired')})"
></Input>
<Tooltip target="useNostr" placement="left">
{$_('section.settings.useNostrTooltip')}
</Tooltip>
</Col>
{/if}
{#if 'stagingSource' in $settings}
<Col md="6" xl="12" xxl="6">
<Input
id="stagingSource"
bind:checked={$settings.stagingSource}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.stagingSource')} ({$_('restartRequired')})"
/>
</Col>
{/if}
</Row>
</ToggleHeader>
</Row><Row>
<ToggleHeader
header={$_('section.settings.section.extraFeatures')}
isOpen={extraFeaturesIsOpen}
>
{#if $settings.bitaxeEnabled} {#if $settings.bitaxeEnabled}
<Row> <Row>
<Label md={6} for="bitaxeHostname" size={$uiSettings.inputSize} <Label md={6} for="bitaxeHostname" size={$uiSettings.inputSize}
@ -378,6 +689,40 @@
</Col> </Col>
</Row> </Row>
{/if} {/if}
{#if $settings.miningPoolStatsEnabled}
<Row>
<Label md={6} for="miningPoolName" size={$uiSettings.inputSize}
>{$_('section.settings.miningPoolName')}</Label
>
<Col md="6">
<Input
type="text"
bind:value={$settings.miningPoolName}
name="miningPoolName"
id="miningPoolName"
bsSize={$uiSettings.inputSize}
required
minlength="64"
></Input>
</Col>
</Row>
<Row>
<Label md={6} for="miningPoolUser" size={$uiSettings.inputSize}
>{$_('section.settings.miningPoolUser')}</Label
>
<Col md="6">
<Input
type="text"
bind:value={$settings.miningPoolUser}
name="miningPoolUser"
id="miningPoolUser"
bsSize={$uiSettings.inputSize}
required
minlength="64"
></Input>
</Col>
</Row>
{/if}
{#if 'nostrZapNotify' in $settings && $settings['nostrZapNotify']} {#if 'nostrZapNotify' in $settings && $settings['nostrZapNotify']}
<Row> <Row>
<Label md={6} for="nostrZapPubkey" size={$uiSettings.inputSize} <Label md={6} for="nostrZapPubkey" size={$uiSettings.inputSize}
@ -446,37 +791,79 @@
</Row> </Row>
{/if} {/if}
<Row> <Row>
<Label md={6} for="mempoolInstance" size="sm" {#if 'bitaxeEnabled' in $settings}
>{$_('section.settings.mempoolnstance')}</Label <Col md="6" xl="12" xxl="6">
<Input
id="bitaxeEnabled"
bind:checked={$settings.bitaxeEnabled}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.bitaxeEnabled')} ({$_('restartRequired')})"
/>
</Col>
{/if}
{#if 'nostrZapNotify' in $settings}
<Col md="6" xl="12" xxl="6">
<Input
id="nostrZapNotify"
bind:checked={$settings.nostrZapNotify}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.nostrZapNotify')} ({$_('restartRequired')})"
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="ledFlashOnZap"
bind:checked={$settings.ledFlashOnZap}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.ledFlashOnZap')}
/>
</Col>
{#if $settings.hasFrontlight && !$settings.flDisable}
<Col md="6" xl="12" xxl="6">
<Input
id="flFlashOnZap"
bind:checked={$settings.flFlashOnZap}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flFlashOnZap')}
/>
</Col>
{/if}
{/if}
</Row>
</ToggleHeader>
</Row><Row>
<ToggleHeader header={$_('section.settings.section.system')} isOpen={systemIsOpen}>
<Row>
<Label md={6} for="tzOffset" size={$uiSettings.inputSize}
>{$_('section.settings.timezoneOffset')}</Label
> >
<Col md="6"> <Col md="6">
<InputGroup size={$uiSettings.inputSize}> <InputGroup size={$uiSettings.inputSize}>
<Input <Input
type="text" type="number"
bind:value={$settings.mempoolInstance} step="1"
name="mempoolInstance" name="tzOffset"
id="mempoolInstance" id="tzOffset"
disabled={$settings.ownDataSource}
bsSize="sm"
required required
></Input> bind:value={$settings.tzOffset}
<InputGroupText>
<Input
addon
type="checkbox"
bind:checked={$settings.mempoolSecure}
disabled={$settings.ownDataSource}
bsSize={$uiSettings.inputSize}
/> />
HTTPS <InputGroupText>{$_('time.minutes')}</InputGroupText>
</InputGroupText> <Button type="button" color="info" on:click={getTzOffsetFromSystem}
>{$_('auto-detect')}</Button
>
</InputGroup> </InputGroup>
<FormText>{$_('section.settings.mempoolInstanceHelpText')}</FormText> <FormText>{$_('section.settings.tzOffsetHelpText')}</FormText>
</Col> </Col>
</Row> </Row>
{#if $settings.httpAuthEnabled} {#if $settings.httpAuthEnabled}
<Row> <Row>
<Label md={6} for="httpAuthUser" size="sm">{$_('section.settings.httpAuthUser')}</Label> <Label md={6} for="httpAuthUser" size="sm"
>{$_('section.settings.httpAuthUser')}</Label
>
<Col md="6"> <Col md="6">
<Input <Input
type="text" type="text"
@ -489,7 +876,9 @@
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Label md={6} for="httpAuthPass" size="sm">{$_('section.settings.httpAuthPass')}</Label> <Label md={6} for="httpAuthPass" size="sm"
>{$_('section.settings.httpAuthPass')}</Label
>
<Col md="6"> <Col md="6">
<InputGroup size={$uiSettings.inputSize}> <InputGroup size={$uiSettings.inputSize}>
<Input <Input
@ -567,161 +956,6 @@
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col md="6" xl="12" xxl="6">
<Input
id="ledTestOnPower"
bind:checked={$settings.ledTestOnPower}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.ledPowerOnTest')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="ledFlashOnUpd"
bind:checked={$settings.ledFlashOnUpd}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.ledFlashOnBlock')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="stealFocus"
bind:checked={$settings.stealFocus}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.StealFocusOnNewBlock')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="mcapBigChar"
bind:checked={$settings.mcapBigChar}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.useBigCharsMcap')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="useBlkCountdown"
bind:checked={$settings.useBlkCountdown}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.useBlkCountdown')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="useSatsSymbol"
bind:checked={$settings.useSatsSymbol}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.useSatsSymbol')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="suffixPrice"
bind:checked={$settings.suffixPrice}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.suffixPrice')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="disableLeds"
bind:checked={$settings.disableLeds}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.disableLeds')}
/>
</Col>
{#if $settings.hasFrontlight}
<Col md="6" xl="12" xxl="6">
<Input
id="flDisable"
bind:checked={$settings.flDisable}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flDisable')}
/>
</Col>
{/if}
{#if $settings.hasFrontlight && !$settings.flDisable}
<Col md="6" xl="12" xxl="6">
<Input
id="flAlwaysOn"
bind:checked={$settings.flAlwaysOn}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flAlwaysOn')}
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="flFlashOnUpd"
bind:checked={$settings.flFlashOnUpd}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flFlashOnUpd')}
/>
</Col>
{/if}
<Col md="6" xl="12" xxl="6">
<Input
id="fetchEurPrice"
bind:checked={$settings.fetchEurPrice}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.fetchEuroPrice')} ({$_('restartRequired')})"
/>
</Col>
<Col md="6" xl="12" xxl="6">
<Input
id="ownDataSource"
bind:checked={$settings.ownDataSource}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.ownDataSource')} ({$_('restartRequired')})"
/>
</Col>
{#if $settings.nostrRelay}
<Col md="6" xl="12" xxl="6">
<Input
id="useNostr"
bind:checked={$settings.useNostr}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.useNostr')} ({$_('restartRequired')})"
/>
</Col>
{/if}
{#if 'nostrZapNotify' in $settings}
<Col md="6" xl="12" xxl="6">
<Input
id="nostrZapNotify"
bind:checked={$settings.nostrZapNotify}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.nostrZapNotify')} ({$_('restartRequired')})"
/>
</Col>
{/if}
{#if 'bitaxeEnabled' in $settings}
<Col md="6" xl="12" xxl="6">
<Input
id="bitaxeEnabled"
bind:checked={$settings.bitaxeEnabled}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.bitaxeEnabled')} ({$_('restartRequired')})"
/>
</Col>
{/if}
<Col md="6" xl="12" xxl="6"> <Col md="6" xl="12" xxl="6">
<Input <Input
id="otaEnabled" id="otaEnabled"
@ -750,47 +984,9 @@
/> />
</Col> </Col>
</Row> </Row>
</ToggleHeader>
</Row>
<Row>
<h3>{$_('section.settings.screens')}</h3>
{#if $settings.screens}
{#each $settings.screens as s}
<Col md="6" xl="12" xxl="6">
<Input
id="screens_{s.id}"
bind:checked={s.enabled}
type="switch"
bsSize={$uiSettings.inputSize}
label={s.name}
/>
</Col>
{/each}
{/if}
</Row>
{#if $settings.actCurrencies}
<Row>
<h3>{$_('section.settings.currencies')}</h3>
<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"
bsSize={$uiSettings.inputSize}
label={c}
/>
<label class="form-check-label" for="currency_{c}">{c}</label>
</div>
</Col>
{/each}
{/if}
</Row>
{/if}
<Row> <Row>
<Col class="d-flex justify-content-end"> <Col class="d-flex justify-content-end">
<Button on:click={handleReset} color="secondary">{$_('button.reset')}</Button> <Button on:click={handleReset} color="secondary">{$_('button.reset')}</Button>

View file

@ -104,8 +104,8 @@
export let xxl = xl; export let xxl = xl;
</script> </script>
<Col {xs} {sm} {md} {lg} {xl} {xxl}> <Col {xs} {sm} {md} {lg} {xl} {xxl} class="mb-4 mb-xl-0">
<Card> <Card id="status">
<CardHeader> <CardHeader>
<CardTitle>{$_('section.status.title', { default: 'Status' })}</CardTitle> <CardTitle>{$_('section.status.title', { default: 'Status' })}</CardTitle>
</CardHeader> </CardHeader>
@ -135,8 +135,8 @@
{/each} {/each}
</ButtonGroup> </ButtonGroup>
</div> </div>
{#if $settings.actCurrencies} {#if $settings.actCurrencies && $settings.ownDataSource}
<div class="d-flex justify-content-center d-none d-sm-flex mt-2"> <div class="d-flex justify-content-center d-sm-flex mt-2">
<ButtonGroup size="sm"> <ButtonGroup size="sm">
{#each $settings.actCurrencies as c} {#each $settings.actCurrencies as c}
<Button <Button
@ -228,7 +228,7 @@
{#if !$settings.ownDataSource} {#if !$settings.ownDataSource}
{$_('section.status.wsPriceConnection')}: {$_('section.status.wsPriceConnection')}:
<span> <span>
{#if $status.connectionStatus && $status.connectionStatus.nostr} {#if $status.connectionStatus && $status.connectionStatus.price}
&#9989; &#9989;
{:else} {:else}
&#10060; &#10060;
@ -248,7 +248,7 @@
{:else} {:else}
{$_('section.status.wsDataConnection')}: {$_('section.status.wsDataConnection')}:
<span> <span>
{#if $status.connectionStatus && $status.connectionStatus.price} {#if $status.connectionStatus && $status.connectionStatus.V2}
&#9989; &#9989;
{:else} {:else}
&#10060; &#10060;

View file

@ -31,21 +31,21 @@
<svelte:head> <svelte:head>
<title>API playground</title> <title>API playground</title>
<script <script
src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui-bundle.min.js" src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-bundle.min.js"
integrity="sha512-Ckle4LZv9LhAfEdohBdUi+QCu0e7HkXHTeSPXfbDzbCsR87QNTUBylkBEPsBNn4Ph83yK1hJ6f2uH4QMtB0hTA==" integrity="sha512-7ihPQv5ibiTr0DW6onbl2MIKegdT6vjpPySyIb4Ftp68kER6Z7Yiub0tFoMmCHzZfQE9+M+KSjQndv6NhYxDgg=="
crossorigin="anonymous" crossorigin="anonymous"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
></script> ></script>
<script <script
src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui-standalone-preset.min.js" src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-standalone-preset.min.js"
integrity="sha512-qwGi7EG31HcylzamsmacHLZJrfUGRuuHEaCMcOojuNpMu+paR554VjaCZ9LdUVTrmF8xC03YVqTzuKx0SDdruA==" integrity="sha512-UrYi+60Ci3WWWcoDXbMmzpoi1xpERbwjPGij6wTh8fXl81qNdioNNHExr9ttnBebKF0ZbVnPlTPlw+zECUK1Xw=="
crossorigin="anonymous" crossorigin="anonymous"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
></script> ></script>
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui.min.css"
integrity="sha512-Ck+X9SARG7WscOTG4a8Qod5Zgd1MZlz4VtyyucjMJ3PnZy2lUl7q/v/0055yIfGM/v+f+216ME0/dv0qqtm6+g==" integrity="sha512-+9UD8YSD9GF7FzOH38L9S6y56aYNx3R4dYbOCgvTJ2ZHpJScsahNdaMQJU/8osUiz9FPu0YZ8wdKf4evUbsGSg=="
crossorigin="anonymous" crossorigin="anonymous"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
/> />

View file

@ -0,0 +1,143 @@
<script lang="ts">
import { Col, Container, Input, InputGroup, InputGroupText, Row } from '@sveltestrap/sveltestrap';
import { onDestroy, onMount } from 'svelte';
import { encode, decode } from 'msgpack-es';
let exchangeRates = {
USD: 57798,
GBP: 44236,
AUD: 86552,
JPY: 8221088,
EUR: 52347,
CAD: 78508
};
let socket: WebSocket;
let currencies = { ...exchangeRates };
let btcValue = 1;
let satsValue = 100000000;
let lastEditedField = 'BTC';
let inputValues = {
BTC: '1',
sats: '100000000',
...Object.fromEntries(
Object.keys(exchangeRates).map((cur) => [cur, exchangeRates[cur].toString()])
)
};
function updateValues(currency: string, value: string) {
lastEditedField = currency;
inputValues[currency] = value;
let numValue = value === '' ? 0 : parseFloat(value);
if (currency === 'BTC') {
btcValue = numValue;
satsValue = Math.round(numValue * 100000000);
} else if (currency === 'sats') {
satsValue = Math.round(numValue);
btcValue = satsValue / 100000000;
} else {
btcValue = numValue / exchangeRates[currency];
satsValue = Math.round(btcValue * 100000000);
}
// Update other currency values
for (let cur in currencies) {
if (cur !== currency) {
currencies[cur] = btcValue * exchangeRates[cur];
inputValues[cur] = formatValue(currencies[cur], cur);
}
}
inputValues.BTC = formatValue(btcValue, 'BTC');
inputValues.sats = formatValue(satsValue, 'sats');
}
function formatValue(value: number, currency: string): string {
if (currency === 'sats') {
return Math.round(value).toString();
} else if (currency === 'BTC') {
return value.toFixed(8).replace(/\.?0+$/, '');
} else {
return value.toFixed(2);
}
}
// async function fetchExchangeRates() {
// try {
// const response = await fetch('https://ws.btclock.dev/api/lastprice');
// const data = await response.json();
// exchangeRates = data;
// currencies = { ...data };
// updateValues(lastEditedField, inputValues[lastEditedField]);
// } catch (error) {
// console.error('Error fetching exchange rates:', error);
// }
// }
onMount(() => {
socket = new WebSocket('ws://ws.btclock.dev/api/v2/ws');
socket.binaryType = 'arraybuffer';
socket.addEventListener('open', () => {
socket.send(
encode({
type: 'subscribe',
eventType: 'price',
currencies: ['USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY']
})
);
});
socket.addEventListener('message', (event) => {
let data = decode(event.data);
if ('price' in data) {
let currencyKey = Object.keys(data.price);
exchangeRates[currencyKey] = data.price[currencyKey];
updateValues(lastEditedField, inputValues[lastEditedField]);
}
});
});
onDestroy(() => {
socket.close();
});
</script>
<Container fluid>
<Row class="justify-content-center">
<Col class="col-md-3 col-sm-12">
<InputGroup size="lg" class="mb-2">
<InputGroupText class="currencyCode">BTC</InputGroupText>
<Input
placeholder="Amount"
type="number"
value={inputValues.BTC}
on:input={(e) => updateValues('BTC', e.target.value)}
/>
</InputGroup>
<InputGroup size="lg" class="mb-2">
<InputGroupText class="sats currencyCode">s</InputGroupText>
<Input
placeholder="Amount"
type="number"
value={inputValues.sats}
on:input={(e) => updateValues('sats', e.target.value)}
/>
</InputGroup>
{#each Object.entries(exchangeRates) as [cur]}
<InputGroup size="lg" class="mb-2">
<InputGroupText class="currencyCode">{cur}</InputGroupText>
<Input
placeholder="Amount"
type="number"
value={inputValues[cur]}
on:input={(e) => updateValues(cur, e.target.value)}
/>
</InputGroup>
{/each}
</Col>
</Row>
</Container>

View file

@ -1,11 +1,11 @@
import adapter from '@sveltejs/adapter-static'; import adapter from '@sveltejs/adapter-static';
import preprocess from 'svelte-preprocess'; import { sveltePreprocess } from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: preprocess({}), preprocess: sveltePreprocess({}),
build: { build: {
rollupOptions: { rollupOptions: {
output: { output: {

View file

@ -1,114 +1,7 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { initMock, settingsJson, statusJson } from '../shared';
const statusJson = { test.beforeEach(initMock);
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('index page has expected columns control, status, settings', async ({ page }) => { test('index page has expected columns control, status, settings', async ({ page }) => {
await page.goto('/'); await page.goto('/');
@ -137,6 +30,8 @@ test('api page has expected load button', async ({ page }) => {
test('timezone can be negative, zero and positive', async ({ page }) => { test('timezone can be negative, zero and positive', async ({ page }) => {
await page.goto('/'); await page.goto('/');
await page.getByRole('button', { name: 'Show all' }).click();
const tzOffsetField = 'input#tzOffset'; const tzOffsetField = 'input#tzOffset';
for (const val of ['-10', '0', '42']) { for (const val of ['-10', '0', '42']) {
@ -149,6 +44,7 @@ test('timezone can be negative, zero and positive', async ({ page }) => {
test('time values can not be zero or negative', async ({ page }) => { test('time values can not be zero or negative', async ({ page }) => {
await page.goto('/'); await page.goto('/');
await page.getByRole('button', { name: 'Show all' }).click();
for (const field of ['#timePerScreen', '#fullRefreshMin', '#minSecPriceUpd']) { for (const field of ['#timePerScreen', '#fullRefreshMin', '#minSecPriceUpd']) {
for (const val of ['42', '210']) { for (const val of ['42', '210']) {
@ -178,7 +74,11 @@ test('time values can not be zero or negative', async ({ page }) => {
}); });
test('info message when fetch eur price is enabled', async ({ page }) => { test('info message when fetch eur price is enabled', async ({ page }) => {
delete (settingsJson as { actCurrencies?: string[] }).actCurrencies;
await page.goto('/'); await page.goto('/');
await page.getByRole('button', { name: 'Show all' }).click();
const inputField = 'input#fetchEurPrice'; const inputField = 'input#fetchEurPrice';
const switchElement = await page.locator(inputField); const switchElement = await page.locator(inputField);
@ -197,6 +97,7 @@ test('info message when fetch eur price is enabled', async ({ page }) => {
test('npub values will be converted to hex pubkeys', async ({ page }) => { test('npub values will be converted to hex pubkeys', async ({ page }) => {
await page.goto('/'); await page.goto('/');
await page.getByRole('button', { name: 'Show all' }).click();
for (const field of ['#nostrZapPubkey']) { for (const field of ['#nostrZapPubkey']) {
for (const val of ['npub1k5f85zx0xdskyayqpfpc0zq6n7vwqjuuxugkayk72fgynp34cs3qfcvqg2']) { for (const val of ['npub1k5f85zx0xdskyayqpfpc0zq6n7vwqjuuxugkayk72fgynp34cs3qfcvqg2']) {
@ -212,6 +113,7 @@ test('npub values will be converted to hex pubkeys', async ({ page }) => {
test('empty nostr relay field is not accepted', async ({ page }) => { test('empty nostr relay field is not accepted', async ({ page }) => {
await page.goto('/'); await page.goto('/');
await page.getByRole('button', { name: 'Show all' }).click();
const nostrRelayField = page.getByLabel('Nostr Relay'); const nostrRelayField = page.getByLabel('Nostr Relay');

View file

@ -0,0 +1,85 @@
import { test, expect } from '@playwright/test';
import { initMock, settingsJson } from '../shared';
test.beforeEach(initMock);
test('capture screenshots across devices', async ({ page }, testInfo) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Control' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Status' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Settings' })).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) => {
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
}
];
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Control' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Status' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Settings' })).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'
});
});

143
tests/shared.ts Normal file
View file

@ -0,0 +1,143 @@
export 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' }
]
};
export 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',
miningPoolStatsEnabled: true,
miningPoolName: 'ocean',
miningPoolUser: '38Qkkei3SuF1Eo45BaYmRHUneRD54yyTFy',
nostrZapNotify: true,
hwRev: 'REV_A_EPD_2_13',
fsRev: '4c5d9616212b27e3f05c35370f0befcf2c5a04b2',
nostrZapPubkey: 'b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422',
lastBuildTime: '1700666677',
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
}
],
actCurrencies: ['USD', 'EUR'],
availableCurrencies: ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD']
};
export const initMock = 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`
});
});
};

View file

@ -76,6 +76,14 @@ export default defineConfig({
} }
} }
}, },
css: {
preprocessorOptions: {
scss: {
quietDeps: true,
silenceDeprecations: ['import']
}
}
},
test: { test: {
include: ['src/**/*.{test,spec}.{js,ts}'], include: ['src/**/*.{test,spec}.{js,ts}'],
globals: true, globals: true,

3323
yarn.lock

File diff suppressed because it is too large Load diff