This commit is contained in:
lollipopkit
2022-12-11 15:31:12 +08:00
parent 7e01c4cbb3
commit cfd28c3009
20 changed files with 1023 additions and 810 deletions

View File

@@ -1,4 +1,5 @@
final _dockerImageReg = RegExp(r'(\S+) +(\S+) +(\S+) +(.+) +(\S+)'); final _dockerImageReg = RegExp(r'(\S+) +(\S+) +(\S+) +(.+) +(\S+)');
class DockerImage { class DockerImage {
final String repo; final String repo;
final String tag; final String tag;
@@ -44,4 +45,3 @@ class DockerImage {
); );
} }
} }

View File

@@ -53,7 +53,7 @@ class DockerProvider extends BusyProvider {
final verRaw = await client!.run('docker version'.withLangExport).string; final verRaw = await client!.run('docker version'.withLangExport).string;
if (verRaw.contains(_dockerNotFound)) { if (verRaw.contains(_dockerNotFound)) {
error = DockerErr(type: DockerErrType.notInstalled); error = DockerErr(type: DockerErrType.notInstalled);
notifyListeners(); setBusyState(false);
return; return;
} }
@@ -65,6 +65,7 @@ class DockerProvider extends BusyProvider {
} }
try { try {
setBusyState();
final cmd = _wrap(_dockerPS); final cmd = _wrap(_dockerPS);
// run docker ps // run docker ps
@@ -97,7 +98,7 @@ class DockerProvider extends BusyProvider {
error = DockerErr(type: DockerErrType.unknown, message: e.toString()); error = DockerErr(type: DockerErrType.unknown, message: e.toString());
rethrow; rethrow;
} finally { } finally {
notifyListeners(); setBusyState(false);
} }
} }
@@ -145,7 +146,8 @@ class DockerProvider extends BusyProvider {
if (code != 0) { if (code != 0) {
setBusyState(false); setBusyState(false);
return DockerErr(type: DockerErrType.unknown, message: errs.join('\n').trim()); return DockerErr(
type: DockerErrType.unknown, message: errs.join('\n').trim());
} }
await refresh(); await refresh();
setBusyState(false); setBusyState(false);

View File

@@ -59,23 +59,28 @@ class _AptManagePageState extends State<AptManagePage>
} }
_aptProvider.init( _aptProvider.init(
si.client!, si.client!,
si.status.sysVer.dist, si.status.sysVer.dist,
() => () => scrollController.jumpTo(scrollController.position.maxScrollExtent),
scrollController.jumpTo(scrollController.position.maxScrollExtent), () => scrollControllerUpdate
() => scrollControllerUpdate .jumpTo(scrollController.position.maxScrollExtent),
.jumpTo(scrollController.position.maxScrollExtent), onPwdRequest,
onPwdRequest, widget.spi.user,
widget.spi.user); );
_aptProvider.refreshInstalled(); _aptProvider.refreshInstalled();
} }
void onSubmitted() { void onSubmitted() {
if (textController.text == '') { if (textController.text == '') {
showRoundDialog(context, s.attention, Text(s.fieldMustNotEmpty), [ showRoundDialog(
TextButton( context,
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok)), s.attention,
]); Text(s.fieldMustNotEmpty),
[
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok)),
],
);
return; return;
} }
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -84,31 +89,32 @@ class _AptManagePageState extends State<AptManagePage>
Future<String> onPwdRequest() async { Future<String> onPwdRequest() async {
if (!mounted) return ''; if (!mounted) return '';
await showRoundDialog( await showRoundDialog(
context, context,
widget.spi.user, widget.spi.user,
TextField( TextField(
controller: textController, controller: textController,
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
obscureText: true, obscureText: true,
onSubmitted: (_) => onSubmitted(), onSubmitted: (_) => onSubmitted(),
decoration: InputDecoration( decoration: InputDecoration(
labelText: s.pwd, labelText: s.pwd,
),
), ),
[ ),
TextButton( [
onPressed: () { TextButton(
Navigator.of(context).pop(); onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, Navigator.of(context).pop();
child: Text(s.cancel)), },
TextButton( child: Text(s.cancel)),
onPressed: () => onSubmitted(), TextButton(
child: Text( onPressed: () => onSubmitted(),
s.ok, child: Text(
style: const TextStyle(color: Colors.red), s.ok,
)), style: const TextStyle(color: Colors.red),
]); )),
],
);
return textController.text.trim(); return textController.text.trim();
} }
@@ -138,11 +144,12 @@ class _AptManagePageState extends State<AptManagePage>
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
child: RoundRectCard( child: RoundRectCard(
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
child: Text( child: Text(
apt.error!, apt.error!,
textAlign: TextAlign.center, textAlign: TextAlign.center,
)), ),
),
), ),
), ),
), ),
@@ -154,17 +161,17 @@ class _AptManagePageState extends State<AptManagePage>
} }
if (apt.updateLog != null && apt.upgradeable == null) { if (apt.updateLog != null && apt.upgradeable == null) {
return SizedBox( return SizedBox(
height: _media.size.height - height:
_media.padding.top - _media.size.height - _media.padding.top - _media.padding.bottom,
_media.padding.bottom, child: ConstrainedBox(
child: ConstrainedBox( constraints: const BoxConstraints.expand(),
constraints: const BoxConstraints.expand(), child: SingleChildScrollView(
child: SingleChildScrollView( padding: const EdgeInsets.all(18),
padding: const EdgeInsets.all(18), controller: scrollControllerUpdate,
controller: scrollControllerUpdate, child: Text(apt.updateLog!),
child: Text(apt.updateLog!), ),
), ),
)); );
} }
return ListView( return ListView(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
@@ -216,23 +223,25 @@ class _AptManagePageState extends State<AptManagePage>
SizedBox( SizedBox(
height: _media.size.height * 0.73, height: _media.size.height * 0.73,
child: ListView( child: ListView(
controller: scrollController, controller: scrollController,
children: apt.upgradeable! children: apt.upgradeable!
.map((e) => _buildUpdateItem(e, apt)) .map((e) => _buildUpdateItem(e, apt))
.toList()), .toList(),
),
) )
] ]
: [ : [
SizedBox( SizedBox(
height: _media.size.height * 0.7, height: _media.size.height * 0.7,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints.expand(), constraints: const BoxConstraints.expand(),
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
controller: scrollController, controller: scrollController,
child: Text(apt.upgradeLog!), child: Text(apt.upgradeLog!),
), ),
)) ),
)
], ],
) )
], ],

View File

