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>
170 lines
6.6 KiB
TypeScript
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);
|
|
}
|