diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n.dart b/.dart_tool/flutter_gen/gen_l10n/l10n.dart index 9bed564a..0776124c 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n.dart @@ -1298,6 +1298,12 @@ abstract class S { /// **'Are you sure to delete server [{server}]?'** String sureToDeleteServer(Object server); + /// No description provided for @suspendTip. + /// + /// In en, this message translates to: + /// **'The suspend function requires root privileges and systemd support.'** + String get suspendTip; + /// No description provided for @syncTip. /// /// In en, this message translates to: diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart index 2994a909..c21f3259 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart @@ -640,6 +640,9 @@ class SDe extends S { return 'Bist du sicher, dass du [$server] löschen willst?'; } + @override + String get suspendTip => 'Die Suspend-Funktion erfordert Root-Rechte und systemd-Unterstützung.'; + @override String get syncTip => 'Nach der automatischen Synchronisierung kann es erforderlich sein, die App neu zu starten, damit bestimmte Änderungen wirksam werden.'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart index 403f6428..1410a8da 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart @@ -640,6 +640,9 @@ class SEn extends S { return 'Are you sure to delete server [$server]?'; } + @override + String get suspendTip => 'The suspend function requires root privileges and systemd support.'; + @override String get syncTip => 'After auto sync, a restart may be required for some changes to take effect.'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart index 5ae10106..45a7e800 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart @@ -640,6 +640,9 @@ class SId extends S { return 'Apakah Anda pasti akan menghapus server [$server]?'; } + @override + String get suspendTip => 'Fungsi penangguhan memerlukan hak akses root dan dukungan systemd.'; + @override String get syncTip => 'Setelah sinkronisasi otomatis, mungkin perlu memulai ulang aplikasi agar perubahan tertentu dapat diterapkan.'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart index 5a11ddbb..e2d5151f 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart @@ -640,6 +640,9 @@ class SZh extends S { return '你确定要删除服务器 [$server] 吗?'; } + @override + String get suspendTip => 'suspend 功能需要 root 权限及 systemd 支持。'; + @override String get syncTip => '在自动同步后,可能需要重新启动,某些更改才能生效。'; @@ -1387,6 +1390,9 @@ class SZhTw extends SZh { return '你確定要刪除服務器 [$server] 嗎?'; } + @override + String get suspendTip => 'suspend 功能需要 root 權限及 systemd 支持。'; + @override String get syncTip => '在自動同步後,可能需要重新啟動,某些更改才能生效。'; diff --git a/lib/core/extension/context/dialog.dart b/lib/core/extension/context/dialog.dart index 1c2bbfa8..21a8bdd3 100644 --- a/lib/core/extension/context/dialog.dart +++ b/lib/core/extension/context/dialog.dart @@ -56,7 +56,7 @@ extension DialogX on BuildContext { void showSnippetDialog( void Function(Snippet s) onSelected, ) { - if (Providers.snippet.snippets.isEmpty) { + if (Pros.snippet.snippets.isEmpty) { showRoundDialog( child: Text(l10n.noSavedSnippet), actions: [ @@ -76,12 +76,12 @@ extension DialogX on BuildContext { return; } - var snippet = Providers.snippet.snippets.first; + var snippet = Pros.snippet.snippets.first; showRoundDialog( title: Text(l10n.choose), child: Picker( - items: Providers.snippet.snippets.map((e) => Text(e.name)).toList(), - onSelected: (idx) => snippet = Providers.snippet.snippets[idx], + items: Pros.snippet.snippets.map((e) => Text(e.name)).toList(), + onSelected: (idx) => snippet = Pros.snippet.snippets[idx], ), actions: [ TextButton( diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index ba5d3bf8..703cc8b0 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -74,9 +74,11 @@ extension SSHClientX on SSHClient { if (context == null) return; final pwd = await context.showPwdDialog(user); if (pwd == null || pwd.isEmpty) { - return; + // Add ctrl + c to exit. + sink.add('\x03'.uint8List); + } else { + sink.add('$pwd\n'.uint8List); } - sink.add('$pwd\n'.uint8List); } }, onStdout: onStdout, diff --git a/lib/core/extension/stringx.dart b/lib/core/extension/stringx.dart index dddd22e7..62f1bb96 100644 --- a/lib/core/extension/stringx.dart +++ b/lib/core/extension/stringx.dart @@ -14,52 +14,5 @@ extension StringX on String { return Color(val); } - int get i => int.parse(this); - - Uri get uri { - return Uri.parse(this); - } - - Widget omitStartStr({ - TextStyle? style, - TextOverflow? overflow, - int? maxLines, - }) { - return LayoutBuilder(builder: (context, size) { - bool exceeded = false; - int len = 0; - for (; !exceeded && len < length; len++) { - // Build the textspan - var span = TextSpan( - text: 'A' * 7 + substring(length - len), - style: style ?? Theme.of(context).textTheme.bodyMedium, - ); - - // Use a textpainter to determine if it will exceed max lines - var tp = TextPainter( - maxLines: maxLines ?? 1, - textDirection: TextDirection.ltr, - text: span, - ); - - // trigger it to layout - tp.layout(maxWidth: size.maxWidth); - - // whether the text overflowed or not - exceeded = tp.didExceedMaxLines; - } - - return Text( - (exceeded ? '...' : '') + substring(length - len), - overflow: overflow ?? TextOverflow.fade, - softWrap: false, - maxLines: maxLines ?? 1, - style: style, - ); - }); - } - - String get withLangExport => 'export LANG=en_US.UTF-8 && $this'; - Uint8List get uint8List => Uint8List.fromList(utf8.encode(this)); } diff --git a/lib/core/update.dart b/lib/core/update.dart index 5d9bd01c..3e9d2606 100644 --- a/lib/core/update.dart +++ b/lib/core/update.dart @@ -39,7 +39,7 @@ Future doUpdate(BuildContext context, {bool force = false}) async { return; } - Providers.app.newestBuild = newest; + Pros.app.newestBuild = newest; if (!force && newest <= BuildData.build) { Loggers.app.info('Update ignored: ${BuildData.build} >= $newest'); diff --git a/lib/core/utils/misc.dart b/lib/core/utils/misc.dart index 99b7a4b3..b5d80319 100644 --- a/lib/core/utils/misc.dart +++ b/lib/core/utils/misc.dart @@ -19,10 +19,10 @@ Future shareFiles(List filePaths) async { } else { text = '${filePaths.length} ${l10n.files}'; } - Providers.app.moveBg = false; + Pros.app.moveBg = false; // ignore: deprecated_member_use await Share.shareFiles(filePaths, subject: text); - Providers.app.moveBg = true; + Pros.app.moveBg = true; return filePaths.isNotEmpty; } @@ -31,9 +31,9 @@ void copy2Clipboard(String text) { } Future pickOneFile() async { - Providers.app.moveBg = false; + Pros.app.moveBg = false; final result = await FilePicker.platform.pickFiles(type: FileType.any); - Providers.app.moveBg = true; + Pros.app.moveBg = true; return result?.files.single.path; } diff --git a/lib/core/utils/ui.dart b/lib/core/utils/ui.dart index cb13a465..7af992d4 100644 --- a/lib/core/utils/ui.dart +++ b/lib/core/utils/ui.dart @@ -7,11 +7,10 @@ import 'package:toolbox/core/utils/platform/base.dart'; import 'package:url_launcher/url_launcher.dart'; import 'misc.dart'; -import '../extension/stringx.dart'; import '../extension/uint8list.dart'; Future openUrl(String url) async { - return await launchUrl(url.uri, mode: LaunchMode.externalApplication); + return await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } void setTransparentNavigationBar(BuildContext context) { diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart index 162885b9..7d474420 100644 --- a/lib/data/model/app/shell_func.dart +++ b/lib/data/model/app/shell_func.dart @@ -18,6 +18,7 @@ enum ShellFunc { process, shutdown, reboot, + suspend, ; String get flag { @@ -32,6 +33,8 @@ enum ShellFunc { return 'sd'; case ShellFunc.reboot: return 'r'; + case ShellFunc.suspend: + return 'sp'; } } @@ -51,10 +54,12 @@ enum ShellFunc { return 'ShutDown'; case ShellFunc.reboot: return 'Reboot'; + case ShellFunc.suspend: + return 'Suspend'; } } - String get cmd { + String get _cmd { switch (this) { case ShellFunc.status: return ''' @@ -96,6 +101,13 @@ if [ "\$userId" = "0" ]; then \treboot else \tsudo -S reboot +fi'''; + case ShellFunc.suspend: + return ''' +if [ "\$userId" = "0" ]; then +\tsystemctl suspend +else +\tsudo -S systemctl suspend fi'''; } } @@ -123,7 +135,7 @@ userId=\$(id -u) for (final func in values) { sb.write(''' ${func.name}() { -${func.cmd.split('\n').map((e) => '\t$e').join('\n')} +${func._cmd.split('\n').map((e) => '\t$e').join('\n')} } '''); diff --git a/lib/data/model/server/conn.dart b/lib/data/model/server/conn.dart index cb4f43c5..132fb30a 100644 --- a/lib/data/model/server/conn.dart +++ b/lib/data/model/server/conn.dart @@ -1,4 +1,3 @@ -import '../../../core/extension/stringx.dart'; import '../../res/misc.dart'; class Conn { @@ -22,10 +21,10 @@ Conn? parseConn(String raw) { if (idx != '') { final vals = idx.split(Miscs.numReg); return Conn( - maxConn: vals[5].i, - active: vals[6].i, - passive: vals[7].i, - fail: vals[8].i, + maxConn: int.tryParse(vals[5]) ?? 0, + active: int.tryParse(vals[6]) ?? 0, + passive: int.tryParse(vals[7]) ?? 0, + fail: int.tryParse(vals[8]) ?? 0, ); } return null; diff --git a/lib/data/model/server/server_private_info.dart b/lib/data/model/server/server_private_info.dart index 59ed2cf1..ca2590c4 100644 --- a/lib/data/model/server/server_private_info.dart +++ b/lib/data/model/server/server_private_info.dart @@ -70,7 +70,7 @@ class ServerPrivateInfo { return data; } - Server? get server => Providers.server.pick(spi: this); + Server? get server => Pros.server.pick(spi: this); bool shouldReconnect(ServerPrivateInfo old) { return id != old.id || diff --git a/lib/data/provider/docker.dart b/lib/data/provider/docker.dart index 9789551b..28b6a62a 100644 --- a/lib/data/provider/docker.dart +++ b/lib/data/provider/docker.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/material.dart'; import 'package:toolbox/core/extension/ssh_client.dart'; -import 'package:toolbox/core/extension/stringx.dart'; import 'package:toolbox/data/model/app/shell_func.dart'; import 'package:toolbox/data/model/docker/image.dart'; import 'package:toolbox/data/model/docker/ps.dart'; @@ -175,9 +174,10 @@ class DockerProvider extends ChangeNotifier { // judge whether to use DOCKER_HOST String _wrap(String cmd) { final dockerHost = Stores.docker.fetch(hostId!); + cmd = 'export LANG=en_US.UTF-8 && $cmd'; if (dockerHost == null || dockerHost.isEmpty) { - return cmd.withLangExport; + return cmd; } - return 'export DOCKER_HOST=$dockerHost && $cmd'.withLangExport; + return 'export DOCKER_HOST=$dockerHost && $cmd'; } } diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index 4d4e37dc..7e962b1a 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -291,7 +291,7 @@ class ServerProvider extends ChangeNotifier { final localPath = joinPath(await Paths.doc, 'install.sh'); final file = File(localPath); file.writeAsString(ShellFunc.allScript); - final sftp = Providers.sftp; + final sftp = Pros.sftp; final completer = Completer(); sftp.add( SftpReq(spi, installShellPath, localPath, SftpReqType.upload), diff --git a/lib/data/provider/snippet.dart b/lib/data/provider/snippet.dart index e1fac275..bd777121 100644 --- a/lib/data/provider/snippet.dart +++ b/lib/data/provider/snippet.dart @@ -31,12 +31,13 @@ class SnippetProvider extends ChangeNotifier { } void _addInternal() { - if (!Stores.setting.fTISBM.fetch() || _snippets.isNotEmpty) { + if (!Stores.first.iSSBM.fetch() || + _snippets.any((e) => e.name == installSBM.name)) { return; } _snippets.add(installSBM); Stores.snippet.put(installSBM); - Stores.setting.fTISBM.put(false); + Stores.first.iSSBM.put(false); } void _updateTags() { diff --git a/lib/data/res/provider.dart b/lib/data/res/provider.dart index 7f6e060d..ee105063 100644 --- a/lib/data/res/provider.dart +++ b/lib/data/res/provider.dart @@ -7,8 +7,8 @@ import 'package:toolbox/data/provider/sftp.dart'; import 'package:toolbox/data/provider/snippet.dart'; import 'package:toolbox/locator.dart'; -class Providers { - const Providers._(); +class Pros { + const Pros._(); static final app = locator(); static final debug = locator(); diff --git a/lib/data/res/store.dart b/lib/data/res/store.dart index 01911716..5b7622de 100644 --- a/lib/data/res/store.dart +++ b/lib/data/res/store.dart @@ -1,4 +1,5 @@ import 'package:toolbox/data/store/docker.dart'; +import 'package:toolbox/data/store/first.dart'; import 'package:toolbox/data/store/history.dart'; import 'package:toolbox/data/store/private_key.dart'; import 'package:toolbox/data/store/server.dart'; @@ -15,4 +16,5 @@ class Stores { static final history = locator(); static final key = locator(); static final snippet = locator(); + static final first = locator(); } diff --git a/lib/data/store/first.dart b/lib/data/store/first.dart new file mode 100644 index 00000000..53349eaa --- /dev/null +++ b/lib/data/store/first.dart @@ -0,0 +1,10 @@ +import 'package:toolbox/core/persistant_store.dart'; + +/// It stores whether is the first time of some. +class FirstStore extends PersistentStore { + /// Add Snippet `Install ServerBoxMonitor` + late final iSSBM = StoreProperty(box, 'installMonitorSnippet', true); + + /// Show tip of suspend + late final showSuspendTip = StoreProperty(box, 'showSuspendTip', true); +} diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 14da1090..6848796b 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -205,7 +205,7 @@ class SettingStore extends PersistentStore { false, ); - /// Only valid on iOS / Android + /// Only valid on iOS / Android / Windows late final useBioAuth = StoreProperty( box, 'useBioAuth', @@ -213,15 +213,11 @@ class SettingStore extends PersistentStore { ); // Never show these settings for users - // Guide for these settings: - // - key should start with `_` and be shorter as possible // // ------BEGIN------ /// Version of store db late final storeVersion = StoreProperty(box, 'storeVersion', 0); - /// Whether is first time to add Snippet - late final fTISBM = StoreProperty(box, '_fTISBM', true); // ------END------ } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 1a0805e1..1f08b041 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -200,6 +200,7 @@ "sureNoPwd": "Bist du sicher, dass du kein Passwort verwenden willst?", "sureStop": "Sind Sie sicher, dass Sie [{item}] stoppen möchten?", "sureToDeleteServer": "Bist du sicher, dass du [{server}] löschen willst?", + "suspendTip": "Die Suspend-Funktion erfordert Root-Rechte und systemd-Unterstützung.", "syncTip": "Nach der automatischen Synchronisierung kann es erforderlich sein, die App neu zu starten, damit bestimmte Änderungen wirksam werden.", "system": "Systeme", "tag": "Tags", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 90fa927c..2b67f6f8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -200,6 +200,7 @@ "sureNoPwd": "Are you sure to use no password?", "sureStop": "Sure to stop [{item}] ?", "sureToDeleteServer": "Are you sure to delete server [{server}]?", + "suspendTip": "The suspend function requires root privileges and systemd support.", "syncTip": "After auto sync, a restart may be required for some changes to take effect.", "system": "System", "tag": "Tags", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index b4ef8f4d..c2fbf261 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -200,6 +200,7 @@ "sureNoPwd": "Apakah Anda pasti tidak menggunakan kata sandi?", "sureStop": "Anda yakin ingin menghentikan [{item}]?", "sureToDeleteServer": "Apakah Anda pasti akan menghapus server [{server}]?", + "suspendTip": "Fungsi penangguhan memerlukan hak akses root dan dukungan systemd.", "syncTip": "Setelah sinkronisasi otomatis, mungkin perlu memulai ulang aplikasi agar perubahan tertentu dapat diterapkan.", "system": "Sistem", "tag": "Tag", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5e454a49..0839c46a 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -200,6 +200,7 @@ "sureNoPwd": "确认使用无密码?", "sureStop": "确定要停止 [{item}] 吗?", "sureToDeleteServer": "你确定要删除服务器 [{server}] 吗?", + "suspendTip": "suspend 功能需要 root 权限及 systemd 支持。", "syncTip": "在自动同步后,可能需要重新启动,某些更改才能生效。", "system": "系统", "tag": "标签", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 2f1cedc5..0ef0baee 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -200,6 +200,7 @@ "sureNoPwd": "確認使用無密碼?", "sureStop": "確定要停止 [{item}] 嗎?", "sureToDeleteServer": "你確定要刪除服務器 [{server}] 嗎?", + "suspendTip": "suspend 功能需要 root 權限及 systemd 支持。", "syncTip": "在自動同步後,可能需要重新啟動,某些更改才能生效。", "system": "系統", "tag": "标签", diff --git a/lib/locator.dart b/lib/locator.dart index 99935a70..183cc345 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,4 +1,5 @@ import 'package:get_it/get_it.dart'; +import 'package:toolbox/data/store/first.dart'; import 'data/provider/app.dart'; import 'data/provider/debug.dart'; @@ -57,6 +58,10 @@ Future _setupLocatorForStores() async { final history = HistoryStore(); await history.init(boxName: 'history'); locator.registerSingleton(history); + + final first = FirstStore(); + await first.init(boxName: 'first'); + locator.registerSingleton(first); } Future setupLocator() async { diff --git a/lib/main.dart b/lib/main.dart index 0c217e0f..ba3bfe3d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -92,8 +92,8 @@ Future initApp() async { } void _setupProviders() { - Providers.snippet.load(); - Providers.key.load(); + Pros.snippet.load(); + Pros.key.load(); } Future _initHive() async { @@ -109,7 +109,7 @@ Future _initHive() async { void _setupLogger() { Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) { - Providers.debug.addLog(record); + Pros.debug.addLog(record); // ignore: avoid_print print(record); }); diff --git a/lib/view/page/backup.dart b/lib/view/page/backup.dart index d9a34ba8..b96c3b96 100644 --- a/lib/view/page/backup.dart +++ b/lib/view/page/backup.dart @@ -164,7 +164,7 @@ class BackupPage extends StatelessWidget { backup.restore(); context.pop(); RebuildNodes.app.rebuild(); - Providers.reload(); + Pros.reload(); }, child: Text(l10n.ok), ), diff --git a/lib/view/page/docker.dart b/lib/view/page/docker.dart index 991afb8f..3a0bea18 100644 --- a/lib/view/page/docker.dart +++ b/lib/view/page/docker.dart @@ -37,7 +37,7 @@ class _DockerManagePageState extends State { @override void dispose() { super.dispose(); - Providers.docker.clear(); + Pros.docker.clear(); _textController.dispose(); } @@ -48,7 +48,7 @@ class _DockerManagePageState extends State { if (client == null) { return; } - Providers.docker + Pros.docker ..init( client, widget.spi.user, @@ -70,7 +70,7 @@ class _DockerManagePageState extends State { IconButton( onPressed: () async { context.showLoadingDialog(); - await Providers.docker.refresh(); + await Pros.docker.refresh(); context.pop(); }, icon: const Icon(Icons.refresh), @@ -78,8 +78,7 @@ class _DockerManagePageState extends State { ], ), body: _buildMain(), - floatingActionButton: - Providers.docker.error == null ? _buildFAB() : null, + floatingActionButton: Pros.docker.error == null ? _buildFAB() : null, ); }); } @@ -156,7 +155,7 @@ class _DockerManagePageState extends State { onPressed: () async { context.pop(); context.showLoadingDialog(); - final result = await Providers.docker.run(cmd); + final result = await Pros.docker.run(cmd); context.pop(); if (result != null) { context.showSnackBar(result.message ?? l10n.unknownError); @@ -182,7 +181,7 @@ class _DockerManagePageState extends State { } Widget _buildMain() { - if (Providers.docker.error != null && Providers.docker.items == null) { + if (Pros.docker.error != null && Pros.docker.items == null) { return SizedBox.expand( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -193,17 +192,17 @@ class _DockerManagePageState extends State { size: 37, ), const SizedBox(height: 27), - Text(Providers.docker.error?.message ?? l10n.unknownError), + Text(Pros.docker.error?.message ?? l10n.unknownError), const SizedBox(height: 27), Padding( padding: const EdgeInsets.all(17), - child: _buildSolution(Providers.docker.error!), + child: _buildSolution(Pros.docker.error!), ) ], ), ); } - if (Providers.docker.items == null || Providers.docker.images == null) { + if (Pros.docker.items == null || Pros.docker.images == null) { return UIs.centerLoading; } @@ -225,12 +224,12 @@ class _DockerManagePageState extends State { ListTile( title: Text(l10n.imagesList), subtitle: Text( - l10n.dockerImagesFmt(Providers.docker.images!.length), + l10n.dockerImagesFmt(Pros.docker.images!.length), style: UIs.textGrey, ), ), ]; - items.addAll(Providers.docker.images!.map(_buildImageItem)); + items.addAll(Pros.docker.images!.map(_buildImageItem)); return Column(children: items); } @@ -259,7 +258,7 @@ class _DockerManagePageState extends State { TextButton( onPressed: () async { context.pop(); - final result = await Providers.docker.run( + final result = await Pros.docker.run( 'docker rmi ${e.id} -f', ); if (result != null) { @@ -273,7 +272,7 @@ class _DockerManagePageState extends State { } Widget _buildLoading() { - if (Providers.docker.runLog == null) return UIs.placeholder; + if (Pros.docker.runLog == null) return UIs.placeholder; return Padding( padding: const EdgeInsets.all(17), child: Column( @@ -282,7 +281,7 @@ class _DockerManagePageState extends State { child: CircularProgressIndicator(), ), UIs.height13, - Text(Providers.docker.runLog ?? '...'), + Text(Pros.docker.runLog ?? '...'), ], ), ); @@ -323,8 +322,8 @@ class _DockerManagePageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(Providers.docker.edition ?? l10n.unknown), - Text(Providers.docker.version ?? l10n.unknown), + Text(Pros.docker.edition ?? l10n.unknown), + Text(Pros.docker.version ?? l10n.unknown), ], ), ); @@ -335,12 +334,12 @@ class _DockerManagePageState extends State { ListTile( title: Text(l10n.containerStatus), subtitle: Text( - _buildPsCardSubtitle(Providers.docker.items!), + _buildPsCardSubtitle(Pros.docker.items!), style: UIs.textGrey, ), ), ]; - items.addAll(Providers.docker.items!.map(_buildPsItem)); + items.addAll(Pros.docker.items!.map(_buildPsItem)); return Column( mainAxisSize: MainAxisSize.min, children: items, @@ -372,7 +371,7 @@ class _DockerManagePageState extends State { onPressed: () async { context.pop(); context.showLoadingDialog(); - await Providers.docker.delete(dItem.containerId); + await Pros.docker.delete(dItem.containerId); context.pop(); }, child: Text(l10n.ok), @@ -382,17 +381,17 @@ class _DockerManagePageState extends State { break; case DockerMenuType.start: context.showLoadingDialog(); - await Providers.docker.start(dItem.containerId); + await Pros.docker.start(dItem.containerId); context.pop(); break; case DockerMenuType.stop: context.showLoadingDialog(); - await Providers.docker.stop(dItem.containerId); + await Pros.docker.stop(dItem.containerId); context.pop(); break; case DockerMenuType.restart: context.showLoadingDialog(); - await Providers.docker.restart(dItem.containerId); + await Pros.docker.restart(dItem.containerId); context.pop(); break; case DockerMenuType.logs: @@ -441,7 +440,7 @@ class _DockerManagePageState extends State { Widget _buildEditHost() { final children = []; - if (Providers.docker.items!.isEmpty && Providers.docker.images!.isEmpty) { + if (Pros.docker.items!.isEmpty && Pros.docker.images!.isEmpty) { children.add(Padding( padding: const EdgeInsets.fromLTRB(17, 17, 17, 0), child: Text( @@ -484,6 +483,6 @@ class _DockerManagePageState extends State { void _onSaveDockerHost(String val) { context.pop(); Stores.docker.put(widget.spi.id, val.trim()); - Providers.docker.refresh(); + Pros.docker.refresh(); } } diff --git a/lib/view/page/full_screen.dart b/lib/view/page/full_screen.dart index f3f33b31..f4182d79 100644 --- a/lib/view/page/full_screen.dart +++ b/lib/view/page/full_screen.dart @@ -368,8 +368,8 @@ class _FullScreenPageState extends State with AfterLayoutMixin { doUpdate(context); } await GetIt.I.allReady(); - await Providers.server.load(); - await Providers.server.refreshData(); + await Pros.server.load(); + await Pros.server.refreshData(); if (!Analysis.enabled) { await Analysis.init(); } diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 9d994af3..7fce618f 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -72,7 +72,7 @@ class _HomePageState extends State void dispose() { super.dispose(); WidgetsBinding.instance.removeObserver(this); - Providers.server.closeServer(); + Pros.server.closeServer(); _pageController.dispose(); } @@ -84,20 +84,20 @@ class _HomePageState extends State switch (state) { case AppLifecycleState.resumed: _auth(); - if (!Providers.server.isAutoRefreshOn) { - Providers.server.startAutoRefresh(); + if (!Pros.server.isAutoRefreshOn) { + Pros.server.startAutoRefresh(); } updateHomeWidget(); break; case AppLifecycleState.paused: // Keep running in background on Android device if (isAndroid && Stores.setting.bgRun.fetch()) { - if (Providers.app.moveBg) { + if (Pros.app.moveBg) { BgRunMC.moveToBg(); } } else { - Providers.server.setDisconnected(); - Providers.server.stopAutoRefresh(); + Pros.server.setDisconnected(); + Pros.server.stopAutoRefresh(); } break; default: @@ -148,7 +148,7 @@ class _HomePageState extends State return IconButton( icon: const Icon(Icons.refresh, size: 23), tooltip: 'Refresh', - onPressed: () => Providers.server.refreshData(onlyFailed: true), + onPressed: () => Pros.server.refreshData(onlyFailed: true), ); }, ), @@ -343,8 +343,8 @@ class _HomePageState extends State } updateHomeWidget(); await GetIt.I.allReady(); - await Providers.server.load(); - await Providers.server.refreshData(); + await Pros.server.load(); + await Pros.server.refreshData(); if (!Analysis.enabled) { Analysis.init(); } diff --git a/lib/view/page/ping.dart b/lib/view/page/ping.dart index b0ece9d3..e9432ee0 100644 --- a/lib/view/page/ping.dart +++ b/lib/view/page/ping.dart @@ -165,7 +165,7 @@ class _PingPageState extends State return; } - if (Providers.server.serverOrder.isEmpty) { + if (Pros.server.serverOrder.isEmpty) { context.showSnackBar(l10n.pingNoServer); return; } @@ -176,7 +176,7 @@ class _PingPageState extends State return; } - await Future.wait(Providers.server.servers.map((e) async { + await Future.wait(Pros.server.servers.map((e) async { if (e.client == null) { return; } diff --git a/lib/view/page/private_key/edit.dart b/lib/view/page/private_key/edit.dart index ff779272..8c1eec95 100644 --- a/lib/view/page/private_key/edit.dart +++ b/lib/view/page/private_key/edit.dart @@ -95,7 +95,7 @@ class _PrivateKeyEditPageState extends State { actions: [ TextButton( onPressed: () { - Providers.key.delete(widget.pki!); + Pros.key.delete(widget.pki!); context.pop(); context.pop(); }, @@ -135,9 +135,9 @@ class _PrivateKeyEditPageState extends State { final decrypted = await compute(decyptPem, [key, pwd]); final pki = PrivateKeyInfo(id: name, key: decrypted); if (widget.pki != null) { - Providers.key.update(widget.pki!, pki); + Pros.key.update(widget.pki!, pki); } else { - Providers.key.add(pki); + Pros.key.add(pki); } } catch (e) { context.showSnackBar(e.toString()); diff --git a/lib/view/page/server/edit.dart b/lib/view/page/server/edit.dart index cf02406a..ecebe1b2 100644 --- a/lib/view/page/server/edit.dart +++ b/lib/view/page/server/edit.dart @@ -58,7 +58,7 @@ class _ServerEditPageState extends State { if (widget.spi?.pubKeyId == null) { _passwordController.text = widget.spi?.pwd ?? ''; } else { - _keyIdx.value = Providers.key.pkis.indexWhere( + _keyIdx.value = Pros.key.pkis.indexWhere( (e) => e.id == widget.spi!.pubKeyId, ); } @@ -115,7 +115,7 @@ class _ServerEditPageState extends State { actions: [ TextButton( onPressed: () { - Providers.server.delServer(widget.spi!.id); + Pros.server.delServer(widget.spi!.id); context.pop(); context.pop(true); }, @@ -183,8 +183,8 @@ class _ServerEditPageState extends State { TagEditor( tags: _tags, onChanged: (p0) => _tags = p0, - allTags: [...Providers.server.tags], - onRenameTag: Providers.server.renameTag, + allTags: [...Pros.server.tags], + onRenameTag: Pros.server.renameTag, ), _buildAuth(), ListTile( @@ -357,7 +357,7 @@ class _ServerEditPageState extends State { user: _usernameController.text, pwd: _passwordController.text.isEmpty ? null : _passwordController.text, pubKeyId: _keyIdx.value != null - ? Providers.key.pkis.elementAt(_keyIdx.value!).id + ? Pros.key.pkis.elementAt(_keyIdx.value!).id : null, tags: _tags, alterUrl: _altUrlController.text.isEmpty ? null : _altUrlController.text, @@ -365,9 +365,9 @@ class _ServerEditPageState extends State { ); if (widget.spi == null) { - Providers.server.addServer(spi); + Pros.server.addServer(spi); } else { - Providers.server.updateServer(widget.spi!, spi); + Pros.server.updateServer(widget.spi!, spi); } context.pop(); diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index ca1fe7ab..5771b636 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -92,8 +92,7 @@ class _ServerPageState extends State return child; } return RefreshIndicator( - onRefresh: () async => - await Providers.server.refreshData(onlyFailed: true), + onRefresh: () async => await Pros.server.refreshData(onlyFailed: true), child: child, ); } @@ -245,22 +244,42 @@ class _ServerPageState extends State mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton( - onPressed: () => srv.client?.execWithPwd( - ShellFunc.shutdown.cmd, - context: context, - ), - icon: const Icon(Icons.power_off), + onPressed: () async { + if (Stores.first.showSuspendTip.fetch()) { + await context.showRoundDialog( + title: Text(l10n.attention), + child: Text(l10n.suspendTip), + ); + Stores.first.showSuspendTip.put(false); + } + srv.client?.execWithPwd( + ShellFunc.suspend.exec, + context: context, + ); + }, + icon: const Icon(Icons.stop), + tooltip: 'Suspend', ), IconButton( onPressed: () => srv.client?.execWithPwd( - ShellFunc.reboot.cmd, + ShellFunc.shutdown.exec, context: context, ), - icon: const Icon(Icons.refresh), + icon: const Icon(Icons.power_off), + tooltip: 'Shutdown', + ), + IconButton( + onPressed: () => srv.client?.execWithPwd( + ShellFunc.reboot.exec, + context: context, + ), + icon: const Icon(Icons.restart_alt), + tooltip: 'Reboot', ), IconButton( onPressed: () => AppRoute.serverEdit(spi: srv.spi).go(context), icon: const Icon(Icons.edit), + tooltip: l10n.edit, ) ], ) @@ -305,7 +324,7 @@ class _ServerPageState extends State Widget? rightCorner; if (!(spi.autoConnect ?? true) && cs == ServerState.disconnected) { rightCorner = InkWell( - onTap: () => Providers.server.refreshData(spi: spi), + onTap: () => Pros.server.refreshData(spi: spi), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 7), child: Icon( @@ -456,8 +475,8 @@ class _ServerPageState extends State @override Future afterFirstLayout(BuildContext context) async { await GetIt.I.allReady(); - await Providers.server.load(); - Providers.server.startAutoRefresh(); + await Pros.server.load(); + Pros.server.startAutoRefresh(); } List _filterServers(ServerProvider pro) => pro.serverOrder diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index d3350954..41fb5074 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -298,7 +298,7 @@ class _SettingPageState extends State { onSelected: (int val) { _updateInterval.value = val; _setting.serverStatusUpdateInterval.put(val); - Providers.server.startAutoRefresh(); + Pros.server.startAutoRefresh(); if (val == 0) { context.showSnackBar(l10n.updateIntervalEqual0); } @@ -883,7 +883,7 @@ class _SettingPageState extends State { child: Text(l10n.sureDelete(e)), actions: [ TextButton( - onPressed: () => Providers.server.delServer(e), + onPressed: () => Pros.server.delServer(e), child: Text(l10n.ok), ) ], diff --git a/lib/view/page/setting/srv_seq.dart b/lib/view/page/setting/srv_seq.dart index 11591fc5..3172544a 100644 --- a/lib/view/page/setting/srv_seq.dart +++ b/lib/view/page/setting/srv_seq.dart @@ -26,13 +26,13 @@ class _ServerOrderPageState extends State { } Widget _buildBody() { - if (Providers.server.serverOrder.isEmpty) { + if (Pros.server.serverOrder.isEmpty) { return Center(child: Text(l10n.noServerAvailable)); } return ReorderableListView.builder( footer: const SizedBox(height: 77), onReorder: (oldIndex, newIndex) => setState(() { - Providers.server.serverOrder.move( + Pros.server.serverOrder.move( oldIndex, newIndex, property: Stores.setting.serverOrder, @@ -41,13 +41,13 @@ class _ServerOrderPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), buildDefaultDragHandles: false, itemBuilder: (_, index) => - _buildItem(index, Providers.server.serverOrder[index]), - itemCount: Providers.server.serverOrder.length, + _buildItem(index, Pros.server.serverOrder[index]), + itemCount: Pros.server.serverOrder.length, ); } Widget _buildItem(int index, String id) { - final spi = Providers.server.pick(id: id)?.spi; + final spi = Pros.server.pick(id: id)?.spi; if (spi == null) { return const SizedBox(); } diff --git a/lib/view/page/snippet/edit.dart b/lib/view/page/snippet/edit.dart index bea4c985..bb79848f 100644 --- a/lib/view/page/snippet/edit.dart +++ b/lib/view/page/snippet/edit.dart @@ -56,7 +56,7 @@ class _SnippetEditPageState extends State return [ IconButton( onPressed: () { - Providers.snippet.del(widget.snippet!); + Pros.snippet.del(widget.snippet!); context.pop(); }, tooltip: l10n.delete, @@ -84,9 +84,9 @@ class _SnippetEditPageState extends State note: note.isEmpty ? null : note, ); if (widget.snippet != null) { - Providers.snippet.update(widget.snippet!, snippet); + Pros.snippet.update(widget.snippet!, snippet); } else { - Providers.snippet.add(snippet); + Pros.snippet.add(snippet); } context.pop(); }, @@ -118,9 +118,9 @@ class _SnippetEditPageState extends State onChanged: (p0) => setState(() { _tags = p0; }), - allTags: [...Providers.server.tags], + allTags: [...Pros.server.tags], onRenameTag: (old, n) => setState(() { - Providers.server.renameTag(old, n); + Pros.server.renameTag(old, n); }), ), Input( diff --git a/lib/view/page/snippet/list.dart b/lib/view/page/snippet/list.dart index 99e60065..8a827fae 100644 --- a/lib/view/page/snippet/list.dart +++ b/lib/view/page/snippet/list.dart @@ -130,15 +130,15 @@ class _SnippetListPageState extends State { final servers = await showDialog>( context: context, builder: (_) => TagPicker( - items: Providers.server.servers.toList(), - tags: Providers.server.tags.toSet(), + items: Pros.server.servers.toList(), + tags: Pros.server.tags.toSet(), ), ); if (servers == null) { return; } final ids = servers.map((e) => e.spi.id).toList(); - final results = await Providers.server.runSnippetsMulti(ids, [snippet]); + final results = await Pros.server.runSnippetsMulti(ids, [snippet]); if (results.isNotEmpty) { // SERVER_NAME: RESULT final result = Map.fromIterables( diff --git a/lib/view/page/storage/local.dart b/lib/view/page/storage/local.dart index 9bb6bea0..ff079114 100644 --- a/lib/view/page/storage/local.dart +++ b/lib/view/page/storage/local.dart @@ -9,11 +9,11 @@ import 'package:toolbox/data/model/sftp/req.dart'; import 'package:toolbox/data/res/misc.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/view/widget/input_field.dart'; +import 'package:toolbox/view/widget/omit_start_text.dart'; import 'package:toolbox/view/widget/picker.dart'; import 'package:toolbox/view/widget/round_rect_card.dart'; import '../../../core/extension/numx.dart'; -import '../../../core/extension/stringx.dart'; import '../../../core/route.dart'; import '../../../core/utils/misc.dart'; import '../../../data/model/app/path_with_prefix.dart'; @@ -89,7 +89,7 @@ class _LocalStoragePageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - (_path?.path ?? l10n.loadingFiles).omitStartStr(), + OmitStartText(_path?.path ?? l10n.loadingFiles), _buildBtns(), ], ), @@ -275,7 +275,7 @@ class _LocalStoragePageState extends State { title: Text(l10n.upload), onTap: () async { context.pop(); - final ids = Providers.server.serverOrder; + final ids = Pros.server.serverOrder; var idx = 0; await context.showRoundDialog( title: Text(l10n.server), @@ -289,7 +289,7 @@ class _LocalStoragePageState extends State { ], ); final id = ids[idx]; - final spi = Providers.server.pick(id: id)?.spi; + final spi = Pros.server.pick(id: id)?.spi; if (spi == null) { return; } @@ -300,7 +300,7 @@ class _LocalStoragePageState extends State { if (remotePath == null) { return; } - Providers.sftp.add(SftpReq( + Pros.sftp.add(SftpReq( spi, '$remotePath/$fileName', file.absolute.path, diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index 60da42d0..9796762a 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -13,10 +13,10 @@ import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/misc.dart'; import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/store.dart'; +import 'package:toolbox/view/widget/omit_start_text.dart'; import 'package:toolbox/view/widget/round_rect_card.dart'; import '../../../core/extension/numx.dart'; -import '../../../core/extension/stringx.dart'; import '../../../core/route.dart'; import '../../../core/utils/misc.dart'; import '../../../data/model/server/server_private_info.dart'; @@ -113,7 +113,7 @@ class _SftpPageState extends State with AfterLayoutMixin { child: Column( mainAxisSize: MainAxisSize.min, children: [ - (_status.path?.path ?? l10n.loadingFiles).omitStartStr(), + OmitStartText(_status.path?.path ?? l10n.loadingFiles), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: children, @@ -162,7 +162,7 @@ class _SftpPageState extends State with AfterLayoutMixin { context.showSnackBar('remote path is null'); return; } - Providers.sftp.add( + Pros.sftp.add( SftpReq( widget.spi, '$remotePath/${path.split('/').last}', @@ -362,14 +362,14 @@ class _SftpPageState extends State with AfterLayoutMixin { localPath, SftpReqType.download, ); - Providers.sftp.add(req, completer: completer); + Pros.sftp.add(req, completer: completer); context.showLoadingDialog(); await completer.future; context.pop(); final result = await AppRoute.editor(path: localPath).go(context); if (result != null && result) { - Providers.sftp + Pros.sftp .add(SftpReq(req.spi, remotePath, localPath, SftpReqType.upload)); context.showSnackBar(l10n.added2List); } @@ -389,7 +389,7 @@ class _SftpPageState extends State with AfterLayoutMixin { context.pop(); final remotePath = _getRemotePath(name); - Providers.sftp.add( + Pros.sftp.add( SftpReq( widget.spi, remotePath, diff --git a/lib/view/page/storage/sftp_mission.dart b/lib/view/page/storage/sftp_mission.dart index b765e9ad..8ffde6a2 100644 --- a/lib/view/page/storage/sftp_mission.dart +++ b/lib/view/page/storage/sftp_mission.dart @@ -141,7 +141,7 @@ class _SftpMissionPageState extends State { actions: [ TextButton( onPressed: () { - Providers.sftp.cancel(id); + Pros.sftp.cancel(id); context.pop(); }, child: Text(l10n.ok), diff --git a/lib/view/widget/omit_start_text.dart b/lib/view/widget/omit_start_text.dart new file mode 100644 index 00000000..fb16ad9c --- /dev/null +++ b/lib/view/widget/omit_start_text.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +class OmitStartText extends StatelessWidget { + final String text; + final int? maxLines; + final TextStyle? style; + final TextOverflow? overflow; + + const OmitStartText( + this.text, { + Key? key, + this.maxLines, + this.style, + this.overflow, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, size) { + bool exceeded = false; + int len = 0; + for (; !exceeded && len < text.length; len++) { + // Build the textspan + var span = TextSpan( + text: 'A' * 7 + text.substring(text.length - len), + style: style ?? Theme.of(context).textTheme.bodyMedium, + ); + + // Use a textpainter to determine if it will exceed max lines + var tp = TextPainter( + maxLines: maxLines ?? 1, + textDirection: TextDirection.ltr, + text: span, + ); + + // trigger it to layout + tp.layout(maxWidth: size.maxWidth); + + // whether the text overflowed or not + exceeded = tp.didExceedMaxLines; + } + + return Text( + (exceeded ? '...' : '') + text.substring(text.length - len), + overflow: overflow ?? TextOverflow.fade, + softWrap: false, + maxLines: maxLines ?? 1, + style: style, + ); + }); + } +} \ No newline at end of file diff --git a/lib/view/widget/server_func_btns.dart b/lib/view/widget/server_func_btns.dart index 581382d1..ae40a283 100644 --- a/lib/view/widget/server_func_btns.dart +++ b/lib/view/widget/server_func_btns.dart @@ -99,14 +99,14 @@ void _onTapMoreBtns( final snippets = await showDialog>( context: context, builder: (_) => TagPicker( - items: Providers.snippet.snippets, - tags: Providers.server.tags.toSet(), + items: Pros.snippet.snippets, + tags: Pros.server.tags.toSet(), ), ); if (snippets == null) { return; } - final result = await Providers.server.runSnippets(spi.id, snippets); + final result = await Pros.server.runSnippets(spi.id, snippets); if (result != null && result.isNotEmpty) { context.showRoundDialog( title: Text(l10n.result), @@ -188,7 +188,7 @@ Future _gotoSSH( } bool _checkClient(BuildContext context, String id) { - final server = Providers.server.pick(id: id); + final server = Pros.server.pick(id: id); if (server == null || server.client == null) { context.showSnackBar(l10n.waitConnection); return false;