diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index ede55cd4..88a5a11e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -690,7 +690,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -700,7 +700,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -826,7 +826,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -836,7 +836,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -854,7 +854,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -864,7 +864,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -885,7 +885,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -898,7 +898,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; @@ -924,7 +924,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -937,7 +937,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -960,7 +960,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -973,7 +973,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -996,7 +996,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -1008,7 +1008,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; @@ -1037,7 +1037,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -1049,7 +1049,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; @@ -1075,7 +1075,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -1087,7 +1087,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index d0cfb68c..459936aa 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -13,6 +13,7 @@ typedef _OnStdin = void Function(SSHSession session); typedef PwdRequestFunc = Future Function(String? user); extension SSHClientX on SSHClient { + /// TODO: delete [exec] Future exec( String cmd, { _OnStdout? onStderr, @@ -135,4 +136,36 @@ extension SSHClientX on SSHClient { return result.takeBytes(); } + + Future runScriptIn( + String cmd, { + String shell = '/bin/sh', + bool stdout = true, + bool stderr = true, + }) async { + final session = await execute('cat | $shell'); + + final result = BytesBuilder(copy: false); + final stdoutDone = Completer(); + final stderrDone = Completer(); + + session.stdout.listen( + stdout ? result.add : (_) {}, + onDone: stdoutDone.complete, + onError: stderrDone.completeError, + ); + session.stderr.listen( + stderr ? result.add : (_) {}, + onDone: stderrDone.complete, + onError: stderrDone.completeError, + ); + + session.stdin.add('$cmd\n'.uint8List); + session.stdin.close(); + + await stdoutDone.future; + await stderrDone.future; + + return result.takeBytes().string; + } } diff --git a/lib/core/utils/ssh_auth.dart b/lib/core/utils/ssh_auth.dart index bf9ad68a..b2754a12 100644 --- a/lib/core/utils/ssh_auth.dart +++ b/lib/core/utils/ssh_auth.dart @@ -4,7 +4,7 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; -import 'package:server_box/data/res/provider.dart'; +import 'package:server_box/data/provider/app.dart'; abstract final class KeybordInteractive { static FutureOr?> defaultHandle( @@ -12,8 +12,8 @@ abstract final class KeybordInteractive { BuildContext? ctx, }) async { try { - final res = await (ctx ?? Pros.app.ctx)?.showPwdDialog( - title: '2FA ${l10n.pwd}', + final res = await (ctx ?? AppProvider.ctx)?.showPwdDialog( + title: l10n.pwd, id: spi.id, label: spi.id, ); diff --git a/lib/data/model/app/menu/server_func.dart b/lib/data/model/app/menu/server_func.dart index c97628e9..978da730 100644 --- a/lib/data/model/app/menu/server_func.dart +++ b/lib/data/model/app/menu/server_func.dart @@ -2,29 +2,47 @@ import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:server_box/core/extension/context/locale.dart'; +import 'package:server_box/data/res/store.dart'; part 'server_func.g.dart'; @HiveType(typeId: 6) enum ServerFuncBtn { @HiveField(0) - terminal, + terminal._(), @HiveField(1) - sftp, + sftp._(), @HiveField(2) - container, + container._(), @HiveField(3) - process, + process._(), //@HiveField(4) //pkg, @HiveField(5) - snippet, + snippet._(), @HiveField(6) - iperf, + iperf._(), // @HiveField(7) // pve, + @HiveField(8) + systemd._(1058), ; + final int? addedVersion; + + const ServerFuncBtn._([this.addedVersion]); + + static void autoAddNewFuncs(int cur) { + if (cur >= systemd.addedVersion!) { + final prop = Stores.setting.serverFuncBtns; + final list = prop.fetch(); + if (!list.contains(systemd.index)) { + list.add(systemd.index); + prop.put(list); + } + } + } + static final defaultIdxs = [ terminal, sftp, @@ -32,6 +50,7 @@ enum ServerFuncBtn { process, //pkg, snippet, + systemd, ].map((e) => e.index).toList(); IconData get icon => switch (this) { @@ -42,6 +61,7 @@ enum ServerFuncBtn { process => Icons.list_alt_outlined, terminal => Icons.terminal, iperf => Icons.speed, + systemd => MingCute.plugin_2_fill, }; String get toStr => switch (this) { @@ -52,5 +72,6 @@ enum ServerFuncBtn { process => l10n.process, terminal => l10n.terminal, iperf => 'iperf', + systemd => 'Systemd', }; } diff --git a/lib/data/model/app/menu/server_func.g.dart b/lib/data/model/app/menu/server_func.g.dart index e679540c..8bc2bdcc 100644 --- a/lib/data/model/app/menu/server_func.g.dart +++ b/lib/data/model/app/menu/server_func.g.dart @@ -25,6 +25,8 @@ class ServerFuncBtnAdapter extends TypeAdapter { return ServerFuncBtn.snippet; case 6: return ServerFuncBtn.iperf; + case 8: + return ServerFuncBtn.systemd; default: return ServerFuncBtn.terminal; } @@ -51,6 +53,9 @@ class ServerFuncBtnAdapter extends TypeAdapter { case ServerFuncBtn.iperf: writer.writeByte(6); break; + case ServerFuncBtn.systemd: + writer.writeByte(8); + break; } } diff --git a/lib/data/model/server/systemd.dart b/lib/data/model/server/systemd.dart new file mode 100644 index 00000000..f378e5c5 --- /dev/null +++ b/lib/data/model/server/systemd.dart @@ -0,0 +1,118 @@ +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter/material.dart'; + +enum SystemdUnitFunc { + start, + stop, + restart, + reload, + enable, + disable, + status, + ; + + IconData get icon => switch (this) { + start => Icons.play_arrow, + stop => Icons.stop, + restart => Icons.refresh, + reload => Icons.refresh, + enable => Icons.check, + disable => Icons.close, + status => Icons.info, + }; +} + +enum SystemdUnitType { + service, + socket, + mount, + timer, + ; + + static SystemdUnitType? fromString(String? value) { + return values.firstWhereOrNull((e) => e.name == value?.toLowerCase()); + } +} + +enum SystemdUnitScope { + system, + user, + ; + + Color? get color => switch (this) { + system => Colors.red, + _ => null, + }; + + String getCmdPrefix(bool isRoot) { + if (this == system) { + return isRoot ? 'systemctl' : 'sudo systemctl'; + } + return 'systemctl --user'; + } +} + +enum SystemdUnitState { + active, + inactive, + failed, + activating, + deactivating, + ; + + static SystemdUnitState? fromString(String? value) { + return values.firstWhereOrNull((e) => e.name == value?.toLowerCase()); + } + + Color? get color => switch (this) { + failed => Colors.red, + _ => null, + }; +} + +final class SystemdUnit { + final String name; + final String? description; + final SystemdUnitType type; + final SystemdUnitScope scope; + final SystemdUnitState state; + + SystemdUnit({ + required this.name, + this.description, + required this.type, + required this.scope, + required this.state, + }); + + String getCmd({ + required SystemdUnitFunc func, + required bool isRoot, + }) { + final prefix = scope.getCmdPrefix(isRoot); + return '$prefix ${func.name} $name'; + } + + List get availableFuncs { + final funcs = {}; + switch (state) { + case SystemdUnitState.active: + funcs.addAll([SystemdUnitFunc.stop, SystemdUnitFunc.restart]); + break; + case SystemdUnitState.inactive: + funcs.addAll([SystemdUnitFunc.start]); + break; + case SystemdUnitState.failed: + funcs.addAll([SystemdUnitFunc.restart]); + break; + case SystemdUnitState.activating: + funcs.addAll([SystemdUnitFunc.stop]); + break; + case SystemdUnitState.deactivating: + funcs.addAll([SystemdUnitFunc.start]); + break; + } + funcs.addAll([SystemdUnitFunc.status]); + return funcs.toList(); + } +} diff --git a/lib/data/provider/app.dart b/lib/data/provider/app.dart index 336e47d5..94a4cec1 100644 --- a/lib/data/provider/app.dart +++ b/lib/data/provider/app.dart @@ -1,30 +1,7 @@ -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; -class AppProvider extends ChangeNotifier { - BuildContext? ctx; +final class AppProvider { + const AppProvider._(); - bool isWearOS = false; - - Future init() async { - await _initIsWearOS(); - } - - Future _initIsWearOS() async { - if (!isAndroid) { - isWearOS = false; - return; - } - - final deviceInfo = DeviceInfoPlugin(); - final androidInfo = await deviceInfo.androidInfo; - - const feat = 'android.hardware.type.watch'; - final hasFeat = androidInfo.systemFeatures.contains(feat); - if (hasFeat) { - isWearOS = true; - return; - } - } + static BuildContext? ctx; } diff --git a/lib/data/provider/systemd.dart b/lib/data/provider/systemd.dart new file mode 100644 index 00000000..22d8db2a --- /dev/null +++ b/lib/data/provider/systemd.dart @@ -0,0 +1,167 @@ +import 'package:dartssh2/dartssh2.dart'; +import 'package:fl_lib/fl_lib.dart'; +import 'package:server_box/core/extension/ssh_client.dart'; +import 'package:server_box/data/model/app/shell_func.dart'; +import 'package:server_box/data/model/server/server_private_info.dart'; +import 'package:server_box/data/model/server/systemd.dart'; +import 'package:server_box/data/res/provider.dart'; + +final class SystemdProvider { + late final SSHClient _client; + + SystemdProvider.init(ServerPrivateInfo spi) { + _client = Pros.server.pick(spi: spi)!.client!; + getUnits(); + } + + final isBusy = false.vn; + final isRoot = false.vn; + final units = [].vn; + + Future getUnits() async { + isBusy.value = true; + + try { + final result = await _client.runScriptIn(_getUnitsCmd); + final units = result.split('\n'); + final isRootRaw = units.firstOrNull; + isRoot.value = isRootRaw == '0'; + + final userUnits = []; + final systemUnits = []; + for (final unit in units.skip(1)) { + if (unit.startsWith('/etc/systemd/system')) { + systemUnits.add(unit); + } else if (unit.startsWith('~/.config/systemd/user')) { + userUnits.add(unit); + } else if (unit.trim().isNotEmpty) { + Loggers.app.warning('Unknown unit: $unit'); + } + } + + final parsedUserUnits = + await _parseUnitObj(userUnits, SystemdUnitScope.user); + final parsedSystemUnits = + await _parseUnitObj(systemUnits, SystemdUnitScope.system); + this.units.value = [...parsedUserUnits, ...parsedSystemUnits]; + } catch (e, s) { + Loggers.app.warning('Parse systemd', e, s); + } + + isBusy.value = false; + } + + Future> _parseUnitObj( + List unitNames, + SystemdUnitScope scope, + ) async { + final unitNames_ = unitNames + .map((e) => e.trim().split('/').last.split('.').first) + .toList(); + final script = ''' +for unit in ${unitNames_.join(' ')}; do + state=\$(systemctl show --no-pager \$unit) + echo -n "${ShellFunc.seperator}\n\$state" +done +'''; + final result = await _client.runScriptIn(script); + final units = result.split(ShellFunc.seperator); + + final parsedUnits = []; + for (final unit in units) { + final parts = unit.split('\n'); + var name = ''; + var type = ''; + var state = ''; + String? description; + for (final part in parts) { + if (part.startsWith('Id=')) { + final val = _getIniVal(part).split('.'); + name = val.first; + type = val.last; + continue; + } + if (part.startsWith('ActiveState=')) { + state = _getIniVal(part); + continue; + } + if (part.startsWith('Description=')) { + description = _getIniVal(part); + continue; + } + } + + final unitType = SystemdUnitType.fromString(type); + if (unitType == null) { + Loggers.app.warning('Unit type: $type'); + continue; + } + final unitState = SystemdUnitState.fromString(state); + if (unitState == null) { + Loggers.app.warning('Unit state: $state'); + continue; + } + + parsedUnits.add(SystemdUnit( + name: name, + type: unitType, + scope: scope, + state: unitState, + description: description, + )); + } + + parsedUnits.sort((a, b) { + // user units first + if (a.scope != b.scope) { + return a.scope == SystemdUnitScope.user ? -1 : 1; + } + // active units first + if (a.state != b.state) { + return a.state == SystemdUnitState.active ? -1 : 1; + } + return a.name.compareTo(b.name); + }); + return parsedUnits; + } +} + +String _getIniVal(String line) { + return line.split('=').last; +} + +const _getUnitsCmd = ''' +# If root, get system & user units, otherwise get user units +uid=\$(id -u) +echo \$uid + +get_files() { + unit_type=\$1 + base_dir=\$2 + + # If base_dir is not a directory, return + if [ ! -d "\$base_dir" ]; then + return + fi + + find "\$base_dir" -type f -name "*.\$unit_type" -print | sort +} + +get_type_files() { + unit_type=\$1 + + base_dir="" + if [ "\$uid" -eq 0 ]; then + get_files \$unit_type /etc/systemd/system + get_files \$unit_type ~/.config/systemd/user + else + get_files \$unit_type ~/.config/systemd/user + fi +} + +types="service socket mount timer" + +for type in \$types; do + get_type_files \$type +done +'''; diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index 2a68f170..6553d8f2 100644 --- a/lib/data/res/build_data.dart +++ b/lib/data/res/build_data.dart @@ -2,6 +2,6 @@ class BuildData { static const String name = "ServerBox"; - static const int build = 1057; + static const int build = 1058; static const int script = 56; } diff --git a/lib/data/res/provider.dart b/lib/data/res/provider.dart index e4ecb611..8db595e3 100644 --- a/lib/data/res/provider.dart +++ b/lib/data/res/provider.dart @@ -1,11 +1,9 @@ -import 'package:server_box/data/provider/app.dart'; import 'package:server_box/data/provider/private_key.dart'; import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/provider/sftp.dart'; import 'package:server_box/data/provider/snippet.dart'; abstract final class Pros { - static final app = AppProvider(); static final key = PrivateKeyProvider(); static final server = ServerProvider(); static final sftp = SftpProvider(); diff --git a/lib/main.dart b/lib/main.dart index 179e4a5d..9feea17d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,7 +32,6 @@ Future main() async { runApp( MultiProvider( providers: [ - ChangeNotifierProvider(create: (_) => Pros.app), ChangeNotifierProvider(create: (_) => Pros.server), ChangeNotifierProvider(create: (_) => Pros.snippet), ChangeNotifierProvider(create: (_) => Pros.key), @@ -97,7 +96,6 @@ Future _initData() async { Pros.snippet.load(); Pros.key.load(); - await Pros.app.init(); if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta; } @@ -136,6 +134,7 @@ Future _doVersionRelated() async { // How to upgrade the data is inside each own func. if (curVer < newVer) { ServerDetailCards.autoAddNewCards(newVer); + ServerFuncBtn.autoAddNewFuncs(newVer); Stores.setting.lastVer.put(newVer); } } diff --git a/lib/view/page/home/home.dart b/lib/view/page/home/home.dart index b4ec1c99..0c0ea82b 100644 --- a/lib/view/page/home/home.dart +++ b/lib/view/page/home/home.dart @@ -8,6 +8,7 @@ import 'package:server_box/core/extension/build.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/route.dart'; import 'package:server_box/data/model/app/tab.dart'; +import 'package:server_box/data/provider/app.dart'; import 'package:server_box/data/res/build_data.dart'; import 'package:server_box/data/res/github_id.dart'; import 'package:server_box/data/res/misc.dart'; @@ -103,7 +104,7 @@ class _HomePageState extends State @override Widget build(BuildContext context) { super.build(context); - Pros.app.ctx = context; + AppProvider.ctx = context; final appBar = _AppBar( selectIndex: _selectIndex, diff --git a/lib/view/page/systemd.dart b/lib/view/page/systemd.dart new file mode 100644 index 00000000..7b5bd301 --- /dev/null +++ b/lib/view/page/systemd.dart @@ -0,0 +1,165 @@ +import 'package:fl_lib/fl_lib.dart'; +import 'package:flutter/material.dart'; +import 'package:server_box/core/route.dart'; +import 'package:server_box/data/model/server/server_private_info.dart'; +import 'package:server_box/data/model/server/systemd.dart'; +import 'package:server_box/data/provider/systemd.dart'; + +final class SystemdPageArgs { + final ServerPrivateInfo spi; + + const SystemdPageArgs({ + required this.spi, + }); +} + +final class SystemdPage extends StatefulWidget { + final SystemdPageArgs args; + + const SystemdPage({ + super.key, + required this.args, + }); + + static const route = AppRoute( + page: SystemdPage.new, + path: '/systemd', + ); + + @override + State createState() => _SystemdPageState(); +} + +final class _SystemdPageState extends State { + late final _pro = SystemdProvider.init(widget.args.spi); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + title: const Text('Systemd'), + actions: [ + Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits), + ], + ), + body: _buildBody(), + ); + } + + Widget _buildBody() { + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: _pro.isBusy.listenVal( + (isBusy) => AnimatedContainer( + duration: Durations.medium1, + curve: Curves.fastEaseInToSlowEaseOut, + height: isBusy ? 50 : 0, + child: isBusy + ? UIs.centerSizedLoadingSmall.paddingOnly(bottom: 7) + : const SizedBox.shrink(), + ), + ), + ), + _buildUnitList(_pro.units), + ], + ); + } + + Widget _buildUnitList(VNode> units) { + return units.listenVal( + (units) { + if (units.isEmpty) { + return SliverToBoxAdapter( + child: ListTile(title: Text(libL10n.empty)) + .cardx + .paddingSymmetric(horizontal: 13), + ); + } + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final unit = units[index]; + return ListTile( + leading: _buildScopeTag(unit.scope), + title: unit.description != null + ? TipText(unit.name, unit.description!) + : Text(unit.name), + subtitle: Wrap(children: [ + _buildStateTag(unit.state), + _buildTypeTag(unit.type), + ]).paddingOnly(top: 7), + trailing: _buildUnitFuncs(unit), + ).cardx.paddingSymmetric(horizontal: 13); + }, + childCount: units.length, + ), + ); + }, + ); + } + + Widget _buildUnitFuncs(SystemdUnit unit) { + return PopupMenu( + items: unit.availableFuncs.map(_buildUnitFuncBtn).toList(), + onSelected: (val) async { + final cmd = unit.getCmd(func: val, isRoot: _pro.isRoot.value); + final sure = await context.showRoundDialog( + title: libL10n.attention, + child: SimpleMarkdown(data: '```shell\n$cmd\n```'), + actions: [ + CountDownBtn( + seconds: 1, + onTap: () => context.pop(true), + text: libL10n.ok, + afterColor: Colors.red, + ), + ], + ); + if (sure != true) return; + + AppRoutes.ssh(spi: widget.args.spi, initCmd: cmd).go(context); + }, + ); + } + + PopupMenuEntry _buildUnitFuncBtn(SystemdUnitFunc func) { + return PopupMenuItem( + value: func, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Icon(func.icon, size: 19), + const SizedBox(width: 10), + Text(func.name.upperFirst), + ], + ), + ); + } + + Widget _buildScopeTag(SystemdUnitScope scope) { + return _buildTag(scope.name.upperFirst, scope.color, true); + } + + Widget _buildStateTag(SystemdUnitState state) { + return _buildTag(state.name.upperFirst, state.color); + } + + Widget _buildTypeTag(SystemdUnitType type) { + return _buildTag(type.name.upperFirst); + } + + Widget _buildTag(String tag, [Color? color, bool noPad = false]) { + return Container( + decoration: BoxDecoration( + color: color?.withOpacity(0.7) ?? UIs.halfAlpha, + borderRadius: BorderRadius.circular(5), + ), + child: Text( + tag, + style: UIs.text11Grey, + ).paddingSymmetric(horizontal: 5, vertical: 1), + ).paddingOnly(right: noPad ? 0 : 5); + } +} diff --git a/lib/view/widget/server_func_btns.dart b/lib/view/widget/server_func_btns.dart index f9d5bbd5..3835c924 100644 --- a/lib/view/widget/server_func_btns.dart +++ b/lib/view/widget/server_func_btns.dart @@ -8,6 +8,7 @@ import 'package:server_box/data/model/app/menu/server_func.dart'; import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/res/provider.dart'; import 'package:server_box/data/res/store.dart'; +import 'package:server_box/view/page/systemd.dart'; import '../../core/route.dart'; import '../../core/utils/server.dart'; @@ -162,6 +163,12 @@ void _onTapMoreBtns( check: () => _checkClient(context, spi.id), ); break; + case ServerFuncBtn.systemd: + SystemdPage.route.go( + context, + args: SystemdPageArgs(spi: spi), + ); + break; } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 524858ac..a74ca10c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,6 @@ import FlutterMacOS import Foundation -import device_info_plus import dynamic_color import icloud_storage import local_auth_darwin @@ -20,7 +19,6 @@ import wakelock_plus import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) IcloudStoragePlugin.register(with: registry.registrar(forPlugin: "IcloudStoragePlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 8c2cde22..e5ac8461 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -471,7 +471,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_TEAM = BA88US33G6; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Server Box"; @@ -481,7 +481,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "Server Box"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -608,7 +608,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_TEAM = BA88US33G6; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Server Box"; @@ -618,7 +618,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "Server Box"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -638,7 +638,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1057; + CURRENT_PROJECT_VERSION = 1058; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BA88US33G6; INFOPLIST_FILE = Runner/Info.plist; @@ -649,7 +649,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.0.1057; + MARKETING_VERSION = 1.0.1058; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "Server Box"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/pubspec.lock b/pubspec.lock index f96a17d2..49d2c4df 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -273,22 +273,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" - device_info_plus: - dependency: "direct main" - description: - name: device_info_plus - sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 - url: "https://pub.dev" - source: hosted - version: "10.1.2" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" - url: "https://pub.dev" - source: hosted - version: "7.0.1" dio: dependency: "direct main" description: @@ -1559,14 +1543,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.5.3" - win32_registry: - dependency: transitive - description: - name: win32_registry - sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" - url: "https://pub.dev" - source: hosted - version: "1.1.4" window_manager: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5be45b5e..ee8c6413 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: server_box description: server status & toolbox app. publish_to: 'none' -version: 1.0.1057+1057 +version: 1.0.1058+1058 environment: sdk: ">=3.0.0" @@ -28,7 +28,6 @@ dependencies: wakelock_plus: ^1.2.4 wake_on_lan: ^4.1.1+3 flutter_adaptive_scaffold: ^0.1.10+2 - device_info_plus: ^10.1.0 extended_image: ^8.2.1 dartssh2: git: