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 { 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 { return (await SecureStore.getItemAsync(LEGACY_ADOPTED_KEY)) === '1'; } export async function markLegacyAdopted(): Promise { 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 { return SecureStore.getItemAsync(CHANGELOG_SEEN_KEY); } export async function setLastSeenChangelog(id: string): Promise { 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 { 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 { 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 { 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 { 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 { 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 { 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 { const v = await SecureStore.getItemAsync(AUTO_ROTATE_KEY); return v === null ? true : v === '1'; } export async function setAutoRotate(on: boolean): Promise { 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 { 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 { await SecureStore.setItemAsync(MIGRATION_SNOOZE_KEY, String(Date.now())); } export async function dismissMigrationNotice(): Promise { await SecureStore.setItemAsync(MIGRATION_DONE_KEY, '1'); } export interface Credentials { baseUrl: string; apiKey: string; } export async function loadCredentials(): Promise { 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 { await SecureStore.setItemAsync(URL_KEY, c.baseUrl); await SecureStore.setItemAsync(API_KEY, c.apiKey); } export async function clearCredentials(): Promise { await SecureStore.deleteItemAsync(URL_KEY); await SecureStore.deleteItemAsync(API_KEY); }