Merge branch 'music-player'

This commit is contained in:
Matt Walsh
2025-03-27 10:19:15 -05:00
39 changed files with 543 additions and 164 deletions

View File

@@ -1,3 +1,4 @@
.git/
Dockerfile
.vscode/
.vscode/
dist/

91
.eslintrc Normal file
View File

@@ -0,0 +1,91 @@
{
"env": {
"browser": true,
"es6": true,
"node": true,
"jquery": true
},
"extends": [
"airbnb-base"
],
"globals": {
"TravelCities": "readonly",
"RegionalCities": "readonly",
"StationInfo": "readonly",
"SunCalc": "readonly"
},
"parserOptions": {
"ecmaVersion": 2023,
"sourceType": "module"
},
"plugins": [],
"rules": {
"indent": [
"error",
"tab",
{
"SwitchCase": 1
}
],
"no-tabs": 0,
"no-console": 0,
"max-len": 0,
"no-use-before-define": [
"error",
{
"variables": false
}
],
"no-param-reassign": [
"error",
{
"props": false
}
],
"no-mixed-operators": [
"error",
{
"groups": [
[
"&",
"|",
"^",
"~",
"<<",
">>",
">>>"
],
[
"==",
"!=",
"===",
"!==",
">",
">=",
"<",
"<="
],
[
"&&",
"||"
],
[
"in",
"instanceof"
]
],
"allowSamePrecedence": true
}
],
"import/extensions": [
"error",
{
"mjs": "always",
"json": "always"
}
]
},
"ignorePatterns": [
"*.min.js"
]
}

View File

@@ -1,74 +0,0 @@
module.exports = {
env: {
browser: true,
commonjs: true,
es6: true,
node: true,
jquery: true,
},
extends: [
'airbnb-base',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
StationInfo: 'readonly',
RegionalCities: 'readonly',
TravelCities: 'readonly',
NoSleep: 'readonly',
states: 'readonly',
SunCalc: 'readonly',
},
parserOptions: {
ecmaVersion: 2023,
},
plugins: [
],
rules: {
indent: [
'error',
'tab',
{
SwitchCase: 1
},
],
'no-tabs': 0,
'no-console': 0,
'max-len': 0,
'no-use-before-define': [
'error',
{
variables: false,
},
],
'no-param-reassign': [
'error',
{
props: false,
},
],
'no-mixed-operators': [
'error',
{
groups: [
['&', '|', '^', '~', '<<', '>>', '>>>'],
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
['&&', '||'],
['in', 'instanceof'],
],
allowSamePrecedence: true,
},
],
'import/extensions': [
'error',
{
mjs: 'always',
json: 'always',
},
],
},
ignorePatterns: [
'*.min.js',
],
};

13
.gitignore vendored
View File

