goon/mobile/src/storage.ts
goon-foss e1dd46c6f2 feat(mobile): first-launch tutorial (pages, features, long-presses, player gestures)
A 7-slide carousel shown once on first launch:
- the three tabs (Scenes/Movies/Sites)
- search, filters, saved searches, Performers/Tags/Favorites
- long-press actions (hide/duplicate a scene, remove a wrong performer, link diagnostics)
- player gestures (tap controls, double-tap ±15s, swipe to scrub, unmute)
- favorites, Hidden content, PIN lock, the ? report button, Sites ★ ratings

Gated by a SecureStore flag; replayable from Settings ⚙ → Replay tutorial (via a
tiny onboarding bus). Suppresses the What's-new popup for brand-new users (the tour
covers it) and marks the changelog seen on finish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:08:56 +02:00

109 lines
3.8 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));
}
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);
}