Compare commits
10 Commits
4ba7f21af8
...
8fcab2701e
Author | SHA1 | Date | |
---|---|---|---|
8fcab2701e | |||
3b1ec0ac3f | |||
e42d5241c0 | |||
5670557392 | |||
4a2644c071 | |||
c6242bb109 | |||
6e7aa7464e | |||
acccc9f458 | |||
025d560f89 | |||
1ada31a00c |
78
.github/workflows/docker-image.yml
vendored
78
.github/workflows/docker-image.yml
vendored
@ -4,66 +4,94 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request:
|
tags:
|
||||||
branches:
|
- 'v*'
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_multi_arch_image:
|
build_multi_arch_image:
|
||||||
name: Build multi-arch Docker image.
|
name: Build multi-arch Docker image.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
|
DH_REGISTRY: docker.io
|
||||||
GH_REGISTRY: ghcr.io
|
GH_REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
REPOSITORY: ${{ github.event.repository.name }}
|
REPOSITORY: ${{ github.event.repository.name }}
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set environment variables
|
|
||||||
run: echo "GIT_BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
registry: ${{ env.DH_REGISTRY }}
|
||||||
|
username: ${{ env.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.GH_REGISTRY }}
|
registry: ${{ env.GH_REGISTRY }}
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Set version variable
|
||||||
|
run: |
|
||||||
|
sed -i "s/const Version.*/const Version: string = '${{ github.head_ref }}.${{ github.sha }}';/" src/version.ts
|
||||||
|
|
||||||
|
- name: Extract metadata for release Docker image
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ env.DOCKER_USERNAME }}/${{ env.REPOSITORY }}
|
||||||
|
${{ env.GH_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
|
- name: Extract metadata for develop Docker image
|
||||||
|
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||||
|
id: meta-develop
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.GH_REGISTRY }}/${{ env.IMAGE_NAME }}
|
${{ env.GH_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
- name: Build and push master
|
|
||||||
if: ${{ github.ref == 'refs/heads/master' }}
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
push: ${{ github.ref == 'refs/heads/master' && github.event_name == 'push' }}
|
|
||||||
tags: |
|
tags: |
|
||||||
${{ steps.meta.outputs.tags }}
|
type=ref,event=branch
|
||||||
${{ secrets.DOCKER_USERNAME }}/${{ env.REPOSITORY }}
|
type=ref,event=pr
|
||||||
|
|
||||||
|
- name: Build and push release Docker image
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
target: deploy
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
build-args: |
|
|
||||||
"COMMIT_SHA=${{ github.sha }}"
|
|
||||||
"BRANCH=${{ env.GIT_BRANCH }}"
|
|
||||||
cache-from: type=gha,scope=${{ github.workflow }}
|
cache-from: type=gha,scope=${{ github.workflow }}
|
||||||
cache-to: type=gha,mode=max,scope=${{ github.workflow }}
|
cache-to: type=gha,mode=max,scope=${{ github.workflow }}
|
||||||
|
|
||||||
|
- name: Build and push develop Docker image
|
||||||
|
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
target: deploy
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta-develop.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-develop.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
FROM public.ecr.aws/docker/library/node:16-alpine3.14
|
FROM node:lts-alpine AS base
|
||||||
|
FROM base AS deploy
|
||||||
|
|
||||||
WORKDIR /usr/src/showpiece
|
WORKDIR /usr/src/showpiece
|
||||||
|
|
||||||
@ -13,4 +14,4 @@ RUN npm run build
|
|||||||
USER node
|
USER node
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
CMD [ "node", "index.js"]
|
CMD [ "npm", "run", "start"]
|
||||||
|
@ -5,7 +5,6 @@ body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
cursor: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
var modulebtns = document.querySelectorAll('#modules button');
|
var modulebtns = document.querySelectorAll('#modules button.module-btn');
|
||||||
|
var expandbtns = document.querySelectorAll('#modules button.expand-btn');
|
||||||
var urltxt = document.getElementById('urltxt');
|
var urltxt = document.getElementById('urltxt');
|
||||||
var submitbtn = document.getElementById('submitbtn');
|
var submitbtn = document.getElementById('submitbtn');
|
||||||
|
|
||||||
@ -25,20 +26,47 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
modulebtns.forEach(function (btn) {
|
function loadModule(event) {
|
||||||
btn.addEventListener('click', function (e) {
|
var mname = event.currentTarget.querySelector('.module-path').innerText;
|
||||||
let mname = btn.querySelector('.module-name').innerText;
|
var inputs = event.currentTarget.parentElement.querySelectorAll('form input');
|
||||||
fetch('api/load', {
|
var params = {};
|
||||||
method: 'POST',
|
inputs.forEach(el => {
|
||||||
cache: 'no-cache',
|
if (el.type === 'checkbox') {
|
||||||
headers: {
|
params[el.name] = el.checked;
|
||||||
'Content-Type': 'application/json'
|
}
|
||||||
},
|
else {
|
||||||
body: JSON.stringify({
|
params[el.name] = el.value;
|
||||||
type: 'module',
|
}
|
||||||
body: mname
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
fetch('api/load', {
|
||||||
|
method: 'POST',
|
||||||
|
cache: 'no-cache',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: 'module',
|
||||||
|
body: `${mname}/?${new URLSearchParams(params).toString()}`
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
modulebtns.forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', loadModule);
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleExpand(event) {
|
||||||
|
const CLPSD = 'collapsed';
|
||||||
|
var parent = event.currentTarget.parentElement;
|
||||||
|
if (parent.classList.contains(CLPSD)) {
|
||||||
|
parent.classList.remove(CLPSD);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parent.classList.add(CLPSD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expandbtns.forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', toggleExpand);
|
||||||
});
|
});
|
||||||
});
|
});
|
29
index.js
29
index.js
@ -1,29 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const fsp = fs.promises;
|
|
||||||
const express = require('express');
|
|
||||||
const json5 = require('json5');
|
|
||||||
const client = require('./client');
|
|
||||||
const server = require('./server');
|
|
||||||
|
|
||||||
|
|
||||||
const configpath = process.env.CONFIG || path.join('config', 'config.json5');
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const configfile = json5.parse(await fsp.readFile(configpath));
|
|
||||||
const port = process.env.PORT || configfile.port || 8080;
|
|
||||||
const config ={
|
|
||||||
port,
|
|
||||||
controlport: process.env.CONTROLPORT || configfile.controlport || port,
|
|
||||||
weather: configfile.weather
|
|
||||||
};
|
|
||||||
const c = new client(config);
|
|
||||||
const s = new server(config);
|
|
||||||
const app = (config.clientonly) ? express() : s.middleware();
|
|
||||||
|
|
||||||
app.set('trust proxy', 1);
|
|
||||||
app.use('/', c.middleware());
|
|
||||||
app.use('/assets/', express.static('assets'));
|
|
||||||
|
|
||||||
app.listen(config.port, () => console.log(`Showpiece listening to port ${config.port}`));
|
|
||||||
})().catch(err => console.error(err));
|
|
13
modules/analog/module.json
Normal file
13
modules/analog/module.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "Swanky Analog",
|
||||||
|
"description": "This face has a pulse",
|
||||||
|
"icon": "icon.png",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Color",
|
||||||
|
"param": "color",
|
||||||
|
"type": "color",
|
||||||
|
"default": "#03A9F4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
modules/contrast/module.json
Normal file
5
modules/contrast/module.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "Contrast",
|
||||||
|
"description": "This design is all business.",
|
||||||
|
"icon": "icon.png"
|
||||||
|
}
|
108
modules/weather/clock.js
Normal file
108
modules/weather/clock.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const minute = 60000;
|
||||||
|
const weatherInterval = 180000;
|
||||||
|
const time = document.getElementById('time');
|
||||||
|
const tempcontainer = document.getElementById('temps');
|
||||||
|
const temp = document.getElementById('temp');
|
||||||
|
const high = document.getElementById('high');
|
||||||
|
const low = document.getElementById('low');
|
||||||
|
const weatherdiv = document.getElementById('weatherdiv');
|
||||||
|
var sun = null;
|
||||||
|
var metric = true;
|
||||||
|
var clock24h = true;
|
||||||
|
var currentWeather = null;
|
||||||
|
|
||||||
|
function getParams() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
var string = urlParams.get('metric');
|
||||||
|
metric = string === null || string === 'true';
|
||||||
|
string = urlParams.get('24h');
|
||||||
|
clock24h = string === null || string === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHours(hours, mod) {
|
||||||
|
const h = hours % mod;
|
||||||
|
if (h === 0) {
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTime() {
|
||||||
|
var d = new Date();
|
||||||
|
var hours = d.getHours();
|
||||||
|
var suffix = '';
|
||||||
|
if (!clock24h) {
|
||||||
|
suffix = ' ' + (Math.floor(hours / 12) ? 'PM' : 'AM');
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyElement(time).appendChild(document.createTextNode(`${('' + getHours(hours, clock24h ? 24 : 12)).padStart(clock24h ? 2 : 1, '0')}:${('' + d.getMinutes()).padStart(2, '0')}${suffix}`));
|
||||||
|
resizeTemp();
|
||||||
|
setDayNight(d);
|
||||||
|
setTimeout(updateTime, minute - (d.getTime() % minute));
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyElement(e) {
|
||||||
|
while (e.firstChild) {
|
||||||
|
e.removeChild(e.lastChild);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTemp(t) {
|
||||||
|
if (metric) {
|
||||||
|
t = (t - 32) / 1.8;
|
||||||
|
}
|
||||||
|
return `${Math.round(t)}°`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeTemp() {
|
||||||
|
tempcontainer.style.width = `${time.offsetWidth}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDayNight(d = new Date()) {
|
||||||
|
if (sun) {
|
||||||
|
document.body.classList.forEach(function (c) {
|
||||||
|
document.body.classList.remove(c);
|
||||||
|
});
|
||||||
|
document.body.classList.add((d >= sun.sunrise && d < sun.sunset) ? 'day' : 'night');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWeather() {
|
||||||
|
fetch('/api/all-weather')
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(weather => {
|
||||||
|
currentWeather = weather.list[0];
|
||||||
|
weatherdiv.classList.forEach(function (c) {
|
||||||
|
weatherdiv.classList.remove(c);
|
||||||
|
});
|
||||||
|
currentWeather.weather.forEach(function (w) {
|
||||||
|
weatherdiv.classList.add(w.main);
|
||||||
|
});
|
||||||
|
emptyElement(temp).appendChild(document.createTextNode(formatTemp(currentWeather.main.temp)));
|
||||||
|
emptyElement(high).appendChild(document.createTextNode(formatTemp(currentWeather.main.temp_max) + '⭎'));
|
||||||
|
emptyElement(low).appendChild(document.createTextNode(formatTemp(currentWeather.main.temp_min) + '⭏'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSun() {
|
||||||
|
fetch('/api/sun')
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(s => {
|
||||||
|
sun = {
|
||||||
|
sunrise: new Date(s.sunrise),
|
||||||
|
sunset: new Date(s.sunset)
|
||||||
|
}
|
||||||
|
setDayNight();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getParams();
|
||||||
|
getSun();
|
||||||
|
updateTime();
|
||||||
|
getWeather();
|
||||||
|
window.addEventListener('resize', resizeTemp);
|
||||||
|
setInterval(getWeather, weatherInterval);
|
||||||
|
setInterval(getSun, weatherInterval);
|
||||||
|
});
|
BIN
modules/weather/icon.png
Normal file
BIN
modules/weather/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
32
modules/weather/index.html
Normal file
32
modules/weather/index.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Weather Clock</title>
|
||||||
|
<link rel="stylesheet" href="/assets/css/reset.css">
|
||||||
|
<link rel="stylesheet" href="style.css?v1">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script src="clock.js"></script>
|
||||||
|
<script src="https://pagecdn.io/lib/pixi/6.2.0/browser/pixi.min.js" crossorigin="anonymous" ></script>
|
||||||
|
<script src="weather-fx.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="weatherdiv">
|
||||||
|
<div id="container">
|
||||||
|
<div class="center">
|
||||||
|
<span id="time">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="temps">
|
||||||
|
<span id="temp"></span>
|
||||||
|
<div class="f-right">
|
||||||
|
<span id="high"></span>
|
||||||
|
<span id="low"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
19
modules/weather/module.json
Normal file
19
modules/weather/module.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "Weather Glass",
|
||||||
|
"description": "See live weather effects",
|
||||||
|
"icon": "icon.png",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Metric Units",
|
||||||
|
"param": "metric",
|
||||||
|
"type": "bool",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "24 Hour Clock",
|
||||||
|
"param": "24h",
|
||||||
|
"type": "bool",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
71
modules/weather/style.css
Normal file
71
modules/weather/style.css
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Fjalla+One&family=Inconsolata:wght@200&display=swap');
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Fjalla One', sans-serif;
|
||||||
|
font-family: 'Inconsolata', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #0277BD;
|
||||||
|
transition: background-color 10s;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.day {
|
||||||
|
background-color: #039BE5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.night {
|
||||||
|
background-color: #311B92;
|
||||||
|
}
|
||||||
|
|
||||||
|
body * {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
margin: auto;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 21vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#time {
|
||||||
|
font-size: 20vh;
|
||||||
|
line-height: 17.5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#temps {
|
||||||
|
margin: auto;
|
||||||
|
font-size: 4vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weatherdiv{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas{
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
136
modules/weather/weather-fx.js
Normal file
136
modules/weather/weather-fx.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const weatherdiv = document.getElementById('weatherdiv');
|
||||||
|
const pixi = new PIXI.Application({ transparent: true });
|
||||||
|
weatherdiv.appendChild(pixi.view);
|
||||||
|
const sprites = [];
|
||||||
|
const snowflakeG = new PIXI.Graphics();
|
||||||
|
snowflakeG.lineStyle(0);
|
||||||
|
snowflakeG.beginFill(0xFFFFFF, 1);
|
||||||
|
snowflakeG.drawCircle(0, 0, 3);
|
||||||
|
snowflakeG.endFill();
|
||||||
|
const drizzleG = new PIXI.Graphics();
|
||||||
|
drizzleG.lineStyle(3, 0xFFFFFF, .6);
|
||||||
|
drizzleG.lineTo(-3, 15);
|
||||||
|
drizzleG.closePath();
|
||||||
|
const rainG = new PIXI.Graphics();
|
||||||
|
rainG.lineStyle(3, 0xFFFFFF, .7);
|
||||||
|
rainG.lineTo(-7.5, 37.5);
|
||||||
|
rainG.closePath();
|
||||||
|
const fogG = new PIXI.Graphics();
|
||||||
|
fogG.lineStyle(0);
|
||||||
|
fogG.beginFill(0x607D8B, .5);
|
||||||
|
fogG.drawCircle(0, 0, 50);
|
||||||
|
fogG.endFill();
|
||||||
|
const cloudMaxOpacity = .8;
|
||||||
|
const cloudG = new PIXI.Graphics();
|
||||||
|
cloudG.lineStyle(0);
|
||||||
|
cloudG.beginFill(0xECEFF1, 1);
|
||||||
|
cloudG.drawCircle(0, 0, 50);
|
||||||
|
cloudG.endFill();
|
||||||
|
var lastFrame = (new Date()).getTime();
|
||||||
|
var lastGen = {
|
||||||
|
snow: lastFrame,
|
||||||
|
rain: lastFrame,
|
||||||
|
fog: lastFrame,
|
||||||
|
cloud: lastFrame
|
||||||
|
};
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
pixi.ticker.add((delta) => {
|
||||||
|
count++;
|
||||||
|
let t = (new Date()).getTime();
|
||||||
|
let weather = weatherdiv.classList;
|
||||||
|
if (weather.contains('Snow')) {
|
||||||
|
if (t - lastGen.snow >= 100000 / pixi.view.width) {
|
||||||
|
let g = new PIXI.Graphics(snowflakeG.geometry);
|
||||||
|
pixi.stage.addChild(g);
|
||||||
|
sprites.push({
|
||||||
|
type: 'snow',
|
||||||
|
x: Math.random() * pixi.view.width,
|
||||||
|
y: -10,
|
||||||
|
speedx: -.4,
|
||||||
|
speedy: 2,
|
||||||
|
graphic: g
|
||||||
|
});
|
||||||
|
lastGen.snow = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (weather.contains('Rain') || weather.contains('Drizzle') || weather.contains('Thunderstorm')) {
|
||||||
|
let drizzle = weather.contains('Drizzle');
|
||||||
|
if (t - lastGen.rain >= (drizzle ? 50000 : 20000) / pixi.view.width) {
|
||||||
|
let g = new PIXI.Graphics((drizzle ? drizzleG : rainG).geometry);
|
||||||
|
pixi.stage.addChild(g);
|
||||||
|
let variance = Math.random() / 2 + 1;
|
||||||
|
sprites.push({
|
||||||
|
type: 'rain',
|
||||||
|
x: Math.random() * pixi.view.width,
|
||||||
|
y: -20,
|
||||||
|
mag: drizzle ? 1.5 : 3,
|
||||||
|
speedx: -1.6 * (drizzle ? 1 : 3) * variance,
|
||||||
|
speedy: 8 * (drizzle ? 1 : 3) * variance,
|
||||||
|
graphic: g
|
||||||
|
});
|
||||||
|
lastGen.rain = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (weather.contains('Clouds') || weather.contains('Fog') || weather.contains('Mist') || weather.contains('Haze') || weather.contains('Dust') || weather.contains('Smoke')) {
|
||||||
|
let fog = weather.contains('Fog') || weather.contains('Haze') || weather.contains('Dust');
|
||||||
|
if (t - lastGen.cloud >= 300000 / pixi.view.width && true) {
|
||||||
|
let g = new PIXI.Graphics((fog ? fogG : cloudG).geometry);
|
||||||
|
g.alpha = 0;
|
||||||
|
pixi.stage.addChild(g);
|
||||||
|
let variance = Math.random() * .8 + .3;
|
||||||
|
sprites.push({
|
||||||
|
type: 'cloud',
|
||||||
|
x: Math.random() * pixi.view.width,
|
||||||
|
y: Math.random() * (pixi.view.height * .20) + 30,
|
||||||
|
speedx: .5 * variance * (Math.random() >= .5 ? 1 : -1),
|
||||||
|
speedy: 0,
|
||||||
|
dying: false,
|
||||||
|
countdown: 0,
|
||||||
|
graphic: g
|
||||||
|
});
|
||||||
|
lastGen.cloud = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sprites.forEach(function (s, index) {
|
||||||
|
s.graphic.x = s.x;
|
||||||
|
s.graphic.y = s.y;
|
||||||
|
if (s.type === 'snow') {
|
||||||
|
s.speedx = Math.min(Math.max(s.speedx + (Math.floor(Math.random() * 2) ? .04 : -.04), -1), 1);
|
||||||
|
}
|
||||||
|
else if (s.type === 'cloud') {
|
||||||
|
if (s.dying) {
|
||||||
|
s.graphic.alpha = Math.min((s.countdown -= delta / 100), cloudMaxOpacity);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s.graphic.alpha = Math.min((s.countdown += delta / 100), cloudMaxOpacity);
|
||||||
|
s.dying = s.countdown >= 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.x += s.speedx * delta;
|
||||||
|
let wrapTolerance = (s.type === 'snow' || s.type === 'rain') ? 3 : 50;
|
||||||
|
if (s.x < -wrapTolerance) {
|
||||||
|
s.x = pixi.view.width - s.x;
|
||||||
|
}
|
||||||
|
else if (s.x > pixi.view.width + wrapTolerance) {
|
||||||
|
s.x = -wrapTolerance;
|
||||||
|
}
|
||||||
|
s.y += s.speedy * delta;
|
||||||
|
if (s.y > pixi.view.height + 15 || s.graphic.alpha <= 0) {
|
||||||
|
s.graphic.destroy();
|
||||||
|
sprites.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lastFrame = t;
|
||||||
|
});
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
pixi.renderer.resize(window.innerWidth, window.innerHeight);
|
||||||
|
pixi.view.width = window.innerWidth;
|
||||||
|
pixi.view.height = window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeCanvas();
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
});
|
5198
package-lock.json
generated
5198
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "showpiece",
|
"name": "showpiece",
|
||||||
"version": "1.0.0",
|
"version": "2.0.1",
|
||||||
"description": "A simple digital signage solution built on the web",
|
"description": "A simple digital signage solution built on the web",
|
||||||
"main": "index.js",
|
"main": "src/index.ts",
|
||||||
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/CorySanin/showpiece.git"
|
"url": "git+https://github.com/CorySanin/showpiece.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "node --experimental-strip-types src/index.ts",
|
||||||
"build": "tailwindcss -i ./styles/control.css -o ./assets/css/control.css",
|
"build": "tailwindcss -i ./styles/control.css -o ./assets/css/control.css",
|
||||||
"watch": "tailwindcss -i ./styles/control.css -o ./assets/css/control.css --watch"
|
"watch": "tailwindcss -i ./styles/control.css -o ./assets/css/control.css --watch"
|
||||||
},
|
},
|
||||||
@ -17,15 +19,22 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cicciosgamino/openweather-apis": "^5.0.1",
|
"dayjs": "1.11.13",
|
||||||
"ejs": "3.1.6",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.17.2",
|
"express": "5.1.0",
|
||||||
"express-ws": "5.0.2",
|
"express-ws": "^5.0.2",
|
||||||
"json5": "^2.2.0",
|
"json5": "^2.2.3",
|
||||||
"phin": "3.6.1"
|
"ky": "^1.8.1",
|
||||||
|
"openweathermap-ts": "1.2.10",
|
||||||
|
"node-cache": "5.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tailwindcss": "^3.0.7"
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/express-ws": "3.0.5",
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"@tailwindcss/cli": "4.1.8",
|
||||||
|
"typescript": "5.8.3",
|
||||||
|
"tailwindcss": "4.1.8"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/CorySanin/showpiece/issues"
|
"url": "https://github.com/CorySanin/showpiece/issues"
|
||||||
|
116
server.js
116
server.js
@ -1,116 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const fsp = fs.promises;
|
|
||||||
const express = require('express');
|
|
||||||
const ExpressWS = require('express-ws');
|
|
||||||
const version = require('./version');
|
|
||||||
|
|
||||||
const VIEWOPTIONS = {
|
|
||||||
outputFunctionName: 'echo'
|
|
||||||
};
|
|
||||||
const MODULES = 'modules';
|
|
||||||
|
|
||||||
class Server {
|
|
||||||
constructor(options = {}) {
|
|
||||||
const app = this._app = express();
|
|
||||||
const controlapp = this._controlapp = express();
|
|
||||||
controlapp.use(express.json());
|
|
||||||
const clients = this._clients = [];
|
|
||||||
this._current = {
|
|
||||||
type: 'none',
|
|
||||||
body: null
|
|
||||||
}
|
|
||||||
app.set('view engine', 'ejs');
|
|
||||||
app.set('view options', VIEWOPTIONS);
|
|
||||||
controlapp.set('view engine', 'ejs');
|
|
||||||
controlapp.set('view options', VIEWOPTIONS);
|
|
||||||
ExpressWS(app);
|
|
||||||
|
|
||||||
app.ws('/control', (ws, req) => {
|
|
||||||
clients.push(ws);
|
|
||||||
console.log(`new client connected: ${req.ip}`);
|
|
||||||
this.send(this._current, [ws]);
|
|
||||||
this.send({
|
|
||||||
type: 'version',
|
|
||||||
body: version
|
|
||||||
}, [ws]);
|
|
||||||
ws.on('close', (ws) => {
|
|
||||||
clients.splice(clients.indexOf(ws), 1);
|
|
||||||
console.log(`client disconnected: ${req.ip}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
controlapp.post('/api/load', (req, res) => {
|
|
||||||
let body = req.body;
|
|
||||||
if ('type' in body) {
|
|
||||||
this.send(this._current = body);
|
|
||||||
}
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
controlapp.get('/', async (req, res) => {
|
|
||||||
res.redirect(`${req.baseUrl}/load`);
|
|
||||||
});
|
|
||||||
|
|
||||||
controlapp.get('/load', async (req, res) => {
|
|
||||||
res.render('load', {
|
|
||||||
modules: await this.getModules()
|
|
||||||
}, (err, html) => {
|
|
||||||
if (!err) {
|
|
||||||
res.send(html);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.send(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(`/${MODULES}/`, express.static(MODULES));
|
|
||||||
|
|
||||||
if (options.port !== options.controlport) {
|
|
||||||
controlapp.use('/assets/', express.static('assets'));
|
|
||||||
controlapp.use(`/${MODULES}/`, express.static(MODULES));
|
|
||||||
controlapp.listen(options.controlport, () => console.log(`Showpiece listening to port ${options.controlport}`));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
app.use('/control/', controlapp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
send(payload, clients = this._clients) {
|
|
||||||
clients.forEach((ws, index) => {
|
|
||||||
try {
|
|
||||||
ws.send(JSON.stringify(payload));
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
clients.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getModules = async () => {
|
|
||||||
let dirs = (await fsp.readdir(MODULES, { withFileTypes: true })).filter(dirent => !dirent.isFile());
|
|
||||||
let result = [];
|
|
||||||
for (const index in dirs) {
|
|
||||||
const dir = dirs[index];
|
|
||||||
let obj = {
|
|
||||||
name: dir.name
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
let icon = path.join(MODULES, dir.name, 'icon.png');
|
|
||||||
await fsp.stat(icon);
|
|
||||||
obj.icon = `/${icon}`;
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
obj.icon = null;
|
|
||||||
}
|
|
||||||
result.push(obj);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
middleware() {
|
|
||||||
return this._app;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Server;
|
|
@ -1,21 +1,24 @@
|
|||||||
const express = require('express');
|
import express from 'express';
|
||||||
const weather = require('./weather');
|
import SunriseSunset from './sunrise-sunset.ts';
|
||||||
const sun = require('./sunrise-sunset');
|
import Weather from './weather.ts';
|
||||||
const version = require('./version');
|
import Version from './version.ts';
|
||||||
|
import type { AppOptions } from './index.ts';
|
||||||
|
|
||||||
const VIEWOPTIONS = {
|
const VIEWOPTIONS = {
|
||||||
outputFunctionName: 'echo'
|
outputFunctionName: 'echo'
|
||||||
};
|
};
|
||||||
|
|
||||||
class Client {
|
class Client {
|
||||||
constructor(options = {}) {
|
private app: express.Application;
|
||||||
const app = this._app = express();
|
constructor(options: AppOptions = {}) {
|
||||||
|
const app: express.Application = this.app = express();
|
||||||
app.set('view engine', 'ejs');
|
app.set('view engine', 'ejs');
|
||||||
app.set('view options', VIEWOPTIONS);
|
app.set('view options', VIEWOPTIONS);
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (_, res) => {
|
||||||
res.render('client', {}, (err, html) => {
|
res.render('client', {}, (err, html) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.send(html);
|
res.send(html);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -26,7 +29,7 @@ class Client {
|
|||||||
|
|
||||||
app.get('/client.js', (req, res) => {
|
app.get('/client.js', (req, res) => {
|
||||||
res.render('clientjs', {
|
res.render('clientjs', {
|
||||||
version
|
Version
|
||||||
}, (err, js) => {
|
}, (err, js) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
res.type('text/javascript').send(js);
|
res.type('text/javascript').send(js);
|
||||||
@ -38,30 +41,30 @@ class Client {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if(options.weather && options.weather.key){
|
if(options.weather && options.weather.key){
|
||||||
let w = new weather();
|
const w = new Weather(options.weather);
|
||||||
w.prepareApi(options.weather).then(async () => {
|
w.getAllWeather().then((resp) => {
|
||||||
let results = await w.getAllWeather();
|
const s = new SunriseSunset({
|
||||||
let s = new sun({
|
latitude: resp.city.coord.lat,
|
||||||
latitude: results.coord.lat,
|
longitude: resp.city.coord.lon
|
||||||
longitude: results.coord.lon
|
|
||||||
});
|
});
|
||||||
app.get('/api/sun', async (req, res) => {
|
app.get('/api/sun', async (_, res) => {
|
||||||
res.send(await s.getSunrise());
|
res.send(await s.getSunrise());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.get('/api/weather', async (req, res) => {
|
app.get('/api/weather', async (_, res) => {
|
||||||
res.send(await w.getSimpleWeather());
|
res.send(await w.getSimpleWeather());
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/all-weather', async (req, res) => {
|
app.get('/api/all-weather', async (_, res) => {
|
||||||
res.send(await w.getAllWeather());
|
res.send(await w.getAllWeather());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware() {
|
middleware(): express.Application {
|
||||||
return this._app;
|
return this.app;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Client;
|
export default Client;
|
||||||
|
export { Client };
|
32
src/index.ts
Normal file
32
src/index.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fsp from 'fs/promises';
|
||||||
|
import express from 'express';
|
||||||
|
import json5 from 'json5';
|
||||||
|
import Client from './client.ts';
|
||||||
|
import Server from './server.ts';
|
||||||
|
import type { WeatherConfig } from "./weather.ts";
|
||||||
|
|
||||||
|
interface AppOptions {
|
||||||
|
port?: number;
|
||||||
|
controlport?: number;
|
||||||
|
modulesDir?: string;
|
||||||
|
weather?: WeatherConfig;
|
||||||
|
clientonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const configpath = process.env.CONFIG || path.join('config', 'config.json5');
|
||||||
|
|
||||||
|
const configfile = json5.parse((await fsp.readFile(configpath)).toString());
|
||||||
|
configfile.port = process.env.PORT || configfile.port || 8080;
|
||||||
|
configfile.controlport = process.env.CONTROLPORT || configfile.controlport || configfile.port;
|
||||||
|
const c = new Client(configfile);
|
||||||
|
const s = new Server(configfile);
|
||||||
|
const app = (configfile?.clientonly) ? express() : s.middleware();
|
||||||
|
app.set('trust proxy', 1);
|
||||||
|
app.use('/', c.middleware());
|
||||||
|
(app as express.Application).use('/assets/', express.static('assets'));
|
||||||
|
process.on('SIGTERM', app.listen(configfile.port, () => {
|
||||||
|
console.log(`Showpiece listening to port ${configfile.port}`);
|
||||||
|
}).close);
|
||||||
|
|
||||||
|
export type { AppOptions }
|
160
src/server.ts
Normal file
160
src/server.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import * as http from "http";
|
||||||
|
import path from 'path';
|
||||||
|
import fsp from 'fs/promises';
|
||||||
|
import json5 from 'json5';
|
||||||
|
import express from 'express';
|
||||||
|
import ExpressWS from 'express-ws';
|
||||||
|
import Version from './version.ts';
|
||||||
|
import type WebSocket from 'ws';
|
||||||
|
import type { AppOptions } from './index.ts';
|
||||||
|
|
||||||
|
const VIEWOPTIONS = {
|
||||||
|
outputFunctionName: 'echo'
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Payload {
|
||||||
|
type: 'none' | 'version';
|
||||||
|
body: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Scene extends Payload {
|
||||||
|
type: 'none';
|
||||||
|
body: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModuleParam {
|
||||||
|
name: string,
|
||||||
|
param: string,
|
||||||
|
type: "color" | "bool",
|
||||||
|
default?: string | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Module {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
parameters?: ModuleParam[]
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
private app: ExpressWS.Application;
|
||||||
|
private current: Scene;
|
||||||
|
private clients: WebSocket[];
|
||||||
|
private modulesDir: string;
|
||||||
|
private webservers: http.Server[] = [];
|
||||||
|
constructor(options: AppOptions = {}) {
|
||||||
|
this.modulesDir = options.modulesDir || process.env['MODULESDIR'] || path.join('.', 'modules');
|
||||||
|
const app = express();
|
||||||
|
const controlapp = express();
|
||||||
|
controlapp.use(express.json());
|
||||||
|
const clients: WebSocket[] = this.clients = [];
|
||||||
|
this.current = {
|
||||||
|
type: 'none',
|
||||||
|
body: null
|
||||||
|
}
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('view options', VIEWOPTIONS);
|
||||||
|
controlapp.set('view engine', 'ejs');
|
||||||
|
controlapp.set('view options', VIEWOPTIONS);
|
||||||
|
this.app = ExpressWS(app).app;
|
||||||
|
|
||||||
|
this.app.ws('/control', (ws, req) => {
|
||||||
|
clients.push(ws);
|
||||||
|
console.log(`new client connected: ${req.ip}`);
|
||||||
|
this.send(this.current, [ws]);
|
||||||
|
this.send({
|
||||||
|
type: 'version',
|
||||||
|
body: Version
|
||||||
|
}, [ws]);
|
||||||
|
ws.on('close', (_) => {
|
||||||
|
clients.splice(clients.indexOf(ws), 1);
|
||||||
|
console.log(`client disconnected: ${req.ip}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
controlapp.post('/api/load', (req, res) => {
|
||||||
|
let body = req.body;
|
||||||
|
if ('type' in body) {
|
||||||
|
this.send(this.current = body);
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
controlapp.get('/', async (req, res) => {
|
||||||
|
res.redirect(`${req.baseUrl}/load`);
|
||||||
|
});
|
||||||
|
|
||||||
|
controlapp.get('/load', async (req, res) => {
|
||||||
|
res.render('load', {
|
||||||
|
modules: await this.getModules()
|
||||||
|
}, (err, html) => {
|
||||||
|
if (!err) {
|
||||||
|
res.send(html);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.send(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(`/modules/`, express.static(this.modulesDir));
|
||||||
|
|
||||||
|
if (options.port !== options.controlport) {
|
||||||
|
controlapp.use('/assets/', express.static('assets'));
|
||||||
|
controlapp.use(`/modules/`, express.static(this.modulesDir));
|
||||||
|
this.webservers.push(controlapp.listen(options.controlport, () => console.log(`Showpiece listening to port ${options.controlport}`)));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
app.use('/control/', controlapp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(payload: Payload, clients: WebSocket[] = this.clients) {
|
||||||
|
clients.forEach((ws, index) => {
|
||||||
|
try {
|
||||||
|
ws.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
clients.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getModules (): Promise<Module[]> {
|
||||||
|
const dirs = (await fsp.readdir(this.modulesDir, { withFileTypes: true })).filter(dirent => !dirent.isFile());
|
||||||
|
const result: Module[] = [];
|
||||||
|
for (const index in dirs) {
|
||||||
|
const dir = dirs[index];
|
||||||
|
let obj: Module = {
|
||||||
|
name: dir.name,
|
||||||
|
path: dir.name
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
obj = json5.parse((await fsp.readFile(path.join(this.modulesDir, dir.name, 'module.json'))).toString());
|
||||||
|
if (obj.icon) {
|
||||||
|
obj.icon = `/${path.join(this.modulesDir, dir.name, obj.icon)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
console.error(`Module metadata for ${dir.name} could not be loaded`);
|
||||||
|
}
|
||||||
|
obj.path = dir.name;
|
||||||
|
result.push(obj);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(): ExpressWS.Application {
|
||||||
|
return this.app;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.webservers.forEach(s => {
|
||||||
|
s.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Server;
|
||||||
|
export { Server };
|
||||||
|
export type { Payload, Scene, Module, ModuleParam };
|
60
src/sunrise-sunset.ts
Normal file
60
src/sunrise-sunset.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import ky from 'ky';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
interface Location {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SunriseResponse {
|
||||||
|
results: SunriseResults | "";
|
||||||
|
status: "OK" | "INVALID_REQUEST";
|
||||||
|
tzid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SunriseResults {
|
||||||
|
sunrise: string;
|
||||||
|
sunset: string;
|
||||||
|
solar_noon: string;
|
||||||
|
day_length: number;
|
||||||
|
civil_twilight_begin: string;
|
||||||
|
civil_twilight_end: string;
|
||||||
|
nautical_twilight_begin: string;
|
||||||
|
nautical_twilight_end: string;
|
||||||
|
astronomical_twilight_begin: string;
|
||||||
|
astronomical_twilight_end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SunriseSunset {
|
||||||
|
private location: Location
|
||||||
|
private state: SunriseResults;
|
||||||
|
private expiration: dayjs.Dayjs;
|
||||||
|
|
||||||
|
constructor(location: Location) {
|
||||||
|
this.location = location;
|
||||||
|
this.invalidateCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateCache(): void {
|
||||||
|
this.expiration = dayjs().subtract(1, 'day');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSunrise(): Promise<SunriseResults> {
|
||||||
|
const now = dayjs();
|
||||||
|
if (now.isAfter(this.expiration)) {
|
||||||
|
console.log('Calling Sunrise API');
|
||||||
|
const resp: SunriseResponse = await ky.get(`https://api.sunrise-sunset.org/json?lat=${this.location.latitude}&lng=${this.location.longitude}&date=today&formatted=0`).json();
|
||||||
|
this.expiration = now.add(1, 'day').set('hour', 0).set('minute', 0).set('second', 0).set('millisecond', 0);
|
||||||
|
this.state = resp.results as SunriseResults;
|
||||||
|
}
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
return `${this.location.latitude},${this.location.longitude}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SunriseSunset;
|
||||||
|
export { SunriseSunset };
|
||||||
|
export type { Location, SunriseResponse, SunriseResults };
|
3
src/version.ts
Normal file
3
src/version.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const Version: string = 'dev';
|
||||||
|
|
||||||
|
export default Version;
|
131
src/weather.ts
Normal file
131
src/weather.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import OpenWeatherMap from 'openweathermap-ts';
|
||||||
|
import NodeCache from "node-cache";
|
||||||
|
import type { Location } from './sunrise-sunset.ts';
|
||||||
|
import type { ThreeHourResponse, CurrentResponse, CountryCode } from 'openweathermap-ts/dist/types/index.js';
|
||||||
|
|
||||||
|
const CACHEPERIOD = 5;
|
||||||
|
const MINUTES = 60;
|
||||||
|
|
||||||
|
interface CityName {
|
||||||
|
cityName: string;
|
||||||
|
state: string;
|
||||||
|
countryCode: CountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WeatherConfig {
|
||||||
|
key: string;
|
||||||
|
lang?: string;
|
||||||
|
coordinates?: number[] | string | Location;
|
||||||
|
zip?: number;
|
||||||
|
country?: CountryCode;
|
||||||
|
cityid?: number;
|
||||||
|
city?: CityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationType = 'coordinates' | 'zip' | 'cityid' | 'city' | null;
|
||||||
|
|
||||||
|
function parseCoords(coords: number[] | string | Location): number[] {
|
||||||
|
if (typeof coords == 'string') {
|
||||||
|
return coords.replace(/\s/g, '').split(',').map(Number.parseFloat);
|
||||||
|
}
|
||||||
|
else if (typeof coords == 'object' && !('length' in coords)) {
|
||||||
|
return [coords.latitude, coords.longitude];
|
||||||
|
}
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Weather {
|
||||||
|
private openWeather: OpenWeatherMap.default;
|
||||||
|
private cache: NodeCache;
|
||||||
|
private locationType: LocationType;
|
||||||
|
|
||||||
|
constructor(options: WeatherConfig) {
|
||||||
|
this.cache = new NodeCache({
|
||||||
|
stdTTL: CACHEPERIOD * MINUTES,
|
||||||
|
useClones: false
|
||||||
|
});
|
||||||
|
this.locationType = null;
|
||||||
|
this.openWeather = new OpenWeatherMap.default({
|
||||||
|
apiKey: options.key
|
||||||
|
});
|
||||||
|
if ('city' in options && 'cityName' in options.city && 'state' in options.city && 'countryCode' in options.city) {
|
||||||
|
this.openWeather.setCityName(options.city);
|
||||||
|
this.locationType = 'city';
|
||||||
|
}
|
||||||
|
if ('cityid' in options) {
|
||||||
|
this.openWeather.setCityId(options.cityid);
|
||||||
|
this.locationType = 'cityid';
|
||||||
|
}
|
||||||
|
if ('zip' in options && 'country' in options) {
|
||||||
|
this.openWeather.setZipCode(options.zip, options.country)
|
||||||
|
this.locationType = 'zip';
|
||||||
|
}
|
||||||
|
if ('coordinates' in options) {
|
||||||
|
const coords = parseCoords(options.coordinates);
|
||||||
|
if (coords.length >= 2) {
|
||||||
|
this.openWeather.setGeoCoordinates(coords[0], coords[1]);
|
||||||
|
this.locationType = 'coordinates';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentWeather(): Promise<CurrentResponse> {
|
||||||
|
let returnable = this.cache.get('current') as undefined | CurrentResponse;
|
||||||
|
if (returnable === undefined) {
|
||||||
|
switch (this.locationType) {
|
||||||
|
case 'city':
|
||||||
|
returnable = await this.openWeather.getCurrentWeatherByCityName();
|
||||||
|
break;
|
||||||
|
case 'cityid':
|
||||||
|
returnable = await this.openWeather.getCurrentWeatherByCityId();
|
||||||
|
break;
|
||||||
|
case 'zip':
|
||||||
|
returnable = await this.openWeather.getCurrentWeatherByZipcode();
|
||||||
|
break;
|
||||||
|
case 'coordinates':
|
||||||
|
returnable = await this.openWeather.getCurrentWeatherByGeoCoordinates();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Can't fetch weather for location type '${this.locationType}'`);
|
||||||
|
}
|
||||||
|
this.cache.set('current', returnable);
|
||||||
|
}
|
||||||
|
return returnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getThreeHourForecast(): Promise<ThreeHourResponse> {
|
||||||
|
let returnable = this.cache.get('forecast') as undefined | ThreeHourResponse;
|
||||||
|
if (returnable === undefined) {
|
||||||
|
switch (this.locationType) {
|
||||||
|
case 'city':
|
||||||
|
returnable = await this.openWeather.getThreeHourForecastByCityName();
|
||||||
|
break;
|
||||||
|
case 'cityid':
|
||||||
|
returnable = await this.openWeather.getThreeHourForecastByCityId();
|
||||||
|
break;
|
||||||
|
case 'zip':
|
||||||
|
returnable = await this.openWeather.getThreeHourForecastByZipcode();
|
||||||
|
break;
|
||||||
|
case 'coordinates':
|
||||||
|
returnable = await this.openWeather.getThreeHourForecastByGeoCoordinates();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Can't fetch weather for location type '${this.locationType}'`);
|
||||||
|
}
|
||||||
|
this.cache.set('forecast', returnable);
|
||||||
|
}
|
||||||
|
return returnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSimpleWeather(): Promise<CurrentResponse> {
|
||||||
|
return this.getSimpleWeather();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllWeather(): Promise<ThreeHourResponse> {
|
||||||
|
return this.getThreeHourForecast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Weather;
|
||||||
|
export { Weather };
|
||||||
|
export type { WeatherConfig, CityName };
|
@ -1,6 +1,4 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
@ -81,6 +79,12 @@ input[type="submit"]:active,
|
|||||||
background-color: theme('colors.indigo.800');
|
background-color: theme('colors.indigo.800');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
background-color: theme('colors.indigo.700');
|
background-color: theme('colors.indigo.700');
|
||||||
color: theme('colors.indigo.50');
|
color: theme('colors.indigo.50');
|
||||||
@ -117,12 +121,31 @@ input[type="submit"]:active,
|
|||||||
transition: margin .25s ease;
|
transition: margin .25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablelist>button {
|
.tablelist>div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablelist>div>button.expand-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 1em;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablelist>div {
|
||||||
|
border: 1px solid theme('colors.indigo.300');
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablelist>div>button.module-btn {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border: 1px solid theme('colors.indigo.300');
|
border-radius: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
line-height: 80px;
|
line-height: 80px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -132,31 +155,54 @@ input[type="submit"]:active,
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablelist>button:hover,
|
.tablelist>div>button.module-btn:hover,
|
||||||
.tablelist>button:focus {
|
.tablelist>div>button.module-btn:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: theme('colors.indigo.600');
|
background-color: theme('colors.indigo.600');
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablelist>button:active,
|
.tablelist>div>button.module-btn:active,
|
||||||
.tablelist>button.active {
|
.tablelist>div>button.module-btn.active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: theme('colors.indigo.800');
|
background-color: theme('colors.indigo.800');
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablelist>button:first-of-type {
|
.tablelist>div:first-of-type,
|
||||||
|
.tablelist>div:first-of-type>button.module-btn {
|
||||||
border-radius: .75em .75em 0 0;
|
border-radius: .75em .75em 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablelist>button:nth-last-of-type(1) {
|
.tablelist>div:nth-last-of-type(1),
|
||||||
border-bottom-left-radius: .75em;
|
.tablelist>div.collapsed:nth-last-of-type(1)>button.module-btn {
|
||||||
border-bottom-right-radius: .75em;
|
border-radius: 0 0 .75em .75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tablelist>button:nth-of-type(n + 2) {
|
.tablelist>div:nth-last-of-type(1) {
|
||||||
|
border-bottom: 1px solid theme('colors.indigo.300');
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablelist>div:nth-of-type(n + 2)>button {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tablelist>div.collapsed form {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablelist>div.collapsed .collapse,
|
||||||
|
.tablelist>div .expand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablelist>div.collapsed .expand,
|
||||||
|
.tablelist>div .collapse{
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablelist form {
|
||||||
|
padding: .5em .8em;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 675px) {
|
@media screen and (max-width: 675px) {
|
||||||
#sidebar {
|
#sidebar {
|
||||||
left: -300px;
|
left: -300px;
|
||||||
@ -168,7 +214,7 @@ input[type="submit"]:active,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 320px) {
|
@media screen and (max-width: 320px) {
|
||||||
.tablelist>button img {
|
.tablelist>div>button.module-btn img {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,72 +0,0 @@
|
|||||||
const phin = require('phin');
|
|
||||||
|
|
||||||
const CACHEPERIOD = 5;
|
|
||||||
const MINUTES = 60000;
|
|
||||||
const DAY = 86400000;
|
|
||||||
|
|
||||||
function tomorrow() {
|
|
||||||
let d = new Date();
|
|
||||||
d = new Date(d.getTime() + DAY);
|
|
||||||
d.setHours(0);
|
|
||||||
d.setMinutes(0);
|
|
||||||
d.setSeconds(0);
|
|
||||||
d.setMilliseconds(0);
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createWeatherApi(options) {
|
|
||||||
const AsyncWeather = (await import('@cicciosgamino/openweather-apis')).AsyncWeather;
|
|
||||||
const weather = await (new AsyncWeather())
|
|
||||||
weather.setLang(options.lang || 'en');
|
|
||||||
weather.setUnits('metric');
|
|
||||||
if ('coordinates' in options) {
|
|
||||||
let coords = options.coordinates;
|
|
||||||
if (typeof coords == 'string') {
|
|
||||||
coords = coords.replace(/\s/g, '').split(',').map(Number.parseFloat);
|
|
||||||
}
|
|
||||||
else if (typeof coords == 'object' && !('length' in coords)) {
|
|
||||||
coords = [coords['latitude'], coords['longitude']];
|
|
||||||
}
|
|
||||||
if (typeof coords == 'object' && 'length' in coords && coords.length >= 2) {
|
|
||||||
weather.setCoordinates(coords[0], coords[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ('zip' in options && 'country' in options) {
|
|
||||||
weather.setZipCodeAndCountryCode(options.zip, options.country);
|
|
||||||
}
|
|
||||||
if ('cityid' in options) {
|
|
||||||
weather.setCityId(options.cityid);
|
|
||||||
}
|
|
||||||
if ('city' in options) {
|
|
||||||
weather.setCity(options.city);
|
|
||||||
}
|
|
||||||
if ('key' in options) {
|
|
||||||
weather.setApiKey(options.key);
|
|
||||||
}
|
|
||||||
return weather;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SunriseSunset {
|
|
||||||
constructor(location) {
|
|
||||||
this._location = location;
|
|
||||||
this._state = {
|
|
||||||
expiration: new Date()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSunrise = async () => {
|
|
||||||
let now = new Date();
|
|
||||||
if (now >= this._state.expiration) {
|
|
||||||
console.log('Calling API');
|
|
||||||
let resp = await phin({
|
|
||||||
url: `https://api.sunrise-sunset.org/json?lat=${this._location.latitude}&lng=${this._location.longitude}&date=today&formatted=0`,
|
|
||||||
parse: 'json'
|
|
||||||
});
|
|
||||||
resp.body.results.expiration = tomorrow();
|
|
||||||
this._state = resp.body.results;
|
|
||||||
}
|
|
||||||
return this._state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = SunriseSunset;
|
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"target": "ESNext",
|
||||||
|
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "node16",
|
||||||
|
"noEmit": true,
|
||||||
|
"allowImportingTsExtensions": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["**/*.spec.ts"]
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
module.exports = 1;
|
|
@ -1,20 +1,39 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const loc = window.location;
|
const loc = window.location;
|
||||||
const server = `${loc.protocol === 'https:' ? 'wss' : 'ws'}://${loc.host}/control`;
|
const server = `${loc.protocol === 'https:' ? 'wss' : 'ws'}://${loc.host}/control`;
|
||||||
|
var width = 0;
|
||||||
|
var height = 0;
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
width = parseInt(urlParams.get('w')) || 0;
|
||||||
|
height = parseInt(urlParams.get('h')) || 0;
|
||||||
|
})();
|
||||||
|
|
||||||
|
function createIFrame() {
|
||||||
|
var iframe = document.createElement('iframe');
|
||||||
|
if (width) {
|
||||||
|
iframe.style.width = `${width}px`;
|
||||||
|
}
|
||||||
|
if (height) {
|
||||||
|
iframe.style.height = `${height}px`;
|
||||||
|
}
|
||||||
|
return iframe;
|
||||||
|
}
|
||||||
|
|
||||||
function handlemessage(data) {
|
function handlemessage(data) {
|
||||||
const msg = JSON.parse(data.data);
|
const msg = JSON.parse(data.data);
|
||||||
if (msg.type === 'module') {
|
if (msg.type === 'module') {
|
||||||
msg.type = 'url';
|
msg.type = 'url';
|
||||||
msg.body = `/modules/${msg.body}/`
|
msg.body = `/modules/${msg.body}`
|
||||||
}
|
}
|
||||||
if (msg.type === 'version') {
|
if (msg.type === 'version') {
|
||||||
if (msg.body !== <%= version %>) {
|
if (msg.body !== '<%= Version %>') {
|
||||||
location.reload(true);
|
location.reload(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (msg.type === 'url') {
|
else if (msg.type === 'url') {
|
||||||
var iframe = document.createElement('iframe');
|
var iframe = createIFrame();
|
||||||
iframe.src = msg.body;
|
iframe.src = msg.body;
|
||||||
iframe.style['z-index'] = -100;
|
iframe.style['z-index'] = -100;
|
||||||
document.body.append(iframe);
|
document.body.append(iframe);
|
||||||
|
@ -25,10 +25,28 @@
|
|||||||
<h2>Module</h2>
|
<h2>Module</h2>
|
||||||
<div class="tablelist" id="modules">
|
<div class="tablelist" id="modules">
|
||||||
<% modules.forEach(function(module) { %>
|
<% modules.forEach(function(module) { %>
|
||||||
<button>
|
<div class="collapsed">
|
||||||
<% if (module.icon) { %><img alt="<%= module.name %> module" class="max-h-full inline-block" src="<%= module.icon %>" /><% } %>
|
<button class="module-btn">
|
||||||
<span class="module-name ml-2"><%= module.name %></span>
|
<% if (module.icon) { %><img alt="<%= module.name %> module" class="max-h-full inline-block" src="<%= module.icon %>" /><% } %>
|
||||||
</button>
|
<span class="module-path hidden"><%= module.path %></span>
|
||||||
|
<span class="module-name ml-2"><%= module.name %></span>
|
||||||
|
</button>
|
||||||
|
<% if (module.parameters) { %>
|
||||||
|
<button class="expand-btn"><span class="expand">⬍</span><span class="collapse">⬆</span></button>
|
||||||
|
<form>
|
||||||
|
<div class="form-grid">
|
||||||
|
<% module.parameters.forEach(function(param) { %>
|
||||||
|
<label><%= param.name %></label>
|
||||||
|
<% if (param.type === 'color') { %>
|
||||||
|
<input type="color" name="<%= param.param %>" value="<%= param.default || '' %>">
|
||||||
|
<% } else if (param.type === 'bool' || param.type === 'boolean') { %>
|
||||||
|
<div><input type="checkbox" name="<%= param.param %>" <% if (param.default) { echo("checked") } %>></div>
|
||||||
|
<% } %>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
69
weather.js
69
weather.js
@ -1,69 +0,0 @@
|
|||||||
const CACHEPERIOD = 5;
|
|
||||||
const MINUTES = 60000;
|
|
||||||
|
|
||||||
async function createWeatherApi(options) {
|
|
||||||
const AsyncWeather = (await import('@cicciosgamino/openweather-apis')).AsyncWeather;
|
|
||||||
const weather = await (new AsyncWeather())
|
|
||||||
weather.setLang(options.lang || 'en');
|
|
||||||
weather.setUnits('metric');
|
|
||||||
if ('coordinates' in options) {
|
|
||||||
let coords = options.coordinates;
|
|
||||||
if (typeof coords == 'string') {
|
|
||||||
coords = coords.replace(/\s/g, '').split(',').map(Number.parseFloat);
|
|
||||||
}
|
|
||||||
else if (typeof coords == 'object' && !('length' in coords)) {
|
|
||||||
coords = [coords['latitude'], coords['longitude']];
|
|
||||||
}
|
|
||||||
if (typeof coords == 'object' && 'length' in coords && coords.length >= 2) {
|
|
||||||
weather.setCoordinates(coords[0], coords[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ('zip' in options && 'country' in options) {
|
|
||||||
weather.setZipCodeAndCountryCode(options.zip, options.country);
|
|
||||||
}
|
|
||||||
if ('cityid' in options) {
|
|
||||||
weather.setCityId(options.cityid);
|
|
||||||
}
|
|
||||||
if ('city' in options) {
|
|
||||||
weather.setCity(options.city);
|
|
||||||
}
|
|
||||||
if ('key' in options) {
|
|
||||||
weather.setApiKey(options.key);
|
|
||||||
}
|
|
||||||
return weather;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Weather {
|
|
||||||
constructor() {
|
|
||||||
this._state = {
|
|
||||||
expiration: new Date()
|
|
||||||
}
|
|
||||||
this._simplestate = {
|
|
||||||
expiration: new Date()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareApi = async (options) => {
|
|
||||||
this._weather = await createWeatherApi(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSimpleWeather = async () => {
|
|
||||||
let now = new Date();
|
|
||||||
if (now >= this._simplestate.expiration) {
|
|
||||||
this._simplestate.weather = await this._weather.getSmartJSON();
|
|
||||||
this._simplestate.expiration = new Date(now.getTime() + CACHEPERIOD * MINUTES);
|
|
||||||
}
|
|
||||||
return this._simplestate.weather;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllWeather = async () => {
|
|
||||||
let now = new Date();
|
|
||||||
if (now >= this._state.expiration) {
|
|
||||||
this._state.weather = await this._weather.getAllWeather();
|
|
||||||
this._state.expiration = new Date(now.getTime() + CACHEPERIOD * MINUTES);
|
|
||||||
}
|
|
||||||
return this._state.weather;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Weather;
|
|
Loading…
x
Reference in New Issue
Block a user