From 81a02890d67144844251c5c67356583876f7f11a Mon Sep 17 00:00:00 2001 From: BiggerRain <15911122312@163.COM> Date: Fri, 7 Feb 2025 20:26:45 +0800 Subject: [PATCH] fix: mouse & keyDown (#122) * feat: impl Coco server related APIs * chore: remove unused method * fix: invoke Rust interfaces in tauri::run() * chore: add invoke * feat: add add_coco_server * fix: trim the tailing forward slash * feat: interface get_user_profiles * chore: add * fix: store the servers in add interface * chore: ass * fix: skip non-publich servers with no token * feat: add * feat: get datasources and connectors * fix: invoke interfaces in tauri::run() * chore: add SidebarRef * refactor: refactoring coco-app * refactor: refactoring coco app * refactor: refactoring project layout * refactor: refactoring server management * chore: cleanup code * chore: display error when connect failed * refactor: refactoring refresh server's info * refactor: refactoring how to connect the coco serverg * chore: rename to cloud * refactor: refactoring remove coco server * fix: refresh current selected server * fix: reset server selection * chore: update login status * feat: add error message tips * fix: fix login and logout * refactor: refactoring http client * fix: fix the datasources * chore: minor fix * refactor: refactoring code * fix: fix search api * chore: optimize part of icons * chore: fix build * refactor: search list icon * refactor: search list icon * chore: lib * feat: add plugin-os * feat: add data-dark * fix: mouse & keyDown * fix: mouse & keyDown * fix: mouse & keyDown --------- Co-authored-by: Steve Lau Co-authored-by: medcl --- src/components/Cloud/Cloud.tsx | 813 ++++++++++---------- src/components/Common/InputBox.tsx | 2 +- src/components/Search/DocumentList.tsx | 143 ++-- src/components/Search/Footer.tsx | 6 +- src/components/Search/InputBox.tsx | 2 +- src/components/Settings/GeneralSettings.tsx | 39 +- src/contexts/ThemeContext.tsx | 26 +- src/main.css | 134 ++-- tailwind.config.js | 2 +- 9 files changed, 582 insertions(+), 585 deletions(-) diff --git a/src/components/Cloud/Cloud.tsx b/src/components/Cloud/Cloud.tsx index 43c580ec..41eb26d9 100644 --- a/src/components/Cloud/Cloud.tsx +++ b/src/components/Cloud/Cloud.tsx @@ -1,476 +1,443 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { - RefreshCcw, - Globe, - PackageOpen, - GitFork, - CalendarSync, - Trash2, - Copy, + RefreshCcw, + Globe, + PackageOpen, + GitFork, + CalendarSync, + Trash2, + Copy, } from "lucide-react"; import { v4 as uuidv4 } from "uuid"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { - onOpenUrl, - getCurrent as getCurrentDeepLinkUrls, + onOpenUrl, + getCurrent as getCurrentDeepLinkUrls, } from "@tauri-apps/plugin-deep-link"; import { invoke } from "@tauri-apps/api/core"; import { UserProfile } from "./UserProfile"; import { DataSourcesList } from "./DataSourcesList"; import { Sidebar } from "./Sidebar"; -import { Connect } from "./Connect.tsx"; +import { Connect } from "./Connect"; import { OpenURLWithBrowser } from "@/utils"; import { useAppStore } from "@/stores/appStore"; import { useConnectStore } from "@/stores/connectStore"; import bannerImg from "@/assets/images/coco-cloud-banner.jpeg"; export default function Cloud() { - const SidebarRef = useRef<{ refreshData: () => void; }>(null); + const SidebarRef = useRef<{ refreshData: () => void }>(null); - // const [error, setError] = useState(null); - const error = useAppStore((state) => state.error); - const setError = useAppStore((state) => state.setError); + const error = useAppStore((state) => state.error); + const setError = useAppStore((state) => state.setError); - const [isConnect, setIsConnect] = useState(true); - // const [ssoRequestID, setSSORequestID] = useState(""); - const ssoRequestID = useAppStore((state) => state.ssoRequestID); - const setSSORequestID = useAppStore((state) => state.setSSORequestID); + const [isConnect, setIsConnect] = useState(true); - // const ssoServerID = useAppStore((state) => state.ssoServerID); - // const setSSOServerID = useAppStore((state) => state.setSSOServerID); + const ssoRequestID = useAppStore((state) => state.ssoRequestID); + const setSSORequestID = useAppStore((state) => state.setSSORequestID); - const endpoint = useAppStore((state) => state.endpoint); + const endpoint = useAppStore((state) => state.endpoint); - const currentService = useConnectStore((state) => state.currentService); - const setCurrentService = useConnectStore((state) => state.setCurrentService); + const currentService = useConnectStore((state) => state.currentService); + const setCurrentService = useConnectStore((state) => state.setCurrentService); - const serverList = useConnectStore((state) => state.serverList); - const setServerList = useConnectStore((state) => state.setServerList); + const serverList = useConnectStore((state) => state.serverList); + const setServerList = useConnectStore((state) => state.setServerList); - const [loading, setLoading] = useState(false); - const [refreshLoading, setRefreshLoading] = useState(false); - // const [profiles, setProfiles] = useState({}); - // const [userInfo, setUserInfo] = useState({}); + const [loading, setLoading] = useState(false); + const [refreshLoading, setRefreshLoading] = useState(false); + // fetch the servers + useEffect(() => { + fetchServers(true); + }, []); + useEffect(() => { + console.log("currentService", currentService); + setLoading(false); + setRefreshLoading(false); + setError(""); + setIsConnect(true); + }, [JSON.stringify(currentService)]); - //fetch the servers - useEffect(() => { - fetchServers(true); - }, []); + const fetchServers = async (resetSelection: boolean) => { + invoke("list_coco_servers") + .then((res: any) => { + console.log("list_coco_servers", res); + setServerList(res); + if (resetSelection && res.length > 0) { + console.log("setCurrentService", res[res.length - 1]); + setCurrentService(res[res.length - 1]); + } else { + console.warn("Service list is empty or last item has no id"); + } + }) + .catch((err: any) => { + setError(err); + console.error(err); + }); + }; - useEffect(() => { - console.log("currentService", currentService); - setLoading(false); + const add_coco_server = (endpointLink: string) => { + if (!endpointLink) { + throw new Error("Endpoint is required"); + } + if ( + !endpointLink.startsWith("http://") && + !endpointLink.startsWith("https://") + ) { + throw new Error("Invalid Endpoint"); + } + + setRefreshLoading(true); + + return invoke("add_coco_server", { endpoint: endpointLink }) + .then((res: any) => { + console.log("add_coco_server", res); + fetchServers(false) + .then((r) => { + console.log("fetchServers", r); + setCurrentService(res); + }) + .catch((err: any) => { + console.error("fetchServers failed:", err); + setError(err); + throw err; // Propagate error back up to outer promise chain + }); + }) + .catch((err: any) => { + // Handle the invoke error + console.error("add coco server failed:", err); + setError(err); + throw err; // Propagate error back up + }) + .finally(() => { setRefreshLoading(false); - setError(""); - // setEndpoint(currentService.endpoint); - setIsConnect(true); - // setUserInfo(profiles[endpoint] || {}) - }, [JSON.stringify(currentService)]); + }); + }; - // const get_user_profiles = useCallback(() => { - // invoke("get_user_profiles") - // .then((res: any) => { - // console.log("get_user_profiles", res); - // setProfiles(res); - // console.log("setUserInfo", res[endpoint]); - // setUserInfo(res[endpoint] || {}) - // }) - // .catch((err: any) => { - // console.error(err); - // }); - // }, [endpoint]); + const handleOAuthCallback = useCallback( + async (code: string | null, serverId: string | null) => { + if (!code) { + setError("No authorization code received"); + return; + } - useEffect(() => { - // get_user_profiles() - }, []) + try { + console.log("Handling OAuth callback:", { code, serverId }); + await invoke("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, + }); - const fetchServers = async (resetSelection: boolean) => { - invoke("list_coco_servers") - .then((res: any) => { - console.log("list_coco_servers", res); - setServerList(res); - if (resetSelection && res.length > 0) { - console.log("setCurrentService", res[res.length - 1]); - setCurrentService(res[res.length - 1]); - } else { - console.warn("Service list is empty or last item has no id"); - } - }) - .catch((err: any) => { - setError(err); - console.error(err); - }); - }; - - const add_coco_server = (endpointLink: string) => { - if (!endpointLink) { - throw new Error('Endpoint is required'); - } - if (!endpointLink.startsWith("http://") && !endpointLink.startsWith("https://")) { - throw new Error('Invalid Endpoint'); + if (serverId != null) { + refreshClick(serverId); } - setRefreshLoading(true); + getCurrentWindow() + .setFocus() + .catch((err) => { + setError(err); + }); + } catch (e) { + console.error("Sign in failed:", e); + setError("SSO login failed: " + e); + throw error; + } finally { + setLoading(false); + } + }, + [ssoRequestID, endpoint] + ); - return invoke("add_coco_server", { endpoint: endpointLink }) - .then((res: any) => { - console.log("add_coco_server", res); - fetchServers(false) - .then((r) => { - console.log("fetchServers", r); - setCurrentService(res); - }) - .catch((err: any) => { - console.error("fetchServers failed:", err); - setError(err); - throw err; // Propagate error back up to outer promise chain - }); - }) - .catch((err: any) => { - // Handle the invoke error - console.error("add coco server failed:", err); - setError(err); - throw err; // Propagate error back up - }) - .finally(() => { - setRefreshLoading(false); - }); - }; + const handleUrl = (url: string) => { + try { + const urlObject = new URL(url); + console.log("handle urlObject:", urlObject); - const handleOAuthCallback = useCallback( - async (code: string | null, serverId: string | null) => { + // TODO, 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 (!code) { - setError("No authorization code received"); - return; - } + if (reqId != ssoRequestID) { + console.log("Request ID not matched, skip"); + setError("Request ID not matched, skip"); + return; + } - try { - console.log("Handling OAuth callback:", { code, serverId }); - await invoke("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((err) => { - setError(err); - }); - - } catch (e) { - console.error("Sign in failed:", e); - setError("SSO login failed: " + e); - // setAuth(undefined, endpoint); - throw error; - } finally { - setLoading(false); - } - }, - [ssoRequestID, endpoint] - ); - - const handleUrl = (url: string) => { - try { - // url = "coco://oauth_callback?code=cuhhi8o2sdbbbcoe0g10ktmht6aky3jmd4xkwsgvzf748i4zdgr898bfeu3kze7ffdusdtbgtnpke8ng3fe6&provider=coco-cloud/" - const urlObject = new URL(url); - console.log("handle urlObject:", urlObject); - - //TODO, 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"); - setError("Request ID not matched, skip"); - return; - } - - const serverId = currentService?.id; - handleOAuthCallback(code, serverId); - - // switch (urlObject.hostname) { - // case "/oauth_callback": - - // break; - - // default: - // console.log("Unhandled deep link path:", urlObject.pathname); - // } - } catch (err) { - console.error("Failed to parse URL:", err); - setError("Invalid URL format: " + err); - } - }; - - - // Fetch the initial deep link intent - useEffect(() => { - // Function to handle pasted URL - const handlePaste = (event: any) => { - const pastedText = event.clipboardData.getData('text'); - 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])) { - handleUrl(urls[0]); - } - } - }) - .catch((err) => { - console.error("Failed to get initial URLs:", err); - setError("Failed to get initial URLs: " + err); - }); - - const unlisten = onOpenUrl((urls) => handleUrl(urls[0])); - - return () => { - unlisten.then((fn) => fn()); - document.removeEventListener('paste', handlePaste); - }; - }, [ssoRequestID]); - - // const generateLogin = () => { - // const requestID = uuidv4(); - // setSSORequestID(requestID); - // setSSOServerID(currentService?.id); // Set server ID - // - // // The URL is now updated when ssoRequestID and ssoServerID are both set - // }; - - const LoginClick = useCallback(() => { - if (loading) return; // Prevent multiple clicks if already loading - - // If the appUid doesn't exist, generate one - // if (!ssoRequestID) { - let requestID = uuidv4(); - setSSORequestID(requestID); - // setSSOServerID(currentService?.id); - // } - - // Generate the login URL with the current appUid - const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`; - - console.log("Open SSO link, requestID:", ssoRequestID, url); - - // Open the URL in a browser - OpenURLWithBrowser(url); - - // Start loading state - setLoading(true); - - }, [ssoRequestID, loading, currentService]); - - const refreshClick = (id: string) => { - setRefreshLoading(true); - invoke("refresh_coco_server_info", { id }) - .then((res: any) => { - console.log("refresh_coco_server_info", id, JSON.stringify(res)); - fetchServers(false).then(r => { - console.log("fetchServers", r); - }); - //update currentService - setCurrentService(res); - }) - .catch((err: any) => { - setError(err); - console.error(err); - }) - .finally(() => { - setRefreshLoading(false); - }); - }; - - function onAddServer() { - setIsConnect(false); - } - function onLogout(id: string) { - console.log("onLogout", id); - setRefreshLoading(true); - invoke("logout_coco_server", { id }) - .then((res: any) => { - console.log("logout_coco_server", id, JSON.stringify(res)); - refreshClick(id); - }) - .catch((err: any) => { - setError(err); - console.error(err); - }).finally(() => { - setRefreshLoading(false); - }); + const serverId = currentService?.id; + handleOAuthCallback(code, serverId); + } catch (err) { + console.error("Failed to parse URL:", err); + setError("Invalid URL format: " + err); } + }; - const remove_coco_server = (id: string) => { - invoke("remove_coco_server", { id }) - .then((res: any) => { - console.log("remove_coco_server", id, JSON.stringify(res)); - fetchServers(true).then(r => { - console.log("fetchServers", r); - }) - }) - .catch((err: any) => { - //TODO display the error message - setError(err); - console.error(err); - }); + // Fetch the initial deep link intent + useEffect(() => { + // Test the handleUrl function + // handleUrl("coco://oauth_callback?code=cui88lg2sdb4dnu97jpgypcugrskkt1i3venntth7gk52exnq8hxufxvqn8hhegoaw369s394bcyb6ehtnhz&request_id=642a985c-6baa-4ec8-be41-d8c6ddbc0e60&provider=coco-cloud/"); + // Function to handle pasted URL + const handlePaste = (event: any) => { + const pastedText = event.clipboardData.getData("text"); + 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); + } }; - return ( -
- + // 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); - {isConnect ? ( -
-
- banner -
-
-
-
- {currentService?.name} -
-
-
- - + getCurrentDeepLinkUrls() + .then((urls) => { + console.log("URLs:", urls); + if (urls && urls.length > 0) { + if (isValidCallbackUrl(urls[0])) { + handleUrl(urls[0]); + } + } + }) + .catch((err) => { + console.error("Failed to get initial URLs:", err); + setError("Failed to get initial URLs: " + err); + }); - {!currentService?.builtin && ( - - )} -
-
+ const unlisten = onOpenUrl((urls) => handleUrl(urls[0])); -
-
- - {" "} - {currentService?.provider?.name} - - | - - {" "} - {currentService?.version?.number} - - | - - {currentService?.updated} - -
-

- {currentService?.provider?.description} -

-
+ return () => { + unlisten.then((fn) => fn()); + document.removeEventListener("paste", handlePaste); + }; + }, [ssoRequestID]); - {currentService?.auth_provider?.sso?.url ? ( -
-

- Account Information -

- {currentService?.profile ? ( - - ) : ( -
- {/* Login Button (conditionally rendered when not loading) */} - {!loading && ( - - )} + const LoginClick = useCallback(() => { + if (loading) return; // Prevent multiple clicks if already loading - {/* Cancel Button and Copy URL button while loading */} - {loading && ( -
- - -
- )} + let requestID = uuidv4(); + setSSORequestID(requestID); - {/* Privacy Policy Link */} - -
- )} -
- ) : null} + // Generate the login URL with the current appUid + const url = `${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${requestID}`; - {currentService?.profile ? : null} -
- ) : ( - + console.log("Open SSO link, requestID:", ssoRequestID, url); + + // Open the URL in a browser + OpenURLWithBrowser(url); + + // Start loading state + setLoading(true); + }, [ssoRequestID, loading, currentService]); + + const refreshClick = (id: string) => { + setRefreshLoading(true); + invoke("refresh_coco_server_info", { id }) + .then((res: any) => { + console.log("refresh_coco_server_info", id, JSON.stringify(res)); + fetchServers(false).then((r) => { + console.log("fetchServers", r); + }); + // update currentService + setCurrentService(res); + }) + .catch((err: any) => { + setError(err); + console.error(err); + }) + .finally(() => { + setRefreshLoading(false); + }); + }; + + function onAddServer() { + setIsConnect(false); + } + function onLogout(id: string) { + console.log("onLogout", id); + setRefreshLoading(true); + invoke("logout_coco_server", { id }) + .then((res: any) => { + console.log("logout_coco_server", id, JSON.stringify(res)); + refreshClick(id); + }) + .catch((err: any) => { + setError(err); + console.error(err); + }) + .finally(() => { + setRefreshLoading(false); + }); + } + + const remove_coco_server = (id: string) => { + invoke("remove_coco_server", { id }) + .then((res: any) => { + console.log("remove_coco_server", id, JSON.stringify(res)); + fetchServers(true).then((r) => { + console.log("fetchServers", r); + }); + }) + .catch((err: any) => { + // TODO display the error message + setError(err); + console.error(err); + }); + }; + + return ( +
+ + +
+ {isConnect ? ( +
+
+ banner +
+
+
+
+ {currentService?.name} +
+
+
+ + + + {!currentService?.builtin && ( + )} -
-
- ); +
+ + +
+
+ + {" "} + {currentService?.provider?.name} + + | + + {" "} + {currentService?.version?.number} + + | + + {currentService?.updated} + +
+

+ {currentService?.provider?.description} +

+
+ + {currentService?.auth_provider?.sso?.url ? ( +
+

+ Account Information +

+ {currentService?.profile ? ( + + ) : ( +
+ {/* Login Button (conditionally rendered when not loading) */} + {!loading && ( + + )} + + {/* Cancel Button and Copy URL button while loading */} + {loading && ( +
+ + +
+ )} + + {/* Privacy Policy Link */} + +
+ )} +
+ ) : null} + + {currentService?.profile ? ( + + ) : null} + + ) : ( + + )} + + + ); } diff --git a/src/components/Common/InputBox.tsx b/src/components/Common/InputBox.tsx index e66d8d17..a8c1908a 100644 --- a/src/components/Common/InputBox.tsx +++ b/src/components/Common/InputBox.tsx @@ -107,7 +107,7 @@ export default function ChatInput({ useEffect(() => { const setupListener = async () => { const unlisten = await listen("tauri://focus", () => { - console.log("Window focused!"); + // console.log("Window focused!"); if (isChatMode) { textareaRef.current?.focus(); } else { diff --git a/src/components/Search/DocumentList.tsx b/src/components/Search/DocumentList.tsx index 2276ab2c..d98eae31 100644 --- a/src/components/Search/DocumentList.tsx +++ b/src/components/Search/DocumentList.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, useCallback } from "react"; import { useInfiniteScroll } from "ahooks"; import { isTauri, invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-shell"; @@ -29,10 +29,11 @@ export const DocumentList: React.FC = ({ const [total, setTotal] = useState(0); const containerRef = useRef(null); const itemRefs = useRef<(HTMLDivElement | null)[]>([]); + const [isKeyboardMode, setIsKeyboardMode] = useState(false); const { data, loading } = useInfiniteScroll( async (d) => { - const from = d?.list.length || 0; + const from = d?.list?.length || 0; let queryStrings: any = { query: input, @@ -55,40 +56,26 @@ export const DocumentList: React.FC = ({ const list = response?.hits || []; const total = response?.total_hits || 0; + // console.log("docs:", list, total); setTotal(total); - getDocDetail(list[0] || {}); - return { - list, - hasMore: from + list.length < total, + list: list, + hasMore: list.length === PAGE_SIZE, }; } catch (error) { console.error("Failed to fetch documents:", error); return { - list: [], + list: d?.list || [], hasMore: false, }; } }, { target: containerRef, - isNoMore: (d) => (d?.list.length || 0) >= total, + isNoMore: (d) => !d?.hasMore, reloadDeps: [input, JSON.stringify(sourceData)], - onBefore: () => { - setTimeout(() => { - const parentRef = containerRef.current; - if (parentRef && parentRef.childElementCount > 10) { - const itemHeight = - (parentRef.firstChild as HTMLElement)?.offsetHeight || 80; - parentRef.scrollTo({ - top: (parentRef.lastChild as HTMLElement)?.offsetTop - itemHeight, - behavior: "instant", - }); - } - }); - }, onFinally: (data) => onFinally(data, containerRef), } ); @@ -96,22 +83,31 @@ export const DocumentList: React.FC = ({ const onFinally = (data: any, ref: any) => { if (data?.page === 1) return; const parentRef = ref.current; - if (!parentRef) return; - const itemHeight = parentRef.firstChild?.offsetHeight || 80; - parentRef.scrollTo({ - top: - parentRef.lastChild?.offsetTop - (data?.list?.length + 1) * itemHeight, - behavior: "instant", + if (!parentRef || selectedItem === null) return; + + const targetElement = itemRefs.current[selectedItem]; + if (!targetElement) return; + + requestAnimationFrame(() => { + targetElement.scrollIntoView({ + behavior: "instant", + block: "nearest", + }); }); }; - function onMouseEnter(index: number, item: any) { - getDocDetail(item); - setSelectedItem(index); - } + const onMouseEnter = useCallback( + (index: number, item: any) => { + if (isKeyboardMode) return; + getDocDetail(item); + setSelectedItem(index); + }, + [isKeyboardMode, getDocDetail] + ); useEffect(() => { setSelectedItem(null); + setIsKeyboardMode(false); }, [isChatMode, input]); const handleOpenURL = async (url: string) => { @@ -126,28 +122,58 @@ export const DocumentList: React.FC = ({ } }; - const handleKeyDown = (e: KeyboardEvent) => { - if (!data?.list?.length) return; + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (!data?.list?.length) return; - if (e.key === "ArrowUp") { - e.preventDefault(); - setSelectedItem((prev) => (prev === null || prev === 0 ? 0 : prev - 1)); - } else if (e.key === "ArrowDown") { - e.preventDefault(); - setSelectedItem((prev) => - prev === null ? 0 : prev === data?.list?.length - 1 ? prev : prev + 1 - ); - } else if (e.key === "Meta") { - e.preventDefault(); - } + if (e.key === "ArrowUp" || e.key === "ArrowDown") { + e.preventDefault(); + setIsKeyboardMode(true); - if (e.key === "Enter" && selectedItem !== null) { - const item = data?.list?.[selectedItem]; - if (item?.url) { - handleOpenURL(item?.url); + if (e.key === "ArrowUp") { + setSelectedItem((prev) => { + const newIndex = prev === null || prev === 0 ? 0 : prev - 1; + getDocDetail(data.list[newIndex]?.document); + return newIndex; + }); + } else { + setSelectedItem((prev) => { + const newIndex = + prev === null + ? 0 + : prev === data.list.length - 1 + ? prev + : prev + 1; + getDocDetail(data.list[newIndex]?.document); + return newIndex; + }); + } + } else if (e.key === "Meta") { + e.preventDefault(); } - } - }; + + if (e.key === "Enter" && selectedItem !== null) { + const item = data?.list?.[selectedItem]; + if (item?.url) { + handleOpenURL(item?.url); + } + } + }, + [data, selectedItem, getDocDetail] + ); + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (e.movementX !== 0 || e.movementY !== 0) { + setIsKeyboardMode(false); + } + }; + + window.addEventListener("mousemove", handleMouseMove); + return () => { + window.removeEventListener("mousemove", handleMouseMove); + }; + }, []); useEffect(() => { window.addEventListener("keydown", handleKeyDown); @@ -155,13 +181,15 @@ export const DocumentList: React.FC = ({ return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, [selectedItem]); + }, [handleKeyDown]); useEffect(() => { if (selectedItem !== null && itemRefs.current[selectedItem]) { - itemRefs.current[selectedItem]?.scrollIntoView({ - behavior: "smooth", - block: "nearest", + requestAnimationFrame(() => { + itemRefs.current[selectedItem]?.scrollIntoView({ + behavior: "instant", + block: "nearest", + }); }); } }, [selectedItem]); @@ -196,13 +224,8 @@ export const DocumentList: React.FC = ({ }`} >
- - - {item?.title} - + {item?.title}
); diff --git a/src/components/Search/Footer.tsx b/src/components/Search/Footer.tsx index 8c80fbc3..caa1976c 100644 --- a/src/components/Search/Footer.tsx +++ b/src/components/Search/Footer.tsx @@ -91,16 +91,16 @@ export default function Footer({ }: FooterProps) {
Quick open - + - +
Open - +
diff --git a/src/components/Search/InputBox.tsx b/src/components/Search/InputBox.tsx index 2268d728..d2e2089e 100644 --- a/src/components/Search/InputBox.tsx +++ b/src/components/Search/InputBox.tsx @@ -152,7 +152,7 @@ export default function ChatInput({ if (!isTauri()) return; const setupListener = async () => { const unlisten = await listen("tauri://focus", () => { - console.log("Window focused!"); + // console.log("Window focused!"); if (isChatMode) { textareaRef.current?.focus(); } else { diff --git a/src/components/Settings/GeneralSettings.tsx b/src/components/Settings/GeneralSettings.tsx index a6e97900..3172346c 100644 --- a/src/components/Settings/GeneralSettings.tsx +++ b/src/components/Settings/GeneralSettings.tsx @@ -21,17 +21,14 @@ import { ShortcutItem } from "./ShortcutItem"; import { Shortcut } from "./shortcut"; import { useShortcutEditor } from "@/hooks/useShortcutEditor"; import { useAppStore } from "@/stores/appStore"; -import {AppTheme} from "@/utils/tauri.ts"; -import {useTheme} from "@/contexts/ThemeContext.tsx"; -// import { useAuthStore } from "@/stores/authStore"; -// import { useConnectStore } from "@/stores/connectStore"; - +import { AppTheme } from "@/utils/tauri"; +import { useTheme } from "@/contexts/ThemeContext"; export function ThemeOption({ - icon: Icon, - title, - theme, - }: { + icon: Icon, + title, + theme, +}: { icon: any; title: string; theme: AppTheme; @@ -41,21 +38,21 @@ export function ThemeOption({ const isSelected = currentTheme === theme; return ( - + ); } diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index c0da9ce0..9ea62592 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -7,7 +7,7 @@ import React, { } from "react"; import { isTauri, invoke } from "@tauri-apps/api/core"; import { getCurrentWindow } from "@tauri-apps/api/window"; -import { listen } from "@tauri-apps/api/event"; +import { listen, emit } from "@tauri-apps/api/event"; import { AppTheme, WindowTheme } from "../utils/tauri"; import { useThemeStore } from "../stores/themeStore"; @@ -45,6 +45,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { unlisten = await currentWindow.onThemeChanged(({ payload: w_theme }) => { console.log("window New theme:", w_theme); setWindowTheme(w_theme); + // Update tray icon + switchTrayIcon(w_theme); if (theme === "auto") applyTheme(w_theme); }); }; @@ -75,6 +77,8 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { const root = window.document.documentElement; root.classList.remove("light", "dark"); root.classList.add(displayTheme); + // + root.setAttribute("data-theme", displayTheme); } // Apply theme to UI and sync with Tauri @@ -91,16 +95,13 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { console.error("Failed to update window theme:", err); } - // Update tray icon - await switchTrayIcon(displayTheme); - // Notify other windows to update the theme - // try { - // console.log("theme-changed", displayTheme); - // await emit("theme-changed", { theme: displayTheme }); - // } catch (err) { - // console.error("Failed to emit theme-changed event:", err); - // } + try { + // console.log("theme-changed", displayTheme); + await emit("theme-changed", { theme: displayTheme }); + } catch (err) { + console.error("Failed to emit theme-changed event:", err); + } } }; @@ -126,19 +127,18 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { // Handle theme changes from user interaction const changeTheme = async (newTheme: AppTheme) => { + console.log("Theme changed to:", newTheme); setTheme(newTheme); const displayTheme = getDisplayTheme(newTheme); await applyTheme(displayTheme); }; useEffect(() => { - if (!isTauri()) return; - let unlisten: () => void; const setupListener = async () => { unlisten = await listen("theme-changed", (event: any) => { - console.log("Theme updated to:", event.payload); + // console.log("Theme updated to:", event.payload); changeClassTheme(event.payload.theme) }); }; diff --git a/src/main.css b/src/main.css index 8ef8e93a..dd586feb 100644 --- a/src/main.css +++ b/src/main.css @@ -2,67 +2,70 @@ @tailwind components; @tailwind utilities; -@layer { - :root { - --background: #ffffff; - --foreground: #09090b; - --border: #e3e3e7; - --docsearch-primary-color: rgb(149, 5, 153); - --docsearch-text-color: rgb(28, 30, 33); - --docsearch-spacing: 12px; - --docsearch-icon-stroke-width: 1.4; - --docsearch-highlight-color: var(--docsearch-primary-color); - --docsearch-muted-color: rgb(150, 159, 175); - --docsearch-modal-container-background: rgba(101, 108, 133, .8); - --docsearch-modal-width: 560px; - --docsearch-modal-height: 600px; - --docsearch-modal-background: rgb(245, 246, 247); - --docsearch-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, .5), 0 3px 8px 0 rgba(85, 90, 100, 1); - --docsearch-searchbox-height: 56px; - --docsearch-searchbox-background: rgb(235, 237, 240); - --docsearch-searchbox-focus-background: #fff; - --docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color); - --docsearch-hit-height: 56px; - --docsearch-hit-color: rgb(68, 73, 80); - --docsearch-hit-active-color: #fff; - --docsearch-hit-background: #fff; - --docsearch-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225); - --docsearch-key-gradient: linear-gradient(-225deg, rgb(213, 219, 228) 0%, rgb(248, 248, 248) 100%); - --docsearch-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230), inset 0 0 1px 1px #fff, 0 1px 2px 1px rgba(30, 35, 90, .4); - --docsearch-footer-height: 44px; - --docsearch-footer-background: #fff; - --docsearch-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232), 0 -3px 6px 0 rgba(69, 98, 155, .12); - --docsearch-icon-color: rgb(21, 21, 21); - } - - .dark { - --background: #09090b; - --foreground: #f9f9f9; - --border: #27272a; - --docsearch-text-color: rgb(245, 246, 247); - --docsearch-modal-container-background: rgba(9, 10, 17, .8); - --docsearch-modal-background: rgb(21, 23, 42); - --docsearch-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9); - --docsearch-searchbox-background: rgb(9, 10, 17); - --docsearch-searchbox-focus-background: #000; - --docsearch-hit-color: rgb(190, 195, 201); - --docsearch-hit-shadow: none; - --docsearch-hit-background: rgb(9, 10, 17); - --docsearch-key-gradient: linear-gradient(-26.5deg, rgb(86, 88, 114) 0%, rgb(49, 53, 91) 100%); - --docsearch-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85), inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, .3); - --docsearch-footer-background: rgb(30, 33, 54); - --docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2); - --docsearch-muted-color: rgb(127, 132, 151); - --docsearch-icon-color: rgb(255, 255, 255); - } +/* Base variables */ +:root { + --spacing-base: 12px; + --modal-width: 560px; + --modal-height: 600px; + --searchbox-height: 56px; + --hit-height: 56px; + --footer-height: 44px; + --icon-stroke-width: 1.4; + --background: #ffffff; + --foreground: #09090b; + --border: #e3e3e7; } +/* Light theme */ +[data-theme="light"] { + --coco-primary-color: rgb(149, 5, 153); + --coco-text-color: rgb(28, 30, 33); + --coco-muted-color: rgb(150, 159, 175); + --coco-modal-container-background: rgba(101, 108, 133, .8); + --coco-modal-background: rgb(245, 246, 247); + --coco-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, .5), 0 3px 8px 0 rgba(85, 90, 100, 1); + --coco-searchbox-background: rgb(235, 237, 240); + --coco-searchbox-focus-background: #fff; + --coco-hit-color: rgb(68, 73, 80); + --coco-hit-active-color: #fff; + --coco-hit-background: #fff; + --coco-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225); + --coco-key-gradient: linear-gradient(-225deg, rgb(213, 219, 228) 0%, rgb(248, 248, 248) 100%); + --coco-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230), inset 0 0 1px 1px #fff, 0 1px 2px 1px rgba(30, 35, 90, .4); + --coco-footer-background: #fff; + --coco-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232), 0 -3px 6px 0 rgba(69, 98, 155, .12); + --coco-icon-color: rgb(21, 21, 21); +} + +/* Dark theme */ +[data-theme="dark"] { + --background: #09090b; + --foreground: #f9f9f9; + --border: #27272a; + --coco-text-color: rgb(245, 246, 247); + --coco-modal-container-background: rgba(9, 10, 17, .8); + --coco-modal-background: rgb(21, 23, 42); + --coco-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9); + --coco-searchbox-background: rgb(9, 10, 17); + --coco-searchbox-focus-background: #000; + --coco-hit-color: rgb(190, 195, 201); + --coco-hit-shadow: none; + --coco-hit-background: rgb(9, 10, 17); + --coco-key-gradient: linear-gradient(-26.5deg, rgb(86, 88, 114) 0%, rgb(49, 53, 91) 100%); + --coco-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85), inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, .3); + --coco-footer-background: rgb(30, 33, 54); + --coco-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2); + --coco-muted-color: rgb(127, 132, 151); + --coco-icon-color: rgb(255, 255, 255); +} + +/* Base styles */ @layer base { * { @apply box-border border-[--border]; } - html{ + html { @apply h-full; } @@ -81,6 +84,7 @@ } } +/* Component styles */ @layer components { .settings-input { @apply block w-full rounded-md border-gray-300 dark:border-gray-600 @@ -99,7 +103,9 @@ } } +/* Utility styles */ @layer utilities { + /* Scrollbar styles */ .custom-scrollbar { scrollbar-width: thin; scrollbar-color: #cbd5e1 transparent; @@ -126,10 +132,12 @@ background-color: #475569; } + /* Background styles */ .bg-100 { background-size: 100% 100%; } + /* Error page styles */ #error-page { display: flex; justify-content: center; @@ -187,19 +195,21 @@ background-color: #f79c42; } - .docsearch-modal-footer-commands-key { + /* coco styles */ + .coco-modal-footer-commands-key { display: flex; align-items: center; justify-content: center; border-radius: 4px; - border: 0px; + border: 0; padding: 2px; - background: var(--docsearch-key-gradient); - box-shadow: var(--docsearch-key-shadow); - color: var(--docsearch-muted-color); + background: var(--coco-key-gradient); + box-shadow: var(--coco-key-shadow); + color: var(--coco-muted-color); } - - .user-select{ + + /* User selection styles */ + .user-select { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -207,4 +217,4 @@ -ms-user-select: none; user-select: none; } -} +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index d756d756..4cbdc5f8 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -42,5 +42,5 @@ export default { }, plugins: [], mode: "jit", - darkMode: "class", + darkMode: ["class", '[data-theme="dark"]'], };