forked from btclock/webui
Compare commits
37 commits
b6565ef853
...
52ab8df77a
Author | SHA1 | Date | |
---|---|---|---|
|
52ab8df77a | ||
|
ca4996dfc8 | ||
|
d5d3882efb | ||
22fffd1f64 | |||
2ba647d8ce | |||
|
ff62c40384 | ||
|
aa0630fe06 | ||
|
6950036437 | ||
|
5bcbff9167 | ||
|
7fc580cb3a | ||
|
3b045dbbfa | ||
|
735fb7d1d0 | ||
|
978f911a54 | ||
|
2d1982ae6c | ||
|
220b961d63 | ||
|
e5244c2356 | ||
|
abeceab14c | ||
|
0828f8e9b8 | ||
|
71ff31f22e | ||
|
51750cc19e | ||
|
4453840dcb | ||
|
367860da2c | ||
|
6440749ae8 | ||
|
93d3642d34 | ||
|
5b0eb4c891 | ||
|
a4c0c16cfb | ||
|
52b4c77fc0 | ||
|
907721a481 | ||
|
1e3a975a84 | ||
|
3b817593e7 | ||
|
183ecd72e6 | ||
|
2270d8f85c | ||
|
b1be5709f9 | ||
|
41fee8e9b5 | ||
|
390b3b538a | ||
|
7902295392 | ||
|
51cde017a9 |
33 changed files with 2840 additions and 3175 deletions
121
.forgejo/workflows/build.yaml
Normal file
121
.forgejo/workflows/build.yaml
Normal 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
|
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
|
@ -10,3 +10,6 @@ updates:
|
|||
schedule:
|
||||
interval: 'daily'
|
||||
versioning-strategy: 'increase-if-necessary'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
|
|
@ -30,7 +30,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:
|
||||
|
||||
|
|
43
package.json
43
package.json
|
@ -13,6 +13,7 @@
|
|||
"postinstall": "patch-package",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"test:integration": "playwright test",
|
||||
"test:screenshots": "playwright test -c playwright.screenshot.config.ts",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -23,41 +24,43 @@
|
|||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||
"@testing-library/svelte": "^5.2.1",
|
||||
"@types/swagger-ui": "^3.52.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.4.0",
|
||||
"@typescript-eslint/parser": "^8.4.0",
|
||||
"@vitest/ui": "^0.34.6",
|
||||
"eslint": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.7.0",
|
||||
"@typescript-eslint/parser": "^8.7.0",
|
||||
"@vitest/ui": "^2.0.5",
|
||||
"eslint": "^9.11.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"sass": "^1.69.5",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte-preprocess": "^5.1.1",
|
||||
"tslib": "^2.4.1",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"sass": "^1.79.3",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^4.0.2",
|
||||
"svelte-preprocess": "^6.0.2",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^5.4.2",
|
||||
"vitest": "^2.0.5",
|
||||
"typescript-eslint": "^8.7.0",
|
||||
"vite": "^5.4.7",
|
||||
"vitest": "^2.1.1",
|
||||
"vitest-github-actions-reporter": "^0.11.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fontsource/antonio": "^5.0.17",
|
||||
"@fontsource/oswald": "^5.0.17",
|
||||
"@fontsource/ubuntu": "^5.0.8",
|
||||
"@fontsource/antonio": "^5.1.0",
|
||||
"@fontsource/oswald": "^5.1.0",
|
||||
"@fontsource/ubuntu": "^5.1.0",
|
||||
"@noble/secp256k1": "^2.1.0",
|
||||
"@playwright/test": "^1.46.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sveltestrap/sveltestrap": "^6.2.7",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"msgpack-es": "^0.0.5",
|
||||
"nostr-tools": "^2.7.1",
|
||||
"patch-package": "^8.0.0",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"swagger-ui": "^5.10.0"
|
||||
"svelte-bootstrap-icons": "^3.1.1",
|
||||
"svelte-i18n": "^4.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"es5-ext": ">=0.10.64",
|
||||
|
|
|
@ -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
|
||||
index 2280025..cfbcfa9 100644
|
||||
index 40fa4c6..738cabf 100644
|
||||
--- a/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,
|
||||
output: {
|
||||
format: 'esm',
|
17
patches/@sveltejs+kit+2.8.5.patch
Normal file
17
patches/@sveltejs+kit+2.8.5.patch
Normal 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
|
||||
},
|
|
@ -10,7 +10,7 @@ const config: PlaywrightTestConfig = {
|
|||
port: 4173
|
||||
},
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
testDir: 'tests',
|
||||
testDir: 'tests/playwright',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
|
|
51
playwright.screenshot.config.ts
Normal file
51
playwright.screenshot.config.ts
Normal 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 } }
|
||||
}
|
||||
]
|
||||
});
|
53
src/components/ColorSchemeSwitcher.svelte
Normal file
53
src/components/ColorSchemeSwitcher.svelte
Normal 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>
|
28
src/components/ToggleHeader.svelte
Normal file
28
src/components/ToggleHeader.svelte
Normal 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>
|
|
@ -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 |
|
@ -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 |
|
@ -42,7 +42,23 @@
|
|||
"httpAuthUser": "WebUI-Benutzername",
|
||||
"httpAuthPass": "WebUI-Passwort",
|
||||
"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",
|
||||
"verticalDesc": "Vrtikale Bildschirmbeschreibung"
|
||||
},
|
||||
"control": {
|
||||
"systemInfo": "Systeminfo",
|
||||
|
@ -81,7 +97,8 @@
|
|||
"swUpdateAvailable": "Eine neuere Version ist verfügbar!",
|
||||
"latestVersion": "Letzte Version",
|
||||
"releaseDate": "Veröffentlichungsdatum",
|
||||
"viewRelease": "Veröffentlichung anzeigen"
|
||||
"viewRelease": "Veröffentlichung anzeigen",
|
||||
"autoUpdate": "Update installieren (experimentell)"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
|
|
|
@ -41,9 +41,11 @@
|
|||
"nostrZapKey": "Nostr zap pubkey",
|
||||
"nostrRelay": "Nostr Relay",
|
||||
"nostrZapNotify": "Nostr Zap Notifications",
|
||||
"useNostr": "Use Nostr datasource",
|
||||
"useNostr": "Use Nostr data source",
|
||||
"bitaxeHostname": "BitAxe hostname or IP",
|
||||
"bitaxeEnabled": "Enable BitAxe",
|
||||
"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",
|
||||
|
@ -52,7 +54,25 @@
|
|||
"httpAuthUser": "WebUI Username",
|
||||
"httpAuthPass": "WebUI Password",
|
||||
"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",
|
||||
"verticalDesc": "Use vertical screen description"
|
||||
},
|
||||
"control": {
|
||||
"systemInfo": "System info",
|
||||
|
@ -93,7 +113,8 @@
|
|||
"swUpToDate": "You are up to date.",
|
||||
"latestVersion": "Latest Version",
|
||||
"releaseDate": "Release Date",
|
||||
"viewRelease": "View Release"
|
||||
"viewRelease": "View Release",
|
||||
"autoUpdate": "Install update (experimental)"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
|
|
|
@ -41,7 +41,23 @@
|
|||
"httpAuthUser": "Nombre de usuario WebUI",
|
||||
"httpAuthPass": "Contraseña WebUI",
|
||||
"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",
|
||||
"verticalDesc": "Descripción de pantalla vertical"
|
||||
},
|
||||
"control": {
|
||||
"turnOff": "Apagar",
|
||||
|
@ -80,7 +96,8 @@
|
|||
"swUpdateAvailable": "¡Una nueva versión está disponible!",
|
||||
"latestVersion": "Ultima versión",
|
||||
"releaseDate": "Fecha de lanzamiento",
|
||||
"viewRelease": "Ver lanzamiento"
|
||||
"viewRelease": "Ver lanzamiento",
|
||||
"autoUpdate": "Instalar actualización (experimental)"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
|
|
|
@ -42,7 +42,23 @@
|
|||
"httpAuthUser": "WebUI-gebruikersnaam",
|
||||
"httpAuthPass": "WebUI-wachtwoord",
|
||||
"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",
|
||||
"verticalDesc": "Verticale schermbeschrijving"
|
||||
},
|
||||
"control": {
|
||||
"systemInfo": "Systeeminformatie",
|
||||
|
@ -80,7 +96,8 @@
|
|||
"swUpdateAvailable": "Een nieuwere versie is beschikbaar!",
|
||||
"latestVersion": "Laatste versie",
|
||||
"releaseDate": "Datum van publicatie",
|
||||
"viewRelease": "Bekijk publicatie"
|
||||
"viewRelease": "Bekijk publicatie",
|
||||
"autoUpdate": "Update installeren (experimenteel)"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
@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';
|
||||
|
@ -9,11 +7,13 @@
|
|||
|
||||
@import './satsymbol';
|
||||
|
||||
$color-mode-type: media-query;
|
||||
$color-mode-type: data;
|
||||
$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';
|
||||
|
@ -28,7 +28,7 @@ $input-font-size-sm: $font-size-base * 0.875;
|
|||
@import '../node_modules/bootstrap/scss/forms';
|
||||
@import '../node_modules/bootstrap/scss/buttons';
|
||||
@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';
|
||||
|
||||
|
@ -43,6 +43,40 @@ $input-font-size-sm: $font-size-base * 0.875;
|
|||
@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 +87,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 +114,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 +155,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 {
|
||||
|
@ -228,3 +285,21 @@ nav {
|
|||
#firmwareUploadProgress {
|
||||
@extend .my-2;
|
||||
}
|
||||
|
||||
.sats {
|
||||
font-family: 'Satoshi Symbol';
|
||||
}
|
||||
|
||||
.currencyCode {
|
||||
width: 20%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.lightMode .bitaxelogo {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
|
|
|
@ -9,11 +9,14 @@
|
|||
NavItem,
|
||||
NavLink,
|
||||
Navbar,
|
||||
NavbarBrand
|
||||
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';
|
||||
|
||||
export const setLocale = (lang: string) => () => {
|
||||
locale.set(lang);
|
||||
|
@ -36,7 +39,7 @@
|
|||
return flagMap[lowercaseCode];
|
||||
} else {
|
||||
// Return null for unsupported language codes
|
||||
return null;
|
||||
return flagMap['en'];
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -51,21 +54,47 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
const toggle = () => {
|
||||
isOpen = !isOpen;
|
||||
};
|
||||
</script>
|
||||
|
||||
<Navbar expand="md">
|
||||
<NavbarBrand>₿TClock</NavbarBrand>
|
||||
<Collapse navbar expand="md">
|
||||
<Navbar expand="md" sticky="xs-top" theme="auto">
|
||||
<NavbarBrand class="d-none d-sm-block">₿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">
|
||||
<Nav class="me-auto" navbar>
|
||||
<NavItem>
|
||||
<NavLink href="/" active={$page.url.pathname === '/'}>Home</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="/convert" active={$page.url.pathname === '/convert'}>Convert</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="/api" active={$page.url.pathname === '/api'}>API</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
{#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>
|
||||
<DropdownMenu end>
|
||||
{#each $locales as locale}
|
||||
|
@ -76,8 +105,11 @@
|
|||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{/if}
|
||||
<ColorSchemeSwitcher></ColorSchemeSwitcher>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
|
||||
<!-- +layout.svelte -->
|
||||
<slot />
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
|
|
@ -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';
|
||||
|
@ -12,15 +13,10 @@
|
|||
import { uiSettings } from '$lib/uiSettings';
|
||||
|
||||
let settings = writable({
|
||||
fgColor: '0'
|
||||
fgColor: '0',
|
||||
bgColor: '0'
|
||||
});
|
||||
|
||||
// let uiSettings = writable({
|
||||
// inputSize: 'sm',
|
||||
// selectClass: '',
|
||||
// btnSize: 'lg'
|
||||
// });
|
||||
|
||||
let status = writable({
|
||||
data: ['L', 'O', 'A', 'D', 'I', 'N', 'G'],
|
||||
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(() => {
|
||||
setupObserver();
|
||||
|
||||
fetchSettingsData();
|
||||
fetchStatusData();
|
||||
|
||||
|
@ -71,6 +103,11 @@
|
|||
});
|
||||
|
||||
function handleResize() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
setupObserver();
|
||||
|
||||
updateScreenSize();
|
||||
}
|
||||
|
||||
|
@ -123,8 +160,10 @@
|
|||
|
||||
<Container fluid>
|
||||
<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>
|
||||
|
||||
<Settings bind:settings on:showToast={showToast} on:formReset={fetchSettingsData} lg="3" xxl="4"
|
||||
></Settings>
|
||||
</Row>
|
||||
|
|
|
@ -105,8 +105,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>
|
||||
|
@ -239,7 +239,7 @@
|
|||
{#if $settings.otaEnabled}
|
||||
<hr />
|
||||
<h3>{$_('section.control.firmwareUpdate')}</h3>
|
||||
<FirmwareUpdater bind:settings />
|
||||
<FirmwareUpdater on:showToast bind:settings />
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { PUBLIC_BASE_URL } from '$lib/config';
|
||||
import { onMount } from 'svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Progress, Alert, Button } from '@sveltestrap/sveltestrap';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let settings = { hwRev: '' };
|
||||
|
||||
let currentVersion: string = $settings.gitTag; // Replace with your current version
|
||||
|
@ -91,6 +93,9 @@
|
|||
const getFirmwareBinaryName = () => {
|
||||
let binaryFilename = '';
|
||||
switch ($settings.hwRev) {
|
||||
case 'REV_V8_EPD_2_13':
|
||||
binaryFilename = 'btclock_rev_v8_213epd_firmware.bin';
|
||||
break;
|
||||
case 'REV_B_EPD_2_13':
|
||||
binaryFilename = 'btclock_rev_b_213epd_firmware.bin';
|
||||
break;
|
||||
|
@ -107,10 +112,40 @@
|
|||
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 () => {
|
||||
try {
|
||||
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) {
|
||||
|
@ -153,7 +188,8 @@
|
|||
)}: {releaseDate} -
|
||||
<a href={releaseUrl} target="_blank">{$_('section.firmwareUpdater.viewRelease')}</a><br />
|
||||
{#if isNewerVersionAvailable}
|
||||
{$_('section.firmwareUpdater.swUpdateAvailable')}
|
||||
{$_('section.firmwareUpdater.swUpdateAvailable')} -
|
||||
<a href="/" on:click={onAutoUpdate}>{$_('section.firmwareUpdater.autoUpdate')}</a>.
|
||||
{:else}
|
||||
{$_('section.firmwareUpdater.swUpToDate')}
|
||||
{/if}
|
||||
|
@ -164,7 +200,7 @@
|
|||
<p>Loading...</p>
|
||||
{/if}
|
||||
<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>
|
||||
<input
|
||||
type="file"
|
||||
|
@ -180,7 +216,7 @@
|
|||
>Update firmware</Button
|
||||
>
|
||||
</div>
|
||||
<div class="col mt-2">
|
||||
<div class="col flex-fill">
|
||||
<label for="webuiFile" class="form-label">WebUI file (littlefs.bin)</label>
|
||||
<input
|
||||
type="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">
|
||||
<div class="textcontainer">
|
||||
{#if char.split('/').length}
|
||||
<span class="top-text">{char.split('/')[0]}</span>
|
||||
<hr />
|
||||
<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,9 @@
|
|||
{#if char.endsWith('bolt')}
|
||||
<ZapIcon></ZapIcon>
|
||||
{/if}
|
||||
{#if char.endsWith('bitaxe')}
|
||||
<img src="/bitaxe.webp" class="bitaxelogo" alt="BitAxe logo" />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if char === 'STS'}
|
||||
<div class="digit sats">S</div>
|
||||
|
@ -82,8 +86,22 @@
|
|||
</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);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -18,10 +18,15 @@
|
|||
InputGroup,
|
||||
InputGroupText,
|
||||
Label,
|
||||
Tooltip,
|
||||
Row
|
||||
} 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;
|
||||
|
||||
|
@ -160,57 +165,247 @@
|
|||
|
||||
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 sm = xs;
|
||||
export let md = sm;
|
||||
export let lg = md;
|
||||
export let xl = lg;
|
||||
export let xxl = xl;
|
||||
|
||||
let screenSettingsIsOpen: boolean,
|
||||
displaysAndLedIsOpen: boolean,
|
||||
dataSourceIsOpen: boolean,
|
||||
extraFeaturesIsOpen: boolean,
|
||||
systemIsOpen: boolean;
|
||||
</script>
|
||||
|
||||
<Col {xs} {sm} {md} {lg} {xl} {xxl}>
|
||||
<Card>
|
||||
<Col {xs} {sm} {md} {lg} {xl} {xxl} class="mb-4 mb-xl-0">
|
||||
<Card id="settings">
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Form on:submit={onSave}>
|
||||
<Form on:submit={onSave} class="clearfix">
|
||||
<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>
|
||||
<Col md="6" xl="12" xxl="6">
|
||||
<Input
|
||||
id="verticalDesc"
|
||||
bind:checked={$settings.verticalDesc}
|
||||
type="switch"
|
||||
bsSize={$uiSettings.inputSize}
|
||||
label={$_('section.settings.verticalDesc')}
|
||||
/>
|
||||
</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
|
||||
>
|
||||
<Col md="6">
|
||||
<Input
|
||||
type="select"
|
||||
bind:value={$settings.fgColor}
|
||||
bind:value={textColor}
|
||||
name="select"
|
||||
id="fgColor"
|
||||
id="textColor"
|
||||
on:change={setTextColor}
|
||||
bsSize={$uiSettings.inputSize}
|
||||
class={$uiSettings.selectClass}
|
||||
>
|
||||
<option value="0">{$_('colors.black')}</option>
|
||||
<option value="65535">{$_('colors.white')}</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>
|
||||
<option value="0">{$_('colors.black')} on {$_('colors.white')}</option>
|
||||
<option value="1">{$_('colors.white')} on {$_('colors.black')}</option>
|
||||
</Input>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Label md={6} for="timePerScreen" size={$uiSettings.inputSize}
|
||||
>{$_('section.settings.timePerScreen')}</Label
|
||||
|
@ -265,28 +460,6 @@
|
|||
<FormText>{$_('section.settings.shortAmountsWarning')}</FormText>
|
||||
</Col>
|
||||
</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>
|
||||
<Label md={6} for="ledBrightness" size={$uiSettings.inputSize}
|
||||
>{$_('section.settings.ledBrightness')}</Label
|
||||
|
@ -338,7 +511,7 @@
|
|||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
{#if $settings.hasLightLevel}
|
||||
{#if !$settings.flDisable && $settings.hasLightLevel}
|
||||
<Row>
|
||||
<Label md={6} for="luxLightToggle" size={$uiSettings.inputSize}
|
||||
>{$_('section.settings.luxLightToggle')} ({$settings.luxLightToggle})</Label
|
||||
|
@ -353,9 +526,156 @@
|
|||
max={1000}
|
||||
step={1}
|
||||
/>
|
||||
<FormText>{$_('section.settings.luxLightToggleText')}</FormText>
|
||||
</Col>
|
||||
</Row>
|
||||
{/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}
|
||||
<Row>
|
||||
<Label md={6} for="bitaxeHostname" size={$uiSettings.inputSize}
|
||||
|
@ -378,6 +698,40 @@
|
|||
</Col>
|
||||
</Row>
|
||||
{/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']}
|
||||
<Row>
|
||||
<Label md={6} for="nostrZapPubkey" size={$uiSettings.inputSize}
|
||||
|
@ -446,37 +800,79 @@
|
|||
</Row>
|
||||
{/if}
|
||||
<Row>
|
||||
<Label md={6} for="mempoolInstance" size="sm"
|
||||
>{$_('section.settings.mempoolnstance')}</Label
|
||||
{#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}
|
||||
{#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">
|
||||
<InputGroup size={$uiSettings.inputSize}>
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={$settings.mempoolInstance}
|
||||
name="mempoolInstance"
|
||||
id="mempoolInstance"
|
||||
disabled={$settings.ownDataSource}
|
||||
bsSize="sm"
|
||||
type="number"
|
||||
step="1"
|
||||
name="tzOffset"
|
||||
id="tzOffset"
|
||||
required
|
||||
></Input>
|
||||
<InputGroupText>
|
||||
<Input
|
||||
addon
|
||||
type="checkbox"
|
||||
bind:checked={$settings.mempoolSecure}
|
||||
disabled={$settings.ownDataSource}
|
||||
bsSize={$uiSettings.inputSize}
|
||||
bind:value={$settings.tzOffset}
|
||||
/>
|
||||
HTTPS
|
||||
</InputGroupText>
|
||||
<InputGroupText>{$_('time.minutes')}</InputGroupText>
|
||||
<Button type="button" color="info" on:click={getTzOffsetFromSystem}
|
||||
>{$_('auto-detect')}</Button
|
||||
>
|
||||
</InputGroup>
|
||||
<FormText>{$_('section.settings.mempoolInstanceHelpText')}</FormText>
|
||||
<FormText>{$_('section.settings.tzOffsetHelpText')}</FormText>
|
||||
</Col>
|
||||
</Row>
|
||||
{#if $settings.httpAuthEnabled}
|
||||
<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">
|
||||
<Input
|
||||
type="text"
|
||||
|
@ -489,7 +885,9 @@
|
|||
</Col>
|
||||
</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">
|
||||
<InputGroup size={$uiSettings.inputSize}>
|
||||
<Input
|
||||
|
@ -567,161 +965,6 @@
|
|||
</Col>
|
||||
</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">
|
||||
<Input
|
||||
id="otaEnabled"
|
||||
|
@ -750,47 +993,9 @@
|
|||
/>
|
||||
</Col>
|
||||
</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>
|
||||
<Col class="d-flex justify-content-end">
|
||||
<Button on:click={handleReset} color="secondary">{$_('button.reset')}</Button>
|
||||
|
|
|
@ -104,8 +104,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="status">
|
||||
<CardHeader>
|
||||
<CardTitle>{$_('section.status.title', { default: 'Status' })}</CardTitle>
|
||||
</CardHeader>
|
||||
|
@ -135,8 +135,8 @@
|
|||
{/each}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{#if $settings.actCurrencies}
|
||||
<div class="d-flex justify-content-center d-none d-sm-flex mt-2">
|
||||
{#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
|
||||
|
@ -151,7 +151,11 @@
|
|||
<hr />
|
||||
{#if $status.data}
|
||||
<section class={lightMode ? 'lightMode' : 'darkMode'}>
|
||||
<Rendered status={$status} className="btclock-wrapper"></Rendered>
|
||||
<Rendered
|
||||
status={$status}
|
||||
className="btclock-wrapper"
|
||||
verticalDesc={$settings.verticalDesc}
|
||||
></Rendered>
|
||||
</section>
|
||||
{$_('section.status.screenCycle')}:
|
||||
<a
|
||||
|
@ -228,7 +232,7 @@
|
|||
{#if !$settings.ownDataSource}
|
||||
{$_('section.status.wsPriceConnection')}:
|
||||
<span>
|
||||
{#if $status.connectionStatus && $status.connectionStatus.nostr}
|
||||
{#if $status.connectionStatus && $status.connectionStatus.price}
|
||||
✅
|
||||
{:else}
|
||||
❌
|
||||
|
@ -248,7 +252,7 @@
|
|||
{:else}
|
||||
{$_('section.status.wsDataConnection')}:
|
||||
<span>
|
||||
{#if $status.connectionStatus && $status.connectionStatus.price}
|
||||
{#if $status.connectionStatus && $status.connectionStatus.V2}
|
||||
✅
|
||||
{:else}
|
||||
❌
|
||||
|
|
|
@ -31,21 +31,21 @@
|
|||
<svelte:head>
|
||||
<title>API playground</title>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui-bundle.min.js"
|
||||
integrity="sha512-Ckle4LZv9LhAfEdohBdUi+QCu0e7HkXHTeSPXfbDzbCsR87QNTUBylkBEPsBNn4Ph83yK1hJ6f2uH4QMtB0hTA=="
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-bundle.min.js"
|
||||
integrity="sha512-7ihPQv5ibiTr0DW6onbl2MIKegdT6vjpPySyIb4Ftp68kER6Z7Yiub0tFoMmCHzZfQE9+M+KSjQndv6NhYxDgg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui-standalone-preset.min.js"
|
||||
integrity="sha512-qwGi7EG31HcylzamsmacHLZJrfUGRuuHEaCMcOojuNpMu+paR554VjaCZ9LdUVTrmF8xC03YVqTzuKx0SDdruA=="
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-standalone-preset.min.js"
|
||||
integrity="sha512-UrYi+60Ci3WWWcoDXbMmzpoi1xpERbwjPGij6wTh8fXl81qNdioNNHExr9ttnBebKF0ZbVnPlTPlw+zECUK1Xw=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui.min.css"
|
||||
integrity="sha512-Ck+X9SARG7WscOTG4a8Qod5Zgd1MZlz4VtyyucjMJ3PnZy2lUl7q/v/0055yIfGM/v+f+216ME0/dv0qqtm6+g=="
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui.min.css"
|
||||
integrity="sha512-+9UD8YSD9GF7FzOH38L9S6y56aYNx3R4dYbOCgvTJ2ZHpJScsahNdaMQJU/8osUiz9FPu0YZ8wdKf4evUbsGSg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
|
|
143
src/routes/convert/+page.svelte
Normal file
143
src/routes/convert/+page.svelte
Normal 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>
|
BIN
static/bitaxe.webp
Normal file
BIN
static/bitaxe.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
|
@ -1,11 +1,11 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
import { sveltePreprocess } from 'svelte-preprocess';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: preprocess({}),
|
||||
preprocess: sveltePreprocess({}),
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
|
|
|
@ -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('/');
|
||||
|
@ -137,6 +30,8 @@ test('api page has expected load button', async ({ page }) => {
|
|||
|
||||
test('timezone can be negative, zero and positive', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Show all' }).click();
|
||||
|
||||
const tzOffsetField = 'input#tzOffset';
|
||||
|
||||
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 }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Show all' }).click();
|
||||
|
||||
for (const field of ['#timePerScreen', '#fullRefreshMin', '#minSecPriceUpd']) {
|
||||
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 }) => {
|
||||
delete (settingsJson as { actCurrencies?: string[] }).actCurrencies;
|
||||
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Show all' }).click();
|
||||
|
||||
const inputField = 'input#fetchEurPrice';
|
||||
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 }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Show all' }).click();
|
||||
|
||||
for (const field of ['#nostrZapPubkey']) {
|
||||
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 }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Show all' }).click();
|
||||
|
||||
const nostrRelayField = page.getByLabel('Nostr Relay');
|
||||
|
88
tests/screenshots/viewport-screenshots.spec.ts
Normal file
88
tests/screenshots/viewport-screenshots.spec.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { initMock, settingsJson, statusJson } 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
|
||||
}
|
||||
];
|
||||
|
||||
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: '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
143
tests/shared.ts
Normal 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`
|
||||
});
|
||||
});
|
||||
};
|
|
@ -76,6 +76,14 @@ export default defineConfig({
|
|||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
quietDeps: true,
|
||||
silenceDeprecations: ['import']
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
globals: true,
|
||||
|
|
Loading…
Reference in a new issue