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>
478 lines
12 KiB
TypeScript
478 lines
12 KiB
TypeScript
import React, {
|
|
createContext,
|
|
useContext,
|
|
useState,
|
|
useEffect,
|
|
useCallback,
|
|
} from "react";
|
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
import {
|
|
KeyboardCustomization,
|
|
KeyConfig,
|
|
KeyboardRow,
|
|
PresetType,
|
|
KeyboardSettings,
|
|
} from "@/types/keyboard";
|
|
import { getPresetById } from "@/app/tabs/sessions/terminal/keyboard/KeyDefinitions";
|
|
|
|
const STORAGE_KEY = "keyboardCustomization";
|
|
const DEFAULT_PRESET_ID: PresetType = "default";
|
|
|
|
const getDefaultConfig = (): KeyboardCustomization => {
|
|
const defaultPreset = getPresetById(DEFAULT_PRESET_ID);
|
|
if (!defaultPreset) {
|
|
throw new Error("Default preset not found");
|
|
}
|
|
|
|
return {
|
|
preset: DEFAULT_PRESET_ID,
|
|
version: 1,
|
|
topBar: {
|
|
pinnedKeys: [],
|
|
keys: [...defaultPreset.topBar.keys],
|
|
},
|
|
fullKeyboard: {
|
|
rows: defaultPreset.fullKeyboard.rows.map((row) => ({
|
|
...row,
|
|
keys: [...row.keys],
|
|
})),
|
|
},
|
|
settings: {
|
|
keySize: "medium",
|
|
compactMode: false,
|
|
hapticFeedback: false,
|
|
showHints: true,
|
|
},
|
|
};
|
|
};
|
|
|
|
interface KeyboardCustomizationContextType {
|
|
config: KeyboardCustomization;
|
|
isLoading: boolean;
|
|
|
|
setPreset: (presetId: PresetType) => Promise<void>;
|
|
|
|
addPinnedKey: (key: KeyConfig) => Promise<void>;
|
|
removePinnedKey: (keyId: string) => Promise<void>;
|
|
reorderPinnedKeys: (keys: KeyConfig[]) => Promise<void>;
|
|
addTopBarKey: (key: KeyConfig) => Promise<void>;
|
|
removeTopBarKey: (keyId: string) => Promise<void>;
|
|
reorderTopBarKeys: (keys: KeyConfig[]) => Promise<void>;
|
|
|
|
addRow: (row: KeyboardRow) => Promise<void>;
|
|
removeRow: (rowId: string) => Promise<void>;
|
|
reorderRows: (rows: KeyboardRow[]) => Promise<void>;
|
|
updateRow: (rowId: string, updates: Partial<KeyboardRow>) => Promise<void>;
|
|
toggleRowVisibility: (rowId: string) => Promise<void>;
|
|
addKeyToRow: (rowId: string, key: KeyConfig) => Promise<void>;
|
|
removeKeyFromRow: (rowId: string, keyId: string) => Promise<void>;
|
|
reorderKeysInRow: (rowId: string, keys: KeyConfig[]) => Promise<void>;
|
|
|
|
updateSettings: (settings: Partial<KeyboardSettings>) => Promise<void>;
|
|
|
|
resetToDefault: () => Promise<void>;
|
|
resetTopBar: () => Promise<void>;
|
|
resetFullKeyboard: () => Promise<void>;
|
|
|
|
exportConfig: () => string;
|
|
importConfig: (jsonString: string) => Promise<void>;
|
|
}
|
|
|
|
const KeyboardCustomizationContext = createContext<
|
|
KeyboardCustomizationContextType | undefined
|
|
>(undefined);
|
|
|
|
export const KeyboardCustomizationProvider: React.FC<{
|
|
children: React.ReactNode;
|
|
}> = ({ children }) => {
|
|
const [config, setConfig] =
|
|
useState<KeyboardCustomization>(getDefaultConfig());
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const loadConfig = async () => {
|
|
try {
|
|
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored) as KeyboardCustomization;
|
|
setConfig(parsed);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load keyboard configuration:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
loadConfig();
|
|
}, []);
|
|
|
|
const saveConfig = useCallback(async (newConfig: KeyboardCustomization) => {
|
|
try {
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newConfig));
|
|
setConfig(newConfig);
|
|
} catch (error) {
|
|
console.error("Failed to save keyboard configuration:", error);
|
|
}
|
|
}, []);
|
|
|
|
const setPreset = useCallback(
|
|
async (presetId: PresetType) => {
|
|
const preset = getPresetById(presetId);
|
|
if (!preset) {
|
|
console.error(`Preset ${presetId} not found`);
|
|
return;
|
|
}
|
|
|
|
const newConfig: KeyboardCustomization = {
|
|
preset: presetId,
|
|
version: 1,
|
|
topBar: {
|
|
pinnedKeys: [],
|
|
keys: [...preset.topBar.keys],
|
|
},
|
|
fullKeyboard: {
|
|
rows: preset.fullKeyboard.rows.map((row) => ({
|
|
...row,
|
|
keys: [...row.keys],
|
|
})),
|
|
},
|
|
settings: { ...config.settings },
|
|
};
|
|
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config.settings, saveConfig],
|
|
);
|
|
|
|
const addPinnedKey = useCallback(
|
|
async (key: KeyConfig) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
topBar: {
|
|
...config.topBar,
|
|
pinnedKeys: [...config.topBar.pinnedKeys, key],
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const removePinnedKey = useCallback(
|
|
async (keyId: string) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
topBar: {
|
|
...config.topBar,
|
|
pinnedKeys: config.topBar.pinnedKeys.filter((k) => k.id !== keyId),
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const reorderPinnedKeys = useCallback(
|
|
async (keys: KeyConfig[]) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
topBar: {
|
|
...config.topBar,
|
|
pinnedKeys: keys,
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const addTopBarKey = useCallback(
|
|
async (key: KeyConfig) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
topBar: {
|
|
...config.topBar,
|
|
keys: [...config.topBar.keys, key],
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const removeTopBarKey = useCallback(
|
|
async (keyId: string) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
topBar: {
|
|
...config.topBar,
|
|
keys: config.topBar.keys.filter((k) => k.id !== keyId),
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const reorderTopBarKeys = useCallback(
|
|
async (keys: KeyConfig[]) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
topBar: {
|
|
...config.topBar,
|
|
keys,
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const addRow = useCallback(
|
|
async (row: KeyboardRow) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
fullKeyboard: {
|
|
rows: [...config.fullKeyboard.rows, row],
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const removeRow = useCallback(
|
|
async (rowId: string) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
fullKeyboard: {
|
|
rows: config.fullKeyboard.rows.filter((r) => r.id !== rowId),
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const reorderRows = useCallback(
|
|
async (rows: KeyboardRow[]) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
fullKeyboard: {
|
|
rows,
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const updateRow = useCallback(
|
|
async (rowId: string, updates: Partial<KeyboardRow>) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
fullKeyboard: {
|
|
rows: config.fullKeyboard.rows.map((row) =>
|
|
row.id === rowId ? { ...row, ...updates } : row,
|
|
),
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const toggleRowVisibility = useCallback(
|
|
async (rowId: string) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
fullKeyboard: {
|
|
rows: config.fullKeyboard.rows.map((row) =>
|
|
row.id === rowId ? { ...row, visible: !row.visible } : row,
|
|
),
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const addKeyToRow = useCallback(
|
|
async (rowId: string, key: KeyConfig) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
fullKeyboard: {
|
|
rows: config.fullKeyboard.rows.map((row) =>
|
|
row.id === rowId ? { ...row, keys: [...row.keys, key] } : row,
|
|
),
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const removeKeyFromRow = useCallback(
|
|
async (rowId: string, keyId: string) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
fullKeyboard: {
|
|
rows: config.fullKeyboard.rows.map((row) =>
|
|
row.id === rowId
|
|
? { ...row, keys: row.keys.filter((k) => k.id !== keyId) }
|
|
: row,
|
|
),
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const reorderKeysInRow = useCallback(
|
|
async (rowId: string, keys: KeyConfig[]) => {
|
|
const newConfig = {
|
|
...config,
|
|
preset: "custom" as PresetType,
|
|
fullKeyboard: {
|
|
rows: config.fullKeyboard.rows.map((row) =>
|
|
row.id === rowId ? { ...row, keys } : row,
|
|
),
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const updateSettings = useCallback(
|
|
async (settings: Partial<KeyboardSettings>) => {
|
|
const newConfig = {
|
|
...config,
|
|
settings: {
|
|
...config.settings,
|
|
...settings,
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
},
|
|
[config, saveConfig],
|
|
);
|
|
|
|
const resetToDefault = useCallback(async () => {
|
|
await saveConfig(getDefaultConfig());
|
|
}, [saveConfig]);
|
|
|
|
const resetTopBar = useCallback(async () => {
|
|
const defaultPreset = getPresetById(
|
|
config.preset === "custom" ? DEFAULT_PRESET_ID : config.preset,
|
|
);
|
|
if (!defaultPreset) return;
|
|
|
|
const newConfig = {
|
|
...config,
|
|
topBar: {
|
|
pinnedKeys: [],
|
|
keys: [...defaultPreset.topBar.keys],
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
}, [config, saveConfig]);
|
|
|
|
const resetFullKeyboard = useCallback(async () => {
|
|
const defaultPreset = getPresetById(
|
|
config.preset === "custom" ? DEFAULT_PRESET_ID : config.preset,
|
|
);
|
|
if (!defaultPreset) return;
|
|
|
|
const newConfig = {
|
|
...config,
|
|
fullKeyboard: {
|
|
rows: defaultPreset.fullKeyboard.rows.map((row) => ({
|
|
...row,
|
|
keys: [...row.keys],
|
|
})),
|
|
},
|
|
};
|
|
await saveConfig(newConfig);
|
|
}, [config, saveConfig]);
|
|
|
|
const exportConfig = useCallback(() => {
|
|
return JSON.stringify(config, null, 2);
|
|
}, [config]);
|
|
|
|
const importConfig = useCallback(
|
|
async (jsonString: string) => {
|
|
try {
|
|
const imported = JSON.parse(jsonString) as KeyboardCustomization;
|
|
|
|
if (!imported.topBar || !imported.fullKeyboard || !imported.settings) {
|
|
throw new Error("Invalid configuration structure");
|
|
}
|
|
|
|
await saveConfig(imported);
|
|
} catch (error) {
|
|
console.error("Failed to import configuration:", error);
|
|
throw error;
|
|
}
|
|
},
|
|
[saveConfig],
|
|
);
|
|
|
|
const value: KeyboardCustomizationContextType = {
|
|
config,
|
|
isLoading,
|
|
setPreset,
|
|
addPinnedKey,
|
|
removePinnedKey,
|
|
reorderPinnedKeys,
|
|
addTopBarKey,
|
|
removeTopBarKey,
|
|
reorderTopBarKeys,
|
|
addRow,
|
|
removeRow,
|
|
reorderRows,
|
|
updateRow,
|
|
toggleRowVisibility,
|
|
addKeyToRow,
|
|
removeKeyFromRow,
|
|
reorderKeysInRow,
|
|
updateSettings,
|
|
resetToDefault,
|
|
resetTopBar,
|
|
resetFullKeyboard,
|
|
exportConfig,
|
|
importConfig,
|
|
};
|
|
|
|
return (
|
|
<KeyboardCustomizationContext.Provider value={value}>
|
|
{children}
|
|
</KeyboardCustomizationContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useKeyboardCustomization = () => {
|
|
const context = useContext(KeyboardCustomizationContext);
|
|
if (!context) {
|
|
throw new Error(
|
|
"useKeyboardCustomization must be used within a KeyboardCustomizationProvider",
|
|
);
|
|
}
|
|
return context;
|
|
};
|