mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-07-10 12:37:31 +02:00
525 lines
14 KiB
TypeScript
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}}¤t_version={{current_version}}",
|
|
];
|
|
|
|
if (snapshotUpdate) {
|
|
endpoints.unshift(
|
|
"https://release.infinilabs.com/coco/app/snapshot/.latest.json?target={{target}}&arch={{arch}}¤t_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);
|
|
},
|
|
};
|
|
};
|