init ssh
This commit is contained in:
@@ -14,13 +14,16 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
A Flutter project which provide charts to display server status and tools to manage server.<br>Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a>.
|
A Flutter project which provide charts to display server status and tools to manage server.
|
||||||
|
<br>
|
||||||
|
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## 🔖 Milestone
|
## 🔖 Feature
|
||||||
- [x] Status chart view
|
- [x] Status chart view
|
||||||
- [x] Snippet ~~market~~, Ping, SFTP, Docker, Apt/Yum and etc.
|
- [x] SSH terminal
|
||||||
|
- [x] `Docker Manage`, `Pkg Manage`, `SFTP`, `Snippet` ~~market~~, `Ping` and etc.
|
||||||
- [x] i18n (English, Chinese)
|
- [x] i18n (English, Chinese)
|
||||||
- [x] Desktop support
|
- [x] Desktop support
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,12 @@ class DropdownBtnItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ServerTabMenuItems {
|
class ServerTabMenuItems {
|
||||||
static const List<DropdownBtnItem> firstItems = [sftp, pkg, docker];
|
static const List<DropdownBtnItem> firstItems = [sftp, snippet, pkg, docker];
|
||||||
static const List<DropdownBtnItem> secondItems = [edit];
|
static const List<DropdownBtnItem> secondItems = [edit];
|
||||||
|
|
||||||
static const sftp =
|
static const sftp =
|
||||||
DropdownBtnItem(text: 'SFTP', icon: Icons.insert_drive_file);
|
DropdownBtnItem(text: 'SFTP', icon: Icons.insert_drive_file);
|
||||||
|
static const snippet = DropdownBtnItem(text: 'Snippet', icon: Icons.code);
|
||||||
static const pkg =
|
static const pkg =
|
||||||
DropdownBtnItem(text: 'Pkg', icon: Icons.system_security_update);
|
DropdownBtnItem(text: 'Pkg', icon: Icons.system_security_update);
|
||||||
static const docker =
|
static const docker =
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
class BuildData {
|
class BuildData {
|
||||||
static const String name = "ServerBox";
|
static const String name = "ServerBox";
|
||||||
static const int build = 183;
|
static const int build = 186;
|
||||||
static const String engine =
|
static const String engine =
|
||||||
"Flutter 3.7.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision b06b8b2710 (4 days ago) • 2023-01-23 16:55:55 -0800\nEngine • revision b24591ed32\nTools • Dart 2.19.0 • DevTools 2.20.1\n";
|
"Flutter 3.7.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision b06b8b2710 (4 days ago) • 2023-01-23 16:55:55 -0800\nEngine • revision b24591ed32\nTools • Dart 2.19.0 • DevTools 2.20.1\n";
|
||||||
static const String buildAt = "2023-01-27 21:38:08.181334";
|
static const String buildAt = "2023-01-28 00:10:21.021365";
|
||||||
static const int modifications = 19;
|
static const int modifications = 8;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import 'package:toolbox/view/page/server/detail.dart';
|
|||||||
import 'package:toolbox/view/page/server/edit.dart';
|
import 'package:toolbox/view/page/server/edit.dart';
|
||||||
import 'package:toolbox/view/page/sftp/view.dart';
|
import 'package:toolbox/view/page/sftp/view.dart';
|
||||||
import 'package:toolbox/view/page/snippet/edit.dart';
|
import 'package:toolbox/view/page/snippet/edit.dart';
|
||||||
|
import 'package:toolbox/view/page/ssh.dart';
|
||||||
import 'package:toolbox/view/widget/picker.dart';
|
import 'package:toolbox/view/widget/picker.dart';
|
||||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||||
|
|
||||||
@@ -160,8 +161,8 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
context, _s.error, Text(ss.failedInfo ?? ''), []),
|
context, _s.error, Text(ss.failedInfo ?? ''), []),
|
||||||
child: Text(_s.clickSee, style: style))
|
child: Text(_s.clickSee, style: style))
|
||||||
: Text(topRightStr, style: style, textScaleFactor: 1.0),
|
: Text(topRightStr, style: style, textScaleFactor: 1.0),
|
||||||
const SizedBox(width: 7),
|
const SizedBox(width: 9),
|
||||||
_buildSnippetBtn(spi),
|
_buildSSHBtn(spi),
|
||||||
_buildMoreBtn(spi),
|
_buildMoreBtn(spi),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -196,60 +197,13 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSnippetBtn(ServerPrivateInfo spi) {
|
Widget _buildSSHBtn(ServerPrivateInfo spi) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: const Icon(Icons.play_arrow),
|
child: const Icon(
|
||||||
onTap: () {
|
Icons.terminal,
|
||||||
final provider = locator<SnippetProvider>();
|
size: 21,
|
||||||
if (provider.snippets.isEmpty) {
|
),
|
||||||
showRoundDialog(
|
onTap: () => AppRoute(SSHPage(spi: spi), 'ssh page').go(context),
|
||||||
context,
|
|
||||||
_s.attention,
|
|
||||||
Text(_s.noSavedSnippet),
|
|
||||||
[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(_s.ok),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () =>
|
|
||||||
AppRoute(const SnippetEditPage(), 'edit snippet')
|
|
||||||
.go(context),
|
|
||||||
child: Text(_s.addOne),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var snippet = provider.snippets.first;
|
|
||||||
showRoundDialog(
|
|
||||||
context,
|
|
||||||
_s.choose,
|
|
||||||
buildPicker(provider.snippets.map((e) => Text(e.name)).toList(),
|
|
||||||
(idx) => snippet = provider.snippets[idx]),
|
|
||||||
[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
final result =
|
|
||||||
await locator<ServerProvider>().runSnippet(spi.id, snippet);
|
|
||||||
showRoundDialog(
|
|
||||||
context,
|
|
||||||
_s.result,
|
|
||||||
Text(result ?? _s.error, style: textSize13),
|
|
||||||
[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text(_s.ok))
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Text(_s.run),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,14 +225,16 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
final item = value as DropdownBtnItem;
|
switch (value as DropdownBtnItem) {
|
||||||
switch (item) {
|
|
||||||
case ServerTabMenuItems.pkg:
|
case ServerTabMenuItems.pkg:
|
||||||
AppRoute(PkgManagePage(spi), 'pkg manage').go(context);
|
AppRoute(PkgManagePage(spi), 'pkg manage').go(context);
|
||||||
break;
|
break;
|
||||||
case ServerTabMenuItems.sftp:
|
case ServerTabMenuItems.sftp:
|
||||||
AppRoute(SFTPPage(spi), 'SFTP').go(context);
|
AppRoute(SFTPPage(spi), 'SFTP').go(context);
|
||||||
break;
|
break;
|
||||||
|
case ServerTabMenuItems.snippet:
|
||||||
|
_showSnippetDialog(spi.id);
|
||||||
|
break;
|
||||||
case ServerTabMenuItems.edit:
|
case ServerTabMenuItems.edit:
|
||||||
AppRoute(ServerEditPage(spi: spi), 'Edit server info').go(context);
|
AppRoute(ServerEditPage(spi: spi), 'Edit server info').go(context);
|
||||||
break;
|
break;
|
||||||
@@ -393,6 +349,57 @@ class _ServerPageState extends State<ServerPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showSnippetDialog(String id) {
|
||||||
|
final provider = locator<SnippetProvider>();
|
||||||
|
if (provider.snippets.isEmpty) {
|
||||||
|
showRoundDialog(
|
||||||
|
context,
|
||||||
|
_s.attention,
|
||||||
|
Text(_s.noSavedSnippet),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(_s.ok),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () =>
|
||||||
|
AppRoute(const SnippetEditPage(), 'edit snippet').go(context),
|
||||||
|
child: Text(_s.addOne),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var snippet = provider.snippets.first;
|
||||||
|
showRoundDialog(
|
||||||
|
context,
|
||||||
|
_s.choose,
|
||||||
|
buildPicker(provider.snippets.map((e) => Text(e.name)).toList(),
|
||||||
|
(idx) => snippet = provider.snippets[idx]),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
final result =
|
||||||
|
await locator<ServerProvider>().runSnippet(id, snippet);
|
||||||
|
showRoundDialog(
|
||||||
|
context,
|
||||||
|
_s.result,
|
||||||
|
Text(result ?? _s.error, style: textSize13),
|
||||||
|
[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(_s.ok))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(_s.run),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,22 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:toolbox/core/route.dart';
|
import 'package:toolbox/core/route.dart';
|
||||||
import 'package:toolbox/core/utils.dart';
|
|
||||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
|
||||||
import 'package:toolbox/data/model/server/snippet.dart';
|
|
||||||
import 'package:toolbox/data/provider/server.dart';
|
|
||||||
import 'package:toolbox/data/provider/snippet.dart';
|
import 'package:toolbox/data/provider/snippet.dart';
|
||||||
import 'package:toolbox/data/res/color.dart';
|
import 'package:toolbox/data/res/color.dart';
|
||||||
import 'package:toolbox/data/res/font_style.dart';
|
import 'package:toolbox/data/res/font_style.dart';
|
||||||
import 'package:toolbox/data/res/padding.dart';
|
import 'package:toolbox/data/res/padding.dart';
|
||||||
import 'package:toolbox/generated/l10n.dart';
|
import 'package:toolbox/generated/l10n.dart';
|
||||||
import 'package:toolbox/locator.dart';
|
|
||||||
import 'package:toolbox/view/page/snippet/edit.dart';
|
import 'package:toolbox/view/page/snippet/edit.dart';
|
||||||
import 'package:toolbox/view/widget/picker.dart';
|
|
||||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||||
|
|
||||||
class SnippetListPage extends StatefulWidget {
|
class SnippetListPage extends StatefulWidget {
|
||||||
const SnippetListPage({Key? key, this.spi}) : super(key: key);
|
const SnippetListPage({Key? key}) : super(key: key);
|
||||||
final ServerPrivateInfo? spi;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_SnippetListPageState createState() => _SnippetListPageState();
|
_SnippetListPageState createState() => _SnippetListPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SnippetListPageState extends State<SnippetListPage> {
|
class _SnippetListPageState extends State<SnippetListPage> {
|
||||||
late ServerPrivateInfo _selectedSpi;
|
|
||||||
|
|
||||||
final _textStyle = TextStyle(color: primaryColor);
|
final _textStyle = TextStyle(color: primaryColor);
|
||||||
|
|
||||||
late S _s;
|
late S _s;
|
||||||
@@ -54,116 +45,45 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
|||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
return Consumer<SnippetProvider>(
|
return Consumer<SnippetProvider>(
|
||||||
builder: (_, key, __) {
|
builder: (_, key, __) {
|
||||||
return key.snippets.isNotEmpty
|
if (key.snippets.isEmpty) {
|
||||||
? ListView.builder(
|
return Center(
|
||||||
padding: const EdgeInsets.all(13),
|
child: Text(_s.noSavedSnippet),
|
||||||
itemCount: key.snippets.length,
|
);
|
||||||
itemExtent: 57,
|
}
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
return RoundRectCard(
|
return ListView.builder(
|
||||||
Padding(
|
padding: const EdgeInsets.all(13),
|
||||||
padding: roundRectCardPadding,
|
itemCount: key.snippets.length,
|
||||||
child: Row(
|
itemExtent: 57,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
itemBuilder: (context, idx) {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
return RoundRectCard(
|
||||||
children: [
|
Padding(
|
||||||
Text(
|
padding: roundRectCardPadding,
|
||||||
key.snippets[idx].name,
|
child: Row(
|
||||||
textAlign: TextAlign.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Row(
|
children: [
|
||||||
children: [
|
Text(
|
||||||
TextButton(
|
key.snippets[idx].name,
|
||||||
onPressed: () => AppRoute(
|
textAlign: TextAlign.center,
|
||||||
SnippetEditPage(
|
),
|
||||||
snippet: key.snippets[idx]),
|
TextButton(
|
||||||
'snippet edit page')
|
onPressed: () => AppRoute(
|
||||||
.go(context),
|
SnippetEditPage(snippet: key.snippets[idx]),
|
||||||
child: Text(
|
'snippet edit page')
|
||||||
_s.edit,
|
.go(context),
|
||||||
style: _textStyle,
|
child: Text(
|
||||||
),
|
_s.edit,
|
||||||
),
|
style: _textStyle,
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
final snippet = key.snippets[idx];
|
|
||||||
if (widget.spi == null) {
|
|
||||||
_showRunDialog(snippet);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
run(context, snippet);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
_s.run,
|
|
||||||
style: _textStyle,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
)
|
),
|
||||||
: Center(
|
);
|
||||||
child: Text(_s.noSavedSnippet),
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showRunDialog(Snippet snippet) {
|
|
||||||
showRoundDialog(
|
|
||||||
context,
|
|
||||||
_s.chooseDestination,
|
|
||||||
Consumer<ServerProvider>(
|
|
||||||
builder: (_, provider, __) {
|
|
||||||
if (provider.servers.isEmpty) {
|
|
||||||
return Text(_s.noServerAvailable);
|
|
||||||
}
|
|
||||||
_selectedSpi = provider.servers.first.info;
|
|
||||||
return buildPicker(
|
|
||||||
provider.servers
|
|
||||||
.map((e) => Text(
|
|
||||||
e.info.name,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
(idx) => _selectedSpi = provider.servers[idx].info);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
run(context, snippet);
|
|
||||||
},
|
|
||||||
child: Text(_s.run),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text(_s.cancel),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> run(BuildContext context, Snippet snippet) async {
|
|
||||||
final id = (widget.spi ?? _selectedSpi).id;
|
|
||||||
final result = await locator<ServerProvider>().runSnippet(id, snippet);
|
|
||||||
if (result != null) {
|
|
||||||
showRoundDialog(
|
|
||||||
context,
|
|
||||||
_s.result,
|
|
||||||
Text(result, style: const TextStyle(fontSize: 13)),
|
|
||||||
[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text(_s.close),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
100
lib/view/page/ssh.dart
Normal file
100
lib/view/page/ssh.dart
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
|
import '../../data/model/server/server_private_info.dart';
|
||||||
|
import '../../data/provider/server.dart';
|
||||||
|
import '../../locator.dart';
|
||||||
|
import '../widget/virtual_keyboard.dart';
|
||||||
|
|
||||||
|
class SSHPage extends StatefulWidget {
|
||||||
|
final ServerPrivateInfo spi;
|
||||||
|
const SSHPage({Key? key, required this.spi}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SSHPageState createState() => _SSHPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SSHPageState extends State<SSHPage> {
|
||||||
|
late final terminal = Terminal(inputHandler: keyboard);
|
||||||
|
|
||||||
|
final keyboard = VirtualKeyboard(defaultInputHandler);
|
||||||
|
|
||||||
|
var title = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
initTerminal();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initTerminal() async {
|
||||||
|
terminal.write('Connecting...\r\n');
|
||||||
|
|
||||||
|
final client = locator<ServerProvider>()
|
||||||
|
.servers
|
||||||
|
.where((e) => e.info.id == widget.spi.id)
|
||||||
|
.first
|
||||||
|
.client;
|
||||||
|
if (client == null) {
|
||||||
|
terminal.write('Failed to connect\r\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.write('Connected\r\n');
|
||||||
|
|
||||||
|
final session = await client.shell(
|
||||||
|
pty: SSHPtyConfig(
|
||||||
|
width: terminal.viewWidth,
|
||||||
|
height: terminal.viewHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
terminal.buffer.clear();
|
||||||
|
terminal.buffer.setCursor(0, 0);
|
||||||
|
|
||||||
|
terminal.onTitleChange = (title) {
|
||||||
|
setState(() => this.title = title);
|
||||||
|
};
|
||||||
|
|
||||||
|
terminal.onResize = (width, height, pixelWidth, pixelHeight) {
|
||||||
|
session.resizeTerminal(width, height, pixelWidth, pixelHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
terminal.onOutput = (data) {
|
||||||
|
session.write(utf8.encode(data) as Uint8List);
|
||||||
|
};
|
||||||
|
|
||||||
|
session.stdout
|
||||||
|
.cast<List<int>>()
|
||||||
|
.transform(const Utf8Decoder())
|
||||||
|
.listen(terminal.write);
|
||||||
|
|
||||||
|
session.stderr
|
||||||
|
.cast<List<int>>()
|
||||||
|
.transform(const Utf8Decoder())
|
||||||
|
.listen(terminal.write);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).appBarTheme.backgroundColor?.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TerminalView(terminal, keyboardType: TextInputType.none),
|
||||||
|
),
|
||||||
|
VirtualKeyboardView(keyboard),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/view/widget/virtual_keyboard.dart
Normal file
80
lib/view/widget/virtual_keyboard.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:xterm/xterm.dart';
|
||||||
|
|
||||||
|
class VirtualKeyboardView extends StatelessWidget {
|
||||||
|
const VirtualKeyboardView(this.keyboard, {super.key});
|
||||||
|
|
||||||
|
final VirtualKeyboard keyboard;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: keyboard,
|
||||||
|
builder: (context, child) => ToggleButtons(
|
||||||
|
isSelected: [keyboard.ctrl, keyboard.alt, keyboard.shift],
|
||||||
|
onPressed: (index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
keyboard.ctrl = !keyboard.ctrl;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
keyboard.alt = !keyboard.alt;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
keyboard.shift = !keyboard.shift;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: const [Text('Ctrl'), Text('Alt'), Text('Shift')],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VirtualKeyboard extends TerminalInputHandler with ChangeNotifier {
|
||||||
|
final TerminalInputHandler _inputHandler;
|
||||||
|
|
||||||
|
VirtualKeyboard(this._inputHandler);
|
||||||
|
|
||||||
|
bool _ctrl = false;
|
||||||
|
|
||||||
|
bool get ctrl => _ctrl;
|
||||||
|
|
||||||
|
set ctrl(bool value) {
|
||||||
|
if (_ctrl != value) {
|
||||||
|
_ctrl = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _shift = false;
|
||||||
|
|
||||||
|
bool get shift => _shift;
|
||||||
|
|
||||||
|
set shift(bool value) {
|
||||||
|
if (_shift != value) {
|
||||||
|
_shift = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _alt = false;
|
||||||
|
|
||||||
|
bool get alt => _alt;
|
||||||
|
|
||||||
|
set alt(bool value) {
|
||||||
|
if (_alt != value) {
|
||||||
|
_alt = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? call(TerminalKeyboardEvent event) {
|
||||||
|
return _inputHandler.call(event.copyWith(
|
||||||
|
ctrl: event.ctrl || _ctrl,
|
||||||
|
shift: event.shift || _shift,
|
||||||
|
alt: event.alt || _alt,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
32
pubspec.lock
32
pubspec.lock
@@ -250,6 +250,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.5"
|
||||||
extended_image:
|
extended_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -638,6 +646,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
|
platform_info:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform_info
|
||||||
|
sha256: "012e73712166cf0b56d3eb95c0d33491f56b428c169eca385f036448474147e4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.0"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -694,6 +710,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.1"
|
||||||
|
quiver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quiver
|
||||||
|
sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
r_upgrade:
|
r_upgrade:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -963,6 +987,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.2"
|
version: "6.2.2"
|
||||||
|
xterm:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: xterm
|
||||||
|
sha256: f65619cb24d03507812e346ddb8386cad9e16a01a481a8f5c8a2eba55b4edada
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ dependencies:
|
|||||||
share_plus: ^6.3.0
|
share_plus: ^6.3.0
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
share_plus_web: ^3.1.0
|
share_plus_web: ^3.1.0
|
||||||
|
xterm: ^3.4.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_native_splash: ^2.1.6
|
flutter_native_splash: ^2.1.6
|
||||||
|
|||||||
Reference in New Issue
Block a user