Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to favorite apps #216

Merged
merged 1 commit into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading