mirror of
https://github.com/MeowLynxSea/vissh.git
synced 2025-07-09 19:44:34 +00:00
完成了终端应用
This commit is contained in:
parent
b561a7f612
commit
33d30aacc8
@ -1,4 +1,5 @@
|
|||||||
# vissh
|
# vissh
|
||||||
|
|
||||||
SSH-based visual Linux administration tool
|
SSH-based visual Linux administration tool
|
||||||
|
|
||||||
Waiting for commit
|
Waiting for commit
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
analyzer:
|
analyzer:
|
||||||
errors:
|
errors:
|
||||||
use_build_context_synchronously: ignore
|
use_build_context_synchronously: ignore
|
||||||
|
library_private_types_in_public_api: ignore
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="vissh"
|
android:label="vissh"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@ -11,6 +13,7 @@
|
|||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'widgets/draggable_window.dart';
|
import 'package:vissh/pages/terminal_page.dart';
|
||||||
import 'models/window_data.dart';
|
import 'package:vissh/widgets/draggable_window.dart';
|
||||||
import 'widgets/taskbar.dart';
|
import 'package:vissh/models/window_data.dart';
|
||||||
import 'pages/login_page.dart';
|
import 'package:vissh/widgets/taskbar.dart';
|
||||||
|
import 'package:vissh/pages/login_page.dart';
|
||||||
|
import 'package:vissh/models/credentials.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
@ -23,8 +26,13 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
class WindowManager extends StatefulWidget {
|
class WindowManager extends StatefulWidget {
|
||||||
final SSHClient sshClient;
|
final SSHClient sshClient;
|
||||||
final String host;
|
final SSHCredentials credentials;
|
||||||
const WindowManager({super.key, required this.sshClient, required this.host});
|
|
||||||
|
const WindowManager({
|
||||||
|
super.key,
|
||||||
|
required this.sshClient,
|
||||||
|
required this.credentials,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<WindowManager> createState() => _WindowManagerState();
|
State<WindowManager> createState() => _WindowManagerState();
|
||||||
@ -40,6 +48,7 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom]);
|
||||||
super.initState();
|
super.initState();
|
||||||
_verifyConnection();
|
_verifyConnection();
|
||||||
}
|
}
|
||||||
@ -65,7 +74,7 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
_addWindow(
|
_addWindow(
|
||||||
'File Explorer',
|
'File Explorer',
|
||||||
const Offset(100, 100),
|
const Offset(100, 100),
|
||||||
const Center(
|
(isActive, onSessionEnd) => const Center(
|
||||||
child: Text('View your files...', style: TextStyle(color: Colors.white)),
|
child: Text('View your files...', style: TextStyle(color: Colors.white)),
|
||||||
),
|
),
|
||||||
Icons.folder_open,
|
Icons.folder_open,
|
||||||
@ -73,15 +82,17 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
_addWindow(
|
_addWindow(
|
||||||
'Terminal',
|
'Terminal',
|
||||||
const Offset(150, 150),
|
const Offset(150, 150),
|
||||||
Center(
|
(isActive, onSessionEnd) => TerminalPage(
|
||||||
child: Text('SSH connected to: ${widget.host}',
|
credentials: widget.credentials,
|
||||||
style: const TextStyle(color: Colors.white)),
|
isActive: isActive,
|
||||||
|
onSessionEnd: onSessionEnd,
|
||||||
),
|
),
|
||||||
Icons.web,
|
Icons.terminal,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addWindow(String title, Offset position, Widget child, IconData icon) {
|
void _addWindow(
|
||||||
|
String title, Offset position, Widget Function(bool, VoidCallback) child, IconData icon) {
|
||||||
setState(() {
|
setState(() {
|
||||||
final id = 'window_${_nextWindowId++}';
|
final id = 'window_${_nextWindowId++}';
|
||||||
_windows.add(
|
_windows.add(
|
||||||
@ -89,7 +100,7 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: title,
|
||||||
position: position,
|
position: position,
|
||||||
size: const Size(400, 300),
|
size: const Size(700, 500),
|
||||||
child: child,
|
child: child,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
),
|
),
|
||||||
@ -105,7 +116,6 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
if (window.isMinimized) {
|
if (window.isMinimized) {
|
||||||
window.isMinimized = false;
|
window.isMinimized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (windowIndex != _windows.length - 1) {
|
if (windowIndex != _windows.length - 1) {
|
||||||
final window = _windows.removeAt(windowIndex);
|
final window = _windows.removeAt(windowIndex);
|
||||||
_windows.add(window);
|
_windows.add(window);
|
||||||
@ -180,13 +190,11 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!_isVerified) {
|
if (!_isVerified) {
|
||||||
// --- Win11 风格的验证界面 ---
|
|
||||||
bool hasError = _verificationFailedMessage.isNotEmpty;
|
bool hasError = _verificationFailedMessage.isNotEmpty;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xff0078D4), // Win11 蓝色背景
|
backgroundColor: const Color(0xff0078D4),
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: NetworkImage('https://www.meowdream.cn/background.jpg'),
|
image: NetworkImage('https://www.meowdream.cn/background.jpg'),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -197,9 +205,8 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 120),
|
const SizedBox(height: 120),
|
||||||
|
|
||||||
hasError
|
hasError
|
||||||
? Icon(Icons.error_outline, color: Colors.redAccent, size: 48)
|
? const Icon(Icons.error_outline, color: Colors.redAccent, size: 48)
|
||||||
: SizedBox(
|
: SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
@ -209,7 +216,6 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
_verificationMessage,
|
_verificationMessage,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@ -219,7 +225,6 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
if (hasError)
|
if (hasError)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||||
@ -232,10 +237,7 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (hasError) const SizedBox(height: 16),
|
||||||
if (hasError)
|
|
||||||
SizedBox(height: 16),
|
|
||||||
|
|
||||||
if (hasError)
|
if (hasError)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -249,7 +251,7 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'返回',
|
'Back',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@ -271,8 +273,7 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final topMostIndex = _windows.lastIndexWhere((w) => !w.isMinimized);
|
final topMostIndex = _windows.lastIndexWhere((w) => !w.isMinimized);
|
||||||
final activeWindowId =
|
final activeWindowId = topMostIndex != -1 ? _windows[topMostIndex].id : null;
|
||||||
topMostIndex != -1 ? _windows[topMostIndex].id : null;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.blueGrey[900],
|
backgroundColor: Colors.blueGrey[900],
|
||||||
@ -322,20 +323,21 @@ class _WindowManagerState extends State<WindowManager> {
|
|||||||
children: [
|
children: [
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onPressed: () => _addWindow(
|
onPressed: () => _addWindow(
|
||||||
'New Window',
|
'Terminal',
|
||||||
const Offset(200, 200),
|
const Offset(150, 150),
|
||||||
const Center(
|
(isActive, onSessionEnd) => TerminalPage(
|
||||||
child: Text('This is the new window content area',
|
credentials: widget.credentials,
|
||||||
style: TextStyle(color: Colors.white))
|
isActive: isActive,
|
||||||
|
onSessionEnd: onSessionEnd,
|
||||||
),
|
),
|
||||||
Icons.add_circle_outline,
|
Icons.terminal,
|
||||||
),
|
),
|
||||||
tooltip: 'Add New Window',
|
tooltip: 'Add New Window',
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
11
lib/models/credentials.dart
Normal file
11
lib/models/credentials.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
class SSHCredentials {
|
||||||
|
final String host;
|
||||||
|
final String username;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
SSHCredentials({
|
||||||
|
required this.host,
|
||||||
|
required this.username,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
}
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
class WindowData {
|
class WindowData {
|
||||||
final String id;
|
final String id;
|
||||||
final String title;
|
final String title;
|
||||||
final Widget child;
|
final Widget Function(bool isActive, VoidCallback onSessionEnd) child;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
Offset position;
|
Offset position;
|
||||||
Size size;
|
Size size;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:dartssh2/dartssh2.dart';
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
|
import '../models/credentials.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({super.key});
|
const LoginPage({super.key});
|
||||||
@ -27,28 +28,41 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
onPasswordRequest: () => _passwordController.text,
|
onPasswordRequest: () => _passwordController.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ignore: use_build_context_synchronously
|
final credentials = SSHCredentials(
|
||||||
|
host: _hostController.text,
|
||||||
|
username: _usernameController.text,
|
||||||
|
password: _passwordController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => WindowManager(
|
||||||
WindowManager(sshClient: client, host: _hostController.text)),
|
sshClient: client,
|
||||||
|
credentials: credentials,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore: use_build_context_synchronously
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Login Failed: $e')),
|
SnackBar(content: Text('Login Failed: $e')),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_isLoading = false;
|
setState(() {
|
||||||
});
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xff0078D4),
|
||||||
body: Container(
|
body: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
@ -58,10 +72,10 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.fromLTRB(40, 20, 40, 20),
|
||||||
width: 300,
|
width: 300,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withValues(alpha: 0.7),
|
color: Colors.black.withValues(alpha: 0.4),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
168
lib/pages/terminal_page.dart
Normal file
168
lib/pages/terminal_page.dart
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:xterm/xterm.dart';
|
||||||
|
import '../models/credentials.dart';
|
||||||
|
|
||||||
|
class TerminalPage extends StatefulWidget {
|
||||||
|
final SSHCredentials credentials;
|
||||||
|
final bool isActive;
|
||||||
|
final VoidCallback onSessionEnd;
|
||||||
|
|
||||||
|
const TerminalPage({
|
||||||
|
super.key,
|
||||||
|
required this.credentials,
|
||||||
|
required this.isActive,
|
||||||
|
required this.onSessionEnd,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TerminalPageState createState() => _TerminalPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TerminalPageState extends State<TerminalPage> {
|
||||||
|
final Terminal terminal = Terminal(maxLines: 10000);
|
||||||
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
SSHClient? client;
|
||||||
|
SSHSession? shell;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_startSshShell();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted && widget.isActive) {
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant TerminalPage oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.isActive && !oldWidget.isActive) {
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
} else if (!widget.isActive && oldWidget.isActive) {
|
||||||
|
_focusNode.unfocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startSshShell() async {
|
||||||
|
try {
|
||||||
|
client = SSHClient(
|
||||||
|
await SSHSocket.connect(widget.credentials.host, 22),
|
||||||
|
username: widget.credentials.username,
|
||||||
|
onPasswordRequest: () => widget.credentials.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
shell = await client!.shell(
|
||||||
|
pty: SSHPtyConfig(
|
||||||
|
width: terminal.viewWidth,
|
||||||
|
height: terminal.viewHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
terminal.onOutput = (data) {
|
||||||
|
shell?.write(utf8.encode(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
terminal.onResize = (width, height, pixelWidth, pixelHeight) {
|
||||||
|
shell?.resizeTerminal(width, height);
|
||||||
|
};
|
||||||
|
|
||||||
|
shell?.stdout.listen((data) {
|
||||||
|
terminal.write(utf8.decode(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
shell?.stderr.listen((data) {
|
||||||
|
terminal.write(utf8.decode(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
await shell?.done;
|
||||||
|
if (mounted) {
|
||||||
|
widget.onSessionEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
terminal.write('Error: $e');
|
||||||
|
widget.onSessionEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalKey? _mapLogicalKey(LogicalKeyboardKey key) {
|
||||||
|
final keyMap = {
|
||||||
|
LogicalKeyboardKey.enter: TerminalKey.enter,
|
||||||
|
LogicalKeyboardKey.backspace: TerminalKey.backspace,
|
||||||
|
LogicalKeyboardKey.arrowUp: TerminalKey.arrowUp,
|
||||||
|
LogicalKeyboardKey.arrowDown: TerminalKey.arrowDown,
|
||||||
|
LogicalKeyboardKey.arrowLeft: TerminalKey.arrowLeft,
|
||||||
|
LogicalKeyboardKey.arrowRight: TerminalKey.arrowRight,
|
||||||
|
LogicalKeyboardKey.tab: TerminalKey.tab,
|
||||||
|
LogicalKeyboardKey.escape: TerminalKey.escape,
|
||||||
|
LogicalKeyboardKey.delete: TerminalKey.delete,
|
||||||
|
LogicalKeyboardKey.home: TerminalKey.home,
|
||||||
|
LogicalKeyboardKey.end: TerminalKey.end,
|
||||||
|
LogicalKeyboardKey.pageUp: TerminalKey.pageUp,
|
||||||
|
LogicalKeyboardKey.pageDown: TerminalKey.pageDown,
|
||||||
|
LogicalKeyboardKey.insert: TerminalKey.insert,
|
||||||
|
LogicalKeyboardKey.f1: TerminalKey.f1,
|
||||||
|
LogicalKeyboardKey.f2: TerminalKey.f2,
|
||||||
|
LogicalKeyboardKey.f3: TerminalKey.f3,
|
||||||
|
LogicalKeyboardKey.f4: TerminalKey.f4,
|
||||||
|
LogicalKeyboardKey.f5: TerminalKey.f5,
|
||||||
|
LogicalKeyboardKey.f6: TerminalKey.f6,
|
||||||
|
LogicalKeyboardKey.f7: TerminalKey.f7,
|
||||||
|
LogicalKeyboardKey.f8: TerminalKey.f8,
|
||||||
|
LogicalKeyboardKey.f9: TerminalKey.f9,
|
||||||
|
LogicalKeyboardKey.f10: TerminalKey.f10,
|
||||||
|
LogicalKeyboardKey.f11: TerminalKey.f11,
|
||||||
|
LogicalKeyboardKey.f12: TerminalKey.f12,
|
||||||
|
};
|
||||||
|
return keyMap[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Focus(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
onKeyEvent: (node, event) {
|
||||||
|
if (event is! KeyDownEvent) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final terminalKey = _mapLogicalKey(event.logicalKey);
|
||||||
|
|
||||||
|
if (terminalKey != null) {
|
||||||
|
terminal.keyInput(
|
||||||
|
terminalKey,
|
||||||
|
ctrl: HardwareKeyboard.instance.isControlPressed,
|
||||||
|
shift: HardwareKeyboard.instance.isShiftPressed,
|
||||||
|
alt: HardwareKeyboard.instance.isAltPressed,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
} else if (event.character != null && event.character!.isNotEmpty) {
|
||||||
|
terminal.textInput(event.character!);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
},
|
||||||
|
child: TerminalView(
|
||||||
|
terminal,
|
||||||
|
autofocus: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode.dispose();
|
||||||
|
shell?.close();
|
||||||
|
client?.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ class DraggableWindow extends StatefulWidget {
|
|||||||
final Offset initialPosition;
|
final Offset initialPosition;
|
||||||
final Size initialSize;
|
final Size initialSize;
|
||||||
final String title;
|
final String title;
|
||||||
final Widget child;
|
final Widget Function(bool isActive, VoidCallback onSessionEnd) child;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final bool isMaximized;
|
final bool isMaximized;
|
||||||
@ -152,7 +152,7 @@ class _DraggableWindowState extends State<DraggableWindow> {
|
|||||||
_top = 0;
|
_top = 0;
|
||||||
_left = 0;
|
_left = 0;
|
||||||
_width = screenSize.width;
|
_width = screenSize.width;
|
||||||
_height = screenSize.height - 40;
|
_height = screenSize.height - 48;
|
||||||
_isMaximized = true;
|
_isMaximized = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -227,7 +227,11 @@ class _DraggableWindowState extends State<DraggableWindow> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildTitleBar(),
|
_buildTitleBar(),
|
||||||
Expanded(child: widget.child),
|
Expanded(
|
||||||
|
child: ClipRect(
|
||||||
|
child: widget.child(widget.isActive, () => widget.onClose(widget.id)),
|
||||||
|
)
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -12,6 +12,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
dartssh2: ^2.13.0
|
dartssh2: ^2.13.0
|
||||||
|
xterm: ^4.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user