mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
feat: deeplink handler for install ext from store (#860)
Co-authored-by: rain9 <15911122312@163.com> Co-authored-by: ayang <473033518@qq.com>
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -83,5 +83,6 @@
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false
|
||||
"editor.detectIndentation": false,
|
||||
"i18n-ally.displayLanguage": "zh"
|
||||
}
|
||||
@@ -69,6 +69,7 @@
|
||||
"updater:default",
|
||||
"windows-version:default",
|
||||
"log:default",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"core:window:allow-unminimize"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::extension::third_party::install::filter_out_incompatible_sub_extensio
|
||||
use crate::server::http_client::HttpClient;
|
||||
use crate::util::platform::Platform;
|
||||
use async_trait::async_trait;
|
||||
use http::Method;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::Map as JsonObject;
|
||||
use serde_json::Value as Json;
|
||||
@@ -172,6 +173,52 @@ pub(crate) async fn search_extension(
|
||||
Ok(extensions)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn extension_detail(
|
||||
id: String,
|
||||
) -> Result<Option<JsonObject<String, Json>>, String> {
|
||||
let url = format!("http://dev.infini.cloud:27200/store/extension/{}", id);
|
||||
let response =
|
||||
HttpClient::send_raw_request(Method::GET, url.as_str(), None, None, None).await?;
|
||||
|
||||
if response.status() == StatusCode::NOT_FOUND {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let response_dbg_str = format!("{:?}", response);
|
||||
// The response of an ES style GET request
|
||||
let mut response: JsonObject<String, Json> = response.json().await.unwrap_or_else(|_e| {
|
||||
panic!(
|
||||
"response body of [/store/extension/<ID>] is not a JSON object, response [{:?}]",
|
||||
response_dbg_str
|
||||
)
|
||||
});
|
||||
let source_json = response.remove("_source").unwrap_or_else(|| {
|
||||
panic!("field [_source] not found in the JSON returned from [/store/extension/<ID>]")
|
||||
});
|
||||
let mut source_obj = match source_json {
|
||||
Json::Object(obj) => obj,
|
||||
_ => panic!(
|
||||
"field [_source] should be a JSON object, but it is not, value: [{}]",
|
||||
source_json
|
||||
),
|
||||
};
|
||||
|
||||
let developer_id = match &source_obj["developer"]["id"] {
|
||||
Json::String(dev) => dev,
|
||||
_ => {
|
||||
panic!(
|
||||
"field [_source.developer.id] should be a string, but it is not, value: [{}]",
|
||||
source_obj["developer"]["id"]
|
||||
)
|
||||
}
|
||||
};
|
||||
let installed = is_extension_installed(developer_id, &id).await;
|
||||
source_obj.insert("installed".to_string(), Json::Bool(installed));
|
||||
|
||||
Ok(Some(source_obj))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_extension_from_store(
|
||||
tauri_app_handle: AppHandle,
|
||||
@@ -250,21 +297,32 @@ pub(crate) async fn install_extension_from_store(
|
||||
e
|
||||
);
|
||||
});
|
||||
let developer_id = extension.developer.clone().expect("developer has been set");
|
||||
|
||||
drop(plugin_json);
|
||||
|
||||
general_check(&extension)?;
|
||||
|
||||
let current_platform = Platform::current();
|
||||
if let Some(ref platforms) = extension.platforms {
|
||||
if !platforms.contains(¤t_platform) {
|
||||
return Err("this extension is not compatible with your OS".into());
|
||||
}
|
||||
}
|
||||
|
||||
if is_extension_installed(&developer_id, &id).await {
|
||||
return Err("Extension already installed.".into());
|
||||
}
|
||||
|
||||
// Extension is compatible with current platform, but it could contain sub
|
||||
// extensions that are not, filter them out.
|
||||
filter_out_incompatible_sub_extensions(&mut extension, Platform::current());
|
||||
filter_out_incompatible_sub_extensions(&mut extension, current_platform);
|
||||
|
||||
// Write extension files to the extension directory
|
||||
let developer = extension.developer.clone().unwrap_or_default();
|
||||
let extension_id = extension.id.clone();
|
||||
let extension_directory = {
|
||||
let mut path = get_third_party_extension_directory(&tauri_app_handle);
|
||||
path.push(developer);
|
||||
path.push(developer_id);
|
||||
path.push(extension_id.as_str());
|
||||
path
|
||||
};
|
||||
|
||||
@@ -161,6 +161,7 @@ pub fn run() {
|
||||
extension::unregister_extension_hotkey,
|
||||
extension::is_extension_enabled,
|
||||
extension::third_party::install::store::search_extension,
|
||||
extension::third_party::install::store::extension_detail,
|
||||
extension::third_party::install::store::install_extension_from_store,
|
||||
extension::third_party::install::local_extension::install_local_extension,
|
||||
extension::third_party::uninstall_extension,
|
||||
|
||||
@@ -13,11 +13,20 @@ export async function getCurrentWindowService() {
|
||||
: currentService;
|
||||
}
|
||||
|
||||
export async function setCurrentWindowService(service: any) {
|
||||
const windowLabel = await platformAdapter.getCurrentWindowLabel();
|
||||
export async function setCurrentWindowService(
|
||||
service: any,
|
||||
isAll?: boolean
|
||||
) {
|
||||
const { setCurrentService, setCloudSelectService } =
|
||||
useConnectStore.getState();
|
||||
|
||||
// all refresh logout
|
||||
if (isAll) {
|
||||
setCloudSelectService(service);
|
||||
setCurrentService(service);
|
||||
return;
|
||||
}
|
||||
// current refresh
|
||||
const windowLabel = await platformAdapter.getCurrentWindowLabel();
|
||||
return windowLabel === SETTINGS_WINDOW_LABEL
|
||||
? setCloudSelectService(service)
|
||||
: setCurrentService(service);
|
||||
@@ -35,7 +44,7 @@ export async function handleLogout(serverId?: string) {
|
||||
// Update the status first
|
||||
setIsCurrentLogin(false);
|
||||
if (service?.id === id) {
|
||||
await setCurrentWindowService({ ...service, profile: null });
|
||||
await setCurrentWindowService({ ...service, profile: null }, true);
|
||||
}
|
||||
const updatedServerList = serverList.map((server) =>
|
||||
server.id === id ? { ...server, profile: null } : server
|
||||
|
||||
@@ -68,11 +68,12 @@ export default function Cloud() {
|
||||
}, [serverList, errors, cloudSelectService]);
|
||||
|
||||
const refreshClick = useCallback(
|
||||
async (id: string) => {
|
||||
async (id: string, callback?: () => void) => {
|
||||
setRefreshLoading(true);
|
||||
await platformAdapter.commands("refresh_coco_server_info", id);
|
||||
await refreshServerList();
|
||||
setRefreshLoading(false);
|
||||
callback && callback();
|
||||
},
|
||||
[refreshServerList]
|
||||
);
|
||||
|
||||
@@ -2,11 +2,6 @@ import { FC, memo, useCallback, useEffect, useState } from "react";
|
||||
import { Copy } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
getCurrent as getCurrentDeepLinkUrls,
|
||||
onOpenUrl,
|
||||
} from "@tauri-apps/plugin-deep-link";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
import { UserProfile } from "./UserProfile";
|
||||
import { OpenURLWithBrowser } from "@/utils";
|
||||
@@ -18,19 +13,21 @@ import { useServers } from "@/hooks/useServers";
|
||||
|
||||
interface ServiceAuthProps {
|
||||
setRefreshLoading: (loading: boolean) => void;
|
||||
refreshClick: (id: string) => void;
|
||||
refreshClick: (id: string, callback?: () => void) => void;
|
||||
}
|
||||
|
||||
const ServiceAuth = memo(
|
||||
({ setRefreshLoading, refreshClick }: ServiceAuthProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const language = useAppStore((state) => state.language);
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
const ssoRequestID = useAppStore((state) => state.ssoRequestID);
|
||||
const setSSORequestID = useAppStore((state) => state.setSSORequestID);
|
||||
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
|
||||
const cloudSelectService = useConnectStore((state) => state.cloudSelectService);
|
||||
const cloudSelectService = useConnectStore(
|
||||
(state) => state.cloudSelectService
|
||||
);
|
||||
|
||||
const { logoutServer } = useServers();
|
||||
|
||||
@@ -64,100 +61,25 @@ const ServiceAuth = memo(
|
||||
[logoutServer]
|
||||
);
|
||||
|
||||
const handleOAuthCallback = useCallback(
|
||||
async (code: string | null, serverId: string | null) => {
|
||||
if (!code || !serverId) {
|
||||
addError("No authorization code received");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Handling OAuth callback:", { code, serverId });
|
||||
await platformAdapter.commands("handle_sso_callback", {
|
||||
serverId: serverId, // Make sure 'server_id' is the correct argument
|
||||
requestId: ssoRequestID, // Make sure 'request_id' is the correct argument
|
||||
code: code,
|
||||
});
|
||||
|
||||
if (serverId != null) {
|
||||
refreshClick(serverId);
|
||||
}
|
||||
|
||||
getCurrentWindow().setFocus();
|
||||
} catch (e) {
|
||||
console.error("Sign in failed:", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[ssoRequestID]
|
||||
);
|
||||
|
||||
const handleUrl = (url: string) => {
|
||||
try {
|
||||
const urlObject = new URL(url.trim());
|
||||
console.log("handle urlObject:", urlObject);
|
||||
|
||||
// pass request_id and check with local, if the request_id are same, then continue
|
||||
const reqId = urlObject.searchParams.get("request_id");
|
||||
const code = urlObject.searchParams.get("code");
|
||||
|
||||
if (reqId != ssoRequestID) {
|
||||
console.log("Request ID not matched, skip");
|
||||
addError("Request ID not matched, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
const serverId = cloudSelectService?.id;
|
||||
handleOAuthCallback(code, serverId);
|
||||
} catch (err) {
|
||||
console.error("Failed to parse URL:", err);
|
||||
addError("Invalid URL format: " + err);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch the initial deep link intent
|
||||
// handle oauth success event
|
||||
useEffect(() => {
|
||||
// Function to handle pasted URL
|
||||
const handlePaste = (event: any) => {
|
||||
const pastedText = event.clipboardData.getData("text").trim();
|
||||
console.log("handle paste text:", pastedText);
|
||||
if (isValidCallbackUrl(pastedText)) {
|
||||
// Handle the URL as if it's a deep link
|
||||
console.log("handle callback on paste:", pastedText);
|
||||
handleUrl(pastedText);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to check if the pasted URL is valid for our deep link scheme
|
||||
const isValidCallbackUrl = (url: string) => {
|
||||
return url && url.startsWith("coco://oauth_callback");
|
||||
};
|
||||
|
||||
// Adding event listener for paste events
|
||||
document.addEventListener("paste", handlePaste);
|
||||
|
||||
getCurrentDeepLinkUrls()
|
||||
.then((urls) => {
|
||||
console.log("URLs:", urls);
|
||||
if (urls && urls.length > 0) {
|
||||
if (isValidCallbackUrl(urls[0].trim())) {
|
||||
handleUrl(urls[0]);
|
||||
}
|
||||
const unlistenOAuth = platformAdapter.listenEvent(
|
||||
"oauth_success",
|
||||
(event) => {
|
||||
const { serverId } = event.payload;
|
||||
if (serverId) {
|
||||
refreshClick(serverId, () => {
|
||||
setLoading(false);
|
||||
});
|
||||
addError(language === "zh" ? "登录成功" : "Login Success", "info");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to get initial URLs:", err);
|
||||
addError("Failed to get initial URLs: " + err);
|
||||
});
|
||||
|
||||
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
document.removeEventListener("paste", handlePaste);
|
||||
unlistenOAuth.then((fn) => fn());
|
||||
};
|
||||
}, [ssoRequestID]);
|
||||
}, [refreshClick]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
@@ -214,7 +136,9 @@ const ServiceAuth = memo(
|
||||
<button
|
||||
className="text-xs text-[#0096FB] dark:text-blue-400 block"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(cloudSelectService?.provider?.privacy_policy)
|
||||
OpenURLWithBrowser(
|
||||
cloudSelectService?.provider?.privacy_policy
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("cloud.privacyPolicy")}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAsyncEffect, useDebounce, useKeyPress, useUnmount } from "ahooks";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { CircleCheck, FolderDown, Loader } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -60,7 +60,7 @@ export interface SearchExtensionItem {
|
||||
views: number;
|
||||
};
|
||||
checksum: string;
|
||||
installed: boolean;
|
||||
installed?: boolean;
|
||||
commands?: Array<{
|
||||
type: string;
|
||||
name: string;
|
||||
@@ -73,7 +73,7 @@ export interface SearchExtensionItem {
|
||||
}>;
|
||||
}
|
||||
|
||||
const ExtensionStore = () => {
|
||||
const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
|
||||
const {
|
||||
searchValue,
|
||||
selectedExtension,
|
||||
@@ -107,7 +107,26 @@ const ExtensionStore = () => {
|
||||
};
|
||||
}, [selectedExtension]);
|
||||
|
||||
const handleExtensionDetail = useCallback(async () => {
|
||||
try {
|
||||
const detail = await platformAdapter.invokeBackend<SearchExtensionItem>(
|
||||
"extension_detail",
|
||||
{
|
||||
id: extensionId,
|
||||
}
|
||||
);
|
||||
setSelectedExtension(detail);
|
||||
setVisibleExtensionDetail(true);
|
||||
} catch (error) {
|
||||
addError(String(error));
|
||||
}
|
||||
}, [extensionId, installingExtensions]);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (extensionId) {
|
||||
return handleExtensionDetail();
|
||||
}
|
||||
|
||||
const result = await platformAdapter.invokeBackend<SearchExtensionItem[]>(
|
||||
"search_extension",
|
||||
{
|
||||
@@ -125,7 +144,7 @@ const ExtensionStore = () => {
|
||||
setList(result ?? []);
|
||||
|
||||
setSelectedExtension(result?.[0]);
|
||||
}, [debouncedSearchValue]);
|
||||
}, [debouncedSearchValue, extensionId]);
|
||||
|
||||
useUnmount(() => {
|
||||
setSelectedExtension(void 0);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, memo, useRef } from "react";
|
||||
import { useEffect, memo, useRef, useCallback, useState } from "react";
|
||||
|
||||
import DropdownList from "./DropdownList";
|
||||
import { SearchResults } from "@/components/Search/SearchResults";
|
||||
@@ -36,6 +36,8 @@ const SearchResultsPanel = memo<{
|
||||
performSearch,
|
||||
} = searchState;
|
||||
|
||||
const [extensionId, setExtensionId] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isChatMode && input) {
|
||||
performSearch(input);
|
||||
@@ -58,26 +60,63 @@ const SearchResultsPanel = memo<{
|
||||
}
|
||||
}, [selectedSearchContent]);
|
||||
|
||||
const handleOpenExtensionStore = useCallback(() => {
|
||||
platformAdapter.showWindow();
|
||||
changeMode && changeMode(false);
|
||||
|
||||
if (visibleExtensionStore || visibleExtensionDetail) return;
|
||||
|
||||
changeInput("");
|
||||
setSearchValue("");
|
||||
setVisibleExtensionStore(true);
|
||||
}, [
|
||||
changeMode,
|
||||
visibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
changeInput,
|
||||
setSearchValue,
|
||||
setVisibleExtensionStore,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = platformAdapter.listenEvent("open-extension-store", () => {
|
||||
platformAdapter.showWindow();
|
||||
changeMode && changeMode(false);
|
||||
const unlisten = platformAdapter.listenEvent(
|
||||
"open-extension-store",
|
||||
handleOpenExtensionStore
|
||||
);
|
||||
const unlisten_install = platformAdapter.listenEvent(
|
||||
"extension_install_success",
|
||||
(event) => {
|
||||
const { extensionId } = event.payload;
|
||||
|
||||
if (visibleExtensionStore || visibleExtensionDetail) return;
|
||||
|
||||
changeInput("");
|
||||
setSearchValue("");
|
||||
setVisibleExtensionStore(true);
|
||||
});
|
||||
setExtensionId(extensionId);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => {
|
||||
fn();
|
||||
});
|
||||
unlisten_install.then((fn) => {
|
||||
fn();
|
||||
});
|
||||
};
|
||||
}, [visibleExtensionStore, visibleExtensionDetail]);
|
||||
}, [handleOpenExtensionStore]);
|
||||
|
||||
if (visibleExtensionStore) return <ExtensionStore />;
|
||||
useEffect(() => {
|
||||
if (visibleExtensionDetail) return;
|
||||
|
||||
setExtensionId(void 0);
|
||||
}, [visibleExtensionDetail]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!extensionId) return;
|
||||
|
||||
handleOpenExtensionStore();
|
||||
}, [extensionId]);
|
||||
|
||||
if (visibleExtensionStore) {
|
||||
return <ExtensionStore extensionId={extensionId} />;
|
||||
}
|
||||
if (goAskAi) return <AskAi isChatMode={isChatMode} />;
|
||||
if (suggests.length === 0) return <NoResults />;
|
||||
|
||||
|
||||
183
src/hooks/useDeepLinkManager.ts
Normal file
183
src/hooks/useDeepLinkManager.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
getCurrent as getCurrentDeepLinkUrls,
|
||||
onOpenUrl,
|
||||
} from "@tauri-apps/plugin-deep-link";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useConnectStore } from "@/stores/connectStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MAIN_WINDOW_LABEL } from "@/constants";
|
||||
import { useAsyncEffect, useEventListener } from "ahooks";
|
||||
|
||||
export interface DeepLinkHandler {
|
||||
pattern: string;
|
||||
handler: (url: URL) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export function useDeepLinkManager() {
|
||||
const addError = useAppStore((state) => state.addError);
|
||||
const ssoRequestID = useAppStore((state) => state.ssoRequestID);
|
||||
const cloudSelectService = useConnectStore(
|
||||
(state) => state.cloudSelectService
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// handle oauth callback
|
||||
const handleOAuthCallback = useCallback(
|
||||
async (url: URL) => {
|
||||
try {
|
||||
const reqId = url.searchParams.get("request_id");
|
||||
const code = url.searchParams.get("code");
|
||||
|
||||
if (reqId !== ssoRequestID) {
|
||||
console.log("Request ID not matched, skip");
|
||||
addError("Request ID not matched, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
const serverId = cloudSelectService?.id;
|
||||
if (!code || !serverId) {
|
||||
addError("No authorization code received");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Handling OAuth callback:", { code, serverId });
|
||||
await platformAdapter.commands("handle_sso_callback", {
|
||||
serverId: serverId,
|
||||
requestId: ssoRequestID,
|
||||
code: code,
|
||||
});
|
||||
|
||||
// trigger oauth success event
|
||||
platformAdapter.emitEvent("oauth_success", { serverId });
|
||||
getCurrentWindow().setFocus();
|
||||
} catch (err) {
|
||||
console.error("Failed to parse OAuth callback URL:", err);
|
||||
addError("Invalid OAuth callback URL format: " + err);
|
||||
}
|
||||
},
|
||||
[ssoRequestID, cloudSelectService, addError]
|
||||
);
|
||||
|
||||
// handle install extension from store
|
||||
const handleInstallExtension = useCallback(async (url: URL) => {
|
||||
const extensionId = url.searchParams.get("id");
|
||||
if (!extensionId) {
|
||||
return console.warn(
|
||||
'received an invalid install_extension_from_store deeplink, missing argument "id"'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await platformAdapter.showWindow();
|
||||
|
||||
await invoke("install_extension_from_store", { id: extensionId });
|
||||
|
||||
// trigger extension install success event
|
||||
platformAdapter.emitEvent("extension_install_success", { extensionId });
|
||||
addError(t("deepLink.extensionInstallSuccessfully"), "info");
|
||||
console.log("Extension installed successfully:", extensionId);
|
||||
} catch (error) {
|
||||
addError(String(error));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// handle deep link
|
||||
const handlers: DeepLinkHandler[] = [
|
||||
{
|
||||
pattern: "oauth_callback",
|
||||
handler: handleOAuthCallback,
|
||||
},
|
||||
{
|
||||
pattern: "install_extension_from_store",
|
||||
handler: async (url) => {
|
||||
const windowLabel = await platformAdapter.getCurrentWindowLabel();
|
||||
|
||||
if (windowLabel !== MAIN_WINDOW_LABEL) return;
|
||||
|
||||
handleInstallExtension(url);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// handle deep link
|
||||
const handleUrl = useCallback(
|
||||
(url: string) => {
|
||||
console.debug("handling deeplink URL", url);
|
||||
|
||||
try {
|
||||
const urlObject = new URL(url.trim());
|
||||
const deeplinkIdentifier = urlObject.hostname;
|
||||
|
||||
// find handler by pattern
|
||||
const handler = handlers.find((h) => h.pattern === deeplinkIdentifier);
|
||||
|
||||
if (handler) {
|
||||
handler.handler(urlObject);
|
||||
} else {
|
||||
console.error("Unknown deep link:", url);
|
||||
addError("Unknown deep link: " + url);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to parse URL:", err);
|
||||
addError("Invalid URL format: " + err);
|
||||
}
|
||||
},
|
||||
[handlers]
|
||||
);
|
||||
|
||||
// handle paste text
|
||||
const handlePaste = useCallback(
|
||||
(event: ClipboardEvent) => {
|
||||
const pastedText = event.clipboardData?.getData("text")?.trim();
|
||||
console.log("handle paste text:", pastedText);
|
||||
|
||||
// coco://oauth_callback
|
||||
if (pastedText && pastedText.startsWith("coco://oauth_callback")) {
|
||||
console.log("handle deeplink on paste:", pastedText);
|
||||
handleUrl(pastedText);
|
||||
}
|
||||
},
|
||||
[handleUrl]
|
||||
);
|
||||
|
||||
// get initial deep link
|
||||
useAsyncEffect(async () => {
|
||||
try {
|
||||
const urls = await getCurrentDeepLinkUrls();
|
||||
|
||||
console.log("Initial DeepLinkUrls:", urls);
|
||||
|
||||
if (urls && urls.length > 0) {
|
||||
handleUrl(urls[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
addError("Failed to get initial URLs: " + error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// handle deep link on paste
|
||||
useEffect(() => {
|
||||
// handle new deep link
|
||||
const unlisten = onOpenUrl((urls) => {
|
||||
console.log("onOpenUrl urls", urls);
|
||||
|
||||
handleUrl(urls[0]);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
};
|
||||
}, []);
|
||||
|
||||
// add paste event listener
|
||||
useEventListener("paste", handlePaste);
|
||||
|
||||
return {
|
||||
handleUrl,
|
||||
};
|
||||
}
|
||||
@@ -573,5 +573,8 @@
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
}
|
||||
},
|
||||
"deepLink": {
|
||||
"extensionInstallSuccessfully": "Extension installed successfully."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,5 +572,8 @@
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
}
|
||||
},
|
||||
"deepLink": {
|
||||
"extensionInstallSuccessfully": "扩展安装成功。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useIconfontScript } from "@/hooks/useScript";
|
||||
import { Extension } from "@/components/Settings/Extensions";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { useServers } from "@/hooks/useServers";
|
||||
import { useDeepLinkManager } from '@/hooks/useDeepLinkManager';
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
@@ -31,6 +32,8 @@ export default function Layout() {
|
||||
|
||||
// init servers isTauri
|
||||
useServers();
|
||||
// init deep link manager
|
||||
useDeepLinkManager();
|
||||
|
||||
const [langUpdated, setLangUpdated] = useState(false);
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ export interface EventPayloads {
|
||||
"chat-create-error": string;
|
||||
[key: `synthesize-${string}`]: any;
|
||||
"check-update": any;
|
||||
"oauth_success": any;
|
||||
"extension_install_success": any;
|
||||
}
|
||||
|
||||
// Window operation interface
|
||||
|
||||
@@ -39,7 +39,9 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
|
||||
|
||||
async showWindow() {
|
||||
const window = await windowWrapper.getWebviewWindow();
|
||||
return window?.show();
|
||||
window?.show();
|
||||
window?.unminimize();
|
||||
return window?.setFocus();
|
||||
},
|
||||
|
||||
async emitEvent(event, payload) {
|
||||
|
||||
Reference in New Issue
Block a user