Mobile terminal
This commit is contained in:
71
src/main.tsx
71
src/main.tsx
@@ -1,25 +1,68 @@
|
|||||||
import {StrictMode} from 'react'
|
import {StrictMode, useEffect, useState, useRef} from 'react'
|
||||||
import {createRoot} from 'react-dom/client'
|
import {createRoot} from 'react-dom/client'
|
||||||
import { useMediaQuery } from "react-responsive";
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import DesktopApp from './ui/Desktop/DesktopApp.tsx'
|
import DesktopApp from './ui/Desktop/DesktopApp.tsx'
|
||||||
import MobileApp from './ui/Mobile/MobileApp.tsx'
|
import { MobileApp } from './ui/Mobile/MobileApp.tsx'
|
||||||
import {ThemeProvider} from "@/components/theme-provider"
|
import {ThemeProvider} from "@/components/theme-provider"
|
||||||
import './i18n/i18n'
|
import './i18n/i18n'
|
||||||
|
|
||||||
|
function useWindowWidth() {
|
||||||
|
const [width, setWidth] = useState(window.innerWidth);
|
||||||
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||||
|
const lastSwitchTime = useRef(0);
|
||||||
|
const isCurrentlyMobile = useRef(window.innerWidth < 768);
|
||||||
|
const hasSwitchedOnce = useRef(false);
|
||||||
|
|
||||||
function RootApp() {
|
useEffect(() => {
|
||||||
const isMobile = useMediaQuery({ maxWidth: 767 });
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
const handleResize = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
const newWidth = window.innerWidth;
|
||||||
|
const newIsMobile = newWidth < 768;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// If we've already switched once, don't switch again for a very long time
|
||||||
|
if (hasSwitchedOnce.current && (now - lastSwitchTime.current) < 10000) {
|
||||||
|
setWidth(newWidth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only switch if we're actually crossing the threshold AND enough time has passed
|
||||||
|
if (newIsMobile !== isCurrentlyMobile.current && (now - lastSwitchTime.current) > 5000) {
|
||||||
|
lastSwitchTime.current = now;
|
||||||
|
isCurrentlyMobile.current = newIsMobile;
|
||||||
|
hasSwitchedOnce.current = true;
|
||||||
|
setWidth(newWidth);
|
||||||
|
setIsMobile(newIsMobile);
|
||||||
|
} else {
|
||||||
|
setWidth(newWidth);
|
||||||
|
}
|
||||||
|
}, 2000); // Even longer debounce
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
return (
|
return () => {
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
clearTimeout(timeoutId);
|
||||||
{isMobile ? <MobileApp /> : <DesktopApp />}
|
window.removeEventListener("resize", handleResize);
|
||||||
</ThemeProvider>
|
};
|
||||||
);
|
}, []);
|
||||||
|
|
||||||
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
function RootApp() {
|
||||||
|
const width = useWindowWidth();
|
||||||
|
const isMobile = width < 768;
|
||||||
|
|
||||||
|
// Use a stable key to prevent unnecessary remounting
|
||||||
|
return isMobile ? <MobileApp key="mobile" /> : <DesktopApp key="desktop" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<RootApp />
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
</StrictMode>
|
<RootApp/>
|
||||||
);
|
</ThemeProvider>
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, {useState, useEffect} from "react"
|
import React, {useState, useEffect} from "react"
|
||||||
import {LeftSidebar} from "@/ui/Desktop/Navigation/LeftSidebar.tsx"
|
import {LeftSidebar} from "@/ui/Desktop/Navigation/LeftSidebar.tsx"
|
||||||
import {Homepage} from "@/ui/Homepage/Homepage.tsx"
|
import {Homepage} from "@/ui/Desktop/Homepage/Homepage.tsx"
|
||||||
import {AppView} from "@/ui/Desktop/Navigation/AppView.tsx"
|
import {AppView} from "@/ui/Desktop/Navigation/AppView.tsx"
|
||||||
import {HostManager} from "@/ui/Desktop/Apps/Host Manager/HostManager.tsx"
|
import {HostManager} from "@/ui/Desktop/Apps/Host Manager/HostManager.tsx"
|
||||||
import {TabProvider, useTabs} from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx"
|
import {TabProvider, useTabs} from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {HomepageAuth} from "@/ui/Homepage/HomepageAuth.tsx";
|
import {HomepageAuth} from "@/ui/Desktop/Homepage/HomepageAuth.tsx";
|
||||||
import {HomepageUpdateLog} from "@/ui/Homepage/HompageUpdateLog.tsx";
|
import {HomepageUpdateLog} from "@/ui/Desktop/Homepage/HompageUpdateLog.tsx";
|
||||||
import {HomepageAlertManager} from "@/ui/Homepage/HomepageAlertManager.tsx";
|
import {HomepageAlertManager} from "@/ui/Desktop/Homepage/HomepageAlertManager.tsx";
|
||||||
import {Button} from "@/components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import { getUserInfo, getDatabaseHealth } from "@/ui/main-axios.ts";
|
import { getUserInfo, getDatabaseHealth } from "@/ui/main-axios.ts";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, {useState, useEffect} from "react";
|
import React, {useState, useEffect} from "react";
|
||||||
import {cn} from "../../lib/utils.ts";
|
import {cn} from "@/lib/utils.ts";
|
||||||
import {Button} from "../../components/ui/button.tsx";
|
import {Button} from "@/components/ui/button.tsx";
|
||||||
import {Input} from "../../components/ui/input.tsx";
|
import {Input} from "@/components/ui/input.tsx";
|
||||||
import {Label} from "../../components/ui/label.tsx";
|
import {Label} from "@/components/ui/label.tsx";
|
||||||
import {Alert, AlertTitle, AlertDescription} from "../../components/ui/alert.tsx";
|
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {LanguageSwitcher} from "../../components/LanguageSwitcher";
|
import {LanguageSwitcher} from "@/components/LanguageSwitcher.tsx";
|
||||||
import {
|
import {
|
||||||
registerUser,
|
registerUser,
|
||||||
loginUser,
|
loginUser,
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
completePasswordReset,
|
completePasswordReset,
|
||||||
getOIDCAuthorizeUrl,
|
getOIDCAuthorizeUrl,
|
||||||
verifyTOTPLogin
|
verifyTOTPLogin
|
||||||
} from "../main-axios.ts";
|
} from "../../main-axios.ts";
|
||||||
|
|
||||||
function setCookie(name: string, value: string, days = 7) {
|
function setCookie(name: string, value: string, days = 7) {
|
||||||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||||||
|
|||||||
@@ -0,0 +1,406 @@
|
|||||||
|
import {useEffect, useRef, useState, useImperativeHandle, forwardRef} from 'react';
|
||||||
|
import {useXTerm} from 'react-xtermjs';
|
||||||
|
import {FitAddon} from '@xterm/addon-fit';
|
||||||
|
import {ClipboardAddon} from '@xterm/addon-clipboard';
|
||||||
|
import {Unicode11Addon} from '@xterm/addon-unicode11';
|
||||||
|
import {WebLinksAddon} from '@xterm/addon-web-links';
|
||||||
|
import {useTranslation} from 'react-i18next';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
mobileTerminalInitialized?: boolean;
|
||||||
|
mobileTerminalWebSocket?: WebSocket | null;
|
||||||
|
mobileTerminalPingInterval?: NodeJS.Timeout | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SSHTerminalProps {
|
||||||
|
hostConfig: any;
|
||||||
|
isVisible: boolean;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||||
|
{hostConfig, isVisible},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
const {instance: terminal, ref: xtermRef} = useXTerm();
|
||||||
|
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||||
|
const webSocketRef = useRef<WebSocket | null>(null);
|
||||||
|
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const wasDisconnectedBySSH = useRef(false);
|
||||||
|
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const isVisibleRef = useRef<boolean>(false);
|
||||||
|
const lastHostConfigRef = useRef<any>(null);
|
||||||
|
const isInitializedRef = useRef<boolean>(false);
|
||||||
|
const terminalInstanceRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||||
|
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||||
|
const notifyTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const DEBOUNCE_MS = 140;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isVisibleRef.current = isVisible;
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
|
function hardRefresh() {
|
||||||
|
try {
|
||||||
|
if (terminal && typeof (terminal as any).refresh === 'function') {
|
||||||
|
(terminal as any).refresh(0, terminal.rows - 1);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNotify(cols: number, rows: number) {
|
||||||
|
if (!(cols > 0 && rows > 0)) return;
|
||||||
|
pendingSizeRef.current = {cols, rows};
|
||||||
|
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||||
|
notifyTimerRef.current = setTimeout(() => {
|
||||||
|
const next = pendingSizeRef.current;
|
||||||
|
const last = lastSentSizeRef.current;
|
||||||
|
if (!next) return;
|
||||||
|
if (last && last.cols === next.cols && last.rows === next.rows) return;
|
||||||
|
if (webSocketRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
webSocketRef.current.send(JSON.stringify({type: 'resize', data: next}));
|
||||||
|
lastSentSizeRef.current = next;
|
||||||
|
}
|
||||||
|
}, DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
disconnect: () => {
|
||||||
|
if (pingIntervalRef.current) {
|
||||||
|
clearInterval(pingIntervalRef.current);
|
||||||
|
pingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
webSocketRef.current?.close();
|
||||||
|
},
|
||||||
|
fit: () => {
|
||||||
|
fitAddonRef.current?.fit();
|
||||||
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
hardRefresh();
|
||||||
|
},
|
||||||
|
sendInput: (data: string) => {
|
||||||
|
if (webSocketRef.current?.readyState === 1) {
|
||||||
|
webSocketRef.current.send(JSON.stringify({type: 'input', data}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notifyResize: () => {
|
||||||
|
try {
|
||||||
|
const cols = terminal?.cols ?? undefined;
|
||||||
|
const rows = terminal?.rows ?? undefined;
|
||||||
|
if (typeof cols === 'number' && typeof rows === 'number') {
|
||||||
|
scheduleNotify(cols, rows);
|
||||||
|
hardRefresh();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refresh: () => hardRefresh(),
|
||||||
|
}), [terminal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('resize', handleWindowResize);
|
||||||
|
return () => window.removeEventListener('resize', handleWindowResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function handleWindowResize() {
|
||||||
|
if (!isVisibleRef.current) return;
|
||||||
|
fitAddonRef.current?.fit();
|
||||||
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
hardRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
||||||
|
ws.addEventListener('open', () => {
|
||||||
|
ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
|
||||||
|
terminal.onData((data) => {
|
||||||
|
ws.send(JSON.stringify({type: 'input', data}));
|
||||||
|
});
|
||||||
|
|
||||||
|
pingIntervalRef.current = setInterval(() => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({type: 'ping'}));
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
window.mobileTerminalPingInterval = pingIntervalRef.current;
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('message', (event) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
if (msg.type === 'data') terminal.write(msg.data);
|
||||||
|
else if (msg.type === 'error') terminal.writeln(`\r\n[${t('terminal.error')}] ${msg.message}`);
|
||||||
|
else if (msg.type === 'connected') {
|
||||||
|
} else if (msg.type === 'disconnected') {
|
||||||
|
wasDisconnectedBySSH.current = true;
|
||||||
|
terminal.writeln(`\r\n[${msg.message || t('terminal.disconnected')}]`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('close', () => {
|
||||||
|
if (!wasDisconnectedBySSH.current) {
|
||||||
|
terminal.writeln(`\r\n[${t('terminal.connectionClosed')}]`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('error', () => {
|
||||||
|
terminal.writeln(`\r\n[${t('terminal.connectionError')}]`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!terminal || !xtermRef.current || !hostConfig) return;
|
||||||
|
|
||||||
|
if (window.mobileTerminalInitialized) {
|
||||||
|
webSocketRef.current = window.mobileTerminalWebSocket || null;
|
||||||
|
pingIntervalRef.current = window.mobileTerminalPingInterval || null;
|
||||||
|
|
||||||
|
terminal.options = {
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: 'bar',
|
||||||
|
scrollback: 10000,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: '"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
|
||||||
|
theme: {background: '#18181b', foreground: '#f7f7f7'},
|
||||||
|
allowTransparency: true,
|
||||||
|
convertEol: true,
|
||||||
|
windowsMode: false,
|
||||||
|
macOptionIsMeta: false,
|
||||||
|
macOptionClickForcesSelection: false,
|
||||||
|
rightClickSelectsWord: false,
|
||||||
|
fastScrollModifier: 'alt',
|
||||||
|
fastScrollSensitivity: 5,
|
||||||
|
allowProposedApi: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
const clipboardAddon = new ClipboardAddon();
|
||||||
|
const unicode11Addon = new Unicode11Addon();
|
||||||
|
const webLinksAddon = new WebLinksAddon();
|
||||||
|
|
||||||
|
fitAddonRef.current = fitAddon;
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(clipboardAddon);
|
||||||
|
terminal.loadAddon(unicode11Addon);
|
||||||
|
terminal.loadAddon(webLinksAddon);
|
||||||
|
terminal.open(xtermRef.current);
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||||
|
resizeTimeout.current = setTimeout(() => {
|
||||||
|
if (!isVisibleRef.current) return;
|
||||||
|
fitAddonRef.current?.fit();
|
||||||
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
hardRefresh();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(xtermRef.current);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddon.fit();
|
||||||
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
hardRefresh();
|
||||||
|
setVisible(true);
|
||||||
|
terminal.focus();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mobileTerminalInitialized = true;
|
||||||
|
isInitializedRef.current = true;
|
||||||
|
terminalInstanceRef.current = terminal;
|
||||||
|
lastHostConfigRef.current = hostConfig;
|
||||||
|
|
||||||
|
terminal.options = {
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: 'bar',
|
||||||
|
scrollback: 10000,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: '"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
|
||||||
|
theme: {background: '#18181b', foreground: '#f7f7f7'},
|
||||||
|
allowTransparency: true,
|
||||||
|
convertEol: true,
|
||||||
|
windowsMode: false,
|
||||||
|
macOptionIsMeta: false,
|
||||||
|
macOptionClickForcesSelection: false,
|
||||||
|
rightClickSelectsWord: false,
|
||||||
|
fastScrollModifier: 'alt',
|
||||||
|
fastScrollSensitivity: 5,
|
||||||
|
allowProposedApi: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
const clipboardAddon = new ClipboardAddon();
|
||||||
|
const unicode11Addon = new Unicode11Addon();
|
||||||
|
const webLinksAddon = new WebLinksAddon();
|
||||||
|
|
||||||
|
fitAddonRef.current = fitAddon;
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(clipboardAddon);
|
||||||
|
terminal.loadAddon(unicode11Addon);
|
||||||
|
terminal.loadAddon(webLinksAddon);
|
||||||
|
terminal.open(xtermRef.current);
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||||
|
resizeTimeout.current = setTimeout(() => {
|
||||||
|
if (!isVisibleRef.current) return;
|
||||||
|
fitAddonRef.current?.fit();
|
||||||
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
hardRefresh();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(xtermRef.current);
|
||||||
|
|
||||||
|
const readyFonts = (document as any).fonts?.ready instanceof Promise ? (document as any).fonts.ready : Promise.resolve();
|
||||||
|
readyFonts.then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddon.fit();
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddon.fit();
|
||||||
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
hardRefresh();
|
||||||
|
setVisible(true);
|
||||||
|
terminal.focus();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const cols = terminal.cols;
|
||||||
|
const rows = terminal.rows;
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development' &&
|
||||||
|
(window.location.port === '3000' || window.location.port === '5173' || window.location.port === '');
|
||||||
|
|
||||||
|
const wsUrl = isDev
|
||||||
|
? 'ws://localhost:8082'
|
||||||
|
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
|
||||||
|
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
webSocketRef.current = ws;
|
||||||
|
window.mobileTerminalWebSocket = ws;
|
||||||
|
wasDisconnectedBySSH.current = false;
|
||||||
|
|
||||||
|
setupWebSocketListeners(ws, cols, rows);
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||||
|
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||||
|
};
|
||||||
|
}, [xtermRef, terminal, hostConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isVisible && fitAddonRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddonRef.current?.fit();
|
||||||
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
hardRefresh();
|
||||||
|
terminal.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [isVisible, terminal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fitAddonRef.current) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddonRef.current?.fit();
|
||||||
|
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
|
||||||
|
hardRefresh();
|
||||||
|
if (terminal && isVisible) {
|
||||||
|
terminal.focus();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}, [isVisible, terminal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={xtermRef}
|
||||||
|
className="h-full w-full m-1"
|
||||||
|
style={{opacity: visible && isVisible ? 1 : 0, overflow: 'hidden'}}
|
||||||
|
onClick={() => {
|
||||||
|
terminal.focus();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.innerHTML = `
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap');
|
||||||
|
|
||||||
|
/* Load NerdFonts locally */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono Nerd Font';
|
||||||
|
src: url('/fonts/JetBrainsMonoNerdFont-Regular.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono Nerd Font';
|
||||||
|
src: url('/fonts/JetBrainsMonoNerdFont-Bold.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono Nerd Font';
|
||||||
|
src: url('/fonts/JetBrainsMonoNerdFont-Italic.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-viewport::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.xterm .xterm-viewport::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(180,180,180,0.7);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(120,120,120,0.9);
|
||||||
|
}
|
||||||
|
.xterm .xterm-viewport {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(180,180,180,0.7) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm {
|
||||||
|
font-feature-settings: "liga" 1, "calt" 1;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-screen {
|
||||||
|
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font', 'Cascadia Code', 'JetBrains Mono', Consolas, "Courier New", monospace !important;
|
||||||
|
font-variant-ligatures: contextual;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-screen .xterm-char {
|
||||||
|
font-feature-settings: "liga" 1, "calt" 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm .xterm-screen .xterm-char[data-char-code^="\\uE"] {
|
||||||
|
font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
|
import {Terminal} from "@/ui/Mobile/Apps/Terminal/Terminal.tsx";
|
||||||
|
|
||||||
export function MobileApp() {
|
export function MobileApp() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="h-screen w-screen bg-[#18181b]">
|
||||||
<h1>Mobile</h1>
|
<Terminal hostConfig={{
|
||||||
|
ip: "n/a",
|
||||||
|
port: 22,
|
||||||
|
username: "n/a",
|
||||||
|
password: "n/a"
|
||||||
|
}} isVisible={true}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user