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.
60 lines
2.2 KiB
TypeScript
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];
|
|
}
|