Initial commit

This commit is contained in:
Djuri 2024-03-17 18:35:26 +01:00
commit ba5370c7ca
36 changed files with 4122 additions and 0 deletions

80
src/routes/+layout.svelte Normal file
View file

@ -0,0 +1,80 @@
<script lang="ts">
import {
Collapse,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
Nav,
NavItem,
NavLink,
Navbar,
NavbarBrand
} from 'sveltestrap';
import { page } from '$app/stores';
import { locale, locales, isLoading } from 'svelte-i18n';
export const setLocale = (lang: string) => () => {
locale.set(lang);
localStorage.setItem('locale', lang);
};
export const getFlagEmoji = (languageCode: string): string | null => {
const flagMap: { [key: string]: string } = {
en: '🇬🇧', // English flag emoji
nl: '🇳🇱', // Dutch flag emoji
es: '🇪🇸' // Spanish flag emoji
};
// Convert the language code to lowercase for case-insensitive matching
const lowercaseCode = languageCode.toLowerCase();
// Check if the language code is in the flagMap
if (Object.prototype.hasOwnProperty.call(flagMap, lowercaseCode)) {
return flagMap[lowercaseCode];
} else {
// Return null for unsupported language codes
return null;
}
};
let languageNames = {};
locale.subscribe(() => {
let newLanguageNames = new Intl.DisplayNames([$locale], { type: 'language' });
for (let l: string of $locales) {
languageNames[l] = newLanguageNames.of(l);
}
});
</script>
<Navbar expand="md">
<NavbarBrand>OrangeBTClock</NavbarBrand>
<Collapse navbar expand="md">
<Nav class="me-auto" navbar>
<NavItem>
<NavLink href="/" active={$page.url.pathname === '/'}>Home</NavLink>
</NavItem>
<NavItem>
<NavLink href="/api" active={$page.url.pathname === '/api'}>API</NavLink>
</NavItem>
</Nav>
{#if !$isLoading}
<Dropdown id="nav-language-dropdown" inNavbar>
<DropdownToggle nav caret>{getFlagEmoji($locale)} {languageNames[$locale]}</DropdownToggle>
<DropdownMenu end>
{#each $locales as locale}
<DropdownItem on:click={setLocale(locale)}
>{getFlagEmoji(locale)} {languageNames[locale]}</DropdownItem
>
{/each}
</DropdownMenu>
</Dropdown>
{/if}
</Collapse>
</Navbar>
<!-- +layout.svelte -->
<slot />

19
src/routes/+layout.ts Normal file
View file

@ -0,0 +1,19 @@
import '$lib/style/app.scss';
import { browser } from '$app/environment';
import '$lib/i18n'; // Import to initialize. Important :)
import { locale, waitLocale } from 'svelte-i18n';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async () => {
if (browser && localStorage.getItem('locale')) {
locale.set(localStorage.getItem('locale'));
} else if (browser) {
locale.set(window.navigator.language);
}
await waitLocale();
};
export const prerender = true;
export const ssr = false;
export const csr = true;

79
src/routes/+page.svelte Normal file
View file

@ -0,0 +1,79 @@
<script lang="ts">
import { PUBLIC_BASE_URL } from '$lib/config';
import { Container, Row, Toast, ToastBody } from 'sveltestrap';
import Settings from './Settings.svelte';
import Status from './Status.svelte';
import Control from './Control.svelte';
import { onDestroy, onMount } from 'svelte';
import { writable } from 'svelte/store';
let settings = writable({
fgColor: '0'
});
let status = writable({});
let statusPollInterval;
const fetchSettingsData = () => {
fetch(PUBLIC_BASE_URL + `/api/settings`)
.then((res) => res.json())
.then((data) => {
settings.set(data);
});
};
const fetchStatusData = () => {
fetch(`${PUBLIC_BASE_URL}/api/status`)
.then((res) => res.json())
.then((data) => {
status.set(data);
});
};
onMount(() => {
fetchSettingsData();
fetchStatusData();
statusPollInterval = setInterval(fetchStatusData, 10000);
});
let toastIsOpen = false;
let toastColor = 'success';
let toastBody = '';
export const showToast = (event) => {
toastIsOpen = true;
toastColor = event.detail.color;
toastBody = event.detail.text;
};
onDestroy(() => {
clearInterval(statusPollInterval); // Cleanup interval when component is destroyed
});
</script>
<svelte:head>
<title>OrangeBTClock</title>
</svelte:head>
<Container fluid>
<Row>
<Control bind:settings></Control>
<Status bind:status></Status>
<Settings bind:settings on:showToast={showToast} on:formReset={fetchSettingsData}></Settings>
</Row>
</Container>
<div class="position-fixed bottom-0 end-0 p-2">
<div class="">
<Toast
isOpen={toastIsOpen}
class="me-1 bg-{toastColor}"
autohide
on:close={() => (toastIsOpen = false)}
>
<ToastBody>
{toastBody}
</ToastBody>
</Toast>
</div>
</div>

40
src/routes/Control.svelte Normal file
View file

@ -0,0 +1,40 @@
<script lang="ts">
import { PUBLIC_BASE_URL } from '$lib/config';
import { _ } from 'svelte-i18n';
import { Button, Card, CardBody, CardHeader, CardTitle, Col } from 'sveltestrap';
export let settings = {};
const restartClock = () => {
fetch(`${PUBLIC_BASE_URL}/api/restart`).catch(() => {});
};
const forceFullRefresh = () => {
fetch(`${PUBLIC_BASE_URL}/api/full_refresh`).catch(() => {});
};
</script>
<Col>
<Card>
<CardHeader>
<CardTitle>{$_('section.control.title', { default: 'Control' })}</CardTitle>
</CardHeader>
<CardBody>
<h3>{$_('section.control.systemInfo')}</h3>
<ul class="small system_info">
<li>{$_('section.control.version')}: {$settings.gitRev}</li>
<li>
{$_('section.control.buildTime')}: {new Date(
$settings.lastBuildTime * 1000
).toLocaleString()}
</li>
<li>IP: {$settings.ip}</li>
<li>{$_('section.control.hostname')}: {$settings.hostname}</li>
</ul>
<Button color="danger" id="restartBtn" on:click={restartClock}>{$_('button.restart')}</Button>
<Button color="warning" id="forceFullRefresh" on:click={forceFullRefresh}
>{$_('button.forceFullRefresh')}</Button
>
</CardBody>
</Card>
</Col>

View file

@ -0,0 +1,22 @@
<script lang="ts">
export let status = {};
</script>
<div class="screen-wrapper" id="screen-wrapper">
<div class="ar-wrapper">
<div class="oc-screen">
<div class="oc-row">
<div class="icon">{status.icon1}</div>
{status.row1}
</div>
<div class="oc-row">
<div class="icon">{status.icon2}</div>
{status.row2}
</div>
<div class="oc-row">
<div class="icon">{status.icon3}</div>
{status.row3}
</div>
</div>
</div>
</div>

261
src/routes/Settings.svelte Normal file
View file

@ -0,0 +1,261 @@
<script lang="ts">
import { PUBLIC_BASE_URL } from '$lib/config';
import { strftime } from '$lib/strftime';
import { createEventDispatcher } from 'svelte';
import { _ } from 'svelte-i18n';
import {
Button,
Card,
CardBody,
CardHeader,
CardTitle,
Col,
Form,
FormText,
Input,
InputGroup,
InputGroupText,
Label,
Row
} from 'sveltestrap';
export let settings;
const wifiTxPowerMap = new Map<string, number>([
['Default', 80],
['19.5dBm', 78], // 19.5dBm
['19dBm', 76], // 19dBm
['18.5dBm', 74], // 18.5dBm
['17dBm', 68], // 17dBm
['15dBm', 60], // 15dBm
['13dBm', 52], // 13dBm
['11dBm', 44], // 11dBm
['8.5dBm', 34], // 8.5dBm
['7dBm', 28], // 7dBm
['5dBm', 20] // 5dBm
]);
const rowOptions = new Map<string, number>([
['BLOCKHEIGHT', 0],
['MEMPOOL_FEES', 1],
['MEMPOOL_FEES_MEDIAN', 2],
['HALVING_COUNTDOWN', 10],
['SATSPERUNIT', 20],
['FIATPRICE', 30],
['MARKETCAP', 40],
['TIME', 99],
['DATE', 100]
]);
const currencyOptions = ['USD', 'EUR', 'GBP', 'YEN'];
const dispatch = createEventDispatcher();
const handleReset = (e: Event) => {
e.preventDefault();
dispatch('formReset');
};
const onSave = async (e: Event) => {
e.preventDefault();
let formSettings = $settings;
delete formSettings['gitRev'];
delete formSettings['ip'];
delete formSettings['lastBuildTime'];
await fetch(`${PUBLIC_BASE_URL}/api/json/settings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formSettings)
})
.then(() => {
dispatch('showToast', {
color: 'success',
text: $_('section.settings.settingsSaved')
});
})
.catch(() => {
dispatch('showToast', {
color: 'danger',
text: $_('section.settings.errorSavingSettings')
});
});
};
</script>
<Col>
<Card>
<CardHeader>
<CardTitle>{$_('section.settings.title', { default: 'Settings' })}</CardTitle>
</CardHeader>
<CardBody>
<Form on:submit={onSave}>
<Row>
<Label md={6} for="fgColor" size="sm"
>{$_('section.settings.row1', { default: 'Row 1' })}</Label
>
<Col md="6">
<Input
type="select"
bind:value={$settings.row1}
name="select"
id="row1"
bsSize="sm"
class="form-select-sm"
>
{#each rowOptions as [key, value]}
<option {value}>{$_(`section.lines.${key}`)}</option>
{/each}
</Input>
</Col>
</Row>
<Row>
<Label md={6} for="fgColor" size="sm"
>{$_('section.settings.row2', { default: 'Row 2' })}</Label
>
<Col md="6">
<Input
type="select"
bind:value={$settings.row2}
name="select"
id="row2"
bsSize="sm"
class="form-select-sm"
>
{#each rowOptions as [key, value]}
<option {value}>{$_(`section.lines.${key}`)}</option>
{/each}
</Input>
</Col>
</Row>
<Row>
<Label md={6} for="row3" size="sm"
>{$_('section.settings.row3', { default: 'Row 3' })}</Label
>
<Col md="6">
<Input
type="select"
bind:value={$settings.row3}
name="select"
id="row3"
bsSize="sm"
class="form-select-sm"
>
{#each rowOptions as [key, value]}
<option {value}>{$_(`section.lines.${key}`)}</option>
{/each}
</Input>
</Col>
</Row>
<Row>
<Label md={6} for="currency" size="sm"
>{$_('section.settings.currency', { default: 'Currency' })}</Label
>
<Col md="6">
<Input
type="select"
bind:value={$settings.currency}
name="select"
id="currency"
bsSize="sm"
class="form-select-sm"
>
{#each currencyOptions as value}
<option {value}>{value}</option>
{/each}
</Input>
</Col>
</Row>
<Row>
<Label md={6} for="timeFormat" size="sm"
>{$_('section.settings.timeFormat', { default: 'Time format' })}</Label
>
<Col md="6">
<Input
type="text"
bind:value={$settings.timeFormat}
name="timeFormat"
id="timeFormat"
bsSize="sm"
maxlength="16"
></Input>
<FormText>{$_('section.settings.preview')}: {strftime($settings.timeFormat)}</FormText>
</Col>
</Row>
<Row>
<Label md={6} for="dateFormat" size="sm"
>{$_('section.settings.dateFormat', { default: 'Date format' })}</Label
>
<Col md="6">
<Input
type="text"
bind:value={$settings.dateFormat}
name="dateFormat"
id="dateFormat"
bsSize="sm"
maxlength="16"
></Input>
<FormText>{$_('section.settings.preview')}: {strftime($settings.dateFormat)}</FormText>
</Col>
</Row>
<Row>
<Label md={6} for="tzOffset" size="sm">{$_('section.settings.timezoneOffset')}</Label>
<Col md="6">
<InputGroup size="sm">
<Input
type="number"
step="1"
name="tzOffset"
id="tzOffset"
bind:value={$settings.timeOffsetMin}
/>
<InputGroupText>{$_('time.minutes')}</InputGroupText>
</InputGroup>
<FormText>{$_('section.settings.tzOffsetHelpText')}</FormText>
</Col>
</Row>
<Row>
<Label md={6} for="wifiTxPower" size="sm"
>{$_('section.settings.wifiTxPower', { default: 'WiFi Tx Power' })}</Label
>
<Col md="6">
<Input
type="select"
bind:value={$settings.txPower}
name="select"
id="fgColor"
bsSize="sm"
class="form-select-sm"
>
{#each wifiTxPowerMap as [key, value]}
<option {value}>{key}</option>
{/each}
</Input>
<FormText>{$_('section.settings.wifiTxPowerText')}</FormText>
</Col>
</Row>
<Row>
<Label md={6} for="mempoolInstance" size="sm"
>{$_('section.settings.mempoolnstance')}</Label
>
<Col md="6">
<Input
type="text"
bind:value={$settings.mempoolInstance}
name="mempoolInstance"
id="mempoolInstance"
bsSize="sm"
></Input>
</Col>
</Row>
<Button on:click={handleReset} color="secondary">{$_('button.reset')}</Button>
<Button color="primary">{$_('button.save')}</Button>
</Form>
</CardBody>
</Card>
</Col>

86
src/routes/Status.svelte Normal file
View file

@ -0,0 +1,86 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { Card, CardBody, CardHeader, CardTitle, Col, Progress, Tooltip } from 'sveltestrap';
import Rendered from './Rendered.svelte';
import { writable } from 'svelte/store';
export let status: writable<object>;
const toTime = (secs: number) => {
var hours = Math.floor(secs / (60 * 60));
var divisor_for_minutes = secs % (60 * 60);
var minutes = Math.floor(divisor_for_minutes / 60);
var divisor_for_seconds = divisor_for_minutes % 60;
var seconds = Math.ceil(divisor_for_seconds);
var obj = {
h: hours,
m: minutes,
s: seconds
};
return obj;
};
const toUptimestring = (secs: number): string => {
let time = toTime(secs);
return `${time.h}h ${time.m}m ${time.s}s`;
};
let memoryFreePercent: number = 50;
let rssiPercent: number = 50;
let wifiStrengthColor: string = 'info';
status.subscribe((value: object) => {
memoryFreePercent = Math.round((value.espFreeHeap / value.espHeapSize) * 100);
rssiPercent = Math.round(((value.rssi + 120) / (-30 + 120)) * 100);
if (value.rssi > -55) {
wifiStrengthColor = 'success';
} else if (value.rssi < -87) {
wifiStrengthColor = 'warning';
} else {
wifiStrengthColor = 'info';
}
});
</script>
<Col>
<Card>
<CardHeader>
<CardTitle>{$_('section.status.title', { default: 'Status' })}</CardTitle>
</CardHeader>
<CardBody>
<section>
<Rendered status={$status}></Rendered>
</section>
<hr />
<Progress striped value={memoryFreePercent}>{memoryFreePercent}%</Progress>
<div class="d-flex justify-content-between">
<div>{$_('section.status.memoryFree')}</div>
<div>
{Math.round($status.espFreeHeap / 1024)} / {Math.round($status.espHeapSize / 1024)} KiB
</div>
</div>
<hr />
<Progress striped id="rssiBar" color={wifiStrengthColor} value={rssiPercent}
>{rssiPercent}%</Progress
>
<Tooltip target="rssiBar" placement="bottom">{$_('rssiBar.tooltip')}</Tooltip>
<div class="d-flex justify-content-between">
<div>{$_('section.status.wifiSignalStrength')}</div>
<div>
{$status.rssi} dBm
</div>
</div>
<hr />
{$_('section.status.uptime')}: {toUptimestring($status.espUptime)}
</CardBody>
</Card>
</Col>