@@ -1,3 +1,14 @@
node_modules
**/debug.log
server/scripts/custom.js
server/scripts/custom.js
#music folder
server/music/*
#except for the readme
!server/music/readme.txt
#and the sample songs
!server/music/default
#dist folder
dist/*
!dist/readme.txt

4
.vscode/launch.json vendored
View File

@@ -50,7 +50,7 @@
"skipFiles": [
"<node_internals>/**",
],
"program": "${workspaceFolder}/index.js",
"program": "${workspaceFolder}/index.mjs",
},
{
"type": "node",
@@ -59,7 +59,7 @@
"skipFiles": [
"<node_internals>/**",
],
"program": "${workspaceFolder}/index.js",
"program": "${workspaceFolder}/index.mjs",
"env": {
"DIST": "1"
}

View File

@@ -98,11 +98,32 @@ As time allows I will be working on the following enhancements.
* Better error reporting when api.weather.gov is down (happens more often than you would think)
## Serving static files
The app can be served as a static set of files on any web server. Run the provided gulp task to create a set of static distribution files:
```
npm run buildDist
```
The resulting files will be in the /dist folder in the root of the project. These can then be uploaded to a web server for hosting, no server-side scripting is required.
## Music
The WeatherStar had wonderful background music from the smooth jazz and new age genres by artists of the time. Lists of the music that played are available by searching online, but it's all copyrighted music and would be difficult to provide as part of this repository.
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks will be posted in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
### Customizing the music
Placing .mp3 files in the `/server/music` folder will override the default music included in the repo. When weatherstar loads in the browser it will load a list if available files and randomize the order when it starts playing. On each loop through the available tracks the order will again be shuffled. If you're using the static files method to host your WeatherStar music is located in `/music`.
### Music doesn't auto play
Ws4kp is muted by default, but if it was unmuted on the last visit it is coded to try and auto play music on subsequent visits. But, it's considered bad form to have a web site play music automatically on load, and I fully agree with this. [Chrome](https://developer.chrome.com/blog/autoplay/#media_engagement_index) and [Firefox](https://hacks.mozilla.org/2019/02/firefox-66-to-block-automatically-playing-audible-video-and-audio/) have extensive details on how and when auto play is allowed.
Chrome seems to be more lenient on auto play and will eventually let a site auto-play music if you're visited it enough recently and manually clicked to start playing music on each visit. It also has a flag you can add to the command line when launching Chrome: `chrome.exe --autoplay-policy=no-user-gesture-required`. This is the best solution when using Kiosk-style setup.
## Community Notes
Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts!
* [Stream as FFMPEG](https://github.com/netbymatt/ws4kp/issues/37#issuecomment-2008491948)
* [Weather like it's 1999](https://blog.scottlabs.io/2024/02/weather-like-its-1999/) Raspberry pi, streaming, music and CRT all combined into a complete solution.
## Customization
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. An sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.

View File

@@ -1,13 +1,13 @@
// pass through api requests
// http(s) modules
const https = require('https');
import https from 'https';
// url parsing
const queryString = require('querystring');
import queryString from 'querystring';
// return an express router
module.exports = (req, res) => {
const cors = (req, res) => {
// add out-going headers
const headers = {};
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
@@ -41,3 +41,5 @@ module.exports = (req, res) => {
console.error(e);
});
};
export default cors;

View File

@@ -1,13 +1,13 @@
// pass through api requests
// http(s) modules
const https = require('https');
import https from 'https';
// url parsing
const queryString = require('querystring');
import queryString from 'querystring';
// return an express router
module.exports = (req, res) => {
const outlook = (req, res) => {
// add out-going headers
const headers = {};
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
@@ -42,3 +42,5 @@ module.exports = (req, res) => {
console.error(e);
});
};
export default outlook;

View File

@@ -1,13 +1,13 @@
// pass through api requests
// http(s) modules
const https = require('https');
import https from 'https';
// url parsing
const queryString = require('querystring');
import queryString from 'querystring';
// return an express router
module.exports = (req, res) => {
const radar = (req, res) => {
// add out-going headers
const headers = {};
headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)';
@@ -42,3 +42,5 @@ module.exports = (req, res) => {
console.error(e);
});
};
export default radar;

1
dist/index.html vendored

File diff suppressed because one or more lines are too long

12
dist/manifest.json vendored
View File

@@ -1,12 +0,0 @@
{
"name": "WeatherStar 4000+",
"icons": [
{
"src": "/images/Logo192.png",
"sizes": "192x192",
"type": "images/png"
}
],
"start_url": "/",
"display": "standalone"
}

1
dist/readme.txt vendored Normal file
View File

@@ -0,0 +1 @@
This folder is a placeholder for static files generated by the gulp task buildDist

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

0
dist/robots.txt vendored
View File

View File

@@ -12,11 +12,13 @@ import s3Upload from 'gulp-s3-upload';
import webpack from 'webpack-stream';
import TerserPlugin from 'terser-webpack-plugin';
import { readFile } from 'fs/promises';
import reader from '../src/playlist-reader.mjs';
import file from "gulp-file";
// get cloudfront
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
const clean = () => deleteAsync(['./dist**']);
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
@@ -87,6 +89,7 @@ const mjsSources = [
'server/scripts/modules/regionalforecast.mjs',
'server/scripts/modules/travelforecast.mjs',
'server/scripts/modules/progress.mjs',
'server/scripts/modules/media.mjs',
'server/scripts/index.mjs',
];
@@ -121,8 +124,9 @@ const compressHtml = async () => {
const otherFiles = [
'server/robots.txt',
'server/manifest.json',
'server/music/**/*.mp3'
];
const copyOtherFiles = () => src(otherFiles, { base: 'server/' })
const copyOtherFiles = () => src(otherFiles, { base: 'server/', encoding: false })
.pipe(dest('./dist'));
const s3 = s3Upload({
@@ -169,10 +173,20 @@ const invalidate = () => cloudfront.send(new CreateInvalidationCommand({
},
}));
const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles));
const buildPlaylist = async () => {
const availableFiles = await reader();
const playlist = { availableFiles };
return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist'))
}
const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles, buildPlaylist));
// upload_images could be in parallel with upload, but _images logs a lot and has little changes
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);
export default publishFrontend;
export {
buildDist,
}

View File

@@ -1,7 +1,8 @@
import updateVendor from './gulp/update-vendor.mjs';
import publishFrontend from './gulp/publish-frontend.mjs';
import publishFrontend, { buildDist } from './gulp/publish-frontend.mjs'
export {
updateVendor,
publishFrontend,
buildDist,
};

View File

@@ -1,29 +1,27 @@
// express
const express = require('express');
import express from 'express';
import fs from 'fs';
import corsPassThru from './cors/index.mjs';
import radarPassThru from './cors/radar.mjs';
import outlookPassThru from './cors/outlook.mjs';
import playlist from './src/playlist.mjs';
const app = express();
const port = process.env.WS4KP_PORT ?? 8080;
const path = require('path');
// template engine
app.set('view engine', 'ejs');
// cors pass through
const fs = require('fs');
const corsPassThru = require('./cors');
const radarPassThru = require('./cors/radar');
const outlookPassThru = require('./cors/outlook');
// cors pass-thru to api.weather.gov
app.get('/stations/*', corsPassThru);
app.get('/Conus/*', radarPassThru);
app.get('/products/*', outlookPassThru);
app.get('/playlist.json', playlist);
// version
const { version } = JSON.parse(fs.readFileSync('package.json'));
const index = (req, res) => {
res.render(path.join(__dirname, 'views/index'), {
res.render('index', {
production: false,
version,
});
@@ -32,15 +30,15 @@ const index = (req, res) => {
// debugging
if (process.env?.DIST === '1') {
// distribution
app.use('/images', express.static(path.join(__dirname, './server/images')));
app.use('/fonts', express.static(path.join(__dirname, './server/fonts')));
app.use('/scripts', express.static(path.join(__dirname, './server/scripts')));
app.use('/', express.static(path.join(__dirname, './dist')));
app.use('/images', express.static('./server/images'));
app.use('/fonts', express.static('./server/fonts'));
app.use('/scripts', express.static('./server/scripts'));
app.use('/', express.static('./dist'));
} else {
// debugging
app.get('/index.html', index);
app.get('/', index);
app.get('*', express.static(path.join(__dirname, './server')));
app.get('*', express.static('./server'));
}
const server = app.listen(port, () => {

97
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"gulp-awspublish": "^8.0.0",
"gulp-concat": "^2.6.1",
"gulp-ejs": "^5.1.0",
"gulp-file": "^0.4.0",
"gulp-htmlmin": "^5.0.1",
"gulp-rename": "^2.0.0",
"gulp-s3-upload": "^1.7.3",
@@ -5473,6 +5474,102 @@
"readable-stream": "2 || 3"
}
},
"node_modules/gulp-file": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/gulp-file/-/gulp-file-0.4.0.tgz",
"integrity": "sha512-3NPCJpAPpbNoV2aml8T96OK3Aof4pm4PMOIa1jSQbMNSNUUXdZ5QjVgLXLStjv0gg9URcETc7kvYnzXdYXUWug==",
"dev": true,
"license": "BSD",
"dependencies": {
"through2": "^0.4.1",
"vinyl": "^2.1.0"
}
},
"node_modules/gulp-file/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true,
"license": "MIT"
},
"node_modules/gulp-file/node_modules/object-keys": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz",
"integrity": "sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==",
"dev": true,
"license": "MIT"
},
"node_modules/gulp-file/node_modules/readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/gulp-file/node_modules/replace-ext": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz",
"integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/gulp-file/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
"dev": true,
"license": "MIT"
},
"node_modules/gulp-file/node_modules/through2": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz",
"integrity": "sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"readable-stream": "~1.0.17",
"xtend": "~2.1.1"
}
},
"node_modules/gulp-file/node_modules/vinyl": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz",
"integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"clone": "^2.1.1",
"clone-buffer": "^1.0.0",
"clone-stats": "^1.0.0",
"cloneable-readable": "^1.0.0",
"remove-trailing-separator": "^1.0.1",
"replace-ext": "^1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/gulp-file/node_modules/xtend": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz",
"integrity": "sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==",
"dev": true,
"dependencies": {
"object-keys": "~0.4.0"
},
"engines": {
"node": ">=0.4"
}
},
"node_modules/gulp-htmlmin": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/gulp-htmlmin/-/gulp-htmlmin-5.0.1.tgz",

