diff --git a/README.md b/README.md index 58aece5c6..a0a1f3382 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ An alternative software store for the Ubuntu Desktop made with Flutter. - [X] install from file-explorer - [X] list installed debs/rpms - [X] remove - - [ ] search for debs/rpms (TBD if wanted) + - [X] update + - [X] search for debs/rpms (TBD if wanted) ## Firmware updater diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 130103598..fe28d9f34 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -53,7 +53,9 @@ "media": "Media", "done": "Done", "systemUpdates": "System updates", - "searchHint": "Search", + "searchHint": "Press enter to search", "updateAll": "Update all", - "noUpdates": "No updates available" + "noUpdates": "No updates available", + "apps": "apps", + "filterSnaps": "Set the snap filter" } \ No newline at end of file diff --git a/lib/store_app/explore/explore_model.dart b/lib/store_app/explore/explore_model.dart index f6eb92e81..83b788ed4 100644 --- a/lib/store_app/explore/explore_model.dart +++ b/lib/store_app/explore/explore_model.dart @@ -1,14 +1,17 @@ import 'dart:async'; +import 'package:packagekit/packagekit.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; import 'package:snapd/snapd.dart'; import 'package:software/store_app/common/snap_section.dart'; class ExploreModel extends SafeChangeNotifier { - final SnapdClient client; + final SnapdClient _snapDClient; + final PackageKitClient _packageKitClient; ExploreModel( - this.client, + this._snapDClient, + this._packageKitClient, ) : _searchQuery = '', sectionNameToSnapsMap = {}, _errorMessage = '', @@ -59,7 +62,7 @@ class ExploreModel extends SafeChangeNotifier { return []; } else { try { - return await client.find( + return await _snapDClient.find( query: _searchQuery, section: selectedSection == SnapSection.all ? null : selectedSection.title, @@ -74,7 +77,7 @@ class ExploreModel extends SafeChangeNotifier { Future<List<Snap>> findSnapsBySection({SnapSection? section}) async { if (section == null) return []; try { - return (await client.find( + return (await _snapDClient.find( section: section == SnapSection.all ? SnapSection.featured.title : section.title, @@ -96,4 +99,27 @@ class ExploreModel extends SafeChangeNotifier { sectionNameToSnapsMap.putIfAbsent(section.title, () => sectionList); notifyListeners(); } + + Future<List<PackageKitPackageId>> findPackageKitPackageIds() async { + if (searchQuery.isEmpty) return []; + final List<PackageKitPackageId> ids = []; + final transaction = await _packageKitClient.createTransaction(); + final completer = Completer(); + transaction.events.listen((event) { + if (event is PackageKitPackageEvent) { + final id = event.packageId; + ids.add(id); + } else if (event is PackageKitErrorCodeEvent) { + } else if (event is PackageKitFinishedEvent) { + completer.complete(); + } + }); + await transaction.searchNames( + [searchQuery], + filter: {}, + ); + await completer.future; + + return ids; + } } diff --git a/lib/store_app/explore/explore_page.dart b/lib/store_app/explore/explore_page.dart index ed572eafd..b59d49fdd 100644 --- a/lib/store_app/explore/explore_page.dart +++ b/lib/store_app/explore/explore_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:packagekit/packagekit.dart'; import 'package:provider/provider.dart'; import 'package:snapd/snapd.dart'; import 'package:software/l10n/l10n.dart'; @@ -19,6 +20,7 @@ class ExplorePage extends StatelessWidget { return ChangeNotifierProvider( create: (_) => ExploreModel( getService<SnapdClient>(), + getService<PackageKitClient>(), ), child: const ExplorePage(), ); @@ -41,31 +43,38 @@ class ExplorePage extends StatelessWidget { : null, flexibleSpace: !model.searchActive ? null : const SearchField(), ), - body: Padding( - padding: const EdgeInsets.only(top: 20), - child: Column( - children: [ - if ((model.selectedSection == SnapSection.featured || - model.selectedSection == SnapSection.all) && - model.searchQuery.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(horizontal: 20), - child: SnapBannerCarousel( - snapSection: SnapSection.featured, - height: 220, - ), + body: Column( + children: [ + if ((model.selectedSection == SnapSection.featured || + model.selectedSection == SnapSection.all) && + model.searchQuery.isEmpty) + const Padding( + padding: EdgeInsets.only(top: 20, left: 20, right: 20), + child: SnapBannerCarousel( + snapSection: SnapSection.featured, + height: 220, ), - if (model.searchQuery.isEmpty && - model.sectionNameToSnapsMap.isNotEmpty) - Expanded( + ), + if (model.searchQuery.isEmpty && + model.sectionNameToSnapsMap.isNotEmpty) + Expanded( + child: Padding( + padding: EdgeInsets.only( + top: (model.selectedSection == SnapSection.featured || + model.selectedSection == SnapSection.all) + ? 0 + : 20, + ), child: SectionBannerGrid(snapSection: model.selectedSection), ), - if (model.errorMessage.isNotEmpty) - _ErrorPage(errorMessage: model.errorMessage), - if (model.searchQuery.isNotEmpty) - const Expanded(child: SearchPage()) - ], - ), + ), + if (model.errorMessage.isNotEmpty) + _ErrorPage(errorMessage: model.errorMessage), + if (model.searchQuery.isNotEmpty) + const Expanded( + child: SearchPage(), + ) + ], ), ); } diff --git a/lib/store_app/explore/search_field.dart b/lib/store_app/explore/search_field.dart index dd684e580..f5676fac1 100644 --- a/lib/store_app/explore/search_field.dart +++ b/lib/store_app/explore/search_field.dart @@ -27,11 +27,14 @@ class _SearchFieldState extends State<SearchField> { final model = context.watch<ExploreModel>(); return TextField( controller: _controller, - onChanged: (value) => model.searchQuery = value, + onEditingComplete: () { + model.searchQuery = _controller.text; + }, + textInputAction: TextInputAction.send, autofocus: true, decoration: InputDecoration( - hintText: - '${context.l10n.searchHint} ${model.selectedSection.localize(context.l10n)} snaps', + suffixText: + '${context.l10n.searchHint} ${model.selectedSection.localize(context.l10n)} ${context.l10n.apps}', suffixIcon: _SectionDropdown( value: model.selectedSection, onChanged: (v) => model.selectedSection = v!, @@ -83,6 +86,7 @@ class _SectionDropdown extends StatelessWidget { @override Widget build(BuildContext context) { return PopupMenuButton<SnapSection>( + tooltip: context.l10n.filterSnaps, splashRadius: 20, onSelected: onChanged, icon: Icon(snapSectionToIcon[value]), diff --git a/lib/store_app/explore/search_page.dart b/lib/store_app/explore/search_page.dart index e9bb0d38f..3e60baded 100644 --- a/lib/store_app/explore/search_page.dart +++ b/lib/store_app/explore/search_page.dart @@ -1,50 +1,117 @@ import 'package:flutter/material.dart'; +import 'package:packagekit/packagekit.dart'; import 'package:provider/provider.dart'; import 'package:snapd/snapd.dart'; +import 'package:software/l10n/l10n.dart'; import 'package:software/snapx.dart'; import 'package:software/store_app/common/app_banner.dart'; import 'package:software/store_app/common/constants.dart'; import 'package:software/store_app/common/snap_dialog.dart'; import 'package:software/store_app/explore/explore_model.dart'; +import 'package:software/store_app/my_apps/package_dialog.dart'; +import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; class SearchPage extends StatelessWidget { const SearchPage({super.key}); + @override + Widget build(BuildContext context) { + return YaruTabbedPage( + tabIcons: const [YaruIcons.package_snap, YaruIcons.package_deb], + tabTitles: [ + context.l10n.snapPackages, + context.l10n.debianPackages, + ], + views: const [ + _SnapSearchPage(), + _PackageKitSearchPage(), + ], + ); + } +} + +class _SnapSearchPage extends StatelessWidget { + // ignore: unused_element + const _SnapSearchPage({super.key}); + + @override + Widget build(BuildContext context) { + final model = context.watch<ExploreModel>(); + + return Padding( + padding: const EdgeInsets.only(top: 20), + child: FutureBuilder<List<Snap>>( + future: model.findSnapsByQuery(), + builder: (context, snapshot) => + snapshot.hasData && snapshot.data!.isNotEmpty + ? GridView( + controller: ScrollController(), + padding: const EdgeInsets.symmetric(horizontal: 20), + gridDelegate: kGridDelegate, + shrinkWrap: true, + children: [ + for (final snap in snapshot.data!) + AppBanner( + name: snap.name, + summary: snap.summary, + url: snap.iconUrl, + onTap: () => showDialog( + context: context, + builder: (context) => SnapDialog.create( + context: context, + huskSnapName: snap.name, + ), + ), + ) + ], + ) + : const SizedBox(), + ), + ); + } +} + +class _PackageKitSearchPage extends StatelessWidget { + // ignore: unused_element + const _PackageKitSearchPage({super.key}); + @override Widget build(BuildContext context) { final model = context.watch<ExploreModel>(); - if (model.searchQuery.isEmpty) return const SizedBox(); - return FutureBuilder<List<Snap>>( - future: model.findSnapsByQuery(), - builder: (context, snapshot) => - snapshot.hasData && snapshot.data!.isNotEmpty - ? GridView( - padding: const EdgeInsets.symmetric(horizontal: 20), - gridDelegate: kGridDelegate, - shrinkWrap: true, - children: [ - for (final snap in snapshot.data!) - AppBanner( - name: snap.name, - summary: snap.summary, - url: snap.iconUrl, - onTap: () => showDialog( - context: context, - builder: (context) => SnapDialog.create( + + return Padding( + padding: const EdgeInsets.only(top: 20), + child: FutureBuilder<List<PackageKitPackageId>>( + future: model.findPackageKitPackageIds(), + builder: (context, snapshot) => + snapshot.hasData && snapshot.data!.isNotEmpty + ? GridView( + controller: ScrollController(), + padding: const EdgeInsets.symmetric(horizontal: 20), + gridDelegate: kGridDelegate, + shrinkWrap: true, + children: [ + for (final id in snapshot.data!) + AppBanner( + name: id.name, + summary: id.version, + icon: const Icon( + YaruIcons.package_deb, + size: 50, + ), + onTap: () => showDialog( context: context, - huskSnapName: snap.name, + builder: (context) => PackageDialog.create( + context, + id, + ), ), - ), - ) - ], - ) - : const Center( - child: Padding( - padding: EdgeInsets.all(20.0), - child: YaruCircularProgressIndicator(), - ), - ), + ) + ], + ) + : const SizedBox(), + ), ); } } diff --git a/lib/store_app/my_apps/my_apps_page.dart b/lib/store_app/my_apps/my_apps_page.dart index 46074bd3f..5dd0a74e0 100644 --- a/lib/store_app/my_apps/my_apps_page.dart +++ b/lib/store_app/my_apps/my_apps_page.dart @@ -18,7 +18,7 @@ class MyAppsPage extends StatelessWidget { tabIcons: const [ YaruIcons.package_snap, YaruIcons.package_deb, - YaruIcons.computer + YaruIcons.synchronizing ], tabTitles: [ context.l10n.snapPackages,