mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-15 12:44:59 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fc43d06e9 | ||
|
|
b922428c40 | ||
|
|
e08f37fedc | ||
|
|
7fb8c88ab8 | ||
|
|
f480c49f1f | ||
|
|
f7558d6beb | ||
|
|
de1e970108 | ||
|
|
9ef59f4c12 | ||
|
|
89ef2cb95c | ||
|
|
e0fb591dea |
@@ -38,9 +38,9 @@ A new Flutter project which provide a chart view to display server status data.
|
||||
- [x] Private key store
|
||||
- [x] Server status detail page
|
||||
- [x] Theme switch
|
||||
- [ ] Execute snippet
|
||||
- [x] Execute snippet
|
||||
- [ ] Migrate from `ssh2` to `dartssh2`
|
||||
- [ ] Desktop support.
|
||||
- [ ] Desktop support
|
||||
|
||||
## Build
|
||||
Please use `make.dart` to build.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="tech.lolli.toolbox">
|
||||
<application
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:label="ServerBox"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -5,6 +5,8 @@ PODS:
|
||||
- GZ-NMSSH (4.1.5)
|
||||
- path_provider (0.0.1):
|
||||
- Flutter
|
||||
- r_upgrade (0.0.1):
|
||||
- Flutter
|
||||
- ssh2 (2.2.3):
|
||||
- Flutter
|
||||
- GZ-NMSSH (~> 4.1.5)
|
||||
@@ -15,6 +17,7 @@ DEPENDENCIES:
|
||||
- countly_flutter (from `.symlinks/plugins/countly_flutter/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- path_provider (from `.symlinks/plugins/path_provider/ios`)
|
||||
- r_upgrade (from `.symlinks/plugins/r_upgrade/ios`)
|
||||
- ssh2 (from `.symlinks/plugins/ssh2/ios`)
|
||||
- url_launcher (from `.symlinks/plugins/url_launcher/ios`)
|
||||
|
||||
@@ -29,6 +32,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
path_provider:
|
||||
:path: ".symlinks/plugins/path_provider/ios"
|
||||
r_upgrade:
|
||||
:path: ".symlinks/plugins/r_upgrade/ios"
|
||||
ssh2:
|
||||
:path: ".symlinks/plugins/ssh2/ios"
|
||||
url_launcher:
|
||||
@@ -39,6 +44,7 @@ SPEC CHECKSUMS:
|
||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||
GZ-NMSSH: d749f8ae2fd0094b953cd1d5abd8e0cab3c93f8d
|
||||
path_provider: d1e9807085df1f9cc9318206cd649dc0b76be3de
|
||||
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
|
||||
ssh2: 74165efc99417a075ecafd52caf93edadfb5eb60
|
||||
url_launcher: b6e016d912f04be9f5bf6e8e82dc599b7ba59649
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ class Analysis {
|
||||
static const _url = 'https://countly.xuty.cc';
|
||||
static const _key = '80372a2a66424b32d0ac8991bfa1ef058bd36b1f';
|
||||
|
||||
static bool _enabled = false;
|
||||
|
||||
static Future<void> init(bool debug) async {
|
||||
_enabled = true;
|
||||
await Countly.setLoggingEnabled(debug);
|
||||
await Countly.init(_url, _key);
|
||||
await Countly.start();
|
||||
@@ -15,10 +18,14 @@ class Analysis {
|
||||
}
|
||||
|
||||
static void recordView(String view) {
|
||||
Countly.recordView(view);
|
||||
if (_enabled) {
|
||||
Countly.recordView(view);
|
||||
}
|
||||
}
|
||||
|
||||
static void recordException(Object exception, [bool fatal = false]) {
|
||||
Countly.logException(exception.toString(), !fatal, null);
|
||||
if (_enabled) {
|
||||
Countly.logException(exception.toString(), !fatal, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:r_upgrade/r_upgrade.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/res/build_data.dart';
|
||||
@@ -41,6 +42,12 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
|
||||
update.min > BuildData.build
|
||||
? 'Your version is too old. \nPlease update to v1.0.${update.newest}.'
|
||||
: 'Update: v1.0.${update.newest} available. \n${update.changelog}',
|
||||
'Update',
|
||||
() => openUrl(Platform.isAndroid ? update.android : update.ios));
|
||||
'Update', () async {
|
||||
if (Platform.isAndroid) {
|
||||
await RUpgrade.upgrade(update.android,
|
||||
fileName: update.android.split('/').last, isAutoRequestInstall: true);
|
||||
} else if (Platform.isIOS) {
|
||||
showSnackBar(context, const Text('Not support iOS now.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ bool isDarkMode(BuildContext context) =>
|
||||
void showSnackBar(BuildContext context, Widget child) =>
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: child));
|
||||
|
||||
void showSnackBarWithAction(
|
||||
BuildContext context, String content, String action, Function onTap) {
|
||||
void showSnackBarWithAction(BuildContext context, String content, String action,
|
||||
GestureTapCallback onTap) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(content),
|
||||
action: SnackBarAction(
|
||||
label: action,
|
||||
onPressed: () => onTap,
|
||||
onPressed: onTap,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -5,5 +5,11 @@ class Memory {
|
||||
int shared;
|
||||
int cache;
|
||||
int avail;
|
||||
Memory({required this.total, required this.used, required this.free, required this.shared, required this.cache, required this.avail});
|
||||
Memory(
|
||||
{required this.total,
|
||||
required this.used,
|
||||
required this.free,
|
||||
required this.shared,
|
||||
required this.cache,
|
||||
required this.avail});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ class ServerStatus {
|
||||
List<DiskInfo> disk;
|
||||
TcpStatus tcp;
|
||||
NetSpeed netSpeed;
|
||||
String? failedInfo;
|
||||
|
||||
ServerStatus(this.cpu2Status, this.memory, this.sysVer, this.uptime,
|
||||
this.disk, this.tcp, this.netSpeed);
|
||||
this.disk, this.tcp, this.netSpeed,
|
||||
{this.failedInfo});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:toolbox/data/model/server/disk_info.dart';
|
||||
import 'package:toolbox/data/model/server/server.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/model/server/server_status.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/model/server/tcp_status.dart';
|
||||
import 'package:toolbox/data/store/server.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
@@ -22,6 +23,8 @@ class ServerProvider extends BusyProvider {
|
||||
List<ServerInfo> _servers = [];
|
||||
List<ServerInfo> get servers => _servers;
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
final logger = Logger('ServerProvider');
|
||||
|
||||
Memory get emptyMemory =>
|
||||
@@ -94,11 +97,19 @@ class ServerProvider extends BusyProvider {
|
||||
final duration =
|
||||
locator<SettingStore>().serverStatusUpdateInterval.fetch()!;
|
||||
if (duration == 0) return;
|
||||
Timer.periodic(Duration(seconds: duration), (_) async {
|
||||
stopAutoRefresh();
|
||||
_timer = Timer.periodic(Duration(seconds: duration), (_) async {
|
||||
await refreshData();
|
||||
});
|
||||
}
|
||||
|
||||
void stopAutoRefresh() {
|
||||
if (_timer != null) {
|
||||
_timer!.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void addServer(ServerPrivateInfo info) {
|
||||
_servers.add(genInfo(info));
|
||||
locator<ServerStore>().put(info);
|
||||
@@ -140,6 +151,7 @@ class ServerProvider extends BusyProvider {
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_servers[idx].connectionState = ServerConnectionState.failed;
|
||||
_servers[idx].status.failedInfo = e.toString().split(', ')[1];
|
||||
notifyListeners();
|
||||
logger.warning(e);
|
||||
}
|
||||
@@ -290,4 +302,8 @@ class ServerProvider extends BusyProvider {
|
||||
}
|
||||
return emptyMemory;
|
||||
}
|
||||
|
||||
Future<String?> runSnippet(int idx, Snippet snippet) {
|
||||
return _servers[idx].client.execute(snippet.script);
|
||||
}
|
||||
}
|
||||
|
||||
32
lib/data/provider/snippet.dart
Normal file
32
lib/data/provider/snippet.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:toolbox/core/provider_base.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/store/snippet.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
class SnippetProvider extends BusyProvider {
|
||||
List<Snippet> get snippets => _snippets;
|
||||
late List<Snippet> _snippets;
|
||||
|
||||
void loadData() {
|
||||
_snippets = locator<SnippetStore>().fetch();
|
||||
}
|
||||
|
||||
void addInfo(Snippet snippet) {
|
||||
_snippets.add(snippet);
|
||||
locator<SnippetStore>().put(snippet);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void delInfo(Snippet snippet) {
|
||||
_snippets.removeWhere((e) => e.name == snippet.name);
|
||||
locator<SnippetStore>().delete(snippet);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateInfo(Snippet old, Snippet newOne) {
|
||||
final idx = _snippets.indexWhere((e) => e.name == old.name);
|
||||
_snippets[idx] = newOne;
|
||||
locator<SnippetStore>().update(old, newOne);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
class BuildData {
|
||||
static const String name = "ToolBox";
|
||||
static const int build = 60;
|
||||
static const String engine = "Flutter 2.5.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 18116933e7 (3 weeks ago) • 2021-10-15 10:46:35 -0700\nEngine • revision d3ea636dc5\nTools • Dart 2.14.4\n";
|
||||
static const String buildAt = "2021-11-02 20:36:41.010803";
|
||||
static const int build = 70;
|
||||
static const String engine = "Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (3 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n";
|
||||
static const String buildAt = "2022-01-03 14:58:50.087508";
|
||||
static const int modifications = 2;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/provider/debug.dart';
|
||||
import 'package:toolbox/data/provider/private_key.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/provider/snippet.dart';
|
||||
import 'package:toolbox/data/service/app.dart';
|
||||
import 'package:toolbox/data/store/private_key.dart';
|
||||
import 'package:toolbox/data/store/server.dart';
|
||||
@@ -19,6 +20,7 @@ void setupLocatorForProviders() {
|
||||
locator.registerSingleton(AppProvider());
|
||||
locator.registerSingleton(DebugProvider());
|
||||
locator.registerSingleton(ServerProvider());
|
||||
locator.registerSingleton(SnippetProvider());
|
||||
locator.registerSingleton(PrivateKeyProvider());
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/provider/debug.dart';
|
||||
import 'package:toolbox/data/provider/private_key.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/provider/snippet.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
Future<void> initApp() async {
|
||||
await Hive.initFlutter();
|
||||
await setupLocator();
|
||||
locator<SnippetProvider>().loadData();
|
||||
locator<PrivateKeyProvider>().loadData();
|
||||
|
||||
///设置Logger
|
||||
@@ -62,6 +64,7 @@ Future<void> main() async {
|
||||
ChangeNotifierProvider(create: (_) => locator<AppProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<DebugProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<SnippetProvider>()),
|
||||
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
|
||||
],
|
||||
child: const MyApp(),
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:toolbox/data/res/url.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/convert.dart';
|
||||
import 'package:toolbox/view/page/debug.dart';
|
||||
import 'package:toolbox/view/page/private_key/stored.dart';
|
||||
import 'package:toolbox/view/page/private_key/list.dart';
|
||||
import 'package:toolbox/view/page/server/tab.dart';
|
||||
import 'package:toolbox/view/page/setting.dart';
|
||||
import 'package:toolbox/view/page/snippet/list.dart';
|
||||
@@ -31,16 +31,37 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
with
|
||||
AutomaticKeepAliveClientMixin,
|
||||
SingleTickerProviderStateMixin,
|
||||
AfterLayoutMixin {
|
||||
AfterLayoutMixin,
|
||||
WidgetsBindingObserver {
|
||||
final List<String> _tabs = ['Servers', 'En/Decode'];
|
||||
late final TabController _tabController;
|
||||
late final ServerProvider _serverProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverProvider = locator<ServerProvider>();
|
||||
WidgetsBinding.instance?.addObserver(this);
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
WidgetsBinding.instance?.removeObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.paused) {
|
||||
_serverProvider.stopAutoRefresh();
|
||||
}
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_serverProvider.startAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
setTransparentNavigationBar(context);
|
||||
@@ -92,8 +113,7 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
leading: const Icon(Icons.snippet_folder),
|
||||
title: const Text('Snippet'),
|
||||
onTap: () =>
|
||||
AppRoute(const SnippetListPage(), 'snippet list')
|
||||
.go(context),
|
||||
AppRoute(const SnippetListPage(), 'snippet list').go(context),
|
||||
),
|
||||
AboutListTile(
|
||||
icon: const Icon(Icons.text_snippet),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/server/private_key_info.dart';
|
||||
import 'package:toolbox/data/provider/private_key.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
@@ -70,8 +71,15 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.send),
|
||||
onPressed: () {
|
||||
final info = PrivateKeyInfo(
|
||||
nameController.text, keyController.text, pwdController.text);
|
||||
final name = nameController.text;
|
||||
final key = keyController.text;
|
||||
final pwd = pwdController.text;
|
||||
if (name.isEmpty || key.isEmpty || pwd.isEmpty) {
|
||||
showSnackBar(
|
||||
context, const Text('Three fields must not be empty.'));
|
||||
return;
|
||||
}
|
||||
final info = PrivateKeyInfo(name, key, pwd);
|
||||
if (widget.info != null) {
|
||||
_provider.updateInfo(widget.info!, info);
|
||||
} else {
|
||||
|
||||
@@ -44,12 +44,16 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(si.info.name),
|
||||
actions: [IconButton(onPressed: () => AppRoute(
|
||||
ServerEditPage(
|
||||
spi: si.info,
|
||||
),
|
||||
'Edit server info page')
|
||||
.go(context), icon: const Icon(Icons.edit))],
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => AppRoute(
|
||||
ServerEditPage(
|
||||
spi: si.info,
|
||||
),
|
||||
'Edit server info page')
|
||||
.go(context),
|
||||
icon: const Icon(Icons.edit))
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(17),
|
||||
@@ -60,7 +64,8 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
_buildCPUView(si.status),
|
||||
_buildDiskView(si.status),
|
||||
_buildMemView(si.status),
|
||||
_buildNetView(si.status.netSpeed)
|
||||
_buildNetView(si.status.netSpeed),
|
||||
SizedBox(height: _media.padding.bottom),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -180,6 +185,20 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
));
|
||||
}
|
||||
|
||||
String convertMB(int mb) {
|
||||
const suffix = ['MB', 'GB', 'TB'];
|
||||
double value = mb.toDouble();
|
||||
int squareTimes = 0;
|
||||
for (; value / 1024 > 1 && squareTimes < 3; squareTimes++) {
|
||||
value /= 1024;
|
||||
}
|
||||
var finalValue = value.toStringAsFixed(1);
|
||||
if (finalValue.endsWith('.0')) {
|
||||
finalValue = finalValue.replaceFirst('.0', '');
|
||||
}
|
||||
return '$finalValue ${suffix[squareTimes]}';
|
||||
}
|
||||
|
||||
Widget _buildMemView(ServerStatus ss) {
|
||||
final pColor = primaryColor;
|
||||
final used = ss.memory.used / ss.memory.total;
|
||||
@@ -193,9 +212,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildMemExplain('Used', pColor),
|
||||
_buildMemExplain('Cache', pColor.withAlpha(77)),
|
||||
_buildMemExplain('Avail', progressColor.resolve(context))
|
||||
_buildMemExplain(convertMB(ss.memory.used), pColor),
|
||||
_buildMemExplain(
|
||||
convertMB(ss.memory.cache), pColor.withAlpha(77)),
|
||||
_buildMemExplain(convertMB(ss.memory.total - ss.memory.used),
|
||||
progressColor.resolve(context))
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -215,7 +236,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
// memory.total == 1: failed to get mem, now mem = [emptyMemory] which is initial value.
|
||||
value: ss.memory.total == 1
|
||||
? 0
|
||||
: ss.memory.cache / ss.memory.total,
|
||||
: ss.memory.cache / (ss.memory.total - ss.memory.used),
|
||||
backgroundColor: progressColor.resolve(context),
|
||||
color: pColor.withAlpha(77),
|
||||
),
|
||||
@@ -227,7 +248,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildMemExplain(String type, Color color) {
|
||||
Widget _buildMemExplain(String value, Color color) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
@@ -236,7 +257,12 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
width: 11,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(type, style: style11, textScaleFactor: 1.0)
|
||||
Text(
|
||||
value,
|
||||
style: style11,
|
||||
textScaleFactor: 1.0,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
onPressed: () {
|
||||
_serverProvider.delServer(widget.spi!);
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'Yes',
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:circle_chart/circle_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:toolbox/core/route.dart';
|
||||
import 'package:toolbox/data/model/server/server.dart';
|
||||
import 'package:toolbox/data/model/server/server_connection_state.dart';
|
||||
import 'package:toolbox/data/model/server/server_status.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/server/detail.dart';
|
||||
@@ -27,6 +29,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
late MediaQueryData _media;
|
||||
late ThemeData _theme;
|
||||
late Color _primaryColor;
|
||||
late RefreshController _refreshController;
|
||||
|
||||
late ServerProvider _serverProvider;
|
||||
|
||||
@@ -34,6 +37,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverProvider = locator<ServerProvider>();
|
||||
_refreshController = RefreshController();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -41,41 +45,54 @@ class _ServerPageState extends State<ServerPage>
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
_theme = Theme.of(context);
|
||||
_primaryColor = Color(locator<SettingStore>().primaryColor.fetch()!);
|
||||
_primaryColor = primaryColor;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Scaffold(
|
||||
body: Consumer<ServerProvider>(builder: (_, pro, __) {
|
||||
if (pro.servers.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'There is no server.\nClick the fab to add one.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: AnimationLimiter(
|
||||
child: Column(
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
childAnimationBuilder: (widget) => SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: widget,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
const SizedBox(height: 13),
|
||||
...pro.servers.map((e) => _buildEachServerCard(e))
|
||||
],
|
||||
))),
|
||||
final autoUpdate =
|
||||
locator<SettingStore>().serverStatusUpdateInterval.fetch() != 0;
|
||||
final child = Consumer<ServerProvider>(builder: (_, pro, __) {
|
||||
if (pro.servers.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'There is no server.\nClick the fab to add one.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}),
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7),
|
||||
child: AnimationLimiter(
|
||||
child: Column(
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
childAnimationBuilder: (widget) => SlideAnimation(
|
||||
verticalOffset: 77.0,
|
||||
child: FadeInAnimation(
|
||||
child: widget,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
const SizedBox(height: 13),
|
||||
...pro.servers.map((e) => _buildEachServerCard(e)),
|
||||
SizedBox(height: _media.padding.bottom),
|
||||
],
|
||||
))),
|
||||
);
|
||||
});
|
||||
return Scaffold(
|
||||
body: autoUpdate
|
||||
? child
|
||||
: SmartRefresher(
|
||||
controller: _refreshController,
|
||||
child: child,
|
||||
onRefresh: () async {
|
||||
await _serverProvider.refreshData();
|
||||
_refreshController.refreshCompleted();
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () =>
|
||||
AppRoute(const ServerEditPage(), 'Add server info page')
|
||||
@@ -91,11 +108,11 @@ class _ServerPageState extends State<ServerPage>
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onLongPress: () => AppRoute(
|
||||
ServerEditPage(
|
||||
spi: si.info,
|
||||
),
|
||||
'Edit server info page')
|
||||
.go(context),
|
||||
ServerEditPage(
|
||||
spi: si.info,
|
||||
),
|
||||
'Edit server info page')
|
||||
.go(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(13),
|
||||
child:
|
||||
@@ -113,6 +130,12 @@ class _ServerPageState extends State<ServerPage>
|
||||
final rootDisk =
|
||||
ss.disk.firstWhere((element) => element.mountLocation == '/');
|
||||
|
||||
final topRightStr =
|
||||
getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime, ss.failedInfo);
|
||||
final hasError = cs == ServerConnectionState.failed && ss.failedInfo != null;
|
||||
final style = TextStyle(
|
||||
color: _theme.textTheme.bodyText1!.color!.withAlpha(100), fontSize: 11);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -124,11 +147,20 @@ class _ServerPageState extends State<ServerPage>
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
textScaleFactor: 1.0,
|
||||
),
|
||||
Text(getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime),
|
||||
textScaleFactor: 1.0,
|
||||
style: TextStyle(
|
||||
color: _theme.textTheme.bodyText1!.color!.withAlpha(100),
|
||||
fontSize: 11))
|
||||
hasError
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _media.size.width * 0.57, maxHeight: 17),
|
||||
child: Marquee(
|
||||
accelerationDuration: const Duration(seconds: 3),
|
||||
accelerationCurve: Curves.linear,
|
||||
decelerationDuration: const Duration(seconds: 3),
|
||||
decelerationCurve: Curves.linear,
|
||||
text: topRightStr,
|
||||
textScaleFactor: 1.0,
|
||||
style: style),
|
||||
)
|
||||
: Text(topRightStr, style: style, textScaleFactor: 1.0),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -149,7 +181,8 @@ class _ServerPageState extends State<ServerPage>
|
||||
);
|
||||
}
|
||||
|
||||
String getTopRightStr(ServerConnectionState cs, String temp, String upTime) {
|
||||
String getTopRightStr(ServerConnectionState cs, String temp, String upTime,
|
||||
String? failedInfo) {
|
||||
switch (cs) {
|
||||
case ServerConnectionState.disconnected:
|
||||
return 'Disconnected';
|
||||
@@ -158,7 +191,7 @@ class _ServerPageState extends State<ServerPage>
|
||||
case ServerConnectionState.connecting:
|
||||
return 'Connecting...';
|
||||
case ServerConnectionState.failed:
|
||||
return 'Failed';
|
||||
return failedInfo ?? 'Failed';
|
||||
default:
|
||||
return 'Unknown State';
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter_material_color_picker/flutter_material_color_picker.dart
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:toolbox/core/update.dart';
|
||||
import 'package:toolbox/data/provider/app.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/data/res/build_data.dart';
|
||||
import 'package:toolbox/data/res/color.dart';
|
||||
import 'package:toolbox/data/store/setting.dart';
|
||||
@@ -22,6 +23,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
double _intervalValue = 0;
|
||||
late Color priColor;
|
||||
static const textStyle = TextStyle(fontSize: 14);
|
||||
late final ServerProvider _serverProvider;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -32,6 +34,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverProvider = locator<ServerProvider>();
|
||||
_store = locator<SettingStore>();
|
||||
_intervalValue = _store.serverStatusUpdateInterval.fetch()!.toDouble();
|
||||
}
|
||||
@@ -66,10 +69,14 @@ class _SettingPageState extends State<SettingPage> {
|
||||
display = 'Current: v1.0.${BuildData.build}';
|
||||
}
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
title: Text(display, style: textStyle,
|
||||
textAlign: TextAlign.start,), onTap: () => doUpdate(context, force: true));
|
||||
contentPadding: EdgeInsets.zero,
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
title: Text(
|
||||
display,
|
||||
style: textStyle,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
onTap: () => doUpdate(context, force: true));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,7 +91,7 @@ class _SettingPageState extends State<SettingPage> {
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Will take effect the next time app launches.',
|
||||
'Will take effect immediately.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
trailing: Text('${_intervalValue.toInt()} s'),
|
||||
@@ -100,8 +107,10 @@ class _SettingPageState extends State<SettingPage> {
|
||||
_intervalValue = newValue;
|
||||
});
|
||||
},
|
||||
onChangeEnd: (val) =>
|
||||
_store.serverStatusUpdateInterval.put(val.toInt()),
|
||||
onChangeEnd: (val) {
|
||||
_store.serverStatusUpdateInterval.put(val.toInt());
|
||||
_serverProvider.startAutoRefresh();
|
||||
},
|
||||
label: '${_intervalValue.toInt()} seconds',
|
||||
divisions: 10,
|
||||
),
|
||||
@@ -109,7 +118,11 @@ class _SettingPageState extends State<SettingPage> {
|
||||
height: 3,
|
||||
),
|
||||
_intervalValue == 0.0
|
||||
? const Text('You set to 0, will not update automatically.')
|
||||
? const Text(
|
||||
'You set to 0, will not update automatically.\nYou can pull to refresh manually.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
: const SizedBox(),
|
||||
const SizedBox(
|
||||
height: 13,
|
||||
|
||||
@@ -1,15 +1,91 @@
|
||||
import 'package:after_layout/after_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/server/snippet.dart';
|
||||
import 'package:toolbox/data/provider/snippet.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/input_decoration.dart';
|
||||
|
||||
class SnippetEditPage extends StatefulWidget {
|
||||
const SnippetEditPage({Key? key}) : super(key: key);
|
||||
const SnippetEditPage({Key? key, this.snippet}) : super(key: key);
|
||||
|
||||
final Snippet? snippet;
|
||||
|
||||
@override
|
||||
_SnippetEditPageState createState() => _SnippetEditPageState();
|
||||
}
|
||||
|
||||
class _SnippetEditPageState extends State<SnippetEditPage> {
|
||||
class _SnippetEditPageState extends State<SnippetEditPage>
|
||||
with AfterLayoutMixin {
|
||||
final nameController = TextEditingController();
|
||||
final scriptController = TextEditingController();
|
||||
|
||||
late SnippetProvider _provider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_provider = locator<SnippetProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(appBar: AppBar(title: const Text('Snippet Edit'),), body: const Center(child: Text('Developing'),),);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Edit'), actions: [
|
||||
widget.snippet != null
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_provider.delInfo(widget.snippet!);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.delete))
|
||||
: const SizedBox()
|
||||
]),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(13),
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: buildDecoration('Name', icon: Icons.info),
|
||||
),
|
||||
TextField(
|
||||
controller: scriptController,
|
||||
autocorrect: false,
|
||||
minLines: 3,
|
||||
maxLines: 10,
|
||||
keyboardType: TextInputType.text,
|
||||
enableSuggestions: false,
|
||||
decoration: buildDecoration('Snippet', icon: Icons.code),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.send),
|
||||
onPressed: () {
|
||||
final name = nameController.text;
|
||||
final script = scriptController.text;
|
||||
if (name.isEmpty || script.isEmpty) {
|
||||
showSnackBar(context, const Text('Two fields must not be empty.'));
|
||||
return;
|
||||
}
|
||||
final snippet = Snippet(name, script);
|
||||
if (widget.snippet != null) {
|
||||
_provider.updateInfo(widget.snippet!, snippet);
|
||||
} else {
|
||||
_provider.addInfo(snippet);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
if (widget.snippet != null) {
|
||||
nameController.text = widget.snippet!.name;
|
||||
scriptController.text = widget.snippet!.script;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
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/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/locator.dart';
|
||||
import 'package:toolbox/view/page/snippet/edit.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
class SnippetListPage extends StatefulWidget {
|
||||
const SnippetListPage({Key? key}) : super(key: key);
|
||||
@@ -8,8 +18,122 @@ class SnippetListPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SnippetListPageState extends State<SnippetListPage> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
final _textStyle = TextStyle(color: primaryColor);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(appBar: AppBar(title: const Text('Snippet List'),), body: const Center(child: Text('Developing'),),);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Snippet List'),
|
||||
),
|
||||
body: _buildBody(),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () =>
|
||||
AppRoute(const SnippetEditPage(), 'snippet edit page').go(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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(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(
|
||||
'Edit',
|
||||
style: _textStyle,
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () => _showRunDialog(key.snippets[idx]),
|
||||
child: Text(
|
||||
'Run',
|
||||
style: _textStyle,
|
||||
))
|
||||
])
|
||||
],
|
||||
));
|
||||
})
|
||||
: const Center(child: Text('No saved snippets.'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showRunDialog(Snippet snippet) {
|
||||
showRoundDialog(context, 'Choose destination',
|
||||
Consumer<ServerProvider>(builder: (_, provider, __) {
|
||||
if (provider.servers.isEmpty) {
|
||||
return const Text('No server available');
|
||||
}
|
||||
return SizedBox(
|
||||
height: 111,
|
||||
child: Stack(children: [
|
||||
Positioned(
|
||||
child: Container(
|
||||
height: 37,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(7)),
|
||||
color: Colors.black12,
|
||||
),
|
||||
),
|
||||
top: 36,
|
||||
bottom: 36,
|
||||
left: 0,
|
||||
right: 0,
|
||||
),
|
||||
ListWheelScrollView.useDelegate(
|
||||
itemExtent: 37,
|
||||
diameterRatio: 1.2,
|
||||
controller: FixedExtentScrollController(initialItem: 0),
|
||||
onSelectedItemChanged: (idx) => _selectedIndex = idx,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
childDelegate: ListWheelChildBuilderDelegate(
|
||||
builder: (context, index) => Center(
|
||||
child: Text(
|
||||
provider.servers[index].info.name,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
childCount: provider.servers.length),
|
||||
)
|
||||
]));
|
||||
}), [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final result = await locator<ServerProvider>()
|
||||
.runSnippet(_selectedIndex, snippet);
|
||||
if (result != null) {
|
||||
showRoundDialog(context, 'Result',
|
||||
Text(result, style: const TextStyle(fontSize: 13)), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Close'))
|
||||
]);
|
||||
}
|
||||
},
|
||||
child: const Text('Run')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
38
pubspec.lock
38
pubspec.lock
@@ -28,7 +28,7 @@ packages:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.1"
|
||||
version: "2.8.2"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -42,7 +42,7 @@ packages:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -117,6 +117,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
fading_edge_scrollview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fading_edge_scrollview
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -258,13 +265,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
marquee:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: marquee
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.10"
|
||||
version: "0.12.11"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -363,6 +377,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
pull_to_refresh:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pull_to_refresh
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
r_upgrade:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: r_upgrade
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.6"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -416,7 +444,7 @@ packages:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
version: "0.4.3"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -486,7 +514,7 @@ packages:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -51,6 +51,9 @@ dependencies:
|
||||
url: https://github.com/LollipopKit/circle_chart
|
||||
ref: main
|
||||
clipboard: ^0.1.3
|
||||
r_upgrade: ^0.3.6
|
||||
pull_to_refresh: ^2.0.0
|
||||
marquee: ^2.2.0
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
|
||||
Reference in New Issue
Block a user