View File

@@ -2,10 +2,12 @@
"name": "ws4kp",
"version": "5.14.4",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.js",
"main": "index.mjs",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build:css": "sass --style=compressed ./server/styles/scss/main.scss ./server/styles/main.css",
"build": "gulp buildDist",
"lint": "eslint ./server/scripts/**/*.mjs",
"lint:fix": "eslint --fix ./server/scripts/**/*.mjs"
},
@@ -20,32 +22,33 @@
},
"homepage": "https://github.com/netbymatt/ws4kp#readme",
"devDependencies": {
"del": "^8.0.0",
"jquery": "^3.6.0",
"jquery-touchswipe": "^1.6.19",
"luxon": "^3.0.0",
"nosleep.js": "^0.12.0",
"suncalc": "^1.8.0",
"swiped-events": "^1.1.4",
"@aws-sdk/client-cloudfront": "^3.609.0",
"gulp-awspublish": "^8.0.0",
"gulp-s3-upload": "^1.7.3",
"del": "^8.0.0",
"eslint": "^8.2.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.10.0",
"gulp": "^5.0.0",
"gulp-awspublish": "^8.0.0",
"gulp-concat": "^2.6.1",
"gulp-ejs": "^5.1.0",
"gulp-file": "^0.4.0",
"gulp-htmlmin": "^5.0.1",
"gulp-rename": "^2.0.0",
"gulp-s3-upload": "^1.7.3",
"gulp-sass": "^6.0.0",
"gulp-terser": "^2.0.0",
"jquery": "^3.6.0",
"jquery-touchswipe": "^1.6.19",
"luxon": "^3.0.0",
"nosleep.js": "^0.12.0",
"sass": "^1.54.0",
"suncalc": "^1.8.0",
"swiped-events": "^1.1.4",
"terser-webpack-plugin": "^5.3.6",
"webpack-stream": "^7.0.0",
"sass": "^1.54.0"
"webpack-stream": "^7.0.0"
},
"dependencies": {
"express": "^4.17.1",
"ejs": "^3.1.5"
"ejs": "^3.1.5",
"express": "^4.17.1"
}
}
}

View File

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 251 B

View File

Before

Width:  |  Height:  |  Size: 455 B

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
server/music/readme.txt Normal file
View File

