feat: integrate Tauri v2 with Android widget and voice assistant
- Add Tauri v2 shell (Cargo, tauri.conf.json, capabilities, plugins) - Migrate all fetch() calls to apiFetch() for Tauri-aware HTTP - Migrate WebSocket endpoints to resolveEndpoints() for dynamic URLs - Add ServerConfigDialog for remote server URL configuration - Add tauri.ts lib with isTauri detection, apiFetch wrapper, plugin helpers - Add server-config Pinia store with persistence via plugin-store - Conditional PWA (disabled in Tauri builds) - Android: home screen transcript widget (last 5 messages, 30s refresh) - Android: voice command / share activity (SpeechRecognizer + WebSocket) - Android: signed release APK with auto-copy to installers/ - Remove stale frontend/src-tauri directory
This commit is contained in:
85
frontend/src/lib/tauri.ts
Normal file
85
frontend/src/lib/tauri.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Tauri detection and fetch wrapper for Agent UI.
|
||||
* In Tauri mode, relative /api/... paths are resolved against the configured server URL.
|
||||
* In web mode, this is a transparent pass-through to native fetch.
|
||||
*/
|
||||
|
||||
// Detect if running inside a Tauri webview
|
||||
export const isTauri = '__TAURI_INTERNALS__' in window
|
||||
|
||||
// Detect mobile Tauri (Android/iOS)
|
||||
export function isMobileTauri(): boolean {
|
||||
if (!isTauri) return false
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
return /android|iphone|ipad|ipod/.test(ua)
|
||||
}
|
||||
|
||||
// Server URL storage (in-memory, loaded from Tauri store on init)
|
||||
let _serverUrl = ''
|
||||
|
||||
export function getServerUrl(): string {
|
||||
return _serverUrl
|
||||
}
|
||||
|
||||
export function setServerUrl(url: string) {
|
||||
// Normalize: remove trailing slash
|
||||
_serverUrl = url.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path to a full URL.
|
||||
* - In web mode: returns the path as-is (relative URLs work with Vite proxy / reverse proxy)
|
||||
* - In Tauri mode: prepends the configured server URL
|
||||
*/
|
||||
export function resolveUrl(path: string): string {
|
||||
if (!isTauri) return path
|
||||
if (!_serverUrl) {
|
||||
console.warn('[Tauri] No server URL configured, using path as-is:', path)
|
||||
return path
|
||||
}
|
||||
// If already absolute, return as-is
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) return path
|
||||
return `${_serverUrl}${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch wrapper that resolves relative API paths in Tauri mode.
|
||||
* In Tauri, uses @tauri-apps/plugin-http for proper CORS-free requests.
|
||||
* In web, delegates to native fetch.
|
||||
*/
|
||||
export async function apiFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
|
||||
if (!isTauri) {
|
||||
return fetch(input, init)
|
||||
}
|
||||
|
||||
// Resolve URL
|
||||
const url = typeof input === 'string' ? resolveUrl(input) : input
|
||||
|
||||
// Use Tauri HTTP plugin for cross-origin requests
|
||||
try {
|
||||
const { fetch: tauriFetch } = await import('@tauri-apps/plugin-http')
|
||||
return tauriFetch(url, init)
|
||||
} catch (e) {
|
||||
// Fallback to native fetch if plugin fails
|
||||
console.warn('[Tauri] HTTP plugin failed, falling back to fetch:', e)
|
||||
return fetch(url, init)
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic plugin imports (only used behind isTauri checks)
|
||||
export async function getTauriStore() {
|
||||
const { LazyStore } = await import('@tauri-apps/plugin-store')
|
||||
return new LazyStore('settings.json')
|
||||
}
|
||||
|
||||
export async function getTauriNotification() {
|
||||
return import('@tauri-apps/plugin-notification')
|
||||
}
|
||||
|
||||
export async function getTauriClipboard() {
|
||||
return import('@tauri-apps/plugin-clipboard-manager')
|
||||
}
|
||||
|
||||
export async function getTauriDialog() {
|
||||
return import('@tauri-apps/plugin-dialog')
|
||||
}
|
||||
Reference in New Issue
Block a user