Compare commits

...

7 Commits

Author SHA1 Message Date
LollipopKit
de1e970108 Add r/w permission 2021-11-21 19:45:42 +08:00
LollipopKit
9ef59f4c12 Fix: app update issue on MIUI 2021-11-21 19:36:07 +08:00
LollipopKit
89ef2cb95c Will display the exception of Server Connection 2021-11-08 19:13:24 +08:00
LollipopKit
e0fb591dea Simply implement snippet running. 2021-11-06 14:05:03 +08:00
LollipopKit
7c34530821 Fix update things 2021-11-02 20:58:50 +08:00
LollipopKit
72c1901989 Add check update btn in setting 2021-11-02 20:31:44 +08:00
LollipopKit
ff76c6c539 Allow keep data on uninstalling 2021-11-02 19:49:29 +08:00
23 changed files with 418 additions and 95 deletions

View File

@@ -31,15 +31,16 @@ A new Flutter project which provide a chart view to display server status data.
</table> </table>
## Milestone ## Milestone
- [x] SSH Connect - [x] SSH connect
- [x] Server Info Store - [x] Server info store
- [x] Status Chart View - [x] Status chart view
- [x] Base64/Url En/Decode - [x] Base64/Url En/Decode
- [x] Private Key Store - [x] Private key store
- [x] Server Status Detail Page - [x] Server status detail page
- [x] Theme Switch - [x] Theme switch
- [ ] Execute Snippet - [ ] Execute snippet
- [ ] Migrate from `ssh2` to `dartssh2` - [ ] Migrate from `ssh2` to `dartssh2`
- [ ] Desktop support.
## Build ## Build
Please use `make.dart` to build. Please use `make.dart` to build.

View File

@@ -1,8 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="tech.lolli.toolbox"> 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:label="ServerBox"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:hasFragileUserData="true"
android:restoreAnyVersion="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"

View File

@@ -6,7 +6,10 @@ class Analysis {
static const _url = 'https://countly.xuty.cc'; static const _url = 'https://countly.xuty.cc';
static const _key = '80372a2a66424b32d0ac8991bfa1ef058bd36b1f'; static const _key = '80372a2a66424b32d0ac8991bfa1ef058bd36b1f';
static bool _enabled = false;
static Future<void> init(bool debug) async { static Future<void> init(bool debug) async {
_enabled = true;
await Countly.setLoggingEnabled(debug); await Countly.setLoggingEnabled(debug);
await Countly.init(_url, _key); await Countly.init(_url, _key);
await Countly.start(); await Countly.start();
@@ -15,10 +18,14 @@ class Analysis {
} }
static void recordView(String view) { static void recordView(String view) {
Countly.recordView(view); if (_enabled) {
Countly.recordView(view);
}
} }
static void recordException(Object exception, [bool fatal = false]) { static void recordException(Object exception, [bool fatal = false]) {
Countly.logException(exception.toString(), !fatal, null); if (_enabled) {
Countly.logException(exception.toString(), !fatal, null);
}
} }
} }

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:r_upgrade/r_upgrade.dart';
import 'package:toolbox/core/utils.dart'; import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/provider/app.dart'; import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/res/build_data.dart'; import 'package:toolbox/data/res/build_data.dart';
@@ -39,8 +40,14 @@ Future<void> doUpdate(BuildContext context, {bool force = false}) async {
showSnackBarWithAction( showSnackBarWithAction(
context, context,
update.min > BuildData.build update.min > BuildData.build
? '您的版本过旧,请及时更新' ? 'Your version is too old. \nPlease update to v1.0.${update.newest}.'
: '${BuildData.name}有更新啦Ver${update.newest}\n${update.changelog}', : 'Update: v1.0.${update.newest} available. \n${update.changelog}',
'更新', 'Update', () async {
() => openUrl(Platform.isAndroid ? update.android : update.ios)); 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.'));
}
});
} }

View File

@@ -15,13 +15,13 @@ bool isDarkMode(BuildContext context) =>
void showSnackBar(BuildContext context, Widget child) => void showSnackBar(BuildContext context, Widget child) =>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: child)); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: child));
void showSnackBarWithAction( void showSnackBarWithAction(BuildContext context, String content, String action,
BuildContext context, String content, String action, Function onTap) { GestureTapCallback onTap) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(content), content: Text(content),
action: SnackBarAction( action: SnackBarAction(
label: action, label: action,
onPressed: () => onTap, onPressed: onTap,
), ),
)); ));
} }

View File

