import AsyncStorage from "@react-native-async-storage/async-storage"; import type { HostConfig, ModelRef, SavedComputer } from "./types"; const HOST_KEY = "pi-mobile:last-host"; const COMPUTERS_KEY = "pi-mobile:computers"; const PROJECTS_PREFIX = "pi-mobile:projects:"; const LAST_MODEL_KEY = "pi-mobile:last-model"; function generateId(): string { return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } function computerKey(host: string, port: string): string { return `${host.trim()}:${port.trim() || "8787"}`; } function sortByRecent(computers: SavedComputer[]): SavedComputer[] { return [...computers].sort((a, b) => b.lastUsedAt - a.lastUsedAt); } function projectsStorageKey(host: string, port: string): string { return `${PROJECTS_PREFIX}${host.trim()}:${port.trim() || "8787"}`; } export function buildBaseUrl(host: string, port: string): string { const trimmedHost = host.trim(); const trimmedPort = port.trim() || "8787"; const withProtocol = trimmedHost.startsWith("http") ? trimmedHost : `http://${trimmedHost}`; const url = new URL(withProtocol); url.port = trimmedPort; return url.origin; } export async function loadHostConfig(): Promise { const raw = await AsyncStorage.getItem(HOST_KEY); if (!raw) { return null; } try { return JSON.parse(raw) as HostConfig; } catch { return null; } } export async function saveHostConfig(config: HostConfig): Promise { await AsyncStorage.setItem(HOST_KEY, JSON.stringify(config)); } export async function loadLastModel(): Promise { const raw = await AsyncStorage.getItem(LAST_MODEL_KEY); if (!raw) { return null; } try { const parsed = JSON.parse(raw) as Partial; return typeof parsed.provider === "string" && typeof parsed.id === "string" ? { provider: parsed.provider, id: parsed.id } : null; } catch { return null; } } export async function saveLastModel(model: ModelRef): Promise { await AsyncStorage.setItem(LAST_MODEL_KEY, JSON.stringify(model)); } export async function loadSavedComputers(): Promise { const raw = await AsyncStorage.getItem(COMPUTERS_KEY); if (raw) { try { const parsed = JSON.parse(raw) as unknown; if (Array.isArray(parsed)) { return sortByRecent( parsed.filter( (entry): entry is SavedComputer => typeof entry === "object" && entry !== null && typeof (entry as SavedComputer).id === "string" && typeof (entry as SavedComputer).name === "string" && typeof (entry as SavedComputer).host === "string" && typeof (entry as SavedComputer).port === "string" && typeof (entry as SavedComputer).lastUsedAt === "number", ), ); } } catch { // fall through to migration } } const legacy = await loadHostConfig(); if (!legacy?.host.trim()) { return []; } const migrated: SavedComputer = { id: generateId(), name: legacy.host.trim(), host: legacy.host.trim(), port: legacy.port.trim() || "8787", lastUsedAt: Date.now(), }; await saveSavedComputers([migrated]); return [migrated]; } export async function saveSavedComputers(computers: SavedComputer[]): Promise { await AsyncStorage.setItem(COMPUTERS_KEY, JSON.stringify(sortByRecent(computers))); } export async function upsertSavedComputer( computers: SavedComputer[], host: string, port: string, name?: string, token?: string, ): Promise { const trimmedHost = host.trim(); const trimmedPort = port.trim() || "8787"; if (!trimmedHost) { return computers; } const key = computerKey(trimmedHost, trimmedPort); const existing = computers.find((entry) => computerKey(entry.host, entry.port) === key); const trimmedName = name?.trim(); const trimmedToken = token?.trim(); if (existing) { const next = computers.map((entry) => entry.id === existing.id ? { ...entry, name: trimmedName || entry.name, host: trimmedHost, port: trimmedPort, token: trimmedToken || entry.token, lastUsedAt: Date.now(), } : entry, ); await saveSavedComputers(next); return sortByRecent(next); } const next = [ { id: generateId(), name: trimmedName || trimmedHost, host: trimmedHost, port: trimmedPort, token: trimmedToken, lastUsedAt: Date.now(), }, ...computers, ]; await saveSavedComputers(next); return sortByRecent(next); } export async function removeSavedComputer( computers: SavedComputer[], id: string, ): Promise { const next = computers.filter((entry) => entry.id !== id); await saveSavedComputers(next); return next; } export async function loadSavedProjects(host: string, port: string): Promise { const raw = await AsyncStorage.getItem(projectsStorageKey(host, port)); if (!raw) { return []; } try { const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) { return []; } return parsed.filter((entry): entry is string => typeof entry === "string" && entry.length > 0); } catch { return []; } } export async function saveSavedProjects( host: string, port: string, projects: string[], ): Promise { await AsyncStorage.setItem(projectsStorageKey(host, port), JSON.stringify(projects)); } function authHeaders(token?: string): HeadersInit | undefined { if (!token) { return undefined; } return { "x-pi-mobile-token": token, }; } export async function checkHealth(baseUrl: string, token?: string): Promise { const response = await fetch(`${baseUrl}/health`, { headers: authHeaders(token), }); if (!response.ok) { return false; } const data = (await response.json()) as { ok?: boolean }; return data.ok === true; } export async function listFolders(baseUrl: string, path = "~", token?: string) { const response = await fetch(`${baseUrl}/fs/list?path=${encodeURIComponent(path)}`, { headers: authHeaders(token), }); if (!response.ok) { const body = (await response.json().catch(() => ({}))) as { error?: string }; throw new Error(body.error ?? "Failed to list folders"); } return response.json(); } export async function createFolder( baseUrl: string, path: string, name: string, token?: string, ): Promise<{ name: string; path: string }> { const response = await fetch(`${baseUrl}/fs/mkdir`, { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders(token), }, body: JSON.stringify({ path, name }), }); if (!response.ok) { const body = (await response.json().catch(() => ({}))) as { error?: string }; throw new Error(body.error ?? "Failed to create folder"); } return response.json(); }