autobrr/web/src/utils/Context.ts
soup 24648e45f7
feat(web): persist releases incognito state (#2042)
* refactor(web): persist incognito state

* feat: merge incognito state into SettingsContext

* feat: merge incognito state into SettingsContext
2025-05-04 19:36:39 +02:00

150 lines
3.8 KiB
TypeScript

/*
* Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import type { StateWithValue } from "react-ridge-state";
import { newRidgeState } from "react-ridge-state";
interface SettingsType {
debug: boolean;
darkTheme: boolean;
scrollOnNewLog: boolean;
indentLogLines: boolean;
hideWrappedText: boolean;
incognitoMode: boolean;
}
export type FilterListState = {
indexerFilter: string[];
sortOrder: string;
status: string;
};
export interface AuthInfo {
username: string;
isLoggedIn: boolean;
authMethod?: 'password' | 'oidc';
profilePicture?: string;
issuerUrl?: string;
}
// Default values
const AuthContextDefaults: AuthInfo = {
username: "",
isLoggedIn: false,
authMethod: undefined,
profilePicture: undefined,
issuerUrl: undefined
};
const SettingsContextDefaults: SettingsType = {
debug: false,
darkTheme: window.matchMedia('(prefers-color-scheme: dark)').matches,
scrollOnNewLog: false,
indentLogLines: false,
hideWrappedText: false,
incognitoMode: false
};
const FilterListContextDefaults: FilterListState = {
indexerFilter: [],
sortOrder: "",
status: ""
};
// eslint-disable-next-line
function ContextMerger<T extends {}>(
key: string,
defaults: T,
ctxState: StateWithValue<T>
) {
let values = structuredClone(defaults);
const storage = localStorage.getItem(key);
if (storage) {
try {
const json = JSON.parse(storage);
if (json === null) {
console.warn(`JSON localStorage value for '${key}' context state is null`);
} else {
values = { ...values, ...json };
}
} catch (e) {
console.error(`Failed to merge ${key} context state: ${e}`);
}
}
ctxState.set(values);
}
const AuthKey = "autobrr_user_auth";
const SettingsKey = "autobrr_settings";
const FilterListKey = "autobrr_filter_list";
export const InitializeGlobalContext = () => {
ContextMerger<AuthInfo>(AuthKey, AuthContextDefaults, AuthContext);
ContextMerger<SettingsType>(
SettingsKey,
SettingsContextDefaults,
SettingsContext
);
ContextMerger<FilterListState>(
FilterListKey,
FilterListContextDefaults,
FilterListContext
);
};
function DefaultSetter<T>(name: string, newState: T, prevState: T) {
try {
localStorage.setItem(name, JSON.stringify(newState));
} catch (e) {
console.error(
`An error occurred while trying to modify '${name}' context state: ${e}`
);
console.warn(` --> prevState: ${prevState}`);
console.warn(` --> newState: ${newState}`);
}
}
export const AuthContext = newRidgeState<AuthInfo>(
AuthContextDefaults,
{
onSet: (newState, prevState) => DefaultSetter(AuthKey, newState, prevState)
}
);
export const SettingsContext = newRidgeState<SettingsType>(
SettingsContextDefaults,
{
onSet: (newState, prevState) => {
document.documentElement.classList.toggle("dark", newState.darkTheme);
DefaultSetter(SettingsKey, newState, prevState);
updateMetaThemeColor(newState.darkTheme);
}
}
);
/**
* Updates the meta theme color based on the current theme state.
* Used by Safari to color the compact tab bar on both iOS and MacOS.
*/
const updateMetaThemeColor = (darkTheme: boolean) => {
const color = darkTheme ? '#121315' : '#f4f4f5';
let metaThemeColor: HTMLMetaElement | null = document.querySelector('meta[name="theme-color"]');
if (!metaThemeColor) {
metaThemeColor = document.createElement('meta') as HTMLMetaElement;
metaThemeColor.name = "theme-color";
document.head.appendChild(metaThemeColor);
}
metaThemeColor.content = color;
};
export const FilterListContext = newRidgeState<FilterListState>(
FilterListContextDefaults,
{
onSet: (newState, prevState) => DefaultSetter(FilterListKey, newState, prevState)
}
);