@@ -54,8 +54,12 @@ class BackupPage extends StatelessWidget {
const SizedBox(height: 7), const SizedBox(height: 7),
const Divider(), const Divider(),
const SizedBox(height: 7), const SizedBox(height: 7),
_buildCard(s.backup, Icons.file_upload, media, _buildCard(
() => _showExportDialog(context, s)) s.backup,
Icons.file_upload,
media,
() => _showExportDialog(context, s),
)
], ],
)), )),
); );
@@ -91,60 +95,66 @@ class BackupPage extends StatelessWidget {
Future<void> _showExportDialog(BuildContext context, S s) async { Future<void> _showExportDialog(BuildContext context, S s) async {
final exportFieldController = TextEditingController() final exportFieldController = TextEditingController()
..text = _diyEncrtpt(json.encode(Backup( ..text = _diyEncrtpt(
backupFormatVersion, json.encode(
DateTime.now().toString().split('.').first, Backup(
server.fetch(), backupFormatVersion,
snippet.fetch(), DateTime.now().toString().split('.').first,
privateKey.fetch(), server.fetch(),
setting.primaryColor.fetch() ?? Colors.pinkAccent.value, snippet.fetch(),
setting.serverStatusUpdateInterval.fetch() ?? 2, privateKey.fetch(),
setting.launchPage.fetch() ?? 0, setting.primaryColor.fetch() ?? Colors.pinkAccent.value,
))); setting.serverStatusUpdateInterval.fetch() ?? 2,
await showRoundDialog( setting.launchPage.fetch() ?? 0,
context,
s.export,
TextField(
decoration: const InputDecoration(
labelText: 'JSON',
), ),
maxLines: 7,
controller: exportFieldController,
), ),
[ );
TextButton( await showRoundDialog(
child: Text(s.copy), context,
onPressed: () { s.export,
Clipboard.setData( TextField(
ClipboardData(text: exportFieldController.text)); decoration: const InputDecoration(
Navigator.pop(context); labelText: 'JSON',
}, ),
), maxLines: 7,
]); controller: exportFieldController,
),
[
TextButton(
child: Text(s.copy),
onPressed: () {
Clipboard.setData(ClipboardData(text: exportFieldController.text));
Navigator.pop(context);
},
),
],
);
} }
Future<void> _showImportDialog(BuildContext context, S s) async { Future<void> _showImportDialog(BuildContext context, S s) async {
final importFieldController = TextEditingController(); final importFieldController = TextEditingController();
await showRoundDialog( await showRoundDialog(
context, context,
s.import, s.import,
TextField( TextField(
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'JSON', labelText: 'JSON',
),
maxLines: 3,
controller: importFieldController,
), ),
[ maxLines: 3,
TextButton( controller: importFieldController,
onPressed: () => Navigator.of(context).pop(), ),
child: Text(s.cancel)), [
TextButton( TextButton(
onPressed: () async => onPressed: () => Navigator.of(context).pop(),
await _import(importFieldController.text.trim(), context, s), child: Text(s.cancel),
child: const Text('GO'), ),
) TextButton(
]); onPressed: () async =>
await _import(importFieldController.text.trim(), context, s),
child: const Text('GO'),
)
],
);
} }
Future<void> _import(String text, BuildContext context, S s) async { Future<void> _import(String text, BuildContext context, S s) async {
@@ -165,32 +175,36 @@ class BackupPage extends StatelessWidget {
} }
await showRoundDialog( await showRoundDialog(
context, s.attention, Text(s.restoreSureWithDate(backup.date)), [ context,
TextButton( s.attention,
onPressed: () => Navigator.of(context).pop(), Text(s.restoreSureWithDate(backup.date)),
child: Text(s.cancel), [
), TextButton(
TextButton( onPressed: () => Navigator.of(context).pop(),
onPressed: () async { child: Text(s.cancel),
for (final s in backup.snippets) { ),
snippet.put(s); TextButton(
} onPressed: () async {
for (final s in backup.spis) { for (final s in backup.snippets) {
server.put(s); snippet.put(s);
} }
for (final s in backup.keys) { for (final s in backup.spis) {
privateKey.put(s); server.put(s);
} }
setting.primaryColor.put(backup.primaryColor); for (final s in backup.keys) {
setting.serverStatusUpdateInterval privateKey.put(s);
.put(backup.serverStatusUpdateInterval); }
setting.launchPage.put(backup.launchPage); setting.primaryColor.put(backup.primaryColor);
Navigator.of(context).pop(); setting.serverStatusUpdateInterval
showSnackBar(context, Text(s.restoreSuccess)); .put(backup.serverStatusUpdateInterval);
}, setting.launchPage.put(backup.launchPage);
child: Text(s.ok), Navigator.of(context).pop();
), showSnackBar(context, Text(s.restoreSuccess));
]); },
child: Text(s.ok),
),
],
);
} catch (e) { } catch (e) {
showSnackBar(context, Text(s.invalidJson)); showSnackBar(context, Text(s.invalidJson));
return; return;

View File

@@ -45,14 +45,17 @@ class _ConvertPageState extends State<ConvertPage>
super.build(context); super.build(context);
return Scaffold( return Scaffold(
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7), padding: const EdgeInsets.symmetric(horizontal: 7),
controller: ScrollController(), controller: ScrollController(),
child: Column(children: [ child: Column(
children: [
const SizedBox(height: 13), const SizedBox(height: 13),
_buildInputTop(), _buildInputTop(),
_buildTypeOption(), _buildTypeOption(),
_buildResult(), _buildResult(),
])), ],
),
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
try { try {
@@ -119,7 +122,8 @@ class _ConvertPageState extends State<ConvertPage>
), ),
TextButton( TextButton(
style: ButtonStyle( style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(primaryColor)), foregroundColor: MaterialStateProperty.all(primaryColor),
),
child: Icon(Icons.copy, semanticLabel: s.copy), child: Icon(Icons.copy, semanticLabel: s.copy),
onPressed: () => Clipboard.setData( onPressed: () => Clipboard.setData(
ClipboardData( ClipboardData(
@@ -136,13 +140,15 @@ class _ConvertPageState extends State<ConvertPage>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(typeOption[_typeOptionIndex], Text(
textScaleFactor: 1.0, typeOption[_typeOptionIndex],
textAlign: TextAlign.left, textScaleFactor: 1.0,
style: TextStyle( textAlign: TextAlign.left,
fontSize: 16.0, style: TextStyle(
fontWeight: FontWeight.w500, fontSize: 16.0,
color: primaryColor)), fontWeight: FontWeight.w500,
color: primaryColor),
),
Text( Text(
s.currentMode, s.currentMode,
textScaleFactor: 1.0, textScaleFactor: 1.0,
@@ -153,15 +159,17 @@ class _ConvertPageState extends State<ConvertPage>
), ),
), ),
children: typeOption children: typeOption
.map((e) => ListTile( .map(
title: Text( (e) => ListTile(
e, title: Text(
style: TextStyle( e,
color: _theme.textTheme.bodyText2?.color?.withAlpha(177), style: TextStyle(
), color: _theme.textTheme.bodyText2?.color?.withAlpha(177),
), ),
trailing: _buildRadio(typeOption.indexOf(e)), ),
)) trailing: _buildRadio(typeOption.indexOf(e)),
),
)
.toList(), .toList(),
), ),
); );

View File

@@ -13,8 +13,10 @@ class _DebugPageState extends State<DebugPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: appBar: AppBar(
AppBar(title: const Text('App log'), backgroundColor: Colors.black), title: const Text('App log'),
backgroundColor: Colors.black,
),
body: _buildTerminal(context), body: _buildTerminal(context),
backgroundColor: Colors.black, backgroundColor: Colors.black,
); );
@@ -31,12 +33,15 @@ class _DebugPageState extends State<DebugPage> {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Consumer<DebugProvider>(builder: (_, debug, __) { child: Consumer<DebugProvider>(
return Column( builder: (_, debug, __) {
return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: debug.widgets); children: debug.widgets,
}), );
},
),
), ),
), ),
); );

