diff --git a/lib/apps_list/cubit/apps_list_cubit.dart b/lib/apps_list/cubit/apps_list_cubit.dart index a14d1cea..9af1ee9c 100644 --- a/lib/apps_list/cubit/apps_list_cubit.dart +++ b/lib/apps_list/cubit/apps_list_cubit.dart @@ -61,7 +61,7 @@ class AppsListCubit extends Cubit { /// Populate the list of visible windows. Future _fetchWindows() async { - final windows = await _nativePlatform.windows( + List windows = await _nativePlatform.windows( showHidden: _settingsCubit.state.showHiddenWindows, ); @@ -69,6 +69,9 @@ class AppsListCubit extends Cubit { // for example the root window, unknown (0) pid, etc. windows.removeWhere((element) => element.process.pid < 10); + // Update windows with favorite data. + windows = await _windowsWithFavoriteData(windows); + for (var i = 0; i < windows.length; i++) { windows[i] = await _refreshWindowProcess(windows[i]); } @@ -78,6 +81,24 @@ class AppsListCubit extends Cubit { emit(state.copyWith(windows: windows)); } + /// Updates the list of windows with favorite data. + /// + /// This method takes a list of windows and retrieves the favorite data for + /// each window from local storage. + /// + /// A window is considered a favorite if its executable name has been saved + /// to the 'favorites' key in local storage. + Future> _windowsWithFavoriteData(List windows) async { + final List favorites = await _storage.getValue('favorites') ?? []; + + return windows.map((window) { + final isFavorite = favorites.contains(window.process.executable); + return window.copyWith( + process: window.process.copyWith(isFavorite: isFavorite), + ); + }).toList(); + } + Timer? _timer; /// The timer which auto-refreshes the list of open windows. @@ -118,6 +139,21 @@ class AppsListCubit extends Cubit { emit(state.copyWith(loading: false)); } + /// Set a window as a favorite or not. + Future setFavorite(Window window, bool favorite) async { + final List favorites = await _storage.getValue('favorites') ?? []; + + if (favorite) { + assert(!favorites.contains(window.process.executable)); + favorites.add(window.process.executable); + } else { + favorites.remove(window.process.executable); + } + + await _storage.saveValue(key: 'favorites', value: favorites); + await manualRefresh(); + } + /// Set a filter for the windows shown in the list. Future setWindowFilter(String pattern) async { emit(state.copyWith(windowFilter: pattern.toLowerCase())); @@ -322,17 +358,24 @@ extension on List { /// Sort the windows by executable name. /// /// If the user has enabled pinning suspended windows to the top of the list, - /// those windows will be sorted to the top. + /// or the window's process has been favorited by the user, those windows will + /// be sorted to the top. void sortWindows(bool pinSuspendedWindows) { sort((a, b) { final aIsSuspended = a.process.status == ProcessStatus.suspended; final bIsSuspended = b.process.status == ProcessStatus.suspended; + final aIsFavorite = a.process.isFavorite; + final bIsFavorite = b.process.isFavorite; + if (pinSuspendedWindows) { if (aIsSuspended && !bIsSuspended) return -1; if (!aIsSuspended && bIsSuspended) return 1; } + if (aIsFavorite && !bIsFavorite) return -1; + if (!aIsFavorite && bIsFavorite) return 1; + return a.process.executable.toLowerCase().compareTo( b.process.executable.toLowerCase(), ); diff --git a/lib/apps_list/widgets/window_tile.dart b/lib/apps_list/widgets/window_tile.dart index f375f184..3e31aeb1 100644 --- a/lib/apps_list/widgets/window_tile.dart +++ b/lib/apps_list/widgets/window_tile.dart @@ -88,7 +88,13 @@ class _WindowTileState extends State { vertical: 2, horizontal: 20, ), - trailing: const _DetailsButton(), + trailing: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + _FavoriteButton(), + _DetailsButton(), + ], + ), onTap: () async { log.i('WindowTile clicked: ${widget.window}'); @@ -105,6 +111,30 @@ class _WindowTileState extends State { } } +/// Button to toggle the favorite status of a window. +class _FavoriteButton extends StatelessWidget { + const _FavoriteButton(); + + @override + Widget build(BuildContext context) { + final appsListCubit = context.read(); + + final window = context.select((WindowCubit cubit) => cubit.state.window); + final isFavorite = window.process.isFavorite; + + return IconButton( + icon: Icon( + (isFavorite) ? Icons.star : Icons.star_border, + color: (isFavorite) ? Colors.yellow : null, + ), + onPressed: () => appsListCubit.setFavorite(window, !isFavorite), + tooltip: (isFavorite) + ? AppLocalizations.of(context)!.favoriteButtonTooltipRemove + : AppLocalizations.of(context)!.favoriteButtonTooltipAdd, + ); + } +} + class _DetailsButton extends StatelessWidget { const _DetailsButton(); diff --git a/lib/localization/app_en.arb b/lib/localization/app_en.arb index 215a8128..cf26ee49 100644 --- a/lib/localization/app_en.arb +++ b/lib/localization/app_en.arb @@ -12,6 +12,15 @@ "@filterWindows": { "description": "Hint text for searchbox that allows the user to filter windows" }, + "@_APPS_LIST_PAGE": {}, + "favoriteButtonTooltipAdd": "Add to favorites", + "@favoriteButtonTooltipAdd": { + "description": "Tooltip for the add to favorites button" + }, + "favoriteButtonTooltipRemove": "Remove from favorites", + "@favoriteButtonTooltipRemove": { + "description": "Tooltip for the remove from favorites button" + }, "@_DETAILS_DIALOG": {}, "detailsDialogTitle": "Details", "@detailsDialogTitle": { @@ -163,4 +172,4 @@ "@repository": { "description": "Label for the repository link" } -} \ No newline at end of file +} diff --git a/lib/native_platform/src/process/models/process.dart b/lib/native_platform/src/process/models/process.dart index 44a7e39b..efb4645f 100644 --- a/lib/native_platform/src/process/models/process.dart +++ b/lib/native_platform/src/process/models/process.dart @@ -24,6 +24,10 @@ class Process with _$Process { /// Name of the executable, for example 'firefox' or 'firefox-bin'. required String executable, + /// Whether the application has been marked as a favorite. + @Default(false) // + bool isFavorite, + /// The Process ID (PID) of the given process. required int pid, diff --git a/test/apps_list/widgets/window_tile_test.dart b/test/apps_list/widgets/window_tile_test.dart index e82769e3..6d06313f 100644 --- a/test/apps_list/widgets/window_tile_test.dart +++ b/test/apps_list/widgets/window_tile_test.dart @@ -81,7 +81,7 @@ void main() { ), ); - await tester.tap(find.byType(IconButton)); + await tester.tap(find.byType(MenuAnchor)); await tester.pumpAndSettle(); expect(find.text('Suspend all instances'), findsOneWidget);