src/App.tsx
Rama main · réplica de lectura del repositorio original.
import { useEffect, useMemo, useState } from "react";
import { useAppStore } from "./store/useAppStore";
import { TitleBar } from "./components/TitleBar";
import { UpdateButton } from "./components/UpdateButton";
import { ConnectionPanel } from "./features/connection/ConnectionPanel";
import { SubscriptionsPanel } from "./features/subscriptions/SubscriptionsPanel";
import { RoutingPanel } from "./features/routing/RoutingPanel";
import { SettingsPanel } from "./features/settings/SettingsPanel";
import { AndroidApp } from "./mobile/AndroidApp";
import { isAndroid } from "./lib/platform";
import type { Theme } from "./lib/types";
import { displayError, useI18n } from "./lib/i18n";
import "./App.css";
import "./mobile/android.css";
type TabId = "connection" | "subscriptions" | "routing" | "settings";
const NAV: { id: TabId; icon: string }[] = [
{ id: "connection", icon: "⚡" },
// Вкладка «Подписки» скрыта (hidden) — панель остаётся в коде, но не в навигации.
// { id: "subscriptions", label: "Подписки", icon: "🔗" },
{ id: "routing", icon: "🧭" },
{ id: "settings", icon: "⚙️" },
];
function applyTheme(theme: Theme) {
const resolved =
theme === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: theme;
document.documentElement.dataset.theme = resolved;
}
function App() {
const { language, t } = useI18n();
const coreVersion = useAppStore((s) => s.coreVersion);
const coreError = useAppStore((s) => s.coreError);
const theme = useAppStore((s) => s.settings?.theme ?? "system");
const connection = useAppStore((s) => s.connection);
const init = useAppStore((s) => s.init);
const [tab, setTab] = useState<TabId>("connection");
useEffect(() => {
init();
}, [init]);
useEffect(() => {
document.documentElement.lang = language;
}, [language]);
useEffect(() => {
applyTheme(theme);
if (theme !== "system") return;
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => applyTheme("system");
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, [theme]);
const statusDot = useMemo(() => {
switch (connection.state) {
case "connected":
return "dot ok";
case "connecting":
case "disconnecting":
return "dot busy";
case "error":
return "dot err";
default:
return "dot";
}
}, [connection.state]);
if (isAndroid) {
return <AndroidApp />;
}
return (
<div className="window-root">
<TitleBar />
<div className="app">
<aside className="sidebar">
<div className="brand">
<img className="brand-logo" src="/logo.png" alt="UniGate" />
<span className="brand-name">UniGate</span>
</div>
<nav className="nav">
{NAV.map((n) => (
<button
key={n.id}
className={`nav-item${tab === n.id ? " active" : ""}`}
onClick={() => setTab(n.id)}
>
<span className="nav-icon">{n.icon}</span>
{t(`nav.${n.id}`)}
{n.id === "connection" && <span className={statusDot} />}
</button>
))}
</nav>
<div className="sidebar-foot">
<UpdateButton />
{coreError ? (
<span className="core-badge err" title={displayError(language, coreError, "errors.core")}>
❌ {t("connection.notAvailable")}
</span>
) : (
<span
className="core-badge"
title={coreVersion ? t("connection.core", { version: coreVersion }) : t("connection.coreChecking")}
>
v{__APP_VERSION__}
</span>
)}
</div>
</aside>
<main className="content">
{tab === "connection" && <ConnectionPanel />}
{tab === "subscriptions" && <SubscriptionsPanel />}
{tab === "routing" && <RoutingPanel />}
{tab === "settings" && <SettingsPanel />}
</main>
</div>
</div>
);
}
export default App;