Skip to content

Commit

Permalink
feat: add ability to favorite apps
Browse files Browse the repository at this point in the history
Clicking the star button on an app will add it to
the favorites list.

Favorited apps will appear at the top of the list.

Resolves #213
  • Loading branch information
Merrit committed Jun 12, 2024
1 parent 742b924 commit a3801ec
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 5 deletions.
47 changes: 45 additions & 2 deletions lib/apps_list/cubit/apps_list_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,17 @@ class AppsListCubit extends Cubit<AppsListState> {

/// Populate the list of visible windows.
Future<void> _fetchWindows() async {
final windows = await _nativePlatform.windows(
List<Window> windows = await _nativePlatform.windows(
showHidden: _settingsCubit.state.showHiddenWindows,
);

// Filter out windows that are likely not desired or workable,
// 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]);
}
Expand All @@ -78,6 +81,24 @@ class AppsListCubit extends Cubit<AppsListState> {
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<List<Window>> _windowsWithFavoriteData(List<Window> windows) async {
final List<String> 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.
Expand Down Expand Up @@ -118,6 +139,21 @@ class AppsListCubit extends Cubit<AppsListState> {
emit(state.copyWith(loading: false));
}

/// Set a window as a favorite or not.
Future<void> setFavorite(Window window, bool favorite) async {
final List<String> 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<void> setWindowFilter(String pattern) async {
emit(state.copyWith(windowFilter: pattern.toLowerCase()));
Expand Down Expand Up @@ -322,17 +358,24 @@ extension on List<Window> {
/// 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(),
);
Expand Down
32 changes: 31 additions & 1 deletion lib/apps_list/widgets/window_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,13 @@ class _WindowTileState extends State<WindowTile> {
vertical: 2,
horizontal: 20,
),
trailing: const _DetailsButton(),
trailing: const Row(
mainAxisSize: MainAxisSize.min,
children: [
_FavoriteButton(),
_DetailsButton(),
],
),
onTap: () async {
log.i('WindowTile clicked: ${widget.window}');

Expand All @@ -105,6 +111,30 @@ class _WindowTileState extends State<WindowTile> {
}
}

/// Button to toggle the favorite status of a window.
class _FavoriteButton extends StatelessWidget {
const _FavoriteButton();

@override
Widget build(BuildContext context) {
final appsListCubit = context.read<AppsListCubit>();

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

Expand Down
11 changes: 10 additions & 1 deletion lib/localization/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -163,4 +172,4 @@
"@repository": {
"description": "Label for the repository link"
}
}
}
4 changes: 4 additions & 0 deletions lib/native_platform/src/process/models/process.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
2 changes: 1 addition & 1 deletion test/apps_list/widgets/window_tile_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit a3801ec

Please sign in to comment.