mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
fix: opening duplicate windows (#88)
* feat: add login page * fix: opening duplicate windows * chore: vscode settings
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -33,8 +33,10 @@
|
||||
"unlisten",
|
||||
"unlistener",
|
||||
"unminimize",
|
||||
"uuidv",
|
||||
"VITE",
|
||||
"webviews",
|
||||
"yuque",
|
||||
"zustand"
|
||||
],
|
||||
"[rust]": {
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"use-debounce": "^10.0.4",
|
||||
"uuid": "^11.0.3",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -92,6 +92,9 @@ importers:
|
||||
use-debounce:
|
||||
specifier: ^10.0.4
|
||||
version: 10.0.4(react@18.3.1)
|
||||
uuid:
|
||||
specifier: ^11.0.3
|
||||
version: 11.0.3
|
||||
zustand:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(@types/react@18.3.11)(react@18.3.1)
|
||||
@@ -2126,6 +2129,10 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
uuid@11.0.3:
|
||||
resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
@@ -4559,6 +4566,8 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@11.0.3: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
vfile-location@5.0.3:
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-size",
|
||||
"core:window:allow-get-all-windows",
|
||||
"core:window:allow-set-focus",
|
||||
"core:app:allow-set-app-theme",
|
||||
"shell:allow-open",
|
||||
"http:default",
|
||||
|
||||
52
src/components/Auth/CocoCloud.tsx
Normal file
52
src/components/Auth/CocoCloud.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Cloud } from "lucide-react";
|
||||
import { UserProfile } from "./UserProfile";
|
||||
import { DataSourcesList } from "./DataSourcesList";
|
||||
|
||||
export default function CocoCloud() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-2 px-4 py-2 bg-white rounded-md border border-gray-200">
|
||||
<Cloud className="w-5 h-5 text-blue-500" />
|
||||
<span className="font-medium">Coco Cloud</span>
|
||||
</div>
|
||||
<span className="px-3 py-1 text-sm text-blue-600 bg-blue-50 rounded-md">
|
||||
Available
|
||||
</span>
|
||||
</div>
|
||||
<button className="p-2 text-gray-500 hover:text-gray-700">
|
||||
<Cloud className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className="text-sm text-gray-500 mb-4">
|
||||
<span>Service provision: INFINI Labs</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span>Version Number: v2.3.0</span>
|
||||
<span className="mx-4">|</span>
|
||||
<span>Update time: 2023-05-12</span>
|
||||
</div>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-medium text-gray-900 mb-4">Account Information</h2>
|
||||
<button className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<UserProfile name="Rain" email="an121245@gmail.com" />
|
||||
<DataSourcesList />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
src/components/Auth/DataSourceItem.tsx
Normal file
64
src/components/Auth/DataSourceItem.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Link2, Trash2 } from 'lucide-react';
|
||||
|
||||
interface Account {
|
||||
email: string;
|
||||
lastSync: string;
|
||||
}
|
||||
|
||||
interface DataSourceItemProps {
|
||||
name: string;
|
||||
type: string;
|
||||
accounts: Account[];
|
||||
}
|
||||
|
||||
export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
|
||||
const isConnected = accounts.length > 0;
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src={`/icons/${type}.svg`}
|
||||
alt={name}
|
||||
className="w-6 h-6"
|
||||
/>
|
||||
<span className="font-medium">{name}</span>
|
||||
</div>
|
||||
<button className="text-blue-500 hover:text-blue-600 flex items-center space-x-1">
|
||||
<Link2 className="w-4 h-4" />
|
||||
<span className="text-sm">{isConnected ? '管理' : '连接账户'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{accounts.map((account, index) => (
|
||||
<div
|
||||
key={account.email}
|
||||
className="flex items-center justify-between py-2 border-t border-gray-100"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-sm text-gray-500">
|
||||
{account.email[0].toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium">
|
||||
{index === 0 ? '我的网盘' : `网盘${index + 1}`}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{account.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-xs text-gray-500">
|
||||
最近同步: {account.lastSync}
|
||||
</span>
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/Auth/DataSourcesList.tsx
Normal file
38
src/components/Auth/DataSourcesList.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { DataSourceItem } from './DataSourceItem';
|
||||
|
||||
export function DataSourcesList() {
|
||||
const dataSources = [
|
||||
{
|
||||
id: 'google-drive',
|
||||
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' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'yuque',
|
||||
name: 'Yuque',
|
||||
type: 'yuque',
|
||||
accounts: []
|
||||
},
|
||||
{
|
||||
id: 'github',
|
||||
name: 'Github',
|
||||
type: 'github',
|
||||
accounts: []
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-medium text-gray-900">数据源</h2>
|
||||
<div className="space-y-4">
|
||||
{dataSources.map(source => (
|
||||
<DataSourceItem key={source.id} {...source} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/components/Auth/Divider.tsx
Normal file
13
src/components/Auth/Divider.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
export function Divider() {
|
||||
return (
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-600"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 text-gray-400 bg-gray-800">or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/components/Auth/LoginForm.tsx
Normal file
56
src/components/Auth/LoginForm.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface LoginFormProps {
|
||||
onSubmit: (email: string, password: string) => void;
|
||||
}
|
||||
|
||||
export function LoginForm({ onSubmit }: LoginFormProps) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(email, password);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
19
src/components/Auth/SocialButton.tsx
Normal file
19
src/components/Auth/SocialButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
interface SocialButtonProps {
|
||||
icon: React.ReactNode;
|
||||
provider: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function SocialButton({ icon, provider, onClick }: SocialButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="w-full flex items-center justify-center gap-3 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{icon}
|
||||
<span>Continue with {provider}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
28
src/components/Auth/UserProfile.tsx
Normal file
28
src/components/Auth/UserProfile.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { User, Edit } from 'lucide-react';
|
||||
|
||||
interface UserProfileProps {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function UserProfile({ name, email }: UserProfileProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-medium text-gray-900">账户信息</h2>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<User className="w-6 h-6 text-gray-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-medium text-gray-900">{name}</span>
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
interface DropdownListProps {
|
||||
selected: (item: any) => void;
|
||||
suggests: any[];
|
||||
|
||||
68
src/components/Settings/Account.tsx
Normal file
68
src/components/Settings/Account.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { OpenBrowserURL } from "@/utils/index";
|
||||
import logoImg from "@/assets/32x32.png";
|
||||
|
||||
export default function Account() {
|
||||
const app_uid = useAppStore((state) => state.app_uid);
|
||||
const setAppUid = useAppStore((state) => state.setAppUid);
|
||||
|
||||
async function initializeUser() {
|
||||
let uid = app_uid;
|
||||
if (!uid) {
|
||||
uid = uuidv4();
|
||||
setAppUid(uid);
|
||||
}
|
||||
// const response = await fetch("/api/register", {
|
||||
// method: "POST",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// body: JSON.stringify({ uid }),
|
||||
// });
|
||||
// const { token } = await response.json();
|
||||
// localStorage.setItem("auth_token", token);
|
||||
OpenBrowserURL(`http://localhost:1420/login?uid=${uid}`);
|
||||
}
|
||||
|
||||
function LoginClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
|
||||
event.preventDefault();
|
||||
initializeUser();
|
||||
}
|
||||
return (
|
||||
<div className="h-[450px] bg-gradient-to-br from-purple-100 via-purple-200 to-gray-200 dark:from-gray-800 dark:via-gray-900 dark:to-black flex flex-col items-center justify-center p-6">
|
||||
<div className="w-full max-w-md flex flex-col items-center text-center space-y-8">
|
||||
<div className="animate-pulse">
|
||||
<img
|
||||
src={logoImg}
|
||||
alt="logo"
|
||||
className="w-12 h-12 text-red-500 dark:text-red-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
Get Started
|
||||
</h1>
|
||||
<p className="text-gray-600 text-sm leading-relaxed max-w-sm dark:text-gray-300">
|
||||
You need to log in or create an account to view your organizations,
|
||||
manage your custom extensions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a
|
||||
href="#"
|
||||
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
||||
>
|
||||
Sign Up
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="text-sm/6 font-semibold text-gray-900 dark:text-gray-100"
|
||||
onClick={LoginClick}
|
||||
>
|
||||
Log In <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import { Settings, Puzzle, User, Users, Settings2, Info } from "lucide-react";
|
||||
import { Settings, Puzzle, User, Settings2, Info } from "lucide-react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import GeneralSettings from "./GeneralSettings";
|
||||
import AboutView from "./AboutView";
|
||||
import Account from "./Account";
|
||||
// import CocoCloud from "@/components/Auth/CocoCloud"
|
||||
import Footer from "../Footer";
|
||||
import { useTheme } from "../../contexts/ThemeContext";
|
||||
import { AppTheme } from "../../utils/tauri";
|
||||
@@ -24,7 +26,6 @@ function SettingsPage() {
|
||||
{ name: "General", icon: Settings },
|
||||
{ name: "Extensions", icon: Puzzle },
|
||||
{ name: "Account", icon: User },
|
||||
{ name: "Organizations", icon: Users },
|
||||
{ name: "Advanced", icon: Settings2 },
|
||||
{ name: "About", icon: Info },
|
||||
];
|
||||
@@ -77,18 +78,8 @@ function SettingsPage() {
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
Account settings content
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
Organizations settings content
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
<Account />
|
||||
{/* <CocoCloud /> */}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SettingsPanel title="">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { getAllWindows, getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
const defaultWindowConfig = {
|
||||
@@ -30,7 +30,10 @@ export const useWindows = () => {
|
||||
const existWin = await getWin(args.label);
|
||||
|
||||
if (existWin) {
|
||||
console.log("Window already exists>>", existWin);
|
||||
console.log("Window already exists>>", existWin, existWin.show);
|
||||
await existWin.show();
|
||||
await existWin.setFocus();
|
||||
await existWin.center();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,55 +81,92 @@ export const useWindows = () => {
|
||||
}, []);
|
||||
|
||||
const listenEvents = useCallback(() => {
|
||||
listen("win-create", (event) => {
|
||||
console.log(event);
|
||||
createWin(event.payload);
|
||||
});
|
||||
let unlistenHandlers: { (): void; (): void; (): void; (): void; }[] = [];
|
||||
|
||||
listen("win-show", async () => {
|
||||
if (!appWindow || appWindow.label.indexOf("main") === -1) return;
|
||||
await appWindow.show();
|
||||
await appWindow.unminimize();
|
||||
await appWindow.setFocus();
|
||||
});
|
||||
|
||||
listen("win-hide", async () => {
|
||||
if (!appWindow || appWindow.label.indexOf("main") === -1) return;
|
||||
await appWindow.hide();
|
||||
});
|
||||
|
||||
listen("win-close", async () => {
|
||||
await appWindow.close();
|
||||
});
|
||||
|
||||
listen("open_settings", (event) => {
|
||||
console.log("open_settings", event);
|
||||
let url = "/ui/settings"
|
||||
if (event.payload==="about") {
|
||||
url = "/ui/settings?tab=about"
|
||||
}
|
||||
createWin({
|
||||
label: "settings",
|
||||
title: "Settings Window",
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
width: 900,
|
||||
height: 600,
|
||||
alwaysOnTop: true,
|
||||
shadow: true,
|
||||
decorations: true,
|
||||
closable: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
url,
|
||||
const setupListeners = async () => {
|
||||
const winCreateHandler = await listen("win-create", (event) => {
|
||||
console.log(event);
|
||||
createWin(event.payload);
|
||||
});
|
||||
});
|
||||
unlistenHandlers.push(winCreateHandler);
|
||||
|
||||
const winShowHandler = await listen("win-show", async () => {
|
||||
if (!appWindow || !appWindow.label.includes("main")) return;
|
||||
await appWindow.show();
|
||||
await appWindow.unminimize();
|
||||
await appWindow.setFocus();
|
||||
});
|
||||
unlistenHandlers.push(winShowHandler);
|
||||
|
||||
const winHideHandler = await listen("win-hide", async () => {
|
||||
if (!appWindow || !appWindow.label.includes("main")) return;
|
||||
await appWindow.hide();
|
||||
});
|
||||
unlistenHandlers.push(winHideHandler);
|
||||
|
||||
const winCloseHandler = await listen("win-close", async () => {
|
||||
await appWindow.close();
|
||||
});
|
||||
unlistenHandlers.push(winCloseHandler);
|
||||
};
|
||||
|
||||
setupListeners();
|
||||
|
||||
// Cleanup function to remove all listeners
|
||||
return () => {
|
||||
unlistenHandlers.forEach((unlistenHandler) => unlistenHandler());
|
||||
};
|
||||
}, [appWindow, createWin]);
|
||||
|
||||
useEffect(() => {
|
||||
listenEvents();
|
||||
const cleanup = listenEvents();
|
||||
return cleanup; // Ensure cleanup on unmount
|
||||
}, [listenEvents]);
|
||||
|
||||
const listenSettingsEvents = useCallback(() => {
|
||||
let unlistenHandler: UnlistenFn;
|
||||
|
||||
const setupListener = async () => {
|
||||
unlistenHandler = await listen("open_settings", (event) => {
|
||||
console.log("open_settings", event);
|
||||
let url = "/ui/settings"
|
||||
if (event.payload === "about") {
|
||||
url = "/ui/settings?tab=about"
|
||||
}
|
||||
createWin({
|
||||
label: "settings",
|
||||
title: "Settings Window",
|
||||
width: 1000,
|
||||
height: 600,
|
||||
alwaysOnTop: false,
|
||||
shadow: true,
|
||||
decorations: true,
|
||||
transparent: false,
|
||||
closable: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
url,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
setupListener();
|
||||
|
||||
// Return the cleanup function to unlisten to the event
|
||||
return () => {
|
||||
if (unlistenHandler) {
|
||||
unlistenHandler();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const cleanup = listenSettingsEvents();
|
||||
return cleanup; // Ensure cleanup on unmount
|
||||
}, [listenSettingsEvents]);
|
||||
|
||||
return {
|
||||
createWin,
|
||||
closeWin,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
body,
|
||||
#root {
|
||||
@apply text-gray-900 rounded-xl overflow-hidden antialiased;
|
||||
@apply text-gray-900 antialiased;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
@@ -34,7 +34,11 @@
|
||||
|
||||
.dark body,
|
||||
.dark #root {
|
||||
@apply text-gray-100 rounded-xl antialiased;
|
||||
@apply text-gray-100;
|
||||
}
|
||||
|
||||
.input-body {
|
||||
@apply rounded-xl overflow-hidden
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
55
src/pages/login/index.tsx
Normal file
55
src/pages/login/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
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';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md bg-gray-800 rounded-xl p-8 shadow-2xl">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Welcome Back</h1>
|
||||
<p className="text-gray-400">Sign in to continue to Coco</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<SocialButton
|
||||
icon={<Github className="w-5 h-5" />}
|
||||
provider="GitHub"
|
||||
onClick={() => GithubClick()}
|
||||
/>
|
||||
<SocialButton
|
||||
icon={<Mail className="w-5 h-5" />}
|
||||
provider="Google"
|
||||
onClick={() => console.log('Google login')}
|
||||
/>
|
||||
<SocialButton
|
||||
icon={<Apple className="w-5 h-5" />}
|
||||
provider="Apple"
|
||||
onClick={() => console.log('Apple login')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<LoginForm onSubmit={(email, password) => console.log(email, password)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,22 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
|
||||
import useEscape from "../hooks/useEscape";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
function updateBodyClass(path: string) {
|
||||
const body = document.body;
|
||||
body.className = "";
|
||||
|
||||
if (path === "/ui") {
|
||||
body.classList.add("input-body");
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
updateBodyClass(location.pathname);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEscape();
|
||||
|
||||
return <Outlet />;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
|
||||
import App from "../App";
|
||||
import ErrorPage from "../error-page";
|
||||
// import Settings from "../components/Settings";
|
||||
import Settings2 from "../components/Settings/index2";
|
||||
import SearchChat from "../components/SearchChat";
|
||||
import Transition from "../components/SearchChat/Transition";
|
||||
import ChatAI from "../components/ChatAI";
|
||||
import MySearch from "../components/MySearch";
|
||||
import Layout from "./Layout";
|
||||
import WebApp from "../pages/web";
|
||||
import DesktopApp from "../pages/app";
|
||||
import App from "@/App";
|
||||
import ErrorPage from "@/error-page";
|
||||
import Settings2 from "@/components/Settings/index2";
|
||||
import SearchChat from "@/components/SearchChat";
|
||||
import Transition from "@/components/SearchChat/Transition";
|
||||
import ChatAI from "@/components/ChatAI";
|
||||
import MySearch from "@/components/MySearch";
|
||||
import WebApp from "@/pages/web";
|
||||
import DesktopApp from "@/pages/app";
|
||||
import Login from "@/pages/login";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
@@ -26,6 +26,7 @@ export const router = createBrowserRouter([
|
||||
{ path: "/ui/transition", element: <Transition /> },
|
||||
{ path: "/ui/app", element: <App /> },
|
||||
{ path: "/web", element: <WebApp /> },
|
||||
{ path: "/login", element: <Login /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { persist } from "zustand/middleware";
|
||||
export type IAppStore = {
|
||||
showTooltip: boolean;
|
||||
setShowTooltip: (showTooltip: boolean) => void;
|
||||
app_uid: string;
|
||||
setAppUid: (app_uid: string) => void,
|
||||
};
|
||||
|
||||
export const useAppStore = create<IAppStore>()(
|
||||
@@ -11,10 +13,15 @@ export const useAppStore = create<IAppStore>()(
|
||||
(set) => ({
|
||||
showTooltip: true,
|
||||
setShowTooltip: (showTooltip: boolean) => set({ showTooltip }),
|
||||
app_uid: "",
|
||||
setAppUid: (app_uid: string) => set({ app_uid }),
|
||||
}),
|
||||
{
|
||||
name: "app-store",
|
||||
partialize: (state) => ({ showTooltip: state.showTooltip }),
|
||||
partialize: (state) => ({
|
||||
showTooltip: state.showTooltip,
|
||||
app_uid: state.app_uid
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
// 1
|
||||
export async function copyToClipboard(text: string) {
|
||||
@@ -57,4 +58,26 @@ export const IsTauri = () => {
|
||||
window !== undefined &&
|
||||
(window as any).__TAURI_INTERNALS__ !== undefined
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const OpenBrowserURL = async (url: string) => {
|
||||
if (!url) return;
|
||||
if (isTauri()) {
|
||||
try {
|
||||
const { open } = await import("@tauri-apps/plugin-shell");
|
||||
await open(url);
|
||||
console.log("URL opened in default browser");
|
||||
} catch (error) {
|
||||
console.error("Failed to open URL:", error);
|
||||
}
|
||||
} else {
|
||||
window.open(url);
|
||||
}
|
||||
};
|
||||
|
||||
export const authWitheGithub = (uid: string) => {
|
||||
const authorizeUrl = "https://github.com/login/oauth/authorize";
|
||||
console.log(111, process.env.NODE_ENV, uid)
|
||||
|
||||
location.href = `${authorizeUrl}?client_id=${"Ov23li4IcdbbWp2RgLTN"}&redirect_uri=${"http://localhost:1420/login"}`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user