feat(mobile): offline downloads, persistent event queue and offline start (M5)
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
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>
This commit is contained in:
1 parent
765345e0f4
commit
be34ee5361
22 files changed
+881
-387
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { RefreshStore } from '@pipu/core';
|
||||
import type { AuthUser, RefreshStore } from '@pipu/core';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
|
||||
// Всё храним в SecureStore (Android Keystore): список серверов маленький, а refresh-токен — секрет.
|
||||
@@ -37,7 +37,7 @@ export async function rememberServer(server: SavedServer): Promise<void> {
|
||||
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));
|
||||
await refreshStoreFor(url).clear();
|
||||
if ((await currentServer()) === url) await SecureStore.deleteItemAsync(CURRENT_KEY);
|
||||
}
|
||||
|
||||
@@ -50,12 +50,32 @@ export async function setCurrentServer(url: string | null): Promise<void> {
|
||||
else await SecureStore.deleteItemAsync(CURRENT_KEY);
|
||||
}
|
||||
|
||||
/** Refresh-токен сервера: у каждого сервера — свой. */
|
||||
/** 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: () => SecureStore.deleteItemAsync(key),
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user