Skip to content

Commit 783cb73

Browse files
SteveLauCRainyNight9ayangweb
authored
feat: deeplink handler for install ext from store (#860)
Co-authored-by: rain9 <15911122312@163.com> Co-authored-by: ayang <473033518@qq.com>
1 parent ee75f0d commit 783cb73

File tree

15 files changed

+375
-126
lines changed

15 files changed

+375
-126
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,6 @@
8383
"i18n-ally.keystyle": "nested",
8484
"editor.tabSize": 2,
8585
"editor.insertSpaces": true,
86-
"editor.detectIndentation": false
86+
"editor.detectIndentation": false,
87+
"i18n-ally.displayLanguage": "zh"
8788
}

src-tauri/capabilities/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"updater:default",
7070
"windows-version:default",
7171
"log:default",
72-
"opener:default"
72+
"opener:default",
73+
"core:window:allow-unminimize"
7374
]
7475
}

src-tauri/src/extension/third_party/install/store.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::extension::third_party::install::filter_out_incompatible_sub_extensio
1919
use crate::server::http_client::HttpClient;
2020
use crate::util::platform::Platform;
2121
use async_trait::async_trait;
22+
use http::Method;
2223
use reqwest::StatusCode;
2324
use serde_json::Map as JsonObject;
2425
use serde_json::Value as Json;
@@ -172,6 +173,52 @@ pub(crate) async fn search_extension(
172173
Ok(extensions)
173174
}
174175

176+
#[tauri::command]
177+
pub(crate) async fn extension_detail(
178+
id: String,
179+
) -> Result<Option<JsonObject<String, Json>>, String> {
180+
let url = format!("http://dev.infini.cloud:27200/store/extension/{}", id);
181+
let response =
182+
HttpClient::send_raw_request(Method::GET, url.as_str(), None, None, None).await?;
183+
184+
if response.status() == StatusCode::NOT_FOUND {
185+
return Ok(None);
186+
}
187+
188+
let response_dbg_str = format!("{:?}", response);
189+
// The response of an ES style GET request
190+
let mut response: JsonObject<String, Json> = response.json().await.unwrap_or_else(|_e| {
191+
panic!(
192+
"response body of [/store/extension/<ID>] is not a JSON object, response [{:?}]",
193+
response_dbg_str
194+
)
195+
});
196+
let source_json = response.remove("_source").unwrap_or_else(|| {
197+
panic!("field [_source] not found in the JSON returned from [/store/extension/<ID>]")
198+
});
199+
let mut source_obj = match source_json {
200+
Json::Object(obj) => obj,
201+
_ => panic!(
202+
"field [_source] should be a JSON object, but it is not, value: [{}]",
203+
source_json
204+
),
205+
};
206+
207+
let developer_id = match &source_obj["developer"]["id"] {
208+
Json::String(dev) => dev,
209+
_ => {
210+
panic!(
211+
"field [_source.developer.id] should be a string, but it is not, value: [{}]",
212+
source_obj["developer"]["id"]
213+
)
214+
}
215+
};
216+
let installed = is_extension_installed(developer_id, &id).await;
217+
source_obj.insert("installed".to_string(), Json::Bool(installed));
218+
219+
Ok(Some(source_obj))
220+
}
221+
175222
#[tauri::command]
176223
pub(crate) async fn install_extension_from_store(
177224
tauri_app_handle: AppHandle,
@@ -250,21 +297,32 @@ pub(crate) async fn install_extension_from_store(
250297
e
251298
);
252299
});
300+
let developer_id = extension.developer.clone().expect("developer has been set");
253301

254302
drop(plugin_json);
255303

256304
general_check(&extension)?;
257305

306+
let current_platform = Platform::current();
307+
if let Some(ref platforms) = extension.platforms {
308+
if !platforms.contains(&current_platform) {
309+
return Err("this extension is not compatible with your OS".into());
310+
}
311+
}
312+
313+
if is_extension_installed(&developer_id, &id).await {
314+
return Err("Extension already installed.".into());
315+
}
316+
258317
// Extension is compatible with current platform, but it could contain sub
259318
// extensions that are not, filter them out.
260-
filter_out_incompatible_sub_extensions(&mut extension, Platform::current());
319+
filter_out_incompatible_sub_extensions(&mut extension, current_platform);
261320

