goon/mobile/src/lib/persistedState.ts
goon-foss 9c988bc12a feat(mobile): przypinanie stron w zakladce Sites + filtry zapamietywane per sekcja
Dwie prosby z apki 2026-09-02.

Sites: dlugie nacisniecie przypina albo odpina strone (gwiazdka i akcent na ramce),
przypiete ida na gore listy w tej samej kolejnosci co reszta, takze przy szukaniu i przy
sortowaniu "Recent". Stan po stronie serwera (/favorites/sites), wiec przechodzi kodem
synchronizacji jak inne ulubione.

Filtry: `usePersistedState` w SecureStore, osobny klucz dla Scenes, JAV, Anime
(`filters_<sekcja>`) i Movies. Zapis dopiero po wczytaniu, zeby domyslne z pierwszego
renderu nie nadpisaly zapisanych; odczyt scalany z domyslnymi, zeby nowe pole w kolejnej
wersji dostalo wartosc. Zapytanie o liste czeka na wczytanie (`enabled`), bez podwojnego
strzalu. SecureStore ma ~2 KB na klucz; przy bardzo dlugiej liscie tagow zapis moze sie
nie udac i wtedy filtr zyje tylko do zamkniecia apki, ale ekran sie nie wywraca.

tsc: 11 bledow przed i po (baza). Wpis w changelogu 2026-09-02b.
2026-09-02 22:44:54 +02:00

60 lines
2.2 KiB
TypeScript

// Stan ekranu zapamietywany miedzy sesjami (prosba z apki 2026-09-02: filtry znikaly
// po zamknieciu apki). SecureStore, bo to jedyny magazyn w projekcie; wartosc to JSON.
//
// Dwie zasady:
// 1. Zapis dopiero PO wczytaniu. Inaczej pierwszy render z domyslna wartoscia nadpisalby
// to, co user zapisal wczesniej, zanim odczyt zdazyl wrocic.
// 2. Odczyt scalany z domyslnymi (`{...defaults, ...saved}`), zeby nowe pole dodane w
// kolejnej wersji dostalo swoja wartosc domyslna, a nie `undefined`.
//
// SecureStore na Androidzie ostrzega powyzej ~2 KB na klucz. Filtry to kilkaset bajtow,
// ale przy bardzo dlugiej liscie tagow zapis moze sie nie udac; wtedy zostaje stan w
// pamieci, a blad jest polykany, bo utrata zapamietanego filtra to nie powod, zeby
// wywracac ekran.
import * as SecureStore from 'expo-secure-store';
import React from 'react';
const PREFIX = 'persisted_v1_';
function keyFor(name: string): string {
// SecureStore przyjmuje tylko [A-Za-z0-9._-]; dwukropki i spacje odpadaja.
return PREFIX + name.replace(/[^A-Za-z0-9._-]/g, '_');
}
export function usePersistedState<T extends object>(
name: string,
defaults: T,
): [T, React.Dispatch<React.SetStateAction<T>>, boolean] {
const [value, setValue] = React.useState<T>(defaults);
const [hydrated, setHydrated] = React.useState(false);
const key = keyFor(name);
React.useEffect(() => {
let cancelled = false;
(async () => {
try {
const raw = await SecureStore.getItemAsync(key);
if (!cancelled && raw) {
const saved = JSON.parse(raw);
if (saved && typeof saved === 'object') setValue({ ...defaults, ...saved });
}
} catch {
// uszkodzony zapis albo brak SecureStore: zostaja domyslne
} finally {
if (!cancelled) setHydrated(true);
}
})();
return () => {
cancelled = true;
};
// `defaults` to stala modulu, `key` zmienia sie tylko z sekcja ekranu.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key]);
React.useEffect(() => {
if (!hydrated) return;
SecureStore.setItemAsync(key, JSON.stringify(value)).catch(() => {});
}, [key, value, hydrated]);
return [value, setValue, hydrated];
}