mirror of
https://github.com/Termix-SSH/Mobile.git
synced 2026-07-28 05:51:43 +02:00
6c40d2f0cc
* fix: register physical Tab key on iPadOS/iOS hardware keyboards (#14) The native module only registered Shift+Tab but not bare Tab, so iOS intercepted it for UI focus navigation. Register an unmodified Tab UIKeyCommand with wantsPriorityOverSystemBehavior and handle both Tab and Shift+Tab in the same JS branch. Fixes Termix-SSH/Support#557 * fix: pass jumpHosts, forceKeyboardInteractive, and overrideCredentialUsername to terminal connection (#15) These fields were defined on SSHHost but omitted from the terminal hostConfig passthrough. Without jumpHosts the backend cannot route through jump servers; without forceKeyboardInteractive hosts that require keyboard-interactive auth fail silently; without overrideCredentialUsername shared credentials use the wrong username. Fixes Termix-SSH/Support#422 Fixes Termix-SSH/Support#638 * fix: improve mobile terminal scroll recovery (#17) * Add mobile remote desktop sessions (#18) * feat: add mobile remote desktop sessions * feat: add remote desktop input controls * feat: polish remote desktop controls * feat: add mobile terminal font selection (#19) * feat: add mobile shift tab hotkey (#20) * feat: initial UI redesign * feat: revamp server login system * chore: update app icon/background color * fix: remove unused fields in the settings tab * feat: improve the user pin system and update all settings modals to use new ui * fix: active server not updating unless app restarts * feat: add version in settings * feat: improve host page UI (updated host form and added credential management) * fix: preserve composed iOS terminal input * ci: restore mobile checks * feat: rewrite of connection system for all types * fix: preserve mobile jump hosts (#30) * fix: guard android user ca trust (#29) * fix: capture ios hardware tab key (#27) * fix: reset terminal input after special keys (#28) * fix: open oidc in system browser (#26) * fix: preserve websocket auth context (#25) * fix: keep none-auth terminal prompts alive (#23) * fix: allow local network SSL in Android app (#21) * feat: allow oidc passkeys * feat: bundeled xterm into app and added docker loading screen * feat: add 2fa, api key, and active sessions to settings and addressed multiple bug fixes and inconsistencies with termix web * chore: update repo images and readme * chore: update readme to split table for screenshots * chore: rename ios to apple in build workflow * fix: android build failure * feat: add snippet management and fix several bugs * fix: web login and oidc incorrect logic * chore: run linter --------- Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
157 lines
4.8 KiB
TypeScript
157 lines
4.8 KiB
TypeScript
import { View, ActivityIndicator, ScrollView } from "react-native";
|
|
import { AlertCircle, RotateCcw } from "lucide-react-native";
|
|
import { Text, Button } from "@/app/components/ui";
|
|
import { useThemeColor } from "@/app/contexts/ThemeContext";
|
|
import { ConnectionLog } from "./useConnectionLog";
|
|
import type { ConnectionLogEntry } from "@/types";
|
|
|
|
export type SessionFrameStatus =
|
|
| "loading"
|
|
| "ready"
|
|
| "error"
|
|
| "empty";
|
|
|
|
/**
|
|
* SessionFrame — the standard chrome shared by every session type (file
|
|
* manager, docker, stats, tunnel). Renders an optional header (title/subtitle +
|
|
* trailing actions), a connection-log panel, and centralized loading / error /
|
|
* empty states so each session type doesn't reinvent them.
|
|
*
|
|
* The terminal and remote-desktop canvases are full-bleed and manage their own
|
|
* chrome, so they don't use this.
|
|
*/
|
|
export function SessionFrame({
|
|
title,
|
|
subtitle,
|
|
headerActions,
|
|
toolbar,
|
|
status = "ready",
|
|
loadingLabel = "Connecting…",
|
|
errorMessage,
|
|
emptyMessage = "Nothing to show",
|
|
emptyIcon,
|
|
onRetry,
|
|
logEntries,
|
|
isConnecting,
|
|
isConnected,
|
|
hasConnectionError,
|
|
onLogClear,
|
|
scroll = false,
|
|
children,
|
|
}: {
|
|
title?: string;
|
|
subtitle?: string;
|
|
headerActions?: React.ReactNode;
|
|
toolbar?: React.ReactNode;
|
|
status?: SessionFrameStatus;
|
|
loadingLabel?: string;
|
|
errorMessage?: string;
|
|
emptyMessage?: string;
|
|
emptyIcon?: React.ReactNode;
|
|
onRetry?: () => void;
|
|
logEntries?: ConnectionLogEntry[];
|
|
isConnecting?: boolean;
|
|
isConnected?: boolean;
|
|
hasConnectionError?: boolean;
|
|
onLogClear?: () => void;
|
|
scroll?: boolean;
|
|
children?: React.ReactNode;
|
|
}) {
|
|
const color = useThemeColor();
|
|
|
|
const Body: any = scroll ? ScrollView : View;
|
|
const bodyProps = scroll
|
|
? { contentContainerStyle: { flexGrow: 1 } }
|
|
: { className: "flex-1" };
|
|
|
|
return (
|
|
<View className="flex-1 bg-background">
|
|
{(title || headerActions) && (
|
|
<View className="flex-row items-center gap-2 px-4 py-3 border-b border-border">
|
|
<View className="flex-1 min-w-0">
|
|
{title ? (
|
|
<Text
|
|
weight="bold"
|
|
className="text-base text-foreground"
|
|
numberOfLines={1}
|
|
>
|
|
{title}
|
|
</Text>
|
|
) : null}
|
|
{subtitle ? (
|
|
<Text
|
|
className="text-xs text-muted-foreground"
|
|
numberOfLines={1}
|
|
>
|
|
{subtitle}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
{headerActions ? (
|
|
<View className="flex-row items-center gap-1.5">{headerActions}</View>
|
|
) : null}
|
|
</View>
|
|
)}
|
|
|
|
{toolbar ? (
|
|
<View className="border-b border-border">{toolbar}</View>
|
|
) : null}
|
|
|
|
{/* Connection log overlay — covers the frame while connecting/errored */}
|
|
{logEntries !== undefined && onLogClear !== undefined ? (
|
|
<ConnectionLog
|
|
entries={logEntries}
|
|
isConnecting={isConnecting ?? false}
|
|
isConnected={isConnected ?? false}
|
|
hasConnectionError={hasConnectionError ?? false}
|
|
onClear={onLogClear}
|
|
/>
|
|
) : null}
|
|
|
|
{/* Suppress the loading spinner when the log overlay is covering the screen */}
|
|
{status === "loading" && !(logEntries && logEntries.length > 0 && !isConnected) ? (
|
|
<View className="flex-1 items-center justify-center gap-3">
|
|
<ActivityIndicator size="large" color={color("accent-brand")} />
|
|
<Text className="text-sm text-muted-foreground">{loadingLabel}</Text>
|
|
</View>
|
|
) : status === "error" ? (
|
|
<View className="flex-1 items-center justify-center px-8 gap-3">
|
|
<AlertCircle size={36} color={color("destructive")} />
|
|
<Text className="text-sm text-muted-foreground text-center leading-5">
|
|
{errorMessage || "Something went wrong"}
|
|
</Text>
|
|
{onRetry ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
icon={<RotateCcw size={14} color={color("foreground")} />}
|
|
onPress={onRetry}
|
|
>
|
|
Retry
|
|
</Button>
|
|
) : null}
|
|
</View>
|
|
) : status === "empty" ? (
|
|
<View className="flex-1 items-center justify-center px-8 gap-3">
|
|
{emptyIcon ?? <AlertCircle size={32} color={color("muted-foreground")} />}
|
|
<Text className="text-sm text-muted-foreground text-center">
|
|
{emptyMessage}
|
|
</Text>
|
|
{onRetry ? (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
icon={<RotateCcw size={14} color={color("foreground")} />}
|
|
onPress={onRetry}
|
|
>
|
|
Refresh
|
|
</Button>
|
|
) : null}
|
|
</View>
|
|
) : (
|
|
<Body {...bodyProps}>{children}</Body>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|