mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
feat: initial ui redesign from demo
This commit is contained in:
+148
-118
@@ -1,53 +1,88 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { prepareClientCacheVersion } from "@/lib/client-cache-version";
|
||||
import { StrictMode, Suspense, lazy, useEffect, useState, useRef } from "react";
|
||||
import { StrictMode, Suspense, lazy, useState, useRef, useEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import "./ui/index.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import "./i18n/i18n";
|
||||
import "./ui/i18n/i18n";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import { Toaster } from "@/components/sonner";
|
||||
import { Auth, getStoredAuth, clearStoredAuth } from "@/auth/Auth";
|
||||
import { applyAccentColor, applyFontSize } from "@/lib/theme";
|
||||
import type { FontSizeId } from "@/types/ui-types";
|
||||
import { useServiceWorker } from "@/hooks/use-service-worker";
|
||||
|
||||
const DesktopApp = lazy(() => import("@/ui/desktop/DesktopApp.tsx"));
|
||||
const MobileApp = lazy(() =>
|
||||
import("@/ui/mobile/MobileApp.tsx").then((module) => ({
|
||||
default: module.MobileApp,
|
||||
})),
|
||||
);
|
||||
const HostManagerApp = lazy(
|
||||
() => import("./ui/desktop/apps/host-manager/HostManagerApp.tsx"),
|
||||
);
|
||||
const TerminalApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/terminal/TerminalApp.tsx"),
|
||||
const AppShell = lazy(() =>
|
||||
import("@/AppShell").then((m) => ({ default: m.AppShell })),
|
||||
);
|
||||
|
||||
// Full-screen apps opened via query params (e.g. from external links or Electron)
|
||||
const TerminalApp = lazy(() => import("@/features/terminal/TerminalApp"));
|
||||
const FileManagerApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/file-manager/FileManagerApp.tsx"),
|
||||
);
|
||||
const TunnelApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/tunnel/TunnelApp.tsx"),
|
||||
() => import("@/features/file-manager/FileManagerApp"),
|
||||
);
|
||||
const TunnelApp = lazy(() => import("@/features/tunnel/TunnelApp"));
|
||||
const ServerStatsApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/server-stats/ServerStatsApp.tsx"),
|
||||
);
|
||||
const DockerApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/docker/DockerApp.tsx"),
|
||||
);
|
||||
const GuacamoleApp = lazy(
|
||||
() => import("@/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx"),
|
||||
() => import("@/features/server-stats/ServerStatsApp"),
|
||||
);
|
||||
const DockerApp = lazy(() => import("@/features/docker/DockerApp"));
|
||||
const GuacamoleApp = lazy(() => import("@/features/guacamole/GuacamoleApp"));
|
||||
|
||||
const ElectronVersionCheck = lazy(() =>
|
||||
import("@/ui/desktop/user/ElectronVersionCheck.tsx").then((module) => ({
|
||||
import("@/user/ElectronVersionCheck").then((module) => ({
|
||||
default: module.ElectronVersionCheck,
|
||||
})),
|
||||
);
|
||||
|
||||
const FullscreenApp: React.FC = () => {
|
||||
type Phase = "idle-auth" | "fading-in" | "idle-app" | "fading-out";
|
||||
|
||||
function useWindowWidth() {
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
const lastSwitchTime = useRef(0);
|
||||
const isCurrentlyMobile = useRef(window.innerWidth < 768);
|
||||
const hasSwitchedOnce = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: number;
|
||||
const handleResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
const newWidth = window.innerWidth;
|
||||
const newIsMobile = newWidth < 768;
|
||||
const now = Date.now();
|
||||
if (hasSwitchedOnce.current && now - lastSwitchTime.current < 10000) {
|
||||
setWidth(newWidth);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
newIsMobile !== isCurrentlyMobile.current &&
|
||||
now - lastSwitchTime.current > 5000
|
||||
) {
|
||||
lastSwitchTime.current = now;
|
||||
isCurrentlyMobile.current = newIsMobile;
|
||||
hasSwitchedOnce.current = true;
|
||||
setWidth(newWidth);
|
||||
} else {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
function FullscreenApp() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const view = searchParams.get("view");
|
||||
const hostId = searchParams.get("hostId");
|
||||
|
||||
switch (view) {
|
||||
case "host-manager":
|
||||
return <HostManagerApp />;
|
||||
case "terminal":
|
||||
return <TerminalApp hostId={hostId || undefined} />;
|
||||
case "file-manager":
|
||||
@@ -63,53 +98,83 @@ const FullscreenApp: React.FC = () => {
|
||||
case "telnet":
|
||||
return <GuacamoleApp hostId={hostId || undefined} />;
|
||||
default:
|
||||
return <DesktopApp />;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
import { useServiceWorker } from "@/hooks/use-service-worker";
|
||||
}
|
||||
|
||||
function useWindowWidth() {
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
const lastSwitchTime = useRef(0);
|
||||
const isCurrentlyMobile = useRef(window.innerWidth < 768);
|
||||
const hasSwitchedOnce = useRef(false);
|
||||
function App() {
|
||||
const stored = getStoredAuth();
|
||||
const [phase, setPhase] = useState<Phase>(
|
||||
stored?.loggedIn ? "idle-app" : "idle-auth",
|
||||
);
|
||||
const [authUsername, setAuthUsername] = useState(stored?.username ?? "");
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const handleResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
const newWidth = window.innerWidth;
|
||||
const newIsMobile = newWidth < 768;
|
||||
const now = Date.now();
|
||||
|
||||
if (hasSwitchedOnce.current && now - lastSwitchTime.current < 10000) {
|
||||
setWidth(newWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
newIsMobile !== isCurrentlyMobile.current &&
|
||||
now - lastSwitchTime.current > 5000
|
||||
) {
|
||||
lastSwitchTime.current = now;
|
||||
isCurrentlyMobile.current = newIsMobile;
|
||||
hasSwitchedOnce.current = true;
|
||||
setWidth(newWidth);
|
||||
} else {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
const savedAccent = localStorage.getItem("termix-accent");
|
||||
if (savedAccent) applyAccentColor(savedAccent);
|
||||
const savedSize = localStorage.getItem(
|
||||
"termix-font-size",
|
||||
) as FontSizeId | null;
|
||||
applyFontSize(savedSize ?? "lg");
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return width;
|
||||
function handleLogin(u: string) {
|
||||
setAuthUsername(u);
|
||||
setPhase("fading-in");
|
||||
timerRef.current = setTimeout(() => setPhase("idle-app"), 450);
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
clearStoredAuth();
|
||||
setPhase("fading-out");
|
||||
timerRef.current = setTimeout(() => {
|
||||
setAuthUsername("");
|
||||
setPhase("idle-auth");
|
||||
}, 450);
|
||||
}
|
||||
|
||||
const showApp =
|
||||
phase === "idle-app" || phase === "fading-in" || phase === "fading-out";
|
||||
const showAuth =
|
||||
phase === "idle-auth" || phase === "fading-in" || phase === "fading-out";
|
||||
const appOpacity = phase === "idle-app" ? 1 : 0;
|
||||
const authOpacity = phase === "idle-auth" ? 1 : 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showApp && (
|
||||
<div
|
||||
className="fixed inset-0 z-0 transition-opacity duration-[450ms] ease-in-out"
|
||||
style={{
|
||||
opacity: appOpacity,
|
||||
pointerEvents: phase === "idle-app" ? "auto" : "none",
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<AppShell username={authUsername} onLogout={handleLogout} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAuth && (
|
||||
<div
|
||||
className="fixed inset-0 z-10 transition-opacity duration-[450ms] ease-in-out"
|
||||
style={{
|
||||
opacity: authOpacity,
|
||||
pointerEvents: phase === "idle-auth" ? "auto" : "none",
|
||||
}}
|
||||
>
|
||||
<Auth onLogin={handleLogin} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Toaster position="bottom-right" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RootApp() {
|
||||
@@ -125,61 +190,26 @@ function RootApp() {
|
||||
(window as Window & { opera?: string }).opera ||
|
||||
"";
|
||||
const isTermixMobile = /Termix-Mobile/.test(userAgent);
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const isFullscreen = searchParams.has("view");
|
||||
|
||||
const renderApp = () => {
|
||||
if (isFullscreen) {
|
||||
return <FullscreenApp />;
|
||||
}
|
||||
if (isFullscreen) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<FullscreenApp />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (isElectron()) {
|
||||
return <DesktopApp />;
|
||||
}
|
||||
if (isElectron() && showVersionCheck) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ElectronVersionCheck onContinue={() => setShowVersionCheck(false)} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (isTermixMobile) {
|
||||
return <MobileApp key="mobile" />;
|
||||
}
|
||||
|
||||
return isMobile ? <MobileApp key="mobile" /> : <DesktopApp key="desktop" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isFullscreen && (
|
||||
<div
|
||||
className="fixed inset-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-base)",
|
||||
backgroundImage: `linear-gradient(
|
||||
135deg,
|
||||
transparent 0%,
|
||||
transparent 49%,
|
||||
rgba(128, 128, 128, 0.03) 49%,
|
||||
rgba(128, 128, 128, 0.03) 51%,
|
||||
transparent 51%,
|
||||
transparent 100%
|
||||
)`,
|
||||
backgroundSize: "80px 80px",
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative min-h-screen" style={{ zIndex: 1 }}>
|
||||
{isElectron() && showVersionCheck && !isFullscreen ? (
|
||||
<Suspense fallback={null}>
|
||||
<ElectronVersionCheck
|
||||
onContinue={() => setShowVersionCheck(false)}
|
||||
isAuthenticated={false}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<Suspense fallback={null}>{renderApp()}</Suspense>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <App />;
|
||||
}
|
||||
|
||||
prepareClientCacheVersion().finally(() => {
|
||||
|
||||
Reference in New Issue
Block a user