feat(mobile): sign-in, tabs, search, collection and background player (M5)
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m39s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / ts / ts (packages + web) (push) Successful in 1m5s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 39s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m39s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / ts / ts (packages + web) (push) Successful in 1m5s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 39s
- expo-router app in src/app: connect (server check, recent servers), login, mandatory credential / password change, tabs Home / Search / Collection / Profile, album and artist pages, full-screen player - AuthSession per server with the refresh token in expo-secure-store (Android Keystore); language from the profile - player on react-native-track-player: ExoPlayer queue with a notification and lock-screen controls via a playback service, 500 MB stream cache, quality by network (Wi-Fi high, cellular medium), play events with played time, reactions, playback state restore and the endless My Wave with feedback - home: pulsing wave circle driven by BPM / energy in the cover palette, activity and mood chips that switch the wave on the fly - New Architecture is off until react-native-track-player supports it; the release build runs codegen only when it is on; query-string is a direct dependency because expo-router 5.1 imports it undeclared Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eaedd1fc62
commit
9fd616da57
28 files changed
+2590
-197
No files matched your search
@@ -0,0 +1,61 @@
|
||||
import type { 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 SecureStore.deleteItemAsync(keyFor(url));
|
||||
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);
|
||||
return {
|
||||
get: () => SecureStore.getItemAsync(key),
|
||||
set: (token) => SecureStore.setItemAsync(key, token),
|
||||
clear: () => SecureStore.deleteItemAsync(key),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user