From ed8a1d18b9ac841e1652f97bb6abd02e9a62606b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Wed, 13 Aug 2025 22:16:55 +0800 Subject: [PATCH] opt.: systemd page (#851) --- .github/workflows/claude-code-review.yml | 6 +- .github/workflows/claude.yml | 10 +-- lib/core/extension/ssh_client.dart | 5 +- lib/data/model/server/systemd.dart | 13 ++++ lib/data/provider/server.dart | 2 +- lib/data/provider/systemd.dart | 91 +++++++++++++----------- lib/view/page/server/tab/content.dart | 5 +- lib/view/page/systemd.dart | 77 +++++++++++++------- pubspec.lock | 4 +- pubspec.yaml | 2 +- 10 files changed, 131 insertions(+), 84 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index a12225aa..45b6dc25 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -12,11 +12,7 @@ on: jobs: claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + if: github.event.pull_request.user.login == 'lollipopkit' runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index bc773072..bceb35bb 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -13,10 +13,12 @@ on: jobs: claude: if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + github.actor == 'lollipopkit' && ( + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + ) runs-on: ubuntu-latest permissions: contents: read diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index 4cff3eca..e126b234 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -132,8 +132,9 @@ extension SSHClientX on SSHClient { if (data.contains('[sudo] password for ')) { isRequestingPwd = true; final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1); - if (context == null) return; - final pwd = context.mounted ? await context.showPwdDialog(title: user, id: id) : null; + final ctx = context ?? WidgetsBinding.instance.focusManager.primaryFocus?.context; + if (ctx == null) return; + final pwd = ctx.mounted ? await ctx.showPwdDialog(title: user, id: id) : null; if (pwd == null || pwd.isEmpty) { session.stdin.close(); } else { diff --git a/lib/data/model/server/systemd.dart b/lib/data/model/server/systemd.dart index c6a3a15c..d68e2d76 100644 --- a/lib/data/model/server/systemd.dart +++ b/lib/data/model/server/systemd.dart @@ -1,5 +1,6 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:server_box/core/extension/context/locale.dart'; enum SystemdUnitFunc { start, @@ -49,6 +50,18 @@ enum SystemdUnitScope { } } +enum SystemdScopeFilter { + all, + system, + user; + + String get displayName => switch (this) { + all => libL10n.all, + system => l10n.system, + user => libL10n.user, + }; +} + enum SystemdUnitState { active, inactive, diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index cec54ef5..371f8225 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -440,7 +440,7 @@ class ServerProvider extends Provider { try { raw = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)).string; - dprint('Get status from ${spi.name}:\n$raw'); + //dprint('Get status from ${spi.name}:\n$raw'); segments = raw?.split(ScriptConstants.separator).map((e) => e.trim()).toList(); if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) { if (Stores.setting.keepStatusWhenErr.fetch()) { diff --git a/lib/data/provider/systemd.dart b/lib/data/provider/systemd.dart index 7f0ad475..da5f435a 100644 --- a/lib/data/provider/systemd.dart +++ b/lib/data/provider/systemd.dart @@ -8,20 +8,31 @@ import 'package:server_box/data/provider/server.dart'; final class SystemdProvider { late final VNode _si; - late final bool _isRoot; SystemdProvider.init(Spi spi) { - _isRoot = spi.isRoot; _si = ServerProvider.pick(spi: spi)!; getUnits(); } final isBusy = false.vn; final units = [].vn; + final scopeFilter = SystemdScopeFilter.all.vn; void dispose() { isBusy.dispose(); units.dispose(); + scopeFilter.dispose(); + } + + List get filteredUnits { + switch (scopeFilter.value) { + case SystemdScopeFilter.all: + return units.value; + case SystemdScopeFilter.system: + return units.value.where((unit) => unit.scope == SystemdUnitScope.system).toList(); + case SystemdScopeFilter.user: + return units.value.where((unit) => unit.scope == SystemdUnitScope.user).toList(); + } } Future getUnits() async { @@ -35,43 +46,33 @@ final class SystemdProvider { final userUnits = []; final systemUnits = []; for (final unit in units) { - if (unit.startsWith('/etc/systemd/system')) { + final maybeSystem = unit.contains('/systemd/system'); + final maybeUser = unit.contains('/.config/systemd/user'); + if (maybeSystem && !maybeUser) { systemUnits.add(unit); - } else if (unit.startsWith('~/.config/systemd/user')) { + } else { 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, - ); + 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); + dprint('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(); + 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 "${ScriptConstants.separator}\n\$state" + echo "\$state" + echo -n "\n${ScriptConstants.separator}\n" done '''; final client = _si.value.client!; @@ -79,21 +80,30 @@ done final units = result.split(ScriptConstants.separator); final parsedUnits = []; - for (final unit in units) { - final parts = unit.split('\n'); + for (final unit in units.where((e) => e.trim().isNotEmpty)) { + final parts = unit + .split('\n') + .where((e) => e.trim().isNotEmpty) + .toList(); + if (parts.isEmpty) continue; 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; + final val = _getIniVal(part); + if (val == null) continue; + // Id=sshd.service + final vals = val.split('.'); + name = vals.first; + type = vals.last; continue; } if (part.startsWith('ActiveState=')) { - state = _getIniVal(part); + final val = _getIniVal(part); + if (val == null) continue; + state = val; continue; } if (part.startsWith('Description=')) { @@ -104,23 +114,17 @@ done final unitType = SystemdUnitType.fromString(type); if (unitType == null) { - Loggers.app.warning('Unit type: $type'); + dprint('Unit type: $type'); continue; } final unitState = SystemdUnitState.fromString(state); if (unitState == null) { - Loggers.app.warning('Unit state: $state'); + dprint('Unit state: $state'); continue; } parsedUnits.add( - SystemdUnit( - name: name, - type: unitType, - scope: scope, - state: unitState, - description: description, - ), + SystemdUnit(name: name, type: unitType, scope: scope, state: unitState, description: description), ); } @@ -150,13 +154,16 @@ done for type in \$types; do get_files \$type /etc/systemd/system - get_files \$type /lib/systemd/system - get_files \$type /usr/lib/systemd/system + # Parsing these paths can lead to SSH transport closed errors + # get_files \$type /lib/systemd/system + # get_files \$type /usr/lib/systemd/system get_files \$type ~/.config/systemd/user done | sort '''; } -String _getIniVal(String line) { - return line.split('=').last; +String? _getIniVal(String line) { + final idx = line.indexOf('='); + if (idx < 0) return null; + return line.substring(idx + 1).trim(); } diff --git a/lib/view/page/server/tab/content.dart b/lib/view/page/server/tab/content.dart index 83de6470..78b610e9 100644 --- a/lib/view/page/server/tab/content.dart +++ b/lib/view/page/server/tab/content.dart @@ -20,7 +20,10 @@ extension on _ServerPageState { Widget _buildTopRightWidget(Server s) { final (child, onTap) = switch (s.conn) { ServerConn.connecting || ServerConn.loading || ServerConn.connected => ( - SizedLoading(_ServerPageState._kCardHeightMin, strokeWidth: 3, padding: 5), + SizedBox.square( + dimension: _ServerPageState._kCardHeightMin, + child: SizedLoading(_ServerPageState._kCardHeightMin, strokeWidth: 3, padding: 3), + ), null, ), ServerConn.failed => ( diff --git a/lib/view/page/systemd.dart b/lib/view/page/systemd.dart index 2b988a1d..580c9fa9 100644 --- a/lib/view/page/systemd.dart +++ b/lib/view/page/systemd.dart @@ -41,38 +41,63 @@ final class _SystemdPageState extends State { return CustomScrollView( slivers: [ SliverToBoxAdapter( - child: _pro.isBusy.listenVal( - (isBusy) => AnimatedContainer( - duration: Durations.medium1, - curve: Curves.fastEaseInToSlowEaseOut, - height: isBusy ? SizedLoading.medium.size : 0, - child: isBusy ? SizedLoading.medium : const SizedBox.shrink(), - ), + child: Column( + children: [ + _buildScopeFilterChips(), + _pro.isBusy.listenVal( + (isBusy) => AnimatedContainer( + duration: Durations.medium1, + curve: Curves.fastEaseInToSlowEaseOut, + height: isBusy ? SizedLoading.medium.size : 0, + width: isBusy ? SizedLoading.medium.size : 0, + child: isBusy ? SizedLoading.medium : const SizedBox.shrink(), + ), + ), + ], ), ), - _buildUnitList(_pro.units), + _buildUnitList(), ], ); } - Widget _buildUnitList(VNode> units) { - return units.listenVal((units) { - if (units.isEmpty) { - return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).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 _buildScopeFilterChips() { + return _pro.scopeFilter.listenVal((currentFilter) { + return Wrap( + spacing: 8, + children: SystemdScopeFilter.values.map((filter) { + final isSelected = filter == currentFilter; + return FilterChip( + selected: isSelected, + label: Text(filter.displayName), + onSelected: (_) => _pro.scopeFilter.value = filter, + ); + }).toList(), + ).paddingSymmetric(horizontal: 13, vertical: 8); + }); + } + + Widget _buildUnitList() { + return _pro.units.listenVal((allUnits) { + return _pro.scopeFilter.listenVal((filter) { + final filteredUnits = _pro.filteredUnits; + if (filteredUnits.isEmpty) { + return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13)); + } + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final unit = filteredUnits[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: filteredUnits.length), + ); + }); }); } diff --git a/pubspec.lock b/pubspec.lock index 670e467d..1dc80906 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -497,8 +497,8 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.0.327" - resolved-ref: "5075a679b814b10742f967066858ba4df92ea4ae" + ref: "v1.0.329" + resolved-ref: "1620fb055986dfcf7587d7a16eb994a39c0d5772" url: "https://github.com/lppcg/fl_lib" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index b0e410dd..00cceb2a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: fl_lib: git: url: https://github.com/lppcg/fl_lib - ref: v1.0.327 + ref: v1.0.329 dependency_overrides: # webdav_client_plus: