mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 15:24:35 +01:00
50% sftp
This commit is contained in:
@@ -354,7 +354,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 96;
|
||||
CURRENT_PROJECT_VERSION = 97;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -362,7 +362,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.96;
|
||||
MARKETING_VERSION = 1.0.97;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -484,7 +484,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 96;
|
||||
CURRENT_PROJECT_VERSION = 97;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -492,7 +492,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.96;
|
||||
MARKETING_VERSION = 1.0.97;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
@@ -508,7 +508,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 96;
|
||||
CURRENT_PROJECT_VERSION = 97;
|
||||
DEVELOPMENT_TEAM = BA88US33G6;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -516,7 +516,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.96;
|
||||
MARKETING_VERSION = 1.0.97;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
|
||||
@@ -191,8 +191,9 @@ class ServerProvider extends BusyProvider {
|
||||
} catch (e) {
|
||||
_servers[idx].connectionState = ServerConnectionState.failed;
|
||||
servers[idx].status.failedInfo = e.toString();
|
||||
notifyListeners();
|
||||
logger.warning(e);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +220,6 @@ class ServerProvider extends BusyProvider {
|
||||
results.add(NetSpeedPart(device, bytesIn, bytesOut, time));
|
||||
}
|
||||
info.status.netSpeed = info.status.netSpeed.update(results);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _getSysVer(ServerPrivateInfo spi, String raw) {
|
||||
@@ -228,8 +228,6 @@ class ServerProvider extends BusyProvider {
|
||||
if (s.length == 2) {
|
||||
info.status.sysVer = s[1].replaceAll('"', '').replaceFirst('\n', '');
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _getCPUTemp(String raw) {
|
||||
@@ -264,14 +262,11 @@ class ServerProvider extends BusyProvider {
|
||||
info.status.cpu2Status =
|
||||
info.status.cpu2Status.update(cpus, _getCPUTemp(temp));
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _getUpTime(ServerPrivateInfo spi, String raw) {
|
||||
_servers.firstWhere((e) => e.info == spi).status.uptime =
|
||||
raw.split('up ')[1].split(', ')[0];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _getTcp(ServerPrivateInfo spi, String raw) {
|
||||
@@ -283,7 +278,6 @@ class ServerProvider extends BusyProvider {
|
||||
final vals = idx.split(RegExp(r'\s{1,}'));
|
||||
info.status.tcp = TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _getDisk(ServerPrivateInfo spi, String raw) {
|
||||
@@ -299,7 +293,6 @@ class ServerProvider extends BusyProvider {
|
||||
int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3]));
|
||||
}
|
||||
info.status.disk = list;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _getMem(ServerPrivateInfo spi, String raw) {
|
||||
@@ -318,7 +311,6 @@ class ServerProvider extends BusyProvider {
|
||||
avail: memList[5]);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async {
|
||||
|
||||
@@ -4,7 +4,7 @@ class BuildData {
|
||||
static const String name = "ServerBox";
|
||||
static const int build = 97;
|
||||
static const String engine =
|
||||
"Flutter 2.10.0 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 5f105a6ca7 (7 days ago) • 2022-02-01 14:15:42 -0800\nEngine • revision 776efd2034\nTools • Dart 2.16.0 • DevTools 2.9.2\n";
|
||||
static const String buildAt = "2022-02-09 10:58:45.586008";
|
||||
static const int modifications = 8;
|
||||
"Flutter 2.8.1 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 77d935af4d (8 weeks ago) • 2021-12-16 08:37:33 -0800\nEngine • revision 890a5fca2e\nTools • Dart 2.15.1\n";
|
||||
static const String buildAt = "2022-02-10 19:30:23.388434";
|
||||
static const int modifications = 9;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ class _MyHomePageState extends State<MyHomePage>
|
||||
animationDuration: const Duration(milliseconds: 300),
|
||||
animateChildDecoration: true,
|
||||
rtlOpening: false,
|
||||
disabledGestures: true,
|
||||
childDecoration: const BoxDecoration(
|
||||
// NOTICE: Uncomment if you want to add shadow behind the page.
|
||||
// Keep in mind that it may cause animation jerks.
|
||||
|
||||
@@ -49,7 +49,8 @@ class _PingPageState extends State<PingPage>
|
||||
RoundRectCard(
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(padding: const EdgeInsets.all(7), child: Text(_result)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(7), child: Text(_result)),
|
||||
),
|
||||
),
|
||||
])),
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:toolbox/data/store/setting.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/page/server/detail.dart';
|
||||
import 'package:toolbox/view/page/server/edit.dart';
|
||||
import 'package:toolbox/view/page/sftp.dart';
|
||||
import 'package:toolbox/view/page/snippet/list.dart';
|
||||
import 'package:toolbox/view/widget/round_rect_card.dart';
|
||||
|
||||
@@ -241,9 +242,16 @@ class _ServerPageState extends State<ServerPage>
|
||||
switch (item) {
|
||||
case MenuItems.ssh:
|
||||
case MenuItems.apt:
|
||||
case MenuItems.sftp:
|
||||
showSnackBar(context, const Text('Now is not supported'));
|
||||
break;
|
||||
case MenuItems.sftp:
|
||||
AppRoute(
|
||||
SFTPPage(
|
||||
spi: spi,
|
||||
),
|
||||
'SFTP')
|
||||
.go(context);
|
||||
break;
|
||||
case MenuItems.snippet:
|
||||
AppRoute(
|
||||
SnippetListPage(
|
||||
|
||||
337
lib/view/page/sftp.dart
Normal file
337
lib/view/page/sftp.dart
Normal file
@@ -0,0 +1,337 @@
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/utils.dart';
|
||||
import 'package:toolbox/data/model/server/server_connection_state.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/provider/server.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
import 'package:toolbox/view/widget/fade_in.dart';
|
||||
|
||||
class SFTPPage extends StatefulWidget {
|
||||
final ServerPrivateInfo? spi;
|
||||
const SFTPPage({this.spi, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SFTPPageState createState() => _SFTPPageState();
|
||||
}
|
||||
|
||||
class _SFTPPageState extends State<SFTPPage> {
|
||||
/// Whether the Left/Right Destination is selected.
|
||||
final List<bool> _selectedDest = List<bool>.filled(2, false);
|
||||
final List<ServerPrivateInfo?> _destSpi =
|
||||
List<ServerPrivateInfo?>.filled(2, null);
|
||||
final List<List<SftpName>?> _files = List<List<SftpName>?>.filled(2, null);
|
||||
final List<String> _paths = List<String>.filled(2, '');
|
||||
final List<SftpClient?> _clients = List<SftpClient?>.filled(2, null);
|
||||
|
||||
final ScrollController _leftScrollController = ScrollController();
|
||||
final ScrollController _rightScrollController = ScrollController();
|
||||
|
||||
late MediaQueryData _media;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.spi != null) {
|
||||
_destSpi[0] = widget.spi;
|
||||
_selectedDest[0] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(_titleText),
|
||||
),
|
||||
body: Row(
|
||||
children: [
|
||||
_buildSingleColumn(true),
|
||||
const VerticalDivider(
|
||||
width: 2,
|
||||
),
|
||||
_buildSingleColumn(false),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String get _titleText {
|
||||
List<String> titles = [
|
||||
'',
|
||||
'',
|
||||
];
|
||||
if (_selectedDest[0]) {
|
||||
titles[0] = _destSpi[0]?.name ?? '';
|
||||
}
|
||||
if (_selectedDest[1]) {
|
||||
titles[1] = _destSpi[1]?.name ?? '';
|
||||
}
|
||||
return titles[0] == '' || titles[1] == '' ? 'SFTP' : titles.join(' - ');
|
||||
}
|
||||
|
||||
Widget _buildSingleColumn(bool left) {
|
||||
Widget child;
|
||||
if (!_selectedDest[left ? 0 : 1]) {
|
||||
child = _buildDestSelector(left);
|
||||
} else {
|
||||
child = _buildFileView(left);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: (_media.size.width - 2) / 2,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget get centerCircleLoading => Center(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: _media.size.height * 0.4,
|
||||
),
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildFileView(bool left) {
|
||||
final spi = _destSpi[left ? 0 : 1];
|
||||
final si =
|
||||
locator<ServerProvider>().servers.firstWhere((s) => s.info == spi);
|
||||
final client = si.client;
|
||||
if (client == null ||
|
||||
si.connectionState != ServerConnectionState.connected) {
|
||||
return centerCircleLoading;
|
||||
}
|
||||
|
||||
if (_files[left ? 0 : 1] == null) {
|
||||
updatePath('/', left);
|
||||
listDir(client, '/', left);
|
||||
return centerCircleLoading;
|
||||
} else {
|
||||
return RefreshIndicator(
|
||||
child: FadeIn(
|
||||
child: ListView.builder(
|
||||
itemCount: _files[left ? 0 : 1]!.length,
|
||||
controller: left ? _leftScrollController : _rightScrollController,
|
||||
itemBuilder: (context, index) {
|
||||
final file = _files[left ? 0 : 1]![index];
|
||||
final isDir = file.attr.mode?.isDirectory ?? true;
|
||||
return ListTile(
|
||||
leading:
|
||||
Icon(isDir ? Icons.folder : Icons.insert_drive_file),
|
||||
title: Text(file.filename),
|
||||
subtitle: isDir
|
||||
? null
|
||||
: Text((convertBytes(file.attr.size ?? 0))),
|
||||
onTap: () {
|
||||
if (isDir) {
|
||||
updatePath(file.filename, left);
|
||||
listDir(client, _paths[left ? 0 : 1], left);
|
||||
} else {
|
||||
// downloadFile(client, file.name);
|
||||
}
|
||||
},
|
||||
onLongPress: () => onItemLongPress(context, left, file));
|
||||
},
|
||||
),
|
||||
key: Key(_paths[left ? 0 : 1]),
|
||||
),
|
||||
onRefresh: () => listDir(client, _paths[left ? 0 : 1], left));
|
||||
}
|
||||
}
|
||||
|
||||
void onItemLongPress(BuildContext context, bool left, SftpName file) {
|
||||
showRoundDialog(
|
||||
context,
|
||||
'Action',
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: const Text('Delete'),
|
||||
onTap: () => showRoundDialog(context, 'Confirm',
|
||||
Text('Are you sure to delete ${file.filename}?'), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text(
|
||||
'Delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
)),
|
||||
]),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.folder),
|
||||
title: const Text('Create Folder'),
|
||||
onTap: () => mkdir(context, left)),
|
||||
ListTile(
|
||||
leading: Icon(left ? Icons.arrow_forward : Icons.arrow_back),
|
||||
title: const Text('Copy'),
|
||||
onTap: () {},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: const Text('Rename'),
|
||||
onTap: () => rename(context, left, file),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.file_download),
|
||||
title: const Text('Download'),
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'))
|
||||
]);
|
||||
}
|
||||
|
||||
void mkdir(BuildContext context, bool left) {
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context,
|
||||
'Create Folder',
|
||||
TextField(
|
||||
controller: textController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Folder Name',
|
||||
),
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(context, 'Attention',
|
||||
const Text('You need input a name.'), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK')),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
_clients[left ? 0 : 1]!
|
||||
.mkdir(_paths[left ? 0 : 1] + '/' + textController.text);
|
||||
},
|
||||
child: const Text(
|
||||
'Create',
|
||||
style: TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
void rename(BuildContext context, bool left, SftpName file) {
|
||||
final textController = TextEditingController();
|
||||
showRoundDialog(
|
||||
context,
|
||||
'Create Folder',
|
||||
TextField(
|
||||
controller: textController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'New Name',
|
||||
),
|
||||
),
|
||||
[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (textController.text == '') {
|
||||
showRoundDialog(context, 'Attention',
|
||||
const Text('You need input a name.'), [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK')),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
await _clients[left ? 0 : 1]!
|
||||
.rename(file.filename, textController.text);
|
||||
},
|
||||
child: const Text(
|
||||
'Create',
|
||||
style: TextStyle(color: Colors.red),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
String convertBytes(int bytes) {
|
||||
const suffix = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
double value = bytes.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]}';
|
||||
}
|
||||
|
||||
void updatePath(String filename, bool left) {
|
||||
if (filename == '..') {
|
||||
_paths[left ? 0 : 1] = _paths[left ? 0 : 1]
|
||||
.substring(0, _paths[left ? 0 : 1].lastIndexOf('/'));
|
||||
if (_paths[left ? 0 : 1] == '') {
|
||||
_paths[left ? 0 : 1] = '/';
|
||||
}
|
||||
return;
|
||||
}
|
||||
_paths[left ? 0 : 1] = _paths[left ? 0 : 1] +
|
||||
(_paths[left ? 0 : 1].endsWith('/') ? '' : '/') +
|
||||
filename;
|
||||
}
|
||||
|
||||
Future<void> listDir(SSHClient client, String path, bool left) async {
|
||||
final sftpc = await client.sftp();
|
||||
_clients[left ? 0 : 1] = sftpc;
|
||||
final fs = await sftpc.listdir(path);
|
||||
fs.sort((a, b) => a.filename.compareTo(b.filename));
|
||||
fs.removeAt(0);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_files[left ? 0 : 1] = fs;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDestSelector(bool left) {
|
||||
return Column(
|
||||
children: locator<ServerProvider>()
|
||||
.servers
|
||||
.map((e) => _buildDestSelectorItem(e.info, left))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDestSelectorItem(ServerPrivateInfo spi, bool left) {
|
||||
return ListTile(
|
||||
title: Text(spi.name),
|
||||
subtitle: Text('${spi.user}@${spi.ip}:${spi.port}'),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_destSpi[left ? 0 : 1] = spi;
|
||||
_selectedDest[left ? 0 : 1] = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
44
lib/view/widget/fade_in.dart
Normal file
44
lib/view/widget/fade_in.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 渐隐渐显实现
|
||||
class FadeIn extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const FadeIn({Key? key, required this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MyFadeInState createState() => _MyFadeInState();
|
||||
}
|
||||
|
||||
class _MyFadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 377),
|
||||
);
|
||||
_animation = Tween(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(_controller);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_controller.forward();
|
||||
return FadeTransition(
|
||||
opacity: _animation,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -44,13 +44,12 @@ Future<String> getFlutterVersion() async {
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getBuildData() async {
|
||||
final modifiedCount = await getGitModificationCount();
|
||||
final data = {
|
||||
'name': appName,
|
||||
'build': await getGitCommitCount() + (modifiedCount == 0 ? 0 : 1),
|
||||
'build': await getGitCommitCount(),
|
||||
'engine': await getFlutterVersion(),
|
||||
'buildAt': DateTime.now().toString(),
|
||||
'modifications': modifiedCount,
|
||||
'modifications': await getGitModificationCount(),
|
||||
};
|
||||
return data;
|
||||
}
|
||||
@@ -84,8 +83,7 @@ void flutterRun(String? mode) {
|
||||
|
||||
Future<void> flutterBuild(String source, String target, bool isAndroid) async {
|
||||
final startTime = DateTime.now();
|
||||
final build = await getGitCommitCount() +
|
||||
(await getGitModificationCount() == 0 ? 0 : 1);
|
||||
final build = await getGitCommitCount();
|
||||
|
||||
final args = [
|
||||
'build',
|
||||
|
||||
Reference in New Issue
Block a user