goon/mobile/src/storage.ts
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

170 lines
6.6 KiB
TypeScript

import * as SecureStore from 'expo-secure-store';
const URL_KEY = 'goon.backend_url';
const API_KEY = 'goon.api_key';
const DEVICE_KEY = 'goon.device_id';
// Stabilne ID instalacji, scope'uje stan usera (favorites/progress/blacklisty) na
// serwerze per urządzenie (bug 2026-06-08: stan był globalny, nowi userzy nadpisywali
// ulubione). Generowane raz, trzymane w SecureStore (przeżywa restart, NIE reinstalację).
let _deviceIdCache: string | null = null;
function _genDeviceId(): string {
// 32 hex chars, wystarczająco unikalne dla ID instalacji (nie security-critical).
let s = '';
for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16);
return s;
}
export async function getDeviceId(): Promise<string> {
if (_deviceIdCache) return _deviceIdCache;
let id = await SecureStore.getItemAsync(DEVICE_KEY);
if (!id) {
id = _genDeviceId();
await SecureStore.setItemAsync(DEVICE_KEY, id);
}
_deviceIdCache = id;
return id;
}
// Jednorazowe przejęcie legacy stanu (sprzed device-scopingu), flaga per instalacja.
const LEGACY_ADOPTED_KEY = 'goon.legacy_adopted';
export async function isLegacyAdopted(): Promise<boolean> {
return (await SecureStore.getItemAsync(LEGACY_ADOPTED_KEY)) === '1';
}
export async function markLegacyAdopted(): Promise<void> {
await SecureStore.setItemAsync(LEGACY_ADOPTED_KEY, '1');
}
// "What's new", ostatnio widziany wpis changelogu. Po dociągnięciu OTA bundle ma
// nowszy changelog → WhatsNewModal pokazuje różnicę i zapisuje najnowszy id.
const CHANGELOG_SEEN_KEY = 'goon.changelog_seen_id';
export async function getLastSeenChangelog(): Promise<string | null> {
return SecureStore.getItemAsync(CHANGELOG_SEEN_KEY);
}
export async function setLastSeenChangelog(id: string): Promise<void> {
await SecureStore.setItemAsync(CHANGELOG_SEEN_KEY, id);
}
// Onboarding/tutorial, pokazany raz przy pierwszym odpaleniu (carousel: tabs,
// funkcje, longpressy, gesty playera). Wersjonowany klucz → bump gdy tutorial
// istotnie się zmieni i chcemy pokazać ponownie. Replay z ustawień przez bus.
const ONBOARDING_SEEN_KEY = 'goon.onboarding_seen_v1';
export async function getOnboardingSeen(): Promise<boolean> {
try {
return (await SecureStore.getItemAsync(ONBOARDING_SEEN_KEY)) === '1';
} catch {
return true; // brak SecureStore → nie blokuj UI tutorialem w kółko
}
}
export async function setOnboardingSeen(seen: boolean): Promise<void> {
try {
if (seen) await SecureStore.setItemAsync(ONBOARDING_SEEN_KEY, '1');
else await SecureStore.deleteItemAsync(ONBOARDING_SEEN_KEY);
} catch {
// ignore, keychain quirks
}
}
// Liczba kolumn w siatkach scen (1/2/3). Feedback mobilism: domyślne 2 kol = za małe
// miniaturki na telefonie do rozpoznania performera. Default 2 (tablet-friendly).
const GRID_COLS_KEY = 'goon.grid_columns';
export async function getGridColumns(): Promise<number> {
const v = await SecureStore.getItemAsync(GRID_COLS_KEY);
const n = v ? parseInt(v, 10) : 2;
return n === 1 || n === 2 || n === 3 ? n : 2;
}
export async function setGridColumns(n: number): Promise<void> {
await SecureStore.setItemAsync(GRID_COLS_KEY, String(n));
}
// Domyślna jakość odtwarzania (user-request: mniej klików). Gdy resolve zwróci kilka
// jakości i jest dopasowanie do tej wartości, apka pomija picker i gra od razu. 'auto'
// = zawsze pokaż picker (stare zachowanie). 'lowest' = najniższa dostępna.
export type DefaultQuality = 'auto' | 2160 | 1080 | 720 | 480 | 'lowest';
const DEFAULT_QUALITY_KEY = 'goon.default_quality';
export async function getDefaultQuality(): Promise<DefaultQuality> {
const v = await SecureStore.getItemAsync(DEFAULT_QUALITY_KEY);
if (!v) return 'auto';
if (v === 'auto' || v === 'lowest') return v;
const n = parseInt(v, 10);
return n === 2160 || n === 1080 || n === 720 || n === 480 ? (n as DefaultQuality) : 'auto';
}
export async function setDefaultQuality(v: DefaultQuality): Promise<void> {
await SecureStore.setItemAsync(DEFAULT_QUALITY_KEY, String(v));
}
// Auto-obrót w playerze. Player domyślnie odblokowuje orientację (unlockAsync), żeby
// dało się oglądać poziomo, ale wtedy telefon położony płasko na stole sam skacze
// między orientacjami (bug-report 55da1c3f: "I can't turn off auto rotate. Which is a
// problem when I put the phone on a flat surface"). Off = player zostaje w pionie,
// a przełącznik pełnego ekranu dalej pozwala wejść w poziom RĘCZNIE.
// Default true = dotychczasowe zachowanie.
const AUTO_ROTATE_KEY = 'goon.player_auto_rotate';
export async function getAutoRotate(): Promise<boolean> {
const v = await SecureStore.getItemAsync(AUTO_ROTATE_KEY);
return v === null ? true : v === '1';
}
export async function setAutoRotate(on: boolean): Promise<void> {
await SecureStore.setItemAsync(AUTO_ROTATE_KEY, on ? '1' : '0');
}
// Baner migracji na nowy podpis APK (patrz `migrationNotice.ts`). Trzymamy moment
// odłożenia „na później", nie zwykłą flagę widziano/nie-widziano: to komunikat, który
// MUSI wrócić, bo bez przeniesienia kodu synchronizacji user traci ulubione. Wraca po
// dobie, chyba że wybierze „Nie pokazuj więcej".
const MIGRATION_SNOOZE_KEY = 'goon.migration_snoozed_at';
const MIGRATION_DONE_KEY = 'goon.migration_dismissed';
const MIGRATION_SNOOZE_MS = 24 * 60 * 60 * 1000;
export async function isMigrationNoticeHidden(): Promise<boolean> {
if ((await SecureStore.getItemAsync(MIGRATION_DONE_KEY)) === '1') return true;
const at = await SecureStore.getItemAsync(MIGRATION_SNOOZE_KEY);
if (!at) return false;
const ts = Number(at);
// Zegar cofnięty ręcznie / śmieć w store → traktuj jak brak odłożenia (pokaż baner).
if (!Number.isFinite(ts)) return false;
return Date.now() - ts < MIGRATION_SNOOZE_MS;
}
export async function snoozeMigrationNotice(): Promise<void> {
await SecureStore.setItemAsync(MIGRATION_SNOOZE_KEY, String(Date.now()));
}
export async function dismissMigrationNotice(): Promise<void> {
await SecureStore.setItemAsync(MIGRATION_DONE_KEY, '1');
}
export interface Credentials {
baseUrl: string;
apiKey: string;
}
export async function loadCredentials(): Promise<Credentials | null> {
const baseUrl = await SecureStore.getItemAsync(URL_KEY);
const apiKey = await SecureStore.getItemAsync(API_KEY);
if (!baseUrl || !apiKey) return null;
return { baseUrl, apiKey };
}
export async function saveCredentials(c: Credentials): Promise<void> {
await SecureStore.setItemAsync(URL_KEY, c.baseUrl);
await SecureStore.setItemAsync(API_KEY, c.apiKey);
}
export async function clearCredentials(): Promise<void> {
await SecureStore.deleteItemAsync(URL_KEY);
await SecureStore.deleteItemAsync(API_KEY);
}