feat: Most settings implemented
Some checks failed
/ build (push) Failing after 1m26s
/ check-changes (push) Successful in 8s

This commit is contained in:
Djuri 2025-05-04 02:12:17 +02:00
parent f8c2f4f228
commit 98ad7d1432
Signed by: djuri
GPG key ID: 61B9B2DDE5AA3AC1
41 changed files with 1976 additions and 421 deletions

View file

@ -0,0 +1,259 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages';
import { CardContainer, Alert } from '$lib/components';
import { getFirmwareBinaryName, getWebUiBinaryName } from '$lib/utils';
import { status, settings, firmwareRelease } from '$lib/stores';
import { onMount } from 'svelte';
import { uploadFirmwareFile, uploadWebUiFile, autoUpdate } from '$lib/clockControl';
// Format date to a more readable format
function formatDate(dateString: string): string {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString();
}
// Upload status
let firmwareUploadProgress = 0;
let webuiUploadProgress = 0;
let isUploading = false;
let uploadError: string | null = null;
let uploadSuccess = false;
let countdownValue = 0;
let countdownInterval: number | null = null;
// File selection status
let firmwareFileSelected = false;
let webuiFileSelected = false;
// Handle file selection change for firmware file
function handleFirmwareFileChange(event: Event) {
const fileInput = event.target as HTMLInputElement;
firmwareFileSelected = !!(fileInput.files && fileInput.files.length > 0);
}
// Handle file selection change for WebUI file
function handleWebUIFileChange(event: Event) {
const fileInput = event.target as HTMLInputElement;
webuiFileSelected = !!(fileInput.files && fileInput.files.length > 0);
}
// Start countdown to reload page after successful upload
function startCountdownToReload(seconds: number) {
countdownValue = seconds;
if (countdownInterval) clearInterval(countdownInterval);
countdownInterval = setInterval(() => {
countdownValue--;
if (countdownValue <= 0) {
if (countdownInterval) clearInterval(countdownInterval);
window.location.reload();
}
}, 1000) as unknown as number;
}
// Handle firmware file upload
function handleFirmwareUpload() {
const fileInput = document.getElementById('firmwareFile') as HTMLInputElement;
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
uploadError = 'Please select a firmware file';
return;
}
isUploading = true;
uploadError = null;
uploadSuccess = false;
firmwareUploadProgress = 0;
uploadFirmwareFile(fileInput.files[0], {
onProgress: (progress) => {
firmwareUploadProgress = progress;
},
onSuccess: () => {
isUploading = false;
uploadSuccess = true;
startCountdownToReload(10);
},
onError: (error) => {
isUploading = false;
uploadError =
typeof error === 'string' && error !== 'FAIL' ? error : 'Failed to upload firmware';
}
});
}
// Handle webUI file upload
function handleWebUIUpload() {
const fileInput = document.getElementById('webuiFile') as HTMLInputElement;
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
uploadError = 'Please select a WebUI file';
return;
}
isUploading = true;
uploadError = null;
uploadSuccess = false;
webuiUploadProgress = 0;
uploadWebUiFile(fileInput.files[0], {
onProgress: (progress) => {
webuiUploadProgress = progress;
},
onSuccess: () => {
isUploading = false;
uploadSuccess = true;
startCountdownToReload(10);
},
onError: (error) => {
isUploading = false;
uploadError = typeof error === 'string' ? error : 'Failed to upload WebUI';
}
});
}
const onAutoUpdate = (e: Event) => {
e.preventDefault();
autoUpdate({
onSuccess: () => {
// toast.push({
// type: 'success',
// message
// });
startCountdownToReload(10);
},
onError: (error) => {
// toast.push({
// type: 'error',
// message: error as string
// });
uploadError = typeof error === 'string' ? error : 'Failed to auto-update';
}
});
};
// Fetch firmware data when component is mounted
onMount(() => {
firmwareRelease.fetchLatest();
// Cleanup on component destroy
return () => {
if (countdownInterval) clearInterval(countdownInterval);
};
});
</script>
<CardContainer title={m['section.control.firmwareUpdate']()} className="">
<div>
{#if $firmwareRelease.isLoading}
<div class="my-4 flex justify-center">
<span class="loading loading-spinner"></span>
</div>
{:else if $firmwareRelease.error}
<Alert type="error" className="mb-4" message={$firmwareRelease.error} />
{:else}
<p class="mb-2 text-sm">
{m['section.firmwareUpdater.latestVersion']()}
{$firmwareRelease.tag_name} - {m['section.firmwareUpdater.releaseDate']()}
{formatDate($firmwareRelease.published_at)} -
<a
href={$firmwareRelease.html_url}
target="_blank"
rel="noopener noreferrer"
class="link link-primary"
>
{m['section.firmwareUpdater.viewRelease']()}
</a>
</p>
{#if !$status.isOTAUpdating && $settings.gitTag !== $firmwareRelease.tag_name}
<Alert type="success" className="mb-4">
{m['section.firmwareUpdater.swUpdateAvailable']()}
{$settings.gitTag}{$firmwareRelease.tag_name} -
<a href="/" class="link link-primary" on:click={onAutoUpdate}
>{m['section.firmwareUpdater.autoUpdate']()}</a
>.
</Alert>
{/if}
{/if}
{#if !$status.isOTAUpdating}
<div class="form-control mb-4">
<label class="label" for="firmwareFile">
<span class="label-text">Firmware File ({getFirmwareBinaryName($settings.hwRev)})</span>
</label>
<div class="flex gap-2">
<input
type="file"
class="file-input file-input-bordered w-full"
id="firmwareFile"
accept=".bin"
disabled={isUploading}
on:change={handleFirmwareFileChange}
/>
<button
class="btn btn-primary w-1/4"
on:click={handleFirmwareUpload}
disabled={isUploading || !firmwareFileSelected}
>
{#if isUploading && firmwareUploadProgress > 0}
{firmwareUploadProgress}%
{:else}
{m['section.control.firmwareUpdate']()}
{/if}
</button>
</div>
{#if isUploading && firmwareUploadProgress > 0}
<progress class="progress progress-primary mt-2" value={firmwareUploadProgress} max="100"
></progress>
{/if}
</div>
<div class="form-control">
<label class="label" for="webuiFile">
<span class="label-text">WebUI File ({getWebUiBinaryName($settings.hwRev)})</span>
</label>
<div class="flex gap-2">
<input
type="file"
class="file-input file-input-bordered w-full"
id="webuiFile"
accept=".bin"
disabled={isUploading}
on:change={handleWebUIFileChange}
/>
<button
class="btn btn-primary w-1/4"
on:click={handleWebUIUpload}
disabled={isUploading || !webuiFileSelected}
>
{#if isUploading && webuiUploadProgress > 0}
{webuiUploadProgress}%
{:else}
Update WebUI
{/if}
</button>
</div>
{#if isUploading && webuiUploadProgress > 0}
<progress class="progress progress-primary mt-2" value={webuiUploadProgress} max="100"
></progress>
{/if}
</div>
{:else}
<Alert type="info" className="my-4">
<span class="loading loading-spinner text-neutral"></span>
{m['section.firmwareUpdater.autoUpdateInProgress']()}
</Alert>
{/if}
{#if uploadSuccess}
<Alert type="success" className="my-4">
{m['section.firmwareUpdater.fileUploadSuccess']({ countdown: countdownValue })}
</Alert>
{:else if uploadError}
<Alert type="error" className="my-4">
{uploadError}
</Alert>
{/if}
<Alert type="warning" className="mt-4">
{m['section.firmwareUpdater.firmwareUpdateText']()}
</Alert>
</div>
</CardContainer>

View file

@ -1,233 +1,255 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages';
import { CardContainer, Toggle, CollapsibleSection } from '$lib/components';
import { settings } from '$lib/stores';
import {
CardContainer,
Toggle,
CollapsibleSection,
TimezoneSelector,
ToggleWrapper,
Radio,
NumericInput,
Select,
RadioGroup,
SettingsInputField
} from '$lib/components';
import { settings, status } from '$lib/stores';
import { DataSourceType } from '$lib/types';
import RangeSlider from '../form/RangeSlider.svelte';
import { miningPoolMap } from '$lib/utils';
let { ...restProps } = $props();
// Show/hide toggles
let showAll = $state(false);
let hideAll = $state(false);
const textColorOptions: [string, boolean][] = [
[m['colors.black']() + ' on ' + m['colors.white'](), false],
[m['colors.white']() + ' on ' + m['colors.black'](), true]
];
function toggleShowAll() {
showAll = true;
hideAll = false;
}
const fontPreferenceOptions: [string, string][] = $settings.availableFonts?.map((font) => {
// Check if the translation key exists in messages
// If it exists, use the translation, otherwise capitalize the font name
const translationKey = `fonts.${font}` as keyof typeof m;
const hasTranslation = translationKey in m && typeof m[translationKey] === 'function';
function toggleHideAll() {
hideAll = true;
showAll = false;
}
return [
hasTranslation
? (m[translationKey] as Function)()
: font.charAt(0).toUpperCase() + font.slice(1),
font
];
});
const handleColorChange = (e: Event) => {
const select = e.target as HTMLSelectElement;
$settings.invertedColor = select.value === 'true';
};
// // Show/hide toggles
// let showAll = $state(false);
// let hideAll = $state(false);
// function toggleShowAll() {
// showAll = true;
// hideAll = false;
// }
// function toggleHideAll() {
// hideAll = true;
// showAll = false;
// }
const handleTimePerScreenChange = (e: Event) => {
const input = e.target as HTMLInputElement;
$settings.timePerScreen = Number(input.value);
};
const handleReset = (e: Event) => {
e.preventDefault();
settings.reset();
console.log('formReset');
};
const handleSave = (e: Event) => {
e.preventDefault();
console.log('formSave');
settings.update($settings);
};
const dataSourceOptions = [
{ label: m['section.settings.dataSource.btclock'](), value: DataSourceType.BTCLOCK_SOURCE },
{
label: m['section.settings.dataSource.thirdParty'](),
value: DataSourceType.THIRD_PARTY_SOURCE
},
{ label: m['section.settings.dataSource.nostr'](), value: DataSourceType.NOSTR_SOURCE },
{ label: m['section.settings.dataSource.custom'](), value: DataSourceType.CUSTOM_SOURCE }
];
const miningPoolOptions = Array.from(miningPoolMap.entries()).map(([key, value]) => ({
label: value,
value: key
}));
</script>
<CardContainer title={m['section.settings.title']()} {...restProps}>
<div class="mb-4 flex justify-end gap-2">
<button class="btn btn-sm" onclick={toggleShowAll}>{m['section.settings.showAll']()}</button>
<button class="btn btn-sm" onclick={toggleHideAll}>{m['section.settings.hideAll']()}</button>
</div>
<div class="grid grid-cols-2 gap-4">
<CollapsibleSection
title={m['section.settings.section.screenSettings']()}
open={showAll || !hideAll}
>
<div class="grid grid-cols-2 gap-4">
<div class="form-control">
<Toggle
label={m['section.settings.StealFocusOnNewBlock']()}
bind:checked={$settings.stealFocus}
/>
<p class="text-xs">
When a new block is mined, it will switch focus from the current screen.
</p>
</div>
<div class="form-control">
<Toggle
label={m['section.settings.useBigCharsMcap']()}
bind:checked={$settings.mcapBigChar}
/>
<p class="text-xs">
Use big characters for the market cap screen instead of using a suffix.
</p>
</div>
<div class="form-control">
<Toggle
label={m['section.settings.useBlkCountdown']()}
bind:checked={$settings.useBlkCountdown}
/>
<p class="text-xs">
When enabled it count down blocks instead of years/monts/days/hours/minutes.
</p>
</div>
<div class="form-control">
<Toggle
label={m['section.settings.useSatsSymbol']()}
bind:checked={$settings.useSatsSymbol}
/>
<p class="text-xs">Prefix satoshi amounts with the sats symbol.</p>
</div>
<div class="form-control">
<Toggle
label={m['section.settings.suffixPrice']()}
bind:checked={$settings.suffixPrice}
/>
<p class="text-xs">Always use a suffix for the ticker screen.</p>
</div>
<div class="form-control">
<Toggle
label={m['section.settings.verticalDesc']()}
bind:checked={$settings.verticalDesc}
/>
<p class="text-xs">Rotate the description of the screen 90 degrees.</p>
</div>
</div>
</CollapsibleSection>
<CollapsibleSection title={m['section.settings.screens']()} open={showAll || !hideAll}>
<div class="grid grid-cols-2 gap-4">
<CardContainer {...restProps}>
<div class="grid gap-4 md:grid-cols-2">
<CollapsibleSection title={m['section.settings.screens']()}>
<div class="grid gap-4 md:grid-cols-2">
{#each $settings.screens as screen (screen.id)}
<div class="form-control">
<Toggle label={screen.name} checked={screen.enabled} />
</div>
<ToggleWrapper label={screen.name} bind:value={screen.enabled} />
{/each}
</div>
</CollapsibleSection>
<CollapsibleSection title={m['section.settings.currencies']()} open={showAll || !hideAll}>
<div class="alert alert-warning">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/></svg
>
<span>restart required</span>
</div>
<div class="grid grid-cols-2 gap-4">
{#each $settings.actCurrencies as currency (currency)}
<div class="form-control">
<Toggle label={currency} checked={$settings.actCurrencies.includes(currency)} />
</div>
<CollapsibleSection title={m['section.settings.currencies']()}>
<div class="grid gap-4 md:grid-cols-2">
{#each $settings.availableCurrencies as currency (currency)}
<ToggleWrapper
id={`currency-${currency}`}
label={currency}
bind:group={$settings.actCurrencies}
value={currency}
/>
{/each}
</div>
</CollapsibleSection>
<CollapsibleSection
title={m['section.settings.section.displaysAndLed']()}
open={showAll || !hideAll}
>
<CollapsibleSection title={m['section.settings.section.screenSettings']()}>
<div class="grid gap-4 md:grid-cols-2">
<ToggleWrapper
label={m['section.settings.StealFocusOnNewBlock']()}
bind:value={$settings.stealFocus}
description="When a new block is mined, it will switch focus from the current screen."
/>
<ToggleWrapper
label={m['section.settings.useBigCharsMcap']()}
bind:value={$settings.mcapBigChar}
description="Use big characters for the market cap screen instead of using a suffix."
/>
<ToggleWrapper
label={m['section.settings.useBlkCountdown']()}
bind:value={$settings.useBlkCountdown}
description="When enabled it count down blocks instead of years/monts/days/hours/minutes."
/>
<ToggleWrapper
label={m['section.settings.useSatsSymbol']()}
bind:value={$settings.useSatsSymbol}
description="Prefix satoshi amounts with the sats symbol."
/>
<ToggleWrapper
label={m['section.settings.suffixPrice']()}
bind:value={$settings.suffixPrice}
description="Always use a suffix for the ticker screen."
/>
<ToggleWrapper
label={m['section.settings.verticalDesc']()}
bind:value={$settings.verticalDesc}
description="Rotate the description of the screen 90 degrees."
/>
</div>
</CollapsibleSection>
<CollapsibleSection title={m['section.settings.section.displaysAndLed']()}>
<div class="grid gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.textColor']()}</span>
</label>
<select class="select select-bordered w-full">
<option>White on Black</option>
<option>Black on White</option>
</select>
</div>
<Select
label={m['section.settings.textColor']()}
bind:value={$settings.invertedColor}
options={textColorOptions.map(([label, value]) => ({ label, value: value }))}
/>
<Select
label={m['section.settings.fontName']()}
bind:value={$settings.fontName}
options={fontPreferenceOptions.map(([label, value]) => ({ label, value }))}
/>
<NumericInput
label={m['section.settings.timePerScreen']()}
value={$settings.timePerScreen ? $settings.timePerScreen : 1}
min={1}
required
step={1}
onchange={handleTimePerScreenChange}
unit={m['time.minutes']()}
/>
<NumericInput
label={m['section.settings.fullRefreshEvery']()}
bind:value={$settings.fullRefreshMin}
min={1}
required
step={1}
unit={m['time.minutes']()}
/>
<NumericInput
label={m['section.settings.timeBetweenPriceUpdates']()}
bind:value={$settings.minSecPriceUpd}
min={1}
required
step={1}
unit={m['time.seconds']()}
/>
<RangeSlider
label={m['section.settings.ledBrightness']()}
bind:value={$settings.ledBrightness}
min={0}
max={255}
step={1}
showValue={true}
/>
<ToggleWrapper
label={m['section.settings.ledPowerOnTest']()}
bind:value={$settings.ledTestOnPower}
/>
<div class="form-control">
<label class="label">
<span class="label-text">Font</span>
</label>
<select class="select select-bordered w-full">
<option>Oswald</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.timePerScreen']()}</span>
</label>
<div class="flex items-center gap-2">
<input type="number" class="input input-bordered w-20" min="1" max="60" value="1" />
<span>{m['time.minutes']()}</span>
</div>
</div>
<div class="form-control flex justify-between">
<label class="label">
<span class="label-text">{m['section.settings.fullRefreshEvery']()}</span>
</label>
<div class="input w-auto">
<input type="number" class="" min="1" max="60" value="60" />
<span class="label">{m['time.minutes']()}</span>
</div>
</div>
<div class="form-control flex justify-between">
<label class="label">
<span class="label-text">{m['section.settings.timeBetweenPriceUpdates']()}</span>
</label>
<div class="input w-auto">
<input type="number" class="" min="1" max="60" value="30" />
<span class="label">{m['time.seconds']()}</span>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.ledBrightness']()}</span>
</label>
<input type="range" min="0" max="100" class="range" value="50" />
</div>
<div class="form-control">
<Toggle
label={m['section.settings.ledPowerOnTest']()}
checked={$settings.ledTestOnPower}
/>
</div>
<div class="form-control">
<Toggle
<ToggleWrapper
label={m['section.settings.ledFlashOnBlock']()}
checked={$settings.ledFlashOnUpd}
bind:value={$settings.ledFlashOnUpd}
/>
</div>
<div class="form-control">
<Toggle label={m['section.settings.disableLeds']()} checked={$settings.disableLeds} />
</div>
<ToggleWrapper
label={m['section.settings.disableLeds']()}
bind:value={$settings.disableLeds}
/>
</div>
</CollapsibleSection>
{#if $settings.hasFrontlight}
<CollapsibleSection title="Frontlight Settings" open={showAll || !hideAll}>
<CollapsibleSection title="Frontlight Settings">
<div class="grid gap-4">
<div class="form-control">
<Toggle label="Disable Frontlight" checked={$settings.flDisable} />
</div>
<ToggleWrapper
label={m['section.settings.flDisable']()}
bind:value={$settings.flDisable}
/>
<div class="form-control">
<Toggle label="Always On" checked={$settings.flAlwaysOn} />
</div>
<ToggleWrapper
label={m['section.settings.flAlwaysOn']()}
bind:value={$settings.flAlwaysOn}
/>
<div class="form-control">
<Toggle label="Flash on Updates" checked={$settings.flFlashOnUpd} />
</div>
<ToggleWrapper
label={m['section.settings.flFlashOnUpd']()}
bind:value={$settings.flFlashOnUpd}
/>
<div class="form-control">
<Toggle label="Flash on Zaps" checked={$settings.flFlashOnZap} />
</div>
<ToggleWrapper
label={m['section.settings.flFlashOnZap']()}
bind:value={$settings.flFlashOnZap}
/>
{#if $settings.hasLightLevel}
<div class="form-control">
<Toggle label="Turn Off in Dark" checked={$settings.flOffWhenDark} />
</div>
<ToggleWrapper
label={m['section.settings.flOffWhenDark']()}
bind:value={$settings.flOffWhenDark}
/>
<div class="form-control">
<label class="label">
@ -238,124 +260,228 @@
min="0"
max="255"
class="range"
value={$settings.luxLightToggle}
bind:value={$settings.luxLightToggle}
/>
</div>
{/if}
<div class="form-control">
<label class="label">
<span class="label-text">Maximum Brightness</span>
</label>
<input
type="range"
min="0"
max="4095"
class="range"
value={$settings.flMaxBrightness}
/>
</div>
<RangeSlider
label={m['section.settings.flMaxBrightness']()}
bind:value={$settings.flMaxBrightness}
min={0}
max={4095}
step={1}
showValue={true}
/>
<div class="form-control">
<label class="label">
<span class="label-text">Effect Delay (ms)</span>
</label>
<input
type="number"
class="input input-bordered w-20"
min="10"
max="1000"
value={$settings.flEffectDelay}
/>
</div>
<NumericInput
label={m['section.settings.flEffectDelay']()}
bind:value={$settings.flEffectDelay}
min={10}
max={1000}
step={1}
unit="ms"
/>
</div>
</CollapsibleSection>
{/if}
<CollapsibleSection
title={m['section.settings.section.dataSource']()}
open={showAll || !hideAll}
>
<CollapsibleSection title={m['section.settings.section.dataSource']()}>
<div class="grid gap-4">
<div class="form-control">
<div class="form-control flex flex-col items-start justify-between md:flex-row">
<label class="label">
<span class="label-text">{m['section.settings.dataSource.label']()}</span>
</label>
<select class="select select-bordered w-full">
<option value="btclock">{m['section.settings.dataSource.btclock']()}</option>
<option value="thirdparty">{m['section.settings.dataSource.thirdParty']()}</option>
<option value="nostr">{m['section.settings.dataSource.nostr']()}</option>
<option value="custom">{m['section.settings.dataSource.custom']()}</option>
</select>
<div class="flex grid grid-cols-1 flex-row gap-2 md:grid-cols-2">
{#each dataSourceOptions as option}
<Radio
label={option.label}
value={option.value}
name="dataSource"
bind:group={$settings.dataSource}
/>
{/each}
</div>
</div>
<div class="form-control">
<div class="form-control flex justify-between">
<label class="label">
<span class="label-text">{m['section.settings.mempoolnstance']()}</span>
</label>
<input type="text" class="input input-bordered w-full" value="mempool.space/coinlcp.io" />
<input
type="text"
class="input input-bordered input flex w-auto"
bind:value={$settings.mempoolInstance}
/>
</div>
<div class="form-control">
<div class="form-control flex justify-between">
<label class="label">
<span class="label-text">{m['section.settings.ceEndpoint']()}</span>
</label>
<input
type="text"
class="input input-bordered w-full"
class="input input-bordered input flex w-auto"
placeholder="Custom Endpoint URL"
bind:value={$settings.ceEndpoint}
/>
</div>
</div>
</CollapsibleSection>
<CollapsibleSection
title={m['section.settings.section.extraFeatures']()}
open={showAll || !hideAll}
>
<CollapsibleSection title={m['section.settings.section.extraFeatures']()}>
<div class="grid gap-4">
<div class="form-control">
<Toggle label={m['section.settings.timeBasedDnd']()} checked={$settings.dnd.enabled} />
</div>
<ToggleWrapper
label={m['section.settings.timeBasedDnd']()}
bind:value={$settings.dnd.timeBasedEnabled}
/>
<h4 class="text-lg font-bold">Bitaxe</h4>
<ToggleWrapper
label={m['section.settings.bitaxeEnabled']()}
bind:value={$settings.bitaxeEnabled}
/>
{#if $settings.bitaxeEnabled}
<SettingsInputField
label={m['section.settings.bitaxeHostname']()}
bind:value={$settings.bitaxeHostname}
/>
{/if}
<h4 class="text-lg font-bold">Mining Pool</h4>
<ToggleWrapper
label={m['section.settings.miningPoolStats']()}
bind:value={$settings.miningPoolStats}
/>
{#if $settings.miningPoolStats}
<div class="form-control flex flex-col items-start justify-between md:flex-row">
<label class="label" for="miningPoolName">
<span class="label-text">{m['section.settings.miningPoolName']()}</span>
</label>
<div class="flex grid grid-cols-1 flex-row gap-2 md:grid-cols-2">
{#each miningPoolOptions as option}
<Radio
label={option.label}
value={option.value}
name="miningPoolName"
bind:group={$settings.miningPoolName}
/>
{/each}
</div>
</div>
{#if $settings.miningPoolName === 'local_public_pool'}
<SettingsInputField
label={m['section.settings.localPoolEndpoint']()}
bind:value={$settings.localPoolEndpoint}
/>
{/if}
<SettingsInputField
label={m['section.settings.miningPoolUser']()}
bind:value={$settings.miningPoolUser}
/>
{/if}
<h4 class="text-lg font-bold">Nostr</h4>
<ToggleWrapper
label={m['section.settings.nostrZapNotify']()}
bind:value={$settings.nostrZapNotify}
/>
{#if $settings.nostrZapNotify}
<SettingsInputField
label={m['section.settings.nostrRelay']()}
bind:value={$settings.nostrRelay}
/>
<SettingsInputField
label={m['section.settings.nostrZapPubkey']()}
bind:value={$settings.nostrZapPubkey}
/>
{/if}
</div>
</CollapsibleSection>
<CollapsibleSection title={m['section.settings.section.system']()} open={showAll || !hideAll}>
<CollapsibleSection title={m['section.settings.section.system']()}>
<div class="grid gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.timezoneOffset']()}</span>
</label>
<div class="flex items-center gap-2">
<select class="select select-bordered w-full">
<option>Europe/Amsterdam</option>
</select>
<button class="btn">{m['auto-detect']()}</button>
</div>
<p class="mt-1 text-sm">{m['section.settings.tzOffsetHelpText']()}</p>
</div>
<TimezoneSelector
selectedTimezone={$settings.tzString}
change={(value: string) => ($settings.tzString = value)}
helpText={''}
/>
<div class="form-control">
<div class="form-control flex justify-between">
<label class="label">
<span class="label-text">{m['section.settings.hostnamePrefix']()}</span>
</label>
<input type="text" class="input input-bordered w-full" value="btclock" />
<input
type="text"
class="input input-bordered input flex w-auto"
bind:value={$settings.hostnamePrefix}
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{m['section.settings.wpTimeout']()}</span>
</label>
<div class="flex items-center gap-2">
<input type="number" class="input input-bordered w-20" min="1" max="900" value="600" />
<span>{m['time.seconds']()}</span>
</div>
</div>
<NumericInput
label={m['section.settings.wpTimeout']()}
bind:value={$settings.wpTimeout}
min={1}
required
step={1}
unit={m['time.seconds']()}
/>
<ToggleWrapper
label={m['section.settings.otaUpdates']()}
bind:value={$settings.otaEnabled}
/>
<ToggleWrapper
label={m['section.settings.enableMdns']()}
bind:value={$settings.mdnsEnabled}
/>
<ToggleWrapper
label={m['section.settings.httpAuthEnabled']()}
bind:value={$settings.httpAuthEnabled}
/>
{#if $settings.httpAuthEnabled}
<SettingsInputField
label={m['section.settings.httpAuthUser']()}
bind:value={$settings.httpAuthUser}
/>
<SettingsInputField
label={m['section.settings.httpAuthPass']()}
bind:value={$settings.httpAuthPass}
/>
{/if}
<ToggleWrapper
label={m['section.settings.enableDebugLog']()}
bind:value={$settings.enableDebugLog}
/>
</div>
</CollapsibleSection>
</div>
<div class="mt-6 flex justify-between">
<button class="btn btn-error">{m['button.reset']()}</button>
<button class="btn btn-primary">{m['button.save']()}</button>
</div>
</CardContainer>
<div class="sticky right-0 bottom-0 left-0">
<div class="navbar bg-base-100 flex justify-around">
<button class="btn btn-error" type="button" onclick={handleReset}>{m['button.reset']()}</button>
{#if $status.isOTAUpdating}
<span class="text-center text-sm text-gray-500"
>OTA Update in progress... Please wait until the update is complete.</span
>
{/if}
<button
class="btn btn-primary"
type="submit"
onclick={handleSave}
disabled={$status.isOTAUpdating}>{m['button.save']()}</button
>
</div>
</div>

View file

@ -7,80 +7,100 @@
import { DataSourceType } from '$lib/types';
import { toUptimestring } from '$lib/utils';
const screens = $settings.screens.map((screen) => ({
id: screen.id,
label: screen.name
}));
let screens: { id: number; label: string }[] = [];
settings.subscribe((settings) => {
screens = settings.screens.map((screen) => ({
id: screen.id,
label: screen.name
}));
});
</script>
<CardContainer title={m['section.status.title']()}>
<div class="mx-auto space-y-4">
<div class="join">
{#each screens as screen (screen.id)}
<TabButton
active={$status.currentScreen === screen.id}
onClick={() => setActiveScreen(screen.id)}
>
{screen.label}
</TabButton>
{/each}
</div>
<div class="join flex justify-center">
{#each $settings.actCurrencies as currency (currency)}
<CurrencyButton
{currency}
active={$status.currency === currency}
onClick={() => setActiveCurrency(currency)}
/>
{/each}
</div>
<div class="mt-8 flex justify-center">
<div class="w-3/4">
<BTClock displays={$status.data} verticalDesc={$settings.verticalDesc} />
{#if !$settings.isLoaded}
<div class="flex justify-center">
<div class="mx-auto flex w-3/4 flex-col items-center justify-center gap-4">
<span class="loading loading-spinner loading-xl"></span>
<p class="text-center text-lg font-bold">Loading...</p>
</div>
</div>
{:else}
<div class="mx-auto space-y-4">
<div class="md:join">
{#each screens as screen (screen.id)}
<TabButton
active={$status.currentScreen === screen.id}
onClick={() => setActiveScreen(screen.id)}
>
{screen.label}
</TabButton>
{/each}
</div>
<div class="text-center text-sm text-gray-500">
{m['section.status.screenCycle']()}: is {$status.timerRunning ? 'running' : 'stopped'}<br />
{m['section.status.doNotDisturb']()}: {$status.dnd.enabled ? m['on']() : m['off']()}
<small>
{#if $status.dnd?.timeBasedEnabled}
{m['section.status.timeBasedDnd']()} ( {$settings.dnd
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings.dnd
.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
<div class="join flex justify-center">
{#each $settings.actCurrencies as currency (currency)}
<CurrencyButton
{currency}
active={$status.currency === currency}
onClick={() => setActiveCurrency(currency)}
/>
{/each}
</div>
<div class="mt-8 flex justify-center">
<div class="w-full md:w-3/4">
<BTClock
theme={$settings.invertColors ? 'light' : 'dark'}
displays={$status.data}
verticalDesc={$settings.verticalDesc}
/>
</div>
</div>
<div class="text-center text-sm text-gray-500">
{m['section.status.screenCycle']()}: is {$status.timerRunning ? 'running' : 'stopped'}<br />
{m['section.status.doNotDisturb']()}: {$status.dnd.active ? m['on']() : m['off']()}
<small>
{#if $status.dnd?.timeBasedEnabled}
{m['section.status.timeBasedDnd']()} ( {$settings.dnd
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings.dnd
.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
{/if}
</small>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{#if $settings.dataSource === DataSourceType.NOSTR_SOURCE || $settings.nostrZapNotify}
<Status
text="Nostr Relay connection"
status={$status.connectionStatus.nostr ? 'online' : 'offline'}
/>
{/if}
</small>
{#if $settings.dataSource === DataSourceType.THIRD_PARTY_SOURCE}
<Status
text={m['section.status.wsPriceConnection']()}
status={$status.connectionStatus.price ? 'online' : 'offline'}
/>
<Status
text={m['section.status.wsMempoolConnection']({ instance: $settings.mempoolInstance })}
status={$status.connectionStatus.blocks ? 'online' : 'offline'}
/>
{:else}
<Status
text={m['section.status.wsDataConnection']()}
status={$status.connectionStatus.V2 ? 'online' : 'offline'}
/>
{/if}
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{#if $settings.dataSource === DataSourceType.NOSTR_SOURCE || $settings.nostrZapNotify}
<Status text="Nostr Relay connection" status={$status.nostr ? 'online' : 'offline'} />
{/if}
{#if $settings.dataSource === DataSourceType.THIRD_PARTY_SOURCE}
<Status
text={m['section.status.wsPriceConnection']()}
status={$status.connectionStatus.price ? 'online' : 'offline'}
/>
<Status
text={m['section.status.wsMempoolConnection']({ instance: $settings.mempoolInstance })}
status={$status.connectionStatus.blocks ? 'online' : 'offline'}
/>
{:else}
<Status
text={m['section.status.wsDataConnection']()}
status={$status.connectionStatus.V2 ? 'online' : 'offline'}
/>
{/if}
<div class="stats mt-4 flex justify-center shadow">
<Stat
title={m['section.status.memoryFree']()}
value={`${Math.round($status.espFreeHeap / 1024)} / ${Math.round($status.espHeapSize / 1024)} KiB`}
/>
<Stat title={m['section.status.wifiSignalStrength']()} value={`${$status.rssi} dBm`} />
<Stat title={m['section.status.uptime']()} value={`${toUptimestring($status.espUptime)}`} />
</div>
</div>
<div class="stats mt-4 flex justify-center shadow">
<Stat
title={m['section.status.memoryFree']()}
value={`${Math.round($status.espFreeHeap / 1024)} / ${Math.round($status.espHeapSize / 1024)} KiB`}
/>
<Stat title={m['section.status.wifiSignalStrength']()} value={`${$status.rssi} dBm`} />
<Stat title={m['section.status.uptime']()} value={`${toUptimestring($status.espUptime)}`} />
</div>
{/if}
</CardContainer>

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages';
import { CardContainer } from '$lib/components';
import { CardContainer, Alert } from '$lib/components';
import { settings } from '$lib/stores';
import { restartClock, forceFullRefresh } from '$lib/clockControl';
import { restartClock } from '$lib/clockControl';
</script>
<CardContainer title="System Information">
@ -22,7 +22,7 @@
</tr>
<tr>
<td>{m['section.control.buildTime']()}</td>
<td>{$settings.lastBuildTime}</td>
<td>{new Date($settings.lastBuildTime * 1000).toLocaleString()}</td>
</tr>
<tr>
<td>IP</td>
@ -36,6 +36,10 @@
<td>{m['section.control.fwCommit']()}</td>
<td class="text-xs">{$settings.gitRev}</td>
</tr>
<tr>
<td>WebUI commit</td>
<td class="text-xs">{$settings.fsRev}</td>
</tr>
<tr>
<td>{m['section.control.hostname']()}</td>
<td>{$settings.hostname}</td>
@ -43,57 +47,11 @@
</tbody>
</table>
</div>
{#if $settings.fsRev !== $settings.gitRev}
<Alert type="warning" message={m['section.system.firmwareOutdated']()} />
{/if}
<div class="mt-4 flex gap-2">
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
</div>
</div>
</CardContainer>
<CardContainer title={m['section.control.firmwareUpdate']()} className="mt-4">
<div>
<p class="mb-2 text-sm">
Latest Version: 3.3.5 - Release Date: 5/2/2025, 12:37:14 AM - <a
href="#"
class="link link-primary">{m['section.firmwareUpdater.viewRelease']()}</a
>
</p>
<p class="text-success mb-4 text-sm">{m['section.firmwareUpdater.swUpToDate']()}</p>
<div class="form-control mb-4">
<label class="label" for="firmwareFile">
<span class="label-text">Firmware File (blib_s3_mini_213epd_firmware.bin)</span>
</label>
<div class="flex gap-2">
<input type="file" class="file-input file-input-bordered w-full" id="firmwareFile" />
<button class="btn btn-primary">{m['section.control.firmwareUpdate']()}</button>
</div>
</div>
<div class="form-control">
<label class="label" for="webuiFile">
<span class="label-text">WebUI File (littlefs_4MB.bin)</span>
</label>
<div class="flex gap-2">
<input type="file" class="file-input file-input-bordered w-full" id="webuiFile" />
<button class="btn btn-primary">Update WebUI</button>
</div>
</div>
<div class="alert alert-warning mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/></svg
>
<span>{m['section.firmwareUpdater.firmwareUpdateText']()}</span>
</div>
</div>
</CardContainer>