fix: opening duplicate windows (#88)

* feat: add login page

* fix: opening duplicate windows

* chore: vscode settings
This commit is contained in:
BiggerRain
2025-01-06 18:22:35 +08:00
committed by GitHub
parent 04338027f0
commit de74cb7c7e
21 changed files with 560 additions and 73 deletions

View File

@@ -33,8 +33,10 @@
"unlisten",
"unlistener",
"unminimize",
"uuidv",
"VITE",
"webviews",
"yuque",
"zustand"
],
"[rust]": {

View File

@@ -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
View File

@@ -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:

View File

@@ -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",

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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[];

View 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>
);
}

View File

@@ -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="">

View File

@@ -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,

View File

@@ -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
View 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>
);
}

View File

@@ -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 />;

View File

@@ -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 /> },
],
},
]);

View File

@@ -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
}),
}
)
);

View File

@@ -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"}`;
};