import type { TransferRecord, ThemeMode, User } from "./types"; // DONE 是"全部数据已交付"的语义终态,把进行时遥测残留归一到这一事实,避免 // history 行还显示"传输中字节 / 待发送 X MB"等错觉。FAILED / CANCELLED 不归 // 一:保留断点位置便于诊断,phase 字段告诉用户"卡在哪个阶段失败"。 export function normalizeTerminal(cur: TransferRecord, finalState: string): TransferRecord { const result: TransferRecord = { ...cur, state: finalState }; if (finalState !== "DONE") { return result; } result.phase = undefined; result.bytesTransferred = cur.fileSize; if (cur.iceStats?.dataChannel) { result.iceStats = { ...cur.iceStats, dataChannel: { ...cur.iceStats.dataChannel, bufferedAmount: 0 }, }; } return result; } // ---- storage keys & readers ---- export const ACCESS_TOKEN_KEY = "cdrop.access_token"; export const USER_KEY = "cdrop.user"; export const SELF_DEVICE_KEY = "cdrop.self_device"; export const THEME_KEY = "cdrop.theme"; // 桌面壳的跨重启会话恢复:Go 侧在启动时从持久化文件读出 session,注入到 // index.html 的 window.__CDROP_SESSION__,因为 wails:// 自定义 scheme 下 WKWebView // 的 localStorage / sessionStorage 都不跨重启存活(custom-scheme origin 无持久化 // 存储)。注入的全局在 bundle 执行前就绪,store 初始化即可同步水合。浏览器无此 // 注入,回落到 sessionStorage(刷新存活、关标签即清;refresh_token 仅在内存)。 // 注入的 session 不含 refresh_token——refresh_token 是长寿命密钥,只在 Go 进程里, // 绝不进入 WebView(凭据策略见 desktop/platform/session.go)。 interface InjectedSession { access_token: string; user: { id: string; name: string; avatar?: string }; } interface InjectedBoot { session?: InjectedSession | null; device_name?: string; api_base?: string; device_type?: string; } function injectedBoot(): InjectedBoot | null { if (typeof window === "undefined") { return null; } return (window as unknown as { __CDROP_BOOT__?: InjectedBoot }).__CDROP_BOOT__ ?? null; } function injectedSession(): InjectedSession | null { const s = injectedBoot()?.session; return s && typeof s.access_token === "string" && s.access_token ? s : null; } // readInjectedDeviceName 在桌面壳里返回启动注入的设备名,让重启后路由守卫不再跳 // /setup(设备名同样存在 Go 侧、随 session 一并注入);浏览器无注入返回 null。 export function readInjectedDeviceName(): string | null { const name = injectedBoot()?.device_name; return name && name.length > 0 ? name : null; } // readInjectedApiBase 返回 /api 请求要前缀的来源。Windows 桌面壳注入 loopback // HTTP 代理地址(http://127.0.0.1:PORT),让 SSE 走 WebView2 正常网络栈而非会缓冲 // 的 custom scheme;macOS 与浏览器无此注入、返回 ""(相对 URL、同源)。 export function readInjectedApiBase(): string { const base = injectedBoot()?.api_base; return base && base.length > 0 ? base : ""; } // readInjectedDeviceType 返回桌面壳注入的客户端类型(macos / windows / linux), // 前端经 X-Device-Type 透传给后端,使设备列表不再把桌面端都标成 browser;浏览器 // 无注入返回 "browser"。 export function readInjectedDeviceType(): string { const t = injectedBoot()?.device_type; return t && t.length > 0 ? t : "browser"; } export function readSessionAccess(): string | null { const inj = injectedSession(); if (inj) { return inj.access_token; } if (typeof window === "undefined") { return null; } return window.sessionStorage.getItem(ACCESS_TOKEN_KEY); } // user.id / user.name 不是机密(公开显示在 UI、JWT 里可解码),随 access_token // 一并持久化,才能让刷新 / 重启后路由守卫不再误跳 /login。 export function readSessionUser(): User | null { const inj = injectedSession(); if (inj?.user && typeof inj.user.id === "string" && typeof inj.user.name === "string") { return { id: inj.user.id, name: inj.user.name, avatar: inj.user.avatar }; } if (typeof window === "undefined") { return null; } const raw = window.sessionStorage.getItem(USER_KEY); if (!raw) { return null; } try { const parsed = JSON.parse(raw) as unknown; if ( parsed && typeof parsed === "object" && typeof (parsed as User).id === "string" && typeof (parsed as User).name === "string" ) { return parsed as User; } return null; } catch { return null; } } export function readSelfDevice(): string | null { if (typeof window === "undefined") { return null; } return window.localStorage.getItem(SELF_DEVICE_KEY); } export function readTheme(): ThemeMode { if (typeof window === "undefined") { return "system"; } const raw = window.localStorage.getItem(THEME_KEY); if (raw === "light" || raw === "dark" || raw === "system") { return raw; } return "system"; }