@@ -0,0 +1,3 @@
.mp3 files placed in this folder will be available via the un-mute button in the application.
No subdirectories will be scanned, and music will be played in a random order.
The default folder will be used only if no .mp3 files are found in this /server/music folder

View File

@@ -0,0 +1,163 @@
import { json } from './utils/fetch.mjs';
import Setting from './utils/setting.mjs';
let playlist;
let currentTrack = 0;
let player;
const mediaPlaying = new Setting('mediaPlaying', 'Media Playing', 'boolean', false, null, true);
document.addEventListener('DOMContentLoaded', () => {
// add the event handler to the page
document.getElementById('ToggleMedia').addEventListener('click', toggleMedia);
// get the playlist
getMedia();
});
const getMedia = async () => {
try {
// fetch the playlist
const rawPlaylist = await json('playlist.json');
// store the playlist
playlist = rawPlaylist;
// enable the media player
enableMediaPlayer();
} catch (e) {
console.error("Couldn't get playlist");
console.error(e);
}
};
const enableMediaPlayer = () => {
// see if files are available
if (playlist?.availableFiles?.length > 0) {
// randomize the list
randomizePlaylist();
// enable the icon
const icon = document.getElementById('ToggleMedia');
icon.classList.add('available');
// set the button type
setIcon();
// if we're already playing (sticky option) then try to start playing
if (mediaPlaying.value === true) {
startMedia();
}
}
};
const setIcon = () => {
// get the icon
const icon = document.getElementById('ToggleMedia');
if (mediaPlaying.value === true) {
icon.classList.add('playing');
} else {
icon.classList.remove('playing');
}
};
const toggleMedia = (forcedState) => {
// handle forcing
if (typeof forcedState === 'boolean') {
mediaPlaying.value = forcedState;
} else {
// toggle the state
mediaPlaying.value = !mediaPlaying.value;
}
// handle the state change
stateChanged();
};
const startMedia = async () => {
// if there's not media player yet, enable it
if (!player) {
initializePlayer();
} else {
try {
await player.play();
} catch (e) {
// report the error
console.error('Couldn\'t play music');
console.error(e);
// set state back to not playing for good UI experience
mediaPlaying.value = false;
stateChanged();
}
}
};
const stopMedia = () => {
if (!player) return;
player.pause();
};
const stateChanged = () => {
// update the icon
setIcon();
// react to the new state
if (mediaPlaying.value) {
startMedia();
} else {
stopMedia();
}
};
const randomizePlaylist = () => {
let availableFiles = [...playlist.availableFiles];
const randomPlaylist = [];
while (availableFiles.length > 0) {
// get a randon item from the available files
const i = Math.floor(Math.random() * availableFiles.length);
// add it to the final list
randomPlaylist.push(availableFiles[i]);
// remove the file from the available files
availableFiles = availableFiles.filter((file, index) => index !== i);
}
playlist.availableFiles = randomPlaylist;
};
const initializePlayer = () => {
// basic sanity checks
if (!playlist.availableFiles || playlist?.availableFiles.length === 0) {
throw new Error('No playlist available');
}
if (player) {
return;
}
// create the player
player = new Audio();
// reset the playlist index
currentTrack = 0;
// add event handlers
player.addEventListener('canplay', playerCanPlay);
player.addEventListener('ended', playerEnded);
// get the first file
player.src = `music/${playlist.availableFiles[currentTrack]}`;
player.type = 'audio/mpeg';
};
const playerCanPlay = async () => {
// check to make sure they user still wants music (protect against slow loading music)
if (!mediaPlaying.value) return;
// start playing
startMedia();
};
const playerEnded = () => {
// next track
currentTrack += 1;
// roll over and re-randomize the tracks
if (currentTrack >= playlist.availableFiles) {
randomizePlaylist();
currentTrack = 0;
}
// update the player source
player.src = `music/${playlist.availableFiles[currentTrack]}`;
};
export {
// eslint-disable-next-line import/prefer-default-export
toggleMedia,
};

View File

@@ -10,7 +10,7 @@ const settings = { speed: { value: 1.0 } };
const init = () => {
// create settings
settings.wide = new Setting('wide', 'Widescreen', 'checkbox', false, wideScreenChange, true);
settings.kiosk = new Setting('kiosk', 'Kiosk', 'boolean', false, kioskChange, false);
settings.kiosk = new Setting('kiosk', 'Kiosk', 'checkbox', false, kioskChange, false);
settings.speed = new Setting('speed', 'Speed', 'select', 1.0, null, true, [
[0.5, 'Very Fast'],
[0.75, 'Fast'],

View File

@@ -167,6 +167,8 @@ class Setting {
case 'select':
this.selectHighlight(newValue);
break;
case 'boolean':
break;
case 'checkbox':
default:
this.element.checked = newValue;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
.media {
display: none;
}
#ToggleMedia {
display: none;
&.available {
display: inline-block;
img.on {
display: none;
}
img.off {
display: block;
}
// icon switch is handled by adding/removing the .playing class
&.playing {
img.on {
display: block;
}
img.off {
display: none;
}
}
}
}

View File

@@ -11,4 +11,5 @@
@import 'radar';
@import 'regional-forecast';
@import 'almanac';
@import 'hazards';
@import 'hazards';
@import 'media';

20
src/playlist-reader.mjs Normal file
View File

@@ -0,0 +1,20 @@
import fs from 'fs/promises';
const mp3Filter = (file) => file.match(/\.mp3$/);
const reader = async () => {
// get the listing of files in the folder
const rawFiles = await fs.readdir('./server/music');
// filter for mp3 files
const files = rawFiles.filter(mp3Filter);
// if files were found return them
if (files.length > 0) {
return files;
}
// fall back to the default folder
const defaultFiles = await fs.readdir('./server/music/default');
return defaultFiles.map((file) => `default/${file}`).filter(mp3Filter);
};
export default reader;

17
src/playlist.mjs Normal file
View File

@@ -0,0 +1,17 @@
import reader from './playlist-reader.mjs';
const playlistGenerator = async (req, res) => {
try {
const availableFiles = await reader();
res.json({
availableFiles,
});
} catch (e) {
console.error(e);
res.json({
availableFiles: [],
});
}
};
export default playlistGenerator;

View File

@@ -14,6 +14,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="manifest" href="manifest.json" />
<link rel="icon" href="images/Logo192.png" />
<link rel="preload" href="playlist.json" />
<meta property="og:image" content="https://weatherstar.netbymatt.com/images/social/1200x600.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="627">
@@ -46,6 +47,7 @@
<script type="module" src="scripts/modules/progress.mjs"></script>
<script type="module" src="scripts/modules/radar.mjs"></script>
<script type="module" src="scripts/modules/settings.mjs"></script>
<script type="module" src="scripts/modules/media.mjs"></script>
<script type="module" src="scripts/index.mjs"></script>
<!-- data -->
<script type="text/javascript" src="scripts/data/travelcities.js"></script>
@@ -129,6 +131,10 @@
<img id="NavigateRefresh" class="navButton" src="images/nav/ic_refresh_white_24dp_2x.png" title="Refresh" />
</div>
<div id="divTwcBottomRight">
<div id="ToggleMedia">
<img class="navButton off" src="images/nav/ic_volume_off_white_24dp_2x.png" title="Unmute" />
<img class="navButton on" src="images/nav/ic_volume_on_white_24dp_2x.png" title="Unmute" />
</div>
<img id="ToggleFullScreen" class="navButton" src="images/nav/ic_fullscreen_white_24dp_2x.png" title="Enter Fullscreen" />
</div>
</div>
@@ -139,6 +145,7 @@
<div class="info">
<a href="https://github.com/netbymatt/ws4kp#weatherstar-4000">More information</a>
</div>
<div class="media"></div>
<div class='heading'>Selected displays</div>
<div id='enabledDisplays'>