diff --git a/web/package-lock.json b/web/package-lock.json index 6907bcb2..23441713 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@fontsource-variable/noto-sans-mono": "^5.0.20", "@fontsource/ibm-plex-mono": "^5.0.13", - "@tabler/icons-svelte": "^3.6.0" + "@tabler/icons-svelte": "^3.6.0", + "ts-deepmerge": "^7.0.0" }, "devDependencies": { "@eslint/js": "^9.5.0", @@ -3048,6 +3049,15 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-deepmerge": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.0.tgz", + "integrity": "sha512-WZ/iAJrKDhdINv1WG6KZIGHrZDar6VfhftG1QJFpVbOYZMYJLJOvZOo1amictRXVdBXZIgBHKswMTXzElngprA==", + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", diff --git a/web/package.json b/web/package.json index af5ab742..eda7e04a 100644 --- a/web/package.json +++ b/web/package.json @@ -36,6 +36,7 @@ "dependencies": { "@fontsource-variable/noto-sans-mono": "^5.0.20", "@fontsource/ibm-plex-mono": "^5.0.13", - "@tabler/icons-svelte": "^3.6.0" + "@tabler/icons-svelte": "^3.6.0", + "ts-deepmerge": "^7.0.0" } } diff --git a/web/src/lib/settings.ts b/web/src/lib/settings.ts new file mode 100644 index 00000000..cdbf607b --- /dev/null +++ b/web/src/lib/settings.ts @@ -0,0 +1,42 @@ +import { readable, type Updater } from 'svelte/store'; +import { merge } from 'ts-deepmerge'; + +import type { RecursivePartial } from './types/generic'; +import type { CobaltSettings } from './types/settings'; + +import defaultSettings from "$lib/settings/defaults"; + +const writeToStorage = (settings: CobaltSettings) => { + localStorage.setItem( + "settings", + JSON.stringify(settings) + ); + + return settings; +} + +const loadFromStorage = () => { + const settings = localStorage.getItem('settings'); + if (!settings) { + return writeToStorage(defaultSettings); + } + + return JSON.parse(settings) as CobaltSettings; +} + +let update: (_: Updater) => void; + +export default readable( + loadFromStorage(), + (_, _update) => { update = _update } +); + +// update settings from outside +export function updateSetting(settings: RecursivePartial) { + update((current) => { + // deep merge partial type into full CobaltSettings type + current = merge(current, settings) as CobaltSettings; + + return writeToStorage(current); + }); +} diff --git a/web/src/lib/settings/defaults.ts b/web/src/lib/settings/defaults.ts new file mode 100644 index 00000000..ddaa83be --- /dev/null +++ b/web/src/lib/settings/defaults.ts @@ -0,0 +1,35 @@ +import type { CobaltSettings } from "$lib/types/settings"; + +const defaultSettings: CobaltSettings = { + schemaVersion: 1, + accessibility: { + reduceAnimations: false, + reduceTransparency: false + }, + appearance: { + theme: "auto" + }, + general: { + customProcessingEndpoint: "", + seenOnboarding: false, + seenSafetyWarning: false + }, + save: { + audioFormat: "mp3", + disableMetadata: false, + downloadMode: "auto", + downloadPopup: true, + filenameStyle: "classic", + tiktokH265: false, + tiktokFullAudio: false, + twitterGif: false, + videoQuality: "720", + youtubeVideoCodec: "h264", + youtubeDubBrowserLang: false + }, + privacy: { + trafficAnalytics: true + } +} + +export default defaultSettings; diff --git a/web/src/lib/types/generic.ts b/web/src/lib/types/generic.ts new file mode 100644 index 00000000..c2927964 --- /dev/null +++ b/web/src/lib/types/generic.ts @@ -0,0 +1,8 @@ +// more readable version of recursive partial taken from stackoverflow: +// https://stackoverflow.com/a/51365037 +export type RecursivePartial = { + [Key in keyof Type]?: + Type[Key] extends (infer ElementType)[] ? RecursivePartial[] : + Type[Key] extends object | undefined ? RecursivePartial : + Type[Key]; +}; diff --git a/web/src/lib/types/settings.ts b/web/src/lib/types/settings.ts new file mode 100644 index 00000000..a65e7843 --- /dev/null +++ b/web/src/lib/types/settings.ts @@ -0,0 +1,41 @@ +type CobaltSettingsAccessibility = { + reduceAnimations: boolean, + reduceTransparency: boolean, +}; + +type CobaltSettingsAppearance = { + theme: "auto" | "light" | "dark", +}; + +type CobaltSettingsGeneral = { + customProcessingEndpoint: string, + seenOnboarding: boolean, + seenSafetyWarning: boolean, +}; + +type CobaltSettingsSave = { + audioFormat: "best" | "mp3" | "ogg" | "wav" | "opus", + disableMetadata: boolean, + downloadMode: "auto" | "audio" | "mute", + downloadPopup: boolean, + filenameStyle: "classic" | "basic" | "pretty" | "nerdy", + tiktokH265: boolean, + tiktokFullAudio: boolean, + twitterGif: boolean, + videoQuality: "max" | "2160" | "1440" | "1080" | "720" | "360" | "240" | "144", + youtubeVideoCodec: "h264" | "av1" | "vp9", + youtubeDubBrowserLang: boolean, +}; + +type CobaltSettingsPrivacy = { + trafficAnalytics: boolean, +}; + +export type CobaltSettings = { + schemaVersion: number, + accessibility: CobaltSettingsAccessibility, + appearance: CobaltSettingsAppearance, + general: CobaltSettingsGeneral, + save: CobaltSettingsSave, + privacy: CobaltSettingsPrivacy, +};