Files
gpx.studio/website/src/lib/state/setting.ts
T
vcoppeandvcoppe 8fe7d8d64d Migration to new Rust-based engine (#401)
* wasm first steps

* parsing start

* parsing continued

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* fixes

* reorganize

* change type

* fix

* progress

* refactoring

* progress

* progress

* progress

* keep style info

* progress

* progress

* remove (copy) in name

* progress

* merge methods into 1

* splice

* progress

* progress

* progress

* cleaning

* progress

* progress

* progress

* visibility

* progress

* progress

* progress

* progress

* progress

* fixes

* progress

* fix markers

* fix

* progress

* progress

* progress

* progress

* progress

* progress

* small fix

* use engine info

* fix popups

* small fix

* progress

* progress

* progress

* clean tool

* handle self closing elements

* reduce data exchanges across boundary

* copy on wasm side to avoid any data corruption

* speed fix

* routing

* refactoring

* reverse

* reverse

* crop

* split

* merge

* progress

* time tool

* fix bounds for waypoints

* reduce

* fix reactivity

* handle corrupt time data

* export

* progress

* remove gpx js package

* remove todo

* update readme

* cleaning

* progress

* fix link fields

* remove unused fields

* refactoring and tests

* import and export of routes, ref #192

* per-chunk categories storage

* cross-tab sync

* outdated state

* refactoring

* reorder methods

* refactoring

* fix

* improve flow

* renaming

* progress

* progress

* fix

* progress

* per file history

* keep original flag

* use history variant

* fix

* progress

* file explorer

* progress

* fix

* sticky folders

* improved sticky folders

* fix sortable

* fix sortable

* fix

* fix tab size

* no file delete on desktop

* fix routing on other file

* fix sorting

* remember language

* open links in browser

* commit missing changes

* avoid selecting on resize

* clarify filename

* explorer actions

* fix folder styling

* drop anywhere in folder

* cleaning

* fixes

* remove focus-ring

* full screen

* small fix

* consistent highlight color

* improved preview

* performance improvements

* sparser data transfers

* sparser data transfers

* show popup once data is loaded

* fix routing control size

* fix

* sparse desktop build

* remove custom ign styles

* use new endpoint for openhikingmap

* strip native menu

* reorder context menu items

* route full support

* route - track conversion

* create route file action

* route export, filter out anchors at high zoom level

* clarify name handling

* move hide & center to view

* routes continued

* update docs

* fix power format, closes #378

* lint fixes

* fix display of additional data with gaps, closes #361

* fix linting issues

* fix import modules

* fix multidrag

* change base to resolve

* fix type errors

* fix layer typings

* fix last type error

* each keys

* improve typings

* deploy to cloudflare

---------

Co-authored-by: vcoppe <vcoppe@hexaly.com>
2026-10-31 18:08:22 +01:00

231 lines
8.0 KiB
TypeScript

import { get, writable, type Writable } from 'svelte/store';
import type { LayerTreeType } from '$lib/data/layers';
/** What the settings were saved as, by key: JSON strings (see `Engine.openStorage`). */
export type StoredSettings = Record<string, string>;
/** Where the settings are kept. */
export type SettingsStorage = {
/** Keeps the JSON of a setting. */
save(key: string, json: string): void;
/** Forgets a setting. */
remove(key: string): void;
};
let storage: SettingsStorage | undefined;
/** Sets where the settings are kept: until then, changes are not saved. */
export function useSettingsStorage(newStorage: SettingsStorage | undefined) {
storage = newStorage;
}
/** Reads the saved value of a setting, `undefined` if there is none or if it cannot be read. */
export function storedValue(stored: StoredSettings, key: string): { value: unknown } | undefined {
if (!Object.hasOwn(stored, key)) {
return undefined;
}
try {
return { value: JSON.parse(stored[key]) };
} catch {
return undefined;
}
}
/** Keeps a value; `undefined` is not something JSON can hold, it means no value. */
function save(key: string, value: unknown) {
const json = JSON.stringify(value);
if (json === undefined) {
storage?.remove(key);
} else {
storage?.save(key, json);
}
}
/**
* Makes a saved value one that the setting can hold: what was saved can be anything (an old
* format, a value that is not allowed anymore), the validator checks it.
*/
export type Validator<V> = (value: unknown) => V;
/**
* The validator of a setting: a boolean or a number can do without one, its saved value is then
* checked against the type of its initial value. The other settings say what they can hold: a
* string is often one of a few, an object has a shape.
*/
type ValidatorArgument<V> = [V] extends [boolean] | [number]
? [validator?: Validator<V>]
: [validator: Validator<V>];
/** Keeps a saved value that has the type of the initial one (a boolean or a number). */
function sameTypeAs<V>(initial: V): Validator<V> {
return (value) => (typeof value === typeof initial ? (value as V) : initial);
}
/**
* A setting: a store whose value is kept when it changes. `S` is what the store holds, which is
* `V` or, for a setting that has no value until it is read, `V | undefined`.
*/
abstract class SettingBase<V, S extends V | undefined> {
protected _key: string;
protected _value: Writable<S>;
protected _validator: Validator<V>;
protected constructor(key: string, initial: S, validator: Validator<V>) {
this._key = key;
this._value = writable(initial);
this._validator = validator;
}
/** The saved value, validated, `undefined` if there is none. */
protected saved(stored: StoredSettings): { value: V } | undefined {
const saved = storedValue(stored, this._key);
return saved && { value: this._validator(saved.value) };
}
/** Takes the saved value, if there is one. */
abstract connect(stored: StoredSettings): void;
subscribe(run: (value: S) => void, invalidate?: (value?: S) => void) {
return this._value.subscribe(run, invalidate);
}
/** Sets the value and keeps it. An object is always kept: it may have been changed in place. */
set(value: V) {
if (typeof value === 'object' || value !== get(this._value)) {
this._value.set(value as S);
save(this._key, value);
}
}
update(callback: (value: any) => any) {
this.set(callback(get(this._value)));
}
}
export class Setting<V> extends SettingBase<V, V> {
constructor(key: string, initial: V, ...[validator]: ValidatorArgument<V>) {
super(key, initial, validator ?? sameTypeAs(initial));
}
connect(stored: StoredSettings) {
const saved = this.saved(stored);
if (saved) {
this._value.set(saved.value);
}
}
}
/** A setting that has no value until it is connected: then the saved one, or the initial one. */
export class SettingInitOnFirstRead<V> extends SettingBase<V, V | undefined> {
private _initial: V;
constructor(key: string, initial: V, ...[validator]: ValidatorArgument<V>) {
super(key, undefined, validator ?? sameTypeAs(initial));
this._initial = initial;
}
connect(stored: StoredSettings) {
this._value.set(this.saved(stored)?.value ?? this._initial);
}
/** Takes the initial value and keeps it, for a setting that was never saved. */
initialize() {
this.set(this._initial);
}
}
/** Whether a value is one of the allowed ones. */
function oneOf<V>(allowed: V[]) {
const dict = new Set<unknown>(allowed);
return (value: unknown): value is V => dict.has(value);
}
export function getValueValidator<V>(allowed: V[], fallback: V): Validator<V> {
const isAllowed = oneOf(allowed);
return (value) => (isAllowed(value) ? value : fallback);
}
export function isString(value: unknown): value is string {
return typeof value === 'string';
}
export function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
/** Keeps the items of a saved list that are what it holds, in their order; nothing if it is not a list. */
export function getListValidator<V>(isItem: (value: unknown) => value is V): Validator<V[]> {
return (value) => (Array.isArray(value) ? value.filter(isItem) : []);
}
/** Keeps the allowed values of a list, in their order; nothing if what was saved is not a list. */
export function getArrayValidator<V>(allowed: V[]): Validator<V[]> {
return getListValidator(oneOf(allowed));
}
/** Keeps the entries of a saved record that are what it holds; `fallback` if it is not a record. */
export function getRecordValidator<V>(
isEntry: (value: unknown) => value is V,
fallback: Record<string, V>
): Validator<Record<string, V>> {
return (value) =>
isRecord(value)
? Object.fromEntries(
Object.entries(value).filter((entry): entry is [string, V] => isEntry(entry[1]))
)
: fallback;
}
export function getLayerValidator(
allowed: Record<string, unknown>,
fallback: string
): Validator<string> {
return (layer) =>
typeof layer === 'string' &&
(Object.hasOwn(allowed, layer) ||
layer.startsWith('custom-') ||
layer.startsWith('extension-'))
? layer
: fallback;
}
/** Whether a value is a record, which can hold the layers of a tree: an object, not null or a list. */
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* The saved tree `t`, with the layers of `allowed`: the choices saved for them are kept, the new
* ones have their default, and of the others only the custom and extension layers are kept.
*/
export function filterLayerTree(t: unknown, allowed: LayerTreeType | undefined): LayerTreeType {
const tree = isRecord(t) ? t : {};
const filtered: LayerTreeType = {};
if (allowed) {
Object.entries(allowed).forEach(([key, value]) => {
const saved = Object.hasOwn(tree, key) ? tree[key] : undefined;
if (typeof value === 'boolean') {
filtered[key] = typeof saved === 'boolean' ? saved : value;
} else {
filtered[key] = filterLayerTree(saved, value);
}
});
}
Object.entries(tree).forEach(([key, value]) => {
if (!Object.hasOwn(filtered, key)) {
if (typeof value === 'boolean') {
if (key.startsWith('custom-') || key.startsWith('extension-')) {
filtered[key] = value;
}
} else if (isRecord(value)) {
filtered[key] = filterLayerTree(value, undefined);
}
}
});
return filtered;
}
export function getLayerTreeValidator(allowed: LayerTreeType): Validator<LayerTreeType> {
return (value) => filterLayerTree(value, allowed);
}