mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
- Replace CSS zoom with CSS transform scaling for better mobile compatibility - Implement wrapper-based scaling approach that includes both content and navigation bar - Replace Almanac layout with CSS Grid for better cross-browser layout - Greatly improve scanline algorithm to handle a wide variety of displays - Add setting to override automatic scanlines to user-specified scale factor - Remove scanline scaling debug functions - Refactor settings module: initialize settings upfront and improve change handler declarations - Enhance scanline SCSS with repeating-linear-gradient for better performance - Add app icon for iOS/iPadOS - Add 'fullscreen' event listener - De-bounce 'resize' event listener - Add 'orientationchange' event listener - Implement three resize scaling algorithms: - Baseline (when no scaling is needed, like on the index page) - Mobile scaling (except Mobile Safari kiosk mode) - Mobile Safari kiosk mode (using manual offset calculations) - Standard fullscreen/kiosk mode (using CSS centering)
226 lines
5.8 KiB
JavaScript
226 lines
5.8 KiB
JavaScript
import { parseQueryString } from '../share.mjs';
|
|
|
|
const SETTINGS_KEY = 'Settings';
|
|
|
|
const DEFAULTS = {
|
|
shortName: undefined,
|
|
name: undefined,
|
|
type: 'checkbox',
|
|
defaultValue: undefined,
|
|
changeAction: () => { },
|
|
sticky: true,
|
|
values: [],
|
|
visible: true,
|
|
};
|
|
|
|
class Setting {
|
|
constructor(shortName, _options) {
|
|
if (shortName === undefined) {
|
|
throw new Error('No name provided for setting');
|
|
}
|
|
// merge options with defaults
|
|
const options = { ...DEFAULTS, ...(_options ?? {}) };
|
|
|
|
// store values and combine with defaults
|
|
this.shortName = shortName;
|
|
this.name = options.name ?? shortName;
|
|
this.defaultValue = options.defaultValue;
|
|
this.myValue = this.defaultValue;
|
|
this.type = options?.type;
|
|
this.sticky = options.sticky;
|
|
this.values = options.values;
|
|
this.visible = options.visible;
|
|
this.changeAction = options.changeAction;
|
|
|
|
// get value from url
|
|
const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`];
|
|
let urlState;
|
|
if (this.type === 'checkbox' && urlValue !== undefined) {
|
|
urlState = urlValue === 'true';
|
|
}
|
|
if (this.type === 'boolean' && urlValue !== undefined) {
|
|
urlState = urlValue === 'true';
|
|
}
|
|
if (this.type === 'select' && urlValue !== undefined) {
|
|
urlState = parseFloat(urlValue);
|
|
}
|
|
if (this.type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
|
|
// couldn't parse as a float, store as a string
|
|
urlState = urlValue;
|
|
}
|
|
|
|
// get existing value if present
|
|
const storedValue = urlState ?? this.getFromLocalStorage();
|
|
if ((this.sticky || urlValue !== undefined) && storedValue !== null) {
|
|
this.myValue = storedValue;
|
|
}
|
|
|
|
// call the change function on startup
|
|
switch (this.type) {
|
|
case 'select':
|
|
this.selectChange({ target: { value: this.myValue } });
|
|
break;
|
|
case 'checkbox':
|
|
default:
|
|
this.checkboxChange({ target: { checked: this.myValue } });
|
|
}
|
|
}
|
|
|
|
generateSelect() {
|
|
// create a radio button set in the selected displays area
|
|
const label = document.createElement('label');
|
|
label.for = `settings-${this.shortName}-select`;
|
|
label.id = `settings-${this.shortName}-label`;
|
|
|
|
const span = document.createElement('span');
|
|
span.innerHTML = `${this.name} `;
|
|
label.append(span);
|
|
|
|
const select = document.createElement('select');
|
|
select.id = `settings-${this.shortName}-select`;
|
|
select.name = `settings-${this.shortName}-select`;
|
|
select.addEventListener('change', (e) => this.selectChange(e));
|
|
|
|
this.values.forEach(([value, text]) => {
|
|
const option = document.createElement('option');
|
|
if (typeof value === 'number') {
|
|
option.value = value.toFixed(2);
|
|
} else {
|
|
option.value = value;
|
|
}
|
|
|
|
option.innerHTML = text;
|
|
select.append(option);
|
|
});
|
|
label.append(select);
|
|
|
|
this.element = label;
|
|
|
|
// set the initial value
|
|
this.selectHighlight(this.myValue);
|
|
|
|
return label;
|
|
}
|
|
|
|
generateCheckbox() {
|
|
// create a checkbox in the selected displays area
|
|
const label = document.createElement('label');
|
|
label.for = `settings-${this.shortName}-checkbox`;
|
|
label.id = `settings-${this.shortName}-label`;
|
|
const checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.value = true;
|
|
checkbox.id = `settings-${this.shortName}-checkbox`;
|
|
checkbox.name = `settings-${this.shortName}-checkbox`;
|
|
checkbox.checked = this.myValue;
|
|
checkbox.addEventListener('change', (e) => this.checkboxChange(e));
|
|
const span = document.createElement('span');
|
|
span.innerHTML = this.name;
|
|
|
|
label.append(checkbox, span);
|
|
|
|
this.element = label;
|
|
|
|
return label;
|
|
}
|
|
|
|
checkboxChange(e) {
|
|
// update the state
|
|
this.myValue = e.target.checked;
|
|
this.storeToLocalStorage(this.myValue);
|
|
|
|
// call change action
|
|
this.changeAction(this.myValue);
|
|
}
|
|
|
|
selectChange(e) {
|
|
// update the value
|
|
this.myValue = parseFloat(e.target.value);
|
|
if (Number.isNaN(this.myValue)) {
|
|
// was a string, store as such
|
|
this.myValue = e.target.value;
|
|
}
|
|
this.storeToLocalStorage(this.myValue);
|
|
|
|
// call the change action
|
|
this.changeAction(this.myValue);
|
|
}
|
|
|
|
storeToLocalStorage(value) {
|
|
if (!this.sticky) return;
|
|
const allSettingsString = localStorage?.getItem(SETTINGS_KEY) ?? '{}';
|
|
const allSettings = JSON.parse(allSettingsString);
|
|
allSettings[this.shortName] = value;
|
|
localStorage?.setItem(SETTINGS_KEY, JSON.stringify(allSettings));
|
|
}
|
|
|
|
getFromLocalStorage() {
|
|
const allSettings = localStorage?.getItem(SETTINGS_KEY);
|
|
try {
|
|
if (allSettings) {
|
|
const storedValue = JSON.parse(allSettings)?.[this.shortName];
|
|
if (storedValue !== undefined) {
|
|
switch (this.type) {
|
|
case 'boolean':
|
|
case 'checkbox':
|
|
return storedValue;
|
|
case 'select':
|
|
return storedValue;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Failed to parse settings from localStorage: ${error} - allSettings=${allSettings}`);
|
|
localStorage?.removeItem(SETTINGS_KEY);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
get value() {
|
|
return this.myValue;
|
|
}
|
|
|
|
set value(newValue) {
|
|
// update the state
|
|
this.myValue = newValue;
|
|
switch (this.type) {
|
|
case 'select':
|
|
this.selectHighlight(newValue);
|
|
break;
|
|
case 'boolean':
|
|
break;
|
|
case 'checkbox':
|
|
default:
|
|
this.element.querySelector('input').checked = newValue;
|
|
}
|
|
this.storeToLocalStorage(this.myValue);
|
|
|
|
// call change action
|
|
this.changeAction(this.myValue);
|
|
}
|
|
|
|
selectHighlight(newValue) {
|
|
// set the dropdown to the provided value
|
|
this?.element?.querySelectorAll('option')?.forEach?.((elem) => {
|
|
elem.selected = (newValue?.toFixed?.(2) === elem.value) || (newValue === elem.value);
|
|
});
|
|
}
|
|
|
|
generate() {
|
|
// don't generate a control for not visible items
|
|
if (!this.visible) return '';
|
|
// call the appropriate control generator
|
|
switch (this.type) {
|
|
case 'select':
|
|
return this.generateSelect();
|
|
case 'checkbox':
|
|
default:
|
|
return this.generateCheckbox();
|
|
}
|
|
}
|
|
}
|
|
|
|
export default Setting;
|