262321
// Write extension files to the extension directory
263-
let developer = extension.developer.clone().unwrap_or_default();
264322
let extension_id = extension.id.clone();
265323
let extension_directory = {
266324
let mut path = get_third_party_extension_directory(&tauri_app_handle);
267-
path.push(developer);
325+
path.push(developer_id);
268326
path.push(extension_id.as_str());
269327
path
270328
};

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ pub fn run() {
161161
extension::unregister_extension_hotkey,
162162
extension::is_extension_enabled,
163163
extension::third_party::install::store::search_extension,
164+
extension::third_party::install::store::extension_detail,
164165
extension::third_party::install::store::install_extension_from_store,
165166
extension::third_party::install::local_extension::install_local_extension,
166167
extension::third_party::uninstall_extension,

src/commands/windowService.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,20 @@ export async function getCurrentWindowService() {
1313
: currentService;
1414
}
1515

16-
export async function setCurrentWindowService(service: any) {
17-
const windowLabel = await platformAdapter.getCurrentWindowLabel();
16+
export async function setCurrentWindowService(
17+
service: any,
18+
isAll?: boolean
19+
) {
1820
const { setCurrentService, setCloudSelectService } =
1921
useConnectStore.getState();
20-
22+
// all refresh logout
23+
if (isAll) {
24+
setCloudSelectService(service);
25+
setCurrentService(service);
26+
return;
27+
}
28+
// current refresh
29+
const windowLabel = await platformAdapter.getCurrentWindowLabel();
2130
return windowLabel === SETTINGS_WINDOW_LABEL
2231
? setCloudSelectService(service)
2332
: setCurrentService(service);
@@ -35,7 +44,7 @@ export async function handleLogout(serverId?: string) {
3544
// Update the status first
3645
setIsCurrentLogin(false);
3746
if (service?.id === id) {
38-
await setCurrentWindowService({ ...service, profile: null });
47+
await setCurrentWindowService({ ...service, profile: null }, true);
3948
}
4049
const updatedServerList = serverList.map((server) =>
4150
server.id === id ? { ...server, profile: null } : server

src/components/Cloud/Cloud.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,12 @@ export default function Cloud() {
6868
}, [serverList, errors, cloudSelectService]);
6969

7070
const refreshClick = useCallback(
71-
async (id: string) => {
71+
async (id: string, callback?: () => void) => {
7272
setRefreshLoading(true);
7373
await platformAdapter.commands("refresh_coco_server_info", id);
7474
await refreshServerList();
7575
setRefreshLoading(false);
76+
callback && callback();
7677
},
7778
[refreshServerList]
7879
);

src/components/Cloud/ServiceAuth.tsx

Lines changed: 23 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import { FC, memo, useCallback, useEffect, useState } from "react";
22
import { Copy } from "lucide-react";
33
import { useTranslation } from "react-i18next";
44
import { v4 as uuidv4 } from "uuid";
5-
import {
6-
getCurrent as getCurrentDeepLinkUrls,
7-
onOpenUrl,
8-
} from "@tauri-apps/plugin-deep-link";
9-
import { getCurrentWindow } from "@tauri-apps/api/window";
105

116
import { UserProfile } from "./UserProfile";
127
import { OpenURLWithBrowser } from "@/utils";
@@ -18,19 +13,21 @@ import { useServers } from "@/hooks/useServers";
1813

1914
interface ServiceAuthProps {
2015
setRefreshLoading: (loading: boolean) => void;
21-
refreshClick: (id: string) => void;
16+
refreshClick: (id: string, callback?: () => void) => void;
2217
}
2318

2419
const ServiceAuth = memo(
2520
({ setRefreshLoading, refreshClick }: ServiceAuthProps) => {
2621
const { t } = useTranslation();
2722

23+
const language = useAppStore((state) => state.language);
24+
const addError = useAppStore((state) => state.addError);
2825
const ssoRequestID = useAppStore((state) => state.ssoRequestID);
2926
const setSSORequestID = useAppStore((state) => state.setSSORequestID);
3027

31-
const addError = useAppStore((state) => state.addError);
32-
33-
const cloudSelectService = useConnectStore((state) => state.cloudSelectService);
28+
const cloudSelectService = useConnectStore(
29+
(state) => state.cloudSelectService
30+
);
3431

3532
const { logoutServer } = useServers();
3633

@@ -64,100 +61,25 @@ const ServiceAuth = memo(
6461
[logoutServer]
6562
);
6663

67-
const handleOAuthCallback = useCallback(
68-
async (code: string | null, serverId: string | null) => {
69-
if (!code || !serverId) {
70-
addError("No authorization code received");
71-
return;
72-
}
73-
74-
try {
75-
console.log("Handling OAuth callback:", { code, serverId });
76-
await platformAdapter.commands("handle_sso_callback", {
77-
serverId: serverId, // Make sure 'server_id' is the correct argument
78-
requestId: ssoRequestID, // Make sure 'request_id' is the correct argument
79-
code: code,
80-
});
81-
82-
if (serverId != null) {
83-
refreshClick(serverId);
84-
}
85-
86-
getCurrentWindow().setFocus();
87-
} catch (e) {
88-
console.error("Sign in failed:", e);
89-
} finally {
90-
setLoading(false);
91-
}
92-
},
93-
[ssoRequestID]
94-
);
95-
96-
const handleUrl = (url: string) => {
97-
try {
98-
const urlObject = new URL(url.trim());
99-
console.log("handle urlObject:", urlObject);
100-
101-
// pass request_id and check with local, if the request_id are same, then continue
102-
const reqId = urlObject.searchParams.get("request_id");
103-
const code = urlObject.searchParams.get("code");
104-
105-
if (reqId != ssoRequestID) {
106-
console.log("Request ID not matched, skip");
107-
addError("Request ID not matched, skip");
108-
return;
109-
}
110-
111-
const serverId = cloudSelectService?.id;
112-
handleOAuthCallback(code, serverId);
113-
} catch (err) {
114-
console.error("Failed to parse URL:", err);
115-
addError("Invalid URL format: " + err);
116-
}
117-
};
118-
119-
// Fetch the initial deep link intent
64+
// handle oauth success event
12065
useEffect(() => {
121-
// Function to handle pasted URL
122-
const handlePaste = (event: any) => {
123-
const pastedText = event.clipboardData.getData("text").trim();
124-
console.log("handle paste text:", pastedText);
125-
if (isValidCallbackUrl(pastedText)) {
126-
// Handle the URL as if it's a deep link
127-
console.log("handle callback on paste:", pastedText);
128-
handleUrl(pastedText);
129-
}
130-
};
131-
132-
// Function to check if the pasted URL is valid for our deep link scheme
133-
const isValidCallbackUrl = (url: string) => {
134-
return url && url.startsWith("coco://oauth_callback");
135-
};
136-
137-
// Adding event listener for paste events
138-
document.addEventListener("paste", handlePaste);
139-
140-
getCurrentDeepLinkUrls()
141-
.then((urls) => {
142-
console.log("URLs:", urls);
143-
if (urls && urls.length > 0) {
144-
if (isValidCallbackUrl(urls[0].trim())) {
145-
handleUrl(urls[0]);
146-
}
66+
const unlistenOAuth = platformAdapter.listenEvent(
67+
"oauth_success",
68+
(event) => {
69+
const { serverId } = event.payload;
70+
if (serverId) {
71+
refreshClick(serverId, () => {
72+
setLoading(false);
73+
});
74+
addError(language === "zh" ? "登录成功" : "Login Success", "info");
14775
}
148-
})
149-
.catch((err) => {
150-
console.error("Failed to get initial URLs:", err);
151-
addError("Failed to get initial URLs: " + err);
152-
});
153-
154-
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
76+
}
77+
);
15578

15679
return () => {
157-
unlisten.then((fn) => fn());
158-
document.removeEventListener("paste", handlePaste);
80+
unlistenOAuth.then((fn) => fn());
15981
};
160-
}, [ssoRequestID]);
82+
}, [refreshClick]);
16183

16284
useEffect(() => {
16385
setLoading(false);
@@ -214,7 +136,9 @@ const ServiceAuth = memo(
214136
<button
215137
className="text-xs text-[#0096FB] dark:text-blue-400 block"
216138
onClick={() =>
217-
OpenURLWithBrowser(cloudSelectService?.provider?.privacy_policy)
139+
OpenURLWithBrowser(
140+
cloudSelectService?.provider?.privacy_policy
141+
)
218142
}
219143
>
220144
{t("cloud.privacyPolicy")}

src/components/Search/ExtensionStore.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useAsyncEffect, useDebounce, useKeyPress, useUnmount } from "ahooks";
2-
import { useEffect, useState } from "react";
2+
import { useCallback, useEffect, useState } from "react";
33
import { CircleCheck, FolderDown, Loader } from "lucide-react";
44
import clsx from "clsx";
55
import { useTranslation } from "react-i18next";
@@ -60,7 +60,7 @@ export interface SearchExtensionItem {
6060
views: number;
6161
};
6262
checksum: string;
63-
installed: boolean;
63+
installed?: boolean;
6464
commands?: Array<{
6565
type: string;
6666
name: string;
@@ -73,7 +73,7 @@ export interface SearchExtensionItem {
7373
}>;
7474
}
7575

76-
const ExtensionStore = () => {
76+
const ExtensionStore = ({ extensionId }: { extensionId?: string }) => {
7777
const {
7878
searchValue,
7979
selectedExtension,
@@ -107,7 +107,26 @@ const ExtensionStore = () => {
107107
};
108108
}, [selectedExtension]);
109109

110+
const handleExtensionDetail = useCallback(async () => {
111+
try {
112+
const detail = await platformAdapter.invokeBackend<SearchExtensionItem>(
113+
"extension_detail",
114+
{
115+
id: extensionId,
116+
}
117+
);
118+
setSelectedExtension(detail);
119+
setVisibleExtensionDetail(true);
120+
} catch (error) {
121+
addError(String(error));
122+
}
123+
}, [extensionId, installingExtensions]);
124+
110125
useAsyncEffect(async () => {
126+
if (extensionId) {
127+
return handleExtensionDetail();
128+
}
129+
111130
const result = await platformAdapter.invokeBackend<SearchExtensionItem[]>(
112131
"search_extension",
113132
{
@@ -125,7 +144,7 @@ const ExtensionStore = () => {
125144
setList(result ?? []);
126145

127146
setSelectedExtension(result?.[0]);
128-
}, [debouncedSearchValue]);
147+
}, [debouncedSearchValue, extensionId]);
129148

130149
useUnmount(() => {
131150
setSelectedExtension(void 0);

0 commit comments

Comments
 (0)