Files
coco-app/src/utils/tauriAdapter.ts
2026-06-23 09:55:34 +08:00

525 lines
14 KiB
TypeScript

import type { OpenDialogOptions, SaveDialogOptions } from "@tauri-apps/plugin-dialog";
import { isWindows10 } from "tauri-plugin-windows-version-api";
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import { metadata } from "tauri-plugin-fs-pro-api";
import { error, info } from "@tauri-apps/plugin-log";
import {
windowWrapper,
eventWrapper,
systemWrapper,
commandWrapper,
} from "./wrappers/tauriWrappers";
import type { BasePlatformAdapter } from "@/types/platform";
import type { AppTheme } from "@/types/index";
import { useAppearanceStore } from "@/stores/appearanceStore";
import { copyToClipboard, dispatchEvent } from ".";
import { useAppStore } from "@/stores/appStore";
import { unrequitable } from "@/utils";
import {
getCurrentWebviewWindow,
WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import {
cursorPosition,
Monitor,
monitorFromPoint,
PhysicalPosition,
Theme,
} from "@tauri-apps/api/window";
export interface TauriPlatformAdapter extends BasePlatformAdapter {
openFileDialog: (
options: OpenDialogOptions,
) => Promise<string | string[] | null>;
saveFileDialog: (
options?: SaveDialogOptions,
) => Promise<string | null>;
metadata: typeof metadata;
error: typeof error;
info: typeof info;
openLogDir: () => Promise<void>;
getCurrentWebviewWindow: () => Promise<WebviewWindow>;
getWindowTheme: () => Promise<Theme | null>;
setWindowTheme: (theme: Theme | null) => Promise<void>;
getAllWindows: () => Promise<WebviewWindow[]>;
setWindowResizable: (resizable: boolean) => Promise<void>;
isWindowResizable: () => Promise<boolean>;
getWindowSize: () => Promise<{ width: number; height: number }>;
setWindowFullscreen: (enable: boolean) => Promise<void>;
isWindowMaximized: () => Promise<boolean>;
setWindowMaximized: (enable: boolean) => Promise<void>;
getWindowPosition: () => Promise<{ x: number; y: number }>;
setWindowPosition: (x: number, y: number) => Promise<void>;
getMonitorFromCursor: () => Promise<Monitor | null>;
centerOnCurrentMonitor: () => Promise<unknown>;
setWindowPhysicalPosition: (x: number, y: number) => Promise<void>;
}
// Create Tauri adapter functions
export const createTauriAdapter = (): TauriPlatformAdapter => {
const getWebviewWindowTitle = (url: string) => {
try {
return new URL(url).hostname || "Coco";
} catch {
return "Coco";
}
};
const openUrlInWebviewWindow = async (url: string) => {
const documentPreview = getDocumentPreviewRoute(url);
const label = `webview-${Date.now()}-${Math.random()
.toString(36)
.slice(2, 8)}`;
const route = documentPreview || `/ui/webview?url=${encodeURIComponent(url)}`;
return new WebviewWindow(label, {
title: getWebviewWindowTitle(url),
url: route,
width: 1100,
height: 760,
minWidth: 640,
minHeight: 420,
center: true,
resizable: true,
maximizable: true,
minimizable: true,
closable: true,
decorations: true,
transparent: false,
shadow: true,
dragDropEnabled: false,
});
};
const getDocumentPreviewRoute = (
url: string,
options?: { serverId?: string; sourceUrl?: string }
) => {
try {
const parsed = new URL(url);
const hashPath = parsed.hash.replace(/^#/, "");
const routePath = `${parsed.pathname}${hashPath}`;
const match = routePath.match(/\/preview\/document\/([^/?#]+)/);
const documentId = match?.[1];
const serverId = options?.serverId;
if (!documentId || !serverId) return "";
const params = new URLSearchParams({
serverId,
documentId,
});
if (options?.sourceUrl) {
params.set("sourceUrl", options.sourceUrl);
}
return `/ui/document-preview?${params.toString()}`;
} catch {
return "";
}
};
const openSearchUrl = async (url: string, data?: any) => {
const documentPreviewRoute = getDocumentPreviewRoute(url, {
serverId: data?.querySource?.id,
sourceUrl: data?.metadata?.raw_content || data?.url,
});
if (documentPreviewRoute) {
const label = `document-preview-${Date.now()}-${Math.random()
.toString(36)
.slice(2, 8)}`;
return new WebviewWindow(label, {
title: data?.title || getWebviewWindowTitle(url),
url: documentPreviewRoute,
width: 1100,
height: 760,
minWidth: 720,
minHeight: 480,
center: true,
resizable: true,
maximizable: true,
minimizable: true,
closable: true,
decorations: true,
transparent: false,
shadow: true,
dragDropEnabled: false,
});
}
return openUrlInWebviewWindow(url);
};
return {
async setWindowSize(width, height) {
return windowWrapper.setLogicalSize(width, height);
},
async getWindowSize() {
return windowWrapper.getLogicalSize();
},
async setWindowResizable(resizable) {
return windowWrapper.setResizable(resizable);
},
async isWindowResizable() {
return windowWrapper.isResizable();
},
async setWindowFullscreen(enable) {
return windowWrapper.setFullscreen(enable);
},
async isWindowMaximized() {
return windowWrapper.isMaximized();
},
async setWindowMaximized(enable) {
return windowWrapper.setMaximized(enable);
},
async getWindowPosition() {
return windowWrapper.getLogicalPosition();
},
async setWindowPosition(x, y) {
return windowWrapper.setLogicalPosition(x, y);
},
async setWindowPhysicalPosition(x: number, y: number) {
const appWindow = getCurrentWebviewWindow();
return appWindow.setPosition(new PhysicalPosition(x, y));
},
async getMonitorFromCursor() {
const appWindow = getCurrentWebviewWindow();
const factor = await appWindow.scaleFactor();
const point = await cursorPosition();
const { x, y } = point.toLogical(factor);
return monitorFromPoint(x, y);
},
async centerOnCurrentMonitor() {
return windowWrapper.centerOnMonitor();
},
async hideWindow() {
const window = await windowWrapper.getCurrentWebviewWindow();
return window?.hide();
},
async showWindow() {
const window = await windowWrapper.getCurrentWebviewWindow();
window?.show();
window?.unminimize();
return window?.setFocus();
},
async emitEvent(event, payload) {
return eventWrapper.emit(event, payload);
},
async listenEvent(event, callback) {
return eventWrapper.listen(event, callback);
},
async checkScreenRecordingPermission() {
return systemWrapper.checkScreenPermission();
},
async captureMonitorScreenshot(id) {
return systemWrapper.captureScreen(id, "monitor");
},
async captureWindowScreenshot(id) {
return systemWrapper.captureScreen(id, "window");
},
commands: commandWrapper.commands,
async invokeBackend(command, args) {
const { invoke } = await import("@tauri-apps/api/core");
return invoke(command, args);
},
convertFileSrc(path) {
return convertFileSrc(path);
},
async setAlwaysOnTop(isPinned) {
const { getCurrentWebviewWindow } =
await import("@tauri-apps/api/webviewWindow");
const window = getCurrentWebviewWindow();
return window.setAlwaysOnTop(isPinned);
},
async requestScreenRecordingPermission() {
const { requestScreenRecordingPermission } =
await import("tauri-plugin-macos-permissions-api");
return requestScreenRecordingPermission();
},
async checkMicrophonePermission() {
const { checkMicrophonePermission } =
await import("tauri-plugin-macos-permissions-api");
return checkMicrophonePermission();
},
async requestMicrophonePermission() {
const { requestMicrophonePermission } =
await import("tauri-plugin-macos-permissions-api");
return requestMicrophonePermission();
},
async checkCameraPermission() {
const { checkCameraPermission } =
await import("tauri-plugin-macos-permissions-api");
return checkCameraPermission();
},
async requestCameraPermission() {
const { requestCameraPermission } =
await import("tauri-plugin-macos-permissions-api");
return requestCameraPermission();
},
async getScreenshotableMonitors() {
const { getScreenshotableMonitors } =
await import("tauri-plugin-screenshots-api");
return getScreenshotableMonitors();
},
async getScreenshotableWindows() {
const { getScreenshotableWindows } =
await import("tauri-plugin-screenshots-api");
return getScreenshotableWindows();
},
async openFileDialog(options) {
const { open } = await import("@tauri-apps/plugin-dialog");
return open(options);
},
async saveFileDialog(options) {
const { save } = await import("@tauri-apps/plugin-dialog");
return save(options);
},
async getFileMetadata(path) {
const { metadata } = await import("tauri-plugin-fs-pro-api");
return metadata(path);
},
async getFileIcon(path, size) {
const { icon } = await import("tauri-plugin-fs-pro-api");
return icon(path, { size });
},
async checkUpdate() {
const { check } = await import("@tauri-apps/plugin-updater");
const { snapshotUpdate } = useAppearanceStore.getState();
const endpoints = [
"https://release.infinilabs.com/coco/app/.latest.json?target={{target}}&arch={{arch}}&current_version={{current_version}}",
];
if (snapshotUpdate) {
endpoints.unshift(
"https://release.infinilabs.com/coco/app/snapshot/.latest.json?target={{target}}&arch={{arch}}&current_version={{current_version}}",
);
}
return check({
endpoints,
});
},
async relaunchApp() {
const { relaunch } = await import("@tauri-apps/plugin-process");
return relaunch();
},
async listenThemeChanged(callback) {
const { listen } = await import("@tauri-apps/api/event");
return listen<AppTheme>("theme-changed", ({ payload }) => {
callback(payload);
});
},
async getCurrentWebviewWindow() {
const { getCurrentWebviewWindow } =
await import("@tauri-apps/api/webviewWindow");
return getCurrentWebviewWindow();
},
async setWindowTheme(theme) {
const window = await this.getCurrentWebviewWindow();
if (window) {
return window.setTheme(theme);
}
},
async getWindowTheme() {
const window = await this.getCurrentWebviewWindow();
if (window) {
return window.theme();
}
return "light";
},
async onThemeChanged(callback) {
const window = await this.getCurrentWebviewWindow();
if (window) {
window.onThemeChanged(callback);
}
},
async getWindowByLabel(label) {
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
const window = await WebviewWindow.getByLabel(label);
return window;
},
async createWindow(label, options) {
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
new WebviewWindow(label, options);
},
async getAllWindows() {
const { getAllWebviewWindows } =
await import("@tauri-apps/api/webviewWindow");
return getAllWebviewWindows();
},
async createWebviewWindow(label, options) {
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
return new WebviewWindow(label, options);
},
async listenWindowEvent(event, callback) {
const { listen } = await import("@tauri-apps/api/event");
return listen(event, callback);
},
isTauri() {
return true;
},
async openUrl(url) {
return openUrlInWebviewWindow(url);
},
async openUrlWithBrowser(url) {
const { openUrl } = await import("@tauri-apps/plugin-opener");
return openUrl(url);
},
isWindows10,
async setShadow(enable) {
const { getCurrentWebviewWindow } =
await import("@tauri-apps/api/webviewWindow");
const appWindow = getCurrentWebviewWindow();
return appWindow.setShadow(enable);
},
metadata,
async revealItemInDir(path) {
const { revealItemInDir } = await import("@tauri-apps/plugin-opener");
revealItemInDir(path);
},
async openSearchItem(data, formatUrl) {
const { invoke } = await import("@tauri-apps/api/core");
console.log("openSearchItem", data);
// Extension store needs to be opened in a different way
if (
data?.type === "AI Assistant" ||
data?.id === "Extension Store" ||
data?.category === "View"
) {
return dispatchEvent("Tab", 9);
}
const hideCoco = () => {
const isPinned = useAppStore.getState().isPinned;
if (isPinned) return;
return invoke("hide_coco");
};
const url = (formatUrl && formatUrl(data)) || data?.url;
if (url?.startsWith("http://") || url?.startsWith("https://")) {
await openSearchUrl(url, data);
return hideCoco();
}
if (data?.on_opened) {
await invoke("open", {
onOpened: data.on_opened,
extraArgs: null,
});
return hideCoco();
}
if (data?.payload?.result?.value) {
return copyToClipboard(data.payload.result.value);
}
},
error,
info,
async searchMCPServers(serverId, queryParams) {
if (await unrequitable()) {
return [];
}
return await commandWrapper.commands("mcp_server_search", {
id: serverId,
queryParams,
});
},
async searchDataSources(serverId, queryParams) {
return await commandWrapper.commands("datasource_search", {
id: serverId,
queryParams,
});
},
async fetchAssistant(serverId, queryParams) {
if (!serverId) {
throw new Error("currentService is undefined");
}
return await commandWrapper.commands("assistant_search", {
serverId,
queryParams,
});
},
async getCurrentWindowLabel() {
const window = await windowWrapper.getCurrentWebviewWindow();
return window.label;
},
async openLogDir() {
const { revealItemInDir } = await import("@tauri-apps/plugin-opener");
const logDir: string = await invoke("app_log_dir");
revealItemInDir(logDir);
},
};
};