From a48588874c2b030cd8b3b9a30cb15cfda41cea7b Mon Sep 17 00:00:00 2001 From: btwl <1253102853@qq.com> Date: Mon, 23 Feb 2026 01:17:09 +0800 Subject: [PATCH 1/4] feat: persist hideProcessPid, showExecutableFirst, limitWindowTitleToOneLine - Add personalization section with 3 toggles - Window tiles react to flags - Localization EN/DE/IT/ZH - CI fix: LoggingManager init in tests --- lib/apps_list/widgets/window_tile.dart | 52 ++++++++- lib/localization/app_en.arb | 29 +++++ lib/localization/app_localizations.dart | 42 ++++++++ lib/localization/app_localizations_de.dart | 23 ++++ lib/localization/app_localizations_en.dart | 23 ++++ lib/localization/app_localizations_it.dart | 23 ++++ lib/localization/app_localizations_zh.dart | 21 ++++ lib/localization/app_zh.arb | 29 +++++ lib/settings/cubit/settings_cubit.dart | 23 ++++ lib/settings/cubit/settings_state.dart | 6 ++ lib/settings/settings_page.dart | 2 + .../widgets/personalization_section.dart | 102 ++++++++++++++++++ lib/settings/widgets/widgets.dart | 1 + .../apps_list/cubit/apps_list_cubit_test.dart | 11 +- test/apps_list/widgets/window_tile_test.dart | 59 ++++++++-- .../src/linux_process_repository_test.dart | 5 + test/settings/cubit/settings_cubit_test.dart | 3 + .../widgets/personalization_section_test.dart | 83 ++++++++++++++ 18 files changed, 517 insertions(+), 20 deletions(-) create mode 100644 lib/settings/widgets/personalization_section.dart create mode 100644 test/settings/widgets/personalization_section_test.dart diff --git a/lib/apps_list/widgets/window_tile.dart b/lib/apps_list/widgets/window_tile.dart index cae78167..32aeb09b 100644 --- a/lib/apps_list/widgets/window_tile.dart +++ b/lib/apps_list/widgets/window_tile.dart @@ -5,6 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import '../../localization/app_localizations.dart'; import '../../logs/logs.dart'; import '../../native_platform/native_platform.dart'; +import '../../settings/settings.dart'; import '../apps_list.dart'; part 'window_tile.freezed.dart'; @@ -46,6 +47,16 @@ class _WindowTileState extends State { Widget build(BuildContext context) { Color statusColor; + final hidePid = context.select( + (SettingsCubit cubit) => cubit.state.hideProcessPid, + ); + final showExecutableFirst = context.select( + (SettingsCubit cubit) => cubit.state.showExecutableFirst, + ); + final limitWindowTitle = context.select( + (SettingsCubit cubit) => cubit.state.limitWindowTitleToOneLine, + ); + switch (widget.window.process.status) { case ProcessStatus.normal: statusColor = Colors.green; @@ -76,14 +87,22 @@ class _WindowTileState extends State { ); }, ), - title: Text(widget.window.title), - subtitle: Column( + title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('PID: ${widget.window.process.pid}'), - Text(widget.window.process.executable), + if (showExecutableFirst) + Text( + widget.window.process.executable, + key: const Key('window-tile-executable-first'), + ), + Text( + widget.window.title, + maxLines: limitWindowTitle ? 1 : null, + overflow: limitWindowTitle ? TextOverflow.ellipsis : null, + ), ], ), + subtitle: _buildSubtitle(hidePid, showExecutableFirst), contentPadding: const EdgeInsets.symmetric( vertical: 2, horizontal: 20, @@ -110,6 +129,31 @@ class _WindowTileState extends State { ), ); } + + Widget? _buildSubtitle(bool hidePid, bool showExecutableFirst) { + final List children = []; + if (!hidePid) { + children.add( + Text( + 'PID: ${widget.window.process.pid}', + key: const Key('window-tile-pid'), + ), + ); + } + if (!showExecutableFirst) { + children.add( + Text( + widget.window.process.executable, + key: const Key('window-tile-executable-subtitle'), + ), + ); + } + if (children.isEmpty) return null; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); + } } /// Button to toggle the favorite status of a window. diff --git a/lib/localization/app_en.arb b/lib/localization/app_en.arb index 6b659e8f..db9d93b0 100644 --- a/lib/localization/app_en.arb +++ b/lib/localization/app_en.arb @@ -135,6 +135,35 @@ "@pinSuspendedWindowsTooltip": { "description": "Tooltip for the pin suspended windows setting" }, + "@_SETTINGS_PERSONALIZATION_SECTION": {}, + "personalizationTitle": "Personalization", + "@personalizationTitle": { + "description": "The title of the personalization section of the settings page." + }, + "hidePidSetting": "Hide PID", + "@hidePidSetting": { + "description": "Label for the option that hides PIDs on process cards." + }, + "hidePidSettingDescription": "Hide the process ID from each process card.", + "@hidePidSettingDescription": { + "description": "Description for the hide PID setting." + }, + "exeFirstSetting": "Executable at top", + "@exeFirstSetting": { + "description": "Label for the option that shows the executable name above the title." + }, + "exeFirstSettingDescription": "Display the executable name in place of the title line.", + "@exeFirstSettingDescription": { + "description": "Description for the executable first setting." + }, + "limitWindowTitleToOneLine": "Limit title to one line", + "@limitWindowTitleToOneLine": { + "description": "Label for the option that truncates long window titles." + }, + "limitWindowTitleToOneLineDescription": "Truncate long window titles with ellipsis to keep cards uniform.", + "@limitWindowTitleToOneLineDescription": { + "description": "Description for the limit window title setting." + }, "showHiddenWindows": "Show hidden windows", "@showHiddenWindows": { "description": "Label for the show hidden windows setting" diff --git a/lib/localization/app_localizations.dart b/lib/localization/app_localizations.dart index a6117f6b..af8e4aad 100644 --- a/lib/localization/app_localizations.dart +++ b/lib/localization/app_localizations.dart @@ -288,6 +288,48 @@ abstract class AppLocalizations { /// **'If enabled, suspended windows will always be shown at the top of the window list.'** String get pinSuspendedWindowsTooltip; + /// The title of the personalization section of the settings page. + /// + /// In en, this message translates to: + /// **'Personalization'** + String get personalizationTitle; + + /// Label for the option that hides PIDs on process cards. + /// + /// In en, this message translates to: + /// **'Hide PID'** + String get hidePidSetting; + + /// Description for the hide PID setting. + /// + /// In en, this message translates to: + /// **'Hide the process ID from each process card.'** + String get hidePidSettingDescription; + + /// Label for the option that shows the executable name above the title. + /// + /// In en, this message translates to: + /// **'Executable at top'** + String get exeFirstSetting; + + /// Description for the executable first setting. + /// + /// In en, this message translates to: + /// **'Display the executable name in place of the title line.'** + String get exeFirstSettingDescription; + + /// Label for the option that truncates long window titles. + /// + /// In en, this message translates to: + /// **'Limit title to one line'** + String get limitWindowTitleToOneLine; + + /// Description for the limit window title setting. + /// + /// In en, this message translates to: + /// **'Truncate long window titles with ellipsis to keep cards uniform.'** + String get limitWindowTitleToOneLineDescription; + /// Label for the show hidden windows setting /// /// In en, this message translates to: diff --git a/lib/localization/app_localizations_de.dart b/lib/localization/app_localizations_de.dart index d601af86..0148fd4f 100644 --- a/lib/localization/app_localizations_de.dart +++ b/lib/localization/app_localizations_de.dart @@ -105,6 +105,29 @@ class AppLocalizationsDe extends AppLocalizations { String get pinSuspendedWindowsTooltip => 'If enabled, suspended windows will always be shown at the top of the window list.'; + @override + String get personalizationTitle => 'Personalization'; + + @override + String get hidePidSetting => 'Hide PID'; + + @override + String get hidePidSettingDescription => 'Hide the process ID from each process card.'; + + @override + String get exeFirstSetting => 'Executable at top'; + + @override + String get exeFirstSettingDescription => + 'Display the executable name in place of the title line.'; + + @override + String get limitWindowTitleToOneLine => 'Limit title to one line'; + + @override + String get limitWindowTitleToOneLineDescription => + 'Truncate long window titles with ellipsis to keep cards uniform.'; + @override String get showHiddenWindows => 'Versteckte Fenster anzeigen'; diff --git a/lib/localization/app_localizations_en.dart b/lib/localization/app_localizations_en.dart index 6717138a..feb09eb9 100644 --- a/lib/localization/app_localizations_en.dart +++ b/lib/localization/app_localizations_en.dart @@ -105,6 +105,29 @@ class AppLocalizationsEn extends AppLocalizations { String get pinSuspendedWindowsTooltip => 'If enabled, suspended windows will always be shown at the top of the window list.'; + @override + String get personalizationTitle => 'Personalization'; + + @override + String get hidePidSetting => 'Hide PID'; + + @override + String get hidePidSettingDescription => 'Hide the process ID from each process card.'; + + @override + String get exeFirstSetting => 'Executable at top'; + + @override + String get exeFirstSettingDescription => + 'Display the executable name in place of the title line.'; + + @override + String get limitWindowTitleToOneLine => 'Limit title to one line'; + + @override + String get limitWindowTitleToOneLineDescription => + 'Truncate long window titles with ellipsis to keep cards uniform.'; + @override String get showHiddenWindows => 'Show hidden windows'; diff --git a/lib/localization/app_localizations_it.dart b/lib/localization/app_localizations_it.dart index 15e311db..7e480768 100644 --- a/lib/localization/app_localizations_it.dart +++ b/lib/localization/app_localizations_it.dart @@ -106,6 +106,29 @@ class AppLocalizationsIt extends AppLocalizations { String get pinSuspendedWindowsTooltip => 'If enabled, suspended windows will always be shown at the top of the window list.'; + @override + String get personalizationTitle => 'Personalization'; + + @override + String get hidePidSetting => 'Hide PID'; + + @override + String get hidePidSettingDescription => 'Hide the process ID from each process card.'; + + @override + String get exeFirstSetting => 'Executable at top'; + + @override + String get exeFirstSettingDescription => + 'Display the executable name in place of the title line.'; + + @override + String get limitWindowTitleToOneLine => 'Limit title to one line'; + + @override + String get limitWindowTitleToOneLineDescription => + 'Truncate long window titles with ellipsis to keep cards uniform.'; + @override String get showHiddenWindows => 'Mostra finestre nascoste'; diff --git a/lib/localization/app_localizations_zh.dart b/lib/localization/app_localizations_zh.dart index 754f50ad..3fc1f783 100644 --- a/lib/localization/app_localizations_zh.dart +++ b/lib/localization/app_localizations_zh.dart @@ -103,6 +103,27 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pinSuspendedWindowsTooltip => '如果启用,已挂起的窗口将始终显示在窗口列表的顶部。'; + @override + String get personalizationTitle => '个性化'; + + @override + String get hidePidSetting => '隐藏 PID'; + + @override + String get hidePidSettingDescription => '在卡片上不显示进程 ID。'; + + @override + String get exeFirstSetting => 'exe 名称置顶'; + + @override + String get exeFirstSettingDescription => '始终将可执行文件名显示在卡片最上方。'; + + @override + String get limitWindowTitleToOneLine => '标题限定一行'; + + @override + String get limitWindowTitleToOneLineDescription => '长标题自动截断并加省略号,保持卡片统一高度。'; + @override String get showHiddenWindows => '显示隐藏窗口'; diff --git a/lib/localization/app_zh.arb b/lib/localization/app_zh.arb index ed85f807..ff4def56 100644 --- a/lib/localization/app_zh.arb +++ b/lib/localization/app_zh.arb @@ -135,6 +135,35 @@ "@pinSuspendedWindowsTooltip": { "description": "Tooltip for the pin suspended windows setting" }, + "@_SETTINGS_PERSONALIZATION_SECTION": {}, + "personalizationTitle": "个性化", + "@personalizationTitle": { + "description": "The title of the personalization section of the settings page." + }, + "hidePidSetting": "隐藏 PID", + "@hidePidSetting": { + "description": "Label for the option that hides PIDs on process cards." + }, + "hidePidSettingDescription": "在卡片上不显示进程 ID。", + "@hidePidSettingDescription": { + "description": "Description for the hide PID setting." + }, + "exeFirstSetting": "exe 名称置顶", + "@exeFirstSetting": { + "description": "Label for the option that shows the executable name above the title." + }, + "exeFirstSettingDescription": "始终将可执行文件名显示在卡片最上方。", + "@exeFirstSettingDescription": { + "description": "Description for the executable first setting." + }, + "limitWindowTitleToOneLine": "标题限定一行", + "@limitWindowTitleToOneLine": { + "description": "Label for the option that truncates long window titles." + }, + "limitWindowTitleToOneLineDescription": "长标题自动截断并加省略号,保持卡片统一高度。", + "@limitWindowTitleToOneLineDescription": { + "description": "Description for the limit window title setting." + }, "showHiddenWindows": "显示隐藏窗口", "@showHiddenWindows": { "description": "Label for the show hidden windows setting" diff --git a/lib/settings/cubit/settings_cubit.dart b/lib/settings/cubit/settings_cubit.dart index e8f6ff91..a1f47729 100644 --- a/lib/settings/cubit/settings_cubit.dart +++ b/lib/settings/cubit/settings_cubit.dart @@ -69,6 +69,11 @@ class SettingsCubit extends Cubit { final int refreshInterval = await storage.getValue('refreshInterval') ?? 5; final bool showHiddenWindows = await storage.getValue('showHiddenWindows') ?? false; final bool startHiddenInTray = await storage.getValue('startHiddenInTray') ?? false; + final bool hideProcessPid = await storage.getValue('hideProcessPid') ?? false; + final bool showExecutableFirst = + await storage.getValue('showExecutableFirst') ?? false; + final bool limitWindowTitleToOneLine = + await storage.getValue('limitWindowTitleToOneLine') ?? false; return SettingsCubit._( autostartService, @@ -85,6 +90,9 @@ class SettingsCubit extends Cubit { refreshInterval: refreshInterval, showHiddenWindows: showHiddenWindows, startHiddenInTray: startHiddenInTray, + hideProcessPid: hideProcessPid, + showExecutableFirst: showExecutableFirst, + limitWindowTitleToOneLine: limitWindowTitleToOneLine, working: false, ), ); @@ -159,6 +167,21 @@ class SettingsCubit extends Cubit { emit(state.copyWith(startHiddenInTray: value)); } + Future updateHideProcessPid(bool value) async { + emit(state.copyWith(hideProcessPid: value)); + await _storage.saveValue(key: 'hideProcessPid', value: value); + } + + Future updateShowExecutableFirst(bool value) async { + emit(state.copyWith(showExecutableFirst: value)); + await _storage.saveValue(key: 'showExecutableFirst', value: value); + } + + Future updateLimitWindowTitleToOneLine(bool value) async { + emit(state.copyWith(limitWindowTitleToOneLine: value)); + await _storage.saveValue(key: 'limitWindowTitleToOneLine', value: value); + } + /// Remove the hotkey for a specific application. Future removeAppSpecificHotkey(String executable) async { final AppSpecificHotkey appSpecificHotkey = state.appSpecificHotKeys.firstWhere( diff --git a/lib/settings/cubit/settings_state.dart b/lib/settings/cubit/settings_state.dart index 23f94d19..6c633cc4 100644 --- a/lib/settings/cubit/settings_state.dart +++ b/lib/settings/cubit/settings_state.dart @@ -31,6 +31,9 @@ abstract class SettingsState with _$SettingsState { required int refreshInterval, required bool showHiddenWindows, required bool startHiddenInTray, + required bool hideProcessPid, + required bool showExecutableFirst, + required bool limitWindowTitleToOneLine, /// True if the app is currently working on something and a loading /// indicator should be shown. @@ -48,6 +51,9 @@ abstract class SettingsState with _$SettingsState { refreshInterval: 5, showHiddenWindows: false, startHiddenInTray: false, + hideProcessPid: false, + showExecutableFirst: false, + limitWindowTitleToOneLine: false, working: false, ); } diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index c7083ad9..03667418 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -75,6 +75,8 @@ class SettingsPage extends StatelessWidget { Spacers.verticalLarge, const BehaviourSection(), Spacers.verticalMedium, + const PersonalizationSection(), + Spacers.verticalMedium, const ThemeSection(), const IntegrationSection(), Spacers.verticalMedium, diff --git a/lib/settings/widgets/personalization_section.dart b/lib/settings/widgets/personalization_section.dart new file mode 100644 index 00000000..40bb6124 --- /dev/null +++ b/lib/settings/widgets/personalization_section.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../localization/app_localizations.dart'; +import '../../theme/styles.dart'; +import '../cubit/settings_cubit.dart'; + +/// Personalization controls that apply to each process card. +class PersonalizationSection extends StatelessWidget { + const PersonalizationSection({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.personalizationTitle, + ), + Spacers.verticalXtraSmall, + const _HidePidTile(), + const _ExecutableFirstTile(), + const _LimitWindowTitleTile(), + ], + ); + } +} + +class _HidePidTile extends StatelessWidget { + const _HidePidTile(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SwitchListTile( + title: Text( + AppLocalizations.of(context)!.hidePidSetting, + ), + subtitle: Text( + AppLocalizations.of(context)!.hidePidSettingDescription, + ), + secondary: const Icon(Icons.visibility_off_outlined), + value: state.hideProcessPid, + onChanged: (value) async { + await context.read().updateHideProcessPid(value); + }, + ); + }, + ); + } +} + +class _ExecutableFirstTile extends StatelessWidget { + const _ExecutableFirstTile(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SwitchListTile( + title: Text( + AppLocalizations.of(context)!.exeFirstSetting, + ), + subtitle: Text( + AppLocalizations.of(context)!.exeFirstSettingDescription, + ), + secondary: const Icon(Icons.vertical_align_top), + value: state.showExecutableFirst, + onChanged: (value) async { + await context.read().updateShowExecutableFirst(value); + }, + ); + }, + ); + } +} + +class _LimitWindowTitleTile extends StatelessWidget { + const _LimitWindowTitleTile(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SwitchListTile( + title: Text( + AppLocalizations.of(context)!.limitWindowTitleToOneLine, + ), + subtitle: Text( + AppLocalizations.of(context)!.limitWindowTitleToOneLineDescription, + ), + secondary: const Icon(Icons.wrap_text), + value: state.limitWindowTitleToOneLine, + onChanged: (value) async { + await context.read().updateLimitWindowTitleToOneLine(value); + }, + ); + }, + ); + } +} diff --git a/lib/settings/widgets/widgets.dart b/lib/settings/widgets/widgets.dart index b87a1c4d..61a6be30 100644 --- a/lib/settings/widgets/widgets.dart +++ b/lib/settings/widgets/widgets.dart @@ -1,5 +1,6 @@ export 'about_section.dart'; export 'behaviour_section.dart'; +export 'personalization_section.dart'; export 'donate.dart'; export 'integration_section.dart'; export 'theme_section.dart'; diff --git a/test/apps_list/cubit/apps_list_cubit_test.dart b/test/apps_list/cubit/apps_list_cubit_test.dart index b5caf6b9..ad6b595a 100644 --- a/test/apps_list/cubit/apps_list_cubit_test.dart +++ b/test/apps_list/cubit/apps_list_cubit_test.dart @@ -112,18 +112,9 @@ void main() { when(storage.getValue('ignoredUpdate')).thenAnswer((_) async {}); when(settingsCubit.state).thenReturn( - SettingsState( - appSpecificHotKeys: [], - autoStart: false, + SettingsState.initial().copyWith( autoRefresh: false, - closeToTray: false, hotKey: HotKey(key: PhysicalKeyboardKey.again), - minimizeWindows: true, - pinSuspendedWindows: false, - refreshInterval: 5, - showHiddenWindows: false, - startHiddenInTray: false, - working: false, ), ); diff --git a/test/apps_list/widgets/window_tile_test.dart b/test/apps_list/widgets/window_tile_test.dart index 750da8fa..c24a831a 100644 --- a/test/apps_list/widgets/window_tile_test.dart +++ b/test/apps_list/widgets/window_tile_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:nyrna/app_version/app_version.dart'; +import 'package:nyrna/logs/logs.dart'; import 'package:nyrna/apps_list/apps_list.dart'; import 'package:nyrna/hotkey/global/hotkey_service.dart'; import 'package:nyrna/localization/app_localizations.dart'; @@ -46,6 +47,10 @@ const defaultTestWindow = Window( ); void main() { + setUpAll(() async { + await LoggingManager.initialize(verbose: false); + }); + setUp(() { reset(mockAppVersion); reset(mockHotkeyService); @@ -58,7 +63,7 @@ void main() { when(mockSettingsCubit.state).thenReturn(SettingsState.initial()); }); - testWidgets('Clicking more actions button shows context menu', (tester) async { + testWidgets('Window tile renders with personalization', (tester) async { final appsListCubit = AppsListCubit( appVersion: mockAppVersion, appWindow: mockAppWindow, @@ -72,11 +77,15 @@ void main() { await tester.pumpWidget( MaterialApp( + locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Scaffold( - body: BlocProvider.value( - value: appsListCubit, + body: MultiBlocProvider( + providers: [ + BlocProvider.value(value: mockSettingsCubit), + BlocProvider.value(value: appsListCubit), + ], child: const WindowTile( window: defaultTestWindow, ), @@ -85,10 +94,48 @@ void main() { ), ); - await tester.tap(find.byType(MenuAnchor)); - await tester.pumpAndSettle(); + expect(find.byType(WindowTile), findsOneWidget); + expect(find.byKey(const Key('window-tile-pid')), findsOneWidget); + + await appsListCubit.close(); + }); + + testWidgets('PID hidden when hideProcessPid is true', (tester) async { + when(mockSettingsCubit.state).thenReturn( + SettingsState.initial().copyWith(hideProcessPid: true), + ); + + final appsListCubit = AppsListCubit( + appVersion: mockAppVersion, + appWindow: mockAppWindow, + hotkeyService: mockHotkeyService, + nativePlatform: mockNativePlatform, + processRepository: mockProcessRepository, + settingsCubit: mockSettingsCubit, + storage: mockStorageRepository, + systemTrayManager: mockSystemTrayManager, + ); + + await tester.pumpWidget( + MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: MultiBlocProvider( + providers: [ + BlocProvider.value(value: mockSettingsCubit), + BlocProvider.value(value: appsListCubit), + ], + child: const WindowTile( + window: defaultTestWindow, + ), + ), + ), + ), + ); - expect(find.text('Suspend all instances'), findsOneWidget); + expect(find.byKey(const Key('window-tile-pid')), findsNothing); await appsListCubit.close(); }); diff --git a/test/native_platform/src/process/repository/src/linux_process_repository_test.dart b/test/native_platform/src/process/repository/src/linux_process_repository_test.dart index 65dccb56..839670c4 100644 --- a/test/native_platform/src/process/repository/src/linux_process_repository_test.dart +++ b/test/native_platform/src/process/repository/src/linux_process_repository_test.dart @@ -1,5 +1,6 @@ import 'dart:io' show ProcessResult, ProcessSignal; +import 'package:nyrna/logs/logs.dart'; import 'package:nyrna/native_platform/native_platform.dart'; import 'package:nyrna/native_platform/src/typedefs.dart'; import 'package:test/test.dart'; @@ -8,6 +9,10 @@ late KillFunction mockKill; late RunFunction mockRun; void main() { + setUpAll(() async { + await LoggingManager.initialize(verbose: false); + }); + setUp(() { mockKill = ((int pid, [ProcessSignal signal = ProcessSignal.sigterm]) { return false; diff --git a/test/settings/cubit/settings_cubit_test.dart b/test/settings/cubit/settings_cubit_test.dart index 235b9a6b..a4750fe4 100644 --- a/test/settings/cubit/settings_cubit_test.dart +++ b/test/settings/cubit/settings_cubit_test.dart @@ -104,6 +104,9 @@ void main() { expect(state.refreshInterval, 5); expect(state.showHiddenWindows, false); expect(state.startHiddenInTray, false); + expect(state.hideProcessPid, false); + expect(state.showExecutableFirst, false); + expect(state.limitWindowTitleToOneLine, false); }); test('ignoring update works', () async { diff --git a/test/settings/widgets/personalization_section_test.dart b/test/settings/widgets/personalization_section_test.dart new file mode 100644 index 00000000..15c147f1 --- /dev/null +++ b/test/settings/widgets/personalization_section_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:nyrna/autostart/autostart_service.dart'; +import 'package:nyrna/hotkey/global/hotkey_service.dart'; +import 'package:nyrna/localization/app_localizations.dart'; +import 'package:nyrna/settings/settings.dart'; +import 'package:nyrna/settings/widgets/personalization_section.dart'; +import 'package:nyrna/storage/storage_repository.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), +]) +import 'personalization_section_test.mocks.dart'; + +void main() { + late SettingsCubit settingsCubit; + late MockAutostartService autostartService; + late MockHotkeyService hotkeyService; + late MockStorageRepository storage; + + setUp(() async { + autostartService = MockAutostartService(); + hotkeyService = MockHotkeyService(); + storage = MockStorageRepository(); + + when(autostartService.enable()).thenAnswer((_) async {}); + when(autostartService.disable()).thenAnswer((_) async {}); + when(hotkeyService.addHotkey(any)).thenAnswer((_) async {}); + when(hotkeyService.removeHotkey(any)).thenAnswer((_) async {}); + when(storage.getValue(any)).thenAnswer((_) async => null); + when( + storage.saveValue( + key: anyNamed('key'), + value: anyNamed('value'), + ), + ).thenAnswer((_) async {}); + when( + storage.getValue(any, storageArea: anyNamed('storageArea')), + ).thenAnswer((_) async => null); + when( + storage.saveValue( + key: anyNamed('key'), + value: anyNamed('value'), + storageArea: anyNamed('storageArea'), + ), + ).thenAnswer((_) async {}); + + settingsCubit = await SettingsCubit.init( + autostartService: autostartService, + hotkeyService: hotkeyService, + storage: storage, + ); + }); + + tearDown(() { + settingsCubit.close(); + }); + + testWidgets('renders 3 personalization tiles', (tester) async { + await tester.pumpWidget( + MaterialApp( + locale: const Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: BlocProvider.value( + value: settingsCubit, + child: const PersonalizationSection(), + ), + ), + ), + ); + + expect(find.textContaining('Hide PID'), findsOneWidget); + expect(find.textContaining('Executable at top'), findsOneWidget); + expect(find.textContaining('Limit title to one line'), findsOneWidget); + }); +} From c3f2ec794649b804434e313ff5003a2614474b71 Mon Sep 17 00:00:00 2001 From: btwl <1253102853@qq.com> Date: Mon, 23 Feb 2026 01:20:28 +0800 Subject: [PATCH 2/4] chore: add ci_local.ps1 to mimic Merrit CI before push --- scripts/ci_local.ps1 | 74 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 scripts/ci_local.ps1 diff --git a/scripts/ci_local.ps1 b/scripts/ci_local.ps1 new file mode 100644 index 00000000..3f78dec0 --- /dev/null +++ b/scripts/ci_local.ps1 @@ -0,0 +1,74 @@ +# Mimic Merrit's CI (tests.yml + build-windows) for local verification. +# Run before pushing to avoid embarrassing CI failures. +# Usage: .\scripts\ci_local.ps1 [-TestsOnly] [-BuildOnly] +# +# Ensure FLUTTER_ROOT is set (e.g. X:\apps\flutter_sdk\flutter) or flutter in PATH. + +param( + [switch]$TestsOnly, + [switch]$BuildOnly +) + +$ErrorActionPreference = "Stop" +$flutter = if ($env:FLUTTER_ROOT) { "$env:FLUTTER_ROOT\bin\flutter.bat" } else { "flutter" } +$dart = if ($env:FLUTTER_ROOT) { "$env:FLUTTER_ROOT\bin\dart.bat" } else { "dart" } +$root = Split-Path -Parent $PSScriptRoot +Push-Location $root + +function Step { param($name) Write-Host "`n===> $name" -ForegroundColor Cyan } +function Fail { param($msg) Write-Host "FAIL: $msg" -ForegroundColor Red; Pop-Location; exit 1 } +function Ok { Write-Host "OK" -ForegroundColor Green } + +try { + if (-not $BuildOnly) { + # ----------- tests.yml (Windows steps) ----------- + Step "Setup: flutter pub get" + & $flutter pub get + if ($LASTEXITCODE -ne 0) { Fail "flutter pub get failed" }; Ok + + Step "Fix localizations formatting" + & $dart format lib/localization/ -l 90 + if ($LASTEXITCODE -ne 0) { Fail "dart format lib/localization failed" }; Ok + + Step "Format all (so Verify will pass)" + & $dart format -l 90 . | Out-Null + Ok + + Step "Verify formatting" + & $dart format -o none --set-exit-if-changed --line-length=90 . + if ($LASTEXITCODE -ne 0) { Fail "Verify formatting failed - run 'dart format -l 90 .'" }; Ok + + Step "Run code generation" + & $flutter pub run build_runner build --delete-conflicting-outputs + if ($LASTEXITCODE -ne 0) { Fail "build_runner failed" }; Ok + + Step "Run i18n generation" + & $flutter gen-l10n + if ($LASTEXITCODE -ne 0) { Fail "gen-l10n failed" }; Ok + + Step "Run tests" + & $flutter test + if ($LASTEXITCODE -ne 0) { Fail "Tests failed" }; Ok + } + + if (-not $TestsOnly) { + # ----------- build-windows.yml ----------- + Step "Flutter config: enable-windows-desktop" + & $flutter config --enable-windows-desktop + if ($LASTEXITCODE -ne 0) { Fail "flutter config failed" }; Ok + + Step "Prepare: flutter pub get" + & $flutter pub get + if ($LASTEXITCODE -ne 0) { Fail "flutter pub get failed" }; Ok + + Step "Run build script (flutter_app_builder --platforms=windows)" + $env:prerelease = "true" + & $flutter pub run flutter_app_builder -v --platforms=windows + if ($LASTEXITCODE -ne 0) { Fail "flutter_app_builder failed" }; Ok + } + + Write-Host "`n=== All CI steps passed ===" -ForegroundColor Green +} +finally { + Pop-Location +} From ff67293398ab33db397ae73b9f0df5fe2ff74e11 Mon Sep 17 00:00:00 2001 From: btwl <1253102853@qq.com> Date: Mon, 23 Feb 2026 01:31:03 +0800 Subject: [PATCH 3/4] ci: remove l10n synthetic-package deprecation, align ci_local with Merrit --- l10n.yaml | 1 - scripts/ci_local.ps1 | 4 ---- 2 files changed, 5 deletions(-) diff --git a/l10n.yaml b/l10n.yaml index 2a1e01d0..423634b4 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,4 +1,3 @@ -synthetic-package: false arb-dir: lib/localization template-arb-file: app_en.arb output-localization-file: app_localizations.dart diff --git a/scripts/ci_local.ps1 b/scripts/ci_local.ps1 index 3f78dec0..767ca3b4 100644 --- a/scripts/ci_local.ps1 +++ b/scripts/ci_local.ps1 @@ -30,10 +30,6 @@ try { & $dart format lib/localization/ -l 90 if ($LASTEXITCODE -ne 0) { Fail "dart format lib/localization failed" }; Ok - Step "Format all (so Verify will pass)" - & $dart format -l 90 . | Out-Null - Ok - Step "Verify formatting" & $dart format -o none --set-exit-if-changed --line-length=90 . if ($LASTEXITCODE -ne 0) { Fail "Verify formatting failed - run 'dart format -l 90 .'" }; Ok From aaa52ee8a6a2b9911ab536821bd22fea075fb783 Mon Sep 17 00:00:00 2001 From: btwl <1253102853@qq.com> Date: Wed, 25 Feb 2026 11:23:34 +0800 Subject: [PATCH 4/4] chore: rename local CI helper --- scripts/{ci_local.ps1 => local_ci_check.ps1} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename scripts/{ci_local.ps1 => local_ci_check.ps1} (97%) diff --git a/scripts/ci_local.ps1 b/scripts/local_ci_check.ps1 similarity index 97% rename from scripts/ci_local.ps1 rename to scripts/local_ci_check.ps1 index 767ca3b4..79aa4b61 100644 --- a/scripts/ci_local.ps1 +++ b/scripts/local_ci_check.ps1 @@ -1,6 +1,6 @@ # Mimic Merrit's CI (tests.yml + build-windows) for local verification. # Run before pushing to avoid embarrassing CI failures. -# Usage: .\scripts\ci_local.ps1 [-TestsOnly] [-BuildOnly] +# Usage: .\scripts/local_ci_check.ps1 [-TestsOnly] [-BuildOnly] # # Ensure FLUTTER_ROOT is set (e.g. X:\apps\flutter_sdk\flutter) or flutter in PATH.