From cef90be0d6bb7b085cc75e2c4e00cd27926d8585 Mon Sep 17 00:00:00 2001 From: Ivo Heck Date: Tue, 10 Mar 2026 21:47:40 +0100 Subject: [PATCH 1/5] add installed lable to installed snaps, split installed snaps and filtering logic into separate providers --- .../lib/manage/local_snap_providers.dart | 149 ++++++++++-------- .../providers/installed_snaps_provider.dart | 32 ++++ .../app_center/lib/search/search_page.dart | 8 + packages/app_center/lib/snapd/snap_model.dart | 8 +- packages/app_center/lib/src/l10n/app_en.arb | 3 +- packages/app_center/lib/widgets/app_card.dart | 89 ++++++++--- .../lib/widgets/category_snap_list.dart | 9 ++ .../app_center/lib/widgets/snap_grid.dart | 5 + 8 files changed, 209 insertions(+), 94 deletions(-) create mode 100644 packages/app_center/lib/providers/installed_snaps_provider.dart diff --git a/packages/app_center/lib/manage/local_snap_providers.dart b/packages/app_center/lib/manage/local_snap_providers.dart index 44869f732..aa98dab54 100644 --- a/packages/app_center/lib/manage/local_snap_providers.dart +++ b/packages/app_center/lib/manage/local_snap_providers.dart @@ -1,9 +1,8 @@ import 'package:app_center/manage/updates_model.dart'; +import 'package:app_center/providers/installed_snaps_provider.dart'; import 'package:app_center/snapd/snapd.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:snapd/snapd.dart'; -import 'package:ubuntu_service/ubuntu_service.dart'; part 'local_snap_providers.g.dart'; @@ -13,70 +12,92 @@ final localSnapSortOrderProvider = StateProvider((_) => SnapSortOrder.alphabeticalAsc); @riverpod -class FilteredLocalSnaps extends _$FilteredLocalSnaps { - late final _snapd = getService(); +Future filteredLocalSnaps(Ref ref) async { + final installedState = await ref.watch(installedSnapsProvider.future); - @override - Future build() async { - final snapListState = await connectionCheck(_snapd.getSnaps, ref); - final snaps = snapListState.snaps; - final refreshableSnaps = - (await ref.read(updatesModelProvider.future)).snaps.map((s) => s.name); - final nonRefreshableSnaps = - snaps.where((s) => !refreshableSnaps.contains(s.name)); - void refreshFunction(_, __) => _refreshWithFilters(nonRefreshableSnaps); - ref.listen(localSnapFilterProvider, refreshFunction); - ref.listen(showLocalSystemAppsProvider, refreshFunction); - ref.listen(localSnapSortOrderProvider, refreshFunction); - return snapListState.copyWith( - snaps: _refreshWithFilters(nonRefreshableSnaps, updateState: false), - ); - } + final filter = ref.watch(localSnapFilterProvider).toLowerCase(); + final showSystemApps = ref.watch(showLocalSystemAppsProvider); + final sortOrder = ref.watch(localSnapSortOrderProvider); - /// Used to add a snap from the list without reloading the whole provider. - /// Should be used when a snap is uninstalled directly from the manage page - /// list for example. - Future addToList(Snap snap) async { - if (!state.hasValue) return; - final localSnap = await _snapd.getSnap(snap.name); - _refreshWithFilters([...state.value!.snaps, localSnap]); - } + final updates = await ref.watch(updatesModelProvider.future); + final updateNames = updates.snaps.map((s) => s.name).toSet(); - /// Used to remove a snap from the list without reloading the whole provider. - /// Should be used when a snap is uninstalled directly from the manage page - /// list for example. - void removeFromList(String snapName) { - if (!state.hasValue) return; - state = AsyncData( - state.value!.copyWith( - snaps: state.value!.snaps.where((s) => s.name != snapName), - ), - ); - } + final filteredSnaps = installedState.snaps + .where((s) => !updateNames.contains(s.name)) + .where((s) => + s.titleOrName.toLowerCase().contains(filter) && + (showSystemApps || s.apps.isNotEmpty)) + .toSet() + .sortedSnaps(sortOrder); - Iterable _refreshWithFilters( - Iterable nonRefreshableSnaps, { - bool updateState = true, - }) { - final filter = ref.read(localSnapFilterProvider).toLowerCase(); - final showSystemApps = ref.read(showLocalSystemAppsProvider); - final sortOrder = ref.read(localSnapSortOrderProvider); - final filteredSnaps = nonRefreshableSnaps - .where( - (snap) => - snap.titleOrName.toLowerCase().contains(filter) && - (showSystemApps || snap.apps.isNotEmpty), - ) - .toSet() - .sortedSnaps(sortOrder); - if (updateState) { - state = AsyncData( - SnapListState( - snaps: filteredSnaps, - hasInternet: state.value?.hasInternet ?? true, - ), - ); - } - return filteredSnaps; - } + return installedState.copyWith(snaps: filteredSnaps); } + +// @riverpod +// class FilteredLocalSnaps extends _$FilteredLocalSnaps { +// late final _snapd = getService(); + +// @override +// Future build() async { +// final snapListState = await connectionCheck(_snapd.getSnaps, ref); +// final snaps = snapListState.snaps; +// final refreshableSnaps = +// (await ref.read(updatesModelProvider.future)).snaps.map((s) => s.name); +// final nonRefreshableSnaps = +// snaps.where((s) => !refreshableSnaps.contains(s.name)); +// void refreshFunction(_, __) => _refreshWithFilters(nonRefreshableSnaps); +// ref.listen(localSnapFilterProvider, refreshFunction); +// ref.listen(showLocalSystemAppsProvider, refreshFunction); +// ref.listen(localSnapSortOrderProvider, refreshFunction); +// return snapListState.copyWith( +// snaps: _refreshWithFilters(nonRefreshableSnaps, updateState: false), +// ); +// } + + // /// Used to add a snap from the list without reloading the whole provider. + // /// Should be used when a snap is uninstalled directly from the manage page + // /// list for example. + // Future addToList(Snap snap) async { + // if (!state.hasValue) return; + // final localSnap = await _snapd.getSnap(snap.name); + // _refreshWithFilters([...state.value!.snaps, localSnap]); + // } + + // /// Used to remove a snap from the list without reloading the whole provider. + // /// Should be used when a snap is uninstalled directly from the manage page + // /// list for example. + // void removeFromList(String snapName) { + // if (!state.hasValue) return; + // state = AsyncData( + // state.value!.copyWith( + // snaps: state.value!.snaps.where((s) => s.name != snapName), + // ), + // ); + // } + +// Iterable _refreshWithFilters( +// Iterable nonRefreshableSnaps, { +// bool updateState = true, +// }) { +// final filter = ref.read(localSnapFilterProvider).toLowerCase(); +// final showSystemApps = ref.read(showLocalSystemAppsProvider); +// final sortOrder = ref.read(localSnapSortOrderProvider); +// final filteredSnaps = nonRefreshableSnaps +// .where( +// (snap) => +// snap.titleOrName.toLowerCase().contains(filter) && +// (showSystemApps || snap.apps.isNotEmpty), +// ) +// .toSet() +// .sortedSnaps(sortOrder); +// if (updateState) { +// state = AsyncData( +// SnapListState( +// snaps: filteredSnaps, +// hasInternet: state.value?.hasInternet ?? true, +// ), +// ); +// } +// return filteredSnaps; +// } +// } diff --git a/packages/app_center/lib/providers/installed_snaps_provider.dart b/packages/app_center/lib/providers/installed_snaps_provider.dart new file mode 100644 index 000000000..f353a83c7 --- /dev/null +++ b/packages/app_center/lib/providers/installed_snaps_provider.dart @@ -0,0 +1,32 @@ +import 'package:app_center/manage/updates_model.dart'; +import 'package:app_center/snapd/snapd_service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:snapd/snapd.dart'; +import 'package:ubuntu_service/ubuntu_service.dart'; + +part 'installed_snaps_provider.g.dart'; + +@riverpod +class InstalledSnaps extends _$InstalledSnaps { + late final _snapd = getService(); + + @override + Future build() async { + return connectionCheck(_snapd.getSnaps, ref); + } + + Future addToList(Snap snap) async { + if (!state.hasValue) return; + final localSnap = await _snapd.getSnap(snap.name); + state = AsyncData(state.value!.copyWith( + snaps: [...state.value!.snaps, localSnap], + )); + } + + void removeFromList(String snapName) { + if (!state.hasValue) return; + state = AsyncData(state.value!.copyWith( + snaps: state.value!.snaps.where((s) => s.name != snapName), + )); + } +} diff --git a/packages/app_center/lib/search/search_page.dart b/packages/app_center/lib/search/search_page.dart index bd0fa9a3f..69110f38d 100644 --- a/packages/app_center/lib/search/search_page.dart +++ b/packages/app_center/lib/search/search_page.dart @@ -2,6 +2,7 @@ import 'package:app_center/appstream/appstream.dart'; import 'package:app_center/error/error.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; +import 'package:app_center/providers/installed_snaps_provider.dart'; import 'package:app_center/search/search.dart'; import 'package:app_center/snapd/multisnap_model.dart'; import 'package:app_center/snapd/snapd.dart'; @@ -271,11 +272,18 @@ class _SnapSearchResults extends ConsumerWidget { ), ), ); + + final installedSnapsModel = ref.watch(installedSnapsProvider); + + final installedSnapsIds = + installedSnapsModel.value?.snaps.map((s) => s.id).toList() ?? []; + return results.when( data: (data) => data.isNotEmpty ? ResponsiveLayoutScrollView( slivers: [ AppCardGrid.fromSnaps( + installedIds: installedSnapsIds, snaps: data, onTap: (snap) => StoreNavigator.pushSearchSnap( context, diff --git a/packages/app_center/lib/snapd/snap_model.dart b/packages/app_center/lib/snapd/snap_model.dart index b9455a843..153758702 100644 --- a/packages/app_center/lib/snapd/snap_model.dart +++ b/packages/app_center/lib/snapd/snap_model.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:app_center/manage/local_snap_providers.dart'; import 'package:app_center/manage/updates_model.dart'; +import 'package:app_center/providers/installed_snaps_provider.dart'; import 'package:app_center/snapd/currently_installing_model.dart'; import 'package:app_center/snapd/snapd.dart'; import 'package:app_center/snapd/snapd_cache.dart'; @@ -97,7 +97,7 @@ class SnapModel extends _$SnapModel { _updateChangeId(changeId); await _listenUntilDone(changeId, ref); unawaited( - ref.read(filteredLocalSnapsProvider.notifier).addToList(storeSnap), + ref.read(installedSnapsProvider.notifier).addToList(storeSnap), ); } @@ -140,7 +140,7 @@ class SnapModel extends _$SnapModel { if (removeFromList && completedSuccessfully) { ref.read(updatesModelProvider.notifier).removeFromList(snapData.name); ref - .read(filteredLocalSnapsProvider.notifier) + .read(installedSnapsProvider.notifier) .addToList(snapData.localSnap!); } return completedSuccessfully; @@ -154,7 +154,7 @@ class SnapModel extends _$SnapModel { _updateChangeId(changeId); await _listenUntilDone(changeId, ref); ref.read(updatesModelProvider.notifier).removeFromList(snapName); - ref.read(filteredLocalSnapsProvider.notifier).removeFromList(snapName); + ref.read(installedSnapsProvider.notifier).removeFromList(snapName); } Future revert() async { diff --git a/packages/app_center/lib/src/l10n/app_en.arb b/packages/app_center/lib/src/l10n/app_en.arb index 96c7d83db..66d03aea3 100644 --- a/packages/app_center/lib/src/l10n/app_en.arb +++ b/packages/app_center/lib/src/l10n/app_en.arb @@ -369,5 +369,6 @@ "codecPageTitle": "Missing media codecs", "codecPageDescription": "You need to install the following codecs for handling certain video and audio formats:", "codecProprietaryDisclaimer": "Some of the recommended codecs could be proprietary.", - "codecInstallAllButton": "Install recommended codecs" + "codecInstallAllButton": "Install recommended codecs", + "installedLable": "Installed" } diff --git a/packages/app_center/lib/widgets/app_card.dart b/packages/app_center/lib/widgets/app_card.dart index c4a99b38c..d541939ba 100644 --- a/packages/app_center/lib/widgets/app_card.dart +++ b/packages/app_center/lib/widgets/app_card.dart @@ -22,10 +22,12 @@ class AppCard extends StatelessWidget { this.compact = false, this.iconUrl, this.footer, + this.isInstalled = false, }); AppCard.fromSnap({ required Snap snap, + bool isInstalled = false, VoidCallback? onTap, }) : this( key: ValueKey(snap.id), @@ -34,6 +36,7 @@ class AppCard extends StatelessWidget { iconUrl: snap.iconUrl, footer: _RatingsInfo(snap: snap), onTap: onTap, + isInstalled: isInstalled, ); AppCard.fromDeb({ @@ -74,6 +77,7 @@ class AppCard extends StatelessWidget { final bool compact; final String? iconUrl; final Widget? footer; + final bool isInstalled; @override Widget build(BuildContext context) { @@ -104,6 +108,7 @@ class AppCard extends StatelessWidget { title: title, summary: summary, footer: footer, + isInstalled: isInstalled, ), ), ], @@ -123,6 +128,7 @@ class RankedAppCard extends StatelessWidget { this.compact = false, this.iconUrl, this.footer, + this.isInstalled = false, super.key, }); @@ -146,6 +152,7 @@ class RankedAppCard extends StatelessWidget { final bool compact; final String? iconUrl; final Widget? footer; + final bool isInstalled; final int rank; @override @@ -192,6 +199,7 @@ class RankedAppCard extends StatelessWidget { title: title, summary: '', footer: footer, + isInstalled: isInstalled, ), ), ], @@ -207,10 +215,16 @@ class RankedAppCard extends StatelessWidget { // TODO: generalize class SnapImageCard extends StatelessWidget { - const SnapImageCard({required this.snap, super.key, this.onTap}); + const SnapImageCard({ + required this.snap, + required this.isInstalled, + super.key, + this.onTap, + }); final Snap snap; final VoidCallback? onTap; + final bool isInstalled; @override Widget build(BuildContext context) { @@ -246,6 +260,7 @@ class SnapImageCard extends StatelessWidget { summary: snap.summary, footer: _RatingsInfo(snap: snap), maxlines: 1, + isInstalled: isInstalled, ), ), ), @@ -256,10 +271,27 @@ class SnapImageCard extends StatelessWidget { } } +class _InstalledLable extends StatelessWidget { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + child: Text(l10n.installedLable), + ); + } +} + class _AppCardBody extends StatelessWidget { const _AppCardBody({ required this.title, required this.summary, + required this.isInstalled, this.footer, this.maxlines = 2, }); @@ -267,37 +299,44 @@ class _AppCardBody extends StatelessWidget { final Widget title; final String summary; final Widget? footer; + final bool isInstalled; final int maxlines; @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Stack( children: [ - ExcludeSemantics( - child: Container( - constraints: BoxConstraints(minHeight: kIconSize), - child: Align( - alignment: Alignment.bottomLeft, - child: title, - ), - ), - ), - if (summary.isNotEmpty) ...[ - Flexible( - child: ExcludeSemantics( - child: Text( - '$summary\n', - maxLines: maxlines, - overflow: TextOverflow.ellipsis, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ExcludeSemantics( + child: Container( + constraints: BoxConstraints(minHeight: kIconSize), + child: Align( + alignment: Alignment.bottomLeft, + child: title, + ), ), ), - ), - ], - if (footer != null) ...[ - footer!, - ], + if (summary.isNotEmpty) ...[ + Flexible( + child: ExcludeSemantics( + child: Text( + '$summary\n', + maxLines: maxlines, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + if (footer != null) ...[ + footer!, + ], + ], + ), + if (isInstalled) + Align(alignment: Alignment.topRight, child: _InstalledLable()), ], ); } diff --git a/packages/app_center/lib/widgets/category_snap_list.dart b/packages/app_center/lib/widgets/category_snap_list.dart index 0d6737078..f62360a11 100644 --- a/packages/app_center/lib/widgets/category_snap_list.dart +++ b/packages/app_center/lib/widgets/category_snap_list.dart @@ -1,4 +1,5 @@ import 'package:app_center/explore/explore_page.dart'; +import 'package:app_center/providers/installed_snaps_provider.dart'; import 'package:app_center/ratings/rated_category_model.dart'; import 'package:app_center/snapd/snapd.dart'; import 'package:app_center/store/store.dart'; @@ -53,14 +54,22 @@ class CategorySnapList extends ConsumerWidget { ?.take(numberOfSnaps) .toList() ?? []; + + final installedSnapsModel = ref.watch(installedSnapsProvider); + + final installedSnapsIds = + installedSnapsModel.value?.snaps.map((s) => s.id).toList() ?? []; + return showScreenshots ? SnapImageCardGrid( snaps: snaps, onTap: (snap) => StoreNavigator.pushSnap(context, name: snap.name), + installedIds: installedSnapsIds, ) : AppCardGrid.fromSnaps( snaps: snaps, onTap: (snap) => StoreNavigator.pushSnap(context, name: snap.name), + installedIds: installedSnapsIds, ); } } diff --git a/packages/app_center/lib/widgets/snap_grid.dart b/packages/app_center/lib/widgets/snap_grid.dart index c82d0a171..c613d4fda 100644 --- a/packages/app_center/lib/widgets/snap_grid.dart +++ b/packages/app_center/lib/widgets/snap_grid.dart @@ -14,11 +14,13 @@ class AppCardGrid extends StatelessWidget { factory AppCardGrid.fromSnaps({ required List snaps, required ValueChanged onTap, + required List installedIds, }) => AppCardGrid( appCards: snaps.map( (snap) => AppCard.fromSnap( snap: snap, + isInstalled: installedIds.contains(snap.id), onTap: () => onTap(snap), ), ), @@ -126,11 +128,13 @@ class SnapImageCardGrid extends StatelessWidget { const SnapImageCardGrid({ required this.snaps, required this.onTap, + required this.installedIds, super.key, }); final List snaps; final ValueChanged onTap; + final List installedIds; @override Widget build(BuildContext context) { @@ -157,6 +161,7 @@ class SnapImageCardGrid extends StatelessWidget { key: ValueKey(snap.id), snap: snap, onTap: () => onTap(snap), + isInstalled: installedIds.contains(snap.id), ); }, ); From 22791621bb78cd817facaa9cc5be803c8d9ff77d Mon Sep 17 00:00:00 2001 From: Ivo Heck Date: Wed, 11 Mar 2026 18:56:40 +0100 Subject: [PATCH 2/5] add german translation --- packages/app_center/lib/src/l10n/app_de.arb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app_center/lib/src/l10n/app_de.arb b/packages/app_center/lib/src/l10n/app_de.arb index 2fc7f73fd..f82cda458 100644 --- a/packages/app_center/lib/src/l10n/app_de.arb +++ b/packages/app_center/lib/src/l10n/app_de.arb @@ -478,5 +478,6 @@ "debPageDocumentationLinkLabel": "Erfahren Sie mehr über die Verwaltung von Debian-Paketen", "@debPageDocumentationLinkLabel": {}, "managePageDocumentationLinkLabel": "In der Dokumentation erfahren Sie, wie Sie Debian-Pakete verwalten können.", - "@managePageDocumentationLinkLabel": {} + "@managePageDocumentationLinkLabel": {}, + "installedLable": "Installiert" } From d2c5c893d3b99b431ed870fa9abdc1782e8e5ccc Mon Sep 17 00:00:00 2001 From: Ivo Heck Date: Wed, 11 Mar 2026 19:05:29 +0100 Subject: [PATCH 3/5] removing old code --- .../lib/manage/local_snap_providers.dart | 69 ------------------- .../providers/installed_snaps_provider.dart | 22 ++++-- 2 files changed, 16 insertions(+), 75 deletions(-) diff --git a/packages/app_center/lib/manage/local_snap_providers.dart b/packages/app_center/lib/manage/local_snap_providers.dart index aa98dab54..9005b86fd 100644 --- a/packages/app_center/lib/manage/local_snap_providers.dart +++ b/packages/app_center/lib/manage/local_snap_providers.dart @@ -32,72 +32,3 @@ Future filteredLocalSnaps(Ref ref) async { return installedState.copyWith(snaps: filteredSnaps); } - -// @riverpod -// class FilteredLocalSnaps extends _$FilteredLocalSnaps { -// late final _snapd = getService(); - -// @override -// Future build() async { -// final snapListState = await connectionCheck(_snapd.getSnaps, ref); -// final snaps = snapListState.snaps; -// final refreshableSnaps = -// (await ref.read(updatesModelProvider.future)).snaps.map((s) => s.name); -// final nonRefreshableSnaps = -// snaps.where((s) => !refreshableSnaps.contains(s.name)); -// void refreshFunction(_, __) => _refreshWithFilters(nonRefreshableSnaps); -// ref.listen(localSnapFilterProvider, refreshFunction); -// ref.listen(showLocalSystemAppsProvider, refreshFunction); -// ref.listen(localSnapSortOrderProvider, refreshFunction); -// return snapListState.copyWith( -// snaps: _refreshWithFilters(nonRefreshableSnaps, updateState: false), -// ); -// } - - // /// Used to add a snap from the list without reloading the whole provider. - // /// Should be used when a snap is uninstalled directly from the manage page - // /// list for example. - // Future addToList(Snap snap) async { - // if (!state.hasValue) return; - // final localSnap = await _snapd.getSnap(snap.name); - // _refreshWithFilters([...state.value!.snaps, localSnap]); - // } - - // /// Used to remove a snap from the list without reloading the whole provider. - // /// Should be used when a snap is uninstalled directly from the manage page - // /// list for example. - // void removeFromList(String snapName) { - // if (!state.hasValue) return; - // state = AsyncData( - // state.value!.copyWith( - // snaps: state.value!.snaps.where((s) => s.name != snapName), - // ), - // ); - // } - -// Iterable _refreshWithFilters( -// Iterable nonRefreshableSnaps, { -// bool updateState = true, -// }) { -// final filter = ref.read(localSnapFilterProvider).toLowerCase(); -// final showSystemApps = ref.read(showLocalSystemAppsProvider); -// final sortOrder = ref.read(localSnapSortOrderProvider); -// final filteredSnaps = nonRefreshableSnaps -// .where( -// (snap) => -// snap.titleOrName.toLowerCase().contains(filter) && -// (showSystemApps || snap.apps.isNotEmpty), -// ) -// .toSet() -// .sortedSnaps(sortOrder); -// if (updateState) { -// state = AsyncData( -// SnapListState( -// snaps: filteredSnaps, -// hasInternet: state.value?.hasInternet ?? true, -// ), -// ); -// } -// return filteredSnaps; -// } -// } diff --git a/packages/app_center/lib/providers/installed_snaps_provider.dart b/packages/app_center/lib/providers/installed_snaps_provider.dart index f353a83c7..8cb98e24e 100644 --- a/packages/app_center/lib/providers/installed_snaps_provider.dart +++ b/packages/app_center/lib/providers/installed_snaps_provider.dart @@ -15,18 +15,28 @@ class InstalledSnaps extends _$InstalledSnaps { return connectionCheck(_snapd.getSnaps, ref); } + // Used to add a snap from the list without reloading the whole provider. + // Should be used when a snap is uninstalled directly from the manage page + // list for example. Future addToList(Snap snap) async { if (!state.hasValue) return; final localSnap = await _snapd.getSnap(snap.name); - state = AsyncData(state.value!.copyWith( - snaps: [...state.value!.snaps, localSnap], - )); + state = AsyncData( + state.value!.copyWith( + snaps: [...state.value!.snaps, localSnap], + ), + ); } + // Used to remove a snap from the list without reloading the whole provider. + // Should be used when a snap is uninstalled directly from the manage page + // list for example. void removeFromList(String snapName) { if (!state.hasValue) return; - state = AsyncData(state.value!.copyWith( - snaps: state.value!.snaps.where((s) => s.name != snapName), - )); + state = AsyncData( + state.value!.copyWith( + snaps: state.value!.snaps.where((s) => s.name != snapName), + ), + ); } } From 60579a124bfb3c7ea5fc8d8eac7030928527584d Mon Sep 17 00:00:00 2001 From: Ivo Heck Date: Fri, 13 Mar 2026 18:22:06 +0100 Subject: [PATCH 4/5] fix broken tests --- packages/app_center/test/games_page_test.dart | 8 +++++++ .../app_center/test/search_page_test.dart | 23 +++++++++++++++++++ packages/app_center/test/test_utils.dart | 15 ++++++++++++ 3 files changed, 46 insertions(+) diff --git a/packages/app_center/test/games_page_test.dart b/packages/app_center/test/games_page_test.dart index 16d133e24..6fe112165 100644 --- a/packages/app_center/test/games_page_test.dart +++ b/packages/app_center/test/games_page_test.dart @@ -1,4 +1,6 @@ import 'package:app_center/games/games.dart'; +import 'package:app_center/manage/updates_model.dart'; +import 'package:app_center/providers/installed_snaps_provider.dart'; import 'package:app_center/search/search.dart'; import 'package:app_center/snapd/snap_category_enum.dart'; import 'package:app_center/snapd/snap_search.dart'; @@ -23,6 +25,8 @@ void main() { setUp(() => registerMockRatingsService(rating: snapRating, snapVotes: [])); tearDown(resetAllServices); + final mockInstalledSnapsState = SnapListState(); + final mockSearchProvider = createMockSnapSearchProvider({ const SnapSearchParameters(query: 'testsn'): [ createSnap(name: 'testsnap', title: 'Test Snap', downloadSize: 3), @@ -69,6 +73,8 @@ void main() { overrides: [ snapSearchProvider .overrideWith((ref, arg) => mockSearchProvider(arg)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: SearchPage(category: SnapCategoryEnum.games.name), ), @@ -129,6 +135,8 @@ void main() { overrides: [ snapSearchProvider .overrideWith((ref, arg) => mockSearchProvider(arg)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const GamesPage(), ), diff --git a/packages/app_center/test/search_page_test.dart b/packages/app_center/test/search_page_test.dart index 363d41ffa..f7575f80c 100644 --- a/packages/app_center/test/search_page_test.dart +++ b/packages/app_center/test/search_page_test.dart @@ -1,3 +1,5 @@ +import 'package:app_center/manage/updates_model.dart'; +import 'package:app_center/providers/installed_snaps_provider.dart'; import 'package:app_center/search/search.dart'; import 'package:app_center/snapd/multisnap_model.dart'; import 'package:app_center/snapd/snapd.dart'; @@ -60,12 +62,17 @@ void main() { }); final multiSnapModel = MockMultiSnapModel(); + final mockInstalledSnapsState = SnapListState(); + + testWidgets('query', (tester) async { await tester.pumpApp( (_) => ProviderScope( overrides: [ snapSearchProvider .overrideWith((ref, query) => mockSearchProvider(query)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const SearchPage(query: 'testsn'), ), @@ -92,6 +99,8 @@ void main() { overrides: [ snapSearchProvider .overrideWith((ref, query) => mockSearchProvider(query)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const SearchPage( query: 'testsn', @@ -121,6 +130,8 @@ void main() { overrides: [ snapSearchProvider .overrideWith((ref, query) => mockSearchProvider(query)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const SearchPage( category: 'education', @@ -145,6 +156,8 @@ void main() { snapSearchProvider .overrideWith((ref, query) => mockSearchProvider(query)), multiSnapModelProvider.overrideWith((ref, arg) => multiSnapModel), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const SearchPage( category: 'gameDev', @@ -172,6 +185,8 @@ void main() { overrides: [ snapSearchProvider .overrideWith((ref, query) => mockSearchProvider(query)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const SearchPage(query: 'testsn'), ), @@ -198,6 +213,8 @@ void main() { overrides: [ snapSearchProvider .overrideWith((ref, query) => mockSearchProvider(query)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const SearchPage(query: 'testsn'), ), @@ -227,6 +244,8 @@ void main() { overrides: [ snapSearchProvider .overrideWith((ref, query) => mockSearchProvider(query)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const SearchPage(query: 'testsn'), ), @@ -259,6 +278,8 @@ void main() { overrides: [ snapSearchProvider .overrideWith((ref, query) => mockSearchProvider(query)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const SearchPage(query: 'foo'), ), @@ -276,6 +297,8 @@ void main() { overrides: [ snapSearchProvider .overrideWith((ref, query) => mockSearchProvider(query)), + + installedSnapsProvider.overrideWith(() => MockInstalledSnaps(mockInstalledSnapsState)), ], child: const SearchPage(query: 'foo', category: 'social'), ), diff --git a/packages/app_center/test/test_utils.dart b/packages/app_center/test/test_utils.dart index a44d3c51a..74188c732 100644 --- a/packages/app_center/test/test_utils.dart +++ b/packages/app_center/test/test_utils.dart @@ -5,9 +5,11 @@ import 'package:app_center/appstream/appstream.dart'; import 'package:app_center/gstreamer/gstreamer_model.dart'; import 'package:app_center/gstreamer/gstreamer_resource.dart'; import 'package:app_center/l10n.dart'; +import 'package:app_center/manage/updates_model.dart'; import 'package:app_center/packagekit/packagekit.dart'; import 'package:app_center/providers/error_stream_provider.dart'; import 'package:app_center/providers/file_system_provider.dart'; +import 'package:app_center/providers/installed_snaps_provider.dart'; import 'package:app_center/ratings/ratings.dart'; import 'package:app_center/snapd/multisnap_model.dart'; import 'package:app_center/snapd/snapd.dart'; @@ -454,3 +456,16 @@ Snap createSnap({ refreshInhibit: refreshInhibit, ); } + +class MockInstalledSnaps extends InstalledSnaps { + MockInstalledSnaps(this.mockState); + final SnapListState mockState; + + @override + Future build() async => mockState; + + @override + Future addToList(dynamic snap) async {} + @override + void removeFromList(String snapName) {} +} \ No newline at end of file From a45d76e81204acebff7846be2d296bebc98df2ec Mon Sep 17 00:00:00 2001 From: Ivo Heck Date: Fri, 13 Mar 2026 19:10:23 +0100 Subject: [PATCH 5/5] add test case, fix typo --- packages/app_center/lib/src/l10n/app_de.arb | 2 +- packages/app_center/lib/src/l10n/app_en.arb | 2 +- packages/app_center/lib/widgets/app_card.dart | 6 +++--- packages/app_center/test/snap_page_test.dart | 13 +++++++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/app_center/lib/src/l10n/app_de.arb b/packages/app_center/lib/src/l10n/app_de.arb index f82cda458..70a31638f 100644 --- a/packages/app_center/lib/src/l10n/app_de.arb +++ b/packages/app_center/lib/src/l10n/app_de.arb @@ -479,5 +479,5 @@ "@debPageDocumentationLinkLabel": {}, "managePageDocumentationLinkLabel": "In der Dokumentation erfahren Sie, wie Sie Debian-Pakete verwalten können.", "@managePageDocumentationLinkLabel": {}, - "installedLable": "Installiert" + "installedLabel": "Installiert" } diff --git a/packages/app_center/lib/src/l10n/app_en.arb b/packages/app_center/lib/src/l10n/app_en.arb index 66d03aea3..ebfec4b31 100644 --- a/packages/app_center/lib/src/l10n/app_en.arb +++ b/packages/app_center/lib/src/l10n/app_en.arb @@ -370,5 +370,5 @@ "codecPageDescription": "You need to install the following codecs for handling certain video and audio formats:", "codecProprietaryDisclaimer": "Some of the recommended codecs could be proprietary.", "codecInstallAllButton": "Install recommended codecs", - "installedLable": "Installed" + "installedLabel": "Installed" } diff --git a/packages/app_center/lib/widgets/app_card.dart b/packages/app_center/lib/widgets/app_card.dart index d541939ba..1232028d9 100644 --- a/packages/app_center/lib/widgets/app_card.dart +++ b/packages/app_center/lib/widgets/app_card.dart @@ -271,7 +271,7 @@ class SnapImageCard extends StatelessWidget { } } -class _InstalledLable extends StatelessWidget { +class _InstalledLabel extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); @@ -282,7 +282,7 @@ class _InstalledLable extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - child: Text(l10n.installedLable), + child: Text(l10n.installedLabel), ); } } @@ -336,7 +336,7 @@ class _AppCardBody extends StatelessWidget { ], ), if (isInstalled) - Align(alignment: Alignment.topRight, child: _InstalledLable()), + Align(alignment: Alignment.topRight, child: _InstalledLabel()), ], ); } diff --git a/packages/app_center/test/snap_page_test.dart b/packages/app_center/test/snap_page_test.dart index b827bbe8f..32ad818d8 100644 --- a/packages/app_center/test/snap_page_test.dart +++ b/packages/app_center/test/snap_page_test.dart @@ -425,5 +425,18 @@ void main() { expect(find.text(snapRating.ratingsBand.localize(l10n)), findsNothing); }); + testWidgets('show installed label', (tester) async { + await tester.pumpApp( + (_) => AppCard( + title: const AppTitle(title: 'Test App'), + summary: 'Summary', + isInstalled: true, + ), + ); + + expect(find.text(tester.l10n.installedLabel), findsOneWidget); + }); + + // TODO: test loading states with snap change in progress }