Initial commit
This commit is contained in:
commit
ba5370c7ca
36 changed files with 4122 additions and 0 deletions
14
.eslintignore
Normal file
14
.eslintignore
Normal file
|
@ -0,0 +1,14 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
/dist
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
31
.eslintrc.cjs
Normal file
31
.eslintrc.cjs
Normal file
|
@ -0,0 +1,31 @@
|
|||
/** @type { import("eslint").Linter.Config } */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
130
.github/workflows/workflow.yml
vendored
Normal file
130
.github/workflows/workflow.yml
vendored
Normal file
|
@ -0,0 +1,130 @@
|
|||
name: WebUI CI
|
||||
|
||||
on: [push]
|
||||
|
||||
env:
|
||||
PUBLIC_BASE_URL: ''
|
||||
|
||||
jobs:
|
||||
check-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
all_changed_and_modified_files_count: ${{ steps.changed-files.outputs.all_changed_and_modified_files_count }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get changed files count
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v40.1.1
|
||||
with:
|
||||
files_ignore: 'doc/**,README.md,Dockerfile,.*'
|
||||
files_ignore_separator: ','
|
||||
- name: Print changed files count
|
||||
run: >
|
||||
echo "Changed files count: ${{
|
||||
steps.changed-files.outputs.all_changed_and_modified_files_count }}"
|
||||
|
||||
build:
|
||||
needs: check-changes
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.check-changes.outputs.all_changed_and_modified_files_count >= 1 }}
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: yarn
|
||||
cache-dependency-path: '**/yarn.lock'
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/node_modules
|
||||
key: ${{ runner.os }}-pio
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Get current date
|
||||
id: dateAndTime
|
||||
run: echo "dateAndTime=$(date +'%Y-%m-%d-%H:%M')" >> $GITHUB_OUTPUT
|
||||
- name: Install mklittlefs
|
||||
run: >
|
||||
git clone https://github.com/earlephilhower/mklittlefs.git /tmp/mklittlefs &&
|
||||
cd /tmp/mklittlefs &&
|
||||
git submodule update --init &&
|
||||
make dist
|
||||
- name: Install yarn
|
||||
run: yarn && yarn postinstall
|
||||
- name: Run linter
|
||||
run: yarn lint
|
||||
# - name: Run vitest tests
|
||||
# run: yarn vitest run
|
||||
# - name: Install Playwright Browsers
|
||||
# run: npx playwright install --with-deps
|
||||
# - name: Run Playwright tests
|
||||
# run: npx playwright test
|
||||
- name: Build WebUI
|
||||
run: yarn build
|
||||
- name: Get current block
|
||||
id: getBlockHeight
|
||||
run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
|
||||
- name: Write block height to file
|
||||
env:
|
||||
BLOCK_HEIGHT: ${{ steps.getBlockHeight.outputs.blockHeight }}
|
||||
run: mkdir -p output && echo "$BLOCK_HEIGHT" > output/version.txt
|
||||
- name: gzip build for LittleFS
|
||||
run: find dist -type f ! -name ".*" -exec sh -c 'mkdir -p "build_gz/$(dirname "${1#dist/}")" && gzip -k "$1" -c > "build_gz/${1#dist/}".gz' _ {} \;
|
||||
- name: Check GZipped directory size
|
||||
run: |
|
||||
# Set the threshold size in bytes
|
||||
THRESHOLD=409600
|
||||
|
||||
# Calculate the total size of files in the directory
|
||||
DIRECTORY_SIZE=$(du -b -s build_gz | awk '{print $1}')
|
||||
|
||||
# Fail the workflow if the size exceeds the threshold
|
||||
if [ "$DIRECTORY_SIZE" -gt "$THRESHOLD" ]; then
|
||||
echo "Directory size exceeds the threshold of $THRESHOLD bytes"
|
||||
exit 1
|
||||
else
|
||||
echo "Directory size is within the threshold"
|
||||
fi
|
||||
- name: Create tarball
|
||||
run: tar czf webui.tgz --strip-components=1 dist
|
||||
- name: Build LittleFS
|
||||
run: /tmp/mklittlefs/mklittlefs -c build_gz -s 409600 output/littlefs.bin
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
webui.tgz
|
||||
output/littlefs.bin
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
|
||||
commit: main
|
||||
name: release-${{ steps.getBlockHeight.outputs.blockHeight }}
|
||||
artifacts: 'output/littlefs.bin,webui.tgz'
|
||||
allowUpdates: true
|
||||
removeArtifacts: true
|
||||
makeLatest: true
|
||||
# - name: Pushes littlefs.bin to web flasher
|
||||
# id: push_directory
|
||||
# uses: cpina/github-action-push-to-another-repository@main
|
||||
# env:
|
||||
# SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||
# with:
|
||||
# source-directory: output/
|
||||
# target-directory: webui/
|
||||
# destination-github-username: 'btclock'
|
||||
# destination-repository-name: 'web-flasher'
|
||||
# target-branch: btclock
|
||||
# user-name: ${{github.actor}}
|
||||
# user-email: ${{github.actor}}@users.noreply.github.com
|
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
dist/
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
5
.prettierignore
Normal file
5
.prettierignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
dist/
|
8
.prettierrc
Normal file
8
.prettierrc
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
38
README.md
Normal file
38
README.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# BTClock WebUI
|
||||
|
||||
[![BTClock CI](https://github.com/btclock/oc-webui/actions/workflows/workflow.yml/badge.svg)](https://github.com/btclock/oc-webui/actions/workflows/workflow.yml)
|
||||
|
||||
The web user-interface for the OrangeBTClock, based on Svelte-kit and BTClock WebUI. It uses Bootstrap for the lay-out.
|
||||
|
||||
## Developing
|
||||
|
||||
After installed dependencies with `yarn`, start a development server:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
yarn dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of the WebUI:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
Make sure the postinstall script is ran, because otherwise the filenames are to long for the LittleFS filesystem.
|
||||
|
||||
## Deploying
|
||||
|
||||
To upload the firmware to the OC, you need to GZIP all the files. You can use the python script `gzip_build.py` for that.
|
||||
|
||||
Then you can make a `LittleFS.bin` with mklittlefs:
|
||||
|
||||
```bash
|
||||
mklittlefs -c build_gz -s 409600 littlefs.bin
|
||||
```
|
||||
|
||||
You can preview the production build with `yarn preview`.
|
48
package.json
Normal file
48
package.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "orangebtclock-webui",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"sass": "^1.72.0",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.3",
|
||||
"vitest": "^1.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fontsource/libre-franklin": "^5.0.17",
|
||||
"bootstrap": "^5.3.3",
|
||||
"date-fns": "^3.5.0",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"sveltestrap": "^5.11.3"
|
||||
}
|
||||
}
|
13
patches/@sveltejs+kit+2.5.4.patch
Normal file
13
patches/@sveltejs+kit+2.5.4.patch
Normal file
|
@ -0,0 +1,13 @@
|
|||
diff --git a/node_modules/@sveltejs/kit/src/exports/vite/index.js b/node_modules/@sveltejs/kit/src/exports/vite/index.js
|
||||
index e80fb78..4536af4 100644
|
||||
--- a/node_modules/@sveltejs/kit/src/exports/vite/index.js
|
||||
+++ b/node_modules/@sveltejs/kit/src/exports/vite/index.js
|
||||
@@ -637,7 +637,7 @@ async function kit({ svelte_config }) {
|
||||
format: 'esm',
|
||||
entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`,
|
||||
chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`,
|
||||
- assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
|
||||
+ assetFileNames: `${prefix}/assets/[hash][extname]`,
|
||||
hoistTransitiveImports: false,
|
||||
sourcemapIgnoreList
|
||||
},
|
12
playwright.config.ts
Normal file
12
playwright.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
export default config;
|
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
11
src/app.html
Normal file
11
src/app.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
7
src/index.test.ts
Normal file
7
src/index.test.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
5
src/lib/config.ts
Normal file
5
src/lib/config.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import * as publicEnv from '$env/static/public';
|
||||
|
||||
export const PUBLIC_BASE_URL: string = Object.hasOwn(publicEnv, 'PUBLIC_BASE_URL')
|
||||
? publicEnv.PUBLIC_BASE_URL
|
||||
: '';
|
17
src/lib/i18n/index.ts
Normal file
17
src/lib/i18n/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { init, register } from 'svelte-i18n';
|
||||
|
||||
const defaultLocale = 'en';
|
||||
|
||||
register('en', () => import('../locales/en.json'));
|
||||
register('nl', () => import('../locales/nl.json'));
|
||||
register('es', () => import('../locales/es.json'));
|
||||
|
||||
init({
|
||||
fallbackLocale: defaultLocale,
|
||||
initialLocale: browser
|
||||
? browser && localStorage.getItem('locale')
|
||||
? localStorage.getItem('locale')
|
||||
: window.navigator.language.slice(0, 2)
|
||||
: defaultLocale
|
||||
});
|
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
95
src/lib/locales/en.json
Normal file
95
src/lib/locales/en.json
Normal file
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"section": {
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"textColor": "Text color",
|
||||
"preview": "Preview",
|
||||
"backgroundColor": "Background color",
|
||||
"ledPowerOnTest": "LED power-on test",
|
||||
"ledFlashOnBlock": "LED flash on new block",
|
||||
"timePerScreen": "Time per screen",
|
||||
"ledBrightness": "LED brightness",
|
||||
"timezoneOffset": "Timezone offset",
|
||||
"timeBetweenPriceUpdates": "Time between price updates",
|
||||
"fullRefreshEvery": "Full refresh every",
|
||||
"mempoolnstance": "Mempool Instance",
|
||||
"hostnamePrefix": "Hostname prefix",
|
||||
"StealFocusOnNewBlock": "Steal focus on new block",
|
||||
"useBigCharsMcap": "Use big characters for market cap",
|
||||
"useBlkCountdown": "Blocks countdown for halving",
|
||||
"useSatsSymbol": "Use sats symbol",
|
||||
"suffixPrice": "Suffix price format",
|
||||
"disableLeds": "Disable all LEDs effects",
|
||||
"otaUpdates": "OTA updates",
|
||||
"enableMdns": "mDNS",
|
||||
"fetchEuroPrice": "Fetch € price",
|
||||
"shortAmountsWarning": "Short amounts might shorten lifespan of the displays",
|
||||
"tzOffsetHelpText": "A restart is required to apply TZ offset.",
|
||||
"screens": "Screens",
|
||||
"wifiTxPowerText": "In most cases this does not need to be set.",
|
||||
"wifiTxPower": "WiFi TX power",
|
||||
"settingsSaved": "Settings saved",
|
||||
"errorSavingSettings": "Error saving settings"
|
||||
},
|
||||
"lines": {
|
||||
"TIME": "Time",
|
||||
"DATE": "Date",
|
||||
"SATSPERUNIT": "Sats per unit (Moscow time)",
|
||||
"FIATPRICE": "Fiat Price",
|
||||
"BLOCKHEIGHT": "Block Height",
|
||||
"MEMPOOL_FEES": "Mempool Fees (summary)",
|
||||
"MEMPOOL_FEES_MEDIAN": "Mempool Fees (median)",
|
||||
"HALVING_COUNTDOWN": "Halving Countdown (blocks)",
|
||||
"MARKETCAP": "Market Cap"
|
||||
},
|
||||
"control": {
|
||||
"systemInfo": "System info",
|
||||
"version": "Version",
|
||||
"buildTime": "Build time",
|
||||
"ledColor": "LED color",
|
||||
"turnOff": "Turn off",
|
||||
"setColor": "Set color",
|
||||
"showText": "Show text",
|
||||
"text": "Text",
|
||||
"title": "Control",
|
||||
"hostname": "Hostname"
|
||||
},
|
||||
"status": {
|
||||
"title": "Status",
|
||||
"screenCycle": "Screen cycle",
|
||||
"memoryFree": "Memory free",
|
||||
"wsPriceConnection": "WS Price connection",
|
||||
"wsMempoolConnection": "WS Mempool.space connection",
|
||||
"fetchEuroNote": "If you use \"Fetch € price\" the WS Price connection will show ❌ since it uses another data source.",
|
||||
"uptime": "Uptime",
|
||||
"wifiSignalStrength": "WiFi Signal strength"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"black": "Black",
|
||||
"white": "White"
|
||||
},
|
||||
"time": {
|
||||
"minutes": "minutes",
|
||||
"seconds": "seconds"
|
||||
},
|
||||
"restartRequired": "restart required",
|
||||
"button": {
|
||||
"save": "Save",
|
||||
"reset": "Reset",
|
||||
"restart": "Restart",
|
||||
"forceFullRefresh": "Force full refresh"
|
||||
},
|
||||
"timer": {
|
||||
"running": "running",
|
||||
"stopped": "stopped"
|
||||
},
|
||||
"sections": {
|
||||
"control": {
|
||||
"keepSameColor": "Keep same color"
|
||||
}
|
||||
},
|
||||
"rssiBar": {
|
||||
"tooltip": "Values > -67 dBm are considered good. > -30 dBm is amazing"
|
||||
}
|
||||
}
|
82
src/lib/locales/es.json
Normal file
82
src/lib/locales/es.json
Normal file
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"section": {
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
"textColor": "Color de texto",
|
||||
"backgroundColor": "Color de fondo",
|
||||
"ledBrightness": "Brillo LED",
|
||||
"screens": "Pantallas",
|
||||
"shortAmountsWarning": "Pequeñas cantidades pueden acortar la vida útil de los displays",
|
||||
"fullRefreshEvery": "Actualización completa cada",
|
||||
"timePerScreen": "Tiempo por pantalla",
|
||||
"tzOffsetHelpText": "Es necesario reiniciar para aplicar la compensación.",
|
||||
"timezoneOffset": "Compensación de zona horaria",
|
||||
"StealFocusOnNewBlock": "Presta atención al nuevo bloque",
|
||||
"ledFlashOnBlock": "El LED parpadea con un bloque nuevo",
|
||||
"useBigCharsMcap": "Utilice caracteres grandes para la market cap",
|
||||
"useBlkCountdown": "Cuenta regresiva en bloques",
|
||||
"useSatsSymbol": "Usar símbolo sats",
|
||||
"fetchEuroPrice": "Obtener precio en €",
|
||||
"timeBetweenPriceUpdates": "Tiempo entre actualizaciones de precios",
|
||||
"ledPowerOnTest": "Prueba de encendido del LED",
|
||||
"enableMdns": "mDNS",
|
||||
"hostnamePrefix": "Prefijo de nombre de host",
|
||||
"mempoolnstance": "Instancia de Mempool",
|
||||
"suffixPrice": "Precio con sufijos",
|
||||
"disableLeds": "Desactivar efectos de LED",
|
||||
"otaUpdates": "Actualización por aire",
|
||||
"wifiTxPowerText": "En la mayoría de los casos no es necesario configurar esto.",
|
||||
"settingsSaved": "Configuración guardada",
|
||||
"errorSavingSettings": "Error al guardar la configuración"
|
||||
},
|
||||
"control": {
|
||||
"turnOff": "Apagar",
|
||||
"setColor": "Establecer el color",
|
||||
"version": "Versión",
|
||||
"ledColor": "color del LED",
|
||||
"systemInfo": "Info del sistema",
|
||||
"showText": "Mostrar texto",
|
||||
"text": "Texto",
|
||||
"title": "Control",
|
||||
"buildTime": "Tiempo de compilación",
|
||||
"hostname": "Nombre del host"
|
||||
},
|
||||
"status": {
|
||||
"memoryFree": "Memoria RAM libre",
|
||||
"wsPriceConnection": "Conexión WebSocket Precio",
|
||||
"wsMempoolConnection": "Conexión WebSocket Mempool.space",
|
||||
"screenCycle": "Ciclo de pantalla",
|
||||
"uptime": "Tiempo de funcionamiento",
|
||||
"fetchEuroNote": "Si utiliza \"Obtener precio en €\", la conexión de Precio WS mostrará ❌ ya que utiliza otra fuente de datos.",
|
||||
"title": "Estado",
|
||||
"wifiSignalStrength": "Fuerza de la señal WiFi"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"save": "Guardar",
|
||||
"reset": "Restaurar",
|
||||
"restart": "Reiniciar",
|
||||
"forceFullRefresh": "Forzar refresco"
|
||||
},
|
||||
"colors": {
|
||||
"black": "Negro",
|
||||
"white": "Blanco"
|
||||
},
|
||||
"restartRequired": "reinicio requerido",
|
||||
"time": {
|
||||
"minutes": "minutos",
|
||||
"seconds": "segundos"
|
||||
},
|
||||
"timer": {
|
||||
"running": "funcionando",
|
||||
"stopped": "detenido"
|
||||
},
|
||||
"sections": {
|
||||
"control": {
|
||||
"keepSameColor": "Mantén el mismo color"
|
||||
}
|
||||
},
|
||||
"rssiBar": {
|
||||
"tooltip": "Se consideran buenos valores > -67 dBm. > -30 dBm es increíble"
|
||||
}
|
||||
}
|
82
src/lib/locales/nl.json
Normal file
82
src/lib/locales/nl.json
Normal file
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"section": {
|
||||
"settings": {
|
||||
"title": "Instellingen",
|
||||
"textColor": "Tekstkleur",
|
||||
"backgroundColor": "Achtergrondkleur",
|
||||
"timeBetweenPriceUpdates": "Tijd tussen prijs updates",
|
||||
"timezoneOffset": "Tijdzone afwijking",
|
||||
"ledBrightness": "LED helderheid",
|
||||
"timePerScreen": "Tijd per scherm",
|
||||
"fullRefreshEvery": "Volledig verversen elke",
|
||||
"shortAmountsWarning": "Lage waardes verkorten mogelijk levensduur schermen",
|
||||
"tzOffsetHelpText": "Herstart nodig voor toepassen afwijking.",
|
||||
"enableMdns": "mDNS",
|
||||
"ledPowerOnTest": "LED test bij aanzetten",
|
||||
"StealFocusOnNewBlock": "Pak aandacht bij nieuw blok",
|
||||
"ledFlashOnBlock": "Knipper led bij nieuw blok",
|
||||
"useBigCharsMcap": "Gebruik grote tekens bij market cap",
|
||||
"useBlkCountdown": "Blocks aftellen voor halving",
|
||||
"useSatsSymbol": "Gebruik sats symbol",
|
||||
"fetchEuroPrice": "Toon € prijs",
|
||||
"screens": "Schermen",
|
||||
"hostnamePrefix": "Hostnaam voorvoegsel",
|
||||
"mempoolnstance": "Mempool instantie",
|
||||
"suffixPrice": "Achtervoegsel prijs formaat",
|
||||
"disableLeds": "Alle LEDs effecten uit",
|
||||
"otaUpdates": "OTA updates",
|
||||
"wifiTxPower": "WiFi TX power",
|
||||
"wifiTxPowerText": "Meestal hoeft dit niet aangepast te worden.",
|
||||
"settingsSaved": "Instellingen opgeslagen",
|
||||
"errorSavingSettings": "Fout bij opslaan instellingen"
|
||||
},
|
||||
"control": {
|
||||
"systemInfo": "Systeeminformatie",
|
||||
"version": "Versie",
|
||||
"buildTime": "Bouwtijd",
|
||||
"setColor": "Kleur instellen",
|
||||
"turnOff": "Uitzetten",
|
||||
"ledColor": "LED kleur",
|
||||
"showText": "Toon tekst",
|
||||
"text": "Tekst",
|
||||
"title": "Besturing"
|
||||
},
|
||||
"status": {
|
||||
"title": "Status",
|
||||
"memoryFree": "Geheugen vrij",
|
||||
"screenCycle": "Scherm cyclus",
|
||||
"wsPriceConnection": "WS Prijs verbinding",
|
||||
"wsMempoolConnection": "WS Mempool.space verbinding",
|
||||
"fetchEuroNote": "Wanneer je \"Toon € prijs\" aanzet, zal de prijsverbinding als ❌ verbroken getoond worden vanwege het gebruik van een andere bron.",
|
||||
"uptime": "Uptime",
|
||||
"wifiSignalStrength": "WiFi signaalsterkte"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"black": "Zwart",
|
||||
"white": "Wit"
|
||||
},
|
||||
"time": {
|
||||
"minutes": "minuten",
|
||||
"seconds": "seconden"
|
||||
},
|
||||
"restartRequired": "herstart nodig",
|
||||
"button": {
|
||||
"save": "Opslaan",
|
||||
"reset": "Herstel",
|
||||
"restart": "Herstart",
|
||||
"forceFullRefresh": "Forceer scherm verversen"
|
||||
},
|
||||
"timer": {
|
||||
"running": "actief",
|
||||
"stopped": "gestopt"
|
||||
},
|
||||
"sections": {
|
||||
"control": {
|
||||
"keepSameColor": "Behoud zelfde kleur"
|
||||
}
|
||||
},
|
||||
"rssiBar": {
|
||||
"tooltip": "Waarden > -67 dBm zijn goed. > -30 dBm is verbazingwekkend"
|
||||
}
|
||||
}
|
68
src/lib/strftime.ts
Normal file
68
src/lib/strftime.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { format, parse, isValid } from 'date-fns';
|
||||
|
||||
export function strftime(formatString: string, dateString?: string): string {
|
||||
const placeholders: { [key: string]: string } = {
|
||||
'%a': 'EEE',
|
||||
'%A': 'EEEE',
|
||||
'%b': 'MMM',
|
||||
'%B': 'MMMM',
|
||||
'%c': 'EEE MMM dd HH:mm:ss yyyy',
|
||||
'%C': '',
|
||||
'%d': 'dd',
|
||||
'%D': 'MM/dd/yy',
|
||||
'%e': 'd',
|
||||
'%F': 'yyyy-MM-dd',
|
||||
'%g': '',
|
||||
'%G': '',
|
||||
'%h': 'MMM',
|
||||
'%H': 'HH',
|
||||
'%I': 'hh',
|
||||
'%j': 'DDD',
|
||||
'%k': 'H',
|
||||
'%l': 'h',
|
||||
'%m': 'MM',
|
||||
'%M': 'mm',
|
||||
'%n': '\n',
|
||||
'%p': 'a',
|
||||
'%P': 'a',
|
||||
'%r': 'hh:mm:ss a',
|
||||
'%R': 'HH:mm',
|
||||
'%s': '',
|
||||
'%S': 'ss',
|
||||
'%t': '\t',
|
||||
'%T': 'HH:mm:ss',
|
||||
'%u': 'E',
|
||||
'%U': '',
|
||||
'%V': '',
|
||||
'%w': 'e',
|
||||
'%W': '',
|
||||
'%x': 'MM/dd/yy',
|
||||
'%X': 'HH:mm:ss',
|
||||
'%y': 'yy',
|
||||
'%Y': 'yyyy',
|
||||
'%z': 'xxx',
|
||||
'%Z': 'zzz',
|
||||
'%%': '%'
|
||||
};
|
||||
|
||||
let convertedFormatString = formatString;
|
||||
|
||||
if (!convertedFormatString) {
|
||||
return '';
|
||||
}
|
||||
|
||||
for (const placeholder in placeholders) {
|
||||
if (Object.prototype.hasOwnProperty.call(placeholders, placeholder)) {
|
||||
convertedFormatString = convertedFormatString.replace(placeholder, placeholders[placeholder]);
|
||||
}
|
||||
}
|
||||
|
||||
let parsedDate;
|
||||
if (dateString && isValid(parse(dateString, 'yyyy-MM-dd', new Date()))) {
|
||||
parsedDate = parse(dateString, 'yyyy-MM-dd', new Date());
|
||||
} else {
|
||||
parsedDate = new Date();
|
||||
}
|
||||
|
||||
return format(parsedDate, convertedFormatString);
|
||||
}
|
195
src/lib/style/app.scss
Normal file
195
src/lib/style/app.scss
Normal file
|
@ -0,0 +1,195 @@
|
|||
@import '../node_modules/bootstrap/scss/functions';
|
||||
@import '../node_modules/bootstrap/scss/variables';
|
||||
@import '../node_modules/bootstrap/scss/variables-dark';
|
||||
|
||||
@import '@fontsource/libre-franklin';
|
||||
|
||||
@font-face {
|
||||
font-family: 'orangeclock-icons';
|
||||
src:
|
||||
url('/subset-orangeclock-icons.woff2') format('woff2'),
|
||||
url('/subset-orangeclock-icons.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-family: 'orangeclock-icons';
|
||||
}
|
||||
|
||||
$form-range-track-bg: #fff;
|
||||
$color-mode-type: media-query;
|
||||
$font-family-base: 'Ubuntu';
|
||||
$font-size-base: 0.9rem;
|
||||
//$font-size-sm: $font-size-base * .875 !default;
|
||||
//$form-label-font-size: $font-size-base * .575 !default;
|
||||
//$input-btn-font-size-sm: 0.4rem;
|
||||
//$form-label-font-size: 0.4rem;
|
||||
$input-font-size-sm: $font-size-base * 0.875;
|
||||
|
||||
// $border-radius: .675rem;
|
||||
|
||||
@import '../node_modules/bootstrap/scss/mixins';
|
||||
@import '../node_modules/bootstrap/scss/maps';
|
||||
@import '../node_modules/bootstrap/scss/utilities';
|
||||
|
||||
@import '../node_modules/bootstrap/scss/root';
|
||||
@import '../node_modules/bootstrap/scss/reboot';
|
||||
@import '../node_modules/bootstrap/scss/type';
|
||||
@import '../node_modules/bootstrap/scss/containers';
|
||||
@import '../node_modules/bootstrap/scss/grid';
|
||||
@import '../node_modules/bootstrap/scss/forms';
|
||||
@import '../node_modules/bootstrap/scss/buttons';
|
||||
@import '../node_modules/bootstrap/scss/button-group';
|
||||
@import '../node_modules/bootstrap/scss/pagination';
|
||||
|
||||
@import '../node_modules/bootstrap/scss/dropdown';
|
||||
|
||||
@import '../node_modules/bootstrap/scss/navbar';
|
||||
@import '../node_modules/bootstrap/scss/nav';
|
||||
@import '../node_modules/bootstrap/scss/card';
|
||||
@import '../node_modules/bootstrap/scss/progress';
|
||||
@import '../node_modules/bootstrap/scss/tooltip';
|
||||
@import '../node_modules/bootstrap/scss/toasts';
|
||||
|
||||
@import '../node_modules/bootstrap/scss/helpers';
|
||||
@import '../node_modules/bootstrap/scss/utilities/api';
|
||||
|
||||
@include media-breakpoint-down(xl) {
|
||||
html {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
button.btn,
|
||||
input[type='button'].btn,
|
||||
input[type='submit'].btn,
|
||||
input[type='reset'].btn {
|
||||
@include button-size(
|
||||
$btn-padding-y-sm,
|
||||
$btn-padding-x-sm,
|
||||
$font-size-sm,
|
||||
$btn-border-radius-sm
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
html {
|
||||
font-size: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.splitText div:first-child::after {
|
||||
display: block;
|
||||
content: '';
|
||||
margin-top: 0px;
|
||||
border-bottom: 2px solid;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
#screen-wrapper {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
width: 75%; /* Container takes up full width */
|
||||
max-width: 75%; /* Ensure it doesn't exceed the available width */
|
||||
position: relative; /* Establishes the containing block for absolute positioning */
|
||||
// background-color: lightgray; /* Optional: Just for visualization */
|
||||
}
|
||||
|
||||
.ar-wrapper {
|
||||
// padding-top: calc(122 / 250 * 100%); /* Aspect ratio: height / width * 100% */
|
||||
// position: relative;
|
||||
// width: 100%; /* Ensure the wrapper takes up full width */
|
||||
padding-top: calc(122 / 250 * 100%); /* Aspect ratio: height / width * 100% */
|
||||
position: relative;
|
||||
width: 100%; /* Ensure the wrapper takes up full width */
|
||||
}
|
||||
|
||||
.oc-screen {
|
||||
position: absolute; /* Position content absolutely within the wrapper */
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: black;
|
||||
|
||||
// border: 1px solid darkgray;
|
||||
background: lightgrey;
|
||||
// border-radius: 5px;
|
||||
// padding: 10px;
|
||||
// margin: 0 auto;
|
||||
// display: flex;
|
||||
flex-direction: column;
|
||||
// flex-wrap: nowrap;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// align-content: center;
|
||||
font-family: 'Libre Franklin';
|
||||
font-weight: 600;
|
||||
.oc-row .icon {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.oc-row:nth-child(1) {
|
||||
font-size: 1.5vw;
|
||||
display: block;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto;
|
||||
align-self: auto;
|
||||
order: 0;
|
||||
}
|
||||
|
||||
.oc-row:nth-child(2) {
|
||||
font-size: 3vw;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto;
|
||||
align-self: auto;
|
||||
order: 0;
|
||||
}
|
||||
|
||||
.oc-row:nth-child(3) {
|
||||
font-size: 2vw;
|
||||
display: block;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
flex-basis: auto;
|
||||
align-self: auto;
|
||||
order: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#customText {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.system_info {
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
}
|
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>
|
BIN
static/subset-orangeclock-icons.woff
Normal file
BIN
static/subset-orangeclock-icons.woff
Normal file
Binary file not shown.
BIN
static/subset-orangeclock-icons.woff2
Normal file
BIN
static/subset-orangeclock-icons.woff2
Normal file
Binary file not shown.
31
svelte.config.js
Normal file
31
svelte.config.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import preprocess from 'svelte-preprocess';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: preprocess({}),
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFilenames: '[hash]'
|
||||
}
|
||||
}
|
||||
},
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter({
|
||||
pages: 'dist',
|
||||
assets: 'dist',
|
||||
fallback: 'bundle.html',
|
||||
precompress: false,
|
||||
strict: true
|
||||
}),
|
||||
appDir: 'build'
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
6
tests/test.ts
Normal file
6
tests/test.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('index page has expected h1', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
|
||||
});
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
19
vite.config.ts
Normal file
19
vite.config.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
build: {
|
||||
minify: true,
|
||||
cssCodeSplit: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: () => 'app',
|
||||
assetFileNames: '[name][extname]'
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
}
|
||||
});
|
Loading…
Reference in a new issue