Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ data/
.codebuddy/
release/
release2/
release-embedded/
dist-electron/
electron/node/
$null
Expand Down
29 changes: 26 additions & 3 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ let mainWindow = null;
let backendProcess = null;
const BACKEND_PORT = 3001;

function isRemoteServerMode() {
return process.env.USE_REMOTE_SERVER === "true";
}

function getRemoteServerUrl() {
const raw = (process.env.REMOTE_SERVER_URL || "").trim();
if (!raw) return "";
return raw.replace(/\/+$/, "");
}

// 获取用户数据目录(存放 SQLite 数据库和字体文件)
function getUserDataPath() {
return path.join(app.getPath("userData"), "nowen-data");
Expand Down Expand Up @@ -110,6 +120,9 @@ function stopBackend() {

// 创建主窗口
function createWindow() {
const useRemote = isRemoteServerMode();
const remoteServerUrl = getRemoteServerUrl();

mainWindow = new BrowserWindow({
width: 1280,
height: 800,
Expand All @@ -124,8 +137,16 @@ function createWindow() {
show: false,
});

// 加载前端页面(通过后端服务提供)
mainWindow.loadURL(`http://localhost:${BACKEND_PORT}`);
if (useRemote) {
if (!remoteServerUrl) {
throw new Error("USE_REMOTE_SERVER=true but REMOTE_SERVER_URL is not set");
}
console.log("[Electron] Remote server mode enabled:", remoteServerUrl);
mainWindow.loadURL(remoteServerUrl);
} else {
// 加载前端页面(通过本地后端服务提供)
mainWindow.loadURL(`http://localhost:${BACKEND_PORT}`);
}

mainWindow.once("ready-to-show", () => {
mainWindow.show();
Expand All @@ -147,7 +168,9 @@ function createWindow() {
// App 生命周期
app.whenReady().then(async () => {
try {
await startBackend();
if (!isRemoteServerMode()) {
await startBackend();
}
createWindow();
} catch (err) {
console.error("[Electron] Failed to start:", err);
Expand Down
106 changes: 103 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X } from "lucide-react";
import { Globe, Menu, PlugZap, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import Sidebar from "@/components/Sidebar";
import NoteList from "@/components/NoteList";
Expand All @@ -17,7 +17,15 @@ import { ThemeProvider } from "@/components/ThemeProvider";
import { SiteSettingsProvider, useSiteSettings } from "@/hooks/useSiteSettings";
import { TooltipProvider } from "@/components/ui/tooltip";
import { User } from "@/types";
import { getServerUrl, clearServerUrl } from "@/lib/api";
import {
clearServerUrl,
getServerUrl,
hasDesktopModeSelection,
isDesktopRemoteModeEnabled,
isElectronRuntime,
setDesktopModeSelection,
setDesktopRemoteMode,
} from "@/lib/api";
import { useBackButton, hideSplashScreen, useStatusBarSync, useKeyboardLayout, isNativePlatform } from "@/hooks/useCapacitor";

function SidebarResizeHandle() {
Expand Down Expand Up @@ -360,6 +368,86 @@ function MobileTopBar() {
);
}

function DesktopModeSelector() {
const { t } = useTranslation();

const handleUseBuiltInServer = () => {
setDesktopModeSelection(true);
setDesktopRemoteMode(false);
clearServerUrl();
localStorage.removeItem("nowen-token");
window.location.reload();
};

const handleUseRemoteServer = () => {
setDesktopModeSelection(true);
setDesktopRemoteMode(true);
localStorage.removeItem("nowen-token");
window.location.reload();
};

return (
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 selection:bg-indigo-500/30 transition-colors px-4">
<div className="fixed inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-to-br from-indigo-500/5 via-transparent to-transparent rounded-full blur-3xl" />
<div className="absolute -bottom-1/2 -right-1/2 w-full h-full bg-gradient-to-tl from-emerald-500/5 via-transparent to-transparent rounded-full blur-3xl" />
</div>

<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="relative w-full max-w-3xl"
>
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-3xl shadow-xl dark:shadow-2xl dark:shadow-black/20 p-8 md:p-10">
<div className="text-center max-w-xl mx-auto">
<h1 className="text-2xl md:text-3xl font-bold text-zinc-900 dark:text-zinc-100 tracking-tight">
{t("startup.title")}
</h1>
<p className="text-sm md:text-base text-zinc-500 dark:text-zinc-400 mt-3 leading-7">
{t("startup.description")}
</p>
</div>

<div className="grid md:grid-cols-2 gap-4 mt-8">
<button
type="button"
onClick={handleUseBuiltInServer}
className="text-left p-6 rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50/70 dark:bg-zinc-800/40 hover:border-emerald-500/40 hover:bg-emerald-50/80 dark:hover:bg-emerald-500/10 transition-colors"
>
<div className="inline-flex items-center justify-center w-11 h-11 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
<PlugZap size={22} />
</div>
<div className="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{t("startup.useBuiltInTitle")}
</div>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400 leading-6">
{t("startup.useBuiltInDesc")}
</p>
</button>

<button
type="button"
onClick={handleUseRemoteServer}
className="text-left p-6 rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50/70 dark:bg-zinc-800/40 hover:border-indigo-500/40 hover:bg-indigo-50/80 dark:hover:bg-indigo-500/10 transition-colors"
>
<div className="inline-flex items-center justify-center w-11 h-11 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400">
<Globe size={22} />
</div>
<div className="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{t("startup.useRemoteTitle")}
</div>
<p className="mt-2 text-sm text-zinc-500 dark:text-zinc-400 leading-6">
{t("startup.useRemoteDesc")}
</p>
</button>
</div>
</div>
</motion.div>
</div>
);
}

function AuthGate() {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const [user, setUser] = useState<User | null>(null);
Expand All @@ -375,9 +463,13 @@ function AuthGate() {
// 判断是否为客户端模式(Electron / Android / 曾配置过服务器地址)
const isCapacitor = !!(window as any).Capacitor?.isNativePlatform?.()
|| !!(window as any).Capacitor?.platform && (window as any).Capacitor.platform !== "web";
const isElectron = isElectronRuntime();
const desktopModeSelected = isElectron ? hasDesktopModeSelection() : true;
const desktopRemoteMode = isDesktopRemoteModeEnabled();
const isClientMode = window.location.protocol === "file:"
|| window.location.protocol === "capacitor:"
|| isCapacitor
|| (isElectron && desktopRemoteMode)
|| !!getServerUrl();

const checkAuth = useCallback(() => {
Expand Down Expand Up @@ -408,13 +500,17 @@ function AuthGate() {
}, []);

useEffect(() => {
if (isElectron && !desktopModeSelected) {
setIsAuthenticated(false);
return;
}
// 客户端模式但没有服务器地址:直接显示登录页(含服务器输入框)
if (isClientMode && !getServerUrl()) {
setIsAuthenticated(false);
return;
}
checkAuth();
}, [checkAuth, isClientMode]);
}, [checkAuth, desktopModeSelected, isClientMode, isElectron]);

const handleDisconnect = () => {
clearServerUrl();
Expand Down Expand Up @@ -442,10 +538,14 @@ function AuthGate() {

// 未登录 → 一体化登录页
if (!isAuthenticated) {
if (isElectron && !desktopModeSelected) {
return <DesktopModeSelector />;
}
return (
<LoginPage
onLogin={handleLogin}
isClientMode={isClientMode}
isDesktopApp={isElectron}
onDisconnect={isClientMode ? handleDisconnect : undefined}
/>
);
Expand Down
47 changes: 45 additions & 2 deletions frontend/src/components/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@ import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Loader2, Lock, User, BookOpen, Globe, CheckCircle2, AlertCircle } from "lucide-react";
import { useTranslation } from "react-i18next";
import { getServerUrl, setServerUrl, clearServerUrl, testServerConnection } from "@/lib/api";
import { getServerUrl, setServerUrl, clearServerUrl, testServerConnection, isDesktopRemoteModeEnabled, setDesktopModeSelection, setDesktopRemoteMode } from "@/lib/api";

interface LoginPageProps {
onLogin: (token: string, user: any) => void;
/** 是否为客户端模式(Electron / Android / 曾配置过服务器地址) */
isClientMode?: boolean;
isDesktopApp?: boolean;
onDisconnect?: () => void;
}

export default function LoginPage({ onLogin, isClientMode = false, onDisconnect }: LoginPageProps) {
export default function LoginPage({ onLogin, isClientMode = false, isDesktopApp = false, onDisconnect }: LoginPageProps) {
const [serverAddress, setServerAddress] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [serverStatus, setServerStatus] = useState<"idle" | "checking" | "ok" | "fail">("idle");
const { t } = useTranslation();
const isDesktopRemoteMode = isDesktopApp && isDesktopRemoteModeEnabled();

// 回填上次的服务器地址
useEffect(() => {
Expand Down Expand Up @@ -107,6 +109,9 @@ export default function LoginPage({ onLogin, isClientMode = false, onDisconnect
};

const handleDisconnect = () => {
if (isDesktopApp) {
setDesktopRemoteMode(false);
}
clearServerUrl();
localStorage.removeItem("nowen-token");
setServerAddress("");
Expand All @@ -117,6 +122,20 @@ export default function LoginPage({ onLogin, isClientMode = false, onDisconnect
onDisconnect?.();
};

const handleEnableRemoteMode = () => {
setDesktopModeSelection(true);
setDesktopRemoteMode(true);
window.location.reload();
};

const handleUseBuiltInServer = () => {
setDesktopModeSelection(true);
setDesktopRemoteMode(false);
clearServerUrl();
localStorage.removeItem("nowen-token");
window.location.reload();
};

const serverStatusIcon = () => {
switch (serverStatus) {
case "checking":
Expand Down Expand Up @@ -292,6 +311,30 @@ export default function LoginPage({ onLogin, isClientMode = false, onDisconnect
</button>
</div>
)}

{isDesktopApp && !isClientMode && (
<div className="mt-3 flex justify-center">
<button
type="button"
onClick={handleEnableRemoteMode}
className="text-xs text-zinc-400 hover:text-indigo-500 dark:text-zinc-500 dark:hover:text-indigo-400 transition-colors"
>
{t("auth.switchToRemote")}
</button>
</div>
)}

{isDesktopRemoteMode && (
<div className="mt-3 flex justify-center">
<button
type="button"
onClick={handleUseBuiltInServer}
className="text-xs text-zinc-400 hover:text-emerald-500 dark:text-zinc-500 dark:hover:text-emerald-400 transition-colors"
>
{t("auth.useBuiltInServer")}
</button>
</div>
)}
</div>

{/* 底部说明 — 客户端模式 */}
Expand Down
Loading