View File

@@ -66,8 +66,9 @@ class _DockerManagePageState extends State<DockerManagePage> {
title: TwoLineText(up: 'Docker', down: widget.spi.name), title: TwoLineText(up: 'Docker', down: widget.spi.name),
actions: [ actions: [
IconButton( IconButton(
onPressed: () => docker.refresh(), onPressed: () => docker.refresh(),
icon: const Icon(Icons.refresh)) icon: const Icon(Icons.refresh),
)
], ],
), ),
body: _buildMain(docker), body: _buildMain(docker),
@@ -124,8 +125,13 @@ class _DockerManagePageState extends State<DockerManagePage> {
TextButton( TextButton(
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
await _showAddCmdPreview(_buildAddCmd(imageCtrl.text.trim(), await _showAddCmdPreview(
nameCtrl.text.trim(), argsCtrl.text.trim())); _buildAddCmd(
imageCtrl.text.trim(),
nameCtrl.text.trim(),
argsCtrl.text.trim(),
),
);
}, },
child: Text(s.ok), child: Text(s.ok),
) )
@@ -191,31 +197,34 @@ class _DockerManagePageState extends State<DockerManagePage> {
Future<String> onPwdRequest() async { Future<String> onPwdRequest() async {
if (!mounted) return ''; if (!mounted) return '';
await showRoundDialog( await showRoundDialog(
context, context,
widget.spi.user, widget.spi.user,
TextField( TextField(
controller: textController, controller: textController,
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
obscureText: true, obscureText: true,
onSubmitted: (_) => onSubmitted(), onSubmitted: (_) => onSubmitted(),
decoration: InputDecoration( decoration: InputDecoration(
labelText: s.pwd, labelText: s.pwd,
),
),
[
TextButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: Text(s.cancel),
),
TextButton(
onPressed: () => onSubmitted(),
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
), ),
), ),
[ ],
TextButton( );
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: Text(s.cancel)),
TextButton(
onPressed: () => onSubmitted(),
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
)),
]);
return textController.text.trim(); return textController.text.trim();
} }
@@ -264,30 +273,29 @@ class _DockerManagePageState extends State<DockerManagePage> {
return const SizedBox(); return const SizedBox();
} }
return ExpansionTile( return ExpansionTile(
title: Text(s.imagesList), title: Text(s.imagesList),
subtitle: Text( subtitle: Text(
s.dockerImagesFmt(docker.images!.length), s.dockerImagesFmt(docker.images!.length),
style: greyTextStyle, style: greyTextStyle,
), ),
children: docker.images! children: docker.images!
.map( .map(
(e) => ListTile( (e) => ListTile(
title: Text(e.repo), title: Text(e.repo),
subtitle: Text('${e.tag} - ${e.size}'), subtitle: Text('${e.tag} - ${e.size}'),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: () async { onPressed: () async {
final result = await _docker.run('docker rmi ${e.id} -f'); final result = await _docker.run('docker rmi ${e.id} -f');
if (result != null) { if (result != null) {
showSnackBar( showSnackBar(
context, Text(getErrMsg(result) ?? s.unknownError)); context, Text(getErrMsg(result) ?? s.unknownError));
} }
}, },
),
), ),
), )
) .toList());
.toList(),
);
} }
Widget _buildLoading(DockerProvider docker) { Widget _buildLoading(DockerProvider docker) {
@@ -318,8 +326,9 @@ class _DockerManagePageState extends State<DockerManagePage> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
TextButton( TextButton(
onPressed: () => _showEditHostDialog(docker), onPressed: () => _showEditHostDialog(docker),
child: Text(s.dockerEditHost)) child: Text(s.dockerEditHost),
)
], ],
), ),
); );
@@ -342,8 +351,9 @@ class _DockerManagePageState extends State<DockerManagePage> {
), ),
[ [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)), child: Text(s.cancel),
),
], ],
); );
} }
@@ -399,14 +409,16 @@ class _DockerManagePageState extends State<DockerManagePage> {
return ExpansionTile( return ExpansionTile(
title: Text(s.containerStatus), title: Text(s.containerStatus),
subtitle: Text(_buildSubtitle(running), style: greyTextStyle), subtitle: Text(_buildSubtitle(running), style: greyTextStyle),
children: running.map((item) { children: running.map(
return ListTile( (item) {
title: Text(item.image), return ListTile(
subtitle: Text(item.status), title: Text(item.image),
trailing: subtitle: Text(item.status),
_buildMoreBtn(item.running, item.containerId, docker.isBusy), trailing:
); _buildMoreBtn(item.running, item.containerId, docker.isBusy),
}).toList(), );
},
).toList(),
); );
} }

View File

@@ -134,12 +134,15 @@ class _MyHomePageState extends State<MyHomePage>
height: 50, height: 50,
width: isSelected ? width : width - 17, width: isSelected ? width : width - 17,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? isDarkMode ? isDarkMode
? Colors.white12 ? Colors.white12
: Colors.black.withOpacity(0.07) : Colors.black.withOpacity(0.07)
: Colors.transparent, : Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(50))), borderRadius: const BorderRadius.all(
Radius.circular(50),
),
),
child: IconButton( child: IconButton(
icon: Icon(item.icon), icon: Icon(item.icon),
tooltip: tabTitleName(context, idx), tooltip: tabTitleName(context, idx),
@@ -158,18 +161,21 @@ class _MyHomePageState extends State<MyHomePage>
Widget _buildBottom(BuildContext context) { Widget _buildBottom(BuildContext context) {
return SafeArea( return SafeArea(
child: Container( child: Container(
height: 56, height: 56,
padding: const EdgeInsets.only(left: 8, top: 4, bottom: 4, right: 8), padding: const EdgeInsets.only(left: 8, top: 4, bottom: 4, right: 8),
width: _width, width: _width,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: tabItems.map((item) { children: tabItems.map(
int itemIndex = tabItems.indexOf(item); (item) {
return _buildItem(itemIndex, item, _selectIndex == itemIndex); int itemIndex = tabItems.indexOf(item);
}).toList(), return _buildItem(itemIndex, item, _selectIndex == itemIndex);
},
).toList(),
),
), ),
)); );
} }
Widget _buildDrawer() { Widget _buildDrawer() {
@@ -217,18 +223,25 @@ class _MyHomePageState extends State<MyHomePage>
leading: const Icon(Icons.info), leading: const Icon(Icons.info),
title: Text(s.feedback), title: Text(s.feedback),
onTap: () => showRoundDialog( onTap: () => showRoundDialog(
context, s.feedback, Text(s.feedbackOnGithub), [ context,
TextButton( s.feedback,
Text(s.feedbackOnGithub),
[
TextButton(
onPressed: () => Clipboard.setData( onPressed: () => Clipboard.setData(
const ClipboardData(text: issueUrl)), const ClipboardData(text: issueUrl)),
child: Text(s.copy)), child: Text(s.copy),
TextButton( ),
TextButton(
onPressed: () => openUrl(issueUrl), onPressed: () => openUrl(issueUrl),
child: Text(s.feedback)), child: Text(s.feedback),
TextButton( ),
TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: Text(s.close)) child: Text(s.close),
]), )
],
),
), ),
ListTile( ListTile(
leading: const Icon(Icons.snippet_folder), leading: const Icon(Icons.snippet_folder),
@@ -271,10 +284,11 @@ class _MyHomePageState extends State<MyHomePage>
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 53, maxWidth: 53), constraints: const BoxConstraints(maxHeight: 53, maxWidth: 53),
child: Container( child: Container(
color: primaryColor, color: primaryColor,
)), ),
),
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 83, maxWidth: 83), constraints: const BoxConstraints(maxHeight: 83, maxWidth: 83),
child: appIcon, child: appIcon,

