goon/mobile/src/storage.ts
jtrzupek 00f4779abe feat(mobile): column toggle, duration filter, saved searches, screen protection (mobilism feedback)
Batch from user feedback: (1) Grid columns 1/2/3 setting (PreferencesContext, persisted) across all scene grids — default 2 was too small on phones. (2) Min-duration filter chips (5/10/20/30+ min) to hide ad-clips. (3) Saved-search chips + Save button (backed by /saved-searches). (4) Re-enabled screen-capture protection (Recents hide + screenshot block) for distributed users — verified active on emulator (screencap returns 0 bytes). (5) 'Checking for updates' gate before the PIN screen so a background OTA restart no longer causes a double PIN prompt. Changelog entry added. Published OTA runtime 1.1 (a9620b12).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:52:27 +02:00

87 lines
3 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);
}
// 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));
}
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);
}