diff --git a/package-lock.json b/package-lock.json
index 44db4c5b..67eeab74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -73,6 +73,7 @@
"tailwind-merge": "^3.3.1",
"validator": "^13.15.15",
"ws": "^8.18.3",
+ "xterm": "^5.3.0",
"zod": "^4.0.5"
},
"devDependencies": {
@@ -10830,6 +10831,13 @@
"node": ">=0.4"
}
},
+ "node_modules/xterm": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
+ "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
+ "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
+ "license": "MIT"
+ },
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
diff --git a/package.json b/package.json
index f25526da..74f44274 100644
--- a/package.json
+++ b/package.json
@@ -91,6 +91,7 @@
"tailwind-merge": "^3.3.1",
"validator": "^13.15.15",
"ws": "^8.18.3",
+ "xterm": "^5.3.0",
"zod": "^4.0.5"
},
"devDependencies": {
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index d1d55c02..0003cbc5 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -208,7 +208,7 @@
"downloadSample": "Download Sample",
"formatGuide": "Format Guide",
"uncategorized": "Uncategorized",
- "confirmDelete": "Are you sure you want to delete \"{{name}}\"?",
+ "confirmDelete": "Are you sure you want to delete \"{{name}}\" ?",
"failedToDeleteHost": "Failed to delete host",
"jsonMustContainHosts": "JSON must contain a \"hosts\" array or be an array of hosts",
"noHostsInJson": "No hosts found in JSON file",
@@ -810,5 +810,9 @@
"invalidVerificationCode": "Invalid verification code",
"failedToDisableTotp": "Failed to disable TOTP",
"failedToGenerateBackupCodes": "Failed to generate backup codes"
+ },
+ "mobile": {
+ "selectHostToStart": "Select a host to start your terminal session",
+ "limitedSupportMessage": "Mobile support is currently limited. A dedicated mobile app is coming soon to enhance your experience."
}
}
\ No newline at end of file
diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json
index ed52f6fa..d5d6edab 100644
--- a/public/locales/zh/translation.json
+++ b/public/locales/zh/translation.json
@@ -614,7 +614,7 @@
"firstUserMessage": "您是第一个用户,将被设为管理员。您可以在侧边栏用户下拉菜单中查看管理员设置。如果您认为这是错误,请检查 docker 日志,或创建",
"external": "外部",
"loginWithExternal": "使用外部提供商登录",
- "loginWithExternalDesc": "使用您配置的外部身份提供商登录",
+ "loginWithExternalDesc": "使用您配置的外部身份提供者登录",
"resetPasswordButton": "重置密码",
"sendResetCode": "发送重置代码",
"resetCodeDesc": "输入您的用户名以接收密码重置代码。代码将记录在 docker 容器日志中。",
@@ -852,5 +852,9 @@
"invalidVerificationCode": "无效的验证码",
"failedToDisableTotp": "禁用 TOTP 失败",
"failedToGenerateBackupCodes": "生成备用码失败"
+ },
+ "mobile": {
+ "selectHostToStart": "选择一个主机以开始您的终端会话",
+ "limitedSupportMessage": "移动端支持目前有限。我们即将推出专门的移动应用以提升您的体验。"
}
}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index 89185ec7..60f4db93 100644
--- a/src/index.css
+++ b/src/index.css
@@ -123,6 +123,10 @@
}
@layer base {
+ html, body {
+ height: 100%;
+ }
+
* {
@apply border-border outline-ring/50;
}
diff --git a/src/ui/Mobile/Apps/Navigation/BottomNavbar.tsx b/src/ui/Mobile/Apps/Navigation/BottomNavbar.tsx
deleted file mode 100644
index 82c9b7aa..00000000
--- a/src/ui/Mobile/Apps/Navigation/BottomNavbar.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import {Button} from "@/components/ui/button";
-import {Menu, X, Terminal as TerminalIcon} from "lucide-react";
-import {useTabs} from "@/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx";
-import {cn} from "@/lib/utils.ts";
-
-interface MenuProps {
- onSidebarOpenClick?: () => void;
-}
-
-export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
- const {tabs, currentTab, setCurrentTab, removeTab} = useTabs();
-
- return (
-
-
-
-
-
-
- {tabs.map(tab => (
-
- setCurrentTab(tab.id)}
- >
-
- {tab.title}
-
- removeTab(tab.id)}
- >
-
-
-
- ))}
-
-
-
- )
-}
diff --git a/src/ui/Mobile/Apps/Terminal/Terminal.tsx b/src/ui/Mobile/Apps/Terminal/Terminal.tsx
index af0c9c80..82944e52 100644
--- a/src/ui/Mobile/Apps/Terminal/Terminal.tsx
+++ b/src/ui/Mobile/Apps/Terminal/Terminal.tsx
@@ -97,17 +97,6 @@ export const Terminal = forwardRef(function SSHTerminal(
return () => window.removeEventListener('resize', handleWindowResize);
}, []);
- useEffect(() => {
- if (!terminal) return;
-
- const textarea = (terminal as any)._core?._textarea as HTMLTextAreaElement | undefined;
- if (textarea) {
- textarea.setAttribute("readonly", "true");
- textarea.setAttribute("inputmode", "none");
- textarea.style.caretColor = "transparent";
- }
- }, [terminal]);
-
function handleWindowResize() {
if (!isVisibleRef.current) return;
fitAddonRef.current?.fit();
@@ -158,7 +147,7 @@ export const Terminal = forwardRef(function SSHTerminal(
if (!terminal || !xtermRef.current || !hostConfig) return;
terminal.options = {
- cursorBlink: true,
+ cursorBlink: false,
cursorStyle: 'bar',
scrollback: 10000,
fontSize: 14,
@@ -173,6 +162,8 @@ export const Terminal = forwardRef(function SSHTerminal(
fastScrollModifier: 'alt',
fastScrollSensitivity: 5,
allowProposedApi: true,
+ disableStdin: true,
+ cursorInactiveStyle: "bar",
};
const fitAddon = new FitAddon();
@@ -187,6 +178,14 @@ export const Terminal = forwardRef(function SSHTerminal(
terminal.loadAddon(webLinksAddon);
terminal.open(xtermRef.current);
+ const textarea = xtermRef.current.querySelector('.xterm-helper-textarea') as HTMLTextAreaElement | null;
+ if (textarea) {
+ textarea.readOnly = true;
+ textarea.blur();
+ }
+
+ terminal.focus = () => {};
+
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
resizeTimeout.current = setTimeout(() => {
diff --git a/src/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx b/src/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx
index 68c40217..d93465ef 100644
--- a/src/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx
+++ b/src/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx
@@ -1,18 +1,26 @@
-import React, {useState} from "react";
+import React, {useState, useCallback, useEffect} from "react";
import Keyboard from "react-simple-keyboard";
import "react-simple-keyboard/build/css/index.css";
import "./kb-dark-theme.css";
interface TerminalKeyboardProps {
onSendInput: (input: string) => void;
+ onLayoutChange: () => void;
}
-export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
+export function TerminalKeyboard({onSendInput, onLayoutChange}: TerminalKeyboardProps) {
const [layoutName, setLayoutName] = useState("default");
const [isCtrl, setIsCtrl] = useState(false);
const [isAlt, setIsAlt] = useState(false);
- const onKeyPress = async (button: string) => {
+ useEffect(() => {
+ if (onLayoutChange) {
+ const timeoutId = setTimeout(() => onLayoutChange(), 100);
+ return () => clearTimeout(timeoutId);
+ }
+ }, [layoutName, onLayoutChange]);
+
+ const onKeyPress = useCallback((button: string) => {
if (button === "{shift}") {
setLayoutName("shift");
return;
@@ -47,20 +55,6 @@ export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
return;
}
- if (button === "{paste}") {
- if (navigator.clipboard?.readText) {
- try {
- const text = await navigator.clipboard.readText();
- if (text) {
- onSendInput(text);
- }
- } catch (err) {
-
- }
- }
- return;
- }
-
let input = button;
const specialKeyMap: { [key: string]: string } = {
@@ -90,8 +84,16 @@ export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
input = `\x1b${input}`;
}
+ try {
+ if (navigator.vibrate) {
+ navigator.vibrate(20);
+ }
+ } catch (e) {
+ console.error("Vibration failed:", e);
+ }
+
onSendInput(input);
- };
+ }, [onSendInput, isCtrl, isAlt]);
const buttonTheme = [
{
@@ -104,7 +106,7 @@ export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
},
{
class: "hg-space-small",
- buttons: "{hide} {less} {more}",
+ buttons: "{hide} {unhide} {less} {more}",
}
];
@@ -116,7 +118,7 @@ export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
}
return (
-
+
",
"F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12",
- "{arrowLeft} {arrowRight} {arrowUp} {arrowDown} {paste} {backspace}",
+ "{arrowLeft} {arrowRight} {arrowUp} {arrowDown} {backspace}",
"{hide} {less} {space} {enter}",
],
hide: [
@@ -166,7 +168,6 @@ export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
"{tab}": "tab",
"{ctrl}": "ctrl",
"{alt}": "alt",
- "{paste}": "paste",
"{end}": "end",
"{home}": "home",
"{pgUp}": "pgUp",
@@ -174,8 +175,9 @@ export function TerminalKeyboard({onSendInput}: TerminalKeyboardProps) {
}}
theme={"hg-theme-default dark-theme"}
useTouchEvents={true}
+ disableButtonHold={true}
buttonTheme={buttonTheme}
/>
);
-}
+}
\ No newline at end of file
diff --git a/src/ui/Mobile/Apps/Terminal/kb-dark-theme.css b/src/ui/Mobile/Apps/Terminal/kb-dark-theme.css
index 9e8a4908..213d48ca 100644
--- a/src/ui/Mobile/Apps/Terminal/kb-dark-theme.css
+++ b/src/ui/Mobile/Apps/Terminal/kb-dark-theme.css
@@ -32,7 +32,7 @@
}
.hg-space-medium {
- width: 60px;
+ width: 70px;
}
.hg-space-small {
diff --git a/src/ui/Mobile/MobileApp.tsx b/src/ui/Mobile/MobileApp.tsx
index 0074f4e4..9e417c5a 100644
--- a/src/ui/Mobile/MobileApp.tsx
+++ b/src/ui/Mobile/MobileApp.tsx
@@ -1,11 +1,12 @@
import React, {useRef, FC, useState, useEffect} from "react";
import {Terminal} from "@/ui/Mobile/Apps/Terminal/Terminal.tsx";
import {TerminalKeyboard} from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx";
-import {BottomNavbar} from "@/ui/Mobile/Apps/Navigation/BottomNavbar.tsx";
-import {LeftSidebar} from "@/ui/Mobile/Apps/Navigation/LeftSidebar.tsx";
-import {TabProvider, useTabs} from "@/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx";
+import {BottomNavbar} from "@/ui/Mobile/Navigation/BottomNavbar.tsx";
+import {LeftSidebar} from "@/ui/Mobile/Navigation/LeftSidebar.tsx";
+import {TabProvider, useTabs} from "@/ui/Mobile/Navigation/Tabs/TabContext.tsx";
import {getUserInfo} from "@/ui/main-axios.ts";
import {HomepageAuth} from "@/ui/Mobile/Homepage/HomepageAuth.tsx";
+import {useTranslation} from "react-i18next";
function getCookie(name: string) {
return document.cookie.split('; ').reduce((r, v) => {
@@ -15,6 +16,7 @@ function getCookie(name: string) {
}
const AppContent: FC = () => {
+ const {t} = useTranslation();
const {tabs, currentTab, getTab} = useTabs();
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
const [ready, setReady] = React.useState(true);
@@ -58,6 +60,14 @@ const AppContent: FC = () => {
return () => window.removeEventListener('storage', handleStorageChange)
}, [])
+ useEffect(() => {
+ const interval = setInterval(() => {
+ fitCurrentTerminal()
+ }, 2000);
+
+ return () => clearInterval(interval);
+ }, []);
+
const handleAuthSuccess = (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => {
setIsAuthenticated(true)
setIsAdmin(authData.isAdmin)
@@ -85,6 +95,10 @@ const AppContent: FC = () => {
const closeSidebar = () => setIsSidebarOpen(false);
+ const handleKeyboardLayoutChange = () => {
+ fitCurrentTerminal();
+ }
+
function handleKeyboardInput(input: string) {
const currentTerminalTab = getTab(currentTab as number);
if (currentTerminalTab && currentTerminalTab.terminalRef?.current?.sendInput) {
@@ -95,7 +109,7 @@ const AppContent: FC = () => {
if (authLoading) {
return (
-
Loading...
+
{t('common.loading')}
)
}
@@ -126,7 +140,7 @@ const AppContent: FC = () => {
{tabs.map(tab => (
{
))}
{tabs.length === 0 && (
-
- Select a host to start a terminal session.
+
+
+ {t('mobile.selectHostToStart')}
+
+
+ {t('mobile.limitedSupportMessage')}
+
)}
- {currentTab &&
}
+ {currentTab &&
+
+
+
+ }
setIsSidebarOpen(true)}
/>
diff --git a/src/ui/Mobile/Navigation/BottomNavbar.tsx b/src/ui/Mobile/Navigation/BottomNavbar.tsx
new file mode 100644
index 00000000..b2fea2d9
--- /dev/null
+++ b/src/ui/Mobile/Navigation/BottomNavbar.tsx
@@ -0,0 +1,48 @@
+import {Button} from "@/components/ui/button.tsx";
+import {Menu, X, Terminal as TerminalIcon} from "lucide-react";
+import {useTabs} from "@/ui/Mobile/Navigation/Tabs/TabContext.tsx";
+import {cn} from "@/lib/utils.ts";
+
+interface MenuProps {
+ onSidebarOpenClick?: () => void;
+}
+
+export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
+ const {tabs, currentTab, setCurrentTab, removeTab} = useTabs();
+
+ return (
+
+
+
+
+
+
+
+ {tabs.map(tab => (
+
+ setCurrentTab(tab.id)}
+ >
+
+ {tab.title}
+
+ removeTab(tab.id)}
+ >
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/src/ui/Mobile/Apps/Navigation/Hosts/FolderCard.tsx b/src/ui/Mobile/Navigation/Hosts/FolderCard.tsx
similarity index 97%
rename from src/ui/Mobile/Apps/Navigation/Hosts/FolderCard.tsx
rename to src/ui/Mobile/Navigation/Hosts/FolderCard.tsx
index 9e63ddbc..85c9e214 100644
--- a/src/ui/Mobile/Apps/Navigation/Hosts/FolderCard.tsx
+++ b/src/ui/Mobile/Navigation/Hosts/FolderCard.tsx
@@ -3,7 +3,7 @@ import {CardTitle} from "@/components/ui/card.tsx";
import {ChevronDown, Folder} from "lucide-react";
import {Button} from "@/components/ui/button.tsx";
import {Separator} from "@/components/ui/separator.tsx";
-import {Host} from "@/ui/Mobile/Apps/Navigation/Hosts/Host.tsx";
+import {Host} from "@/ui/Mobile/Navigation/Hosts/Host.tsx";
interface SSHHost {
id: number;
diff --git a/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx b/src/ui/Mobile/Navigation/Hosts/Host.tsx
similarity index 97%
rename from src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx
rename to src/ui/Mobile/Navigation/Hosts/Host.tsx
index 8145c7d8..bcbd2467 100644
--- a/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx
+++ b/src/ui/Mobile/Navigation/Hosts/Host.tsx
@@ -4,7 +4,7 @@ import {Button} from "@/components/ui/button.tsx";
import {ButtonGroup} from "@/components/ui/button-group.tsx";
import {Server, Terminal} from "lucide-react";
import {getServerStatusById} from "@/ui/main-axios.ts";
-import {useTabs} from "@/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx";
+import {useTabs} from "@/ui/Mobile/Navigation/Tabs/TabContext.tsx";
interface SSHHost {
id: number;
diff --git a/src/ui/Mobile/Apps/Navigation/LeftSidebar.tsx b/src/ui/Mobile/Navigation/LeftSidebar.tsx
similarity index 99%
rename from src/ui/Mobile/Apps/Navigation/LeftSidebar.tsx
rename to src/ui/Mobile/Navigation/LeftSidebar.tsx
index d98b968a..0da6c3d2 100644
--- a/src/ui/Mobile/Apps/Navigation/LeftSidebar.tsx
+++ b/src/ui/Mobile/Navigation/LeftSidebar.tsx
@@ -10,7 +10,7 @@ import {Button} from "@/components/ui/button.tsx";
import {ChevronUp, Menu, User2} from "lucide-react";
import React, {useState, useEffect, useMemo, useCallback} from "react";
import {Separator} from "@/components/ui/separator.tsx";
-import {FolderCard} from "@/ui/Mobile/Apps/Navigation/Hosts/FolderCard.tsx";
+import {FolderCard} from "@/ui/Mobile/Navigation/Hosts/FolderCard.tsx";
import {getSSHHosts} from "@/ui/main-axios.ts";
import {useTranslation} from "react-i18next";
import {Input} from "@/components/ui/input.tsx";
@@ -153,7 +153,7 @@ export function LeftSidebar({isSidebarOpen, setIsSidebarOpen, onHostConnect, dis
-