@@ -5,5 +5,11 @@ class Memory {
int shared; int shared;
int cache; int cache;
int avail; 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});
} }

View File

@@ -37,7 +37,8 @@ class ServerStatus {
List<DiskInfo> disk; List<DiskInfo> disk;
TcpStatus tcp; TcpStatus tcp;
NetSpeed netSpeed; NetSpeed netSpeed;
String? failedInfo;
ServerStatus(this.cpu2Status, this.memory, this.sysVer, this.uptime, ServerStatus(this.cpu2Status, this.memory, this.sysVer, this.uptime,
this.disk, this.tcp, this.netSpeed); this.disk, this.tcp, this.netSpeed, {this.failedInfo});
} }

View File

@@ -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.dart';
import 'package:toolbox/data/model/server/server_private_info.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/server_status.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/model/server/tcp_status.dart'; import 'package:toolbox/data/model/server/tcp_status.dart';
import 'package:toolbox/data/store/server.dart'; import 'package:toolbox/data/store/server.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
@@ -140,6 +141,7 @@ class ServerProvider extends BusyProvider {
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
_servers[idx].connectionState = ServerConnectionState.failed; _servers[idx].connectionState = ServerConnectionState.failed;
_servers[idx].status.failedInfo = e.toString().split(', ')[1];
notifyListeners(); notifyListeners();
logger.warning(e); logger.warning(e);
} }
@@ -290,4 +292,8 @@ class ServerProvider extends BusyProvider {
} }
return emptyMemory; return emptyMemory;
} }
Future<String?> runSnippet(int idx, Snippet snippet) {
return _servers[idx].client.execute(snippet.script);
}
} }

View 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();
}
}

View File

@@ -2,9 +2,8 @@
class BuildData { class BuildData {
static const String name = "ToolBox"; static const String name = "ToolBox";
static const int build = 55; static const int build = 64;
static const String engine = static const String engine = "Flutter 2.5.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 18116933e7 (5 weeks ago) • 2021-10-15 10:46:35 -0700\nEngine • revision d3ea636dc5\nTools • Dart 2.14.4\n";
"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-21 19:42:23.223010";
static const String buildAt = "2021-11-02 15:32:29.280614"; static const int modifications = 2;
static const int modifications = 1;
} }

View File

@@ -3,6 +3,7 @@ import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/debug.dart'; import 'package:toolbox/data/provider/debug.dart';
import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.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/service/app.dart';
import 'package:toolbox/data/store/private_key.dart'; import 'package:toolbox/data/store/private_key.dart';
import 'package:toolbox/data/store/server.dart'; import 'package:toolbox/data/store/server.dart';
@@ -19,6 +20,7 @@ void setupLocatorForProviders() {
locator.registerSingleton(AppProvider()); locator.registerSingleton(AppProvider());
locator.registerSingleton(DebugProvider()); locator.registerSingleton(DebugProvider());
locator.registerSingleton(ServerProvider()); locator.registerSingleton(ServerProvider());
locator.registerSingleton(SnippetProvider());
locator.registerSingleton(PrivateKeyProvider()); locator.registerSingleton(PrivateKeyProvider());
} }

View File

@@ -10,11 +10,13 @@ import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/debug.dart'; import 'package:toolbox/data/provider/debug.dart';
import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/data/provider/snippet.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
Future<void> initApp() async { Future<void> initApp() async {
await Hive.initFlutter(); await Hive.initFlutter();
await setupLocator(); await setupLocator();
locator<SnippetProvider>().loadData();
locator<PrivateKeyProvider>().loadData(); locator<PrivateKeyProvider>().loadData();
///设置Logger ///设置Logger
@@ -62,6 +64,7 @@ Future<void> main() async {
ChangeNotifierProvider(create: (_) => locator<AppProvider>()), ChangeNotifierProvider(create: (_) => locator<AppProvider>()),
ChangeNotifierProvider(create: (_) => locator<DebugProvider>()), ChangeNotifierProvider(create: (_) => locator<DebugProvider>()),
ChangeNotifierProvider(create: (_) => locator<ServerProvider>()), ChangeNotifierProvider(create: (_) => locator<ServerProvider>()),
ChangeNotifierProvider(create: (_) => locator<SnippetProvider>()),
ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()), ChangeNotifierProvider(create: (_) => locator<PrivateKeyProvider>()),
], ],
child: const MyApp(), child: const MyApp(),

View File

