feat: systemd management (#532)

This commit is contained in:
lollipopkit🏳️‍⚧️
2024-08-14 14:29:03 +08:00
committed by GitHub
parent 46d5840276
commit 41e3fcb23a
18 changed files with 557 additions and 93 deletions

View File

@@ -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;

View File

@@ -13,6 +13,7 @@ typedef _OnStdin = void Function(SSHSession session);
typedef PwdRequestFunc = Future<String?> Function(String? user);
extension SSHClientX on SSHClient {
/// TODO: delete [exec]
Future<SSHSession> exec(
String cmd, {
_OnStdout? onStderr,
@@ -135,4 +136,36 @@ extension SSHClientX on SSHClient {
return result.takeBytes();
}
Future<String> 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<void>();
final stderrDone = Completer<void>();
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;
}
}

View File

@@ -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<List<String>?> 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,
);

View File

@@ -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',
};
}

View File

@@ -25,6 +25,8 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
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<ServerFuncBtn> {
case ServerFuncBtn.iperf:
writer.writeByte(6);
break;
case ServerFuncBtn.systemd:
writer.writeByte(8);
break;
}
}

View File

@@ -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<SystemdUnitFunc> get availableFuncs {
final funcs = <SystemdUnitFunc>{};
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();
}
}

View File

@@ -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<void> init() async {
await _initIsWearOS();
}
Future<void> _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;
}

View File

@@ -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 = <SystemdUnit>[].vn;
Future<void> 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 = <String>[];
final systemUnits = <String>[];
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<List<SystemdUnit>> _parseUnitObj(
List<String> 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 = <SystemdUnit>[];
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
''';

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -32,7 +32,6 @@ Future<void> 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<void> _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<void> _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);
}
}

View File

@@ -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<HomePage>
@override
Widget build(BuildContext context) {
super.build(context);
Pros.app.ctx = context;
AppProvider.ctx = context;
final appBar = _AppBar(
selectIndex: _selectIndex,

165
lib/view/page/systemd.dart Normal file
View File

@@ -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<void, SystemdPageArgs>(
page: SystemdPage.new,
path: '/systemd',
);
@override
State<SystemdPage> createState() => _SystemdPageState();
}
final class _SystemdPageState extends State<SystemdPage> {
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: <Widget>[
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<List<SystemdUnit>> 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<SystemdUnitFunc>(
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);
}
}

View File

@@ -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;
}
}

View File

@@ -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"))

View File

@@ -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 = "";

View File

@@ -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:

View File

@@ -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: