mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 03:27:43 +01:00
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 <stevelauc@outlook.com> Co-authored-by: medcl <m@medcl.net>
This commit is contained in:
@@ -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<string | null>(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<any>({});
|
||||
// const [userInfo, setUserInfo] = useState<any>({});
|
||||
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 (
|
||||
<div className="flex bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar ref={SidebarRef} onAddServer={onAddServer} serverList={serverList} />
|
||||
// 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");
|
||||
};
|
||||
|
||||
<main className="flex-1 p-4 py-8">
|
||||
// Adding event listener for paste events
|
||||
document.addEventListener("paste", handlePaste);
|
||||
|
||||
{isConnect ? (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
|
||||
<img
|
||||
width="100%"
|
||||
src={currentService?.provider?.banner || bannerImg}
|
||||
alt="banner"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center text-gray-900 dark:text-white font-medium">
|
||||
{currentService?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => OpenURLWithBrowser(currentService?.provider?.website)}
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => refreshClick(currentService?.id)}
|
||||
>
|
||||
<RefreshCcw
|
||||
className={`w-3.5 h-3.5 ${refreshLoading ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
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 && (
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => remove_coco_server(currentService?.id)}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
const unlisten = onOpenUrl((urls) => handleUrl(urls[0]));
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
|
||||
<span className="flex items-center gap-1">
|
||||
<PackageOpen className="w-4 h-4" />{" "}
|
||||
{currentService?.provider?.name}
|
||||
</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<GitFork className="w-4 h-4" />{" "}
|
||||
{currentService?.version?.number}
|
||||
</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<CalendarSync className="w-4 h-4" /> {currentService?.updated}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
{currentService?.provider?.description}
|
||||
</p>
|
||||
</div>
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
document.removeEventListener("paste", handlePaste);
|
||||
};
|
||||
}, [ssoRequestID]);
|
||||
|
||||
{currentService?.auth_provider?.sso?.url ? (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
Account Information
|
||||
</h2>
|
||||
{currentService?.profile ? (
|
||||
<UserProfile server={currentService?.id} userInfo={currentService?.profile} onLogout={onLogout} />
|
||||
) : (
|
||||
<div>
|
||||
{/* Login Button (conditionally rendered when not loading) */}
|
||||
{!loading && (
|
||||
<button
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||
onClick={LoginClick}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
const LoginClick = useCallback(() => {
|
||||
if (loading) return; // Prevent multiple clicks if already loading
|
||||
|
||||
{/* Cancel Button and Copy URL button while loading */}
|
||||
{loading && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
|
||||
onClick={() => setLoading(false)} // Reset loading state
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
||||
);
|
||||
}}
|
||||
className="text-xl text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
<Copy className="inline mr-2" /> {/* Lucide Copy Icon */}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
let requestID = uuidv4();
|
||||
setSSORequestID(requestID);
|
||||
|
||||
{/* Privacy Policy Link */}
|
||||
<button
|
||||
className="text-xs text-[#0096FB] dark:text-blue-400 block"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(currentService?.provider?.privacy_policy)
|
||||
}
|
||||
>
|
||||
EULA | Privacy Policy
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : 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 ? <DataSourcesList server={currentService?.id} /> : null}
|
||||
</div>
|
||||
) : (
|
||||
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} />
|
||||
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 (
|
||||
<div className="flex bg-gray-50 dark:bg-gray-900">
|
||||
<Sidebar
|
||||
ref={SidebarRef}
|
||||
onAddServer={onAddServer}
|
||||
serverList={serverList}
|
||||
/>
|
||||
|
||||
<main className="flex-1 p-4 py-8">
|
||||
{isConnect ? (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="w-full rounded-[4px] bg-[rgba(229,229,229,1)] dark:bg-gray-800 mb-6">
|
||||
<img
|
||||
width="100%"
|
||||
src={currentService?.provider?.banner || bannerImg}
|
||||
alt="banner"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center text-gray-900 dark:text-white font-medium">
|
||||
{currentService?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(currentService?.provider?.website)
|
||||
}
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => refreshClick(currentService?.id)}
|
||||
>
|
||||
<RefreshCcw
|
||||
className={`w-3.5 h-3.5 ${
|
||||
refreshLoading ? "animate-spin" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{!currentService?.builtin && (
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-[6px] bg-white dark:bg-gray-800 border border-[rgba(228,229,239,1)] dark:border-gray-700"
|
||||
onClick={() => remove_coco_server(currentService?.id)}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 text-[#ff4747]" />
|
||||
</button>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2 flex">
|
||||
<span className="flex items-center gap-1">
|
||||
<PackageOpen className="w-4 h-4" />{" "}
|
||||
{currentService?.provider?.name}
|
||||
</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<GitFork className="w-4 h-4" />{" "}
|
||||
{currentService?.version?.number}
|
||||
</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<CalendarSync className="w-4 h-4" /> {currentService?.updated}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
{currentService?.provider?.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{currentService?.auth_provider?.sso?.url ? (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
Account Information
|
||||
</h2>
|
||||
{currentService?.profile ? (
|
||||
<UserProfile
|
||||
server={currentService?.id}
|
||||
userInfo={currentService?.profile}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{/* Login Button (conditionally rendered when not loading) */}
|
||||
{!loading && (
|
||||
<button
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mb-3"
|
||||
onClick={LoginClick}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Cancel Button and Copy URL button while loading */}
|
||||
{loading && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
className="px-6 py-2 text-white bg-red-500 rounded-md hover:bg-red-600 transition-colors mb-3"
|
||||
onClick={() => setLoading(false)} // Reset loading state
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${currentService?.auth_provider?.sso?.url}/?provider=${currentService?.id}&product=coco&request_id=${ssoRequestID}`
|
||||
);
|
||||
}}
|
||||
className="text-xl text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
<Copy className="inline mr-2" />{" "}
|
||||
{/* Lucide Copy Icon */}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Privacy Policy Link */}
|
||||
<button
|
||||
className="text-xs text-[#0096FB] dark:text-blue-400 block"
|
||||
onClick={() =>
|
||||
OpenURLWithBrowser(
|
||||
currentService?.provider?.privacy_policy
|
||||
)
|
||||
}
|
||||
>
|
||||
EULA | Privacy Policy
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{currentService?.profile ? (
|
||||
<DataSourcesList server={currentService?.id} />
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<Connect setIsConnect={setIsConnect} onAddServer={add_coco_server} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<DocumentListProps> = ({
|
||||
const [total, setTotal] = useState(0);
|
||||
const containerRef = useRef<HTMLDivElement>(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<DocumentListProps> = ({
|
||||
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<DocumentListProps> = ({
|
||||
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<DocumentListProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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<DocumentListProps> = ({
|
||||
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<DocumentListProps> = ({
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-2 items-center flex-1 min-w-0">
|
||||
|
||||
<ItemIcon item={item} />
|
||||
<span
|
||||
className={`text-sm truncate`}
|
||||
>
|
||||
{item?.title}
|
||||
</span>
|
||||
<span className={`text-sm truncate`}>{item?.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -91,16 +91,16 @@ export default function Footer({ }: FooterProps) {
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="gap-1 flex items-center text-[#666] dark:text-[#666] text-sm">
|
||||
<span className="mr-1.5 ">Quick open</span>
|
||||
<kbd className="docsearch-modal-footer-commands-key pr-1">
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
<Command className="w-3 h-3" />
|
||||
</kbd>
|
||||
<kbd className="docsearch-modal-footer-commands-key pr-1">
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
<ArrowDown01 className="w-3 h-3" />
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex items-center text-[#666] dark:text-[#666] text-sm">
|
||||
<span className="mr-1.5 ">Open</span>
|
||||
<kbd className="docsearch-modal-footer-commands-key pr-1">
|
||||
<kbd className="coco-modal-footer-commands-key pr-1">
|
||||
<CornerDownLeft className="w-3 h-3" />
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
onClick={() => changeTheme(theme)}
|
||||
className={`p-4 rounded-lg border-2 ${
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
|
||||
: "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
|
||||
} flex flex-col items-center justify-center space-y-2 transition-all`}
|
||||
<button
|
||||
onClick={() => changeTheme(theme)}
|
||||
className={`p-4 rounded-lg border-2 ${
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
|
||||
: "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
|
||||
} flex flex-col items-center justify-center space-y-2 transition-all`}
|
||||
>
|
||||
<Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} />
|
||||
<span
|
||||
className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}
|
||||
>
|
||||
<Icon className={`w-6 h-6 ${isSelected ? "text-blue-500" : ""}`} />
|
||||
<span
|
||||
className={`text-sm font-medium ${isSelected ? "text-blue-500" : ""}`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</button>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
};
|
||||
|
||||
134
src/main.css
134
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,5 +42,5 @@ export default {
|
||||
},
|
||||
plugins: [],
|
||||
mode: "jit",
|
||||
darkMode: "class",
|
||||
darkMode: ["class", '[data-theme="dark"]'],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user