ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m7s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Canceled after 4m15s
ci / ts / ts (packages + web) (push) Canceled after 17m42s
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 18m52s
- Downloads queue (2 parallel, Wi-Fi only by default) stores files under the app's document directory and track metadata in SQLite; Collection gets a Downloads tab, album and player screens get download buttons. - Player plays downloaded files and local covers; My Wave falls back to a shuffle of downloaded tracks when the server is unreachable. - Playback events that fail on the network are kept in SQLite and sent when the connection returns. - AuthSession can open offline with the cached user and fetches an access token before the first request once online. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import type { AuthUser, RefreshStore } from '@pipu/core';
|
|
import * as SecureStore from 'expo-secure-store';
|
|
|
|
// Всё храним в SecureStore (Android Keystore): список серверов маленький, а refresh-токен — секрет.
|
|
const SERVERS_KEY = 'pipu.servers';
|
|
const CURRENT_KEY = 'pipu.server';
|
|
|
|
export interface SavedServer {
|
|
url: string;
|
|
version: string;
|
|
/** Последний вошедший логин — подставляется в форму входа. */
|
|
username?: string;
|
|
}
|
|
|
|
/** Ключи SecureStore — только [A-Za-z0-9._-]: адрес сервера превращаем в безопасный суффикс. */
|
|
function keyFor(url: string): string {
|
|
let h = 0;
|
|
for (const ch of url) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
return `pipu.rt.${h.toString(36)}`;
|
|
}
|
|
|
|
export async function loadServers(): Promise<SavedServer[]> {
|
|
try {
|
|
const raw = await SecureStore.getItemAsync(SERVERS_KEY);
|
|
return raw ? (JSON.parse(raw) as SavedServer[]) : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** Сохраняет сервер наверх списка (последний использованный — первый). */
|
|
export async function rememberServer(server: SavedServer): Promise<void> {
|
|
const list = (await loadServers()).filter((s) => s.url !== server.url);
|
|
await SecureStore.setItemAsync(SERVERS_KEY, JSON.stringify([server, ...list].slice(0, 10)));
|
|
}
|
|
|
|
export async function forgetServer(url: string): Promise<void> {
|
|
const list = (await loadServers()).filter((s) => s.url !== url);
|
|
await SecureStore.setItemAsync(SERVERS_KEY, JSON.stringify(list));
|
|
await refreshStoreFor(url).clear();
|
|
if ((await currentServer()) === url) await SecureStore.deleteItemAsync(CURRENT_KEY);
|
|
}
|
|
|
|
export async function currentServer(): Promise<string | null> {
|
|
return SecureStore.getItemAsync(CURRENT_KEY);
|
|
}
|
|
|
|
export async function setCurrentServer(url: string | null): Promise<void> {
|
|
if (url) await SecureStore.setItemAsync(CURRENT_KEY, url);
|
|
else await SecureStore.deleteItemAsync(CURRENT_KEY);
|
|
}
|
|
|
|
/** Refresh-токен сервера (у каждого сервера — свой) и последний пользователь — для запуска без сети. */
|
|
export function refreshStoreFor(url: string): RefreshStore {
|
|
const key = keyFor(url);
|
|
const userKey = key.replace('pipu.rt.', 'pipu.user.');
|
|
return {
|
|
get: () => SecureStore.getItemAsync(key),
|
|
set: (token) => SecureStore.setItemAsync(key, token),
|
|
clear: async () => {
|
|
await SecureStore.deleteItemAsync(key);
|
|
await SecureStore.deleteItemAsync(userKey);
|
|
},
|
|
getUser: async () => {
|
|
try {
|
|
const raw = await SecureStore.getItemAsync(userKey);
|
|
return raw ? (JSON.parse(raw) as AuthUser) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
setUser: (user) => SecureStore.setItemAsync(userKey, JSON.stringify(user)),
|
|
};
|
|
}
|
|
|
|
/** Короткий стабильный хэш строки — для имён файлов и ключей. */
|
|
export function shortHash(s: string): string {
|
|
let h = 0;
|
|
for (const ch of s) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
return h.toString(36);
|
|
}
|