Compare commits

..

24 commits
872360 ... main

Author SHA1 Message Date
Djuri Baars
924be8fc2e Fix locale-related bugs and test it with screenshots
All checks were successful
/ check-changes (push) Successful in 5s
/ build (push) Successful in 3m20s
2024-12-20 18:57:36 +01:00
Djuri Baars
23529dbd4b Improve project layout
All checks were successful
/ check-changes (push) Successful in 15s
/ build (push) Successful in 3m38s
2024-12-20 18:19:01 +01:00
Djuri Baars
20c81628f1 More vite improvements
All checks were successful
/ check-changes (push) Successful in 13s
/ build (push) Successful in 3m35s
2024-12-20 17:59:52 +01:00
Djuri Baars
25258b43a7 Settings refactor 2024-12-20 17:56:10 +01:00
Djuri Baars
8a9c013f24 Remove old patches
Some checks failed
/ check-changes (push) Successful in 14s
/ build (push) Failing after 3m17s
2024-12-20 17:18:33 +01:00
Djuri Baars
cefa98148a Updates and cleanup
Some checks failed
/ build (push) Failing after 58s
/ check-changes (push) Successful in 5s
2024-12-20 17:16:50 +01:00
Djuri Baars
551d714cce Remove woff(1) assets, show something when mining pool logo is shown
All checks were successful
/ build (push) Successful in 3m35s
/ check-changes (push) Successful in 14s
2024-12-20 15:22:59 +01:00
Djuri Baars
fd328d4f05 Add more mining pools
All checks were successful
/ check-changes (push) Successful in 14s
/ build (push) Successful in 3m37s
2024-12-20 04:03:35 +01:00
Djuri Baars
dfe703d676 Fix capitalization
All checks were successful
/ check-changes (push) Successful in 14s
/ build (push) Successful in 3m31s
2024-12-20 01:23:45 +01:00
Djuri Baars
a00eb54573 Get available pools from device
All checks were successful
/ build (push) Successful in 3m23s
/ check-changes (push) Successful in 5s
2024-12-20 01:20:15 +01:00
711c625648 bugfix for long preferences key 2024-12-18 21:24:50 -06:00
f458417536 Add mining pool stats enable/disable toggle 2024-12-18 21:24:50 -06:00
0c70c74a1a still untested 2024-12-18 21:24:50 -06:00
2bea761d3c work-in-progress, untested 2024-12-18 21:24:50 -06:00
Djuri Baars
85b9b17506 Add alt tag to bitaxe logo
All checks were successful
/ build (push) Successful in 3m22s
/ check-changes (push) Successful in 5s
2024-12-18 01:28:17 +01:00
Djuri Baars
eff18ba0c3 Add bitaxe icon and modify tests for it
Some checks failed
/ check-changes (push) Successful in 5s
/ build (push) Failing after 53s
2024-12-18 01:24:21 +01:00
Djuri Baars
266a99be96 Add vertical screen description option
All checks were successful
/ build (push) Successful in 4m2s
/ check-changes (push) Successful in 5s
2024-12-18 00:45:26 +01:00
Djuri Baars
653a39d0a3 Improvements for xs screens
All checks were successful
/ check-changes (push) Successful in 5s
/ build (push) Successful in 3m57s
2024-12-12 23:04:13 +01:00
Djuri Baars
68c247f3cc Fix screen selector UI, add screenshot maker
All checks were successful
/ check-changes (push) Successful in 7s
/ build (push) Successful in 3m57s
2024-12-12 19:50:36 +01:00
Djuri Baars
25e91b2086 Dependencies update, add switch for frontlight off when dark
All checks were successful
/ check-changes (push) Successful in 14s
/ build (push) Successful in 3m54s
2024-12-10 14:49:44 +01:00
Djuri Baars
f0fa58b5ea Fix LittleFS image generation
All checks were successful
/ check-changes (push) Successful in 6s
/ build (push) Successful in 3m29s
2024-11-29 00:57:07 +01:00
Djuri Baars
b8ed628bf5 Fix formatting
All checks were successful
/ check-changes (push) Successful in 10s
/ build (push) Successful in 4m29s
2024-11-29 00:13:43 +01:00
Djuri Baars
00af5f6521 Dependency updates and small fixes
Some checks failed
/ check-changes (push) Successful in 7s
/ build (push) Failing after 1m6s
2024-11-29 00:10:33 +01:00
Djuri Baars
51cce2ee9f Add color mode switcher 2024-11-28 23:30:14 +01:00
37 changed files with 1583 additions and 1184 deletions

View file

