mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-15 12:23:42 +08:00
- Implemented `telegramAPIURL` and `validateTelegramAPIURL` functions for constructing and validating Telegram API URLs. - Added semantic versioning utilities in `version.go` to compare versions and validate semantic version formats. - Created tests for version comparison logic in `version_test.go`. - Introduced IMS call handling in `call_runtime.go`, including methods for dialing, answering, and hanging up calls. - Added tests for incoming call handling and validation in `call_runtime_test.go`. - Developed a new `PluginsCard` component for managing plugins via URL or file upload in the web interface. - Implemented plugin management functions in `extensions.ts` and created an `ExtensionPage` for displaying plugin contributions.
135 lines
4.5 KiB
TypeScript
135 lines
4.5 KiB
TypeScript
import { useEffect, useState, type ReactElement } from "react";
|
|
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
|
import { AuthProvider, useAuth } from "./store/auth";
|
|
import { LanguageProvider } from "./lib/i18n";
|
|
import { AuthenticatedShell } from "./components/shell/AuthenticatedShell";
|
|
import { UnauthenticatedShell } from "./components/shell/UnauthenticatedShell";
|
|
import { Disclaimer } from "./components/Disclaimer";
|
|
import { MessageHost } from "./components/ui/message";
|
|
import { ConfirmHost } from "./components/ui/MessageBox";
|
|
import { LoadingScreen } from "./components/ui/LoadingScreen";
|
|
import LoginPage from "./pages/LoginPage";
|
|
import DashboardPage from "./pages/DashboardPage";
|
|
import DevicesPage from "./pages/DevicesPage";
|
|
import ProxyPage from "./pages/ProxyPage";
|
|
import SmsPage from "./pages/SmsPage";
|
|
import LogsPage from "./pages/LogsPage";
|
|
import SettingsPage from "./pages/SettingsPage";
|
|
import ExtensionPage from "./pages/ExtensionPage";
|
|
|
|
const THEME_KEY = "theme";
|
|
const DISCLAIMER_KEY = "vocat_disclaimer_agreed_at";
|
|
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
|
|
|
|
function useTheme() {
|
|
const [isDark, setIsDark] = useState<boolean>(() => {
|
|
try {
|
|
return localStorage.getItem(THEME_KEY) === "dark";
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
useEffect(() => {
|
|
document.documentElement.classList.toggle("dark", isDark);
|
|
try {
|
|
localStorage.setItem(THEME_KEY, isDark ? "dark" : "light");
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}, [isDark]);
|
|
return { isDark, toggle: () => setIsDark((value) => !value) };
|
|
}
|
|
|
|
function RequireAuth({ children }: { children: ReactElement }) {
|
|
const { ready, isAuthenticated } = useAuth();
|
|
const location = useLocation();
|
|
if (!ready) return <LoadingScreen />;
|
|
if (!isAuthenticated) {
|
|
return <Navigate to={`/login?redirect=${encodeURIComponent(location.pathname)}`} replace />;
|
|
}
|
|
return children;
|
|
}
|
|
|
|
function LoginLayout({ isDark, onToggleTheme }: { isDark: boolean; onToggleTheme: () => void }) {
|
|
const { ready, isAuthenticated } = useAuth();
|
|
if (ready && isAuthenticated) return <Navigate to="/" replace />;
|
|
return <UnauthenticatedShell isDark={isDark} onToggleTheme={onToggleTheme} />;
|
|
}
|
|
|
|
function AppRoot() {
|
|
const { isDark, toggle } = useTheme();
|
|
const { ready, isAuthenticated } = useAuth();
|
|
const [showDisclaimer, setShowDisclaimer] = useState(false);
|
|
const [firstTime, setFirstTime] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!isAuthenticated) {
|
|
setShowDisclaimer(false);
|
|
return;
|
|
}
|
|
let ts: number | null = null;
|
|
try {
|
|
const raw = localStorage.getItem(DISCLAIMER_KEY);
|
|
ts = raw === null ? null : Number(raw);
|
|
} catch {
|
|
ts = null;
|
|
}
|
|
const expired = ts === null || Number.isNaN(ts) || Date.now() - ts >= SEVEN_DAYS;
|
|
if (expired) {
|
|
setFirstTime(ts === null || Number.isNaN(ts));
|
|
setShowDisclaimer(true);
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
function agree() {
|
|
try {
|
|
localStorage.setItem(DISCLAIMER_KEY, String(Date.now()));
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
setShowDisclaimer(false);
|
|
}
|
|
|
|
if (!ready) return <LoadingScreen />;
|
|
|
|
return (
|
|
<div className="h-screen w-screen overflow-hidden bg-gray-50 font-sans text-gray-900 transition-colors duration-300 selection:bg-indigo-500 selection:text-white dark:bg-[#101014] dark:text-gray-100">
|
|
<Routes>
|
|
<Route path="/login" element={<LoginLayout isDark={isDark} onToggleTheme={toggle} />}>
|
|
<Route index element={<LoginPage />} />
|
|
</Route>
|
|
<Route
|
|
path="/"
|
|
element={
|
|
<RequireAuth>
|
|
<AuthenticatedShell isDark={isDark} onToggleTheme={toggle} />
|
|
</RequireAuth>
|
|
}
|
|
>
|
|
<Route index element={<DashboardPage />} />
|
|
<Route path="devices/*" element={<DevicesPage />} />
|
|
<Route path="proxy" element={<ProxyPage />} />
|
|
<Route path="sms" element={<SmsPage />} />
|
|
<Route path="extensions/:pluginId/:contributionId" element={<ExtensionPage />} />
|
|
<Route path="logs" element={<LogsPage />} />
|
|
<Route path="settings" element={<SettingsPage />} />
|
|
</Route>
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
{showDisclaimer && <Disclaimer firstTime={firstTime} onAgree={agree} />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<LanguageProvider>
|
|
<MessageHost />
|
|
<ConfirmHost />
|
|
<AppRoot />
|
|
</LanguageProvider>
|
|
</AuthProvider>
|
|
);
|
|
}
|