From a7256041219869c6d402d3d1f4919dc1ccd739ce Mon Sep 17 00:00:00 2001 From: LollipopKit <2036293523@qq.com> Date: Tue, 26 Oct 2021 07:31:42 +0800 Subject: [PATCH] BBreaking change --- lib/app.dart | 28 +++-- lib/data/model/private_key_info.dart | 48 ++++++++ lib/data/provider/private_key.dart | 32 +++++ lib/data/provider/server.dart | 27 +++-- lib/data/res/build_data.dart | 6 +- lib/data/store/private_key.dart | 33 +++++ lib/data/store/setting.dart | 7 +- lib/locator.dart | 7 ++ lib/main.dart | 3 + lib/view/page/home.dart | 34 ++++-- lib/view/page/private_key/edit.dart | 98 +++++++++++++++ lib/view/page/private_key/stored.dart | 58 +++++++++ .../server/{server_edit.dart => edit.dart} | 97 +++++++++++---- .../page/server/{server_tab.dart => tab.dart} | 113 +++++++----------- lib/view/page/setting.dart | 108 +++++++++++++++++ lib/view/widget/circle_pie.dart | 32 ----- lib/view/widget/round_rect_card.dart | 20 ++++ lib/view/widget/url_text.dart | 91 ++++++++++++++ pubspec.lock | 45 ++++--- pubspec.yaml | 6 +- 20 files changed, 710 insertions(+), 183 deletions(-) create mode 100644 lib/data/model/private_key_info.dart create mode 100644 lib/data/provider/private_key.dart create mode 100644 lib/data/store/private_key.dart create mode 100644 lib/view/page/private_key/edit.dart create mode 100644 lib/view/page/private_key/stored.dart rename lib/view/page/server/{server_edit.dart => edit.dart} (58%) rename lib/view/page/server/{server_tab.dart => tab.dart} (67%) create mode 100644 lib/view/page/setting.dart delete mode 100644 lib/view/widget/circle_pie.dart create mode 100644 lib/view/widget/round_rect_card.dart create mode 100644 lib/view/widget/url_text.dart diff --git a/lib/app.dart b/lib/app.dart index 3d074598..274af118 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:toolbox/data/store/setting.dart'; +import 'package:toolbox/locator.dart'; import 'package:toolbox/view/page/home.dart'; class MyApp extends StatelessWidget { @@ -7,13 +9,23 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( - title: 'ToolBox', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - darkTheme: ThemeData.dark(), - home: const MyHomePage(title: 'ToolBox'), - ); + return ValueListenableBuilder( + valueListenable: locator().primaryColor.listenable(), + builder: (_, value, __) { + final primaryColor = Color(value); + return MaterialApp( + title: 'ToolBox', + theme: ThemeData( + primaryColor: primaryColor, + appBarTheme: AppBarTheme(backgroundColor: primaryColor), + floatingActionButtonTheme: + FloatingActionButtonThemeData(backgroundColor: primaryColor), + iconTheme: IconThemeData(color: primaryColor), + primaryIconTheme: IconThemeData(color: primaryColor), + ), + darkTheme: ThemeData.dark().copyWith(primaryColor: primaryColor), + home: MyHomePage(primaryColor: primaryColor), + ); + }); } } diff --git a/lib/data/model/private_key_info.dart b/lib/data/model/private_key_info.dart new file mode 100644 index 00000000..2e63253c --- /dev/null +++ b/lib/data/model/private_key_info.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +/// +/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/ +/// +class PrivateKeyInfo { +/* +{ + "id": "", + "private_key": "", + "password": "" +} +*/ + + late String id; + late String privateKey; + late String password; + + PrivateKeyInfo( + this.id, + this.privateKey, + this.password, + ); + PrivateKeyInfo.fromJson(Map json) { + id = json["id"].toString(); + privateKey = json["private_key"].toString(); + password = json["password"].toString(); + } + Map toJson() { + final Map data = {}; + data["id"] = id; + data["private_key"] = privateKey; + data["password"] = password; + return data; + } +} + +List? getPrivateKeyInfoList(dynamic data) { + List ss = []; + if (data is String) { + data = json.decode(data); + } + for (var t in data) { + ss.add(PrivateKeyInfo.fromJson(t)); + } + + return ss; +} diff --git a/lib/data/provider/private_key.dart b/lib/data/provider/private_key.dart new file mode 100644 index 00000000..9b6ea3f6 --- /dev/null +++ b/lib/data/provider/private_key.dart @@ -0,0 +1,32 @@ +import 'package:toolbox/core/provider_base.dart'; +import 'package:toolbox/data/model/private_key_info.dart'; +import 'package:toolbox/data/store/private_key.dart'; +import 'package:toolbox/locator.dart'; + +class PrivateKeyProvider extends BusyProvider { + List get infos => _infos; + late List _infos; + + void loadData() { + _infos = locator().fetch(); + } + + void addInfo(PrivateKeyInfo info) { + _infos.add(info); + locator().put(info); + notifyListeners(); + } + + void delInfo(PrivateKeyInfo info) { + _infos.removeWhere((e) => e.id == info.id); + locator().delete(info); + notifyListeners(); + } + + void updateInfo(PrivateKeyInfo old, PrivateKeyInfo newInfo) { + final idx = _infos.indexWhere((e) => e.id == old.id); + _infos[idx] = newInfo; + locator().update(old, newInfo); + notifyListeners(); + } +} diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index 43044682..529a6cea 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -10,6 +10,7 @@ import 'package:toolbox/data/model/server_private_info.dart'; import 'package:toolbox/data/model/server_status.dart'; import 'package:toolbox/data/model/tcp_status.dart'; import 'package:toolbox/data/store/server.dart'; +import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/locator.dart'; class ServerProvider extends BusyProvider { @@ -55,18 +56,28 @@ class ServerProvider extends BusyProvider { } Future refreshData() async { - final _serversStatus = await Future.wait( - _servers.map((s) => _getData(s.info, _servers.indexOf(s)))); - int idx = 0; - for (var item in _serversStatus) { - _servers[idx].status = item; - idx++; + List _serversStatus = []; + try { + _serversStatus = await Future.wait( + _servers.map((s) => _getData(s.info, _servers.indexOf(s)))); + } catch (e) { + rethrow; + } finally { + int idx = 0; + for (var item in _serversStatus) { + _servers[idx].status = item; + idx++; + } + notifyListeners(); } - notifyListeners(); } Future startAutoRefresh() async { - Timer.periodic(const Duration(seconds: 3), (_) async { + Timer.periodic( + Duration( + seconds: locator() + .serverStatusUpdateInterval + .fetch()!), (_) async { await refreshData(); }); } diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index b8981b18..2374618c 100644 --- a/lib/data/res/build_data.dart +++ b/lib/data/res/build_data.dart @@ -2,9 +2,9 @@ class BuildData { static const String name = "ToolBox"; - static const int build = 18; + static const int build = 20; static const String engine = "Flutter 2.5.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 18116933e7 (10 days ago) • 2021-10-15 10:46:35 -0700\nEngine • revision d3ea636dc5\nTools • Dart 2.14.4\n"; - static const String buildAt = "2021-10-25 16:56:13.551427"; - static const int modifications = 0; + static const String buildAt = "2021-10-26 00:28:50.061797"; + static const int modifications = 21; } diff --git a/lib/data/store/private_key.dart b/lib/data/store/private_key.dart new file mode 100644 index 00000000..8fbe882c --- /dev/null +++ b/lib/data/store/private_key.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:toolbox/core/persistant_store.dart'; +import 'package:toolbox/data/model/private_key_info.dart'; + +class PrivateKeyStore extends PersistentStore { + void put(PrivateKeyInfo info) { + final ss = fetch(); + if (!have(info)) ss.add(info); + box.put('key', json.encode(ss)); + } + + List fetch() { + return getPrivateKeyInfoList( + json.decode(box.get('key', defaultValue: '[]')!))!; + } + + void delete(PrivateKeyInfo s) { + final ss = fetch(); + ss.removeAt(index(s)); + box.put('key', json.encode(ss)); + } + + void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) { + final ss = fetch(); + ss[index(old)] = newInfo; + box.put('key', json.encode(ss)); + } + + int index(PrivateKeyInfo s) => fetch().indexWhere((e) => e.id == s.id); + + bool have(PrivateKeyInfo s) => index(s) != -1; +} diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index cd0ec0ca..06333884 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -1,6 +1,9 @@ +import 'package:flutter/material.dart'; import 'package:toolbox/core/persistant_store.dart'; class SettingStore extends PersistentStore { - StoreProperty get receiveNotification => - property('notify', defaultValue: true); + StoreProperty get primaryColor => + property('primaryColor', defaultValue: Colors.deepPurpleAccent.value); + StoreProperty get serverStatusUpdateInterval => + property('serverStatusUpdateInterval', defaultValue: 3); } diff --git a/lib/locator.dart b/lib/locator.dart index 800f26e6..45edf119 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,8 +1,10 @@ import 'package:get_it/get_it.dart'; 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/service/app.dart'; +import 'package:toolbox/data/store/private_key.dart'; import 'package:toolbox/data/store/server.dart'; import 'package:toolbox/data/store/setting.dart'; @@ -16,6 +18,7 @@ void setupLocatorForProviders() { locator.registerSingleton(AppProvider()); locator.registerSingleton(DebugProvider()); locator.registerSingleton(ServerProvider()); + locator.registerSingleton(PrivateKeyProvider()); } Future setupLocatorForStores() async { @@ -26,6 +29,10 @@ Future setupLocatorForStores() async { final server = ServerStore(); await server.init(boxName: 'server'); locator.registerSingleton(server); + + final key = PrivateKeyStore(); + await key.init(boxName: 'key'); + locator.registerSingleton(key); } Future setupLocator() async { diff --git a/lib/main.dart b/lib/main.dart index 272992b2..54b362d3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,12 +8,14 @@ import 'package:toolbox/app.dart'; import 'package:toolbox/core/analysis.dart'; 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/locator.dart'; Future initApp() async { await Hive.initFlutter(); await setupLocator(); + locator().loadData(); ///设置Logger Logger.root.level = Level.ALL; // defaults to Level.INFO @@ -60,6 +62,7 @@ Future main() async { ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), + ChangeNotifierProvider(create: (_) => locator()), ], child: const MyApp(), ), diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index eb44ba96..6ecb73d6 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -9,11 +9,14 @@ import 'package:toolbox/data/res/build_data.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/server/server_tab.dart'; +import 'package:toolbox/view/page/private_key/stored.dart'; +import 'package:toolbox/view/page/server/tab.dart'; +import 'package:toolbox/view/page/setting.dart'; +import 'package:toolbox/view/widget/url_text.dart'; class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); - final String title; + const MyHomePage({Key? key, required this.primaryColor}) : super(key: key); + final Color primaryColor; @override State createState() => _MyHomePageState(); @@ -42,9 +45,10 @@ class _MyHomePageState extends State title: GestureDetector( onLongPress: () => AppRoute(const DebugPage(), 'Debug Page').go(context), - child: Text(widget.title), + child: const Text('ToolBox'), ), bottom: TabBar( + indicatorColor: widget.primaryColor, tabs: _tabs.map((e) => Tab(text: e)).toList(), controller: _tabController, ), @@ -67,19 +71,27 @@ class _MyHomePageState extends State accountEmail: Text(_buildVersionStr()), currentAccountPicture: _buildIcon(), ), - // const ListTile( - // leading: Icon(Icons.settings), - // title: Text('设置'), - // ), + ListTile( + leading: const Icon(Icons.settings), + title: const Text('Setting'), + onTap: () => AppRoute(const SettingPage(), 'Setting').go(context), + ), + ListTile( + leading: const Icon(Icons.vpn_key), + title: const Text('Private Key'), + onTap: () => + AppRoute(const StoredPrivateKeysPage(), 'Setting').go(context), + ), AboutListTile( icon: const Icon(Icons.text_snippet), - child: const Text('Open source licenses'), + child: const Text('Licences'), applicationName: BuildData.name, applicationVersion: _buildVersionStr(), applicationIcon: _buildIcon(), aboutBoxChildren: const [ - Text('''\nMade with Love. - \nAll rights reserved.'''), + UrlText( + text: '''\nMade with ❤️ by https://github.com/LollipopKit . + \nAll rights reserved.''', replace: 'LollipopKit'), ], ), ], diff --git a/lib/view/page/private_key/edit.dart b/lib/view/page/private_key/edit.dart new file mode 100644 index 00000000..78537944 --- /dev/null +++ b/lib/view/page/private_key/edit.dart @@ -0,0 +1,98 @@ +import 'package:after_layout/after_layout.dart'; +import 'package:flutter/material.dart'; +import 'package:toolbox/data/model/private_key_info.dart'; +import 'package:toolbox/data/provider/private_key.dart'; +import 'package:toolbox/locator.dart'; + +class PrivateKeyEditPage extends StatefulWidget { + const PrivateKeyEditPage({Key? key, this.info}) : super(key: key); + + final PrivateKeyInfo? info; + + @override + _PrivateKeyEditPageState createState() => _PrivateKeyEditPageState(); +} + +class _PrivateKeyEditPageState extends State + with AfterLayoutMixin { + final nameController = TextEditingController(); + final keyController = TextEditingController(); + final pwdController = TextEditingController(); + + late PrivateKeyProvider _provider; + + @override + void initState() { + super.initState(); + _provider = locator(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Edit'), actions: [ + widget.info != null + ? IconButton( + onPressed: () { + _provider.delInfo(widget.info!); + 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: keyController, + autocorrect: false, + minLines: 3, + maxLines: 10, + keyboardType: TextInputType.text, + decoration: _buildDecoration('Private Key', icon: Icons.vpn_key), + ), + TextField( + controller: pwdController, + autocorrect: false, + keyboardType: TextInputType.text, + obscureText: true, + decoration: _buildDecoration('Password', icon: Icons.password), + ), + ], + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.send), + onPressed: () { + final info = PrivateKeyInfo( + nameController.text, keyController.text, pwdController.text); + if (widget.info != null) { + _provider.updateInfo(widget.info!, info); + } else { + _provider.addInfo(info); + } + Navigator.of(context).pop(); + }, + ), + ); + } + + InputDecoration _buildDecoration(String label, + {TextStyle? textStyle, IconData? icon}) { + return InputDecoration( + labelText: label, labelStyle: textStyle, icon: Icon(icon)); + } + + @override + void afterFirstLayout(BuildContext context) { + if (widget.info != null) { + nameController.text = widget.info!.id; + keyController.text = widget.info!.privateKey; + pwdController.text = widget.info!.password; + } + } +} diff --git a/lib/view/page/private_key/stored.dart b/lib/view/page/private_key/stored.dart new file mode 100644 index 00000000..f3c9b12a --- /dev/null +++ b/lib/view/page/private_key/stored.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:toolbox/core/route.dart'; +import 'package:toolbox/data/provider/private_key.dart'; +import 'package:toolbox/view/page/private_key/edit.dart'; +import 'package:toolbox/view/widget/round_rect_card.dart'; + +class StoredPrivateKeysPage extends StatefulWidget { + const StoredPrivateKeysPage({Key? key}) : super(key: key); + + @override + _PrivateKeyListState createState() => _PrivateKeyListState(); +} + +class _PrivateKeyListState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Private Keys'), + ), + body: Consumer( + builder: (_, key, __) { + return key.infos.isNotEmpty + ? ListView.builder( + padding: const EdgeInsets.all(13), + itemCount: key.infos.length, + itemExtent: 57, + itemBuilder: (context, idx) { + return RoundRectCard(Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + key.infos[idx].id, + textAlign: TextAlign.center, + ), + TextButton( + onPressed: () => AppRoute( + PrivateKeyEditPage(info: key.infos[idx]), + 'private key edit page') + .go(context), + child: const Text('Edit')) + ], + )); + }) + : const Center(child: Text('No saved private keys.')); + }, + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () => + AppRoute(const PrivateKeyEditPage(), 'private key edit page') + .go(context), + ), + ); + } +} diff --git a/lib/view/page/server/server_edit.dart b/lib/view/page/server/edit.dart similarity index 58% rename from lib/view/page/server/server_edit.dart rename to lib/view/page/server/edit.dart index f7f7e908..6e3999b2 100644 --- a/lib/view/page/server/server_edit.dart +++ b/lib/view/page/server/edit.dart @@ -1,8 +1,13 @@ import 'package:after_layout/after_layout.dart'; 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_private_info.dart'; +import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/locator.dart'; +import 'package:toolbox/view/page/private_key/edit.dart'; class ServerEditPage extends StatefulWidget { const ServerEditPage({Key? key, this.spi}) : super(key: key); @@ -25,6 +30,9 @@ class _ServerEditPageState extends State with AfterLayoutMixin { bool usePublicKey = false; + int _typeOptionIndex = -1; + final List _keyInfo = ['', '']; + @override void initState() { super.initState(); @@ -34,7 +42,16 @@ class _ServerEditPageState extends State with AfterLayoutMixin { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Edit')), + appBar: AppBar(title: const Text('Edit'), actions: [ + widget.spi != null + ? IconButton( + onPressed: () { + _serverProvider.delServer(widget.spi!); + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.delete)) + : const SizedBox() + ]), body: SingleChildScrollView( padding: const EdgeInsets.all(17), child: Column( @@ -49,6 +66,7 @@ class _ServerEditPageState extends State with AfterLayoutMixin { TextField( controller: ipController, keyboardType: TextInputType.text, + autocorrect: false, decoration: _buildDecoration('Host', icon: Icons.storage), ), TextField( @@ -60,6 +78,7 @@ class _ServerEditPageState extends State with AfterLayoutMixin { TextField( controller: usernameController, keyboardType: TextInputType.text, + autocorrect: false, decoration: _buildDecoration('User', icon: Icons.account_box), ), const SizedBox(height: 7), @@ -71,37 +90,59 @@ class _ServerEditPageState extends State with AfterLayoutMixin { onChanged: (val) => setState(() => usePublicKey = val)), ], ), - usePublicKey + !usePublicKey ? TextField( - controller: keyController, + controller: passwordController, + obscureText: true, keyboardType: TextInputType.text, - autocorrect: false, - maxLines: 10, - minLines: 5, - decoration: - _buildDecoration('Private Key', icon: Icons.vpn_key), + decoration: _buildDecoration('Pwd', icon: Icons.password), onSubmitted: (_) => {}, ) : const SizedBox(), - TextField( - controller: passwordController, - obscureText: true, - keyboardType: TextInputType.text, - decoration: _buildDecoration('Pwd', icon: Icons.password), - onSubmitted: (_) => {}, - ), + usePublicKey + ? Consumer(builder: (_, key, __) { + final tiles = key.infos + .map( + (e) => ListTile( + contentPadding: EdgeInsets.zero, + title: Text(e.id, textAlign: TextAlign.start), + trailing: _buildRadio(key.infos.indexOf(e), + e.privateKey, e.password)), + ) + .toList(); + tiles.add(ListTile( + title: const Text('Add a Private Key'), + contentPadding: EdgeInsets.zero, + trailing: IconButton( + icon: const Icon(Icons.add), + onPressed: () => AppRoute(const PrivateKeyEditPage(), + 'private key edit page') + .go(context), + ), + )); + return ExpansionTile( + tilePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + title: const Text( + 'Choose Key', + style: TextStyle(fontSize: 14), + ), + children: tiles, + ); + }) + : const SizedBox() ], ), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.send), onPressed: () { - final authorization = keyController.text.isEmpty - ? passwordController.text - : { - "privateKey": keyController.text, - "passphrase": passwordController.text - }; + if (usePublicKey && _typeOptionIndex == -1) { + showSnackBar(context, const Text('Please select a private key.')); + } + final authorization = usePublicKey + ? {"privateKey": _keyInfo[0], "passphrase": _keyInfo[1]} + : passwordController.text; final spi = ServerPrivateInfo( name: nameController.text, ip: ipController.text, @@ -121,6 +162,20 @@ class _ServerEditPageState extends State with AfterLayoutMixin { ); } + Radio _buildRadio(int index, String key, String pwd) { + return Radio( + value: index, + groupValue: _typeOptionIndex, + onChanged: (int? value) { + setState(() { + _typeOptionIndex = value!; + _keyInfo[0] = key; + _keyInfo[1] = pwd; + }); + }, + ); + } + InputDecoration _buildDecoration(String label, {TextStyle? textStyle, IconData? icon}) { return InputDecoration( diff --git a/lib/view/page/server/server_tab.dart b/lib/view/page/server/tab.dart similarity index 67% rename from lib/view/page/server/server_tab.dart rename to lib/view/page/server/tab.dart index b9a5f97a..68ce83b9 100644 --- a/lib/view/page/server/server_tab.dart +++ b/lib/view/page/server/tab.dart @@ -1,5 +1,5 @@ import 'package:after_layout/after_layout.dart'; -import 'package:charts_flutter/flutter.dart' as chart; +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'; @@ -9,9 +9,9 @@ import 'package:toolbox/core/route.dart'; import 'package:toolbox/data/model/server_private_info.dart'; import 'package:toolbox/data/model/server_status.dart'; import 'package:toolbox/data/provider/server.dart'; +import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/locator.dart'; -import 'package:toolbox/view/page/server/server_edit.dart'; -import 'package:toolbox/view/widget/circle_pie.dart'; +import 'package:toolbox/view/page/server/edit.dart'; class ServerPage extends StatefulWidget { const ServerPage({Key? key}) : super(key: key); @@ -24,6 +24,7 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMixin, AfterLayoutMixin { late MediaQueryData _media; late ThemeData _theme; + late Color _primaryColor; late ServerProvider _serverProvider; @@ -38,6 +39,7 @@ class _ServerPageState extends State super.didChangeDependencies(); _media = MediaQuery.of(context); _theme = Theme.of(context); + _primaryColor = Color(locator().primaryColor.fetch()!); } @override @@ -102,19 +104,6 @@ class _ServerPageState extends State } Widget _buildRealServerCard(ServerStatus ss, String serverName) { - final cpuData = [ - IndexPercent(0, ss.cpuPercent!.toInt()), - IndexPercent(1, 100 - ss.cpuPercent!.toInt()), - ]; - final memData = []; - for (var e in ss.memList!) { - memData.add(IndexPercent(ss.memList!.indexOf(e), e!.toInt())); - } - - final mem1 = memData[1]; - memData[1] = memData.last; - memData.last = mem1; - final rootDisk = ss.disk!.firstWhere((element) => element!.mountLocation == '/'); @@ -139,23 +128,9 @@ class _ServerPageState extends State Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildPercentCircle(ss.cpuPercent!, 'CPU', [ - chart.Series( - id: 'CPU', - domainFn: (IndexPercent cpu, _) => cpu.id, - measureFn: (IndexPercent cpu, _) => cpu.percent, - data: cpuData, - ) - ]), + _buildPercentCircle(ss.cpuPercent! + 0.01, 'CPU'), _buildPercentCircle( - ss.memList![1]! / ss.memList![0]! * 100, 'Mem', [ - chart.Series( - id: 'Mem', - domainFn: (IndexPercent mem, _) => mem.id, - measureFn: (IndexPercent mem, _) => mem.percent, - data: memData, - ) - ]), + ss.memList![1]! / ss.memList![0]! * 100 + 0.01, 'Mem'), _buildIOData('Net', 'Conn:\n' + ss.tcp!.maxConn!.toString(), 'Fail:\n' + ss.tcp!.fail.toString()), _buildIOData('Disk', 'Total:\n' + rootDisk!.size!, @@ -172,59 +147,53 @@ class _ServerPageState extends State return SizedBox( width: _media.size.width * 0.2, height: _media.size.height * 0.1, - child: Stack( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Positioned.fill( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - up, - style: statusTextStyle, - textAlign: TextAlign.center, - ), - const SizedBox(height: 3), - Text( - down + '\n', - style: statusTextStyle, - textAlign: TextAlign.center, - ) - ], - ), - ), + Text( + up, + style: statusTextStyle, + textAlign: TextAlign.center, ), - Positioned( - child: Text(title, textAlign: TextAlign.center), - bottom: 0, - left: 0, - right: 0) + const SizedBox(height: 3), + Text( + down + '\n', + style: statusTextStyle, + textAlign: TextAlign.center, + ), + Text(title, textAlign: TextAlign.center) ], ), ); } - Widget _buildPercentCircle(double percent, String title, - List> series) { + Widget _buildPercentCircle(double percent, String title) { return SizedBox( width: _media.size.width * 0.2, height: _media.size.height * 0.1, - child: Stack( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - DonutPieChart(series), - Positioned.fill( - child: Center( - child: Text( - '${percent.toStringAsFixed(1)}%\n', - textAlign: TextAlign.center, + Stack( + children: [ + CircleChart( + progressColor: _primaryColor, + progressNumber: percent, + maxNumber: 100, + width: _media.size.width * 0.37, + height: _media.size.height * 0.09, ), - ), + Positioned.fill( + child: Center( + child: Text( + '${percent.toStringAsFixed(1)}%', + textAlign: TextAlign.center, + ), + ), + ), + ], ), - Positioned( - child: Text(title, textAlign: TextAlign.center), - bottom: 0, - left: 0, - right: 0) + Text(title, textAlign: TextAlign.center), ], ), ); diff --git a/lib/view/page/setting.dart b/lib/view/page/setting.dart new file mode 100644 index 00000000..0e9ed07e --- /dev/null +++ b/lib/view/page/setting.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_material_color_picker/flutter_material_color_picker.dart'; +import 'package:toolbox/core/utils.dart'; +import 'package:toolbox/data/store/setting.dart'; +import 'package:toolbox/locator.dart'; +import 'package:toolbox/view/widget/round_rect_card.dart'; + +class SettingPage extends StatefulWidget { + const SettingPage({Key? key}) : super(key: key); + + @override + _SettingPageState createState() => _SettingPageState(); +} + +class _SettingPageState extends State { + late SettingStore _store; + late int _selectedColorValue; + final TextEditingController _intervalController = TextEditingController(); + + @override + void initState() { + super.initState(); + _store = locator(); + _intervalController.text = + _store.serverStatusUpdateInterval.fetch()!.toString(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Setting'), + ), + body: ListView( + padding: const EdgeInsets.all(17), + children: [ + RoundRectCard(_buildAppColorPreview()), + RoundRectCard( + ListTile( + contentPadding: EdgeInsets.zero, + title: const Text( + 'Server status update interval (seconds)', + style: TextStyle(fontSize: 14), + textAlign: TextAlign.start, + ), + trailing: SizedBox( + width: MediaQuery.of(context).size.width * 0.1, + child: TextField( + textAlign: TextAlign.center, + controller: _intervalController, + keyboardType: TextInputType.number, + onSubmitted: (val) { + _store.serverStatusUpdateInterval.put(int.parse(val)); + showSnackBar( + context, + const Text( + 'This setting will take effect \nthe next time app launch')); + }, + ), + ), + ), + ) + ], + ), + ); + } + + Widget _buildAppColorPreview() { + final nowAppColor = _store.primaryColor.fetch()!; + return ExpansionTile( + tilePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + children: [ + _buildAppColorPicker(Color(nowAppColor)), + _buildColorPickerConfirmBtn() + ], + trailing: ClipOval( + child: Container( + color: Color(nowAppColor), + height: 27, + width: 27, + ), + ), + title: const Text( + 'App primary color', + style: TextStyle(fontSize: 14), + )); + } + + Widget _buildAppColorPicker(Color selected) { + return MaterialColorPicker( + shrinkWrap: true, + onColorChange: (Color color) { + _selectedColorValue = color.value; + }, + selectedColor: selected); + } + + Widget _buildColorPickerConfirmBtn() { + return IconButton( + icon: const Icon(Icons.save), + onPressed: (() { + _store.primaryColor.put(_selectedColorValue); + setState(() {}); + }), + ); + } +} diff --git a/lib/view/widget/circle_pie.dart b/lib/view/widget/circle_pie.dart deleted file mode 100644 index 556495cd..00000000 --- a/lib/view/widget/circle_pie.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:charts_flutter/flutter.dart' as charts; -import 'package:flutter/material.dart'; - -class DonutPieChart extends StatelessWidget { - final List> seriesList; - final bool animate; - - const DonutPieChart(this.seriesList, {Key? key, this.animate = false}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return charts.PieChart(seriesList, - animate: animate, - layoutConfig: charts.LayoutConfig( - leftMarginSpec: charts.MarginSpec.fixedPixel(1), - topMarginSpec: charts.MarginSpec.fixedPixel(1), - rightMarginSpec: charts.MarginSpec.fixedPixel(1), - bottomMarginSpec: charts.MarginSpec.fixedPixel(17)), - defaultRenderer: charts.ArcRendererConfig( - arcWidth: 6, - arcRatio: 0.2, - )); - } -} - -class IndexPercent { - final int id; - final int percent; - - IndexPercent(this.id, this.percent); -} diff --git a/lib/view/widget/round_rect_card.dart b/lib/view/widget/round_rect_card.dart new file mode 100644 index 00000000..220acb1c --- /dev/null +++ b/lib/view/widget/round_rect_card.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class RoundRectCard extends StatelessWidget { + const RoundRectCard(this.child, {Key? key}) : super(key: key); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 17), + child: child, + ), + margin: const EdgeInsets.symmetric(vertical: 7), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(17))), + ); + } +} diff --git a/lib/view/widget/url_text.dart b/lib/view/widget/url_text.dart new file mode 100644 index 00000000..3788d1bb --- /dev/null +++ b/lib/view/widget/url_text.dart @@ -0,0 +1,91 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:toolbox/core/utils.dart'; + +const regUrl = + r"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]*"; + +class UrlText extends StatelessWidget { + final String text; + final String? replace; + final TextStyle style; + + const UrlText( + {Key? key, + required this.text, + this.replace, + this.style = const TextStyle()}) + : super(key: key); + + List _getTextSpans(bool isDarkMode) { + List widgets = []; + final reg = RegExp(regUrl); + Iterable _matches = reg.allMatches(text); + List<_ResultMatch> resultMatches = <_ResultMatch>[]; + int start = 0; + + for (Match match in _matches) { + final group0 = match.group(0); + if (group0 != null && group0.isNotEmpty) { + if (start != match.start) { + _ResultMatch result1 = _ResultMatch(); + result1.isUrl = false; + result1.text = text.substring(start, match.start); + resultMatches.add(result1); + } + + _ResultMatch result2 = _ResultMatch(); + result2.isUrl = true; + result2.text = match.group(0)!; + resultMatches.add(result2); + start = match.end; + } + } + + if (start < text.length) { + _ResultMatch result1 = _ResultMatch(); + result1.isUrl = false; + result1.text = text.substring(start); + resultMatches.add(result1); + } + + for (var result in resultMatches) { + if (result.isUrl) { + widgets.add(_LinkTextSpan( + replace: replace ?? result.text, + text: result.text, + style: style.copyWith(color: Colors.blue))); + } else { + widgets.add(TextSpan( + text: result.text, + style: style.copyWith( + color: isDarkMode ? Colors.white : Colors.black, + ))); + } + } + return widgets; + } + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan(children: _getTextSpans(isDarkMode(context))), + ); + } +} + +class _LinkTextSpan extends TextSpan { + _LinkTextSpan({TextStyle? style, required String text, String? replace}) + : super( + style: style, + text: replace, + recognizer: TapGestureRecognizer() + ..onTap = () { + openUrl(text); + }); +} + +class _ResultMatch { + late bool isUrl; + late String text; +} diff --git a/pubspec.lock b/pubspec.lock index 69840ccb..ce8fc000 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -36,20 +36,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" - charts_common: - dependency: transitive - description: - name: charts_common - url: "https://pub.dartlang.org" - source: hosted - version: "0.11.0" - charts_flutter: + circle_chart: dependency: "direct main" description: - name: charts_flutter - url: "https://pub.dartlang.org" - source: hosted - version: "0.11.0" + path: "." + ref: main + resolved-ref: f3e0088bb08c5d05473b1d568943f4f8732dd984 + url: "https://github.com/LollipopKit/circle_chart" + source: git + version: "0.0.3" clock: dependency: transitive description: @@ -86,7 +81,7 @@ packages: name: dio url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" extended_image: dependency: "direct main" description: @@ -134,13 +129,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + flutter_material_color_picker: + dependency: "direct main" + description: + name: flutter_material_color_picker + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0+2" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" flutter_staggered_animations: dependency: "direct main" description: @@ -185,7 +187,7 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.13.3" + version: "0.13.4" http_client_helper: dependency: transitive description: @@ -200,13 +202,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.0" - intl: - dependency: transitive - description: - name: intl - url: "https://pub.dartlang.org" - source: hosted - version: "0.17.0" js: dependency: transitive description: @@ -318,7 +313,7 @@ packages: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.2.3" + version: "4.2.4" provider: dependency: "direct main" description: @@ -435,7 +430,7 @@ packages: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.4" + version: "3.0.5" vector_math: dependency: transitive description: @@ -449,7 +444,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.2.9" + version: "2.2.10" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f7346c4c..3c99ff02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,8 +44,12 @@ dependencies: url: https://github.com/Countly/countly-sdk-flutter-bridge.git ref: master ssh2: ^2.2.3 - charts_flutter: ^0.11.0 logging: ^1.0.2 + flutter_material_color_picker: ^1.1.0+2 + circle_chart: + git: + url: https://github.com/LollipopKit/circle_chart + ref: main dev_dependencies: