diff --git a/packages/app_center/assets/app-center.desktop b/packages/app_center/assets/app-center.desktop index 8f17d391a..2512a76c2 100644 --- a/packages/app_center/assets/app-center.desktop +++ b/packages/app_center/assets/app-center.desktop @@ -2,7 +2,7 @@ Type=Application Version=1.0 Exec=snap-store %U -Icon=software-store +Icon=bin/data/flutter_assets/assets/app-center.png Terminal=false Categories=System;Utility;PackageManager;SoftwareManagement;Network;Settings; Keywords=Ubuntu;Applications;Apps;Store;Software;Snaps; diff --git a/packages/app_center/lib/constants.dart b/packages/app_center/lib/constants.dart index 7ec5b10a4..452dc7da9 100644 --- a/packages/app_center/lib/constants.dart +++ b/packages/app_center/lib/constants.dart @@ -8,3 +8,5 @@ const kShimmerBaseLight = Color.fromARGB(120, 228, 228, 228); const kShimmerBaseDark = Color.fromARGB(255, 51, 51, 51); const kShimmerHighLightLight = Color.fromARGB(200, 247, 247, 247); const kShimmerHighLightDark = Color.fromARGB(255, 57, 57, 57); + +const kCircularProgressIndicatorHeight = 16.0; diff --git a/packages/app_center/lib/src/deb/deb_model.dart b/packages/app_center/lib/src/deb/deb_model.dart index 2963404bb..70804e232 100644 --- a/packages/app_center/lib/src/deb/deb_model.dart +++ b/packages/app_center/lib/src/deb/deb_model.dart @@ -46,7 +46,7 @@ class DebModel extends ChangeNotifier { AsyncValue<void> _state; PackageKitPackageInfo? packageInfo; - bool get isInstalled => packageInfo!.info == PackageKitInfo.installed; + bool get isInstalled => packageInfo?.info == PackageKitInfo.installed; Stream<PackageKitServiceError> get errorStream => packageKit.errorStream; diff --git a/packages/app_center/lib/src/deb/deb_page.dart b/packages/app_center/lib/src/deb/deb_page.dart index bb7392e57..859e32a02 100644 --- a/packages/app_center/lib/src/deb/deb_page.dart +++ b/packages/app_center/lib/src/deb/deb_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:app_center/appstream.dart'; +import 'package:app_center/constants.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; import 'package:app_center/src/deb/deb_model.dart'; @@ -79,7 +80,7 @@ class _DebView extends StatelessWidget { final debInfos = <AppInfo>[ ( label: l10n.snapPageVersionLabel, - value: Text(debModel.packageInfo!.packageId.version) + value: Text(debModel.packageInfo?.packageId.version ?? '') ), if (debModel.component.urls.isNotEmpty) ( @@ -173,7 +174,7 @@ class _DebActionButtons extends ConsumerWidget { .whenOrNull(data: (data) => data); return Center( child: SizedBox.square( - dimension: 16, + dimension: kCircularProgressIndicatorHeight, child: YaruCircularProgressIndicator( value: (transaction?.percentage ?? 0) / 100.0, strokeWidth: 2, @@ -195,7 +196,10 @@ class _DebActionButtons extends ConsumerWidget { mainAxisSize: MainAxisSize.min, overflowButtonSpacing: 8, children: [ - primaryActionButton, + if (debModel.packageInfo != null) + primaryActionButton + else + Text(l10n.debPageErrorNoPackageInfo), if (debModel.activeTransactionId != null) cancelButton ].whereNotNull().toList(), ); diff --git a/packages/app_center/lib/src/explore/explore_page.dart b/packages/app_center/lib/src/explore/explore_page.dart index 6009f5a4a..b23cb9a0c 100644 --- a/packages/app_center/lib/src/explore/explore_page.dart +++ b/packages/app_center/lib/src/explore/explore_page.dart @@ -1,6 +1,7 @@ import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; import 'package:app_center/snapd.dart'; +import 'package:app_center/src/store/store_pages.dart'; import 'package:app_center/store.dart'; import 'package:app_center/widgets.dart'; import 'package:collection/collection.dart'; @@ -172,8 +173,14 @@ class _CategoryBanner extends ConsumerWidget { snaps: featuredSnaps?.take(kNumberOfBannerSnaps).toList() ?? [], slogan: category.slogan(l10n), buttonLabel: category.buttonLabel(l10n), - onPressed: () => - StoreNavigator.pushSearch(context, category: category.categoryName), + onPressed: () { + if (displayedCategories.contains(category)) { + ref.read(yaruPageControllerProvider).index = + displayedCategories.indexOf(category) + 1; + } else { + StoreNavigator.pushSearch(context, category: category.categoryName); + } + }, colors: category.bannerColors, ); } diff --git a/packages/app_center/lib/src/l10n/app_en.arb b/packages/app_center/lib/src/l10n/app_en.arb index b2d293054..3b1404523 100644 --- a/packages/app_center/lib/src/l10n/app_en.arb +++ b/packages/app_center/lib/src/l10n/app_en.arb @@ -1,4 +1,5 @@ { + "appCenterLabel": "App Center", "appstreamSearchGreylist": "app;application;package;program;programme;suite;tool", "snapPageChannelLabel": "Channel", "snapPageConfinementLabel": "Confinement", @@ -195,6 +196,6 @@ "snapReportPrivacyAgreementLabel": "In submitting this form, I confirm that I have read and agree to ", "snapReportPrivacyAgreementCanonicalPrivacyNotice": "Canonical’s Privacy Notice ", "snapReportPrivacyAgreementAndLabel": "and ", - "snapReportPrivacyAgreementPrivacyPolicy": "Privacy Policy" - + "snapReportPrivacyAgreementPrivacyPolicy": "Privacy Policy", + "debPageErrorNoPackageInfo": "No package information found" } diff --git a/packages/app_center/lib/src/manage/manage_page.dart b/packages/app_center/lib/src/manage/manage_page.dart index cf3130b0d..e372ef95b 100644 --- a/packages/app_center/lib/src/manage/manage_page.dart +++ b/packages/app_center/lib/src/manage/manage_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:app_center/constants.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; import 'package:app_center/snapd.dart'; @@ -126,6 +127,7 @@ class _ManageView extends ConsumerWidget { : index == 0 ? ManageTilePosition.first : ManageTilePosition.middle, + showUpdateButton: true, ), ), SliverList.list(children: [ @@ -224,7 +226,7 @@ class _ActionButtons extends ConsumerWidget { loading: () => ( l10n.managePageCheckingForUpdates, const SizedBox( - height: 24, + height: kCircularProgressIndicatorHeight, child: YaruCircularProgressIndicator( strokeWidth: 4, ), @@ -233,6 +235,9 @@ class _ActionButtons extends ConsumerWidget { error: (_, __) => ('', const SizedBox.shrink()), ); + final updatesInprogress = updatesModel.refreshableSnapNames.isNotEmpty && + !updatesModel.state.isLoading && + updatesModel.activeChangeId != null; return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -270,7 +275,7 @@ class _ActionButtons extends ConsumerWidget { return Row( children: [ SizedBox.square( - dimension: 16, + dimension: kCircularProgressIndicatorHeight, child: YaruCircularProgressIndicator( value: change?.progress, strokeWidth: 2, @@ -303,6 +308,19 @@ class _ActionButtons extends ConsumerWidget { ], ), ), + const SizedBox(width: 8), + PushButton.outlined( + onPressed: updatesInprogress + ? () => ref + .read(updatesModelProvider) + .cancelChange(updatesModel.activeChangeId!) + : null, + child: Text( + l10n.snapActionCancelLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ], ); } @@ -314,10 +332,12 @@ class _ManageSnapTile extends ConsumerWidget { const _ManageSnapTile({ required this.snap, this.position = ManageTilePosition.middle, + this.showUpdateButton = false, }); final Snap snap; final ManageTilePosition position; + final bool showUpdateButton; @override Widget build(BuildContext context, WidgetRef ref) { @@ -438,43 +458,148 @@ class _ManageSnapTile extends ConsumerWidget { ) ], ), - trailing: ButtonBar( - mainAxisSize: MainAxisSize.min, - children: [ - Visibility( - maintainSize: true, - maintainAnimation: true, - maintainState: true, - visible: snapLauncher.isLaunchable, - child: OutlinedButton( - onPressed: snapLauncher.open, - child: Text(l10n.snapActionOpenLabel), + trailing: showUpdateButton + ? buildButtonBarForUpdate(ref, l10n, snapLauncher, context) + : buildButtonBarForOpen(ref, l10n, snapLauncher, context), + ); + } + + ButtonBar buildButtonBarForUpdate(WidgetRef ref, AppLocalizations l10n, + SnapLauncher snapLauncher, BuildContext context) { + return ButtonBar( + mainAxisSize: MainAxisSize.min, + children: [ + Consumer( + builder: (context, ref, child) { + final snapModel = ref.watch(snapModelProvider(snap.name)); + final updatesModel = ref.watch(updatesModelProvider); + + return PushButton.outlined( + onPressed: updatesModel.activeChangeId != null + ? null + : () { + ref.read(snapModelProvider(snap.name)).refresh(); + }, + child: snapModel.activeChangeId != null + ? Consumer( + builder: (context, ref, child) { + final change = ref + .watch(changeProvider(snapModel.activeChangeId)) + .whenOrNull(data: (data) => data); + return Row( + children: [ + SizedBox.square( + dimension: kCircularProgressIndicatorHeight, + child: YaruCircularProgressIndicator( + value: change?.progress, + strokeWidth: 2, + ), + ), + if (change != null) ...[ + const SizedBox(width: 8), + Flexible( + child: Text( + change.localize(l10n) ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ] + ], + ); + }, + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(YaruIcons.download), + const SizedBox(width: 8), + Flexible( + child: Text( + l10n.snapActionUpdateLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + }, + ), + MenuAnchor( + menuChildren: [ + Visibility( + maintainSize: true, + maintainAnimation: true, + maintainState: true, + visible: snapLauncher.isLaunchable, + child: MenuItemButton( + onPressed: snapLauncher.open, + child: Text(l10n.snapActionOpenLabel), + ), ), + MenuItemButton( + onPressed: () => + StoreNavigator.pushSnap(context, name: snap.name), + child: Text( + l10n.managePageShowDetailsLabel, + style: Theme.of(context).textTheme.bodyMedium, + ), + ) + ], + builder: (context, controller, child) => YaruOptionButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Icon(YaruIcons.view_more_horizontal), ), - MenuAnchor( - menuChildren: [ - MenuItemButton( - onPressed: () => - StoreNavigator.pushSnap(context, name: snap.name), - child: Text( - l10n.managePageShowDetailsLabel, - style: Theme.of(context).textTheme.bodyMedium, - ), - ) - ], - builder: (context, controller, child) => YaruOptionButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: const Icon(YaruIcons.view_more_horizontal), - ), - ) - ], - ), + ) + ], + ); + } + + ButtonBar buildButtonBarForOpen(WidgetRef ref, AppLocalizations l10n, + SnapLauncher snapLauncher, BuildContext context) { + return ButtonBar( + mainAxisSize: MainAxisSize.min, + children: [ + Visibility( + maintainSize: true, + maintainAnimation: true, + maintainState: true, + visible: snapLauncher.isLaunchable, + child: OutlinedButton( + onPressed: snapLauncher.open, + child: Text(l10n.snapActionOpenLabel), + ), + ), + MenuAnchor( + menuChildren: [ + MenuItemButton( + onPressed: () => + StoreNavigator.pushSnap(context, name: snap.name), + child: Text( + l10n.managePageShowDetailsLabel, + style: Theme.of(context).textTheme.bodyMedium, + ), + ) + ], + builder: (context, controller, child) => YaruOptionButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Icon(YaruIcons.view_more_horizontal), + ), + ) + ], ); } } diff --git a/packages/app_center/lib/src/packagekit/packagekit_service.dart b/packages/app_center/lib/src/packagekit/packagekit_service.dart index f49019b49..a81fbe347 100644 --- a/packages/app_center/lib/src/packagekit/packagekit_service.dart +++ b/packages/app_center/lib/src/packagekit/packagekit_service.dart @@ -145,13 +145,20 @@ class PackageKitService { String name, [ @visibleForTesting String? architecture, ]) async { - final arch = architecture ?? await _getNativeArchitecture(); + final possibleArchs = [ + architecture ?? await _getNativeArchitecture(), + 'all' + ]; PackageKitPackageInfo? info; await _createTransaction( action: (transaction) => transaction.resolve([name]), listener: (event) { - if (event is PackageKitPackageEvent && event.packageId.arch == arch) { + if (event is PackageKitPackageEvent && + possibleArchs.contains(event.packageId.arch)) { info = event; + } else { + log.error( + 'Couldn\'t resolve package $name with architectures $possibleArchs'); } }, ).then(waitTransaction); diff --git a/packages/app_center/lib/src/search/search_page.dart b/packages/app_center/lib/src/search/search_page.dart index 8dcb02985..33024f0e8 100644 --- a/packages/app_center/lib/src/search/search_page.dart +++ b/packages/app_center/lib/src/search/search_page.dart @@ -51,6 +51,7 @@ class SearchPage extends StatelessWidget { Consumer(builder: (context, ref, child) { final sortOrder = ref.watch(snapSortOrderProvider); return MenuButtonBuilder<SnapSortOrder?>( + expanded: false, values: const [ null, SnapSortOrder.alphabeticalAsc, diff --git a/packages/app_center/lib/src/snapd/snap_category_enum.dart b/packages/app_center/lib/src/snapd/snap_category_enum.dart index 2525db8a3..098c0e741 100644 --- a/packages/app_center/lib/src/snapd/snap_category_enum.dart +++ b/packages/app_center/lib/src/snapd/snap_category_enum.dart @@ -107,7 +107,7 @@ enum SnapCategoryEnum { artAndDesign => selected ? YaruIcons.rule_and_pen_filled : YaruIcons.rule_and_pen, booksAndReference => selected ? YaruIcons.book_filled : YaruIcons.book, - development => YaruIcons.wrench, + development => selected ? YaruIcons.wrench_filled : YaruIcons.wrench, devicesAndIot => selected ? YaruIcons.chip_filled : YaruIcons.chip, education => selected ? YaruIcons.education_filled : YaruIcons.education, diff --git a/packages/app_center/lib/src/snapd/snap_page.dart b/packages/app_center/lib/src/snapd/snap_page.dart index 66ac4696e..5bc675cde 100644 --- a/packages/app_center/lib/src/snapd/snap_page.dart +++ b/packages/app_center/lib/src/snapd/snap_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:app_center/constants.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; import 'package:app_center/ratings.dart'; @@ -329,7 +330,7 @@ class _SnapActionButtons extends ConsumerWidget { return Row( children: [ SizedBox.square( - dimension: 16, + dimension: kCircularProgressIndicatorHeight, child: YaruCircularProgressIndicator( value: change?.progress, strokeWidth: 2, @@ -454,6 +455,7 @@ class _RatingsActionButtons extends ConsumerWidget { ), ), child: YaruIconButton( + mouseCursor: SystemMouseCursors.click, icon: Icon( ratingsModel.vote == VoteStatus.up ? Icons.thumb_up @@ -486,6 +488,7 @@ class _RatingsActionButtons extends ConsumerWidget { ), ), child: YaruIconButton( + mouseCursor: SystemMouseCursors.click, icon: Icon( ratingsModel.vote == VoteStatus.down ? Icons.thumb_down diff --git a/packages/app_center/lib/src/snapd/updates_model.dart b/packages/app_center/lib/src/snapd/updates_model.dart index 25a7d1d3b..5f37dd6c2 100644 --- a/packages/app_center/lib/src/snapd/updates_model.dart +++ b/packages/app_center/lib/src/snapd/updates_model.dart @@ -63,4 +63,26 @@ class UpdatesModel extends ChangeNotifier { _activeChangeId = null; await refresh(); } + + Future<void> cancelChange(String changeId) async { + if (changeId.isEmpty) return; + + try { + final changeDetails = await snapd.getChange(changeId); + + // If the change is already completed, ignore silently. + // If it wouldn't be ignored, an error would be displayed to the user, + // which might be confusing. + if (changeDetails.ready) { + return; + } + + final abortChange = await snapd.abortChange(changeId); + await snapd.waitChange(abortChange.id); + _activeChangeId = null; + notifyListeners(); + } on SnapdException catch (e) { + _handleError(e); + } + } } diff --git a/packages/app_center/lib/src/store/store_app.dart b/packages/app_center/lib/src/store/store_app.dart index 0d1e727f7..30179df6b 100644 --- a/packages/app_center/lib/src/store/store_app.dart +++ b/packages/app_center/lib/src/store/store_app.dart @@ -17,6 +17,9 @@ import 'package:yaru_widgets/yaru_widgets.dart'; final materialAppNavigatorKeyProvider = Provider((ref) => GlobalKey<NavigatorState>()); +final yaruPageControllerProvider = + Provider((ref) => YaruPageController(length: pages.length)); + class StoreApp extends ConsumerStatefulWidget { const StoreApp({super.key}); @@ -43,55 +46,72 @@ class _StoreAppState extends ConsumerState<StoreApp> { localizationsDelegates: localizationsDelegates, navigatorKey: ref.watch(materialAppNavigatorKeyProvider), supportedLocales: supportedLocales, - home: Scaffold( - appBar: YaruWindowTitleBar( - title: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: kSearchBarWidth), - child: SearchField( - onSearch: (query) => - _navigator.pushAndRemoveSearch(query: query), - onSnapSelected: (name) => _navigator.pushSnap(name: name), - onDebSelected: (id) => _navigator.pushDeb(id: id), + home: YaruWindowTitleSetter( + child: Scaffold( + appBar: YaruWindowTitleBar( + title: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: kSearchBarWidth), + child: SearchField( + onSearch: (query) => + _navigator.pushAndRemoveSearch(query: query), + onSnapSelected: (name) => _navigator.pushSnap(name: name), + onDebSelected: (id) => _navigator.pushDeb(id: id), + ), ), ), - ), - body: YaruMasterDetailPage( - navigatorKey: _navigatorKey, - navigatorObservers: [StoreObserver(ref)], - initialRoute: ref.watch(initialRouteProvider), - length: pages.length, - tileBuilder: (context, index, selected, availableWidth) => - pages[index].tileBuilder(context, selected), - pageBuilder: (context, index) => pages[index].pageBuilder(context), - layoutDelegate: const YaruMasterFixedPaneDelegate( - paneWidth: kPaneWidth, - ), - breakpoint: 0, // always landscape - onGenerateRoute: (settings) => - switch (StoreRoutes.routeOf(settings)) { - StoreRoutes.deb => MaterialPageRoute( - settings: settings, - builder: (_) => DebPage( - id: StoreRoutes.debOf(settings)!, - )), - StoreRoutes.snap => MaterialPageRoute( - settings: settings, - builder: (_) => SnapPage( - snapName: StoreRoutes.snapOf(settings)!, + body: YaruMasterDetailPage( + navigatorKey: _navigatorKey, + navigatorObservers: [StoreObserver(ref)], + initialRoute: ref.watch(initialRouteProvider), + controller: ref.watch(yaruPageControllerProvider), + tileBuilder: (context, index, selected, availableWidth) => + pages[index].tileBuilder(context, selected), + pageBuilder: (context, index) => + pages[index].pageBuilder(context), + layoutDelegate: const YaruMasterFixedPaneDelegate( + paneWidth: kPaneWidth, + ), + breakpoint: 0, // always landscape + onGenerateRoute: (settings) => + switch (StoreRoutes.routeOf(settings)) { + StoreRoutes.deb => MaterialPageRoute( + settings: settings, + builder: (_) => DebPage( + id: StoreRoutes.debOf(settings)!, + )), + StoreRoutes.snap => MaterialPageRoute( + settings: settings, + builder: (_) => SnapPage( + snapName: StoreRoutes.snapOf(settings)!, + ), ), - ), - StoreRoutes.search => MaterialPageRoute( - settings: settings, - builder: (_) => SearchPage( - query: StoreRoutes.queryOf(settings), - category: StoreRoutes.categoryOf(settings), + StoreRoutes.search => MaterialPageRoute( + settings: settings, + builder: (_) => SearchPage( + query: StoreRoutes.queryOf(settings), + category: StoreRoutes.categoryOf(settings), + ), ), - ), - _ => null, - }, + _ => null, + }, + ), ), ), ), ); } } + +class YaruWindowTitleSetter extends StatelessWidget { + const YaruWindowTitleSetter({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + YaruWindow.of(context).setTitle(l10n.appCenterLabel); + + return child; + } +} diff --git a/packages/app_center/lib/src/store/store_pages.dart b/packages/app_center/lib/src/store/store_pages.dart index d520eb227..8348c8df0 100644 --- a/packages/app_center/lib/src/store/store_pages.dart +++ b/packages/app_center/lib/src/store/store_pages.dart @@ -8,6 +8,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; +final displayedCategories = [ + SnapCategoryEnum.featured, + SnapCategoryEnum.productivity, + SnapCategoryEnum.development, + SnapCategoryEnum.games, +]; + typedef StorePage = ({ Widget Function(BuildContext context, bool selected) tileBuilder, WidgetBuilder pageBuilder, @@ -16,20 +23,15 @@ typedef StorePage = ({ final pages = <StorePage>[ ( tileBuilder: (context, selected) => YaruMasterTile( - leading: Icon(ExplorePage.icon(true)), + leading: Icon(ExplorePage.icon(selected)), title: Text(ExplorePage.label(context)), ), pageBuilder: (_) => const ExplorePage(), ), - for (final category in [ - SnapCategoryEnum.featured, - SnapCategoryEnum.productivity, - SnapCategoryEnum.development, - SnapCategoryEnum.games, - ]) + for (final category in displayedCategories) ( tileBuilder: (context, selected) => YaruMasterTile( - leading: Icon(category.icon(true)), + leading: Icon(category.icon(selected)), title: Text(category.localize(AppLocalizations.of(context))), ), pageBuilder: (_) => SearchPage(category: category.categoryName), @@ -44,7 +46,7 @@ final pages = <StorePage>[ ), ( tileBuilder: (context, selected) => YaruMasterTile( - leading: Icon(ManagePage.icon(true)), + leading: Icon(ManagePage.icon(selected)), title: Text(ManagePage.label(context)), trailing: Consumer( builder: (context, ref, child) { @@ -60,7 +62,7 @@ final pages = <StorePage>[ ), ( tileBuilder: (context, selected) => YaruMasterTile( - leading: Icon(AboutPage.icon(true)), + leading: Icon(AboutPage.icon(selected)), title: Text(AboutPage.label(context)), ), pageBuilder: (_) => const AboutPage(), diff --git a/packages/app_center/pubspec.yaml b/packages/app_center/pubspec.yaml index 994f3d66f..88ce58af5 100644 --- a/packages/app_center/pubspec.yaml +++ b/packages/app_center/pubspec.yaml @@ -44,13 +44,14 @@ dependencies: ubuntu_logger: ^0.1.0 ubuntu_service: ^0.3.0 ubuntu_test: ^0.1.0-0 - ubuntu_widgets: ^0.3.1 + ubuntu_widgets: ^0.3.2 url_launcher: ^6.2.1 xdg_directories: ^1.0.3 yaru: ^1.2.0 - yaru_icons: ^2.2.2 + yaru_icons: ^2.3.0 yaru_test: ^0.1.5 - yaru_widgets: ^3.3.0 + yaru_widgets: ^3.4.0 + yaru_window: ^0.2.0 dev_dependencies: build_runner: ^2.4.6 @@ -67,3 +68,4 @@ flutter: generate: true assets: - assets/ + \ No newline at end of file diff --git a/packages/app_center/test/manage_page_test.dart b/packages/app_center/test/manage_page_test.dart index 5e4c5e0c2..3f38b73fc 100644 --- a/packages/app_center/test/manage_page_test.dart +++ b/packages/app_center/test/manage_page_test.dart @@ -35,6 +35,13 @@ void main() { channel: 'latest/stable', ), ]; + + final snapModel = createMockSnapModel( + hasUpdate: true, + localSnap: refreshableSnaps[0], + storeSnap: refreshableSnaps[0], + ); + testWidgets('list installed snaps', (tester) async { await tester.pumpApp( (_) => ProviderScope( @@ -126,7 +133,8 @@ void main() { ), ), showLocalSystemAppsProvider.overrideWith((ref) => true), - updatesModelProvider.overrideWith((_) => mockUpdatesModel) + updatesModelProvider.overrideWith((_) => mockUpdatesModel), + snapModelProvider.overrideWith((_, __) => snapModel) ], child: const ManagePage(), ), @@ -143,6 +151,44 @@ void main() { verify(mockUpdatesModel.updateAll()).called(1); }); + testWidgets('refresh individual snap', (tester) async { + final mockUpdatesModel = createMockUpdatesModel( + refreshableSnapNames: refreshableSnaps.map((snap) => snap.name)); + + final snapModel = createMockSnapModel( + hasUpdate: true, + localSnap: refreshableSnaps[0], + storeSnap: refreshableSnaps[0], + ); + + await tester.pumpApp( + (_) => ProviderScope( + overrides: [ + launchProvider.overrideWith((_, __) => createMockSnapLauncher()), + snapModelProvider.overrideWith((ref, arg) => snapModel), + manageModelProvider.overrideWith( + (_) => createMockManageModel( + refreshableSnaps: refreshableSnaps, + ), + ), + showLocalSystemAppsProvider.overrideWith((ref) => true), + updatesModelProvider.overrideWith((_) => mockUpdatesModel) + ], + child: const ManagePage(), + ), + ); + + final testTile = find.snapTile('Snap with an update'); + expect(testTile, findsOneWidget); + expect(find.descendant(of: testTile, matching: find.text('2.0')), + findsOneWidget); + expect(find.descendant(of: testTile, matching: find.text('latest/stable')), + findsOneWidget); + + await tester.tap(find.text(tester.l10n.snapActionUpdateLabel)); + verify(snapModel.refresh()).called(1); + }); + testWidgets('refreshing all', (tester) async { final mockUpdatesModel = createMockUpdatesModel( refreshableSnapNames: refreshableSnaps.map((snap) => snap.name), @@ -166,6 +212,7 @@ void main() { updatesModelProvider.overrideWith((_) => mockUpdatesModel), changeProvider .overrideWith((_, __) => Stream.fromIterable([mockChange])), + snapModelProvider.overrideWith((_, __) => snapModel) ], child: const ManagePage(), ), @@ -188,6 +235,34 @@ void main() { ); }); + testWidgets('cancel refresh all', (tester) async { + final mockUpdatesModel = createMockUpdatesModel( + refreshableSnapNames: refreshableSnaps.map((snap) => snap.name), + isBusy: true, + ); + + await tester.pumpApp( + (_) => ProviderScope( + overrides: [ + launchProvider.overrideWith((_, __) => createMockSnapLauncher()), + manageModelProvider.overrideWith( + (_) => createMockManageModel( + refreshableSnaps: refreshableSnaps, + ), + ), + snapModelProvider.overrideWith((_, __) => snapModel), + updatesModelProvider.overrideWith((_) => mockUpdatesModel), + ], + child: const ManagePage(), + ), + ); + await tester.pump(); + + final cancelButton = find.buttonWithText(tester.l10n.snapActionCancelLabel); + expect(cancelButton, findsOneWidget); + expect(cancelButton, isEnabled); + }); + testWidgets('error dialog', (tester) async { await tester.pumpApp( (_) => ProviderScope( @@ -209,7 +284,8 @@ void main() { ), ), ), - ) + ), + snapModelProvider.overrideWith((_, __) => snapModel) ], child: const ManagePage(), ), diff --git a/packages/app_center/test/packagekit_service_test.dart b/packages/app_center/test/packagekit_service_test.dart index d35d61ed1..506ca5ebb 100644 --- a/packages/app_center/test/packagekit_service_test.dart +++ b/packages/app_center/test/packagekit_service_test.dart @@ -159,6 +159,29 @@ void main() { final info = await packageKit.resolve('foo', 'amd64'); expect(info!.packageId.arch, equals('amd64')); }); + + test('architecture \'all\'', () async { + final mockTransaction = createMockPackageKitTransaction( + events: const [ + PackageKitPackageEvent( + info: PackageKitInfo.available, + packageId: PackageKitPackageId( + name: 'foo', + version: '1.0', + arch: 'all', + ), + summary: 'summary', + ), + ], + ); + final mockClient = + createMockPackageKitClient(transaction: mockTransaction); + final packageKit = + PackageKitService(dbus: createMockDbusClient(), client: mockClient); + await packageKit.activateService(); + final info = await packageKit.resolve('foo', 'all'); + expect(info!.packageId.arch, equals('all')); + }); }); test('cancel', () async { diff --git a/packages/app_center/test/test_utils.mocks.dart b/packages/app_center/test/test_utils.mocks.dart index 41e3e0160..488cc2c30 100644 --- a/packages/app_center/test/test_utils.mocks.dart +++ b/packages/app_center/test/test_utils.mocks.dart @@ -1452,6 +1452,16 @@ class MockUpdatesModel extends _i1.Mock implements _i6.UpdatesModel { returnValueForMissingStub: _i14.Future<void>.value(), ) as _i14.Future<void>); + @override + _i14.Future<void> cancelChange(String? changeId) => (super.noSuchMethod( + Invocation.method( + #cancelChange, + [changeId], + ), + returnValue: _i14.Future<void>.value(), + returnValueForMissingStub: _i14.Future<void>.value(), + ) as _i14.Future<void>); + @override void addListener(_i15.VoidCallback? listener) => super.noSuchMethod( Invocation.method(