View File

@@ -51,8 +51,9 @@ class _PingPageState extends State<PingPage>
super.build(context); super.build(context);
return Scaffold( return Scaffold(
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 7), padding: const EdgeInsets.symmetric(horizontal: 7),
child: Column(children: [ child: Column(
children: [
const SizedBox(height: 13), const SizedBox(height: 13),
buildInput(context, _textEditingController, buildInput(context, _textEditingController,
hint: s.inputDomainHere, hint: s.inputDomainHere,
@@ -69,7 +70,9 @@ class _PingPageState extends State<PingPage>
return _buildResultItem(result); return _buildResultItem(result);
}), }),
), ),
])), ],
),
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'ping fab', heroTag: 'ping fab',
onPressed: () { onPressed: () {
@@ -87,19 +90,30 @@ class _PingPageState extends State<PingPage>
Widget _buildResultItem(PingResult result) { Widget _buildResultItem(PingResult result) {
final unknown = s.unknown; final unknown = s.unknown;
final ms = s.ms; final ms = s.ms;
return RoundRectCard(ListTile( return RoundRectCard(
contentPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17), ListTile(
title: Text(result.serverName, contentPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 17),
title: Text(
result.serverName,
style: TextStyle( style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: primaryColor)), fontSize: 18,
subtitle: Text( fontWeight: FontWeight.bold,
_buildPingSummary(result, unknown, ms), color: primaryColor,
style: summaryTextStyle, ),
), ),
trailing: Text( subtitle: Text(
_buildPingSummary(result, unknown, ms),
style: summaryTextStyle,
),
trailing: Text(
'${s.pingAvg}${result.statistic?.avg?.toStringAsFixed(2) ?? s.unknown} $ms', '${s.pingAvg}${result.statistic?.avg?.toStringAsFixed(2) ?? s.unknown} $ms',
style: TextStyle(fontSize: 14, color: primaryColor)), style: TextStyle(
)); fontSize: 14,
color: primaryColor,
),
),
),
);
} }
String _buildPingSummary(PingResult result, String unknown, String ms) { String _buildPingSummary(PingResult result, String unknown, String ms) {

View File

@@ -51,17 +51,20 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(s.edit, style: textSize18), actions: [ appBar: AppBar(
widget.info != null title: Text(s.edit, style: textSize18),
? IconButton( actions: [
tooltip: s.delete, widget.info != null
onPressed: () { ? IconButton(
_provider.delInfo(widget.info!); tooltip: s.delete,
Navigator.of(context).pop(); onPressed: () {
}, _provider.delInfo(widget.info!);
icon: const Icon(Icons.delete)) Navigator.of(context).pop();
: const SizedBox() },
]), icon: const Icon(Icons.delete))
: const SizedBox()
],
),
body: ListView( body: ListView(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
children: [ children: [

View File

@@ -51,19 +51,22 @@ class _PrivateKeyListState extends State<StoredPrivateKeysPage> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
TextButton( TextButton(
onPressed: () => AppRoute( onPressed: () => AppRoute(
PrivateKeyEditPage(info: key.infos[idx]), PrivateKeyEditPage(info: key.infos[idx]),
'private key edit page') 'private key edit page')
.go(context), .go(context),
child: Text( child: Text(
s.edit, s.edit,
style: _textStyle, style: _textStyle,
)) ),
)
], ],
), ),
)); ));
}) })
: Center(child: Text(s.noSavedPrivateKey)); : Center(
child: Text(s.noSavedPrivateKey),
);
}, },
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(

View File

@@ -37,8 +37,9 @@ class _ServerDetailPageState extends State<ServerDetailPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<ServerProvider>(builder: (_, provider, __) { return Consumer<ServerProvider>(builder: (_, provider, __) {
return _buildMainPage( return _buildMainPage(
provider.servers provider.servers.firstWhere(
.firstWhere((e) => '${e.info.ip}:${e.info.port}' == widget.id), (e) => '${e.info.ip}:${e.info.port}' == widget.id,
),
); );
}); });
} }
@@ -211,8 +212,9 @@ class _ServerDetailPageState extends State<ServerDetailPage>
_buildMemExplain( _buildMemExplain(
(ss.memory.cache * mb).convertBytes, pColor.withAlpha(77)), (ss.memory.cache * mb).convertBytes, pColor.withAlpha(77)),
_buildMemExplain( _buildMemExplain(
((ss.memory.total - ss.memory.used) * mb).convertBytes, ((ss.memory.total - ss.memory.used) * mb).convertBytes,
progressColor.resolve(context)) progressColor.resolve(context),
)
], ],
), ),
const SizedBox( const SizedBox(
@@ -363,11 +365,12 @@ class _ServerDetailPageState extends State<ServerDetailPage>
textScaleFactor: 1.0), textScaleFactor: 1.0),
), ),
SizedBox( SizedBox(
width: _media.size.width / 4, width: _media.size.width / 4,
child: Text(ns.speedOut(device: device), child: Text(ns.speedOut(device: device),
style: textSize11, style: textSize11,
textAlign: TextAlign.right, textAlign: TextAlign.right,
textScaleFactor: 1.0)) textScaleFactor: 1.0),
)
], ],
), ),
); );

View File

@@ -63,30 +63,40 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(s.edit, style: textSize18), actions: [ appBar: AppBar(
widget.spi != null title: Text(s.edit, style: textSize18),
? IconButton( actions: [
onPressed: () { widget.spi != null
showRoundDialog(context, s.attention, ? IconButton(
Text(s.sureToDeleteServer(widget.spi!.name)), [ onPressed: () {
TextButton( showRoundDialog(
onPressed: () { context,
_serverProvider.delServer(widget.spi!); s.attention,
Navigator.of(context).pop(); Text(s.sureToDeleteServer(widget.spi!.name)),
Navigator.of(context).pop(); [
}, TextButton(
child: Text( onPressed: () {
s.ok, _serverProvider.delServer(widget.spi!);
style: const TextStyle(color: Colors.red), Navigator.of(context).pop();
)), Navigator.of(context).pop();
TextButton( },
onPressed: () => Navigator.of(context).pop(), child: Text(
child: Text(s.cancel)) s.ok,
]); style: const TextStyle(color: Colors.red),
}, ),
icon: const Icon(Icons.delete)) ),
: const SizedBox() TextButton(
]), onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel),
)
],
);
},
icon: const Icon(Icons.delete),
)
: const SizedBox()
],
),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(17), padding: const EdgeInsets.all(17),
child: Column( child: Column(
@@ -148,42 +158,47 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
) )
: const SizedBox(), : const SizedBox(),
usePublicKey usePublicKey
? Consumer<PrivateKeyProvider>(builder: (_, key, __) { ? Consumer<PrivateKeyProvider>(
for (var item in key.infos) { builder: (_, key, __) {
if (item.id == widget.spi?.pubKeyId) { for (var item in key.infos) {
_pubKeyIndex ??= key.infos.indexOf(item); if (item.id == widget.spi?.pubKeyId) {
_pubKeyIndex ??= key.infos.indexOf(item);
}
} }
} final tiles = key.infos
final tiles = key.infos .map(
.map( (e) => ListTile(
(e) => ListTile( contentPadding: EdgeInsets.zero,
contentPadding: EdgeInsets.zero, title: Text(e.id, textAlign: TextAlign.start),
title: Text(e.id, textAlign: TextAlign.start), trailing: _buildRadio(key.infos.indexOf(e), e)),
trailing: _buildRadio(key.infos.indexOf(e), e)), )
) .toList();
.toList(); tiles.add(
tiles.add(ListTile( ListTile(
title: Text(s.addPrivateKey), title: Text(s.addPrivateKey),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () => AppRoute(const PrivateKeyEditPage(), onPressed: () => AppRoute(
'private key edit page') const PrivateKeyEditPage(),
.go(context), 'private key edit page')
), .go(context),
)); ),
return ExpansionTile( ),
textColor: primaryColor, );
iconColor: primaryColor, return ExpansionTile(
tilePadding: EdgeInsets.zero, textColor: primaryColor,
childrenPadding: EdgeInsets.zero, iconColor: primaryColor,
title: Text( tilePadding: EdgeInsets.zero,
s.choosePrivateKey, childrenPadding: EdgeInsets.zero,
style: const TextStyle(fontSize: 14), title: Text(
), s.choosePrivateKey,
children: tiles, style: const TextStyle(fontSize: 14),
); ),
}) children: tiles,
);
},
)
: const SizedBox() : const SizedBox()
], ],
), ),
@@ -197,18 +212,19 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
} }
if (!usePublicKey && passwordController.text == '') { if (!usePublicKey && passwordController.text == '') {
final cancel = await showRoundDialog<bool>( final cancel = await showRoundDialog<bool>(
context, context,
s.attention, s.attention,
Text(s.sureNoPwd), Text(s.sureNoPwd),
[ [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: Text(s.ok)), child: Text(s.ok)),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text(s.cancel)) child: Text(s.cancel))
], ],
barrierDismiss: false); barrierDismiss: false,
);
if (cancel ?? true) { if (cancel ?? true) {
return; return;
} }
@@ -230,12 +246,13 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final authorization = passwordController.text; final authorization = passwordController.text;
final spi = ServerPrivateInfo( final spi = ServerPrivateInfo(
name: nameController.text, name: nameController.text,
ip: ipController.text, ip: ipController.text,
port: int.parse(portController.text), port: int.parse(portController.text),
user: usernameController.text, user: usernameController.text,
pwd: authorization, pwd: authorization,
pubKeyId: usePublicKey ? _keyInfo!.id : null); pubKeyId: usePublicKey ? _keyInfo!.id : null,
);
if (widget.spi == null) { if (widget.spi == null) {
_serverProvider.addServer(spi); _serverProvider.addServer(spi);

View File

@@ -55,30 +55,32 @@ class _ServerPageState extends State<ServerPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final child = Consumer<ServerProvider>(builder: (_, pro, __) { final child = Consumer<ServerProvider>(
if (pro.servers.isEmpty) { builder: (_, pro, __) {
return Center( if (pro.servers.isEmpty) {
child: Text( return Center(
s.serverTabEmpty, child: Text(
textAlign: TextAlign.center, s.serverTabEmpty,
textAlign: TextAlign.center,
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(7),
controller: ScrollController(),
itemBuilder: (ctx, idx) {
if (idx == pro.servers.length) {
return SizedBox(height: _media.padding.bottom);
}
return _buildEachServerCard(pro.servers[idx]);
},
itemCount: pro.servers.length + 1,
separatorBuilder: (_, __) => const SizedBox(
height: 3,
), ),
); );
} },
return ListView.separated( );
padding: const EdgeInsets.all(7),
controller: ScrollController(),
itemBuilder: (ctx, idx) {
if (idx == pro.servers.length) {
return SizedBox(height: _media.padding.bottom);
}
return _buildEachServerCard(pro.servers[idx]);
},
itemCount: pro.servers.length + 1,
separatorBuilder: (_, __) => const SizedBox(
height: 3,
),
);
});
return Scaffold( return Scaffold(
body: child, body: child,
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
@@ -102,9 +104,10 @@ class _ServerPageState extends State<ServerPage>
'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: _buildRealServerCard( child: _buildRealServerCard(
si.status, si.info.name, si.connectionState, si.info)), si.status, si.info.name, si.connectionState, si.info),
),
onTap: () => AppRoute(ServerDetailPage('${si.info.ip}:${si.info.port}'), onTap: () => AppRoute(ServerDetailPage('${si.info.ip}:${si.info.port}'),
'server detail page') 'server detail page')
.go(context), .go(context),

View File

@@ -71,18 +71,19 @@ class _SettingPageState extends State<SettingPage> {
} }
Widget _buildCheckUpdate() { Widget _buildCheckUpdate() {
return Consumer<AppProvider>(builder: (_, app, __) { return Consumer<AppProvider>(
String display; builder: (_, app, __) {
if (app.newestBuild != null) { String display;
if (app.newestBuild! > BuildData.build) { if (app.newestBuild != null) {
display = s.versionHaveUpdate(app.newestBuild!); if (app.newestBuild! > BuildData.build) {
display = s.versionHaveUpdate(app.newestBuild!);
} else {
display = s.versionUpdated(BuildData.build);
}
} else { } else {
display = s.versionUpdated(BuildData.build); display = s.versionUnknownUpdate(BuildData.build);
} }
} else { return ListTile(
display = s.versionUnknownUpdate(BuildData.build);
}
return ListTile(
contentPadding: roundRectCardPadding, contentPadding: roundRectCardPadding,
trailing: const Icon(Icons.keyboard_arrow_right), trailing: const Icon(Icons.keyboard_arrow_right),
title: Text( title: Text(
@@ -90,8 +91,10 @@ class _SettingPageState extends State<SettingPage> {
style: textSize13, style: textSize13,
textAlign: TextAlign.start, textAlign: TextAlign.start,
), ),
onTap: () => doUpdate(context, force: true)); onTap: () => doUpdate(context, force: true),
}); );
},
);
} }
Widget _buildUpdateInterval() { Widget _buildUpdateInterval() {
@@ -147,24 +150,22 @@ class _SettingPageState extends State<SettingPage> {
Widget _buildAppColorPreview() { Widget _buildAppColorPreview() {
return ExpansionTile( return ExpansionTile(
textColor: priColor, textColor: priColor,
tilePadding: roundRectCardPadding, tilePadding: roundRectCardPadding,
childrenPadding: roundRectCardPadding, childrenPadding: roundRectCardPadding,
trailing: ClipOval( trailing: ClipOval(
child: Container( child: Container(
color: priColor, color: priColor,
height: 27, height: 27,
width: 27, width: 27,
),
), ),
title: Text( ),
s.appPrimaryColor, title: Text(
style: textSize13, s.appPrimaryColor,
), style: textSize13,
children: [ ),
_buildAppColorPicker(priColor), children: [_buildAppColorPicker(priColor), _buildColorPickerConfirmBtn()],
_buildColorPickerConfirmBtn() );
]);
} }
Widget _buildAppColorPicker(Color selected) { Widget _buildAppColorPicker(Color selected) {
@@ -204,16 +205,18 @@ class _SettingPageState extends State<SettingPage> {
), ),
), ),
children: tabs children: tabs
.map((e) => ListTile( .map(
contentPadding: EdgeInsets.zero, (e) => ListTile(
title: Text( contentPadding: EdgeInsets.zero,
tabTitleName(context, tabs.indexOf(e)), title: Text(
style: TextStyle( tabTitleName(context, tabs.indexOf(e)),
fontSize: 14, style: TextStyle(
color: _theme.textTheme.bodyText2!.color!.withAlpha(177)), fontSize: 14,
), color: _theme.textTheme.bodyText2!.color!.withAlpha(177)),
trailing: _buildRadio(tabs.indexOf(e)), ),
)) trailing: _buildRadio(tabs.indexOf(e)),
),
)
.toList(), .toList(),
); );
} }

View File

@@ -141,18 +141,21 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
void showFileActionDialog(FileSystemEntity file) { void showFileActionDialog(FileSystemEntity file) {
final fileName = file.path.split('/').last; final fileName = file.path.split('/').last;
showRoundDialog( showRoundDialog(
context, context,
s.choose, s.choose,
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.delete), leading: const Icon(Icons.delete),
title: Text(s.delete), title: Text(s.delete),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
showRoundDialog( showRoundDialog(
context, s.sureDelete(fileName), const SizedBox(), [ context,
s.sureDelete(fileName),
const SizedBox(),
[
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)), child: Text(s.cancel)),
@@ -164,21 +167,24 @@ class _SFTPDownloadedPageState extends State<SFTPDownloadedPage> {
}, },
child: Text(s.ok), child: Text(s.ok),
), ),
]); ],
}, );
), },
ListTile( ),
leading: const Icon(Icons.open_in_new), ListTile(
title: Text(s.open), leading: const Icon(Icons.open_in_new),
onTap: () { title: Text(s.open),
shareFiles(context, [file.absolute.path]); onTap: () {
}), shareFiles(context, [file.absolute.path]);
], },
), ),
[ ],
TextButton( ),
onPressed: (() => Navigator.of(context).pop()), [
child: Text(s.close)) TextButton(
]); onPressed: (() => Navigator.of(context).pop()),
child: Text(s.close))
],
);
} }
} }

View File

