mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-31 05:14:56 +01:00
init ssh
This commit is contained in:
@@ -14,13 +14,16 @@
|
||||
</p>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
## 🔖 Milestone
|
||||
## 🔖 Feature
|
||||
- [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] Desktop support
|
||||
|
||||
|
||||
@@ -24,11 +24,12 @@ class DropdownBtnItem {
|
||||
}
|
||||
|
||||
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 sftp =
|
||||
DropdownBtnItem(text: 'SFTP', icon: Icons.insert_drive_file);
|
||||
static const snippet = DropdownBtnItem(text: 'Snippet', icon: Icons.code);
|
||||
static const pkg =
|
||||
DropdownBtnItem(text: 'Pkg', icon: Icons.system_security_update);
|
||||
static const docker =
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 183;
|
||||
static const int build = 186;
|
||||
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";
|
||||
static const String buildAt = "2023-01-27 21:38:08.181334";
|
||||
static const int modifications = 19;
|
||||
static const String buildAt = "2023-01-28 00:10:21.021365";
|
||||
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/sftp/view.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/round_rect_card.dart';
|
||||
|
||||
@@ -160,8 +161,8 @@ class _ServerPageState extends State<ServerPage>
|
||||
context, _s.error, Text(ss.failedInfo ?? ''), []),
|
||||
child: Text(_s.clickSee, style: style))
|
||||
: Text(topRightStr, style: style, textScaleFactor: 1.0),
|
||||
const SizedBox(width: 7),
|
||||
_buildSnippetBtn(spi),
|
||||
const SizedBox(width: 9),
|
||||
_buildSSHBtn(spi),
|
||||
_buildMoreBtn(spi),
|
||||
],
|
||||
)
|
||||
@@ -196,60 +197,13 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSnippetBtn(ServerPrivateInfo spi) {
|
||||
Widget _buildSSHBtn(ServerPrivateInfo spi) {
|
||||
return GestureDetector(
|
||||
child: const Icon(Icons.play_arrow),
|
||||
onTap: () {
|
||||
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(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),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.terminal,
|
||||
size: 21,
|
||||
),
|
||||
onTap: () => AppRoute(SSHPage(spi: spi), 'ssh page').go(context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -271,14 +225,16 @@ class _ServerPageState extends State<ServerPage>
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
final item = value as DropdownBtnItem;
|
||||
switch (item) {
|
||||
switch (value as DropdownBtnItem) {
|
||||
case ServerTabMenuItems.pkg:
|
||||
AppRoute(PkgManagePage(spi), 'pkg manage').go(context);
|
||||
break;
|
||||
case ServerTabMenuItems.sftp:
|
||||
AppRoute(SFTPPage(spi), 'SFTP').go(context);
|
||||
break;
|
||||
case ServerTabMenuItems.snippet:
|
||||
_showSnippetDialog(spi.id);
|
||||
break;
|
||||
case ServerTabMenuItems.edit:
|
||||
AppRoute(ServerEditPage(spi: spi), 'Edit server info').go(context);
|
||||
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
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.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/res/color.dart';
|
||||
import 'package:toolbox/data/res/font_style.dart';
|
||||
import 'package:toolbox/data/res/padding.dart';
|
||||
import 'package:toolbox/generated/l10n.dart';
|
||||
import 'package:toolbox/locator.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';
|
||||
|
||||
class SnippetListPage extends StatefulWidget {
|
||||
const SnippetListPage({Key? key, this.spi}) : super(key: key);
|
||||
final ServerPrivateInfo? spi;
|
||||
const SnippetListPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SnippetListPageState createState() => _SnippetListPageState();
|
||||
}
|
||||
|
||||
class _SnippetListPageState extends State<SnippetListPage> {
|
||||
late ServerPrivateInfo _selectedSpi;
|
||||
|
||||
final _textStyle = TextStyle(color: primaryColor);
|
||||
|
||||
late S _s;
|
||||
@@ -54,116 +45,45 @@ class _SnippetListPageState extends State<SnippetListPage> {
|
||||
Widget _buildBody() {
|
||||
return Consumer<SnippetProvider>(
|
||||
builder: (_, key, __) {
|
||||
return key.snippets.isNotEmpty
|
||||
? ListView.builder(
|
||||
padding: const EdgeInsets.all(13),
|
||||
itemCount: key.snippets.length,
|
||||
itemExtent: 57,
|
||||
itemBuilder: (context, idx) {
|
||||
return RoundRectCard(
|
||||
Padding(
|
||||
padding: roundRectCardPadding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
key.snippets[idx].name,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => AppRoute(
|
||||
SnippetEditPage(
|
||||
snippet: key.snippets[idx]),
|
||||
'snippet edit page')
|
||||
.go(context),
|
||||
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,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
if (key.snippets.isEmpty) {
|
||||
return Center(
|
||||
child: Text(_s.noSavedSnippet),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(13),
|
||||
itemCount: key.snippets.length,
|
||||
itemExtent: 57,
|
||||
itemBuilder: (context, idx) {
|
||||
return RoundRectCard(
|
||||
Padding(
|
||||
padding: roundRectCardPadding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
key.snippets[idx].name,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => AppRoute(
|
||||
SnippetEditPage(snippet: key.snippets[idx]),
|
||||
'snippet edit page')
|
||||
.go(context),
|
||||
child: Text(
|
||||
_s.edit,
|
||||
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"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
extended_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -638,6 +646,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -694,6 +710,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
r_upgrade:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -963,6 +987,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.2"
|
||||
xterm:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: xterm
|
||||
sha256: f65619cb24d03507812e346ddb8386cad9e16a01a481a8f5c8a2eba55b4edada
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -53,6 +53,7 @@ dependencies:
|
||||
share_plus: ^6.3.0
|
||||
intl: ^0.17.0
|
||||
share_plus_web: ^3.1.0
|
||||
xterm: ^3.4.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_native_splash: ^2.1.6
|
||||
|
||||
Reference in New Issue
Block a user