import type { ItemEvent } from '@webdrop/core'; import type { FileItem, ShareEvent, WorkspaceEvent } from '../types'; import { previewKindOf } from '@/lib/fileKind'; import { publish } from './port'; export type TerminalKind = 'desktop' | 'phone' | 'tablet'; interface ClientHints { mobile?: boolean; model?: string; platform?: string; platformVersion?: string; getHighEntropyValues?: (hints: string[]) => Promise; } let clientHintsPromise: Promise | undefined; async function readClientHints(): Promise { const hints = (navigator as Navigator & { userAgentData?: ClientHints }).userAgentData; if (!hints?.getHighEntropyValues) return hints ?? {}; clientHintsPromise ??= hints.getHighEntropyValues(['model', 'platform', 'platformVersion']).catch(() => hints); return clientHintsPromise; } function clean(value: string | undefined): string | undefined { const result = value?.replace(/[<>\u0000-\u001f]/g, '').trim().slice(0, 48); return result || undefined; } function version(value: string | undefined): string | undefined { return value?.split('.').slice(0, 2).join('.') || undefined; } export async function deviceIdentity(endpointSecret: Uint8Array): Promise<{ name: string; kind: TerminalKind }> { const ua = navigator.userAgent; const hints = await readClientHints(); const android = /Android/i.test(ua) || hints.platform === 'Android'; const iphone = /iPhone|iPod/i.test(ua); const ipad = /iPad/i.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); const tablet = ipad || (android && (hints.mobile === false || (!hints.mobile && !/Mobile/i.test(ua)))); const mobile = !tablet && (android || iphone || /Mobile/i.test(ua)); const kind: TerminalKind = tablet ? 'tablet' : mobile ? 'phone' : 'desktop'; const iosVersion = ua.match(/(?:iPhone )?OS ([\d_]+)/i)?.[1]?.replaceAll('_', '.'); const windowsVersion = version(hints.platformVersion); const macVersion = ua.match(/Mac OS X ([\d_]+)/i)?.[1]?.replaceAll('_', '.'); const androidVersion = ua.match(/Android ([\d.]+)/i)?.[1]; const platformVersion = version(hints.platformVersion); let identity: string; if (clean(hints.model)) { identity = `${clean(hints.model)} (${clean(hints.platform) ?? 'Android'}${platformVersion ? ` ${platformVersion}` : ''})`; } else if (iphone) { identity = `iPhone${iosVersion ? ` (iOS ${iosVersion})` : ''}`; } else if (ipad) { identity = `iPad${iosVersion ? ` (iPadOS ${iosVersion})` : ''}`; } else if (/Windows/i.test(ua) || hints.platform === 'Windows') { identity = `Windows${windowsVersion ? ` ${Number(windowsVersion.split('.')[0]) >= 13 ? '11' : '10'}` : ''}`; } else if (android) { identity = `Android${platformVersion ?? androidVersion ? ` ${platformVersion ?? androidVersion}` : ''}`; } else if (/Macintosh|Mac OS X/i.test(ua) || hints.platform === 'macOS') { identity = `Mac${platformVersion ?? macVersion ? ` (macOS ${platformVersion ?? macVersion})` : ''}`; } else if (/CrOS/i.test(ua) || hints.platform === 'Chrome OS') { identity = 'ChromeOS'; } else if (/Linux/i.test(ua) || hints.platform === 'Linux') { identity = 'Linux'; } else { identity = '未知设备'; } const suffix = Array.from(endpointSecret.subarray(0, 4), (b) => b.toString(16).padStart(2, '0')).join('').toUpperCase(); return { name: `${identity} · ${suffix}`, kind }; } export function showRejected(resourceId: string, event: Extract, share = false): void { if (share) publish(resourceId, { t: 'integrity', message: '分享清单校验失败,内容已拒绝显示。' }); else if (event.itemId) publish(resourceId, { t: 'item', item: { id: event.itemId, kind: 'text', ts: Date.now(), state: { kind: 'tampered' }, text: '' }, }); else publish(resourceId, { t: 'error', message: '内容校验失败,已拒绝该记录。' }); } export function makeFileItem(id: string, name: string, mime: string, size: number, ts: number, state: FileItem['state'], remaining: number | null = null): FileItem { return { id, kind: 'file', ts, state, name, mime, size, remaining, preview: previewKindOf(mime, name) }; } export function publishWorkspace(resourceId: string, event: WorkspaceEvent): void { publish(resourceId, event); } export function publishShare(resourceId: string, event: ShareEvent): void { publish(resourceId, event); }