@@ -58,16 +58,18 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
Widget _wrapInCard(SftpDownloadStatus status, String? subtitle, Widget _wrapInCard(SftpDownloadStatus status, String? subtitle,
{Widget? trailing}) { {Widget? trailing}) {
return RoundRectCard(ListTile( return RoundRectCard(
title: Text(status.fileName), ListTile(
subtitle: subtitle == null title: Text(status.fileName),
? null subtitle: subtitle == null
: Text( ? null
subtitle, : Text(
style: grey, subtitle,
), style: grey,
trailing: trailing, ),
)); trailing: trailing,
),
);
} }
Widget _buildItem(SftpDownloadStatus status) { Widget _buildItem(SftpDownloadStatus status) {
@@ -77,11 +79,14 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
switch (status.status) { switch (status.status) {
case SftpWorkerStatus.finished: case SftpWorkerStatus.finished:
final time = status.spentTime.toString(); final time = status.spentTime.toString();
return _wrapInCard(status, return _wrapInCard(
'${s.downloadFinished} ${s.spentTime(time == 'null' ? s.unknown : (time.substring(0, time.length - 7)))}', status,
trailing: IconButton( '${s.downloadFinished} ${s.spentTime(time == 'null' ? s.unknown : (time.substring(0, time.length - 7)))}',
onPressed: () => shareFiles(context, [status.item.localPath]), trailing: IconButton(
icon: const Icon(Icons.open_in_new))); onPressed: () => shareFiles(context, [status.item.localPath]),
icon: const Icon(Icons.open_in_new),
),
);
case SftpWorkerStatus.downloading: case SftpWorkerStatus.downloading:
return _wrapInCard( return _wrapInCard(
status, status,
@@ -94,11 +99,14 @@ class _SFTPDownloadingPageState extends State<SFTPDownloadingPage> {
case SftpWorkerStatus.sshConnectted: case SftpWorkerStatus.sshConnectted:
return _wrapInCard(status, s.sftpSSHConnected, trailing: loadingIcon); return _wrapInCard(status, s.sftpSSHConnected, trailing: loadingIcon);
default: default:
return _wrapInCard(status, s.unknown, return _wrapInCard(
trailing: const Icon( status,
Icons.error, s.unknown,
size: 40, trailing: const Icon(
)); Icons.error,
size: 40,
),
);
} }
} }
} }

View File

@@ -55,34 +55,36 @@ class _SFTPPageState extends State<SFTPPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: TwoLineText(up: 'SFTP', down: widget.spi.name), title: TwoLineText(up: 'SFTP', down: widget.spi.name),
actions: [ actions: [
IconButton( IconButton(
onPressed: (() => showRoundDialog( onPressed: (() => showRoundDialog(
context, context,
s.choose, s.choose,
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.folder), leading: const Icon(Icons.folder),
title: Text(s.createFolder), title: Text(s.createFolder),
onTap: () => mkdir(context)), onTap: () => mkdir(context)),
ListTile( ListTile(
leading: const Icon(Icons.insert_drive_file), leading: const Icon(Icons.insert_drive_file),
title: Text(s.createFile), title: Text(s.createFile),
onTap: () => newFile(context)), onTap: () => newFile(context)),
], ],
), ),
[ [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: Text(s.close)) child: Text(s.close))
])), ],
icon: const Icon(Icons.add), )),
) icon: const Icon(Icons.add),
]), )
],
),
body: _buildFileView(), body: _buildFileView(),
); );
} }
@@ -121,85 +123,90 @@ class _SFTPPageState extends State<SFTPPage> {
return centerCircleLoading; return centerCircleLoading;
} else { } else {
return RefreshIndicator( return RefreshIndicator(
child: FadeIn( child: FadeIn(
key: Key(_status.spi!.name + _status.path!.path), key: Key(_status.spi!.name + _status.path!.path),
child: ListView.builder( child: ListView.builder(
itemCount: _status.files!.length + 1, itemCount: _status.files!.length + 1,
controller: _scrollController, controller: _scrollController,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
return _buildDestSelector(); return _buildDestSelector();
} }
final file = _status.files![index - 1]; final file = _status.files![index - 1];
final isDir = file.attr.isDirectory; final isDir = file.attr.isDirectory;
return ListTile( return ListTile(
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file), leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
title: Text(file.filename), title: Text(file.filename),
trailing: Text( trailing: Text(
DateTime.fromMillisecondsSinceEpoch( DateTime.fromMillisecondsSinceEpoch(
(file.attr.modifyTime ?? 0) * 1000) (file.attr.modifyTime ?? 0) * 1000)
.toString() .toString()
.replaceFirst('.000', ''), .replaceFirst('.000', ''),
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
subtitle: subtitle:
isDir ? null : Text((file.attr.size ?? 0).convertBytes), isDir ? null : Text((file.attr.size ?? 0).convertBytes),
onTap: () { onTap: () {
if (isDir) { if (isDir) {
_status.path?.update(file.filename); _status.path?.update(file.filename);
listDir(path: _status.path?.path); listDir(path: _status.path?.path);
} else { } else {
onItemPress(context, file, true); onItemPress(context, file, true);
} }
}, },
onLongPress: () => onItemPress(context, file, false), onLongPress: () => onItemPress(context, file, false),
); );
}, },
),
), ),
onRefresh: () => listDir(path: _status.path?.path)); ),
onRefresh: () => listDir(path: _status.path?.path),
);
} }
} }
void onItemPress(BuildContext context, SftpName file, bool showDownload) { void onItemPress(BuildContext context, SftpName file, bool showDownload) {
showRoundDialog( showRoundDialog(
context, context,
s.choose, s.choose,
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.delete), leading: const Icon(Icons.delete),
title: Text(s.delete), title: Text(s.delete),
onTap: () => delete(context, file), onTap: () => delete(context, file),
), ),
ListTile( ListTile(
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),
title: Text(s.rename), title: Text(s.rename),
onTap: () => rename(context, file), onTap: () => rename(context, file),
), ),
showDownload showDownload
? ListTile( ? ListTile(
leading: const Icon(Icons.download), leading: const Icon(Icons.download),
title: Text(s.download), title: Text(s.download),
onTap: () => download(context, file), onTap: () => download(context, file),
) )
: const SizedBox() : const SizedBox()
], ],
), ),
[ [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(), child: Text(s.cancel))
child: Text(s.cancel)) ],
]); );
} }
void download(BuildContext context, SftpName name) { void download(BuildContext context, SftpName name) {
showRoundDialog(context, s.download, showRoundDialog(
Text('${s.dl2Local(name.filename)}\n${s.keepForeground}'), [ context,
TextButton( s.download,
onPressed: () => Navigator.of(context).pop(), child: Text(s.cancel)), Text('${s.dl2Local(name.filename)}\n${s.keepForeground}'),
TextButton( [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
final prePath = _status.path!.path; final prePath = _status.path!.path;
@@ -207,36 +214,56 @@ class _SFTPPageState extends State<SFTPPage> {
prePath + (prePath.endsWith('/') ? '' : '/') + name.filename; prePath + (prePath.endsWith('/') ? '' : '/') + name.filename;
final local = '${(await sftpDownloadDir).path}$remotePath'; final local = '${(await sftpDownloadDir).path}$remotePath';
final pubKeyId = _status.spi!.pubKeyId; final pubKeyId = _status.spi!.pubKeyId;
locator<SftpDownloadProvider>().add( locator<SftpDownloadProvider>().add(
DownloadItem(_status.spi!, remotePath, local), DownloadItem(
key: pubKeyId == null _status.spi!,
? null remotePath,
: locator<PrivateKeyStore>().get(pubKeyId).privateKey); local,
),
key: pubKeyId == null
? null
: locator<PrivateKeyStore>().get(pubKeyId).privateKey,
);
Navigator.of(context).pop(); Navigator.of(context).pop();
showRoundDialog(context, s.goSftpDlPage, const SizedBox(), [ showRoundDialog(
TextButton( context,
s.goSftpDlPage,
const SizedBox(),
[
TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)), child: Text(s.cancel),
TextButton( ),
TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
AppRoute(const SFTPDownloadingPage(), 'sftp downloading') AppRoute(const SFTPDownloadingPage(), 'sftp downloading')
.go(context); .go(context);
}, },
child: Text(s.ok)) child: Text(s.ok),
]); )
],
);
}, },
child: Text(s.download)) child: Text(s.download),
]); )
],
);
} }
void delete(BuildContext context, SftpName file) { void delete(BuildContext context, SftpName file) {
Navigator.of(context).pop(); Navigator.of(context).pop();
showRoundDialog(context, s.attention, Text(s.sureDelete(file.filename)), [ showRoundDialog(
TextButton( context,
onPressed: () => Navigator.of(context).pop(), s.attention,
child: const Text('Cancel')), Text(s.sureDelete(file.filename)),
TextButton( [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel')),
TextButton(
onPressed: () { onPressed: () {
_status.client!.remove(file.filename); _status.client!.remove(file.filename);
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -245,126 +272,149 @@ class _SFTPPageState extends State<SFTPPage> {
child: Text( child: Text(
s.delete, s.delete,
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
)), ),
]); ),
],
);
} }
void mkdir(BuildContext context) { void mkdir(BuildContext context) {
Navigator.of(context).pop(); Navigator.of(context).pop();
final textController = TextEditingController(); final textController = TextEditingController();
showRoundDialog( showRoundDialog(
context, context,
s.createFolder, s.createFolder,
TextField( TextField(
controller: textController, controller: textController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: s.name, labelText: s.name,
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel),
),
TextButton(
onPressed: () {
if (textController.text == '') {
showRoundDialog(
context,
s.attention,
Text(s.fieldMustNotEmpty),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok)),
],
);
return;
}
_status.client!
.mkdir('${_status.path!.path}/${textController.text}');
Navigator.of(context).pop();
listDir();
},
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
), ),
), ),
[ ],
TextButton( );
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () {
if (textController.text == '') {
showRoundDialog(
context, s.attention, Text(s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok)),
]);
return;
}
_status.client!
.mkdir('${_status.path!.path}/${textController.text}');
Navigator.of(context).pop();
listDir();
},
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
)),
]);
} }
void newFile(BuildContext context) { void newFile(BuildContext context) {
Navigator.of(context).pop(); Navigator.of(context).pop();
final textController = TextEditingController(); final textController = TextEditingController();
showRoundDialog( showRoundDialog(
context, context,
s.createFile, s.createFile,
TextField( TextField(
controller: textController, controller: textController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: s.name, labelText: s.name,
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel),
),
TextButton(
onPressed: () async {
if (textController.text == '') {
showRoundDialog(
context,
s.attention,
Text(s.fieldMustNotEmpty),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok),
),
],
);
return;
}
(await _status.client!
.open('${_status.path!.path}/${textController.text}'))
.writeBytes(Uint8List(0));
Navigator.of(context).pop();
listDir();
},
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
), ),
), ),
[ ],
TextButton( );
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () async {
if (textController.text == '') {
showRoundDialog(
context, s.attention, Text(s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok)),
]);
return;
}
(await _status.client!
.open('${_status.path!.path}/${textController.text}'))
.writeBytes(Uint8List(0));
Navigator.of(context).pop();
listDir();
},
child: Text(
s.ok,
style: const TextStyle(color: Colors.red),
)),
]);
} }
void rename(BuildContext context, SftpName file) { void rename(BuildContext context, SftpName file) {
Navigator.of(context).pop(); Navigator.of(context).pop();
final textController = TextEditingController(); final textController = TextEditingController();
showRoundDialog( showRoundDialog(
context, context,
s.rename, s.rename,
TextField( TextField(
controller: textController, controller: textController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: s.name, labelText: s.name,
),
),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () async {
if (textController.text == '') {
showRoundDialog(
context,
s.attention,
Text(s.fieldMustNotEmpty),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok),
),
],
);
return;
}
await _status.client!.rename(file.filename, textController.text);
Navigator.of(context).pop();
listDir();
},
child: Text(
s.rename,
style: const TextStyle(color: Colors.red),
), ),
), ),
[ ],
TextButton( );
onPressed: () => Navigator.of(context).pop(),
child: Text(s.cancel)),
TextButton(
onPressed: () async {
if (textController.text == '') {
showRoundDialog(
context, s.attention, Text(s.fieldMustNotEmpty), [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok)),
]);
return;
}
await _status.client!
.rename(file.filename, textController.text);
Navigator.of(context).pop();
listDir();
},
child: Text(
s.rename,
style: const TextStyle(color: Colors.red),
)),
]);
} }
Future<void> listDir({String? path, SSHClient? client}) async { Future<void> listDir({String? path, SSHClient? client}) async {
@@ -388,10 +438,17 @@ class _SFTPPageState extends State<SFTPPage> {
}); });
} }
} catch (e) { } catch (e) {
await showRoundDialog(context, s.error, Text(e.toString()), [ await showRoundDialog(
TextButton( context,
onPressed: () => Navigator.of(context).pop(), child: Text(s.ok)) s.error,
]); Text(e.toString()),
[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.ok),
)
],
);
if (_status.path!.undo()) { if (_status.path!.undo()) {
await listDir(); await listDir();
} }
@@ -420,11 +477,12 @@ class _SFTPPageState extends State<SFTPPage> {
_status.selected = true; _status.selected = true;
_status.path = AbsolutePath('/'); _status.path = AbsolutePath('/');
listDir( listDir(
client: locator<ServerProvider>() client: locator<ServerProvider>()
.servers .servers
.firstWhere((s) => s.info == spi) .firstWhere((s) => s.info == spi)
.client, .client,
path: '/'); path: '/',
);
}, },
); );
} }

View File

@@ -41,17 +41,20 @@ class _SnippetEditPageState extends State<SnippetEditPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(s.edit, style: textSize18), actions: [ appBar: AppBar(
widget.snippet != null title: Text(s.edit, style: textSize18),
? IconButton( actions: [
onPressed: () { widget.snippet != null
_provider.del(widget.snippet!); ? IconButton(
Navigator.of(context).pop(); onPressed: () {
}, _provider.del(widget.snippet!);
tooltip: s.delete, Navigator.of(context).pop();
icon: const Icon(Icons.delete)) },
: const SizedBox() tooltip: s.delete,
]), icon: const Icon(Icons.delete))
: const SizedBox()
],
),
body: ListView( body: ListView(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
children: [ children: [

View File

@@ -59,107 +59,135 @@ class _SnippetListPageState extends State<SnippetListPage> {
itemCount: key.snippets.length, itemCount: key.snippets.length,
itemExtent: 57, itemExtent: 57,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return RoundRectCard(Padding( return RoundRectCard(
padding: roundRectCardPadding, Padding(
child: Row( padding: roundRectCardPadding,
crossAxisAlignment: CrossAxisAlignment.center, child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center,
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Text( children: [
key.snippets[idx].name, Text(
textAlign: TextAlign.center, key.snippets[idx].name,
), textAlign: TextAlign.center,
Row(children: [ ),
TextButton( Row(
onPressed: () => AppRoute( children: [
SnippetEditPage( TextButton(
snippet: key.snippets[idx]), onPressed: () => AppRoute(
'snippet edit page') SnippetEditPage(
.go(context), snippet: key.snippets[idx]),
child: Text( 'snippet edit page')
s.edit, .go(context),
style: _textStyle, child: Text(
)), s.edit,
TextButton( style: _textStyle,
onPressed: () { ),
final snippet = key.snippets[idx]; ),
if (widget.spi == null) { TextButton(
_showRunDialog(snippet); onPressed: () {
return; final snippet = key.snippets[idx];
} if (widget.spi == null) {
run(context, snippet); _showRunDialog(snippet);
}, return;
child: Text( }
s.run, run(context, snippet);
style: _textStyle, },
)) child: Text(
]) s.run,
], style: _textStyle,
),
)
],
)
],
),
), ),
)); );
}) },
: Center(child: Text(s.noSavedSnippet)); )
: Center(
child: Text(s.noSavedSnippet),
);
}, },
); );
} }
void _showRunDialog(Snippet snippet) { void _showRunDialog(Snippet snippet) {
showRoundDialog(context, s.chooseDestination, showRoundDialog(
Consumer<ServerProvider>(builder: (_, provider, __) { context,
if (provider.servers.isEmpty) { s.chooseDestination,
return Text(s.noServerAvailable); Consumer<ServerProvider>(
} builder: (_, provider, __) {
_selectedIndex = provider.servers.first.info; if (provider.servers.isEmpty) {
return SizedBox( return Text(s.noServerAvailable);
height: 111, }
child: Stack(children: [ _selectedIndex = provider.servers.first.info;
Positioned( return SizedBox(
top: 36, height: 111,
bottom: 36, child: Stack(
left: 0, children: [
right: 0, Positioned(
child: Container( top: 36,
height: 37, bottom: 36,
decoration: const BoxDecoration( left: 0,
borderRadius: BorderRadius.all(Radius.circular(7)), right: 0,
color: Colors.black12, child: Container(
height: 37,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(7)),
color: Colors.black12,
),
),
), ),
), ListWheelScrollView.useDelegate(
itemExtent: 37,
diameterRatio: 1.2,
controller: FixedExtentScrollController(initialItem: 0),
onSelectedItemChanged: (idx) =>
_selectedIndex = provider.servers[idx].info,
physics: const FixedExtentScrollPhysics(),
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) => Center(
child: Text(
provider.servers[index].info.name,
textAlign: TextAlign.center,
),
),
childCount: provider.servers.length),
)
],
), ),
ListWheelScrollView.useDelegate( );
itemExtent: 37, },
diameterRatio: 1.2, ),
controller: FixedExtentScrollController(initialItem: 0), [
onSelectedItemChanged: (idx) => TextButton(
_selectedIndex = provider.servers[idx].info, onPressed: () async => run(context, snippet),
physics: const FixedExtentScrollPhysics(), child: Text(s.run),
childDelegate: ListWheelChildBuilderDelegate( ),
builder: (context, index) => Center( TextButton(
child: Text( onPressed: () => Navigator.of(context).pop(),
provider.servers[index].info.name, child: Text(s.cancel),
textAlign: TextAlign.center, ),
), ],
), );
childCount: provider.servers.length),
)
]));
}), [
TextButton(
onPressed: () async => run(context, snippet), child: Text(s.run)),
TextButton(
onPressed: () => Navigator.of(context).pop(), child: Text(s.cancel)),
]);
} }
Future<void> run(BuildContext context, Snippet snippet) async { Future<void> run(BuildContext context, Snippet snippet) async {
final result = await locator<ServerProvider>() final result = await locator<ServerProvider>()
.runSnippet(widget.spi ?? _selectedIndex, snippet); .runSnippet(widget.spi ?? _selectedIndex, snippet);
if (result != null) { if (result != null) {
showRoundDialog(context, s.result, showRoundDialog(
Text(result, style: const TextStyle(fontSize: 13)), [ context,
TextButton( s.result,
onPressed: () => Navigator.of(context).pop(), child: Text(s.close)) Text(result, style: const TextStyle(fontSize: 13)),
]); [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(s.close),
)
],
);
} }
} }
} }