@@ -13,7 +13,7 @@ import 'package:toolbox/data/res/url.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/page/convert.dart'; import 'package:toolbox/view/page/convert.dart';
import 'package:toolbox/view/page/debug.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/server/tab.dart';
import 'package:toolbox/view/page/setting.dart'; import 'package:toolbox/view/page/setting.dart';
import 'package:toolbox/view/page/snippet/list.dart'; import 'package:toolbox/view/page/snippet/list.dart';
@@ -92,8 +92,7 @@ class _MyHomePageState extends State<MyHomePage>
leading: const Icon(Icons.snippet_folder), leading: const Icon(Icons.snippet_folder),
title: const Text('Snippet'), title: const Text('Snippet'),
onTap: () => onTap: () =>
AppRoute(const SnippetListPage(), 'snippet list') AppRoute(const SnippetListPage(), 'snippet list').go(context),
.go(context),
), ),
AboutListTile( AboutListTile(
icon: const Icon(Icons.text_snippet), icon: const Icon(Icons.text_snippet),

View File

@@ -1,5 +1,6 @@
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.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/model/server/private_key_info.dart';
import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/private_key.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
@@ -70,8 +71,15 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.send), child: const Icon(Icons.send),
onPressed: () { onPressed: () {
final info = PrivateKeyInfo( final name = nameController.text;
nameController.text, keyController.text, pwdController.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) { if (widget.info != null) {
_provider.updateInfo(widget.info!, info); _provider.updateInfo(widget.info!, info);
} else { } else {

View File

@@ -44,12 +44,16 @@ class _ServerDetailPageState extends State<ServerDetailPage>
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(si.info.name), title: Text(si.info.name),
actions: [IconButton(onPressed: () => AppRoute( actions: [
ServerEditPage( IconButton(
spi: si.info, onPressed: () => AppRoute(
), ServerEditPage(
'Edit server info page') spi: si.info,
.go(context), icon: const Icon(Icons.edit))], ),
'Edit server info page')
.go(context),
icon: const Icon(Icons.edit))
],
), ),
body: ListView( body: ListView(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),

View File

@@ -59,6 +59,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
onPressed: () { onPressed: () {
_serverProvider.delServer(widget.spi!); _serverProvider.delServer(widget.spi!);
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pop();
}, },
child: const Text( child: const Text(
'Yes', 'Yes',

View File

@@ -91,11 +91,11 @@ class _ServerPageState extends State<ServerPage>
return Card( return Card(
child: InkWell( child: InkWell(
onLongPress: () => AppRoute( onLongPress: () => AppRoute(
ServerEditPage( ServerEditPage(
spi: si.info, spi: si.info,
), ),
'Edit server info page') 'Edit server info page')
.go(context), .go(context),
child: Padding( child: Padding(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
child: child:
@@ -124,7 +124,7 @@ class _ServerPageState extends State<ServerPage>
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
textScaleFactor: 1.0, textScaleFactor: 1.0,
), ),
Text(getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime), Text(getTopRightStr(cs, ss.cpu2Status.temp, ss.uptime, ss.failedInfo),
textScaleFactor: 1.0, textScaleFactor: 1.0,
style: TextStyle( style: TextStyle(
color: _theme.textTheme.bodyText1!.color!.withAlpha(100), color: _theme.textTheme.bodyText1!.color!.withAlpha(100),
@@ -149,7 +149,7 @@ 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) { switch (cs) {
case ServerConnectionState.disconnected: case ServerConnectionState.disconnected:
return 'Disconnected'; return 'Disconnected';
@@ -158,7 +158,7 @@ class _ServerPageState extends State<ServerPage>
case ServerConnectionState.connecting: case ServerConnectionState.connecting:
return 'Connecting...'; return 'Connecting...';
case ServerConnectionState.failed: case ServerConnectionState.failed:
return 'Failed'; return failedInfo ?? 'Failed';
default: default:
return 'Unknown State'; return 'Unknown State';
} }

View File

@@ -1,5 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart'; 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/res/build_data.dart';
import 'package:toolbox/data/res/color.dart'; import 'package:toolbox/data/res/color.dart';
import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/data/store/setting.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
@@ -17,6 +21,7 @@ class _SettingPageState extends State<SettingPage> {
late int _selectedColorValue; late int _selectedColorValue;
double _intervalValue = 0; double _intervalValue = 0;
late Color priColor; late Color priColor;
static const textStyle = TextStyle(fontSize: 14);
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@@ -40,56 +45,83 @@ class _SettingPageState extends State<SettingPage> {
body: ListView( body: ListView(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
children: [ children: [
RoundRectCard(_buildAppColorPreview()), _buildAppColorPreview(),
RoundRectCard( _buildUpdateInterval(),
ExpansionTile( _buildCheckUpdate()
tilePadding: EdgeInsets.zero, ].map((e) => RoundRectCard(e)).toList(),
childrenPadding: EdgeInsets.zero,
textColor: priColor,
title: const Text(
'Server status update interval',
style: TextStyle(fontSize: 14),
textAlign: TextAlign.start,
),
subtitle: const Text(
'Will take effect the next time app launches.',
style: TextStyle(color: Colors.grey),
),
trailing: Text('${_intervalValue.toInt()} s'),
children: [
Slider(
thumbColor: priColor,
activeColor: priColor.withOpacity(0.7),
min: 0,
max: 10,
value: _intervalValue,
onChanged: (newValue) {
setState(() {
_intervalValue = newValue;
});
},
onChangeEnd: (val) =>
_store.serverStatusUpdateInterval.put(val.toInt()),
label: '${_intervalValue.toInt()} seconds',
divisions: 10,
),
const SizedBox(
height: 3,
),
_intervalValue == 0.0
? const Text('You set to 0, will not update automatically.')
: const SizedBox(),
const SizedBox(
height: 13,
)
],
),
)
],
), ),
); );
} }
Widget _buildCheckUpdate() {
return Consumer<AppProvider>(builder: (_, app, __) {
String display;
if (app.newestBuild != null) {
if (app.newestBuild! > BuildData.build) {
display = 'Found: v1.0.${app.newestBuild}, click to update';
} else {
display = 'Current: v1.0.${BuildData.build}is up to date';
}
} else {
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));
});
}
Widget _buildUpdateInterval() {
return ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
textColor: priColor,
title: const Text(
'Server status update interval',
style: textStyle,
textAlign: TextAlign.start,
),
subtitle: const Text(
'Will take effect the next time app launches.',
style: TextStyle(color: Colors.grey),
),
trailing: Text('${_intervalValue.toInt()} s'),
children: [
Slider(
thumbColor: priColor,
activeColor: priColor.withOpacity(0.7),
min: 0,
max: 10,
value: _intervalValue,
onChanged: (newValue) {
setState(() {
_intervalValue = newValue;
});
},
onChangeEnd: (val) =>
_store.serverStatusUpdateInterval.put(val.toInt()),
label: '${_intervalValue.toInt()} seconds',
divisions: 10,
),
const SizedBox(
height: 3,
),
_intervalValue == 0.0
? const Text('You set to 0, will not update automatically.')
: const SizedBox(),
const SizedBox(
height: 13,
)
],
);
}
Widget _buildAppColorPreview() { Widget _buildAppColorPreview() {
return ExpansionTile( return ExpansionTile(
textColor: priColor, textColor: priColor,
@@ -108,7 +140,7 @@ class _SettingPageState extends State<SettingPage> {
), ),
title: const Text( title: const Text(
'App primary color', 'App primary color',
style: TextStyle(fontSize: 14), style: textStyle,
)); ));
} }

View File

@@ -1,15 +1,91 @@
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.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 { class SnippetEditPage extends StatefulWidget {
const SnippetEditPage({Key? key}) : super(key: key); const SnippetEditPage({Key? key, this.snippet}) : super(key: key);
final Snippet? snippet;
@override @override
_SnippetEditPageState createState() => _SnippetEditPageState(); _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container(); 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;
}
} }
} }

View File

@@ -1,4 +1,14 @@
import 'package:flutter/material.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/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 { class SnippetListPage extends StatefulWidget {
const SnippetListPage({Key? key}) : super(key: key); const SnippetListPage({Key? key}) : super(key: key);
@@ -8,8 +18,121 @@ class SnippetListPage extends StatefulWidget {
} }
class _SnippetListPageState extends State<SnippetListPage> { class _SnippetListPageState extends State<SnippetListPage> {
int _selectedIndex = 0;
final _textStyle = TextStyle(color: primaryColor);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container(); 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')),
]);
} }
} }

View File

@@ -363,6 +363,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.1" version: "6.0.1"
r_upgrade:
dependency: "direct main"
description:
name: r_upgrade
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.6"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

@@ -51,6 +51,7 @@ dependencies:
url: https://github.com/LollipopKit/circle_chart url: https://github.com/LollipopKit/circle_chart
ref: main ref: main
clipboard: ^0.1.3 clipboard: ^0.1.3
r_upgrade: ^0.3.6
dev_dependencies: dev_dependencies: