diff --git a/.vscode/settings.json b/.vscode/settings.json index 87c8e33..ce5aa52 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "dyld", "fullscreen", "headlessui", + "Icdbb", "icns", "INFINI", "inputbox", diff --git a/package.json b/package.json index a9fbe41..3e5fc64 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@headlessui/react": "^2.1.10", + "@react-oauth/google": "^0.12.1", "@tauri-apps/api": ">=2.0.0", "@tauri-apps/plugin-autostart": "~2", "@tauri-apps/plugin-global-shortcut": "~2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b553b67..3cb20f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@headlessui/react': specifier: ^2.1.10 version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-oauth/google': + specifier: ^0.12.1 + version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tauri-apps/api': specifier: '>=2.0.0' version: 2.0.2 @@ -501,6 +504,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + '@react-oauth/google@0.12.1': + resolution: {integrity: sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@react-stately/utils@3.10.4': resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==} peerDependencies: @@ -2602,6 +2611,11 @@ snapshots: clsx: 2.1.1 react: 18.3.1 + '@react-oauth/google@0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@react-stately/utils@3.10.4(react@18.3.1)': dependencies: '@swc/helpers': 0.5.13 diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 8f9d945..f7074fa 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -28,7 +28,7 @@ "core:window:allow-get-all-windows", "core:window:allow-set-focus", "core:app:allow-set-app-theme", - "shell:allow-open", + "shell:default", "http:default", "http:allow-fetch", "http:allow-fetch-cancel", diff --git a/src/assets/images/apple.png b/src/assets/images/apple.png new file mode 100644 index 0000000..05d54ec Binary files /dev/null and b/src/assets/images/apple.png differ diff --git a/src/assets/images/bg-login.png b/src/assets/images/bg-login.png new file mode 100644 index 0000000..3684a6d Binary files /dev/null and b/src/assets/images/bg-login.png differ diff --git a/src/assets/images/coco-logo.png b/src/assets/images/coco-logo.png new file mode 100644 index 0000000..f017a50 Binary files /dev/null and b/src/assets/images/coco-logo.png differ diff --git a/src/assets/images/github.png b/src/assets/images/github.png new file mode 100644 index 0000000..6757e43 Binary files /dev/null and b/src/assets/images/github.png differ diff --git a/src/assets/images/google.png b/src/assets/images/google.png new file mode 100644 index 0000000..ea325ee Binary files /dev/null and b/src/assets/images/google.png differ diff --git a/src/components/AppAI/Footer.tsx b/src/components/AppAI/Footer.tsx index 5fd92c4..85624b7 100644 --- a/src/components/AppAI/Footer.tsx +++ b/src/components/AppAI/Footer.tsx @@ -5,6 +5,8 @@ import { CornerDownLeft, } from "lucide-react"; +import logoImg from "@/assets/32x32.png"; + interface FooterProps { isChat: boolean; name?: string; @@ -17,6 +19,13 @@ export default function Footer({ name }: FooterProps) { className="px-4 z-999 mx-[1px] h-10 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden" >
+
+ + + Version 1.0.0 + +
+ {name ? (
{name} diff --git a/src/components/Auth/CocoCloud.tsx b/src/components/Auth/CocoCloud.tsx index f7b303f..b1fe393 100644 --- a/src/components/Auth/CocoCloud.tsx +++ b/src/components/Auth/CocoCloud.tsx @@ -1,52 +1,70 @@ +import { useState } from "react"; import { Cloud } from "lucide-react"; + import { UserProfile } from "./UserProfile"; import { DataSourcesList } from "./DataSourcesList"; +import { Sidebar } from "./Sidebar"; +import { ConnectService } from "./ConnectService"; export default function CocoCloud() { + const [isLogin, setIsLogin] = useState(true); + const [isConnect, setIsConnect] = useState(true); + return ( -
-
-
-
-
- - Coco Cloud +
+ + +
+ {isConnect ?
+
+
+
+ + Coco Cloud +
+ + Available +
- - Available - +
- -
-
-
- Service provision: INFINI Labs - | - Version Number: v2.3.0 - | - Update time: 2023-05-12 + +
+
+ Service provision: INFINI Labs + | + Version Number: v2.3.0 + | + Update time: 2023-05-12 +
+

+ Coco Cloud provides users with a cloud storage and data + integration platform that supports account registration and data + source management. Users can integrate multiple data sources (such + as Google Drive, yuque, GitHub, etc.), easily access and search + for files, documents and codes across platforms, and achieve + efficient data collaboration and management. +

+
+ +
+

+ Account Information +

+ {isLogin ? ( + + ) : ( + + )}
-

- Coco Cloud provides users with a cloud storage and data integration - platform that supports account registration and data source - management. Users can integrate multiple data sources (such as - Google Drive, yuque, GitHub, etc.), easily access and search for - files, documents and codes across platforms, and achieve efficient - data collaboration and management. -

-
-
-

Account Information

- -
- - -
+ {isLogin ? : null } +
: } +
); } diff --git a/src/components/Auth/ConnectService.tsx b/src/components/Auth/ConnectService.tsx new file mode 100644 index 0000000..423f227 --- /dev/null +++ b/src/components/Auth/ConnectService.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { ArrowLeft } from 'lucide-react'; + +export function ConnectService() { + const [sourceName, setSourceName] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log('Connecting Google Drive with name:', sourceName); + }; + + return ( +
+
+ +
+ +
+

+ Coco needs to obtain authorization from your Google Drive account +

+
+ +
+
+ + setSourceName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Your Google Drive" + /> +
+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/Auth/DataSourceItem.tsx b/src/components/Auth/DataSourceItem.tsx index f0d1f2d..e8c72b7 100644 --- a/src/components/Auth/DataSourceItem.tsx +++ b/src/components/Auth/DataSourceItem.tsx @@ -1,4 +1,4 @@ -import { Link2, Trash2 } from 'lucide-react'; +import { Link2, Trash2 } from "lucide-react"; interface Account { email: string; @@ -18,21 +18,19 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
- {name} + {name} {name}
+
+ {isConnected ? "Manage" : "Connect Accounts"} +
{accounts.map((account, index) => ( -
@@ -44,14 +42,14 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
- {index === 0 ? '我的网盘' : `网盘${index + 1}`} + {index === 0 ? "My network disk" : `Network disk ${index + 1}`}
{account.email}
- 最近同步: {account.lastSync} + Recently Synced: {account.lastSync}
); -} \ No newline at end of file +} diff --git a/src/components/Auth/DataSourcesList.tsx b/src/components/Auth/DataSourcesList.tsx index 8c2ce35..213b5f2 100644 --- a/src/components/Auth/DataSourcesList.tsx +++ b/src/components/Auth/DataSourcesList.tsx @@ -7,8 +7,8 @@ export function DataSourcesList() { name: 'Google Drive', type: 'google', accounts: [ - { email: 'an121245@gmail.com', lastSync: '2025年1月2日 09:50 AM' }, - { email: '9paiii@gmail.com', lastSync: '2025年1月2日 09:50 AM' } + { email: 'an121245@gmail.com', lastSync: '2025-01-02 09:50 AM' }, + { email: '9paiii@gmail.com', lastSync: '2025-01-02 09:50 AM' } ] }, { @@ -27,7 +27,7 @@ export function DataSourcesList() { return (
-

数据源

+

Data Source

{dataSources.map(source => ( diff --git a/src/components/Auth/Sidebar.tsx b/src/components/Auth/Sidebar.tsx new file mode 100644 index 0000000..fe94b8a --- /dev/null +++ b/src/components/Auth/Sidebar.tsx @@ -0,0 +1,28 @@ +import { Cloud, Plus } from "lucide-react"; + +export function Sidebar() { + return ( +
+
+
+ + Coco Cloud +
+ +
+ +
+
+ Third-party services +
+ + +
+
+
+ ); +} diff --git a/src/components/Auth/UserProfile.tsx b/src/components/Auth/UserProfile.tsx index 27c8821..63f4f8d 100644 --- a/src/components/Auth/UserProfile.tsx +++ b/src/components/Auth/UserProfile.tsx @@ -8,7 +8,6 @@ interface UserProfileProps { export function UserProfile({ name, email }: UserProfileProps) { return (
-

账户信息

diff --git a/src/components/Auth/callback.template.ts b/src/components/Auth/callback.template.ts new file mode 100644 index 0000000..9eed79b --- /dev/null +++ b/src/components/Auth/callback.template.ts @@ -0,0 +1,59 @@ +export default ` + + + + + + + + + Coco Auth + + + +
+

Coco

+

You are now signed in. Please re-open the Coco desktop app to continue.

+
+

+ + +`; diff --git a/src/components/Auth/login2.tsx b/src/components/Auth/login2.tsx new file mode 100644 index 0000000..05d70ec --- /dev/null +++ b/src/components/Auth/login2.tsx @@ -0,0 +1,55 @@ +import { useEffect } from 'react'; +import { Github, Mail, Apple } from 'lucide-react'; +import { useSearchParams } from "react-router-dom"; + +import { LoginForm } from '@/components/Auth/LoginForm'; +import { SocialButton } from '@/components/Auth/SocialButton'; +import { Divider } from '@/components/Auth/Divider'; +import { authWitheGithub } from '@/utils/index'; + +export default function LoginPage() { + const [searchParams] = useSearchParams(); + const uid = searchParams.get("uid"); + const code = searchParams.get("code"); + + useEffect(()=>{ + + }, [code]) + + function GithubClick() { + uid && authWitheGithub(uid) + } + + return ( +
+
+
+

Welcome Back

+

Sign in to continue to Coco

+
+ +
+ } + provider="GitHub" + onClick={() => GithubClick()} + /> + } + provider="Google" + onClick={() => console.log('Google login')} + /> + } + provider="Apple" + onClick={() => console.log('Apple login')} + /> +
+ + + + console.log(email, password)} /> +
+
+ ); +} \ No newline at end of file diff --git a/src/components/Settings/Account.tsx b/src/components/Settings/Account.tsx index 11d1c3a..dc80691 100644 --- a/src/components/Settings/Account.tsx +++ b/src/components/Settings/Account.tsx @@ -1,13 +1,133 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import { v4 as uuidv4 } from "uuid"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import * as shell from "@tauri-apps/plugin-shell"; import { useAppStore } from "@/stores/appStore"; +import { useAuthStore } from "@/stores/authStore"; import { OpenBrowserURL } from "@/utils/index"; import logoImg from "@/assets/32x32.png"; +import callbackTemplate from "@/components/Auth/callback.template"; +import { clientEnv } from "@/utils/env"; + export default function Account() { const app_uid = useAppStore((state) => state.app_uid); const setAppUid = useAppStore((state) => state.setAppUid); + const { auth, setAuth } = useAuthStore(); + + const navigate = useNavigate(); + + const [loading, setLoading] = useState(false); + + useEffect(() => { + let unsubscribe: (() => void) | undefined; + + const setupAuthListener = async () => { + try { + if (!auth) { + // Replace the current route with signin + // navigate("/signin", { replace: true }); + } + } catch (error) { + console.error("Failed to set up auth listener:", error); + } + }; + + setupAuthListener(); + + // Clean up logic on unmount + return () => { + const cleanup = async () => { + try { + await invoke("plugin:oauth|stop"); + } catch (e) { + // Ignore errors if no server is running + } + if (unsubscribe) { + unsubscribe(); + } + }; + + cleanup(); + }; + }, [auth]); + + async function signIn() { + let res: (url: URL) => void; + + try { + const stopListening = await listen( + "oauth://url", + (data: { payload: string }) => { + if (!data.payload.includes("token")) { + return; + } + + const urlObject = new URL(data.payload); + res(urlObject); + } + ); + + // Stop any existing OAuth server first + try { + await invoke("plugin:oauth|stop"); + } catch (e) { + // Ignore errors if no server is running + } + + const port: string = await invoke("plugin:oauth|start", { + config: { + response: callbackTemplate, + headers: { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store, no-cache, must-revalidate", + Pragma: "no-cache", + }, + // Add a cleanup function to stop the server after handling the request + cleanup: true, + }, + }); + + await shell.open( + `${clientEnv.COCO_SERVER_URL}/api/desktop/session/request?port=${port}` + ); + + const url = await new Promise((r) => { + res = r; + }); + stopListening(); + + const token = url.searchParams.get("token"); + const user_id = url.searchParams.get("user_id"); + const expires = Number(url.searchParams.get("expires")); + if (!token || !expires || !user_id) { + throw new Error("Invalid token or expires"); + } + + await setAuth({ + token, + user_id, + expires, + plan: { upgraded: false, last_checked: 0 }, + }); + + getCurrentWindow() + .setFocus() + .catch(() => {}); + + return navigate("/"); + } catch (error) { + console.error("Sign in failed:", error); + await setAuth(undefined); + throw error; + } + } + async function initializeUser() { let uid = app_uid; if (!uid) { @@ -22,6 +142,15 @@ export default function Account() { // const { token } = await response.json(); // localStorage.setItem("auth_token", token); OpenBrowserURL(`http://localhost:1420/login?uid=${uid}`); + + setLoading(true); + try { + await signIn(); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } } function LoginClick(event: React.MouseEvent) { @@ -59,7 +188,7 @@ export default function Account() { className="text-sm/6 font-semibold text-gray-900 dark:text-gray-100" onClick={LoginClick} > - Log In + {loading ? "Signing In..." : "Sign In"}
diff --git a/src/components/Settings/index2.tsx b/src/components/Settings/index2.tsx index 50dc8e1..54d1b85 100644 --- a/src/components/Settings/index2.tsx +++ b/src/components/Settings/index2.tsx @@ -7,7 +7,7 @@ import SettingsPanel from "./SettingsPanel"; import GeneralSettings from "./GeneralSettings"; import AboutView from "./AboutView"; import Account from "./Account"; -// import CocoCloud from "@/components/Auth/CocoCloud" +import CocoCloud from "@/components/Auth/CocoCloud" import Footer from "../Footer"; import { useTheme } from "../../contexts/ThemeContext"; import { AppTheme } from "../../utils/tauri"; @@ -25,7 +25,7 @@ function SettingsPage() { const tabs = [ { name: "General", icon: Settings }, { name: "Extensions", icon: Puzzle }, - { name: "Account", icon: User }, + { name: "Connect", icon: User }, { name: "Advanced", icon: Settings2 }, { name: "About", icon: Info }, ]; @@ -79,7 +79,7 @@ function SettingsPage() { - {/* */} + diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index f8a9983..5d09503 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -1,55 +1,124 @@ -import { Github, Mail, Apple } from 'lucide-react'; +import { useEffect } from "react"; +import { GoogleOAuthProvider } from "@react-oauth/google"; import { useSearchParams } from "react-router-dom"; -import { LoginForm } from '@/components/Auth/LoginForm'; -import { SocialButton } from '@/components/Auth/SocialButton'; -import { Divider } from '@/components/Auth/Divider'; -import { authWitheGithub } from '@/utils/index'; -import { useEffect } from 'react'; +import { authWitheGithub } from "@/utils/index"; +import loginImg from "@/assets/images/bg-login.png"; +import logoImg from "@/assets/images/coco-logo.png"; +import AppleImg from "@/assets/images/apple.png"; +import GithubImg from "@/assets/images/github.png"; +import GoogleImg from "@/assets/images/google.png"; export default function LoginPage() { + const handleGoogleSignIn = (response: any) => { + console.log("Google Login Success:", response); + // response.credential + }; + const [searchParams] = useSearchParams(); const uid = searchParams.get("uid"); const code = searchParams.get("code"); - useEffect(()=>{ - - }, [code]) + useEffect(() => {}, [code]); - function GithubClick() { - uid && authWitheGithub(uid) + function handleGithubSignIn() { + uid && authWitheGithub(uid); } - return ( -
-
-
-

Welcome Back

-

Sign in to continue to Coco

-
+ const clientId = "YOUR_APPLE_CLIENT_ID"; + const redirectUri = "http://localhost:3000"; + const scope = "name email"; -
- } - provider="GitHub" - onClick={() => GithubClick()} - /> - } - provider="Google" - onClick={() => console.log('Google login')} - /> - } - provider="Apple" - onClick={() => console.log('Apple login')} + const handleAppleSignIn = () => { + const authUrl = `https://appleid.apple.com/auth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code`; + window.location.href = authUrl; + }; + + return ( +
+
+ {/* Background Image */} +
+ Background +
- + {/* Content */} +
+ {/* Logo */} +
+ Coco +
- console.log(email, password)} /> + {/* Main Text */} +
+

+ INSERT +
+ THE +
+ STRAW +

+

+ LET'S BEGIN! +

+

+ With Coco AI, accessing your data is as easy as sipping fresh + coconut juice. +

+
+ + {/* Social Login Buttons */} +
+ Sign in With +
+
+ + + + + + + +
+
+ + {/* Footer */} +
+ © {new Date().getFullYear()} Coco Labs. All rights reserved. +
); -} \ No newline at end of file +} diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts new file mode 100644 index 0000000..314a816 --- /dev/null +++ b/src/stores/authStore.ts @@ -0,0 +1,34 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +export type Plan = { + upgraded: boolean; + last_checked: number; +}; + +export type AuthStore = { + token: string; + user_id: string | null; + expires: number; + plan: Plan | null; +}; + +export type IAuthStore = { + auth: AuthStore | undefined; + setAuth: (auth: AuthStore | undefined) => void; + resetAuth: () => void; +}; + +export const useAuthStore = create()( + persist( + (set) => ({ + auth: undefined, + setAuth: (auth) => set({ auth }), + resetAuth: () => set({ auth: undefined }), + }), + { + name: "auth-store", + partialize: (state) => ({ auth: state.auth }), + } + ) +);