@ -81,7 +81,7 @@ jobs:
- name: Check GZipped directory size - name: Check GZipped directory size
run: | run: |
# Set the threshold size in bytes # Set the threshold size in bytes
THRESHOLD=419840 THRESHOLD=410000
# Calculate the total size of files in the directory # Calculate the total size of files in the directory
DIRECTORY_SIZE=$(du -b -s build_gz | awk '{print $1}') DIRECTORY_SIZE=$(du -b -s build_gz | awk '{print $1}')
@ -98,7 +98,7 @@ jobs:
- name: Build LittleFS - name: Build LittleFS
run: | run: |
set -e set -e
/tmp/mklittlefs/mklittlefs -c build_gz -s 419840 output/littlefs.bin /tmp/mklittlefs/mklittlefs -c build_gz -s 410000 output/littlefs.bin
- name: Upload artifacts - name: Upload artifacts
uses: https://code.forgejo.org/forgejo/upload-artifact@v4 uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with: with:

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": {
@ -32,6 +33,7 @@
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.2.6",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.79.3", "sass": "^1.79.3",
"svelte": "^4.2.19", "svelte": "^4.2.19",
"svelte-check": "^4.0.2", "svelte-check": "^4.0.2",

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 40fa4c6..738cabf 100644 index ad519c9..bee1516 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
@@ -655,9 +655,9 @@ async function kit({ svelte_config }) { @@ -644,9 +644,9 @@ async function kit({ svelte_config }) {
input, input,
output: { output: {
format: 'esm', format: 'esm',
@ -13,5 +13,18 @@ index 40fa4c6..738cabf 100644
+ chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[hash].${ext}`, + chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[hash].${ext}`,
+ assetFileNames: `${prefix}/assets/[hash][extname]`, + assetFileNames: `${prefix}/assets/[hash][extname]`,
hoistTransitiveImports: false, hoistTransitiveImports: false,
sourcemapIgnoreList sourcemapIgnoreList,
}, manualChunks:
@@ -661,9 +661,9 @@ async function kit({ svelte_config }) {
worker: {
rollupOptions: {
output: {
- entryFileNames: `${prefix}/workers/[name]-[hash].js`,
- chunkFileNames: `${prefix}/workers/chunks/[name]-[hash].js`,
- assetFileNames: `${prefix}/workers/assets/[name]-[hash][extname]`,
+ entryFileNames: `${prefix}/workers/[hash].js`,
+ chunkFileNames: `${prefix}/workers/chunks/[hash].js`,
+ assetFileNames: `${prefix}/workers/assets/[hash][extname]`,
hoistTransitiveImports: false
}
}

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,7 +55,10 @@
"ledFlashOnZap": "LED blinkt bei Nostr Zap", "ledFlashOnZap": "LED blinkt bei Nostr Zap",
"flFlashOnZap": "Displaybeleuchting bei Nostr Zap", "flFlashOnZap": "Displaybeleuchting bei Nostr Zap",
"showAll": "Alle anzeigen", "showAll": "Alle anzeigen",
"hideAll": "Alles ausblenden" "hideAll": "Alles ausblenden",
"flOffWhenDark": "Displaybeleuchtung aus, wenn es dunkel ist",
"luxLightToggleText": "Zum Deaktivieren auf 0 setzen",
"verticalDesc": "Vrtikale Bildschirmbeschreibung"
}, },
"control": { "control": {
"systemInfo": "Systeminfo", "systemInfo": "Systeminfo",

View file

@ -44,6 +44,9 @@
"useNostr": "Use Nostr data source", "useNostr": "Use Nostr data source",
"bitaxeHostname": "BitAxe hostname or IP", "bitaxeHostname": "BitAxe hostname or IP",
"bitaxeEnabled": "Enable BitAxe", "bitaxeEnabled": "Enable BitAxe",
"miningPoolStats": "Enable Mining Pool Stats",
"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",
@ -67,7 +70,10 @@
"ledFlashOnZap": "LED flash on Nostr Zap", "ledFlashOnZap": "LED flash on Nostr Zap",
"flFlashOnZap": "Frontlight flash on Nostr Zap", "flFlashOnZap": "Frontlight flash on Nostr Zap",
"showAll": "Show all", "showAll": "Show all",
"hideAll": "Hide all" "hideAll": "Hide all",
"flOffWhenDark": "Frontlight off when dark",
"luxLightToggleText": "Set to 0 to disable",
"verticalDesc": "Use vertical screen description"
}, },
"control": { "control": {
"systemInfo": "System info", "systemInfo": "System info",

View file

@ -54,7 +54,10 @@
"ledFlashOnZap": "LED parpadeante con Nostr Zap", "ledFlashOnZap": "LED parpadeante con Nostr Zap",
"flFlashOnZap": "Flash de luz frontal con Nostr Zap", "flFlashOnZap": "Flash de luz frontal con Nostr Zap",
"showAll": "Mostrar todo", "showAll": "Mostrar todo",
"hideAll": "Ocultar todo" "hideAll": "Ocultar todo",
"flOffWhenDark": "Luz de la pantalla cuando está oscuro",
"luxLightToggleText": "Establecer en 0 para desactivar",
"verticalDesc": "Descripción de pantalla vertical"
}, },
"control": { "control": {
"turnOff": "Apagar", "turnOff": "Apagar",

View file

@ -55,7 +55,10 @@
"ledFlashOnZap": "Knipper LED bij Nostr Zap", "ledFlashOnZap": "Knipper LED bij Nostr Zap",
"flFlashOnZap": "Knipper displaylicht bij Nostr Zap", "flFlashOnZap": "Knipper displaylicht bij Nostr Zap",
"showAll": "Toon alles", "showAll": "Toon alles",
"hideAll": "Alles verbergen" "hideAll": "Alles verbergen",
"flOffWhenDark": "Displaylicht uit als het donker is",
"luxLightToggleText": "Stel in op 0 om uit te schakelen",
"verticalDesc": "Verticale schermbeschrijving"
}, },
"control": { "control": {
"systemInfo": "Systeeminformatie", "systemInfo": "Systeeminformatie",

View file

@ -1,19 +1,33 @@
@use '@fontsource/ubuntu/scss/mixins' as Ubuntu;
@use '@fontsource/antonio/scss/mixins' as Antonio;
@import '../node_modules/bootstrap/scss/functions'; @import '../node_modules/bootstrap/scss/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/oswald/latin-400.css'; @include Ubuntu.faces(
@import '@fontsource/antonio/latin-400.css'; $subsets: latin,
$weights: 400,
$formats: 'woff2',
$directory: '@fontsource/ubuntu/files'
);
@include Antonio.faces(
$subsets: latin,
$weights: 400,
$formats: 'woff2',
$directory: '@fontsource/antonio/files'
);
@import './satsymbol'; @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';
@ -43,6 +57,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 +101,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 +128,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,19 +169,25 @@ 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 { &.verticalDesc > .splitText:first-child {
.textcontainer {
transform: rotate(-90deg);
}
}
.splitText .textcontainer :first-child::after {
display: block; display: block;
content: ''; content: '';
margin-top: 0px; margin-top: 0px;
border-bottom: 2px solid; border-bottom: 2px solid;
margin-bottom: 3px; // margin-bottom: 3px;
} }
.splitText { .splitText {
font-size: calc(0.5vw + 1vh); font-size: calc(0.3vw + 1vh);
.top-text, .top-text,
.bottom-text { .bottom-text {
@ -242,3 +313,7 @@ nav {
input[type='number'] { input[type='number'] {
text-align: right; text-align: right;
} }
.lightMode .bitaxelogo {
filter: brightness(0) saturate(100%);
}

View file

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

View file

@ -12,9 +12,12 @@
NavbarBrand, NavbarBrand,
NavbarToggler 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 '$lib/components';
import { derived } from 'svelte/store';
export const setLocale = (lang: string) => () => { export const setLocale = (lang: string) => () => {
locale.set(lang); locale.set(lang);
@ -37,19 +40,20 @@
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'];
} }
}; };
let languageNames = {}; let languageNames = {};
const currentLocale = derived(locale, ($locale) => $locale || 'en');
locale.subscribe(() => { locale.subscribe(() => {
if ($locale) { const localeToUse = $locale || 'en';
let newLanguageNames = new Intl.DisplayNames([$locale], { type: 'language' }); let newLanguageNames = new Intl.DisplayNames([localeToUse], { type: 'language' });
for (let l of $locales) { for (let l of $locales) {
languageNames[l] = newLanguageNames.of(l); languageNames[l] = newLanguageNames.of(l) || l;
}
} }
}); });
@ -60,8 +64,23 @@
}; };
</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>
<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} /> <NavbarToggler on:click={toggle} />
<Collapse {isOpen} navbar expand="sm"> <Collapse {isOpen} navbar expand="sm">
@ -77,8 +96,11 @@
</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($currentLocale)}
{languageNames[$currentLocale] || 'English'}</DropdownToggle
>
<DropdownMenu end> <DropdownMenu end>
{#each $locales as locale} {#each $locales as locale}
<DropdownItem on:click={setLocale(locale)} <DropdownItem on:click={setLocale(locale)}
@ -88,8 +110,11 @@
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
{/if} {/if}
<ColorSchemeSwitcher></ColorSchemeSwitcher>
</Collapse> </Collapse>
</Navbar> </Navbar>
<!-- +layout.svelte --> <!-- +layout.svelte -->
<main>
<slot /> <slot />
</main>

View file

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

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';
@ -16,12 +17,6 @@
bgColor: '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,
@ -60,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();
@ -72,6 +103,11 @@
}); });
function handleResize() { function handleResize() {
if (observer) {
observer.disconnect();
}
setupObserver();
updateScreenSize(); updateScreenSize();
} }
@ -125,7 +161,9 @@
<Container fluid> <Container fluid>
<Row> <Row>
<Control bind:settings on:showToast={showToast} 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>

View file

@ -200,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"
@ -216,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

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

View file

@ -3,7 +3,6 @@
import { PUBLIC_BASE_URL } from '$lib/config'; import { PUBLIC_BASE_URL } from '$lib/config';
import { uiSettings } from '$lib/uiSettings'; import { uiSettings } from '$lib/uiSettings';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { import {
Button, Button,
@ -13,18 +12,12 @@
CardTitle, CardTitle,
Col, Col,
Form, Form,
FormText,
Input,
InputGroup,
InputGroupText,
Label,
Tooltip,
Row Row
} from '@sveltestrap/sveltestrap'; } from '@sveltestrap/sveltestrap';
import EyeIcon from '../icons/EyeIcon.svelte'; import EyeIcon from 'svelte-bootstrap-icons/lib/Eye.svelte';
import EyeSlashIcon from '../icons/EyeSlashIcon.svelte'; import EyeSlashIcon from 'svelte-bootstrap-icons/lib/EyeSlash.svelte';
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import ToggleHeader from '../components/ToggleHeader.svelte'; import { SettingsSwitch, SettingsInput, SettingsSelect, ToggleHeader } from '$lib/components';
export let settings; export let settings;
@ -42,6 +35,21 @@
['5dBm', 20] // 5dBm ['5dBm', 20] // 5dBm
]); ]);
const miningPoolMap = new Map<string, string>([
['noderunners', 'Noderunners.network'],
['braiins', 'Braiins Pool'],
['ocean', 'ocean.xyz'],
['satoshi_radio', 'Satoshi Radio pool'],
['public_pool', 'public-pool.io'],
['gobrrr_pool', 'Go Brrr pool']
]);
const getMiningPoolName = (name: string) => {
if (miningPoolMap.has(name)) return miningPoolMap.get(name);
return name;
};
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const handleReset = (e: Event) => { const handleReset = (e: Event) => {
@ -139,7 +147,7 @@
} }
}; };
const checkValidNostrPubkey = (key) => { const checkValidNostrPubkey = (key: string) => {
if (isValidNpub($settings[key])) { if (isValidNpub($settings[key])) {
dispatch('showToast', { dispatch('showToast', {
color: 'info', color: 'info',
@ -219,8 +227,8 @@
systemIsOpen: 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"> <div class="float-end">
<small <small
@ -241,6 +249,7 @@
</div> </div>
<CardTitle>{$_('section.settings.title', { default: 'Settings' })}</CardTitle> <CardTitle>{$_('section.settings.title', { default: 'Settings' })}</CardTitle>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<Form on:submit={onSave} class="clearfix"> <Form on:submit={onSave} class="clearfix">
<Row> <Row>
@ -250,98 +259,86 @@
isOpen={screenSettingsIsOpen} isOpen={screenSettingsIsOpen}
> >
<Row> <Row>
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="stealFocus" id="stealFocus"
bind:checked={$settings.stealFocus} bind:checked={$settings.stealFocus}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.StealFocusOnNewBlock')} label={$_('section.settings.StealFocusOnNewBlock')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/> />
</Col> <SettingsSwitch
<Col md="6" xl="12" xxl="6">
<Input
id="mcapBigChar" id="mcapBigChar"
bind:checked={$settings.mcapBigChar} bind:checked={$settings.mcapBigChar}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.useBigCharsMcap')} label={$_('section.settings.useBigCharsMcap')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/> />
</Col> <SettingsSwitch
<Col md="6" xl="12" xxl="6">
<Input
id="useBlkCountdown" id="useBlkCountdown"
bind:checked={$settings.useBlkCountdown} bind:checked={$settings.useBlkCountdown}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.useBlkCountdown')} label={$_('section.settings.useBlkCountdown')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/> />
</Col> <SettingsSwitch
<Col md="6" xl="12" xxl="6">
<Input
id="useSatsSymbol" id="useSatsSymbol"
bind:checked={$settings.useSatsSymbol} bind:checked={$settings.useSatsSymbol}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.useSatsSymbol')} label={$_('section.settings.useSatsSymbol')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/> />
</Col> <SettingsSwitch
<Col md="6" xl="12" xxl="6">
<Input
id="suffixPrice" id="suffixPrice"
bind:checked={$settings.suffixPrice} bind:checked={$settings.suffixPrice}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.suffixPrice')} label={$_('section.settings.suffixPrice')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/> />
</Col> <SettingsSwitch
<Col md="6" xl="12" xxl="6">
<Input
disabled={!$settings.suffixPrice}
id="mowMode" id="mowMode"
bind:checked={$settings.mowMode} bind:checked={$settings.mowMode}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.mowMode')} label={$_('section.settings.mowMode')}
/> size={$uiSettings.inputSize}
</Col> col={{ md: '6', xl: '12', xxl: '6' }}
<Col md="6" xl="12" xxl="6">
<Input
disabled={!$settings.suffixPrice} disabled={!$settings.suffixPrice}
/>
<SettingsSwitch
id="suffixShareDot" id="suffixShareDot"
bind:checked={$settings.suffixShareDot} bind:checked={$settings.suffixShareDot}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.suffixShareDot')} label={$_('section.settings.suffixShareDot')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
disabled={!$settings.suffixPrice}
/>
<SettingsSwitch
id="verticalDesc"
bind:checked={$settings.verticalDesc}
label={$_('section.settings.verticalDesc')}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/> />
</Col>
{#if !$settings.actCurrencies} {#if !$settings.actCurrencies}
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="fetchEurPrice" id="fetchEurPrice"
bind:checked={$settings.fetchEurPrice} bind:checked={$settings.fetchEurPrice}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.fetchEuroPrice')} ({$_('restartRequired')})" label="{$_('section.settings.fetchEuroPrice')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/> />
</Col>
{/if} {/if}
</Row> </Row>
<Row> <Row>
<h5>{$_('section.settings.screens')}</h5> <h5>{$_('section.settings.screens')}</h5>
{#if $settings.screens} {#if $settings.screens}
{#each $settings.screens as s} {#each $settings.screens as s}
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="screens_{s.id}" id="screens_{s.id}"
bind:checked={s.enabled} bind:checked={s.enabled}
type="switch"
bsSize={$uiSettings.inputSize}
label={s.name} label={s.name}
size={$uiSettings.inputSize}
col={{ md: '6', xl: '12', xxl: '6' }}
/> />
</Col>
{/each} {/each}
{/if} {/if}
</Row> </Row>
@ -375,207 +372,157 @@
header={$_('section.settings.section.displaysAndLed')} header={$_('section.settings.section.displaysAndLed')}
isOpen={displaysAndLedIsOpen} isOpen={displaysAndLedIsOpen}
> >
<Row> <SettingsSelect
<Label md={6} for="textColor" size={$uiSettings.inputSize}
>{$_('section.settings.textColor', { default: 'Text color' })}</Label
>
<Col md="6">
<Input
type="select"
bind:value={textColor}
name="select"
id="textColor" id="textColor"
on:change={setTextColor} label={$_('section.settings.textColor')}
bsSize={$uiSettings.inputSize} bind:value={textColor}
class={$uiSettings.selectClass} options={[
> [$_('colors.black') + ' on ' + $_('colors.white'), '0'],
<option value="0">{$_('colors.black')} on {$_('colors.white')}</option> [$_('colors.white') + ' on ' + $_('colors.black'), '1']
<option value="1">{$_('colors.white')} on {$_('colors.black')}</option> ]}
</Input> size={$uiSettings.inputSize}
</Col> selectClass={$uiSettings.selectClass}
</Row> onChange={setTextColor}
/>
<Row> <SettingsInput
<Label md={6} for="timePerScreen" size={$uiSettings.inputSize}
>{$_('section.settings.timePerScreen')}</Label
>
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type="number"
id="timePerScreen" id="timePerScreen"
min={1} label={$_('section.settings.timePerScreen')}
step="1"
required
bind:value={$settings.timePerScreen} bind:value={$settings.timePerScreen}
/>
<InputGroupText>{$_('time.minutes')}</InputGroupText>
</InputGroup>
</Col>
</Row>
<Row>
<Label md={6} for="fullRefreshMin" size={$uiSettings.inputSize}
>{$_('section.settings.fullRefreshEvery')}</Label
>
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type="number" type="number"
min={1}
step="1"
required={true}
suffix={$_('time.minutes')}
size={$uiSettings.inputSize}
/>
<SettingsInput
id="fullRefreshMin" id="fullRefreshMin"
min={1} label={$_('section.settings.fullRefreshEvery')}
step="1"
required
bind:value={$settings.fullRefreshMin} bind:value={$settings.fullRefreshMin}
/>
<InputGroupText>{$_('time.minutes')}</InputGroupText>
</InputGroup>
</Col>
</Row>
<Row>
<Label md={6} for="minSecPriceUpd" size={$uiSettings.inputSize}
>{$_('section.settings.timeBetweenPriceUpdates')}</Label
>
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type="number" type="number"
id="minSecPriceUpd"
min={1} min={1}
step="1" step="1"
bind:value={$settings.minSecPriceUpd} required={true}
suffix={$_('time.minutes')}
size={$uiSettings.inputSize}
/> />
<InputGroupText>{$_('time.seconds')}</InputGroupText>
</InputGroup> <SettingsInput
<FormText>{$_('section.settings.shortAmountsWarning')}</FormText> id="minSecPriceUpd"
</Col> label={$_('section.settings.timeBetweenPriceUpdates')}
</Row> bind:value={$settings.minSecPriceUpd}
<Row> type="number"
<Label md={6} for="ledBrightness" size={$uiSettings.inputSize} min={1}
>{$_('section.settings.ledBrightness')}</Label step="1"
> suffix={$_('time.seconds')}
<Col md="6"> helpText={$_('section.settings.shortAmountsWarning')}
<Input size={$uiSettings.inputSize}
type="range" />
name="ledBrightness"
<SettingsInput
id="ledBrightness" id="ledBrightness"
label={$_('section.settings.ledBrightness')}
bind:value={$settings.ledBrightness} bind:value={$settings.ledBrightness}
type="range"
min={0} min={0}
max={255} max={255}
step={1} step={1}
size={$uiSettings.inputSize}
/> />
</Col>
</Row>
{#if $settings.hasFrontlight && !$settings.flDisable} {#if $settings.hasFrontlight && !$settings.flDisable}
<Row> <SettingsInput
<Label md={6} for="flMaxBrightness" size={$uiSettings.inputSize}
>{$_('section.settings.flMaxBrightness')}</Label
>
<Col md="6">
<Input
type="range"
name="flMaxBrightness"
id="flMaxBrightness" id="flMaxBrightness"
label={$_('section.settings.flMaxBrightness')}
bind:value={$settings.flMaxBrightness} bind:value={$settings.flMaxBrightness}
on:change={onFlBrightnessChange} type="range"
min={0} min={0}
max={4095} max={4095}
step={1} step={1}
size={$uiSettings.inputSize}
onChange={onFlBrightnessChange}
/> />
</Col>
</Row> <SettingsInput
<Row>
<Label md={6} for="flEffectDelay" size={$uiSettings.inputSize}
>{$_('section.settings.flEffectDelay')}</Label
>
<Col md="6">
<Input
type="range"
name="flEffectDelay"
id="flEffectDelay" id="flEffectDelay"
label={$_('section.settings.flEffectDelay')}
bind:value={$settings.flEffectDelay} bind:value={$settings.flEffectDelay}
type="range"
min={5} min={5}
max={300} max={300}
step={1} step={1}
size={$uiSettings.inputSize}
/> />
</Col>
</Row>
{/if} {/if}
{#if !$settings.flDisable && $settings.hasLightLevel} {#if !$settings.flDisable && $settings.hasLightLevel}
<Row> <SettingsInput
<Label md={6} for="luxLightToggle" size={$uiSettings.inputSize}
>{$_('section.settings.luxLightToggle')} ({$settings.luxLightToggle})</Label
>
<Col md="6">
<Input
type="range"
name="luxLightToggle"
id="luxLightToggle" id="luxLightToggle"
label={`${$_('section.settings.luxLightToggle')} (${$settings.luxLightToggle})`}
bind:value={$settings.luxLightToggle} bind:value={$settings.luxLightToggle}
type="range"
min={0} min={0}
max={1000} max={1000}
step={1} step={1}
helpText={$_('section.settings.luxLightToggleText')}
size={$uiSettings.inputSize}
/> />
</Col>
</Row>
{/if} {/if}
<Row> <Row>
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="ledTestOnPower" id="ledTestOnPower"
bind:checked={$settings.ledTestOnPower} bind:checked={$settings.ledTestOnPower}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.ledPowerOnTest')} label={$_('section.settings.ledPowerOnTest')}
size={$uiSettings.inputSize}
/> />
</Col>
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="ledFlashOnUpd" id="ledFlashOnUpd"
bind:checked={$settings.ledFlashOnUpd} bind:checked={$settings.ledFlashOnUpd}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.ledFlashOnBlock')} label={$_('section.settings.ledFlashOnBlock')}
size={$uiSettings.inputSize}
/> />
</Col>
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="disableLeds" id="disableLeds"
bind:checked={$settings.disableLeds} bind:checked={$settings.disableLeds}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.disableLeds')} label={$_('section.settings.disableLeds')}
size={$uiSettings.inputSize}
/> />
</Col>
{#if $settings.hasFrontlight} {#if $settings.hasFrontlight}
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="flDisable" id="flDisable"
bind:checked={$settings.flDisable} bind:checked={$settings.flDisable}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flDisable')} label={$_('section.settings.flDisable')}
size={$uiSettings.inputSize}
/> />
</Col>
{/if} {/if}
{#if $settings.hasFrontlight && !$settings.flDisable} {#if $settings.hasFrontlight && !$settings.flDisable}
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="flAlwaysOn" id="flAlwaysOn"
bind:checked={$settings.flAlwaysOn} bind:checked={$settings.flAlwaysOn}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flAlwaysOn')} label={$_('section.settings.flAlwaysOn')}
size={$uiSettings.inputSize}
/> />
</Col>
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="flFlashOnUpd" id="flFlashOnUpd"
bind:checked={$settings.flFlashOnUpd} bind:checked={$settings.flFlashOnUpd}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flFlashOnUpd')} label={$_('section.settings.flFlashOnUpd')}
size={$uiSettings.inputSize}
/>
<SettingsSwitch
id="flOffWhenDark"
bind:checked={$settings.flOffWhenDark}
label={$_('section.settings.flOffWhenDark')}
size={$uiSettings.inputSize}
/> />
</Col>
{/if} {/if}
</Row> </Row>
</ToggleHeader> </ToggleHeader>
@ -584,69 +531,40 @@
header={$_('section.settings.section.dataSource')} header={$_('section.settings.section.dataSource')}
isOpen={dataSourceIsOpen} isOpen={dataSourceIsOpen}
> >
<Row> <SettingsInput
<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" id="mempoolInstance"
label={$_('section.settings.mempoolnstance')}
bind:value={$settings.mempoolInstance}
disabled={$settings.ownDataSource} disabled={$settings.ownDataSource}
bsSize="sm" required={true}
required helpText={$_('section.settings.mempoolInstanceHelpText')}
></Input> size={$uiSettings.inputSize}
<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> <Row>
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="ownDataSource" id="ownDataSource"
bind:checked={$settings.ownDataSource} bind:checked={$settings.ownDataSource}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.ownDataSource')} ({$_('restartRequired')})" label="{$_('section.settings.ownDataSource')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/> />
</Col>
{#if $settings.nostrRelay} {#if $settings.nostrRelay}
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="useNostr" id="useNostr"
bind:checked={$settings.useNostr} bind:checked={$settings.useNostr}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.useNostr')} ({$_('restartRequired')})" label="{$_('section.settings.useNostr')} ({$_('restartRequired')})"
></Input> size={$uiSettings.inputSize}
<Tooltip target="useNostr" placement="left"> />
{$_('section.settings.useNostrTooltip')}
</Tooltip>
</Col>
{/if} {/if}
{#if 'stagingSource' in $settings} {#if 'stagingSource' in $settings}
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="stagingSource" id="stagingSource"
bind:checked={$settings.stagingSource} bind:checked={$settings.stagingSource}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.stagingSource')} ({$_('restartRequired')})" label="{$_('section.settings.stagingSource')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/> />
</Col>
{/if} {/if}
</Row> </Row>
</ToggleHeader> </ToggleHeader>
@ -656,287 +574,213 @@
isOpen={extraFeaturesIsOpen} isOpen={extraFeaturesIsOpen}
> >
{#if $settings.bitaxeEnabled} {#if $settings.bitaxeEnabled}
<Row> <SettingsInput
<Label md={6} for="bitaxeHostname" size={$uiSettings.inputSize}
>{$_('section.settings.bitaxeHostname')}</Label
>
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type="text"
bind:value={$settings.bitaxeHostname}
name="bitaxeHostname"
valid={validBitaxe}
id="bitaxeHostname" id="bitaxeHostname"
required label={$_('section.settings.bitaxeHostname')}
></Input> bind:value={$settings.bitaxeHostname}
required={true}
valid={validBitaxe}
size={$uiSettings.inputSize}
>
<Button type="button" color="success" on:click={testBitaxe} <Button type="button" color="success" on:click={testBitaxe}
>{$_('test', { default: 'Test' })}</Button >{$_('test', { default: 'Test' })}</Button
> >
</InputGroup> </SettingsInput>
</Col> {/if}
</Row> {#if $settings.miningPoolStats}
<SettingsSelect
id="miningPoolName"
label={$_('section.settings.miningPoolName')}
bind:value={$settings.miningPoolName}
options={$settings.availablePools.map((pool) => [getMiningPoolName(pool), pool])}
size={$uiSettings.inputSize}
selectClass={$uiSettings.selectClass}
/>
<SettingsInput
id="miningPoolUser"
label={$_('section.settings.miningPoolUser')}
bind:value={$settings.miningPoolUser}
required={true}
size={$uiSettings.inputSize}
/>
{/if} {/if}
{#if 'nostrZapNotify' in $settings && $settings['nostrZapNotify']} {#if 'nostrZapNotify' in $settings && $settings['nostrZapNotify']}
<Row> <SettingsInput
<Label md={6} for="nostrZapPubkey" size={$uiSettings.inputSize}
>{$_('section.settings.nostrZapPubkey')}</Label
>
<Col md="6">
<Input
type="text"
bind:value={$settings.nostrZapPubkey}
name="nostrZapPubkey"
id="nostrZapPubkey" id="nostrZapPubkey"
on:change={() => checkValidNostrPubkey('nostrZapPubkey')} label={$_('section.settings.nostrZapPubkey')}
invalid={!isValidHexPubKey($settings.nostrZapPubkey)} bind:value={$settings.nostrZapPubkey}
bsSize={$uiSettings.inputSize} required={true}
required
minlength="64" minlength="64"
></Input> invalid={!isValidHexPubKey($settings.nostrZapPubkey)}
{#if !isValidHexPubKey($settings.nostrZapPubkey)} helpText={!isValidHexPubKey($settings.nostrZapPubkey)
<FormText>{$_('section.settings.invalidNostrPubkey')}</FormText> ? $_('section.settings.invalidNostrPubkey')
{/if} : undefined}
</Col> size={$uiSettings.inputSize}
</Row> onChange={() => checkValidNostrPubkey('nostrZapPubkey')}
/>
{/if} {/if}
{#if $settings.useNostr} {#if $settings.useNostr}
<Row> <SettingsInput
<Label md={6} for="nostrPubKey" size={$uiSettings.inputSize}
>{$_('section.settings.nostrPubKey')}</Label
>
<Col md="6">
<Input
type="text"
bind:value={$settings.nostrPubKey}
name="nostrPubKey"
id="nostrPubKey" id="nostrPubKey"
on:change={() => checkValidNostrPubkey('nostrPubKey')} label={$_('section.settings.nostrPubKey')}
bind:value={$settings.nostrPubKey}
invalid={!isValidHexPubKey($settings.nostrPubKey)} invalid={!isValidHexPubKey($settings.nostrPubKey)}
bsSize={$uiSettings.inputSize} helpText={!isValidHexPubKey($settings.nostrPubKey)
></Input> ? $_('section.settings.invalidNostrPubkey')
{#if !isValidHexPubKey($settings.nostrPubKey)} : undefined}
<FormText>{$_('section.settings.invalidNostrPubkey')}</FormText> size={$uiSettings.inputSize}
{/if} onChange={() => checkValidNostrPubkey('nostrPubKey')}
</Col> />
</Row>
{/if} {/if}
{#if 'nostrZapNotify' in $settings || $settings.useNostr} {#if 'nostrZapNotify' in $settings || $settings.useNostr}
<Row> <SettingsInput
<Label md={6} for="nostrRelay" size={$uiSettings.inputSize}
>{$_('section.settings.nostrRelay')}</Label
>
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type="text"
bind:value={$settings.nostrRelay}
name="nostrRelay"
id="nostrRelay" id="nostrRelay"
label={$_('section.settings.nostrRelay')}
bind:value={$settings.nostrRelay}
required={true}
valid={validNostrRelay} valid={validNostrRelay}
bsSize={$uiSettings.inputSize} size={$uiSettings.inputSize}
required >
></Input>
<Button type="button" color="success" on:click={testNostrRelay} <Button type="button" color="success" on:click={testNostrRelay}
>{$_('test', { default: 'Test' })}</Button >{$_('test', { default: 'Test' })}</Button
> >
</InputGroup> </SettingsInput>
</Col>
</Row>
{/if} {/if}
<Row> <Row>
{#if 'bitaxeEnabled' in $settings} {#if 'bitaxeEnabled' in $settings}
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="bitaxeEnabled" id="bitaxeEnabled"
bind:checked={$settings.bitaxeEnabled} bind:checked={$settings.bitaxeEnabled}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.bitaxeEnabled')} ({$_('restartRequired')})" label="{$_('section.settings.bitaxeEnabled')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/>
{/if}
{#if 'miningPoolStats' in $settings}
<SettingsSwitch
id="miningPoolStats"
bind:checked={$settings.miningPoolStats}
label="{$_('section.settings.miningPoolStats')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/> />
</Col>
{/if} {/if}
{#if 'nostrZapNotify' in $settings} {#if 'nostrZapNotify' in $settings}
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="nostrZapNotify" id="nostrZapNotify"
bind:checked={$settings.nostrZapNotify} bind:checked={$settings.nostrZapNotify}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.nostrZapNotify')} ({$_('restartRequired')})" label="{$_('section.settings.nostrZapNotify')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/> />
</Col> <SettingsSwitch
<Col md="6" xl="12" xxl="6">
<Input
id="ledFlashOnZap" id="ledFlashOnZap"
bind:checked={$settings.ledFlashOnZap} bind:checked={$settings.ledFlashOnZap}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.ledFlashOnZap')} label={$_('section.settings.ledFlashOnZap')}
size={$uiSettings.inputSize}
/> />
</Col>
{#if $settings.hasFrontlight && !$settings.flDisable} {#if $settings.hasFrontlight && !$settings.flDisable}
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="flFlashOnZap" id="flFlashOnZap"
bind:checked={$settings.flFlashOnZap} bind:checked={$settings.flFlashOnZap}
type="switch"
bsSize={$uiSettings.inputSize}
label={$_('section.settings.flFlashOnZap')} label={$_('section.settings.flFlashOnZap')}
size={$uiSettings.inputSize}
/> />
</Col>
{/if} {/if}
{/if} {/if}
</Row> </Row>
</ToggleHeader> </ToggleHeader>
</Row><Row> </Row><Row>
<ToggleHeader header={$_('section.settings.section.system')} isOpen={systemIsOpen}> <ToggleHeader header={$_('section.settings.section.system')} isOpen={systemIsOpen}>
<Row> <SettingsInput
<Label md={6} for="tzOffset" size={$uiSettings.inputSize} id="tzOffset"
>{$_('section.settings.timezoneOffset')}</Label label={$_('section.settings.timezoneOffset')}
> bind:value={$settings.tzOffset}
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type="number" type="number"
step="1" step="1"
name="tzOffset" required={true}
id="tzOffset" suffix={$_('time.minutes')}
required helpText={$_('section.settings.tzOffsetHelpText')}
bind:value={$settings.tzOffset} size={$uiSettings.inputSize}
/> >
<InputGroupText>{$_('time.minutes')}</InputGroupText>
<Button type="button" color="info" on:click={getTzOffsetFromSystem} <Button type="button" color="info" on:click={getTzOffsetFromSystem}
>{$_('auto-detect')}</Button >{$_('auto-detect')}</Button
> >
</InputGroup> </SettingsInput>
<FormText>{$_('section.settings.tzOffsetHelpText')}</FormText>
</Col>
</Row>
{#if $settings.httpAuthEnabled} {#if $settings.httpAuthEnabled}
<Row> <SettingsInput
<Label md={6} for="httpAuthUser" size="sm"
>{$_('section.settings.httpAuthUser')}</Label
>
<Col md="6">
<Input
type="text"
bind:value={$settings.httpAuthUser}
name="httpAuthUser"
id="httpAuthUser" id="httpAuthUser"
bsSize="sm" label={$_('section.settings.httpAuthUser')}
required bind:value={$settings.httpAuthUser}
></Input> required={true}
</Col> size={$uiSettings.inputSize}
</Row> />
<Row> <SettingsInput
<Label md={6} for="httpAuthPass" size="sm"
>{$_('section.settings.httpAuthPass')}</Label
>
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type={showPassword ? 'text' : 'password'}
bind:value={$settings.httpAuthPass}
name="httpAuthPass"
id="httpAuthPass" id="httpAuthPass"
bsSize="sm" label={$_('section.settings.httpAuthPass')}
required bind:value={$settings.httpAuthPass}
></Input> type={showPassword ? 'text' : 'password'}
required={true}
size={$uiSettings.inputSize}
>
<Button <Button
type="button" type="button"
on:click={() => (showPassword = !showPassword)} on:click={() => (showPassword = !showPassword)}
color={showPassword ? 'success' : 'danger'} color={showPassword ? 'success' : 'danger'}
>{#if !showPassword}<EyeIcon></EyeIcon>{:else}<EyeSlashIcon
></EyeSlashIcon>{/if}</Button
> >
</InputGroup> {#if !showPassword}<EyeIcon />{:else}<EyeSlashIcon />{/if}
<FormText>{$_('section.settings.httpAuthText')}</FormText> </Button>
</Col> </SettingsInput>
</Row>
{/if} {/if}
<Row>
<Label md={6} for="hostnamePrefix" size={$uiSettings.inputSize} <SettingsInput
>{$_('section.settings.hostnamePrefix')}</Label
>
<Col md="6">
<Input
type="text"
bind:value={$settings.hostnamePrefix}
name="hostnamePrefix"
id="hostnamePrefix" id="hostnamePrefix"
bsSize={$uiSettings.inputSize} label={$_('section.settings.hostnamePrefix')}
required bind:value={$settings.hostnamePrefix}
required={true}
minlength="1" minlength="1"
></Input> size={$uiSettings.inputSize}
</Col> />
</Row>
<Row> <SettingsSelect
<Label md={6} for="wifiTxPower" size={$uiSettings.inputSize}
>{$_('section.settings.wifiTxPower', { default: 'WiFi Tx Power' })}</Label
>
<Col md="6">
<Input
type="select"
bind:value={$settings.txPower}
name="select"
id="wifiTxPower" id="wifiTxPower"
bsSize={$uiSettings.inputSize} label={$_('section.settings.wifiTxPower', { default: 'WiFi Tx Power' })}
class={$uiSettings.selectClass} bind:value={$settings.txPower}
> options={Array.from(wifiTxPowerMap.entries())}
{#each wifiTxPowerMap as [key, value]} size={$uiSettings.inputSize}
<option {value}>{key}</option> selectClass={$uiSettings.selectClass}
{/each} helpText={$_('section.settings.wifiTxPowerText')}
</Input> />
<FormText>{$_('section.settings.wifiTxPowerText')}</FormText>
</Col> <SettingsInput
</Row>
<Row>
<Label md={6} for="wpTimeout" size={$uiSettings.inputSize}
>{$_('section.settings.wpTimeout')}</Label
>
<Col md="6">
<InputGroup size={$uiSettings.inputSize}>
<Input
type="number"
id="wpTimeout" id="wpTimeout"
label={$_('section.settings.wpTimeout')}
bind:value={$settings.wpTimeout}
type="number"
min={1} min={1}
step="1" step="1"
bind:value={$settings.wpTimeout} required={true}
required suffix={$_('time.seconds')}
size={$uiSettings.inputSize}
/> />
<InputGroupText>{$_('time.seconds')}</InputGroupText>
</InputGroup>
</Col>
</Row>
<Row> <Row>
<Col md="6" xl="12" xxl="6"> <SettingsSwitch
<Input
id="otaEnabled" id="otaEnabled"
bind:checked={$settings.otaEnabled} bind:checked={$settings.otaEnabled}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.otaUpdates')} ({$_('restartRequired')})" label="{$_('section.settings.otaUpdates')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/> />
</Col> <SettingsSwitch
<Col md="6" xl="12" xxl="6">
<Input
id="mdnsEnabled" id="mdnsEnabled"
bind:checked={$settings.mdnsEnabled} bind:checked={$settings.mdnsEnabled}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.enableMdns')} ({$_('restartRequired')})" label="{$_('section.settings.enableMdns')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/> />
</Col> <SettingsSwitch
<Col md="6" xl="12" xxl="6">
<Input
id="httpAuthEnabled" id="httpAuthEnabled"
bind:checked={$settings.httpAuthEnabled} bind:checked={$settings.httpAuthEnabled}
type="switch"
bsSize={$uiSettings.inputSize}
label="{$_('section.settings.httpAuthEnabled')} ({$_('restartRequired')})" label="{$_('section.settings.httpAuthEnabled')} ({$_('restartRequired')})"
size={$uiSettings.inputSize}
/> />
</Col>
</Row> </Row>
</ToggleHeader> </ToggleHeader>
</Row> </Row>

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>
@ -151,7 +151,11 @@
<hr /> <hr />
{#if $status.data} {#if $status.data}
<section class={lightMode ? 'lightMode' : 'darkMode'}> <section class={lightMode ? 'lightMode' : 'darkMode'}>
<Rendered status={$status} className="btclock-wrapper"></Rendered> <Rendered
status={$status}
className="btclock-wrapper"
verticalDesc={$settings.verticalDesc}
></Rendered>
</section> </section>
{$_('section.status.screenCycle')}: {$_('section.status.screenCycle')}:
<a <a

BIN
static/bitaxe.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

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('/');
@ -181,6 +74,8 @@ test('time values can not be zero or negative', async ({ page }) => {
}); });
test('info message when fetch eur price is enabled', async ({ page }) => { 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(); await page.getByRole('button', { name: 'Show all' }).click();

View file

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

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',
miningPoolStats: false,
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

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

674
yarn.lock

File diff suppressed because it is too large Load diff