Initial commit
This commit is contained in:
commit
ba5370c7ca
36 changed files with 4122 additions and 0 deletions
80
src/routes/+layout.svelte
Normal file
80
src/routes/+layout.svelte
Normal 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
19
src/routes/+layout.ts
Normal 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
79
src/routes/+page.svelte
Normal 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
40
src/routes/Control.svelte
Normal 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>
|
22
src/routes/Rendered.svelte
Normal file
22
src/routes/Rendered.svelte
Normal 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
261
src/routes/Settings.svelte
Normal 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
86
src/routes/Status.svelte
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue