feat: Lint fixes, add forgejo workflow and e2e tests
This commit is contained in:
parent
af2f593fb8
commit
5917713b0d
39 changed files with 1666 additions and 1506 deletions
20
src/app.css
20
src/app.css
|
@ -1,26 +1,26 @@
|
|||
@import 'tailwindcss';
|
||||
@plugin "daisyui" {
|
||||
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--secondary: #6b7280;
|
||||
--accent: #f59e0b;
|
||||
--primary: #3b82f6;
|
||||
--secondary: #6b7280;
|
||||
--accent: #f59e0b;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html, body {
|
||||
@apply h-full;
|
||||
html,
|
||||
body {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply bg-base-200;
|
||||
@apply bg-base-200;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-base-200 pt-16;
|
||||
}
|
||||
@apply bg-base-200 pt-16;
|
||||
}
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
<html lang="%paraglide.lang%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents" class="h-full">%sveltekit.body%</div>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,112 +1,111 @@
|
|||
import { PUBLIC_BASE_URL } from '$env/static/public';
|
||||
import { baseUrl } from './env';
|
||||
|
||||
/**
|
||||
* Sets custom text to display on the clock
|
||||
*/
|
||||
export const setCustomText = (newText: string) => {
|
||||
return fetch(`${baseUrl}/api/show/text/${newText}`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/show/text/${newText}`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the LED colors
|
||||
*/
|
||||
export const setLEDcolor = (ledStatus: { hex: string }[]) => {
|
||||
return fetch(`${baseUrl}/api/lights/set`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(ledStatus)
|
||||
}).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/lights/set`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(ledStatus)
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns off all LEDs
|
||||
*/
|
||||
export const turnOffLeds = () => {
|
||||
return fetch(`${baseUrl}/api/lights/off`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/lights/off`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Restarts the clock
|
||||
*/
|
||||
export const restartClock = () => {
|
||||
return fetch(`${baseUrl}/api/restart`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/restart`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces a full refresh of the clock
|
||||
*/
|
||||
export const forceFullRefresh = () => {
|
||||
return fetch(`${baseUrl}/api/full_refresh`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/full_refresh`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random color hex code
|
||||
*/
|
||||
export const generateRandomColor = () => {
|
||||
return `#${Math.floor(Math.random() * 16777215)
|
||||
.toString(16)
|
||||
.padStart(6, '0')}`;
|
||||
};
|
||||
return `#${Math.floor(Math.random() * 16777215)
|
||||
.toString(16)
|
||||
.padStart(6, '0')}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the active screen
|
||||
*/
|
||||
export const setActiveScreen = async (screenId: string) => {
|
||||
return fetch(`${baseUrl}/api/show/screen/${screenId}`);
|
||||
}
|
||||
return fetch(`${baseUrl}/api/show/screen/${screenId}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the active currency
|
||||
*/
|
||||
export const setActiveCurrency = async (currency: string) => {
|
||||
return fetch(`${baseUrl}/api/show/currency/${currency}`);
|
||||
}
|
||||
return fetch(`${baseUrl}/api/show/currency/${currency}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns on the frontlight
|
||||
*/
|
||||
export const turnOnFrontlight = () => {
|
||||
return fetch(`${baseUrl}/api/frontlight/on`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/frontlight/on`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Flashes the frontlight
|
||||
*/
|
||||
export const flashFrontlight = () => {
|
||||
return fetch(`${baseUrl}/api/frontlight/flash`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/frontlight/flash`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns off the frontlight
|
||||
*/
|
||||
export const turnOffFrontlight = () => {
|
||||
return fetch(`${baseUrl}/api/frontlight/off`).catch(() => {});
|
||||
return fetch(`${baseUrl}/api/frontlight/off`).catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the timer
|
||||
*/
|
||||
*/
|
||||
export const toggleTimer = (currentStatus: boolean) => (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (currentStatus) {
|
||||
fetch(`${baseUrl}/api/action/pause`);
|
||||
} else {
|
||||
fetch(`${baseUrl}/api/action/timer_restart`);
|
||||
}
|
||||
e.preventDefault();
|
||||
if (currentStatus) {
|
||||
fetch(`${baseUrl}/api/action/pause`);
|
||||
} else {
|
||||
fetch(`${baseUrl}/api/action/timer_restart`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the do not disturb mode
|
||||
*/
|
||||
export const toggleDoNotDisturb = (currentStatus: boolean) => (e: Event) => {
|
||||
e.preventDefault();
|
||||
console.log(currentStatus);
|
||||
if (!currentStatus) {
|
||||
fetch(`${baseUrl}/api/dnd/enable`);
|
||||
} else {
|
||||
fetch(`${baseUrl}/api/dnd/disable`);
|
||||
}
|
||||
};
|
||||
e.preventDefault();
|
||||
console.log(currentStatus);
|
||||
if (!currentStatus) {
|
||||
fetch(`${baseUrl}/api/dnd/enable`);
|
||||
} else {
|
||||
fetch(`${baseUrl}/api/dnd/disable`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -60,10 +60,6 @@
|
|||
// $: if (containerWidth > 0) {
|
||||
// containerHeight = containerWidth * deviceRatio;
|
||||
// }
|
||||
|
||||
const fontSizeSingle = '4.5rem';
|
||||
const fontSizeMedium = '2.0rem';
|
||||
const fontSizeSplit = '1.0rem';
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -190,7 +186,6 @@
|
|||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
|
||||
.vertical-desc .split-text {
|
||||
transform: rotate(270deg);
|
||||
height: 50%;
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
currency,
|
||||
active = false,
|
||||
onClick,
|
||||
...restProps
|
||||
} = $props();
|
||||
let { currency, active = false, onClick, ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="btn join-item {active ? 'btn-primary' : 'btn-outline'} btn-xs"
|
||||
on:click={onClick}
|
||||
{...restProps}
|
||||
<button
|
||||
class="btn join-item {active ? 'btn-primary' : 'btn-outline'} btn-xs"
|
||||
on:click={onClick}
|
||||
{...restProps}
|
||||
>
|
||||
{currency}
|
||||
</button>
|
||||
{currency}
|
||||
</button>
|
||||
|
|
|
@ -1,26 +1,19 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
value = $bindable(''),
|
||||
label = "",
|
||||
placeholder = "",
|
||||
id = "",
|
||||
type = "text",
|
||||
...restProps
|
||||
} = $props();
|
||||
let {
|
||||
value = $bindable(''),
|
||||
label = '',
|
||||
placeholder = '',
|
||||
id = '',
|
||||
type = 'text',
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="form-control w-full">
|
||||
{#if label}
|
||||
<label for={id} class="label">
|
||||
<span class="label-text">{label}</span>
|
||||
</label>
|
||||
{/if}
|
||||
<input
|
||||
type={type}
|
||||
{placeholder}
|
||||
{id}
|
||||
class="input input-bordered w-full"
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
{#if label}
|
||||
<label for={id} class="label">
|
||||
<span class="label-text">{label}</span>
|
||||
</label>
|
||||
{/if}
|
||||
<input {type} {placeholder} {id} class="input input-bordered w-full" bind:value {...restProps} />
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,10 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
checked = $bindable(false),
|
||||
label = "",
|
||||
id = "",
|
||||
...restProps
|
||||
} = $props();
|
||||
let { checked = $bindable(false), label = '', id = '', ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<label class="flex items-center justify-between gap-2 cursor-pointer">
|
||||
{#if label}
|
||||
<span class="label-text text-xs">{label}</span>
|
||||
{/if}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-xs"
|
||||
{id}
|
||||
bind:checked
|
||||
{...restProps}
|
||||
/>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center justify-between gap-2">
|
||||
{#if label}
|
||||
<span class="label-text text-xs">{label}</span>
|
||||
{/if}
|
||||
<input type="checkbox" class="toggle toggle-primary toggle-xs" {id} bind:checked {...restProps} />
|
||||
</label>
|
||||
|
|
|
@ -18,4 +18,4 @@ export { default as CollapsibleSection } from './layout/CollapsibleSection.svelt
|
|||
export { default as ControlSection } from './sections/ControlSection.svelte';
|
||||
export { default as StatusSection } from './sections/StatusSection.svelte';
|
||||
export { default as SettingsSection } from './sections/SettingsSection.svelte';
|
||||
export { default as SystemSection } from './sections/SystemSection.svelte';
|
||||
export { default as SystemSection } from './sections/SystemSection.svelte';
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
title,
|
||||
open = $bindable(false),
|
||||
...restProps
|
||||
} = $props();
|
||||
let { title, open = $bindable(false), ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<div class="collapse collapse-arrow bg-base-200 rounded-lg mb-2" {...restProps}>
|
||||
<input type="checkbox" bind:checked={open} />
|
||||
<div class="collapse-title text-lg font-medium">
|
||||
{title}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-arrow bg-base-200 collapse mb-2 rounded-lg" {...restProps}>
|
||||
<input type="checkbox" bind:checked={open} />
|
||||
<div class="collapse-title text-lg font-medium">
|
||||
{title}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,87 +1,105 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
...restProps
|
||||
} = $props();
|
||||
|
||||
import { setLocale, getLocale } from '$lib/paraglide/runtime';
|
||||
import { locales } from '$lib/paraglide/runtime';
|
||||
import { page } from '$app/stores';
|
||||
let { ...restProps } = $props();
|
||||
|
||||
// Navigation items
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/settings', label: 'Settings' },
|
||||
{ href: '/system', label: 'System' },
|
||||
{ href: '/apidoc', label: 'API' }
|
||||
];
|
||||
import { setLocale, getLocale } from '$lib/paraglide/runtime';
|
||||
import { locales } from '$lib/paraglide/runtime';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
// Helper function to check if a link is active
|
||||
function isActive(href: string) {
|
||||
return $page.url.pathname === href;
|
||||
}
|
||||
// Navigation items
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/settings', label: 'Settings' },
|
||||
{ href: '/system', label: 'System' },
|
||||
{ href: '/apidoc', label: 'API' }
|
||||
];
|
||||
|
||||
const getLocaleName = (locale: string) => {
|
||||
return new Intl.DisplayNames([locale], { type: 'language' }).of(locale)
|
||||
}
|
||||
// Helper function to check if a link is active
|
||||
function isActive(href: string) {
|
||||
return $page.url.pathname === href;
|
||||
}
|
||||
|
||||
const getLanguageName = (locale: string) => {
|
||||
return getLocaleName(locale.split('-')[0])
|
||||
}
|
||||
const getLocaleName = (locale: string) => {
|
||||
return new Intl.DisplayNames([locale], { type: 'language' }).of(locale);
|
||||
};
|
||||
|
||||
const getEmojiFlag = (locale: string) => {
|
||||
const countryCode = locale.split('-')[1];
|
||||
const getLanguageName = (locale: string) => {
|
||||
return getLocaleName(locale.split('-')[0]);
|
||||
};
|
||||
|
||||
if (!countryCode || countryCode === 'US') {
|
||||
return '🇺🇸';
|
||||
}
|
||||
const getEmojiFlag = (locale: string) => {
|
||||
const countryCode = locale.split('-')[1];
|
||||
|
||||
return [...countryCode.toUpperCase()]
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
}
|
||||
if (!countryCode || countryCode === 'US') {
|
||||
return '🇺🇸';
|
||||
}
|
||||
|
||||
// Function to get the current flag
|
||||
const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧';
|
||||
return [...countryCode.toUpperCase()]
|
||||
.map((char) => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
};
|
||||
|
||||
// Function to get the current flag
|
||||
const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧';
|
||||
</script>
|
||||
|
||||
<div class="navbar bg-base-100 fixed top-0 z-50 shadow-sm w-full" {...restProps}>
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul tabindex="-1" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||
{#each navItems as { href, label }}
|
||||
<li>
|
||||
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<a href="/" class="btn btn-ghost text-xl">BTClock</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
{#each navItems as { href, label }}
|
||||
<li>
|
||||
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="dropdown dropdown-end mr-2">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost">
|
||||
<span class="text-sm">{getCurrentFlag()} {getLanguageName(getLocale())}</span>
|
||||
</div>
|
||||
<ul tabindex="-1" class="mt-3 z-[1] p-2 shadow menu dropdown-content bg-base-100 rounded-box w-auto">
|
||||
{#each locales as locale}
|
||||
<li><button onclick={() => setLocale(locale)} class="flex items-center gap-2 text-nowrap">{getEmojiFlag(locale)} {getLanguageName(locale)}</button></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar bg-base-100 fixed top-0 z-50 w-full shadow-sm" {...restProps}>
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h8m-8 6h16"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
|
||||
>
|
||||
{#each navItems as { href, label } (href)}
|
||||
<li>
|
||||
<a {href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<a href="/" class="btn btn-ghost text-xl">BTClock</a>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
{#each navItems as { href, label } (href)}
|
||||
<li>
|
||||
<a {href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="dropdown dropdown-end mr-2">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost">
|
||||
<span class="text-sm">{getCurrentFlag()} {getLanguageName(getLocale())}</span>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="menu dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-auto p-2 shadow"
|
||||
>
|
||||
{#each locales as locale (locale)}
|
||||
<li>
|
||||
<button onclick={() => setLocale(locale)} class="flex items-center gap-2 text-nowrap"
|
||||
>{getEmojiFlag(locale)} {getLanguageName(locale)}</button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
import { CardContainer, InputField, Toggle } from '$lib/components';
|
||||
import { settings, status } from '$lib/stores';
|
||||
import { onDestroy } from 'svelte';
|
||||
import {
|
||||
setCustomText,
|
||||
setLEDcolor,
|
||||
turnOffLeds,
|
||||
restartClock,
|
||||
import {
|
||||
setCustomText,
|
||||
setLEDcolor,
|
||||
turnOffLeds,
|
||||
restartClock,
|
||||
forceFullRefresh,
|
||||
generateRandomColor,
|
||||
flashFrontlight,
|
||||
|
@ -17,10 +17,10 @@
|
|||
import type { LedStatus } from '$lib/types';
|
||||
|
||||
let ledStatus = $state<LedStatus[]>([
|
||||
{hex: '#000000'},
|
||||
{hex: '#000000'},
|
||||
{hex: '#000000'},
|
||||
{hex: '#000000'}
|
||||
{ hex: '#000000' },
|
||||
{ hex: '#000000' },
|
||||
{ hex: '#000000' },
|
||||
{ hex: '#000000' }
|
||||
]);
|
||||
let customText = $state('');
|
||||
let keepLedsSameColor = $state(false);
|
||||
|
@ -28,7 +28,7 @@
|
|||
const checkSyncLeds = (e: Event) => {
|
||||
if (keepLedsSameColor && e.target instanceof HTMLInputElement) {
|
||||
const targetValue = e.target.value;
|
||||
|
||||
|
||||
ledStatus.forEach((element, i) => {
|
||||
if (ledStatus[i].hex != targetValue) {
|
||||
ledStatus[i].hex = targetValue;
|
||||
|
@ -81,19 +81,21 @@
|
|||
<div class="flex justify-between gap-2">
|
||||
<div class="mb-4 flex flex-wrap gap-2">
|
||||
{#if ledStatus.length > 0}
|
||||
{#each ledStatus as led}
|
||||
{#each ledStatus as led (led)}
|
||||
<input
|
||||
type="color"
|
||||
class="btn btn-square"
|
||||
bind:value={led.hex}
|
||||
onchange={checkSyncLeds}
|
||||
/>
|
||||
{/each}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
<Toggle label={m['sections.control.keepSameColor']()} bind:checked={keepLedsSameColor} />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-secondary" onclick={turnOffLeds}>{m['section.control.turnOff']()}</button>
|
||||
<button class="btn btn-secondary" onclick={turnOffLeds}
|
||||
>{m['section.control.turnOff']()}</button
|
||||
>
|
||||
<button class="btn btn-primary" onclick={() => setLEDcolor(ledStatus)}
|
||||
>{m['section.control.setColor']()}</button
|
||||
>
|
||||
|
@ -102,19 +104,24 @@
|
|||
</div>
|
||||
|
||||
{#if $settings.hasFrontlight && !$settings.flDisable}
|
||||
<div>
|
||||
<h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button class="btn btn-secondary" onclick={() => turnOnFrontlight()}>{m['section.control.turnOn']()}</button>
|
||||
<button class="btn btn-primary" onclick={() => turnOffFrontlight()}>{m['section.control.turnOff']()}</button>
|
||||
<button class="btn btn-accent" onclick={() => flashFrontlight()}>{m['section.control.flashFrontlight']()}</button>
|
||||
<div>
|
||||
<h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-secondary" onclick={() => turnOnFrontlight()}
|
||||
>{m['section.control.turnOn']()}</button
|
||||
>
|
||||
<button class="btn btn-primary" onclick={() => turnOffFrontlight()}
|
||||
>{m['section.control.turnOff']()}</button
|
||||
>
|
||||
<button class="btn btn-accent" onclick={() => flashFrontlight()}
|
||||
>{m['section.control.flashFrontlight']()}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<h3 class="mb-2 font-medium">{m['section.control.title']()}</h3>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
|
||||
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,341 +1,361 @@
|
|||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { CardContainer, Toggle, CollapsibleSection } from '$lib/components';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
let { ...restProps } = $props();
|
||||
|
||||
// Show/hide toggles
|
||||
let showAll = $state(false);
|
||||
let hideAll = $state(false);
|
||||
|
||||
function toggleShowAll() {
|
||||
showAll = true;
|
||||
hideAll = false;
|
||||
}
|
||||
|
||||
function toggleHideAll() {
|
||||
hideAll = true;
|
||||
showAll = false;
|
||||
}
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { CardContainer, Toggle, CollapsibleSection } from '$lib/components';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
let { ...restProps } = $props();
|
||||
|
||||
// Show/hide toggles
|
||||
let showAll = $state(false);
|
||||
let hideAll = $state(false);
|
||||
|
||||
function toggleShowAll() {
|
||||
showAll = true;
|
||||
hideAll = false;
|
||||
}
|
||||
|
||||
function toggleHideAll() {
|
||||
hideAll = true;
|
||||
showAll = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardContainer title={m["section.settings.title"]()} {...restProps}>
|
||||
<div class="flex justify-end gap-2 mb-4">
|
||||
<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>
|
||||
<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 gap-4 grid-cols-2">
|
||||
<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>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.section.screenSettings"]()} open={showAll || !hideAll}>
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.screens"]()} open={showAll || !hideAll}>
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
{#each $settings.screens as screen}
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={screen.name}
|
||||
checked={screen.enabled}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<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>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.currencies"]()} open={showAll || !hideAll}>
|
||||
<div class="alert alert-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" 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="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="grid gap-4 grid-cols-2">
|
||||
|
||||
{#each $settings.actCurrencies as currency}
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={currency}
|
||||
checked={$settings.actCurrencies.includes(currency)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<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>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.section.displaysAndLed"]()} open={showAll || !hideAll}>
|
||||
<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>
|
||||
|
||||
<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="w-auto input">
|
||||
<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="w-auto input">
|
||||
<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
|
||||
label={m["section.settings.ledFlashOnBlock"]()}
|
||||
checked={$settings.ledFlashOnUpd}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m["section.settings.disableLeds"]()}
|
||||
checked={$settings.disableLeds}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<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>
|
||||
|
||||
{#if $settings.hasFrontlight}
|
||||
<CollapsibleSection title="Frontlight Settings" open={showAll || !hideAll}>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label="Disable Frontlight"
|
||||
checked={$settings.flDisable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label="Always On"
|
||||
checked={$settings.flAlwaysOn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label="Flash on Updates"
|
||||
checked={$settings.flFlashOnUpd}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label="Flash on Zaps"
|
||||
checked={$settings.flFlashOnZap}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if $settings.hasLightLevel}
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label="Turn Off in Dark"
|
||||
checked={$settings.flOffWhenDark}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Light Level Threshold</span>
|
||||
</label>
|
||||
<input type="range" min="0" max="255" class="range" 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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
{/if}
|
||||
<CollapsibleSection title={m['section.settings.screens']()} open={showAll || !hideAll}>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{#each $settings.screens as screen (screen.id)}
|
||||
<div class="form-control">
|
||||
<Toggle label={screen.name} checked={screen.enabled} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.section.dataSource"]()} open={showAll || !hideAll}>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<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>
|
||||
|
||||
<div class="form-control">
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.ceEndpoint"]()}</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" placeholder="Custom Endpoint URL" />
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.section.extraFeatures"]()} open={showAll || !hideAll}>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<Toggle
|
||||
label={m["section.settings.timeBasedDnd"]()}
|
||||
checked={$settings.dnd.enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection title={m["section.settings.section.system"]()} open={showAll || !hideAll}>
|
||||
<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="text-sm mt-1">{m["section.settings.tzOffsetHelpText"]()}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m["section.settings.hostnamePrefix"]()}</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" value="btclock" />
|
||||
</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>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection
|
||||
title={m['section.settings.section.displaysAndLed']()}
|
||||
open={showAll || !hideAll}
|
||||
>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
<div class="flex justify-between mt-6">
|
||||
<button class="btn btn-error">{m["button.reset"]()}</button>
|
||||
<button class="btn btn-primary">{m["button.save"]()}</button>
|
||||
</div>
|
||||
</CardContainer>
|
||||
<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
|
||||
label={m['section.settings.ledFlashOnBlock']()}
|
||||
checked={$settings.ledFlashOnUpd}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle label={m['section.settings.disableLeds']()} checked={$settings.disableLeds} />
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
{#if $settings.hasFrontlight}
|
||||
<CollapsibleSection title="Frontlight Settings" open={showAll || !hideAll}>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<Toggle label="Disable Frontlight" checked={$settings.flDisable} />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle label="Always On" checked={$settings.flAlwaysOn} />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle label="Flash on Updates" checked={$settings.flFlashOnUpd} />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<Toggle label="Flash on Zaps" checked={$settings.flFlashOnZap} />
|
||||
</div>
|
||||
|
||||
{#if $settings.hasLightLevel}
|
||||
<div class="form-control">
|
||||
<Toggle label="Turn Off in Dark" checked={$settings.flOffWhenDark} />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Light Level Threshold</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="255"
|
||||
class="range"
|
||||
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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
{/if}
|
||||
|
||||
<CollapsibleSection
|
||||
title={m['section.settings.section.dataSource']()}
|
||||
open={showAll || !hideAll}
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<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>
|
||||
|
||||
<div class="form-control">
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m['section.settings.ceEndpoint']()}</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Custom Endpoint URL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection
|
||||
title={m['section.settings.section.extraFeatures']()}
|
||||
open={showAll || !hideAll}
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<div class="form-control">
|
||||
<Toggle label={m['section.settings.timeBasedDnd']()} checked={$settings.dnd.enabled} />
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection title={m['section.settings.section.system']()} open={showAll || !hideAll}>
|
||||
<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>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">{m['section.settings.hostnamePrefix']()}</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered w-full" value="btclock" />
|
||||
</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>
|
||||
</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>
|
||||
|
|
|
@ -2,53 +2,54 @@
|
|||
import { m } from '$lib/paraglide/messages';
|
||||
import { CardContainer, TabButton, CurrencyButton, Stat, Status } from '$lib/components';
|
||||
import { status, settings } from '$lib/stores';
|
||||
import { setActiveScreen, setActiveCurrency } from '$lib/clockControl';
|
||||
import { setActiveScreen, setActiveCurrency } from '$lib/clockControl';
|
||||
import BTClock from '../BTClock.svelte';
|
||||
import { DataSourceType } from '$lib/types';
|
||||
import { toUptimestring } from '$lib/utils';
|
||||
|
||||
const screens = $settings.screens.map(screen => ({
|
||||
id: screen.id,
|
||||
label: screen.name
|
||||
}));
|
||||
const screens = $settings.screens.map((screen) => ({
|
||||
id: screen.id,
|
||||
label: screen.name
|
||||
}));
|
||||
</script>
|
||||
|
||||
<CardContainer title={m['section.status.title']()}>
|
||||
<div class="space-y-4 mx-auto">
|
||||
<div class="mx-auto space-y-4">
|
||||
<div class="join">
|
||||
{#each screens as screen}
|
||||
<TabButton active={$status.currentScreen === screen.id} onClick={() => setActiveScreen(screen.id)}>
|
||||
{#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}
|
||||
<CurrencyButton
|
||||
currency={currency}
|
||||
active={$status.currency === currency}
|
||||
onClick={() => setActiveCurrency(currency)}
|
||||
/>
|
||||
{/each}
|
||||
{#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">
|
||||
<!-- Bitcoin value display showing blocks/price -->
|
||||
<BTClock displays={$status.data} verticalDesc={$settings.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.enabled ? m['on']() : m['off']()}
|
||||
{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')} )
|
||||
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings.dnd
|
||||
.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
|
||||
{/if}
|
||||
</small>
|
||||
</div>
|
||||
|
@ -58,18 +59,28 @@
|
|||
<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'} />
|
||||
<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'} />
|
||||
<Status
|
||||
text={m['section.status.wsDataConnection']()}
|
||||
status={$status.connectionStatus.V2 ? 'online' : 'offline'}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="flex justify-center stats shadow mt-4">
|
||||
<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 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>
|
||||
</CardContainer>
|
||||
|
|
|
@ -1,102 +1,99 @@
|
|||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { CardContainer } from '$lib/components';
|
||||
import { settings } from '$lib/stores';
|
||||
import {
|
||||
restartClock,
|
||||
forceFullRefresh
|
||||
} from '$lib/clockControl';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { CardContainer } from '$lib/components';
|
||||
import { settings } from '$lib/stores';
|
||||
import { restartClock, forceFullRefresh } from '$lib/clockControl';
|
||||
</script>
|
||||
|
||||
<CardContainer title="System Information">
|
||||
<div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-sm table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">System info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-sm table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">System info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{m['section.control.version']()}</td>
|
||||
<td>{$settings.gitTag}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.buildTime']()}</td>
|
||||
<td>{$settings.lastBuildTime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP</td>
|
||||
<td>{$settings.ip}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HW revision</td>
|
||||
<td>{$settings.hwRev}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.fwCommit']()}</td>
|
||||
<td class="text-xs">{$settings.gitRev}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.hostname']()}</td>
|
||||
<td>{$settings.hostname}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<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>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{m['section.control.version']()}</td>
|
||||
<td>{$settings.gitTag}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.buildTime']()}</td>
|
||||
<td>{$settings.lastBuildTime}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP</td>
|
||||
<td>{$settings.ip}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HW revision</td>
|
||||
<td>{$settings.hwRev}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.fwCommit']()}</td>
|
||||
<td class="text-xs">{$settings.gitRev}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{m['section.control.hostname']()}</td>
|
||||
<td>{$settings.hostname}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<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>
|
||||
<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 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="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>
|
||||
<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>
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
title,
|
||||
className = "",
|
||||
...restProps
|
||||
} = $props();
|
||||
let { title, className = '', ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl w-full {className}" {...restProps}>
|
||||
<div class="card-body">
|
||||
{#if title}
|
||||
<h2 class="card-title">{title}</h2>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 w-full shadow-xl {className}" {...restProps}>
|
||||
<div class="card-body">
|
||||
{#if title}
|
||||
<h2 class="card-title">{title}</h2>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
title,
|
||||
value,
|
||||
desc = "",
|
||||
icon = "",
|
||||
className = "",
|
||||
...restProps
|
||||
} = $props();
|
||||
let { title, value, desc = '', icon = '', className = '', ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<div class="stat {className}" {...restProps}>
|
||||
{#if icon}
|
||||
<div class="stat-figure text-primary">
|
||||
{@html icon}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="stat-title">{title}</div>
|
||||
<div class="stat-value">{value}</div>
|
||||
{#if desc}
|
||||
<div class="stat-desc">{desc}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if icon}
|
||||
<div class="stat-figure text-primary">
|
||||
{@html icon}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="stat-title">{title}</div>
|
||||
<div class="stat-value">{value}</div>
|
||||
{#if desc}
|
||||
<div class="stat-desc">{desc}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,33 +1,35 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
status = "offline" as "online" | "offline" | "error" | "warning",
|
||||
text,
|
||||
className = "",
|
||||
...restProps
|
||||
} = $props();
|
||||
let {
|
||||
status = 'offline' as 'online' | 'offline' | 'error' | 'warning',
|
||||
text,
|
||||
className = '',
|
||||
...restProps
|
||||
} = $props();
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case "online":
|
||||
return "bg-success";
|
||||
case "offline":
|
||||
return "bg-base-300";
|
||||
case "error":
|
||||
return "bg-error";
|
||||
case "warning":
|
||||
return "bg-warning";
|
||||
default:
|
||||
return "bg-base-300";
|
||||
}
|
||||
};
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return 'bg-success';
|
||||
case 'offline':
|
||||
return 'bg-base-300';
|
||||
case 'error':
|
||||
return 'bg-error';
|
||||
case 'warning':
|
||||
return 'bg-warning';
|
||||
default:
|
||||
return 'bg-base-300';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2 {className}" {...restProps}>
|
||||
<div class="relative flex">
|
||||
<div class="{getStatusColor()} h-3 w-3 rounded-full"></div>
|
||||
{#if status === "online"}
|
||||
<div class="{getStatusColor()} animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-sm">{text}</span>
|
||||
</div>
|
||||
<div class="relative flex">
|
||||
<div class="{getStatusColor()} h-3 w-3 rounded-full"></div>
|
||||
{#if status === 'online'}
|
||||
<div
|
||||
class="{getStatusColor()} absolute inline-flex h-3 w-3 animate-ping rounded-full opacity-75"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-sm">{text}</span>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
<script lang="ts">
|
||||
let {
|
||||
active = false,
|
||||
onClick,
|
||||
...restProps
|
||||
} = $props();
|
||||
let { active = false, onClick, ...restProps } = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="btn btn-sm join-item {active ? 'btn-primary' : 'btn-outline'}"
|
||||
on:click={onClick}
|
||||
{...restProps}
|
||||
<button
|
||||
class="btn btn-sm join-item {active ? 'btn-primary' : 'btn-outline'}"
|
||||
on:click={onClick}
|
||||
{...restProps}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
<slot />
|
||||
</button>
|
||||
|
|
|
@ -1,71 +1,86 @@
|
|||
<script lang="ts">
|
||||
type Position =
|
||||
| 'top-start' | 'top-center' | 'top-end'
|
||||
| 'middle-start' | 'middle-center' | 'middle-end'
|
||||
| 'bottom-start' | 'bottom-center' | 'bottom-end';
|
||||
|
||||
type AlertType = 'info' | 'success' | 'warning' | 'error';
|
||||
type Position =
|
||||
| 'top-start'
|
||||
| 'top-center'
|
||||
| 'top-end'
|
||||
| 'middle-start'
|
||||
| 'middle-center'
|
||||
| 'middle-end'
|
||||
| 'bottom-start'
|
||||
| 'bottom-center'
|
||||
| 'bottom-end';
|
||||
|
||||
let props = $props();
|
||||
|
||||
let message = props.message || '';
|
||||
let type = (props.type as AlertType) || 'info';
|
||||
let position = (props.position as Position) || 'bottom-end';
|
||||
let duration = props.duration || 3000;
|
||||
let showClose = props.showClose || false;
|
||||
|
||||
// Create a new object without the known props
|
||||
let restProps = { ...props };
|
||||
delete restProps.message;
|
||||
delete restProps.type;
|
||||
delete restProps.position;
|
||||
delete restProps.duration;
|
||||
delete restProps.showClose;
|
||||
type AlertType = 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
let visible = $state(true);
|
||||
let props = $props();
|
||||
|
||||
// Map position prop to DaisyUI classes
|
||||
const getPositionClasses = (pos: Position): string => {
|
||||
const [vertical, horizontal] = pos.split('-');
|
||||
|
||||
let classes = 'toast';
|
||||
|
||||
// Vertical position
|
||||
if (vertical === 'top') classes += ' toast-top';
|
||||
if (vertical === 'middle') classes += ' toast-middle';
|
||||
// bottom is default, no class needed
|
||||
|
||||
// Horizontal position
|
||||
if (horizontal === 'start') classes += ' toast-start';
|
||||
if (horizontal === 'center') classes += ' toast-center';
|
||||
if (horizontal === 'end') classes += ' toast-end';
|
||||
|
||||
return classes;
|
||||
};
|
||||
let message = props.message || '';
|
||||
let type = (props.type as AlertType) || 'info';
|
||||
let position = (props.position as Position) || 'bottom-end';
|
||||
let duration = props.duration || 3000;
|
||||
let showClose = props.showClose || false;
|
||||
|
||||
// Auto-hide the toast after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
visible = false;
|
||||
}, duration);
|
||||
}
|
||||
// Create a new object without the known props
|
||||
let restProps = { ...props };
|
||||
delete restProps.message;
|
||||
delete restProps.type;
|
||||
delete restProps.position;
|
||||
delete restProps.duration;
|
||||
delete restProps.showClose;
|
||||
|
||||
const closeToast = () => {
|
||||
visible = false;
|
||||
};
|
||||
let visible = $state(true);
|
||||
|
||||
// Map position prop to DaisyUI classes
|
||||
const getPositionClasses = (pos: Position): string => {
|
||||
const [vertical, horizontal] = pos.split('-');
|
||||
|
||||
let classes = 'toast';
|
||||
|
||||
// Vertical position
|
||||
if (vertical === 'top') classes += ' toast-top';
|
||||
if (vertical === 'middle') classes += ' toast-middle';
|
||||
// bottom is default, no class needed
|
||||
|
||||
// Horizontal position
|
||||
if (horizontal === 'start') classes += ' toast-start';
|
||||
if (horizontal === 'center') classes += ' toast-center';
|
||||
if (horizontal === 'end') classes += ' toast-end';
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
// Auto-hide the toast after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
visible = false;
|
||||
}, duration);
|
||||
}
|
||||
|
||||
const closeToast = () => {
|
||||
visible = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div class={getPositionClasses(position)} {...restProps}>
|
||||
<div class="alert alert-{type}">
|
||||
<span>{message}</span>
|
||||
{#if showClose}
|
||||
<button class="btn btn-circle btn-xs" onclick={closeToast}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class={getPositionClasses(position)} {...restProps}>
|
||||
<div class="alert alert-{type}">
|
||||
<span>{message}</span>
|
||||
{#if showClose}
|
||||
<button class="btn btn-circle btn-xs" onclick={closeToast}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import { PUBLIC_BASE_URL } from '$env/static/public';
|
||||
|
||||
export const baseUrl = PUBLIC_BASE_URL;
|
||||
export const baseUrl = PUBLIC_BASE_URL;
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export { settings, type Settings } from './settings';
|
||||
export { status, type Status } from './status';
|
||||
export { status, type Status } from './status';
|
||||
|
|
|
@ -4,158 +4,158 @@ import type { Settings } from '$lib/types';
|
|||
|
||||
// Create a default settings object
|
||||
const defaultSettings: Settings = {
|
||||
numScreens: 7,
|
||||
invertedColor: false,
|
||||
timerSeconds: 60,
|
||||
timerRunning: false,
|
||||
minSecPriceUpd: 30,
|
||||
fullRefreshMin: 60,
|
||||
wpTimeout: 600,
|
||||
tzString: "UTC",
|
||||
dataSource: 0,
|
||||
mempoolInstance: "mempool.space",
|
||||
mempoolSecure: true,
|
||||
localPoolEndpoint: "localhost:2019",
|
||||
nostrPubKey: "",
|
||||
nostrRelay: "wss://relay.damus.io",
|
||||
nostrZapNotify: false,
|
||||
nostrZapPubkey: "",
|
||||
ledFlashOnZap: true,
|
||||
fontName: "oswald",
|
||||
availableFonts: ["antonio", "oswald"],
|
||||
customEndpoint: "ws-staging.btclock.dev",
|
||||
customEndpointDisableSSL: false,
|
||||
ledTestOnPower: true,
|
||||
ledFlashOnUpd: true,
|
||||
ledBrightness: 255,
|
||||
stealFocus: false,
|
||||
mcapBigChar: true,
|
||||
mdnsEnabled: true,
|
||||
otaEnabled: true,
|
||||
useSatsSymbol: true,
|
||||
useBlkCountdown: true,
|
||||
suffixPrice: false,
|
||||
disableLeds: false,
|
||||
mowMode: false,
|
||||
verticalDesc: true,
|
||||
suffixShareDot: false,
|
||||
enableDebugLog: false,
|
||||
hostnamePrefix: "btclock",
|
||||
hostname: "btclock",
|
||||
ip: "",
|
||||
txPower: 80,
|
||||
gitReleaseUrl: "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest",
|
||||
bitaxeEnabled: false,
|
||||
bitaxeHostname: "bitaxe1",
|
||||
miningPoolStats: false,
|
||||
miningPoolName: "noderunners",
|
||||
miningPoolUser: "",
|
||||
availablePools: [
|
||||
"ocean",
|
||||
"noderunners",
|
||||
"satoshi_radio",
|
||||
"braiins",
|
||||
"public_pool",
|
||||
"local_public_pool",
|
||||
"gobrrr_pool",
|
||||
"ckpool",
|
||||
"eu_ckpool"
|
||||
],
|
||||
httpAuthEnabled: false,
|
||||
httpAuthUser: "btclock",
|
||||
httpAuthPass: "satoshi",
|
||||
hasFrontlight: false,
|
||||
// Default frontlight settings
|
||||
flDisable: false,
|
||||
flMaxBrightness: 2684,
|
||||
flAlwaysOn: false,
|
||||
flEffectDelay: 50,
|
||||
flFlashOnUpd: true,
|
||||
flFlashOnZap: true,
|
||||
// Default light sensor settings
|
||||
hasLightLevel: false,
|
||||
luxLightToggle: 128,
|
||||
flOffWhenDark: false,
|
||||
hwRev: "",
|
||||
fsRev: "",
|
||||
gitRev: "",
|
||||
gitTag: "",
|
||||
lastBuildTime: "",
|
||||
screens: [
|
||||
{id: 0, name: "Block Height", enabled: true},
|
||||
{id: 3, name: "Time", enabled: false},
|
||||
{id: 4, name: "Halving countdown", enabled: false},
|
||||
{id: 6, name: "Block Fee Rate", enabled: false},
|
||||
{id: 10, name: "Sats per dollar", enabled: true},
|
||||
{id: 20, name: "Ticker", enabled: true},
|
||||
{id: 30, name: "Market Cap", enabled: false}
|
||||
],
|
||||
actCurrencies: ["USD"],
|
||||
availableCurrencies: ["USD", "EUR", "GBP", "JPY", "AUD", "CAD"],
|
||||
poolLogosUrl: "https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main",
|
||||
ceEndpoint: "ws-staging.btclock.dev",
|
||||
ceDisableSSL: false,
|
||||
dnd: {
|
||||
enabled: false,
|
||||
timeBasedEnabled: false,
|
||||
startHour: 23,
|
||||
startMinute: 0,
|
||||
endHour: 7,
|
||||
endMinute: 0
|
||||
}
|
||||
numScreens: 7,
|
||||
invertedColor: false,
|
||||
timerSeconds: 60,
|
||||
timerRunning: false,
|
||||
minSecPriceUpd: 30,
|
||||
fullRefreshMin: 60,
|
||||
wpTimeout: 600,
|
||||
tzString: 'UTC',
|
||||
dataSource: 0,
|
||||
mempoolInstance: 'mempool.space',
|
||||
mempoolSecure: true,
|
||||
localPoolEndpoint: 'localhost:2019',
|
||||
nostrPubKey: '',
|
||||
nostrRelay: 'wss://relay.damus.io',
|
||||
nostrZapNotify: false,
|
||||
nostrZapPubkey: '',
|
||||
ledFlashOnZap: true,
|
||||
fontName: 'oswald',
|
||||
availableFonts: ['antonio', 'oswald'],
|
||||
customEndpoint: 'ws-staging.btclock.dev',
|
||||
customEndpointDisableSSL: false,
|
||||
ledTestOnPower: true,
|
||||
ledFlashOnUpd: true,
|
||||
ledBrightness: 255,
|
||||
stealFocus: false,
|
||||
mcapBigChar: true,
|
||||
mdnsEnabled: true,
|
||||
otaEnabled: true,
|
||||
useSatsSymbol: true,
|
||||
useBlkCountdown: true,
|
||||
suffixPrice: false,
|
||||
disableLeds: false,
|
||||
mowMode: false,
|
||||
verticalDesc: true,
|
||||
suffixShareDot: false,
|
||||
enableDebugLog: false,
|
||||
hostnamePrefix: 'btclock',
|
||||
hostname: 'btclock',
|
||||
ip: '',
|
||||
txPower: 80,
|
||||
gitReleaseUrl: 'https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest',
|
||||
bitaxeEnabled: false,
|
||||
bitaxeHostname: 'bitaxe1',
|
||||
miningPoolStats: false,
|
||||
miningPoolName: 'noderunners',
|
||||
miningPoolUser: '',
|
||||
availablePools: [
|
||||
'ocean',
|
||||
'noderunners',
|
||||
'satoshi_radio',
|
||||
'braiins',
|
||||
'public_pool',
|
||||
'local_public_pool',
|
||||
'gobrrr_pool',
|
||||
'ckpool',
|
||||
'eu_ckpool'
|
||||
],
|
||||
httpAuthEnabled: false,
|
||||
httpAuthUser: 'btclock',
|
||||
httpAuthPass: 'satoshi',
|
||||
hasFrontlight: false,
|
||||
// Default frontlight settings
|
||||
flDisable: false,
|
||||
flMaxBrightness: 2684,
|
||||
flAlwaysOn: false,
|
||||
flEffectDelay: 50,
|
||||
flFlashOnUpd: true,
|
||||
flFlashOnZap: true,
|
||||
// Default light sensor settings
|
||||
hasLightLevel: false,
|
||||
luxLightToggle: 128,
|
||||
flOffWhenDark: false,
|
||||
hwRev: '',
|
||||
fsRev: '',
|
||||
gitRev: '',
|
||||
gitTag: '',
|
||||
lastBuildTime: '',
|
||||
screens: [
|
||||
{ id: 0, name: 'Block Height', enabled: true },
|
||||
{ id: 3, name: 'Time', enabled: false },
|
||||
{ id: 4, name: 'Halving countdown', enabled: false },
|
||||
{ id: 6, name: 'Block Fee Rate', enabled: false },
|
||||
{ id: 10, name: 'Sats per dollar', enabled: true },
|
||||
{ id: 20, name: 'Ticker', enabled: true },
|
||||
{ id: 30, name: 'Market Cap', enabled: false }
|
||||
],
|
||||
actCurrencies: ['USD'],
|
||||
availableCurrencies: ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD'],
|
||||
poolLogosUrl: 'https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main',
|
||||
ceEndpoint: 'ws-staging.btclock.dev',
|
||||
ceDisableSSL: false,
|
||||
dnd: {
|
||||
enabled: false,
|
||||
timeBasedEnabled: false,
|
||||
startHour: 23,
|
||||
startMinute: 0,
|
||||
endHour: 7,
|
||||
endMinute: 0
|
||||
}
|
||||
};
|
||||
|
||||
// Create the Svelte store
|
||||
function createSettingsStore() {
|
||||
const { subscribe, set, update } = writable<Settings>(defaultSettings);
|
||||
const { subscribe, set, update } = writable<Settings>(defaultSettings);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
fetch: async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/settings`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching settings: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
set(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
return defaultSettings;
|
||||
}
|
||||
},
|
||||
update: async (newSettings: Partial<Settings>) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/settings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newSettings),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error updating settings: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Update the local store with the new settings
|
||||
update(currentSettings => ({ ...currentSettings, ...newSettings }));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update settings:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
set: (newSettings: Settings) => set(newSettings),
|
||||
reset: () => set(defaultSettings)
|
||||
};
|
||||
return {
|
||||
subscribe,
|
||||
fetch: async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/settings`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching settings: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
set(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
return defaultSettings;
|
||||
}
|
||||
},
|
||||
update: async (newSettings: Partial<Settings>) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/settings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(newSettings)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error updating settings: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Update the local store with the new settings
|
||||
update((currentSettings) => ({ ...currentSettings, ...newSettings }));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update settings:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
set: (newSettings: Settings) => set(newSettings),
|
||||
reset: () => set(defaultSettings)
|
||||
};
|
||||
}
|
||||
|
||||
export const settings = createSettingsStore();
|
||||
|
||||
// Initialize the store by fetching settings when this module is first imported
|
||||
if (typeof window !== 'undefined') {
|
||||
settings.fetch();
|
||||
}
|
||||
settings.fetch();
|
||||
}
|
||||
|
|
|
@ -3,178 +3,178 @@ import { baseUrl } from '$lib/env';
|
|||
import type { Status } from '$lib/types';
|
||||
// Create a default status object
|
||||
const defaultStatus: Status = {
|
||||
currentScreen: 0,
|
||||
numScreens: 0,
|
||||
timerRunning: false,
|
||||
isOTAUpdating: false,
|
||||
espUptime: 0,
|
||||
espFreeHeap: 0,
|
||||
espHeapSize: 0,
|
||||
connectionStatus: {
|
||||
price: false,
|
||||
blocks: false,
|
||||
V2: false,
|
||||
nostr: false
|
||||
},
|
||||
rssi: 0,
|
||||
currency: "USD",
|
||||
dnd: {
|
||||
enabled: false,
|
||||
timeBasedEnabled: false,
|
||||
startTime: "00:00",
|
||||
endTime: "00:00",
|
||||
active: false
|
||||
},
|
||||
data: [],
|
||||
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"}
|
||||
]
|
||||
currentScreen: 0,
|
||||
numScreens: 0,
|
||||
timerRunning: false,
|
||||
isOTAUpdating: false,
|
||||
espUptime: 0,
|
||||
espFreeHeap: 0,
|
||||
espHeapSize: 0,
|
||||
connectionStatus: {
|
||||
price: false,
|
||||
blocks: false,
|
||||
V2: false,
|
||||
nostr: false
|
||||
},
|
||||
rssi: 0,
|
||||
currency: 'USD',
|
||||
dnd: {
|
||||
enabled: false,
|
||||
timeBasedEnabled: false,
|
||||
startTime: '00:00',
|
||||
endTime: '00:00',
|
||||
active: false
|
||||
},
|
||||
data: [],
|
||||
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' }
|
||||
]
|
||||
};
|
||||
|
||||
// Create the Svelte store
|
||||
function createStatusStore() {
|
||||
const { subscribe, set, update } = writable<Status>(defaultStatus);
|
||||
let eventSource: EventSource | null = null;
|
||||
const { subscribe, set, update } = writable<Status>(defaultStatus);
|
||||
let eventSource: EventSource | null = null;
|
||||
|
||||
// Clean up function to close SSE connection
|
||||
const cleanup = () => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
};
|
||||
// Clean up function to close SSE connection
|
||||
const cleanup = () => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Create the store object with methods
|
||||
const store = {
|
||||
subscribe,
|
||||
fetch: async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching status: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
set(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch status:', error);
|
||||
return defaultStatus;
|
||||
}
|
||||
},
|
||||
startListening: () => {
|
||||
// Clean up any existing connections first
|
||||
cleanup();
|
||||
|
||||
// Only run in the browser, not during SSR
|
||||
if (typeof window === 'undefined') return;
|
||||
// Create the store object with methods
|
||||
const store = {
|
||||
subscribe,
|
||||
fetch: async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching status: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
set(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch status:', error);
|
||||
return defaultStatus;
|
||||
}
|
||||
},
|
||||
startListening: () => {
|
||||
// Clean up any existing connections first
|
||||
cleanup();
|
||||
|
||||
try {
|
||||
// Create a new EventSource connection
|
||||
eventSource = new EventSource(`${baseUrl}/events`);
|
||||
// Only run in the browser, not during SSR
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Handle status updates
|
||||
eventSource.addEventListener('status', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update(currentStatus => ({ ...currentStatus, ...data }));
|
||||
} catch (error) {
|
||||
console.error('Error processing status event:', error);
|
||||
}
|
||||
});
|
||||
try {
|
||||
// Create a new EventSource connection
|
||||
eventSource = new EventSource(`${baseUrl}/events`);
|
||||
|
||||
// Handle connection status updates
|
||||
eventSource.addEventListener('connection', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update(currentStatus => ({
|
||||
...currentStatus,
|
||||
connectionStatus: {
|
||||
...currentStatus.connectionStatus,
|
||||
...data
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error processing connection event:', error);
|
||||
}
|
||||
});
|
||||
// Handle status updates
|
||||
eventSource.addEventListener('status', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update((currentStatus) => ({ ...currentStatus, ...data }));
|
||||
} catch (error) {
|
||||
console.error('Error processing status event:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle screen updates
|
||||
eventSource.addEventListener('screen', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update(currentStatus => ({
|
||||
...currentStatus,
|
||||
currentScreen: data.screen || currentStatus.currentScreen
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error processing screen event:', error);
|
||||
}
|
||||
});
|
||||
// Handle connection status updates
|
||||
eventSource.addEventListener('connection', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update((currentStatus) => ({
|
||||
...currentStatus,
|
||||
connectionStatus: {
|
||||
...currentStatus.connectionStatus,
|
||||
...data
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error processing connection event:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle generic messages
|
||||
eventSource.onmessage = (event) => {
|
||||
if (event.type === 'message') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update(currentStatus => ({ ...currentStatus, ...data }));
|
||||
} catch (error) {
|
||||
console.error('Error processing message event:', error);
|
||||
}
|
||||
};
|
||||
// Handle screen updates
|
||||
eventSource.addEventListener('screen', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update((currentStatus) => ({
|
||||
...currentStatus,
|
||||
currentScreen: data.screen || currentStatus.currentScreen
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error processing screen event:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('EventSource failed:', error);
|
||||
cleanup();
|
||||
// Attempt to reconnect after a delay
|
||||
setTimeout(() => store.startListening(), 5000);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to setup event source:', error);
|
||||
}
|
||||
},
|
||||
stopListening: cleanup,
|
||||
|
||||
// Function to set the current screen
|
||||
setScreen: async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
// Make the GET request to change the screen
|
||||
const response = await fetch(`${baseUrl}/api/show/screen/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error setting screen: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Update the store with the new screen ID
|
||||
update(currentStatus => ({
|
||||
...currentStatus,
|
||||
currentScreen: id
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to set screen:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return store;
|
||||
// Handle generic messages
|
||||
eventSource.onmessage = (event) => {
|
||||
if (event.type === 'message') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
update((currentStatus) => ({ ...currentStatus, ...data }));
|
||||
} catch (error) {
|
||||
console.error('Error processing message event:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle errors
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('EventSource failed:', error);
|
||||
cleanup();
|
||||
// Attempt to reconnect after a delay
|
||||
setTimeout(() => store.startListening(), 5000);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to setup event source:', error);
|
||||
}
|
||||
},
|
||||
stopListening: cleanup,
|
||||
|
||||
// Function to set the current screen
|
||||
setScreen: async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
// Make the GET request to change the screen
|
||||
const response = await fetch(`${baseUrl}/api/show/screen/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error setting screen: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Update the store with the new screen ID
|
||||
update((currentStatus) => ({
|
||||
...currentStatus,
|
||||
currentScreen: id
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to set screen:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
export const status = createStatusStore();
|
||||
|
||||
// Initialize the store by fetching initial status and starting to listen for updates
|
||||
if (typeof window !== 'undefined') {
|
||||
status.fetch().then(() => status.startListening());
|
||||
|
||||
// Clean up the EventSource when the window is unloaded
|
||||
window.addEventListener('beforeunload', () => {
|
||||
status.stopListening();
|
||||
});
|
||||
}
|
||||
status.fetch().then(() => status.startListening());
|
||||
|
||||
// Clean up the EventSource when the window is unloaded
|
||||
window.addEventListener('beforeunload', () => {
|
||||
status.stopListening();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
*/
|
||||
|
||||
export interface LedStatus {
|
||||
hex: string;
|
||||
}
|
||||
hex: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data source types
|
||||
* Data source types
|
||||
*/
|
||||
export enum DataSourceType {
|
||||
BTCLOCK_SOURCE = 0,
|
||||
|
@ -26,31 +26,31 @@ export interface Status {
|
|||
espFreeHeap: number;
|
||||
espHeapSize: number;
|
||||
connectionStatus: {
|
||||
price: boolean;
|
||||
blocks: boolean;
|
||||
V2: boolean;
|
||||
nostr: boolean;
|
||||
price: boolean;
|
||||
blocks: boolean;
|
||||
V2: boolean;
|
||||
nostr: boolean;
|
||||
};
|
||||
rssi: number;
|
||||
currency: string;
|
||||
dnd: {
|
||||
enabled: boolean;
|
||||
timeBasedEnabled: boolean;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
active: boolean;
|
||||
enabled: boolean;
|
||||
timeBasedEnabled: boolean;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
active: boolean;
|
||||
};
|
||||
data: string[];
|
||||
leds: Array<{
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
hex: string;
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
hex: string;
|
||||
}>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// Define the Settings interface based on the API response structure
|
||||
// Define the Settings interface based on the API response structure
|
||||
export interface Settings {
|
||||
numScreens: number;
|
||||
invertedColor: boolean;
|
||||
|
@ -120,9 +120,9 @@ export interface Settings {
|
|||
gitTag: string;
|
||||
lastBuildTime: string;
|
||||
screens: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
actCurrencies: string[];
|
||||
availableCurrencies: string[];
|
||||
|
@ -130,13 +130,12 @@ export interface Settings {
|
|||
ceEndpoint: string;
|
||||
ceDisableSSL: boolean;
|
||||
dnd: {
|
||||
enabled: boolean;
|
||||
timeBasedEnabled: boolean;
|
||||
startHour: number;
|
||||
startMinute: number;
|
||||
endHour: number;
|
||||
endMinute: number;
|
||||
enabled: boolean;
|
||||
timeBasedEnabled: boolean;
|
||||
startHour: number;
|
||||
startMinute: number;
|
||||
endHour: number;
|
||||
endMinute: number;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
export const toTime = (secs: number) => {
|
||||
const hours = Math.floor(secs / (60 * 60));
|
||||
const hours = Math.floor(secs / (60 * 60));
|
||||
|
||||
const divisor_for_minutes = secs % (60 * 60);
|
||||
const minutes = Math.floor(divisor_for_minutes / 60);
|
||||
const divisor_for_minutes = secs % (60 * 60);
|
||||
const minutes = Math.floor(divisor_for_minutes / 60);
|
||||
|
||||
const divisor_for_seconds = divisor_for_minutes % 60;
|
||||
const seconds = Math.ceil(divisor_for_seconds);
|
||||
const divisor_for_seconds = divisor_for_minutes % 60;
|
||||
const seconds = Math.ceil(divisor_for_seconds);
|
||||
|
||||
const obj = {
|
||||
h: hours,
|
||||
m: minutes,
|
||||
s: seconds
|
||||
};
|
||||
return obj;
|
||||
const obj = {
|
||||
h: hours,
|
||||
m: minutes,
|
||||
s: seconds
|
||||
};
|
||||
return obj;
|
||||
};
|
||||
|
||||
export const toUptimestring = (secs: number): string => {
|
||||
const time = toTime(secs);
|
||||
const time = toTime(secs);
|
||||
|
||||
return `${time.h}h ${time.m}m ${time.s}s`;
|
||||
return `${time.h}h ${time.m}m ${time.s}s`;
|
||||
};
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
status.stopListening();
|
||||
}
|
||||
});
|
||||
export const prerender = true;
|
||||
</script>
|
||||
|
||||
<Navbar />
|
||||
|
|
2
src/routes/+layout.ts
Normal file
2
src/routes/+layout.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const prerender = true;
|
||||
export const ssr = false;
|
|
@ -1,21 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { settings, status } from '$lib/stores';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { ControlSection, StatusSection, SettingsSection } from '$lib/components';
|
||||
|
||||
|
||||
import { ControlSection, StatusSection } from '$lib/components';
|
||||
</script>
|
||||
|
||||
<div class=" mx-auto px-2 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<ControlSection />
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<StatusSection />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div>
|
||||
<ControlSection />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<StatusSection />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
let isLoaded = $state(false);
|
||||
let scalarApiReference;
|
||||
let scalarApiReference;
|
||||
|
||||
function initializeScalar() {
|
||||
// @ts-ignore - Scalar is loaded dynamically
|
||||
// @ts-expect-error - Scalar is loaded dynamically
|
||||
if (window.Scalar) {
|
||||
// @ts-ignore - Scalar is loaded dynamically
|
||||
// @ts-expect-error - Scalar is loaded dynamically
|
||||
scalarApiReference = window.Scalar.createApiReference('#app', {
|
||||
url: '/swagger.json',
|
||||
hideDarkModeToggle: true,
|
||||
hideClientButton: true,
|
||||
baseServerURL: baseUrl
|
||||
hideDarkModeToggle: true,
|
||||
hideClientButton: true,
|
||||
baseServerURL: baseUrl
|
||||
});
|
||||
isLoaded = true;
|
||||
} else {
|
||||
|
@ -34,33 +34,33 @@
|
|||
}
|
||||
|
||||
let darkMode = $state(false);
|
||||
let handler: (e: MediaQueryListEvent) => void;
|
||||
let mediaQuery: MediaQueryList;
|
||||
let handler: (e: MediaQueryListEvent) => void;
|
||||
let mediaQuery: MediaQueryList;
|
||||
onMount(() => {
|
||||
loadScalarScript();
|
||||
darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
handler = (e: MediaQueryListEvent) => {
|
||||
darkMode = e.matches;
|
||||
};
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
handler = (e: MediaQueryListEvent) => {
|
||||
darkMode = e.matches;
|
||||
};
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
|
||||
return () => {
|
||||
isLoaded = false;
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (mediaQuery) {
|
||||
mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
if (isLoaded) {
|
||||
document.querySelectorAll('style[data-scalar]').forEach(el => el.remove());
|
||||
onDestroy(() => {
|
||||
if (mediaQuery) {
|
||||
mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
if (isLoaded) {
|
||||
document.querySelectorAll('style[data-scalar]').forEach((el) => el.remove());
|
||||
|
||||
scalarApiReference.destroy();
|
||||
}
|
||||
});
|
||||
scalarApiReference.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { SettingsSection } from '$lib/components';
|
||||
import { SettingsSection } from '$lib/components';
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Settings</h1>
|
||||
<SettingsSection />
|
||||
</div>
|
||||
<h1 class="mb-4 text-2xl font-bold">Settings</h1>
|
||||
<SettingsSection />
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { SystemSection } from '$lib/components';
|
||||
import { SystemSection } from '$lib/components';
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">System Management</h1>
|
||||
<SystemSection />
|
||||
</div>
|
||||
<h1 class="mb-4 text-2xl font-bold">System Management</h1>
|
||||
<SystemSection />
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue