diff --git a/apps/weblibre/lib/core/routing/routes.addons.dart b/apps/weblibre/lib/core/routing/routes.addons.dart index c43dded7..688871cb 100644 --- a/apps/weblibre/lib/core/routing/routes.addons.dart +++ b/apps/weblibre/lib/core/routing/routes.addons.dart @@ -27,6 +27,10 @@ part of 'routes.dart'; name: 'AddonDetailsRoute', path: 'details/:addonId', ), + TypedGoRoute( + name: 'AddonListingDetailsRoute', + path: 'listing/:addonId', + ), TypedGoRoute( name: 'AddonPermissionsRoute', path: 'permissions/:addonId', @@ -57,6 +61,19 @@ class AddonDetailsRoute extends GoRouteData with $AddonDetailsRoute { } } +class AddonListingDetailsRoute extends GoRouteData + with $AddonListingDetailsRoute { + final String addonId; + final AddonListing $extra; + + const AddonListingDetailsRoute({required this.addonId, required this.$extra}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return AddonListingDetailsScreen(listing: $extra); + } +} + class AddonPermissionsRoute extends GoRouteData with $AddonPermissionsRoute { final String addonId; diff --git a/apps/weblibre/lib/core/routing/routes.dart b/apps/weblibre/lib/core/routing/routes.dart index 3b854623..c5cb4707 100644 --- a/apps/weblibre/lib/core/routing/routes.dart +++ b/apps/weblibre/lib/core/routing/routes.dart @@ -30,6 +30,7 @@ import 'package:weblibre/domain/entities/profile.dart'; import 'package:weblibre/features/about/presentation/screens/about.dart'; import 'package:weblibre/features/addons/presentation/screens/addon_details.dart'; import 'package:weblibre/features/addons/presentation/screens/addon_internal_settings.dart'; +import 'package:weblibre/features/addons/presentation/screens/addon_listing_details.dart'; import 'package:weblibre/features/addons/presentation/screens/addon_manager.dart'; import 'package:weblibre/features/addons/presentation/screens/addon_permissions.dart'; import 'package:weblibre/features/bangs/data/models/bang.dart'; diff --git a/apps/weblibre/lib/core/routing/routes.g.dart b/apps/weblibre/lib/core/routing/routes.g.dart index 0fa879a2..8fa0c258 100644 --- a/apps/weblibre/lib/core/routing/routes.g.dart +++ b/apps/weblibre/lib/core/routing/routes.g.dart @@ -942,6 +942,11 @@ RouteBase get $addonManagerRoute => GoRouteData.$route( name: 'AddonDetailsRoute', factory: $AddonDetailsRoute._fromState, ), + GoRouteData.$route( + path: 'listing/:addonId', + name: 'AddonListingDetailsRoute', + factory: $AddonListingDetailsRoute._fromState, + ), GoRouteData.$route( path: 'permissions/:addonId', name: 'AddonPermissionsRoute', @@ -1001,6 +1006,36 @@ mixin $AddonDetailsRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } +mixin $AddonListingDetailsRoute on GoRouteData { + static AddonListingDetailsRoute _fromState(GoRouterState state) => + AddonListingDetailsRoute( + addonId: state.pathParameters['addonId']!, + $extra: state.extra as AddonListing, + ); + + AddonListingDetailsRoute get _self => this as AddonListingDetailsRoute; + + @override + String get location => GoRouteData.$location( + '/addons/listing/${Uri.encodeComponent(_self.addonId)}', + ); + + @override + void go(BuildContext context) => context.go(location, extra: _self.$extra); + + @override + Future push(BuildContext context) => + context.push(location, extra: _self.$extra); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: _self.$extra); + + @override + void replace(BuildContext context) => + context.replace(location, extra: _self.$extra); +} + mixin $AddonPermissionsRoute on GoRouteData { static AddonPermissionsRoute _fromState(GoRouterState state) => AddonPermissionsRoute(addonId: state.pathParameters['addonId']!); diff --git a/apps/weblibre/lib/features/addons/domain/providers.dart b/apps/weblibre/lib/features/addons/domain/providers.dart index 1128d0cd..f7ebfad4 100644 --- a/apps/weblibre/lib/features/addons/domain/providers.dart +++ b/apps/weblibre/lib/features/addons/domain/providers.dart @@ -23,6 +23,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:riverpod/experimental/persist.dart'; import 'package:riverpod_annotation/experimental/persist.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/addons/utils/addon_html.dart'; import 'package:weblibre/features/geckoview/domain/providers.dart'; import 'package:weblibre/features/user/data/providers.dart'; @@ -137,6 +138,50 @@ Future addonStoreInfo(Ref ref, String addonId) { return ref.read(addonServiceProvider).getAddonStoreInfo(addonId); } +@Riverpod() +Future> featuredAddonListings(Ref ref, AddonStoreApp app) { + return ref.read(addonServiceProvider).getFeaturedAddonListings(app: app); +} + +@Riverpod() +Future> searchAddonListings( + Ref ref, + String query, + AddonStoreApp app, +) async { + final trimmed = query.trim(); + if (trimmed.isEmpty) { + return ref.watch(featuredAddonListingsProvider(app).future); + } + return ref.read(addonServiceProvider).searchAddonListings( + query: trimmed, + app: app, + ); +} + +@Riverpod(keepAlive: true) +class AddonStoreAppFilter extends _$AddonStoreAppFilter { + void setApp(AddonStoreApp app) => state = app; + + @override + AddonStoreApp build() => AddonStoreApp.android; +} + +@Riverpod() +Future addonDescriptionMarkdown(Ref ref, String addonId) async { + final description = await ref.watch( + addonDetailsProvider( + addonId, + ).selectAsync((addon) => addon?.description ?? ''), + ); + return turndownAddonHtml(description); +} + +@Riverpod() +Future addonHtmlMarkdown(Ref ref, String html) { + return turndownAddonHtml(html); +} + @Riverpod() Future lastAddonUpdateAttempt( Ref ref, diff --git a/apps/weblibre/lib/features/addons/domain/providers.g.dart b/apps/weblibre/lib/features/addons/domain/providers.g.dart index 81226298..d3913feb 100644 --- a/apps/weblibre/lib/features/addons/domain/providers.g.dart +++ b/apps/weblibre/lib/features/addons/domain/providers.g.dart @@ -173,6 +173,360 @@ final class AddonStoreInfoFamily extends $Family String toString() => r'addonStoreInfoProvider'; } +@ProviderFor(featuredAddonListings) +final featuredAddonListingsProvider = FeaturedAddonListingsFamily._(); + +final class FeaturedAddonListingsProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + FeaturedAddonListingsProvider._({ + required FeaturedAddonListingsFamily super.from, + required AddonStoreApp super.argument, + }) : super( + retry: null, + name: r'featuredAddonListingsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$featuredAddonListingsHash(); + + @override + String toString() { + return r'featuredAddonListingsProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + final argument = this.argument as AddonStoreApp; + return featuredAddonListings(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is FeaturedAddonListingsProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$featuredAddonListingsHash() => + r'94f2eb452297611d5b91ac827cb73045cc553826'; + +final class FeaturedAddonListingsFamily extends $Family + with + $FunctionalFamilyOverride>, AddonStoreApp> { + FeaturedAddonListingsFamily._() + : super( + retry: null, + name: r'featuredAddonListingsProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + FeaturedAddonListingsProvider call(AddonStoreApp app) => + FeaturedAddonListingsProvider._(argument: app, from: this); + + @override + String toString() => r'featuredAddonListingsProvider'; +} + +@ProviderFor(searchAddonListings) +final searchAddonListingsProvider = SearchAddonListingsFamily._(); + +final class SearchAddonListingsProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + SearchAddonListingsProvider._({ + required SearchAddonListingsFamily super.from, + required (String, AddonStoreApp) super.argument, + }) : super( + retry: null, + name: r'searchAddonListingsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$searchAddonListingsHash(); + + @override + String toString() { + return r'searchAddonListingsProvider' + '' + '$argument'; + } + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + final argument = this.argument as (String, AddonStoreApp); + return searchAddonListings(ref, argument.$1, argument.$2); + } + + @override + bool operator ==(Object other) { + return other is SearchAddonListingsProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$searchAddonListingsHash() => + r'e20c27fb597093b92f8f4d402f196cc707a3e928'; + +final class SearchAddonListingsFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr>, + (String, AddonStoreApp) + > { + SearchAddonListingsFamily._() + : super( + retry: null, + name: r'searchAddonListingsProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + SearchAddonListingsProvider call(String query, AddonStoreApp app) => + SearchAddonListingsProvider._(argument: (query, app), from: this); + + @override + String toString() => r'searchAddonListingsProvider'; +} + +@ProviderFor(AddonStoreAppFilter) +final addonStoreAppFilterProvider = AddonStoreAppFilterProvider._(); + +final class AddonStoreAppFilterProvider + extends $NotifierProvider { + AddonStoreAppFilterProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'addonStoreAppFilterProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$addonStoreAppFilterHash(); + + @$internal + @override + AddonStoreAppFilter create() => AddonStoreAppFilter(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AddonStoreApp value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$addonStoreAppFilterHash() => + r'167303f223c14ce2c118aa5b76342cc3433dc8f0'; + +abstract class _$AddonStoreAppFilter extends $Notifier { + AddonStoreApp build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + AddonStoreApp, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} + +@ProviderFor(addonDescriptionMarkdown) +final addonDescriptionMarkdownProvider = AddonDescriptionMarkdownFamily._(); + +final class AddonDescriptionMarkdownProvider + extends $FunctionalProvider, String, FutureOr> + with $FutureModifier, $FutureProvider { + AddonDescriptionMarkdownProvider._({ + required AddonDescriptionMarkdownFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'addonDescriptionMarkdownProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$addonDescriptionMarkdownHash(); + + @override + String toString() { + return r'addonDescriptionMarkdownProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String; + return addonDescriptionMarkdown(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is AddonDescriptionMarkdownProvider && + other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$addonDescriptionMarkdownHash() => + r'e60659ccef172237e44b9f77b79df4fe11997617'; + +final class AddonDescriptionMarkdownFamily extends $Family + with $FunctionalFamilyOverride, String> { + AddonDescriptionMarkdownFamily._() + : super( + retry: null, + name: r'addonDescriptionMarkdownProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + AddonDescriptionMarkdownProvider call(String addonId) => + AddonDescriptionMarkdownProvider._(argument: addonId, from: this); + + @override + String toString() => r'addonDescriptionMarkdownProvider'; +} + +@ProviderFor(addonHtmlMarkdown) +final addonHtmlMarkdownProvider = AddonHtmlMarkdownFamily._(); + +final class AddonHtmlMarkdownProvider + extends $FunctionalProvider, String, FutureOr> + with $FutureModifier, $FutureProvider { + AddonHtmlMarkdownProvider._({ + required AddonHtmlMarkdownFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'addonHtmlMarkdownProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$addonHtmlMarkdownHash(); + + @override + String toString() { + return r'addonHtmlMarkdownProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String; + return addonHtmlMarkdown(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is AddonHtmlMarkdownProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$addonHtmlMarkdownHash() => r'51d6c7d7cf6040acc9540893dacad581606ff4a8'; + +final class AddonHtmlMarkdownFamily extends $Family + with $FunctionalFamilyOverride, String> { + AddonHtmlMarkdownFamily._() + : super( + retry: null, + name: r'addonHtmlMarkdownProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + AddonHtmlMarkdownProvider call(String html) => + AddonHtmlMarkdownProvider._(argument: html, from: this); + + @override + String toString() => r'addonHtmlMarkdownProvider'; +} + @ProviderFor(lastAddonUpdateAttempt) final lastAddonUpdateAttemptProvider = LastAddonUpdateAttemptFamily._(); diff --git a/apps/weblibre/lib/features/addons/presentation/screens/addon_browse.dart b/apps/weblibre/lib/features/addons/presentation/screens/addon_browse.dart new file mode 100644 index 00000000..c24231f8 --- /dev/null +++ b/apps/weblibre/lib/features/addons/presentation/screens/addon_browse.dart @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'dart:async'; + +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/addons/domain/providers.dart'; +import 'package:weblibre/features/addons/presentation/widgets/addon_listing_card.dart'; + +class AddonBrowseView extends HookConsumerWidget { + const AddonBrowseView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final app = ref.watch(addonStoreAppFilterProvider); + + final searchController = useTextEditingController(); + final query = useState(''); + final debounceTimer = useRef(null); + + useEffect( + () => + () => debounceTimer.value?.cancel(), + const [], + ); + + final listingsAsync = ref.watch( + searchAddonListingsProvider(query.value, app), + ); + + final installed = ref.watch( + addonListProvider.select( + (value) => + value.value?.where((a) => a.isInstalled).map((a) => a.id).toSet() ?? + const {}, + ), + ); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: AddonStoreApp.android, + icon: Icon(Icons.phone_android), + label: Text('Android'), + ), + ButtonSegment( + value: AddonStoreApp.firefox, + icon: Icon(Icons.desktop_windows), + label: Text('Desktop'), + ), + ], + selected: {app}, + onSelectionChanged: (selection) => ref + .read(addonStoreAppFilterProvider.notifier) + .setApp(selection.first), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: TextField( + controller: searchController, + decoration: InputDecoration( + hintText: 'Search addons.mozilla.org', + prefixIcon: const Icon(Icons.search), + suffixIcon: query.value.isEmpty + ? null + : IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + searchController.clear(); + query.value = ''; + }, + ), + border: const OutlineInputBorder(), + isDense: true, + ), + onChanged: (text) { + debounceTimer.value?.cancel(); + debounceTimer.value = Timer( + const Duration(milliseconds: 400), + () => query.value = text, + ); + }, + ), + ), + if (app == AddonStoreApp.firefox) + const Padding( + padding: EdgeInsets.fromLTRB(16, 0, 16, 12), + child: _DesktopCompatibilityWarning(), + ), + Expanded( + child: listingsAsync.when( + skipLoadingOnReload: true, + data: (listings) => + _ListingList(listings: listings, installedIds: installed), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, size: 40), + const SizedBox(height: 12), + const Text('Failed to load extensions'), + const SizedBox(height: 8), + Text(error.toString(), textAlign: TextAlign.center), + ], + ), + ), + ), + ), + ), + ], + ); + } +} + +class _DesktopCompatibilityWarning extends StatelessWidget { + const _DesktopCompatibilityWarning(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.tertiaryContainer.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: theme.colorScheme.tertiary), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: theme.colorScheme.tertiary, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Desktop extensions are not reviewed for mobile. Some may not ' + 'work, may crash, or may behave unexpectedly on Android.', + style: TextStyle( + color: theme.colorScheme.onTertiaryContainer, + fontSize: 12, + ), + ), + ), + ], + ), + ); + } +} + +class _ListingList extends StatelessWidget { + final List listings; + final Set installedIds; + + const _ListingList({required this.listings, required this.installedIds}); + + @override + Widget build(BuildContext context) { + if (listings.isEmpty) { + return const Center(child: Text('No extensions found.')); + } + + return FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView.builder( + controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: listings.length, + itemBuilder: (context, index) { + final listing = listings[index]; + return AddonListingCard( + listing: listing, + isInstalled: installedIds.contains(listing.id), + onTap: () async { + await AddonListingDetailsRoute( + addonId: listing.id, + $extra: listing, + ).push(context); + }, + ); + }, + ); + }, + ); + } +} diff --git a/apps/weblibre/lib/features/addons/presentation/screens/addon_details.dart b/apps/weblibre/lib/features/addons/presentation/screens/addon_details.dart index 6ba1f68f..f9a24a3b 100644 --- a/apps/weblibre/lib/features/addons/presentation/screens/addon_details.dart +++ b/apps/weblibre/lib/features/addons/presentation/screens/addon_details.dart @@ -18,6 +18,7 @@ * along with this program. If not, see . */ import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -27,6 +28,7 @@ import 'package:weblibre/features/addons/extensions/addon_info.dart'; import 'package:weblibre/features/addons/presentation/screens/addon_internal_settings.dart'; import 'package:weblibre/features/addons/presentation/widgets/addon_ui.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart'; +import 'package:weblibre/utils/number_format.dart'; import 'package:weblibre/utils/ui_helper.dart'; class AddonDetailsScreen extends ConsumerWidget { @@ -163,6 +165,7 @@ class _AddonHeader extends StatelessWidget { final theme = Theme.of(context); return Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -188,7 +191,6 @@ class _AddonHeader extends StatelessWidget { const SizedBox(height: 12), Wrap( spacing: 8, - runSpacing: 8, children: [ Chip( label: Text( @@ -204,7 +206,7 @@ class _AddonHeader extends StatelessWidget { avatar: const Icon(Icons.star, size: 18), label: Text( '${addon.ratingAverage!.toStringAsFixed(1)}' - ' (${addon.ratingReviews ?? 0})', + ' (${formatCompactNumber(addon.ratingReviews ?? 0)})', ), ), ], @@ -272,6 +274,7 @@ class _ManagementSection extends ConsumerWidget { Text('Management', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, child: Column( children: [ if (addon.isSupported) @@ -423,7 +426,7 @@ class _UpdatesSection extends ConsumerWidget { addon.installedVersion != availableVersion; return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('Updates', style: theme.textTheme.titleMedium), const SizedBox(height: 8), @@ -566,20 +569,42 @@ void _reportUpdateResult( } } -class _DescriptionCard extends StatelessWidget { +class _DescriptionCard extends ConsumerWidget { final AddonInfo addon; const _DescriptionCard({required this.addon}); @override - Widget build(BuildContext context) { - final description = addon.description; + Widget build(BuildContext context, WidgetRef ref) { + final markdownAsync = ref.watch(addonDescriptionMarkdownProvider(addon.id)); return Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, child: Padding( padding: const EdgeInsets.all(16), - child: Text( - description.isNotEmpty ? description : 'No description provided.', + child: markdownAsync.when( + skipLoadingOnReload: true, + data: (markdown) => markdown.isEmpty + ? const Text('No description provided.') + : MarkdownBody( + data: markdown, + selectable: true, + onTapLink: (text, href, title) { + if (href != null && href.isNotEmpty) { + launchUrl(Uri.parse(href)); + } + }, + ), + loading: () => Text( + addon.description.isNotEmpty + ? addon.description + : 'Loading description…', + ), + error: (_, _) => Text( + addon.description.isNotEmpty + ? addon.description + : 'No description provided.', + ), ), ), ); @@ -594,6 +619,7 @@ class _DetailsCard extends StatelessWidget { @override Widget build(BuildContext context) { return Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, child: Column( children: [ if ((addon.authorName ?? '').isNotEmpty) diff --git a/apps/weblibre/lib/features/addons/presentation/screens/addon_listing_details.dart b/apps/weblibre/lib/features/addons/presentation/screens/addon_listing_details.dart new file mode 100644 index 00000000..0f2de31e --- /dev/null +++ b/apps/weblibre/lib/features/addons/presentation/screens/addon_listing_details.dart @@ -0,0 +1,648 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:weblibre/features/addons/domain/providers.dart'; +import 'package:weblibre/features/addons/presentation/widgets/addon_listing_card.dart'; +import 'package:weblibre/features/addons/utils/permissions.dart'; +import 'package:weblibre/features/geckoview/domain/providers.dart'; +import 'package:weblibre/utils/number_format.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +class AddonListingDetailsScreen extends ConsumerWidget { + final AddonListing listing; + + const AddonListingDetailsScreen({required this.listing, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + + final installedAsync = ref.watch(addonListProvider); + + final isInstalled = installedAsync.maybeWhen( + data: (addons) => addons.any((a) => a.id == listing.id && a.isInstalled), + orElse: () => false, + ); + + return Scaffold( + appBar: AppBar(title: Text(listing.name)), + body: SafeArea( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.all(16), + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AddonListingIcon(iconUrl: listing.iconUrl, size: 64), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(listing.name, style: theme.textTheme.titleLarge), + if (listing.authorName != null) ...[ + const SizedBox(height: 4), + _AuthorLink( + name: listing.authorName!, + url: listing.authorUrl, + ), + ], + const SizedBox(height: 8), + Text( + 'Version ${listing.latestVersion}', + style: theme.textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + _InstallButton(listing: listing, isInstalled: isInstalled), + const SizedBox(height: 16), + Wrap( + spacing: 8, + children: [ + if (listing.promoted == AddonStorePromoted.recommended) + const Chip( + avatar: Icon(Icons.verified, size: 16), + label: Text('Recommended'), + ), + if (listing.ratingAverage != null) + Chip( + avatar: const Icon(Icons.star, size: 16), + label: Text( + '${listing.ratingAverage!.toStringAsFixed(1)}' + '${listing.ratingReviews != null ? ' (${formatCompactNumber(listing.ratingReviews!)})' : ''}', + ), + ), + if (listing.averageDailyUsers != null) + Chip( + avatar: const Icon(Icons.group_outlined, size: 16), + label: Text( + '${formatCompactNumber(listing.averageDailyUsers!)} users', + ), + ), + ], + ), + if (listing.previews.isNotEmpty) ...[ + const SizedBox(height: 24), + _ScreenshotsSection(previews: listing.previews), + ], + if ((listing.summary ?? '').isNotEmpty) ...[ + const SizedBox(height: 16), + Text(listing.summary!, style: theme.textTheme.bodyLarge), + ], + if ((listing.description ?? '').isNotEmpty) ...[ + const SizedBox(height: 16), + const _SectionHeader(title: 'About this extension'), + const SizedBox(height: 8), + _ExpandableDescription(html: listing.description!), + ], + if (_hasFriendlyPermissions(listing)) ...[ + const SizedBox(height: 24), + const _SectionHeader(title: 'Permissions'), + const SizedBox(height: 8), + _PermissionsSection(listing: listing), + ], + if (_hasTechnicalPermissions(listing)) ...[ + const SizedBox(height: 24), + const _SectionHeader(title: 'Technical permissions'), + const SizedBox(height: 8), + _TechnicalPermissionsSection(listing: listing), + ], + const SizedBox(height: 24), + const _SectionHeader(title: 'More information'), + const SizedBox(height: 8), + _MoreInformationSection(listing: listing), + ], + ); + }, + ), + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + final String title; + const _SectionHeader({required this.title}); + + @override + Widget build(BuildContext context) { + return Text(title, style: Theme.of(context).textTheme.titleMedium); + } +} + +class _AuthorLink extends StatelessWidget { + final String name; + final String? url; + + const _AuthorLink({required this.name, required this.url}); + + @override + Widget build(BuildContext context) { + final style = Theme.of(context).textTheme.bodyMedium; + if (url == null) return Text('by $name', style: style); + return InkWell( + onTap: () => launchUrl(Uri.parse(url!)), + child: Text( + 'by $name', + style: style?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ); + } +} + +class _InstallButton extends ConsumerWidget { + final AddonListing listing; + final bool isInstalled; + + const _InstallButton({required this.listing, required this.isInstalled}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final busy = ref.watch(addonBusyIdsProvider).contains(listing.id); + + if (isInstalled) { + return FilledButton.icon( + onPressed: null, + icon: const Icon(Icons.check), + label: const Text('Installed'), + ); + } + + return FilledButton.icon( + onPressed: busy + ? null + : () async { + ref.read(addonBusyIdsProvider.notifier).add(listing.id); + try { + await ref + .read(addonServiceProvider) + .installAddon(Uri.parse(listing.downloadUrl)); + ref.invalidate(addonListProvider); + ref.invalidate(addonDetailsProvider(listing.id)); + if (!context.mounted) return; + showInfoMessage(context, '${listing.name} installed'); + } catch (error) { + if (!context.mounted) return; + showInfoMessage(context, 'Install failed: $error'); + } finally { + ref.read(addonBusyIdsProvider.notifier).remove(listing.id); + } + }, + icon: busy + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.download_outlined), + label: const Text('Install'), + ); + } +} + +class _ScreenshotsSection extends StatelessWidget { + final List previews; + const _ScreenshotsSection({required this.previews}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 200, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: previews.length, + separatorBuilder: (_, _) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final p = previews[index]; + return GestureDetector( + onTap: () => _showFullScreenImage(context, p.imageUrl), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + p.thumbnailUrl ?? p.imageUrl, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => Container( + width: 300, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + child: const Icon(Icons.broken_image_outlined), + ), + ), + ), + ); + }, + ), + ); + } + + void _showFullScreenImage(BuildContext context, String url) { + Navigator.of(context).push( + PageRouteBuilder( + opaque: false, + barrierColor: Colors.black87, + pageBuilder: (_, _, _) => _FullScreenImage(url: url), + ), + ); + } +} + +class _FullScreenImage extends StatelessWidget { + final String url; + const _FullScreenImage({required this.url}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Center( + child: InteractiveViewer( + child: Image.network(url, fit: BoxFit.contain), + ), + ), + ), + ); + } +} + +class _ExpandableDescription extends HookConsumerWidget { + final String html; + const _ExpandableDescription({required this.html}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final expanded = useState(false); + final markdownAsync = ref.watch(addonHtmlMarkdownProvider(html)); + final body = markdownAsync.when( + skipLoadingOnReload: true, + data: (markdown) => MarkdownBody( + data: markdown.isEmpty ? html : markdown, + onTapLink: (_, href, _) { + if (href != null) launchUrl(Uri.parse(href)); + }, + ), + loading: () => Text(html), + error: (_, _) => Text(html), + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedSize( + duration: const Duration(milliseconds: 200), + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: expanded.value ? double.infinity : 160, + ), + child: ShaderMask( + shaderCallback: (bounds) { + if (expanded.value) { + return const LinearGradient( + colors: [Colors.black, Colors.black], + ).createShader(bounds); + } + return const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black, Colors.black, Colors.transparent], + stops: [0.0, 0.75, 1.0], + ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: body, + ), + ), + ), + ), + TextButton( + onPressed: () => expanded.value = !expanded.value, + child: Text(expanded.value ? 'Show less' : 'Read more'), + ), + ], + ); + } +} + +typedef _PermissionGroup = ({String title, List perms}); + +class _PermissionsSection extends StatelessWidget { + final AddonListing listing; + const _PermissionsSection({required this.listing}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final items = []; + + final groups = <_PermissionGroup>[ + (title: 'Required', perms: listing.permissions), + (title: 'Websites', perms: listing.hostPermissions), + (title: 'Optional', perms: listing.optionalPermissions), + (title: 'Data collection', perms: listing.dataCollectionPermissions), + ]; + + for (final group in groups) { + final friendly = group.perms + .map(describePermission) + .where((d) => !d.technical) + .toList(); + if (friendly.isEmpty) continue; + items.add( + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 4), + child: Text(group.title, style: theme.textTheme.titleSmall), + ), + ); + for (final d in friendly) { + items.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Text('\u2022 ${d.text}', style: theme.textTheme.bodyMedium), + ), + ); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items, + ); + } +} + +class _TechnicalPermissionsSection extends StatelessWidget { + final AddonListing listing; + const _TechnicalPermissionsSection({required this.listing}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final items = []; + + final groups = <_PermissionGroup>[ + (title: 'Required', perms: listing.permissions), + (title: 'Websites', perms: listing.hostPermissions), + (title: 'Optional', perms: listing.optionalPermissions), + (title: 'Data collection', perms: listing.dataCollectionPermissions), + ]; + + final monoStyle = TextStyle( + fontFamily: 'monospace', + fontSize: (theme.textTheme.bodyMedium?.fontSize ?? 14) - 1, + color: theme.colorScheme.onSurfaceVariant, + ); + + for (final group in groups) { + final technical = group.perms + .map(describePermission) + .where((d) => d.technical) + .toList(); + if (technical.isEmpty) continue; + items.add( + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 4), + child: Text(group.title, style: theme.textTheme.titleSmall), + ), + ); + for (final d in technical) { + items.add( + Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Text.rich( + TextSpan( + children: [ + const TextSpan(text: '\u2022 '), + TextSpan(text: d.text, style: monoStyle), + ], + style: theme.textTheme.bodyMedium, + ), + ), + ), + ); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items, + ); + } +} + +bool _hasTechnicalPermissions(AddonListing l) { + bool any(List list) => + list.any((p) => describePermission(p).technical); + return any(l.permissions) || + any(l.hostPermissions) || + any(l.optionalPermissions) || + any(l.dataCollectionPermissions); +} + +bool _hasFriendlyPermissions(AddonListing l) { + bool any(List list) => + list.any((p) => !describePermission(p).technical); + return any(l.permissions) || + any(l.hostPermissions) || + any(l.optionalPermissions) || + any(l.dataCollectionPermissions); +} + +class _MoreInformationSection extends StatelessWidget { + final AddonListing listing; + const _MoreInformationSection({required this.listing}); + + @override + Widget build(BuildContext context) { + final rows = []; + + rows.add(_InfoRow(label: 'Version', value: listing.latestVersion)); + + if (listing.fileSize != null) { + rows.add(_InfoRow(label: 'Size', value: formatBytes(listing.fileSize!))); + } + + if (listing.lastUpdated != null) { + rows.add( + _InfoRow( + label: 'Last updated', + value: formatIsoDate(listing.lastUpdated!), + ), + ); + } + + if (listing.categories.isNotEmpty) { + rows.add( + _InfoRow(label: 'Categories', value: listing.categories.join(', ')), + ); + } + + if (listing.licenseName != null) { + rows.add( + _InfoRow( + label: 'License', + value: listing.licenseName!, + url: listing.licenseUrl, + ), + ); + } + + final links = []; + if (listing.homepageUrl != null) { + links.add( + _LinkTile( + icon: Icons.home_outlined, + label: 'Homepage', + url: listing.homepageUrl!, + ), + ); + } + if (listing.supportUrl != null) { + links.add( + _LinkTile( + icon: Icons.help_outline, + label: 'Support site', + url: listing.supportUrl!, + ), + ); + } + if (listing.supportEmail != null) { + links.add( + _LinkTile( + icon: Icons.email_outlined, + label: listing.supportEmail!, + url: 'mailto:${listing.supportEmail!}', + ), + ); + } + links.add( + _LinkTile( + icon: Icons.public, + label: 'View on addons.mozilla.org', + url: listing.detailUrl, + ), + ); + if (listing.ratingUrl != null) { + links.add( + _LinkTile( + icon: Icons.reviews_outlined, + label: 'Reviews', + url: listing.ratingUrl!, + ), + ); + } + if (listing.hasPrivacyPolicy && listing.slug != null) { + links.add( + _LinkTile( + icon: Icons.privacy_tip_outlined, + label: 'Privacy policy', + url: 'https://addons.mozilla.org/addon/${listing.slug}/privacy/', + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [...rows, const SizedBox(height: 8), ...links], + ); + } +} + +class _InfoRow extends StatelessWidget { + final String label; + final String value; + final String? url; + + const _InfoRow({required this.label, required this.value, this.url}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final valueWidget = url != null + ? InkWell( + onTap: () => launchUrl(Uri.parse(url!)), + child: Text( + value, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ) + : Text(value, style: theme.textTheme.bodyMedium); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 120, + child: Text( + label, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded(child: valueWidget), + ], + ), + ); + } +} + +class _LinkTile extends StatelessWidget { + final IconData icon; + final String label; + final String url; + + const _LinkTile({required this.icon, required this.label, required this.url}); + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: EdgeInsets.zero, + leading: Icon(icon), + title: Text(label), + trailing: const Icon(Icons.open_in_new, size: 18), + onTap: () => launchUrl(Uri.parse(url)), + ); + } +} diff --git a/apps/weblibre/lib/features/addons/presentation/screens/addon_manager.dart b/apps/weblibre/lib/features/addons/presentation/screens/addon_manager.dart index 02b2a3d9..258c11fe 100644 --- a/apps/weblibre/lib/features/addons/presentation/screens/addon_manager.dart +++ b/apps/weblibre/lib/features/addons/presentation/screens/addon_manager.dart @@ -17,13 +17,16 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/addons/domain/providers.dart'; import 'package:weblibre/features/addons/extensions/addon_info.dart'; +import 'package:weblibre/features/addons/presentation/screens/addon_browse.dart'; import 'package:weblibre/features/addons/presentation/widgets/addon_ui.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/install_local_addon_dialog.dart'; import 'package:weblibre/utils/ui_helper.dart'; class AddonManagerScreen extends ConsumerWidget { @@ -35,67 +38,107 @@ class AddonManagerScreen extends ConsumerWidget { Future refresh() => ref.read(addonListProvider.notifier).refresh(); - return Scaffold( - appBar: AppBar( - title: const Text('Extensions'), - actions: [ - IconButton( - onPressed: addonsAsync.isLoading ? null : refresh, - icon: const Icon(Icons.refresh), + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text('Extensions'), + bottom: const TabBar( + tabs: [ + Tab(text: 'Installed'), + Tab(text: 'Browse'), + ], ), - _TriggerAllUpdatesButton( - enabled: addonsAsync.maybeWhen( - data: (addons) => - addons.any((a) => a.isInstalled && a.isSupported), - orElse: () => false, + actions: [ + IconButton( + onPressed: addonsAsync.isLoading ? null : refresh, + icon: const Icon(Icons.refresh), ), - ), - ], - ), - body: addonsAsync.when( - skipLoadingOnReload: true, - skipError: true, - data: (addons) => RefreshIndicator( - onRefresh: refresh, - child: _AddonList(addons: addons), + _AddonManagerOverflowMenu( + canCheckForUpdates: addonsAsync.maybeWhen( + data: (addons) => + addons.any((a) => a.isInstalled && a.isSupported), + orElse: () => false, + ), + ), + ], + ), + body: SafeArea( + child: TabBarView( + children: [ + addonsAsync.when( + skipLoadingOnReload: true, + skipError: true, + data: (addons) => RefreshIndicator( + onRefresh: refresh, + child: _AddonList(addons: addons), + ), + error: (error, _) => + _AddonLoadError(error: error, onRetry: refresh), + loading: () => const Center(child: CircularProgressIndicator()), + ), + const AddonBrowseView(), + ], + ), ), - error: (error, _) => _AddonLoadError(error: error, onRetry: refresh), - loading: () => const Center(child: CircularProgressIndicator()), ), ); } } -class _TriggerAllUpdatesButton extends ConsumerWidget { - final bool enabled; +enum _AddonManagerMenuAction { checkForUpdates, installFromFile } - const _TriggerAllUpdatesButton({required this.enabled}); +class _AddonManagerOverflowMenu extends ConsumerWidget { + final bool canCheckForUpdates; + + const _AddonManagerOverflowMenu({required this.canCheckForUpdates}); @override Widget build(BuildContext context, WidgetRef ref) { - final busy = ref.watch( + final updatesBusy = ref.watch( bulkAddonUpdateProvider.select((value) => value.isLoading), ); - return IconButton( - onPressed: enabled && !busy - ? () async { - await ref.read(bulkAddonUpdateProvider.notifier).triggerAll(); - if (!context.mounted) return; - showInfoMessage( - context, - 'Background update checks started for installed extensions', - ); - } - : null, - icon: busy + return PopupMenuButton<_AddonManagerMenuAction>( + icon: updatesBusy ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Icon(Icons.system_update_alt), - tooltip: 'Check all installed extensions for updates', + : const Icon(Icons.more_vert), + onSelected: (action) async { + switch (action) { + case _AddonManagerMenuAction.checkForUpdates: + await ref.read(bulkAddonUpdateProvider.notifier).triggerAll(); + if (!context.mounted) return; + showInfoMessage( + context, + 'Background update checks started for installed extensions', + ); + case _AddonManagerMenuAction.installFromFile: + await showInstallLocalAddonDialog(context); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: _AddonManagerMenuAction.checkForUpdates, + enabled: canCheckForUpdates && !updatesBusy, + child: const ListTile( + leading: Icon(Icons.system_update_alt), + title: Text('Check for updates'), + contentPadding: EdgeInsets.zero, + ), + ), + const PopupMenuItem( + value: _AddonManagerMenuAction.installFromFile, + child: ListTile( + leading: Icon(Icons.file_open), + title: Text('Install from file'), + contentPadding: EdgeInsets.zero, + ), + ), + ], ); } } @@ -113,78 +156,50 @@ class _AddonList extends StatelessWidget { final disabled = addons .where((a) => a.isInstalled && a.isSupported && !a.isEnabled) .toList(); - final recommended = addons.where((a) => !a.isInstalled).toList(); final unsupported = addons .where((a) => a.isInstalled && !a.isSupported) .toList(); + final installed = enabled.length + disabled.length + unsupported.length; - return ListView( - padding: const EdgeInsets.all(16), - children: [ - const Card( - child: ListTile( - leading: Icon(Icons.info_outline), - title: Text('Addon updates run in the background'), - subtitle: Text( - 'Use each extension detail screen to view its last update result or trigger a manual check.', - ), - ), - ), - if (enabled.isNotEmpty) ...[ - const SizedBox(height: 16), - const _Section(title: 'Enabled'), - for (final addon in enabled) _AddonCard(addon: addon), - ], - if (disabled.isNotEmpty) ...[ - const SizedBox(height: 16), - const _Section(title: 'Disabled'), - for (final addon in disabled) _AddonCard(addon: addon), - ], - if (recommended.isNotEmpty) ...[ - const SizedBox(height: 16), - const _Section(title: 'Available'), - for (final addon in recommended) - _AddonCard( - addon: addon, - action: _InstallAction(addon: addon), - ), - ], - if (unsupported.isNotEmpty) ...[ - const SizedBox(height: 16), - const _Section(title: 'Unsupported'), - for (final addon in unsupported) - _AddonCard( - addon: addon, - action: _UninstallAction(addon: addon), - ), - ], - if (addons.isEmpty) - const Padding( - padding: EdgeInsets.only(top: 48), - child: Center(child: Text('No extensions available right now.')), - ), - ], - ); - } -} - -class _InstallAction extends ConsumerWidget { - final AddonInfo addon; - - const _InstallAction({required this.addon}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final busy = ref.watch(addonBusyIdsProvider).contains(addon.id); - return FilledButton( - onPressed: busy - ? null - : () async { - await ref.read(addonListProvider.notifier).install(addon); - if (!context.mounted) return; - showInfoMessage(context, '${addon.displayName} installed'); - }, - child: const Text('Install'), + return FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + padding: const EdgeInsets.all(16), + children: [ + if (enabled.isNotEmpty) ...[ + const SizedBox(height: 16), + const _Section(title: 'Enabled'), + for (final addon in enabled) _AddonCard(addon: addon), + ], + if (disabled.isNotEmpty) ...[ + const SizedBox(height: 16), + const _Section(title: 'Disabled'), + for (final addon in disabled) _AddonCard(addon: addon), + ], + if (unsupported.isNotEmpty) ...[ + const SizedBox(height: 16), + const _Section(title: 'Unsupported'), + for (final addon in unsupported) + _AddonCard( + addon: addon, + action: _UninstallAction(addon: addon), + ), + ], + if (installed == 0) + const Padding( + padding: EdgeInsets.only(top: 48), + child: Center( + child: Text( + 'No extensions installed yet.\nBrowse the store to find some.', + textAlign: TextAlign.center, + ), + ), + ), + ], + ); + }, ); } } @@ -236,6 +251,7 @@ class _AddonCard extends ConsumerWidget { final busy = ref.watch(addonBusyIdsProvider).contains(addon.id); return Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, margin: const EdgeInsets.only(bottom: 12), child: InkWell( onTap: busy @@ -267,7 +283,6 @@ class _AddonCard extends ConsumerWidget { const SizedBox(height: 8), Wrap( spacing: 8, - runSpacing: 8, children: [ if (addon.isAllowedInPrivateBrowsing) const Chip(label: Text('Private Browsing')), diff --git a/apps/weblibre/lib/features/addons/presentation/screens/addon_permissions.dart b/apps/weblibre/lib/features/addons/presentation/screens/addon_permissions.dart index 58b85bc1..85ee74fe 100644 --- a/apps/weblibre/lib/features/addons/presentation/screens/addon_permissions.dart +++ b/apps/weblibre/lib/features/addons/presentation/screens/addon_permissions.dart @@ -71,8 +71,9 @@ class AddonPermissionsScreen extends ConsumerWidget { padding: const EdgeInsets.all(16), children: [ if (permissions.isEmpty && dataCollection.isEmpty) - const Card( - child: ListTile( + Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: const ListTile( leading: Icon(Icons.verified_user_outlined), title: Text('No special permissions listed'), subtitle: Text( @@ -87,6 +88,7 @@ class AddonPermissionsScreen extends ConsumerWidget { ), const SizedBox(height: 8), Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, child: Column( children: [ for (final permission in permissions) @@ -106,6 +108,7 @@ class AddonPermissionsScreen extends ConsumerWidget { ), const SizedBox(height: 8), Card( + color: Theme.of(context).colorScheme.surfaceContainerHigh, child: Column( children: [ for (final permission in dataCollection) diff --git a/apps/weblibre/lib/features/addons/presentation/widgets/addon_listing_card.dart b/apps/weblibre/lib/features/addons/presentation/widgets/addon_listing_card.dart new file mode 100644 index 00000000..4b16d2a0 --- /dev/null +++ b/apps/weblibre/lib/features/addons/presentation/widgets/addon_listing_card.dart @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter/material.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:weblibre/utils/number_format.dart'; + +class AddonListingIcon extends StatelessWidget { + final String? iconUrl; + final double size; + + const AddonListingIcon({required this.iconUrl, this.size = 40, super.key}); + + @override + Widget build(BuildContext context) { + final borderRadius = BorderRadius.circular(12); + if (iconUrl == null || iconUrl!.isEmpty) { + return _fallback(context); + } + return ClipRRect( + borderRadius: borderRadius, + child: Image.network( + iconUrl!, + width: size, + height: size, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => _fallback(context), + ), + ); + } + + Widget _fallback(BuildContext context) { + final theme = Theme.of(context); + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(Icons.extension, color: theme.colorScheme.onSurfaceVariant), + ); + } +} + +class AddonListingCard extends StatelessWidget { + final AddonListing listing; + final bool isInstalled; + final VoidCallback? onTap; + final Widget? trailing; + + const AddonListingCard({ + required this.listing, + required this.isInstalled, + this.onTap, + this.trailing, + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Card( + color: theme.colorScheme.surfaceContainerHigh, + margin: const EdgeInsets.only(bottom: 12), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AddonListingIcon(iconUrl: listing.iconUrl), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(listing.name, style: theme.textTheme.titleMedium), + if ((listing.summary ?? '').isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + listing.summary!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + if (listing.promoted == AddonStorePromoted.recommended) + const Chip( + avatar: Icon(Icons.verified, size: 16), + label: Text('Recommended'), + ), + if (listing.ratingAverage != null) + Chip( + avatar: const Icon(Icons.star, size: 16), + label: Text( + listing.ratingAverage!.toStringAsFixed(1), + ), + ), + if (listing.averageDailyUsers != null) + Chip( + avatar: const Icon(Icons.group_outlined, size: 16), + label: Text( + formatCompactNumber(listing.averageDailyUsers!), + ), + ), + if (isInstalled) + const Chip( + avatar: Icon(Icons.check, size: 16), + label: Text('Installed'), + ), + ], + ), + ], + ), + ), + const SizedBox(width: 8), + trailing ?? const Icon(Icons.chevron_right), + ], + ), + ), + ), + ); + } +} diff --git a/apps/weblibre/lib/features/addons/utils/addon_html.dart b/apps/weblibre/lib/features/addons/utils/addon_html.dart new file mode 100644 index 00000000..e9b3978b --- /dev/null +++ b/apps/weblibre/lib/features/addons/utils/addon_html.dart @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; + +/// AMO descriptions mix raw HTML tags with entity-escaped ones +/// (e.g. `<b>not</b>` next to `
  • …
`). DOMParser +/// would decode the entities to literal text characters, so turndown would +/// emit them verbatim. Decode entities once here so the escaped tags become +/// real markup before the extension runs. +String _decodeHtmlEntities(String input) { + return input + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(''', "'") + .replaceAll(' ', ' ') + .replaceAll('&', '&'); +} + +Future turndownAddonHtml(String description) async { + if (description.isEmpty) return ''; + + final results = await GeckoBrowserExtensionService.turndownHtml([ + _decodeHtmlEntities(description), + ], timeout: const Duration(seconds: 3)); + + final markdown = results.firstOrNull?.markdown?.trim(); + return (markdown == null || markdown.isEmpty) ? description : markdown; +} diff --git a/apps/weblibre/lib/features/addons/utils/permissions.dart b/apps/weblibre/lib/features/addons/utils/permissions.dart new file mode 100644 index 00000000..480e0b66 --- /dev/null +++ b/apps/weblibre/lib/features/addons/utils/permissions.dart @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +typedef PermissionDescription = ({String text, bool technical}); + +PermissionDescription describePermission(String raw) { + String? mapped; + switch (raw) { + case 'bookmarks': + mapped = 'Read and modify bookmarks'; + case 'browserSettings': + mapped = 'Read and modify browser settings'; + case 'browsingData': + mapped = 'Clear recent browsing history, cookies, and related data'; + case 'clipboardRead': + mapped = 'Read data you copy and paste'; + case 'clipboardWrite': + mapped = 'Input data to the clipboard'; + case 'contextualIdentities': + mapped = 'Access and modify container tabs'; + case 'cookies': + mapped = 'Access cookies for visited sites'; + case 'downloads': + mapped = 'Download files and read/modify download history'; + case 'downloads.open': + mapped = 'Open files downloaded to your computer'; + case 'find': + mapped = 'Read the text of all open tabs'; + case 'geolocation': + mapped = 'Access your location'; + case 'history': + mapped = 'Access browsing history'; + case 'management': + mapped = 'Monitor extension usage and manage themes'; + case 'nativeMessaging': + mapped = 'Exchange messages with programs other than the browser'; + case 'notifications': + mapped = 'Display notifications'; + case 'pkcs11': + mapped = 'Provide cryptographic authentication services'; + case 'privacy': + mapped = 'Read and modify privacy settings'; + case 'proxy': + mapped = 'Control browser proxy settings'; + case 'sessions': + mapped = 'Access recently closed tabs'; + case 'tabs': + mapped = 'Access browser tabs'; + case 'tabHide': + mapped = 'Hide and show browser tabs'; + case 'topSites': + mapped = 'Access browsing history'; + case 'webNavigation': + mapped = 'Access browser activity during navigation'; + case '': + mapped = 'Access your data for all websites'; + } + if (mapped != null) return (text: mapped, technical: false); + if (raw.startsWith('http') || + raw.contains('://') || + raw.contains('*') || + raw.startsWith('file:')) { + return (text: 'Access your data for $raw', technical: false); + } + return (text: raw, technical: true); +} diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart index 7b06732a..b3741385 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart @@ -1809,27 +1809,6 @@ class _ExtensionsCard extends HookConsumerWidget { await const AddonManagerRoute().push(rootContext); }, ), - _buildSubTile( - 'Get Extensions', - icon: MdiIcons.puzzlePlus, - onTap: () async { - Navigator.pop(context); - final tabMode = TabMode.fromTabType( - ref - .read(generalSettingsWithDefaultsProvider) - .effectiveDefaultCreateTabType, - ); - await ref - .read(tabRepositoryProvider.notifier) - .addTab( - url: Uri.parse('https://addons.mozilla.org'), - tabMode: tabMode, - containerSelection: - const TabContainerSelection.unassigned(), - selectTab: true, - ); - }, - ), ], ), ), diff --git a/apps/weblibre/lib/features/intent_gatekeeper/domain/services/intent_gatekeeper.g.dart b/apps/weblibre/lib/features/intent_gatekeeper/domain/services/intent_gatekeeper.g.dart index 50adfc37..4097e913 100644 --- a/apps/weblibre/lib/features/intent_gatekeeper/domain/services/intent_gatekeeper.g.dart +++ b/apps/weblibre/lib/features/intent_gatekeeper/domain/services/intent_gatekeeper.g.dart @@ -33,7 +33,7 @@ final class IntentGatekeeperProvider IntentGatekeeper create() => IntentGatekeeper(); } -String _$intentGatekeeperHash() => r'94df8850478ad6695eb14752e82af1919ea8a077'; +String _$intentGatekeeperHash() => r'0ab4a96dde7a21df5dd32d034f841bf4d40dbb10'; abstract class _$IntentGatekeeper extends $StreamNotifier { diff --git a/apps/weblibre/lib/features/settings/presentation/screens/extensions_settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/extensions_settings.dart index 32272d59..9af975c1 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/extensions_settings.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/extensions_settings.dart @@ -26,7 +26,6 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart'; -import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/install_local_addon_dialog.dart'; import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart'; import 'package:weblibre/features/settings/presentation/widgets/sections.dart'; @@ -47,7 +46,6 @@ class ExtensionsSettingsScreen extends StatelessWidget { children: const [ SettingSection(name: 'Extensions'), _ManageExtensionsTile(), - _InstallLocalAddonTile(), _AddonCollectionTile(), SettingSection(name: 'Updates'), _AutoUpdateTile(), @@ -90,33 +88,6 @@ class _ManageExtensionsTile extends StatelessWidget { } } -class _InstallLocalAddonTile extends StatelessWidget { - const _InstallLocalAddonTile(); - - @override - Widget build(BuildContext context) { - return CustomListTile( - title: 'Install from File', - subtitle: 'Install an extension from a local .xpi file', - prefix: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Icon( - MdiIcons.puzzle, - size: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - suffix: FilledButton.icon( - onPressed: () async { - await showInstallLocalAddonDialog(context); - }, - icon: const Icon(Icons.file_open), - label: const Text('Install'), - ), - ); - } -} - class _AddonCollectionTile extends StatelessWidget { const _AddonCollectionTile(); diff --git a/apps/weblibre/lib/utils/number_format.dart b/apps/weblibre/lib/utils/number_format.dart new file mode 100644 index 00000000..41dfd153 --- /dev/null +++ b/apps/weblibre/lib/utils/number_format.dart @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import 'package:intl/intl.dart'; + +/// Formats integers in a compact, locale-aware form (e.g. 10.6M, 1.3K). +String formatCompactNumber(num value) { + return NumberFormat.compact().format(value); +} + +/// Formats a byte count as a short, human-readable string (B/KB/MB). +String formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; + return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB'; +} + +/// Parses an ISO-8601 timestamp and formats it as a short local date. +/// Returns the original string if parsing fails. +String formatIsoDate(String iso) { + try { + final dt = DateTime.parse(iso).toLocal(); + return DateFormat.yMMMd().format(dt); + } catch (_) { + return iso; + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoAddonsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoAddonsApiImpl.kt index 12dc8475..2a9325e3 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoAddonsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoAddonsApiImpl.kt @@ -13,11 +13,16 @@ import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes import eu.weblibre.flutter_mozilla_components.pigeons.AddonDisabledReason import eu.weblibre.flutter_mozilla_components.pigeons.AddonIncognito import eu.weblibre.flutter_mozilla_components.pigeons.AddonInfo +import eu.weblibre.flutter_mozilla_components.pigeons.AddonListing +import eu.weblibre.flutter_mozilla_components.pigeons.AddonListingPreview +import eu.weblibre.flutter_mozilla_components.pigeons.AddonStoreApp import eu.weblibre.flutter_mozilla_components.pigeons.AddonStoreInfo +import eu.weblibre.flutter_mozilla_components.pigeons.AddonStorePromoted import eu.weblibre.flutter_mozilla_components.pigeons.AddonUpdateAttemptInfo import eu.weblibre.flutter_mozilla_components.pigeons.AddonUpdateStatus import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonsApi import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionActionType +import org.json.JSONArray import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -35,6 +40,7 @@ import mozilla.components.feature.addons.ui.displayName import mozilla.components.feature.addons.ui.summary import mozilla.components.feature.addons.ui.translateDescription import org.mozilla.geckoview.WebExtension.InstallException.ErrorCodes.ERROR_POSTPONED +import java.util.Locale import org.json.JSONObject class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi { @@ -56,6 +62,23 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi { "Update downloaded and will be applied after restarting the app." private const val DEFAULT_AMO_SERVER_URL = "https://addons.mozilla.org" private const val PERIODIC_UPDATE_RESTORE_DELAY_MS = 10_000L + private const val STORE_INFO_CACHE_TTL_MS = 30L * 60L * 1000L + } + + private data class StoreInfoCacheEntry(val value: AddonStoreInfo, val timestampMs: Long) + private val storeInfoCache = java.util.concurrent.ConcurrentHashMap() + + private fun cachedStoreInfo(addonId: String): AddonStoreInfo? { + val entry = storeInfoCache[addonId] ?: return null + if (System.currentTimeMillis() - entry.timestampMs > STORE_INFO_CACHE_TTL_MS) { + storeInfoCache.remove(addonId) + return null + } + return entry.value + } + + private fun cacheStoreInfo(addonId: String, info: AddonStoreInfo) { + storeInfoCache[addonId] = StoreInfoCacheEntry(info, System.currentTimeMillis()) } override fun getAddons(allowCache: Boolean, callback: (Result>) -> Unit) { @@ -84,12 +107,25 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi { scope.launch { runCatching { val installedAddon = components.core.addonManager.getAddonByID(addonId) - (installedAddon ?: components.core.addonManager.getAddons(allowCache = allowCache) - .find { it.id == addonId })?.toPigeon( - context = context, - isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(addonId), - isLocalFileInstalled = isLocalFileInstalledAddon(addonId), - ) + val resolved = installedAddon ?: components.core.addonManager.getAddons( + allowCache = allowCache, + ).find { it.id == addonId } + + val isLocalFile = isLocalFileInstalledAddon(addonId) + val storeInfo = if (resolved != null && resolved.isInstalled() && !isLocalFile && + resolved.needsAmoEnrichment() + ) { + runCatching { fetchAddonStoreInfo(addonId) }.getOrNull() + } else { + null + } + + resolved?.toPigeon( + context = context, + isAutoUpdateEnabled = isAutoUpdateEffectivelyEnabledForAddon(addonId), + isLocalFileInstalled = isLocalFile, + storeInfo = storeInfo, + ) }.fold( onSuccess = { callback(Result.success(it)) }, onFailure = { callback(Result.failure(it)) }, @@ -108,6 +144,51 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi { } } + override fun searchAddonListings( + query: String, + app: AddonStoreApp, + page: Long, + pageSize: Long, + callback: (Result>) -> Unit, + ) { + scope.launch { + runCatching { + fetchAddonListings( + query = query.ifBlank { null }, + app = app, + page = page.toInt().coerceAtLeast(1), + pageSize = pageSize.toInt().coerceIn(1, 50), + sort = if (query.isBlank()) "users" else null, + ) + }.fold( + onSuccess = { callback(Result.success(it)) }, + onFailure = { callback(Result.failure(it)) }, + ) + } + } + + override fun getFeaturedAddonListings( + app: AddonStoreApp, + pageSize: Long, + callback: (Result>) -> Unit, + ) { + scope.launch { + runCatching { + fetchAddonListings( + query = null, + app = app, + page = 1, + pageSize = pageSize.toInt().coerceIn(1, 50), + sort = "users", + promoted = "recommended", + ) + }.fold( + onSuccess = { callback(Result.success(it)) }, + onFailure = { callback(Result.failure(it)) }, + ) + } + } + override fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType) { scope.launch { withContext(Dispatchers.Main.immediate) { @@ -394,6 +475,8 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi { } private suspend fun fetchAddonStoreInfo(addonId: String): AddonStoreInfo? { + cachedStoreInfo(addonId)?.let { return it } + val response = components.core.client.fetch( Request( url = addonStoreInfoUrl(addonId), @@ -416,15 +499,205 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi { return null } - return AddonStoreInfo( + val language = Locale.getDefault().language + val ratings = json.optJSONObject("ratings") + val ratingAverage = ratings?.optDouble("average")?.takeIf { !it.isNaN() && it > 0.0 } + val ratingReviews = ratings?.optInt("text_count", -1)?.takeIf { it >= 0 }?.toLong() + val firstAuthor = json.optJSONArray("authors")?.optJSONObject(0) + + val storeInfo = AddonStoreInfo( latestVersion = latestVersion, latestXpiUrl = latestXpiUrl, + ratingAverage = ratingAverage, + ratingReviews = ratingReviews, + summary = json.pickTranslation("summary", language), + description = json.pickTranslation("description", language), + homepageUrl = json.pickHomepage(language) + ?: json.optString("url").takeIf { it.isNotBlank() }, + detailUrl = json.optString("url").takeIf { it.isNotBlank() }, + ratingUrl = json.optString("ratings_url").takeIf { it.isNotBlank() }, + authorName = firstAuthor?.optString("name")?.takeIf { it.isNotBlank() }, + authorUrl = firstAuthor?.optString("url")?.takeIf { it.isNotBlank() }, ) + cacheStoreInfo(addonId, storeInfo) + return storeInfo } private fun addonStoreInfoUrl(addonId: String): String { - val baseUrl = components.addonCollection?.serverURL?.trimEnd('/') ?: DEFAULT_AMO_SERVER_URL - return "$baseUrl/api/v5/addons/addon/$addonId/" + return "${amoBaseUrl()}/api/v5/addons/addon/$addonId/" + } + + private fun amoBaseUrl(): String { + return components.addonCollection?.serverURL?.trimEnd('/') ?: DEFAULT_AMO_SERVER_URL + } + + private suspend fun fetchAddonListings( + query: String?, + app: AddonStoreApp, + page: Int, + pageSize: Int, + sort: String? = null, + promoted: String? = null, + ): List { + val params = mutableListOf() + params += "app=${app.queryValue()}" + params += "type=extension" + params += "page=$page" + params += "page_size=$pageSize" + if (!query.isNullOrBlank()) { + params += "q=${java.net.URLEncoder.encode(query, "UTF-8")}" + } + sort?.let { params += "sort=$it" } + promoted?.let { params += "promoted=$it" } + params += "lang=${Locale.getDefault().language}" + + val url = "${amoBaseUrl()}/api/v5/addons/search/?${params.joinToString("&")}" + val response = components.core.client.fetch( + Request( + url = url, + method = Request.Method.GET, + headers = MutableHeaders("Accept" to "application/json"), + ), + ) + if (response.status !in 200..299) { + return emptyList() + } + + val body = response.body.useStream { stream -> + String(stream.readAllBytes(), Charsets.UTF_8) + } + val json = JSONObject(body) + val results = json.optJSONArray("results") ?: return emptyList() + val language = Locale.getDefault().language + val listings = mutableListOf() + for (index in 0 until results.length()) { + val entry = results.optJSONObject(index) ?: continue + entry.toAddonListing(language)?.let { listings += it } + } + return listings + } + + private fun JSONObject.toAddonListing(language: String): AddonListing? { + val id = optString("guid").takeIf { it.isNotBlank() } ?: return null + val currentVersion = optJSONObject("current_version") ?: return null + val file = currentVersion.optJSONObject("file") + ?: currentVersion.optJSONArray("files")?.optJSONObject(0) + val downloadUrl = file?.optString("url").orEmpty() + val latestVersion = currentVersion.optString("version").orEmpty() + if (downloadUrl.isBlank() || latestVersion.isBlank()) return null + + val name = pickTranslation("name", language) ?: return null + val ratings = optJSONObject("ratings") + val ratingAverage = ratings?.optDouble("average")?.takeIf { !it.isNaN() && it > 0.0 } + val ratingReviews = ratings?.optInt("text_count", -1)?.takeIf { it >= 0 }?.toLong() + val author = optJSONArray("authors")?.optJSONObject(0) + val averageDailyUsers = optInt("average_daily_users", -1).takeIf { it >= 0 }?.toLong() + + val previews = mutableListOf() + optJSONArray("previews")?.let { arr -> + for (i in 0 until arr.length()) { + val p = arr.optJSONObject(i) ?: continue + val imageUrl = p.optString("image_url").takeIf { it.isNotBlank() } ?: continue + previews += AddonListingPreview( + imageUrl = imageUrl, + thumbnailUrl = p.optString("thumbnail_url").takeIf { it.isNotBlank() }, + caption = p.pickTranslation("caption", language), + ) + } + } + + val permissions = file?.stringArray("permissions").orEmpty() + val hostPermissions = file?.stringArray("host_permissions").orEmpty() + val optionalPermissions = file?.stringArray("optional_permissions").orEmpty() + val dataCollectionPermissions = file?.stringArray("data_collection_permissions").orEmpty() + val fileSize = file?.optLong("size", -1L)?.takeIf { it >= 0L } + + val license = currentVersion.optJSONObject("license") + val licenseName = license?.pickTranslation("name", language) + val licenseUrl = license?.pickTranslatedUrl("url", language) + + val supportUrl = pickTranslatedUrl("support_url", language) + val supportEmail = pickTranslation("support_email", language) + + val categories = mutableListOf() + optJSONObject("categories")?.let { cats -> + val keys = cats.keys() + while (keys.hasNext()) { + val key = keys.next() + val arr = cats.optJSONArray(key) ?: continue + for (i in 0 until arr.length()) { + val c = arr.optString(i) + if (!c.isNullOrBlank() && !categories.contains(c)) categories += c + } + } + } + + return AddonListing( + id = id, + name = name, + summary = pickTranslation("summary", language), + description = pickTranslation("description", language), + iconUrl = optString("icon_url").takeIf { it.isNotBlank() }, + latestVersion = latestVersion, + downloadUrl = downloadUrl, + ratingAverage = ratingAverage, + ratingReviews = ratingReviews, + authorName = author?.optString("name")?.takeIf { it.isNotBlank() }, + authorUrl = author?.optString("url")?.takeIf { it.isNotBlank() }, + homepageUrl = pickHomepage(language), + detailUrl = optString("url").orEmpty(), + ratingUrl = optString("ratings_url").takeIf { it.isNotBlank() }, + averageDailyUsers = averageDailyUsers, + promoted = pickPromoted(), + previews = previews, + permissions = permissions, + hostPermissions = hostPermissions, + optionalPermissions = optionalPermissions, + dataCollectionPermissions = dataCollectionPermissions, + fileSize = fileSize, + lastUpdated = optString("last_updated").takeIf { it.isNotBlank() && it != "null" }, + licenseName = licenseName, + licenseUrl = licenseUrl, + supportUrl = supportUrl, + supportEmail = supportEmail, + categories = categories, + hasPrivacyPolicy = optBoolean("has_privacy_policy", false), + slug = optString("slug").takeIf { it.isNotBlank() && it != "null" }, + ) + } + + private fun JSONObject.stringArray(key: String): List { + val arr = optJSONArray(key) ?: return emptyList() + val out = mutableListOf() + for (i in 0 until arr.length()) { + val s = arr.optString(i) + if (!s.isNullOrBlank() && s != "null") out += s + } + return out + } + + private fun JSONObject.pickPromoted(): AddonStorePromoted { + val categories = optJSONObject("promoted")?.optJSONArray("category") + ?: optJSONArray("promoted") + val singleCategory = optJSONObject("promoted")?.optString("category") + val candidates = buildList { + if (categories is JSONArray) { + for (i in 0 until categories.length()) { + categories.optString(i)?.let { add(it) } + } + } + if (!singleCategory.isNullOrBlank()) add(singleCategory) + } + return when { + candidates.any { it.equals("recommended", ignoreCase = true) } -> AddonStorePromoted.RECOMMENDED + candidates.any { it.equals("line", ignoreCase = true) } -> AddonStorePromoted.LINE + else -> AddonStorePromoted.NONE + } + } + + private fun AddonStoreApp.queryValue(): String = when (this) { + AddonStoreApp.ANDROID -> "android" + AddonStoreApp.FIREFOX -> "firefox" } private fun saveManualUpdateAttempt( @@ -687,10 +960,62 @@ class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi { } } +private fun Addon.needsAmoEnrichment(): Boolean { + val hasRating = (rating?.average ?: 0f) > 0f + val hasDescription = translatableDescription.values.any { it.isNotBlank() } + return !hasRating || !hasDescription +} + +private fun JSONObject.pickHomepage(language: String): String? { + return pickTranslatedUrl("homepage", language) +} + +// AMO represents URL-bearing translated fields either as a flat translated map +// (locale → url), a plain string, or a nested { url: {locale: url, ...}, outgoing: {...} }. +private fun JSONObject.pickTranslatedUrl(key: String, language: String): String? { + if (isNull(key)) return null + val value = opt(key) + if (value is JSONObject && value.has("url")) { + return value.pickTranslation("url", language) + } + return pickTranslation(key, language) +} + +private fun JSONObject.pickTranslation(key: String, language: String): String? { + if (isNull(key)) return null + return when (val value = opt(key)) { + is String -> value.takeIf { it.isNotBlank() } + is JSONObject -> { + val lower = language.lowercase(Locale.ROOT) + // Filter out keys whose value is JSONObject.NULL — optString would + // return the literal string "null" for those. + val populatedKeys = value.keys().asSequence() + .filter { !value.isNull(it) } + .toList() + val defaultLocale = value.optString("_default").takeIf { + it.isNotBlank() && it != "null" + } + val candidateKeys = listOfNotNull( + populatedKeys.firstOrNull { it.equals(lower, ignoreCase = true) }, + populatedKeys.firstOrNull { it.lowercase(Locale.ROOT).startsWith("$lower-") }, + defaultLocale?.takeIf { populatedKeys.contains(it) }, + "en-US".takeIf { populatedKeys.contains(it) }, + populatedKeys.firstOrNull { it != "_default" }, + ) + candidateKeys + .asSequence() + .map { value.optString(it) } + .firstOrNull { it.isNotBlank() } + } + else -> null + } +} + private fun Addon.toPigeon( context: Context, isAutoUpdateEnabled: Boolean, isLocalFileInstalled: Boolean, + storeInfo: AddonStoreInfo? = null, ): AddonInfo { val installedState = installedState val localizedName = displayName(context) @@ -701,24 +1026,34 @@ private fun Addon.toPigeon( "" } + val geckoRatingAverage = rating?.average?.toDouble()?.takeIf { it > 0.0 } + val geckoRatingReviews = rating?.reviews?.toLong()?.takeIf { it > 0L } + val resolvedSummary = localizedSummary?.takeIf { it.isNotBlank() } ?: storeInfo?.summary + val resolvedDescription = localizedDescription.ifBlank { storeInfo?.description.orEmpty() } + val resolvedHomepage = homepageUrl.ifBlank { storeInfo?.homepageUrl.orEmpty() } + val resolvedDetailUrl = detailUrl.ifBlank { storeInfo?.detailUrl.orEmpty() } + val resolvedRatingUrl = ratingUrl.ifBlank { storeInfo?.ratingUrl.orEmpty() } + val resolvedAuthorName = author?.name ?: storeInfo?.authorName + val resolvedAuthorUrl = author?.url ?: storeInfo?.authorUrl + return AddonInfo( id = id, displayName = localizedName, - summary = localizedSummary, - description = localizedDescription, + summary = resolvedSummary, + description = resolvedDescription, downloadUrl = downloadUrl, version = version, installedVersion = installedState?.version, translatedPermissions = translatePermissions(context), translatedRequiredDataCollectionPermissions = translateRequiredDataCollectionPermissions(context), - authorName = author?.name, - authorUrl = author?.url, - homepageUrl = homepageUrl, - detailUrl = detailUrl, - ratingUrl = ratingUrl, - ratingAverage = rating?.average?.toDouble(), - ratingReviews = rating?.reviews?.toLong(), + authorName = resolvedAuthorName, + authorUrl = resolvedAuthorUrl, + homepageUrl = resolvedHomepage, + detailUrl = resolvedDetailUrl, + ratingUrl = resolvedRatingUrl, + ratingAverage = geckoRatingAverage ?: storeInfo?.ratingAverage, + ratingReviews = geckoRatingReviews ?: storeInfo?.ratingReviews, createdAt = createdAt, updatedAt = updatedAt, icon = provideIcon()?.toWebPBytes(), diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index cc8b250b..9152b73a 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -390,6 +390,29 @@ enum class AddonUpdateStatus(val raw: Int) { } } +enum class AddonStoreApp(val raw: Int) { + ANDROID(0), + FIREFOX(1); + + companion object { + fun ofRaw(raw: Int): AddonStoreApp? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class AddonStorePromoted(val raw: Int) { + NONE(0), + RECOMMENDED(1), + LINE(2); + + companion object { + fun ofRaw(raw: Int): AddonStorePromoted? { + return values().firstOrNull { it.raw == raw } + } + } +} + enum class GeckoSuggestionType(val raw: Int) { SESSION(0), CLIPBOARD(1), @@ -2666,23 +2689,242 @@ data class AddonInfo ( } } +/** Generated class from Pigeon that represents data sent in messages. */ +data class AddonListingPreview ( + val imageUrl: String, + val thumbnailUrl: String? = null, + val caption: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): AddonListingPreview { + val imageUrl = pigeonVar_list[0] as String + val thumbnailUrl = pigeonVar_list[1] as String? + val caption = pigeonVar_list[2] as String? + return AddonListingPreview(imageUrl, thumbnailUrl, caption) + } + } + fun toList(): List { + return listOf( + imageUrl, + thumbnailUrl, + caption, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as AddonListingPreview + return GeckoPigeonUtils.deepEquals(this.imageUrl, other.imageUrl) && GeckoPigeonUtils.deepEquals(this.thumbnailUrl, other.thumbnailUrl) && GeckoPigeonUtils.deepEquals(this.caption, other.caption) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + GeckoPigeonUtils.deepHash(this.imageUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.thumbnailUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.caption) + return result + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class AddonListing ( + val id: String, + val name: String, + val summary: String? = null, + val description: String? = null, + val iconUrl: String? = null, + val latestVersion: String, + val downloadUrl: String, + val ratingAverage: Double? = null, + val ratingReviews: Long? = null, + val authorName: String? = null, + val authorUrl: String? = null, + val homepageUrl: String? = null, + val detailUrl: String, + val ratingUrl: String? = null, + val averageDailyUsers: Long? = null, + val promoted: AddonStorePromoted, + val previews: List, + val permissions: List, + val hostPermissions: List, + val optionalPermissions: List, + val dataCollectionPermissions: List, + val fileSize: Long? = null, + val lastUpdated: String? = null, + val licenseName: String? = null, + val licenseUrl: String? = null, + val supportUrl: String? = null, + val supportEmail: String? = null, + val categories: List, + val hasPrivacyPolicy: Boolean, + val slug: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): AddonListing { + val id = pigeonVar_list[0] as String + val name = pigeonVar_list[1] as String + val summary = pigeonVar_list[2] as String? + val description = pigeonVar_list[3] as String? + val iconUrl = pigeonVar_list[4] as String? + val latestVersion = pigeonVar_list[5] as String + val downloadUrl = pigeonVar_list[6] as String + val ratingAverage = pigeonVar_list[7] as Double? + val ratingReviews = pigeonVar_list[8] as Long? + val authorName = pigeonVar_list[9] as String? + val authorUrl = pigeonVar_list[10] as String? + val homepageUrl = pigeonVar_list[11] as String? + val detailUrl = pigeonVar_list[12] as String + val ratingUrl = pigeonVar_list[13] as String? + val averageDailyUsers = pigeonVar_list[14] as Long? + val promoted = pigeonVar_list[15] as AddonStorePromoted + val previews = pigeonVar_list[16] as List + val permissions = pigeonVar_list[17] as List + val hostPermissions = pigeonVar_list[18] as List + val optionalPermissions = pigeonVar_list[19] as List + val dataCollectionPermissions = pigeonVar_list[20] as List + val fileSize = pigeonVar_list[21] as Long? + val lastUpdated = pigeonVar_list[22] as String? + val licenseName = pigeonVar_list[23] as String? + val licenseUrl = pigeonVar_list[24] as String? + val supportUrl = pigeonVar_list[25] as String? + val supportEmail = pigeonVar_list[26] as String? + val categories = pigeonVar_list[27] as List + val hasPrivacyPolicy = pigeonVar_list[28] as Boolean + val slug = pigeonVar_list[29] as String? + return AddonListing(id, name, summary, description, iconUrl, latestVersion, downloadUrl, ratingAverage, ratingReviews, authorName, authorUrl, homepageUrl, detailUrl, ratingUrl, averageDailyUsers, promoted, previews, permissions, hostPermissions, optionalPermissions, dataCollectionPermissions, fileSize, lastUpdated, licenseName, licenseUrl, supportUrl, supportEmail, categories, hasPrivacyPolicy, slug) + } + } + fun toList(): List { + return listOf( + id, + name, + summary, + description, + iconUrl, + latestVersion, + downloadUrl, + ratingAverage, + ratingReviews, + authorName, + authorUrl, + homepageUrl, + detailUrl, + ratingUrl, + averageDailyUsers, + promoted, + previews, + permissions, + hostPermissions, + optionalPermissions, + dataCollectionPermissions, + fileSize, + lastUpdated, + licenseName, + licenseUrl, + supportUrl, + supportEmail, + categories, + hasPrivacyPolicy, + slug, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as AddonListing + return GeckoPigeonUtils.deepEquals(this.id, other.id) && GeckoPigeonUtils.deepEquals(this.name, other.name) && GeckoPigeonUtils.deepEquals(this.summary, other.summary) && GeckoPigeonUtils.deepEquals(this.description, other.description) && GeckoPigeonUtils.deepEquals(this.iconUrl, other.iconUrl) && GeckoPigeonUtils.deepEquals(this.latestVersion, other.latestVersion) && GeckoPigeonUtils.deepEquals(this.downloadUrl, other.downloadUrl) && GeckoPigeonUtils.deepEquals(this.ratingAverage, other.ratingAverage) && GeckoPigeonUtils.deepEquals(this.ratingReviews, other.ratingReviews) && GeckoPigeonUtils.deepEquals(this.authorName, other.authorName) && GeckoPigeonUtils.deepEquals(this.authorUrl, other.authorUrl) && GeckoPigeonUtils.deepEquals(this.homepageUrl, other.homepageUrl) && GeckoPigeonUtils.deepEquals(this.detailUrl, other.detailUrl) && GeckoPigeonUtils.deepEquals(this.ratingUrl, other.ratingUrl) && GeckoPigeonUtils.deepEquals(this.averageDailyUsers, other.averageDailyUsers) && GeckoPigeonUtils.deepEquals(this.promoted, other.promoted) && GeckoPigeonUtils.deepEquals(this.previews, other.previews) && GeckoPigeonUtils.deepEquals(this.permissions, other.permissions) && GeckoPigeonUtils.deepEquals(this.hostPermissions, other.hostPermissions) && GeckoPigeonUtils.deepEquals(this.optionalPermissions, other.optionalPermissions) && GeckoPigeonUtils.deepEquals(this.dataCollectionPermissions, other.dataCollectionPermissions) && GeckoPigeonUtils.deepEquals(this.fileSize, other.fileSize) && GeckoPigeonUtils.deepEquals(this.lastUpdated, other.lastUpdated) && GeckoPigeonUtils.deepEquals(this.licenseName, other.licenseName) && GeckoPigeonUtils.deepEquals(this.licenseUrl, other.licenseUrl) && GeckoPigeonUtils.deepEquals(this.supportUrl, other.supportUrl) && GeckoPigeonUtils.deepEquals(this.supportEmail, other.supportEmail) && GeckoPigeonUtils.deepEquals(this.categories, other.categories) && GeckoPigeonUtils.deepEquals(this.hasPrivacyPolicy, other.hasPrivacyPolicy) && GeckoPigeonUtils.deepEquals(this.slug, other.slug) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + GeckoPigeonUtils.deepHash(this.id) + result = 31 * result + GeckoPigeonUtils.deepHash(this.name) + result = 31 * result + GeckoPigeonUtils.deepHash(this.summary) + result = 31 * result + GeckoPigeonUtils.deepHash(this.description) + result = 31 * result + GeckoPigeonUtils.deepHash(this.iconUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.latestVersion) + result = 31 * result + GeckoPigeonUtils.deepHash(this.downloadUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.ratingAverage) + result = 31 * result + GeckoPigeonUtils.deepHash(this.ratingReviews) + result = 31 * result + GeckoPigeonUtils.deepHash(this.authorName) + result = 31 * result + GeckoPigeonUtils.deepHash(this.authorUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.homepageUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.detailUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.ratingUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.averageDailyUsers) + result = 31 * result + GeckoPigeonUtils.deepHash(this.promoted) + result = 31 * result + GeckoPigeonUtils.deepHash(this.previews) + result = 31 * result + GeckoPigeonUtils.deepHash(this.permissions) + result = 31 * result + GeckoPigeonUtils.deepHash(this.hostPermissions) + result = 31 * result + GeckoPigeonUtils.deepHash(this.optionalPermissions) + result = 31 * result + GeckoPigeonUtils.deepHash(this.dataCollectionPermissions) + result = 31 * result + GeckoPigeonUtils.deepHash(this.fileSize) + result = 31 * result + GeckoPigeonUtils.deepHash(this.lastUpdated) + result = 31 * result + GeckoPigeonUtils.deepHash(this.licenseName) + result = 31 * result + GeckoPigeonUtils.deepHash(this.licenseUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.supportUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.supportEmail) + result = 31 * result + GeckoPigeonUtils.deepHash(this.categories) + result = 31 * result + GeckoPigeonUtils.deepHash(this.hasPrivacyPolicy) + result = 31 * result + GeckoPigeonUtils.deepHash(this.slug) + return result + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class AddonStoreInfo ( val latestVersion: String, - val latestXpiUrl: String + val latestXpiUrl: String, + val ratingAverage: Double? = null, + val ratingReviews: Long? = null, + val summary: String? = null, + val description: String? = null, + val homepageUrl: String? = null, + val detailUrl: String? = null, + val ratingUrl: String? = null, + val authorName: String? = null, + val authorUrl: String? = null ) { companion object { fun fromList(pigeonVar_list: List): AddonStoreInfo { val latestVersion = pigeonVar_list[0] as String val latestXpiUrl = pigeonVar_list[1] as String - return AddonStoreInfo(latestVersion, latestXpiUrl) + val ratingAverage = pigeonVar_list[2] as Double? + val ratingReviews = pigeonVar_list[3] as Long? + val summary = pigeonVar_list[4] as String? + val description = pigeonVar_list[5] as String? + val homepageUrl = pigeonVar_list[6] as String? + val detailUrl = pigeonVar_list[7] as String? + val ratingUrl = pigeonVar_list[8] as String? + val authorName = pigeonVar_list[9] as String? + val authorUrl = pigeonVar_list[10] as String? + return AddonStoreInfo(latestVersion, latestXpiUrl, ratingAverage, ratingReviews, summary, description, homepageUrl, detailUrl, ratingUrl, authorName, authorUrl) } } fun toList(): List { return listOf( latestVersion, latestXpiUrl, + ratingAverage, + ratingReviews, + summary, + description, + homepageUrl, + detailUrl, + ratingUrl, + authorName, + authorUrl, ) } override fun equals(other: Any?): Boolean { @@ -2693,13 +2935,22 @@ data class AddonStoreInfo ( return true } val other = other as AddonStoreInfo - return GeckoPigeonUtils.deepEquals(this.latestVersion, other.latestVersion) && GeckoPigeonUtils.deepEquals(this.latestXpiUrl, other.latestXpiUrl) + return GeckoPigeonUtils.deepEquals(this.latestVersion, other.latestVersion) && GeckoPigeonUtils.deepEquals(this.latestXpiUrl, other.latestXpiUrl) && GeckoPigeonUtils.deepEquals(this.ratingAverage, other.ratingAverage) && GeckoPigeonUtils.deepEquals(this.ratingReviews, other.ratingReviews) && GeckoPigeonUtils.deepEquals(this.summary, other.summary) && GeckoPigeonUtils.deepEquals(this.description, other.description) && GeckoPigeonUtils.deepEquals(this.homepageUrl, other.homepageUrl) && GeckoPigeonUtils.deepEquals(this.detailUrl, other.detailUrl) && GeckoPigeonUtils.deepEquals(this.ratingUrl, other.ratingUrl) && GeckoPigeonUtils.deepEquals(this.authorName, other.authorName) && GeckoPigeonUtils.deepEquals(this.authorUrl, other.authorUrl) } override fun hashCode(): Int { var result = javaClass.hashCode() result = 31 * result + GeckoPigeonUtils.deepHash(this.latestVersion) result = 31 * result + GeckoPigeonUtils.deepHash(this.latestXpiUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.ratingAverage) + result = 31 * result + GeckoPigeonUtils.deepHash(this.ratingReviews) + result = 31 * result + GeckoPigeonUtils.deepHash(this.summary) + result = 31 * result + GeckoPigeonUtils.deepHash(this.description) + result = 31 * result + GeckoPigeonUtils.deepHash(this.homepageUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.detailUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.ratingUrl) + result = 31 * result + GeckoPigeonUtils.deepHash(this.authorName) + result = 31 * result + GeckoPigeonUtils.deepHash(this.authorUrl) return result } } @@ -5014,500 +5265,520 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { } 141.toByte() -> { return (readValue(buffer) as Long?)?.let { - GeckoSuggestionType.ofRaw(it.toInt()) + AddonStoreApp.ofRaw(it.toInt()) } } 142.toByte() -> { return (readValue(buffer) as Long?)?.let { - TrackingProtectionPolicy.ofRaw(it.toInt()) + AddonStorePromoted.ofRaw(it.toInt()) } } 143.toByte() -> { return (readValue(buffer) as Long?)?.let { - HttpsOnlyMode.ofRaw(it.toInt()) + GeckoSuggestionType.ofRaw(it.toInt()) } } 144.toByte() -> { return (readValue(buffer) as Long?)?.let { - QueryParameterStripping.ofRaw(it.toInt()) + TrackingProtectionPolicy.ofRaw(it.toInt()) } } 145.toByte() -> { return (readValue(buffer) as Long?)?.let { - BounceTrackingProtectionMode.ofRaw(it.toInt()) + HttpsOnlyMode.ofRaw(it.toInt()) } } 146.toByte() -> { return (readValue(buffer) as Long?)?.let { - ColorScheme.ofRaw(it.toInt()) + QueryParameterStripping.ofRaw(it.toInt()) } } 147.toByte() -> { return (readValue(buffer) as Long?)?.let { - CookieBannerHandlingMode.ofRaw(it.toInt()) + BounceTrackingProtectionMode.ofRaw(it.toInt()) } } 148.toByte() -> { return (readValue(buffer) as Long?)?.let { - AppLinksMode.ofRaw(it.toInt()) + ColorScheme.ofRaw(it.toInt()) } } 149.toByte() -> { return (readValue(buffer) as Long?)?.let { - WebContentIsolationStrategy.ofRaw(it.toInt()) + CookieBannerHandlingMode.ofRaw(it.toInt()) } } 150.toByte() -> { return (readValue(buffer) as Long?)?.let { - CustomCookiePolicy.ofRaw(it.toInt()) + AppLinksMode.ofRaw(it.toInt()) } } 151.toByte() -> { return (readValue(buffer) as Long?)?.let { - TrackingScope.ofRaw(it.toInt()) + WebContentIsolationStrategy.ofRaw(it.toInt()) } } 152.toByte() -> { return (readValue(buffer) as Long?)?.let { - DohSettingsMode.ofRaw(it.toInt()) + CustomCookiePolicy.ofRaw(it.toInt()) } } 153.toByte() -> { return (readValue(buffer) as Long?)?.let { - DownloadStatus.ofRaw(it.toInt()) + TrackingScope.ofRaw(it.toInt()) } } 154.toByte() -> { return (readValue(buffer) as Long?)?.let { - LogLevel.ofRaw(it.toInt()) + DohSettingsMode.ofRaw(it.toInt()) } } 155.toByte() -> { return (readValue(buffer) as Long?)?.let { - SyncEngineValue.ofRaw(it.toInt()) + DownloadStatus.ofRaw(it.toInt()) } } 156.toByte() -> { return (readValue(buffer) as Long?)?.let { - MlProgressType.ofRaw(it.toInt()) + LogLevel.ofRaw(it.toInt()) } } 157.toByte() -> { return (readValue(buffer) as Long?)?.let { - MlProgressStatus.ofRaw(it.toInt()) + SyncEngineValue.ofRaw(it.toInt()) } } 158.toByte() -> { return (readValue(buffer) as Long?)?.let { - ClearDataType.ofRaw(it.toInt()) + MlProgressType.ofRaw(it.toInt()) } } 159.toByte() -> { return (readValue(buffer) as Long?)?.let { - GeckoFetchMethod.ofRaw(it.toInt()) + MlProgressStatus.ofRaw(it.toInt()) } } 160.toByte() -> { return (readValue(buffer) as Long?)?.let { - GeckoFetchRedircet.ofRaw(it.toInt()) + ClearDataType.ofRaw(it.toInt()) } } 161.toByte() -> { return (readValue(buffer) as Long?)?.let { - GeckoFetchCookiePolicy.ofRaw(it.toInt()) + GeckoFetchMethod.ofRaw(it.toInt()) } } 162.toByte() -> { return (readValue(buffer) as Long?)?.let { - BookmarkNodeType.ofRaw(it.toInt()) + GeckoFetchRedircet.ofRaw(it.toInt()) } } 163.toByte() -> { return (readValue(buffer) as Long?)?.let { - SitePermissionStatus.ofRaw(it.toInt()) + GeckoFetchCookiePolicy.ofRaw(it.toInt()) } } 164.toByte() -> { return (readValue(buffer) as Long?)?.let { - AutoplayStatus.ofRaw(it.toInt()) + BookmarkNodeType.ofRaw(it.toInt()) } } 165.toByte() -> { - return (readValue(buffer) as? List)?.let { - TranslationOptions.fromList(it) + return (readValue(buffer) as Long?)?.let { + SitePermissionStatus.ofRaw(it.toInt()) } } 166.toByte() -> { - return (readValue(buffer) as? List)?.let { - TranslationLanguage.fromList(it) + return (readValue(buffer) as Long?)?.let { + AutoplayStatus.ofRaw(it.toInt()) } } 167.toByte() -> { return (readValue(buffer) as? List)?.let { - TranslationDetectedLanguages.fromList(it) + TranslationOptions.fromList(it) } } 168.toByte() -> { return (readValue(buffer) as? List)?.let { - TranslationPair.fromList(it) + TranslationLanguage.fromList(it) } } 169.toByte() -> { return (readValue(buffer) as? List)?.let { - TranslationEngineStateData.fromList(it) + TranslationDetectedLanguages.fromList(it) } } 170.toByte() -> { return (readValue(buffer) as? List)?.let { - TabTranslationStateData.fromList(it) + TranslationPair.fromList(it) } } 171.toByte() -> { return (readValue(buffer) as? List)?.let { - ReaderState.fromList(it) + TranslationEngineStateData.fromList(it) } } 172.toByte() -> { return (readValue(buffer) as? List)?.let { - AddTabParams.fromList(it) + TabTranslationStateData.fromList(it) } } 173.toByte() -> { return (readValue(buffer) as? List)?.let { - LastMediaAccessState.fromList(it) + ReaderState.fromList(it) } } 174.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryMetadataKey.fromList(it) + AddTabParams.fromList(it) } } 175.toByte() -> { return (readValue(buffer) as? List)?.let { - PackageCategoryValue.fromList(it) + LastMediaAccessState.fromList(it) } } 176.toByte() -> { return (readValue(buffer) as? List)?.let { - ExternalPackage.fromList(it) + HistoryMetadataKey.fromList(it) } } 177.toByte() -> { return (readValue(buffer) as? List)?.let { - LoadUrlFlagsValue.fromList(it) + PackageCategoryValue.fromList(it) } } 178.toByte() -> { return (readValue(buffer) as? List)?.let { - SourceValue.fromList(it) + ExternalPackage.fromList(it) } } 179.toByte() -> { return (readValue(buffer) as? List)?.let { - TabState.fromList(it) + LoadUrlFlagsValue.fromList(it) } } 180.toByte() -> { return (readValue(buffer) as? List)?.let { - RecoverableTab.fromList(it) + SourceValue.fromList(it) } } 181.toByte() -> { return (readValue(buffer) as? List)?.let { - IconRequest.fromList(it) + TabState.fromList(it) } } 182.toByte() -> { return (readValue(buffer) as? List)?.let { - ResourceSize.fromList(it) + RecoverableTab.fromList(it) } } 183.toByte() -> { return (readValue(buffer) as? List)?.let { - Resource.fromList(it) + IconRequest.fromList(it) } } 184.toByte() -> { return (readValue(buffer) as? List)?.let { - IconResult.fromList(it) + ResourceSize.fromList(it) } } 185.toByte() -> { return (readValue(buffer) as? List)?.let { - CookiePartitionKey.fromList(it) + Resource.fromList(it) } } 186.toByte() -> { return (readValue(buffer) as? List)?.let { - Cookie.fromList(it) + IconResult.fromList(it) } } 187.toByte() -> { return (readValue(buffer) as? List)?.let { - VisitInfo.fromList(it) + CookiePartitionKey.fromList(it) } } 188.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryHighlightWeights.fromList(it) + Cookie.fromList(it) } } 189.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryHighlight.fromList(it) + VisitInfo.fromList(it) } } 190.toByte() -> { return (readValue(buffer) as? List)?.let { - TopFrecentSiteInfo.fromList(it) + HistoryHighlightWeights.fromList(it) } } 191.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryItem.fromList(it) + HistoryHighlight.fromList(it) } } 192.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryState.fromList(it) + TopFrecentSiteInfo.fromList(it) } } 193.toByte() -> { return (readValue(buffer) as? List)?.let { - ReaderableState.fromList(it) + HistoryItem.fromList(it) } } 194.toByte() -> { return (readValue(buffer) as? List)?.let { - SecurityInfoState.fromList(it) + HistoryState.fromList(it) } } 195.toByte() -> { return (readValue(buffer) as? List)?.let { - TabContentState.fromList(it) + ReaderableState.fromList(it) } } 196.toByte() -> { return (readValue(buffer) as? List)?.let { - FindResultState.fromList(it) + SecurityInfoState.fromList(it) } } 197.toByte() -> { return (readValue(buffer) as? List)?.let { - CustomSelectionAction.fromList(it) + TabContentState.fromList(it) } } 198.toByte() -> { return (readValue(buffer) as? List)?.let { - WebExtensionData.fromList(it) + FindResultState.fromList(it) } } 199.toByte() -> { return (readValue(buffer) as? List)?.let { - AddonInfo.fromList(it) + CustomSelectionAction.fromList(it) } } 200.toByte() -> { return (readValue(buffer) as? List)?.let { - AddonStoreInfo.fromList(it) + WebExtensionData.fromList(it) } } 201.toByte() -> { return (readValue(buffer) as? List)?.let { - AddonUpdateAttemptInfo.fromList(it) + AddonInfo.fromList(it) } } 202.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoSuggestion.fromList(it) + AddonListingPreview.fromList(it) } } 203.toByte() -> { return (readValue(buffer) as? List)?.let { - TabContent.fromList(it) + AddonListing.fromList(it) } } 204.toByte() -> { return (readValue(buffer) as? List)?.let { - ContentBlocking.fromList(it) + AddonStoreInfo.fromList(it) } } 205.toByte() -> { return (readValue(buffer) as? List)?.let { - DohSettings.fromList(it) + AddonUpdateAttemptInfo.fromList(it) } } 206.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoEngineSettings.fromList(it) + GeckoSuggestion.fromList(it) } } 207.toByte() -> { return (readValue(buffer) as? List)?.let { - AutocompleteResult.fromList(it) + TabContent.fromList(it) } } 208.toByte() -> { return (readValue(buffer) as? List)?.let { - UnknownHitResult.fromList(it) + ContentBlocking.fromList(it) } } 209.toByte() -> { return (readValue(buffer) as? List)?.let { - ImageHitResult.fromList(it) + DohSettings.fromList(it) } } 210.toByte() -> { return (readValue(buffer) as? List)?.let { - VideoHitResult.fromList(it) + GeckoEngineSettings.fromList(it) } } 211.toByte() -> { return (readValue(buffer) as? List)?.let { - AudioHitResult.fromList(it) + AutocompleteResult.fromList(it) } } 212.toByte() -> { return (readValue(buffer) as? List)?.let { - ImageSrcHitResult.fromList(it) + UnknownHitResult.fromList(it) } } 213.toByte() -> { return (readValue(buffer) as? List)?.let { - PhoneHitResult.fromList(it) + ImageHitResult.fromList(it) } } 214.toByte() -> { return (readValue(buffer) as? List)?.let { - EmailHitResult.fromList(it) + VideoHitResult.fromList(it) } } 215.toByte() -> { return (readValue(buffer) as? List)?.let { - GeoHitResult.fromList(it) + AudioHitResult.fromList(it) } } 216.toByte() -> { return (readValue(buffer) as? List)?.let { - DownloadState.fromList(it) + ImageSrcHitResult.fromList(it) } } 217.toByte() -> { return (readValue(buffer) as? List)?.let { - ShareInternetResourceState.fromList(it) + PhoneHitResult.fromList(it) } } 218.toByte() -> { return (readValue(buffer) as? List)?.let { - AddonCollection.fromList(it) + EmailHitResult.fromList(it) } } 219.toByte() -> { return (readValue(buffer) as? List)?.let { - SyncEngineStatus.fromList(it) + GeoHitResult.fromList(it) } } 220.toByte() -> { return (readValue(buffer) as? List)?.let { - SyncAccountInfo.fromList(it) + DownloadState.fromList(it) } } 221.toByte() -> { return (readValue(buffer) as? List)?.let { - SyncDevice.fromList(it) + ShareInternetResourceState.fromList(it) } } 222.toByte() -> { return (readValue(buffer) as? List)?.let { - SyncIncomingTab.fromList(it) + AddonCollection.fromList(it) } } 223.toByte() -> { return (readValue(buffer) as? List)?.let { - SyncRemoteTab.fromList(it) + SyncEngineStatus.fromList(it) } } 224.toByte() -> { return (readValue(buffer) as? List)?.let { - SyncDeviceTabs.fromList(it) + SyncAccountInfo.fromList(it) } } 225.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoPref.fromList(it) + SyncDevice.fromList(it) } } 226.toByte() -> { return (readValue(buffer) as? List)?.let { - MlProgressData.fromList(it) + SyncIncomingTab.fromList(it) } } 227.toByte() -> { return (readValue(buffer) as? List)?.let { - ContainerSiteAssignment.fromList(it) + SyncRemoteTab.fromList(it) } } 228.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoHeader.fromList(it) + SyncDeviceTabs.fromList(it) } } 229.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoFetchRequest.fromList(it) + GeckoPref.fromList(it) } } 230.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoFetchResponse.fromList(it) + MlProgressData.fromList(it) } } 231.toByte() -> { return (readValue(buffer) as? List)?.let { - BookmarkNode.fromList(it) + ContainerSiteAssignment.fromList(it) } } 232.toByte() -> { return (readValue(buffer) as? List)?.let { - BookmarkInfo.fromList(it) + GeckoHeader.fromList(it) } } 233.toByte() -> { return (readValue(buffer) as? List)?.let { - SitePermissions.fromList(it) + GeckoFetchRequest.fromList(it) } } 234.toByte() -> { return (readValue(buffer) as? List)?.let { - TrackingProtectionException.fromList(it) + GeckoFetchResponse.fromList(it) } } 235.toByte() -> { return (readValue(buffer) as? List)?.let { - PwaIcon.fromList(it) + BookmarkNode.fromList(it) } } 236.toByte() -> { return (readValue(buffer) as? List)?.let { - ShareTargetFiles.fromList(it) + BookmarkInfo.fromList(it) } } 237.toByte() -> { return (readValue(buffer) as? List)?.let { - ShareTargetParams.fromList(it) + SitePermissions.fromList(it) } } 238.toByte() -> { return (readValue(buffer) as? List)?.let { - ShareTarget.fromList(it) + TrackingProtectionException.fromList(it) } } 239.toByte() -> { return (readValue(buffer) as? List)?.let { - ExternalApplicationResource.fromList(it) + PwaIcon.fromList(it) } } 240.toByte() -> { + return (readValue(buffer) as? List)?.let { + ShareTargetFiles.fromList(it) + } + } + 241.toByte() -> { + return (readValue(buffer) as? List)?.let { + ShareTargetParams.fromList(it) + } + } + 242.toByte() -> { + return (readValue(buffer) as? List)?.let { + ShareTarget.fromList(it) + } + } + 243.toByte() -> { + return (readValue(buffer) as? List)?.let { + ExternalApplicationResource.fromList(it) + } + } + 244.toByte() -> { return (readValue(buffer) as? List)?.let { PwaManifest.fromList(it) } @@ -5565,406 +5836,422 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { stream.write(140) writeValue(stream, value.raw.toLong()) } - is GeckoSuggestionType -> { + is AddonStoreApp -> { stream.write(141) writeValue(stream, value.raw.toLong()) } - is TrackingProtectionPolicy -> { + is AddonStorePromoted -> { stream.write(142) writeValue(stream, value.raw.toLong()) } - is HttpsOnlyMode -> { + is GeckoSuggestionType -> { stream.write(143) writeValue(stream, value.raw.toLong()) } - is QueryParameterStripping -> { + is TrackingProtectionPolicy -> { stream.write(144) writeValue(stream, value.raw.toLong()) } - is BounceTrackingProtectionMode -> { + is HttpsOnlyMode -> { stream.write(145) writeValue(stream, value.raw.toLong()) } - is ColorScheme -> { + is QueryParameterStripping -> { stream.write(146) writeValue(stream, value.raw.toLong()) } - is CookieBannerHandlingMode -> { + is BounceTrackingProtectionMode -> { stream.write(147) writeValue(stream, value.raw.toLong()) } - is AppLinksMode -> { + is ColorScheme -> { stream.write(148) writeValue(stream, value.raw.toLong()) } - is WebContentIsolationStrategy -> { + is CookieBannerHandlingMode -> { stream.write(149) writeValue(stream, value.raw.toLong()) } - is CustomCookiePolicy -> { + is AppLinksMode -> { stream.write(150) writeValue(stream, value.raw.toLong()) } - is TrackingScope -> { + is WebContentIsolationStrategy -> { stream.write(151) writeValue(stream, value.raw.toLong()) } - is DohSettingsMode -> { + is CustomCookiePolicy -> { stream.write(152) writeValue(stream, value.raw.toLong()) } - is DownloadStatus -> { + is TrackingScope -> { stream.write(153) writeValue(stream, value.raw.toLong()) } - is LogLevel -> { + is DohSettingsMode -> { stream.write(154) writeValue(stream, value.raw.toLong()) } - is SyncEngineValue -> { + is DownloadStatus -> { stream.write(155) writeValue(stream, value.raw.toLong()) } - is MlProgressType -> { + is LogLevel -> { stream.write(156) writeValue(stream, value.raw.toLong()) } - is MlProgressStatus -> { + is SyncEngineValue -> { stream.write(157) writeValue(stream, value.raw.toLong()) } - is ClearDataType -> { + is MlProgressType -> { stream.write(158) writeValue(stream, value.raw.toLong()) } - is GeckoFetchMethod -> { + is MlProgressStatus -> { stream.write(159) writeValue(stream, value.raw.toLong()) } - is GeckoFetchRedircet -> { + is ClearDataType -> { stream.write(160) writeValue(stream, value.raw.toLong()) } - is GeckoFetchCookiePolicy -> { + is GeckoFetchMethod -> { stream.write(161) writeValue(stream, value.raw.toLong()) } - is BookmarkNodeType -> { + is GeckoFetchRedircet -> { stream.write(162) writeValue(stream, value.raw.toLong()) } - is SitePermissionStatus -> { + is GeckoFetchCookiePolicy -> { stream.write(163) writeValue(stream, value.raw.toLong()) } - is AutoplayStatus -> { + is BookmarkNodeType -> { stream.write(164) writeValue(stream, value.raw.toLong()) } - is TranslationOptions -> { + is SitePermissionStatus -> { stream.write(165) - writeValue(stream, value.toList()) + writeValue(stream, value.raw.toLong()) } - is TranslationLanguage -> { + is AutoplayStatus -> { stream.write(166) - writeValue(stream, value.toList()) + writeValue(stream, value.raw.toLong()) } - is TranslationDetectedLanguages -> { + is TranslationOptions -> { stream.write(167) writeValue(stream, value.toList()) } - is TranslationPair -> { + is TranslationLanguage -> { stream.write(168) writeValue(stream, value.toList()) } - is TranslationEngineStateData -> { + is TranslationDetectedLanguages -> { stream.write(169) writeValue(stream, value.toList()) } - is TabTranslationStateData -> { + is TranslationPair -> { stream.write(170) writeValue(stream, value.toList()) } - is ReaderState -> { + is TranslationEngineStateData -> { stream.write(171) writeValue(stream, value.toList()) } - is AddTabParams -> { + is TabTranslationStateData -> { stream.write(172) writeValue(stream, value.toList()) } - is LastMediaAccessState -> { + is ReaderState -> { stream.write(173) writeValue(stream, value.toList()) } - is HistoryMetadataKey -> { + is AddTabParams -> { stream.write(174) writeValue(stream, value.toList()) } - is PackageCategoryValue -> { + is LastMediaAccessState -> { stream.write(175) writeValue(stream, value.toList()) } - is ExternalPackage -> { + is HistoryMetadataKey -> { stream.write(176) writeValue(stream, value.toList()) } - is LoadUrlFlagsValue -> { + is PackageCategoryValue -> { stream.write(177) writeValue(stream, value.toList()) } - is SourceValue -> { + is ExternalPackage -> { stream.write(178) writeValue(stream, value.toList()) } - is TabState -> { + is LoadUrlFlagsValue -> { stream.write(179) writeValue(stream, value.toList()) } - is RecoverableTab -> { + is SourceValue -> { stream.write(180) writeValue(stream, value.toList()) } - is IconRequest -> { + is TabState -> { stream.write(181) writeValue(stream, value.toList()) } - is ResourceSize -> { + is RecoverableTab -> { stream.write(182) writeValue(stream, value.toList()) } - is Resource -> { + is IconRequest -> { stream.write(183) writeValue(stream, value.toList()) } - is IconResult -> { + is ResourceSize -> { stream.write(184) writeValue(stream, value.toList()) } - is CookiePartitionKey -> { + is Resource -> { stream.write(185) writeValue(stream, value.toList()) } - is Cookie -> { + is IconResult -> { stream.write(186) writeValue(stream, value.toList()) } - is VisitInfo -> { + is CookiePartitionKey -> { stream.write(187) writeValue(stream, value.toList()) } - is HistoryHighlightWeights -> { + is Cookie -> { stream.write(188) writeValue(stream, value.toList()) } - is HistoryHighlight -> { + is VisitInfo -> { stream.write(189) writeValue(stream, value.toList()) } - is TopFrecentSiteInfo -> { + is HistoryHighlightWeights -> { stream.write(190) writeValue(stream, value.toList()) } - is HistoryItem -> { + is HistoryHighlight -> { stream.write(191) writeValue(stream, value.toList()) } - is HistoryState -> { + is TopFrecentSiteInfo -> { stream.write(192) writeValue(stream, value.toList()) } - is ReaderableState -> { + is HistoryItem -> { stream.write(193) writeValue(stream, value.toList()) } - is SecurityInfoState -> { + is HistoryState -> { stream.write(194) writeValue(stream, value.toList()) } - is TabContentState -> { + is ReaderableState -> { stream.write(195) writeValue(stream, value.toList()) } - is FindResultState -> { + is SecurityInfoState -> { stream.write(196) writeValue(stream, value.toList()) } - is CustomSelectionAction -> { + is TabContentState -> { stream.write(197) writeValue(stream, value.toList()) } - is WebExtensionData -> { + is FindResultState -> { stream.write(198) writeValue(stream, value.toList()) } - is AddonInfo -> { + is CustomSelectionAction -> { stream.write(199) writeValue(stream, value.toList()) } - is AddonStoreInfo -> { + is WebExtensionData -> { stream.write(200) writeValue(stream, value.toList()) } - is AddonUpdateAttemptInfo -> { + is AddonInfo -> { stream.write(201) writeValue(stream, value.toList()) } - is GeckoSuggestion -> { + is AddonListingPreview -> { stream.write(202) writeValue(stream, value.toList()) } - is TabContent -> { + is AddonListing -> { stream.write(203) writeValue(stream, value.toList()) } - is ContentBlocking -> { + is AddonStoreInfo -> { stream.write(204) writeValue(stream, value.toList()) } - is DohSettings -> { + is AddonUpdateAttemptInfo -> { stream.write(205) writeValue(stream, value.toList()) } - is GeckoEngineSettings -> { + is GeckoSuggestion -> { stream.write(206) writeValue(stream, value.toList()) } - is AutocompleteResult -> { + is TabContent -> { stream.write(207) writeValue(stream, value.toList()) } - is UnknownHitResult -> { + is ContentBlocking -> { stream.write(208) writeValue(stream, value.toList()) } - is ImageHitResult -> { + is DohSettings -> { stream.write(209) writeValue(stream, value.toList()) } - is VideoHitResult -> { + is GeckoEngineSettings -> { stream.write(210) writeValue(stream, value.toList()) } - is AudioHitResult -> { + is AutocompleteResult -> { stream.write(211) writeValue(stream, value.toList()) } - is ImageSrcHitResult -> { + is UnknownHitResult -> { stream.write(212) writeValue(stream, value.toList()) } - is PhoneHitResult -> { + is ImageHitResult -> { stream.write(213) writeValue(stream, value.toList()) } - is EmailHitResult -> { + is VideoHitResult -> { stream.write(214) writeValue(stream, value.toList()) } - is GeoHitResult -> { + is AudioHitResult -> { stream.write(215) writeValue(stream, value.toList()) } - is DownloadState -> { + is ImageSrcHitResult -> { stream.write(216) writeValue(stream, value.toList()) } - is ShareInternetResourceState -> { + is PhoneHitResult -> { stream.write(217) writeValue(stream, value.toList()) } - is AddonCollection -> { + is EmailHitResult -> { stream.write(218) writeValue(stream, value.toList()) } - is SyncEngineStatus -> { + is GeoHitResult -> { stream.write(219) writeValue(stream, value.toList()) } - is SyncAccountInfo -> { + is DownloadState -> { stream.write(220) writeValue(stream, value.toList()) } - is SyncDevice -> { + is ShareInternetResourceState -> { stream.write(221) writeValue(stream, value.toList()) } - is SyncIncomingTab -> { + is AddonCollection -> { stream.write(222) writeValue(stream, value.toList()) } - is SyncRemoteTab -> { + is SyncEngineStatus -> { stream.write(223) writeValue(stream, value.toList()) } - is SyncDeviceTabs -> { + is SyncAccountInfo -> { stream.write(224) writeValue(stream, value.toList()) } - is GeckoPref -> { + is SyncDevice -> { stream.write(225) writeValue(stream, value.toList()) } - is MlProgressData -> { + is SyncIncomingTab -> { stream.write(226) writeValue(stream, value.toList()) } - is ContainerSiteAssignment -> { + is SyncRemoteTab -> { stream.write(227) writeValue(stream, value.toList()) } - is GeckoHeader -> { + is SyncDeviceTabs -> { stream.write(228) writeValue(stream, value.toList()) } - is GeckoFetchRequest -> { + is GeckoPref -> { stream.write(229) writeValue(stream, value.toList()) } - is GeckoFetchResponse -> { + is MlProgressData -> { stream.write(230) writeValue(stream, value.toList()) } - is BookmarkNode -> { + is ContainerSiteAssignment -> { stream.write(231) writeValue(stream, value.toList()) } - is BookmarkInfo -> { + is GeckoHeader -> { stream.write(232) writeValue(stream, value.toList()) } - is SitePermissions -> { + is GeckoFetchRequest -> { stream.write(233) writeValue(stream, value.toList()) } - is TrackingProtectionException -> { + is GeckoFetchResponse -> { stream.write(234) writeValue(stream, value.toList()) } - is PwaIcon -> { + is BookmarkNode -> { stream.write(235) writeValue(stream, value.toList()) } - is ShareTargetFiles -> { + is BookmarkInfo -> { stream.write(236) writeValue(stream, value.toList()) } - is ShareTargetParams -> { + is SitePermissions -> { stream.write(237) writeValue(stream, value.toList()) } - is ShareTarget -> { + is TrackingProtectionException -> { stream.write(238) writeValue(stream, value.toList()) } - is ExternalApplicationResource -> { + is PwaIcon -> { stream.write(239) writeValue(stream, value.toList()) } - is PwaManifest -> { + is ShareTargetFiles -> { stream.write(240) writeValue(stream, value.toList()) } + is ShareTargetParams -> { + stream.write(241) + writeValue(stream, value.toList()) + } + is ShareTarget -> { + stream.write(242) + writeValue(stream, value.toList()) + } + is ExternalApplicationResource -> { + stream.write(243) + writeValue(stream, value.toList()) + } + is PwaManifest -> { + stream.write(244) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -8486,6 +8773,8 @@ interface GeckoAddonsApi { fun getAddons(allowCache: Boolean, callback: (Result>) -> Unit) fun getAddonById(addonId: String, allowCache: Boolean, callback: (Result) -> Unit) fun getAddonStoreInfo(addonId: String, callback: (Result) -> Unit) + fun searchAddonListings(query: String, app: AddonStoreApp, page: Long, pageSize: Long, callback: (Result>) -> Unit) + fun getFeaturedAddonListings(app: AddonStoreApp, pageSize: Long, callback: (Result>) -> Unit) fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType) fun enableAddon(addonId: String, callback: (Result) -> Unit) fun disableAddon(addonId: String, callback: (Result) -> Unit) @@ -8569,6 +8858,50 @@ interface GeckoAddonsApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.searchAddonListings$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val queryArg = args[0] as String + val appArg = args[1] as AddonStoreApp + val pageArg = args[2] as Long + val pageSizeArg = args[3] as Long + api.searchAddonListings(queryArg, appArg, pageArg, pageSizeArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getFeaturedAddonListings$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val appArg = args[0] as AddonStoreApp + val pageSizeArg = args[1] as Long + api.getFeaturedAddonListings(appArg, pageSizeArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$separatedMessageChannelSuffix", codec) if (api != null) { diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart index 562874e4..7dfcad27 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -41,7 +41,11 @@ export 'src/pigeons/gecko.g.dart' AddonDisabledReason, AddonIncognito, AddonInfo, + AddonListing, + AddonListingPreview, + AddonStoreApp, AddonStoreInfo, + AddonStorePromoted, AddonUpdateAttemptInfo, AddonUpdateStatus, AppLinksMode, diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_addon.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_addon.dart index 0208fbfe..a5e84589 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_addon.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_addon.dart @@ -49,6 +49,22 @@ class GeckoAddonService extends GeckoAddonEvents { return _api.getAddonStoreInfo(addonId); } + Future> searchAddonListings({ + required String query, + required AddonStoreApp app, + int page = 1, + int pageSize = 25, + }) { + return _api.searchAddonListings(query, app, page, pageSize); + } + + Future> getFeaturedAddonListings({ + required AddonStoreApp app, + int pageSize = 25, + }) { + return _api.getFeaturedAddonListings(app, pageSize); + } + Future invokeAddonAction( String extensionId, WebExtensionActionType actionType, diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index bf47c52f..008237d2 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -224,6 +224,17 @@ enum AddonUpdateStatus { error, } +enum AddonStoreApp { + android, + firefox, +} + +enum AddonStorePromoted { + none, + recommended, + line, +} + enum GeckoSuggestionType { session, clipboard, @@ -2619,20 +2630,291 @@ class AddonInfo { int get hashCode => _deepHash([runtimeType, ..._toList()]); } +class AddonListingPreview { + AddonListingPreview({ + required this.imageUrl, + this.thumbnailUrl, + this.caption, + }); + + String imageUrl; + + String? thumbnailUrl; + + String? caption; + + List _toList() { + return [ + imageUrl, + thumbnailUrl, + caption, + ]; + } + + Object encode() { + return _toList(); } + + static AddonListingPreview decode(Object result) { + result as List; + return AddonListingPreview( + imageUrl: result[0]! as String, + thumbnailUrl: result[1] as String?, + caption: result[2] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! AddonListingPreview || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(imageUrl, other.imageUrl) && _deepEquals(thumbnailUrl, other.thumbnailUrl) && _deepEquals(caption, other.caption); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class AddonListing { + AddonListing({ + required this.id, + required this.name, + this.summary, + this.description, + this.iconUrl, + required this.latestVersion, + required this.downloadUrl, + this.ratingAverage, + this.ratingReviews, + this.authorName, + this.authorUrl, + this.homepageUrl, + required this.detailUrl, + this.ratingUrl, + this.averageDailyUsers, + required this.promoted, + required this.previews, + required this.permissions, + required this.hostPermissions, + required this.optionalPermissions, + required this.dataCollectionPermissions, + this.fileSize, + this.lastUpdated, + this.licenseName, + this.licenseUrl, + this.supportUrl, + this.supportEmail, + required this.categories, + required this.hasPrivacyPolicy, + this.slug, + }); + + String id; + + String name; + + String? summary; + + String? description; + + String? iconUrl; + + String latestVersion; + + String downloadUrl; + + double? ratingAverage; + + int? ratingReviews; + + String? authorName; + + String? authorUrl; + + String? homepageUrl; + + String detailUrl; + + String? ratingUrl; + + int? averageDailyUsers; + + AddonStorePromoted promoted; + + List previews; + + List permissions; + + List hostPermissions; + + List optionalPermissions; + + List dataCollectionPermissions; + + int? fileSize; + + String? lastUpdated; + + String? licenseName; + + String? licenseUrl; + + String? supportUrl; + + String? supportEmail; + + List categories; + + bool hasPrivacyPolicy; + + String? slug; + + List _toList() { + return [ + id, + name, + summary, + description, + iconUrl, + latestVersion, + downloadUrl, + ratingAverage, + ratingReviews, + authorName, + authorUrl, + homepageUrl, + detailUrl, + ratingUrl, + averageDailyUsers, + promoted, + previews, + permissions, + hostPermissions, + optionalPermissions, + dataCollectionPermissions, + fileSize, + lastUpdated, + licenseName, + licenseUrl, + supportUrl, + supportEmail, + categories, + hasPrivacyPolicy, + slug, + ]; + } + + Object encode() { + return _toList(); } + + static AddonListing decode(Object result) { + result as List; + return AddonListing( + id: result[0]! as String, + name: result[1]! as String, + summary: result[2] as String?, + description: result[3] as String?, + iconUrl: result[4] as String?, + latestVersion: result[5]! as String, + downloadUrl: result[6]! as String, + ratingAverage: result[7] as double?, + ratingReviews: result[8] as int?, + authorName: result[9] as String?, + authorUrl: result[10] as String?, + homepageUrl: result[11] as String?, + detailUrl: result[12]! as String, + ratingUrl: result[13] as String?, + averageDailyUsers: result[14] as int?, + promoted: result[15]! as AddonStorePromoted, + previews: (result[16]! as List).cast(), + permissions: (result[17]! as List).cast(), + hostPermissions: (result[18]! as List).cast(), + optionalPermissions: (result[19]! as List).cast(), + dataCollectionPermissions: (result[20]! as List).cast(), + fileSize: result[21] as int?, + lastUpdated: result[22] as String?, + licenseName: result[23] as String?, + licenseUrl: result[24] as String?, + supportUrl: result[25] as String?, + supportEmail: result[26] as String?, + categories: (result[27]! as List).cast(), + hasPrivacyPolicy: result[28]! as bool, + slug: result[29] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! AddonListing || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(id, other.id) && _deepEquals(name, other.name) && _deepEquals(summary, other.summary) && _deepEquals(description, other.description) && _deepEquals(iconUrl, other.iconUrl) && _deepEquals(latestVersion, other.latestVersion) && _deepEquals(downloadUrl, other.downloadUrl) && _deepEquals(ratingAverage, other.ratingAverage) && _deepEquals(ratingReviews, other.ratingReviews) && _deepEquals(authorName, other.authorName) && _deepEquals(authorUrl, other.authorUrl) && _deepEquals(homepageUrl, other.homepageUrl) && _deepEquals(detailUrl, other.detailUrl) && _deepEquals(ratingUrl, other.ratingUrl) && _deepEquals(averageDailyUsers, other.averageDailyUsers) && _deepEquals(promoted, other.promoted) && _deepEquals(previews, other.previews) && _deepEquals(permissions, other.permissions) && _deepEquals(hostPermissions, other.hostPermissions) && _deepEquals(optionalPermissions, other.optionalPermissions) && _deepEquals(dataCollectionPermissions, other.dataCollectionPermissions) && _deepEquals(fileSize, other.fileSize) && _deepEquals(lastUpdated, other.lastUpdated) && _deepEquals(licenseName, other.licenseName) && _deepEquals(licenseUrl, other.licenseUrl) && _deepEquals(supportUrl, other.supportUrl) && _deepEquals(supportEmail, other.supportEmail) && _deepEquals(categories, other.categories) && _deepEquals(hasPrivacyPolicy, other.hasPrivacyPolicy) && _deepEquals(slug, other.slug); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + class AddonStoreInfo { AddonStoreInfo({ required this.latestVersion, required this.latestXpiUrl, + this.ratingAverage, + this.ratingReviews, + this.summary, + this.description, + this.homepageUrl, + this.detailUrl, + this.ratingUrl, + this.authorName, + this.authorUrl, }); String latestVersion; String latestXpiUrl; + double? ratingAverage; + + int? ratingReviews; + + String? summary; + + String? description; + + String? homepageUrl; + + String? detailUrl; + + String? ratingUrl; + + String? authorName; + + String? authorUrl; + List _toList() { return [ latestVersion, latestXpiUrl, + ratingAverage, + ratingReviews, + summary, + description, + homepageUrl, + detailUrl, + ratingUrl, + authorName, + authorUrl, ]; } @@ -2644,6 +2926,15 @@ class AddonStoreInfo { return AddonStoreInfo( latestVersion: result[0]! as String, latestXpiUrl: result[1]! as String, + ratingAverage: result[2] as double?, + ratingReviews: result[3] as int?, + summary: result[4] as String?, + description: result[5] as String?, + homepageUrl: result[6] as String?, + detailUrl: result[7] as String?, + ratingUrl: result[8] as String?, + authorName: result[9] as String?, + authorUrl: result[10] as String?, ); } @@ -2656,7 +2947,7 @@ class AddonStoreInfo { if (identical(this, other)) { return true; } - return _deepEquals(latestVersion, other.latestVersion) && _deepEquals(latestXpiUrl, other.latestXpiUrl); + return _deepEquals(latestVersion, other.latestVersion) && _deepEquals(latestXpiUrl, other.latestXpiUrl) && _deepEquals(ratingAverage, other.ratingAverage) && _deepEquals(ratingReviews, other.ratingReviews) && _deepEquals(summary, other.summary) && _deepEquals(description, other.description) && _deepEquals(homepageUrl, other.homepageUrl) && _deepEquals(detailUrl, other.detailUrl) && _deepEquals(ratingUrl, other.ratingUrl) && _deepEquals(authorName, other.authorName) && _deepEquals(authorUrl, other.authorUrl); } @override @@ -5326,306 +5617,318 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is AddonUpdateStatus) { buffer.putUint8(140); writeValue(buffer, value.index); - } else if (value is GeckoSuggestionType) { + } else if (value is AddonStoreApp) { buffer.putUint8(141); writeValue(buffer, value.index); - } else if (value is TrackingProtectionPolicy) { + } else if (value is AddonStorePromoted) { buffer.putUint8(142); writeValue(buffer, value.index); - } else if (value is HttpsOnlyMode) { + } else if (value is GeckoSuggestionType) { buffer.putUint8(143); writeValue(buffer, value.index); - } else if (value is QueryParameterStripping) { + } else if (value is TrackingProtectionPolicy) { buffer.putUint8(144); writeValue(buffer, value.index); - } else if (value is BounceTrackingProtectionMode) { + } else if (value is HttpsOnlyMode) { buffer.putUint8(145); writeValue(buffer, value.index); - } else if (value is ColorScheme) { + } else if (value is QueryParameterStripping) { buffer.putUint8(146); writeValue(buffer, value.index); - } else if (value is CookieBannerHandlingMode) { + } else if (value is BounceTrackingProtectionMode) { buffer.putUint8(147); writeValue(buffer, value.index); - } else if (value is AppLinksMode) { + } else if (value is ColorScheme) { buffer.putUint8(148); writeValue(buffer, value.index); - } else if (value is WebContentIsolationStrategy) { + } else if (value is CookieBannerHandlingMode) { buffer.putUint8(149); writeValue(buffer, value.index); - } else if (value is CustomCookiePolicy) { + } else if (value is AppLinksMode) { buffer.putUint8(150); writeValue(buffer, value.index); - } else if (value is TrackingScope) { + } else if (value is WebContentIsolationStrategy) { buffer.putUint8(151); writeValue(buffer, value.index); - } else if (value is DohSettingsMode) { + } else if (value is CustomCookiePolicy) { buffer.putUint8(152); writeValue(buffer, value.index); - } else if (value is DownloadStatus) { + } else if (value is TrackingScope) { buffer.putUint8(153); writeValue(buffer, value.index); - } else if (value is LogLevel) { + } else if (value is DohSettingsMode) { buffer.putUint8(154); writeValue(buffer, value.index); - } else if (value is SyncEngineValue) { + } else if (value is DownloadStatus) { buffer.putUint8(155); writeValue(buffer, value.index); - } else if (value is MlProgressType) { + } else if (value is LogLevel) { buffer.putUint8(156); writeValue(buffer, value.index); - } else if (value is MlProgressStatus) { + } else if (value is SyncEngineValue) { buffer.putUint8(157); writeValue(buffer, value.index); - } else if (value is ClearDataType) { + } else if (value is MlProgressType) { buffer.putUint8(158); writeValue(buffer, value.index); - } else if (value is GeckoFetchMethod) { + } else if (value is MlProgressStatus) { buffer.putUint8(159); writeValue(buffer, value.index); - } else if (value is GeckoFetchRedircet) { + } else if (value is ClearDataType) { buffer.putUint8(160); writeValue(buffer, value.index); - } else if (value is GeckoFetchCookiePolicy) { + } else if (value is GeckoFetchMethod) { buffer.putUint8(161); writeValue(buffer, value.index); - } else if (value is BookmarkNodeType) { + } else if (value is GeckoFetchRedircet) { buffer.putUint8(162); writeValue(buffer, value.index); - } else if (value is SitePermissionStatus) { + } else if (value is GeckoFetchCookiePolicy) { buffer.putUint8(163); writeValue(buffer, value.index); - } else if (value is AutoplayStatus) { + } else if (value is BookmarkNodeType) { buffer.putUint8(164); writeValue(buffer, value.index); - } else if (value is TranslationOptions) { + } else if (value is SitePermissionStatus) { buffer.putUint8(165); - writeValue(buffer, value.encode()); - } else if (value is TranslationLanguage) { + writeValue(buffer, value.index); + } else if (value is AutoplayStatus) { buffer.putUint8(166); - writeValue(buffer, value.encode()); - } else if (value is TranslationDetectedLanguages) { + writeValue(buffer, value.index); + } else if (value is TranslationOptions) { buffer.putUint8(167); writeValue(buffer, value.encode()); - } else if (value is TranslationPair) { + } else if (value is TranslationLanguage) { buffer.putUint8(168); writeValue(buffer, value.encode()); - } else if (value is TranslationEngineStateData) { + } else if (value is TranslationDetectedLanguages) { buffer.putUint8(169); writeValue(buffer, value.encode()); - } else if (value is TabTranslationStateData) { + } else if (value is TranslationPair) { buffer.putUint8(170); writeValue(buffer, value.encode()); - } else if (value is ReaderState) { + } else if (value is TranslationEngineStateData) { buffer.putUint8(171); writeValue(buffer, value.encode()); - } else if (value is AddTabParams) { + } else if (value is TabTranslationStateData) { buffer.putUint8(172); writeValue(buffer, value.encode()); - } else if (value is LastMediaAccessState) { + } else if (value is ReaderState) { buffer.putUint8(173); writeValue(buffer, value.encode()); - } else if (value is HistoryMetadataKey) { + } else if (value is AddTabParams) { buffer.putUint8(174); writeValue(buffer, value.encode()); - } else if (value is PackageCategoryValue) { + } else if (value is LastMediaAccessState) { buffer.putUint8(175); writeValue(buffer, value.encode()); - } else if (value is ExternalPackage) { + } else if (value is HistoryMetadataKey) { buffer.putUint8(176); writeValue(buffer, value.encode()); - } else if (value is LoadUrlFlagsValue) { + } else if (value is PackageCategoryValue) { buffer.putUint8(177); writeValue(buffer, value.encode()); - } else if (value is SourceValue) { + } else if (value is ExternalPackage) { buffer.putUint8(178); writeValue(buffer, value.encode()); - } else if (value is TabState) { + } else if (value is LoadUrlFlagsValue) { buffer.putUint8(179); writeValue(buffer, value.encode()); - } else if (value is RecoverableTab) { + } else if (value is SourceValue) { buffer.putUint8(180); writeValue(buffer, value.encode()); - } else if (value is IconRequest) { + } else if (value is TabState) { buffer.putUint8(181); writeValue(buffer, value.encode()); - } else if (value is ResourceSize) { + } else if (value is RecoverableTab) { buffer.putUint8(182); writeValue(buffer, value.encode()); - } else if (value is Resource) { + } else if (value is IconRequest) { buffer.putUint8(183); writeValue(buffer, value.encode()); - } else if (value is IconResult) { + } else if (value is ResourceSize) { buffer.putUint8(184); writeValue(buffer, value.encode()); - } else if (value is CookiePartitionKey) { + } else if (value is Resource) { buffer.putUint8(185); writeValue(buffer, value.encode()); - } else if (value is Cookie) { + } else if (value is IconResult) { buffer.putUint8(186); writeValue(buffer, value.encode()); - } else if (value is VisitInfo) { + } else if (value is CookiePartitionKey) { buffer.putUint8(187); writeValue(buffer, value.encode()); - } else if (value is HistoryHighlightWeights) { + } else if (value is Cookie) { buffer.putUint8(188); writeValue(buffer, value.encode()); - } else if (value is HistoryHighlight) { + } else if (value is VisitInfo) { buffer.putUint8(189); writeValue(buffer, value.encode()); - } else if (value is TopFrecentSiteInfo) { + } else if (value is HistoryHighlightWeights) { buffer.putUint8(190); writeValue(buffer, value.encode()); - } else if (value is HistoryItem) { + } else if (value is HistoryHighlight) { buffer.putUint8(191); writeValue(buffer, value.encode()); - } else if (value is HistoryState) { + } else if (value is TopFrecentSiteInfo) { buffer.putUint8(192); writeValue(buffer, value.encode()); - } else if (value is ReaderableState) { + } else if (value is HistoryItem) { buffer.putUint8(193); writeValue(buffer, value.encode()); - } else if (value is SecurityInfoState) { + } else if (value is HistoryState) { buffer.putUint8(194); writeValue(buffer, value.encode()); - } else if (value is TabContentState) { + } else if (value is ReaderableState) { buffer.putUint8(195); writeValue(buffer, value.encode()); - } else if (value is FindResultState) { + } else if (value is SecurityInfoState) { buffer.putUint8(196); writeValue(buffer, value.encode()); - } else if (value is CustomSelectionAction) { + } else if (value is TabContentState) { buffer.putUint8(197); writeValue(buffer, value.encode()); - } else if (value is WebExtensionData) { + } else if (value is FindResultState) { buffer.putUint8(198); writeValue(buffer, value.encode()); - } else if (value is AddonInfo) { + } else if (value is CustomSelectionAction) { buffer.putUint8(199); writeValue(buffer, value.encode()); - } else if (value is AddonStoreInfo) { + } else if (value is WebExtensionData) { buffer.putUint8(200); writeValue(buffer, value.encode()); - } else if (value is AddonUpdateAttemptInfo) { + } else if (value is AddonInfo) { buffer.putUint8(201); writeValue(buffer, value.encode()); - } else if (value is GeckoSuggestion) { + } else if (value is AddonListingPreview) { buffer.putUint8(202); writeValue(buffer, value.encode()); - } else if (value is TabContent) { + } else if (value is AddonListing) { buffer.putUint8(203); writeValue(buffer, value.encode()); - } else if (value is ContentBlocking) { + } else if (value is AddonStoreInfo) { buffer.putUint8(204); writeValue(buffer, value.encode()); - } else if (value is DohSettings) { + } else if (value is AddonUpdateAttemptInfo) { buffer.putUint8(205); writeValue(buffer, value.encode()); - } else if (value is GeckoEngineSettings) { + } else if (value is GeckoSuggestion) { buffer.putUint8(206); writeValue(buffer, value.encode()); - } else if (value is AutocompleteResult) { + } else if (value is TabContent) { buffer.putUint8(207); writeValue(buffer, value.encode()); - } else if (value is UnknownHitResult) { + } else if (value is ContentBlocking) { buffer.putUint8(208); writeValue(buffer, value.encode()); - } else if (value is ImageHitResult) { + } else if (value is DohSettings) { buffer.putUint8(209); writeValue(buffer, value.encode()); - } else if (value is VideoHitResult) { + } else if (value is GeckoEngineSettings) { buffer.putUint8(210); writeValue(buffer, value.encode()); - } else if (value is AudioHitResult) { + } else if (value is AutocompleteResult) { buffer.putUint8(211); writeValue(buffer, value.encode()); - } else if (value is ImageSrcHitResult) { + } else if (value is UnknownHitResult) { buffer.putUint8(212); writeValue(buffer, value.encode()); - } else if (value is PhoneHitResult) { + } else if (value is ImageHitResult) { buffer.putUint8(213); writeValue(buffer, value.encode()); - } else if (value is EmailHitResult) { + } else if (value is VideoHitResult) { buffer.putUint8(214); writeValue(buffer, value.encode()); - } else if (value is GeoHitResult) { + } else if (value is AudioHitResult) { buffer.putUint8(215); writeValue(buffer, value.encode()); - } else if (value is DownloadState) { + } else if (value is ImageSrcHitResult) { buffer.putUint8(216); writeValue(buffer, value.encode()); - } else if (value is ShareInternetResourceState) { + } else if (value is PhoneHitResult) { buffer.putUint8(217); writeValue(buffer, value.encode()); - } else if (value is AddonCollection) { + } else if (value is EmailHitResult) { buffer.putUint8(218); writeValue(buffer, value.encode()); - } else if (value is SyncEngineStatus) { + } else if (value is GeoHitResult) { buffer.putUint8(219); writeValue(buffer, value.encode()); - } else if (value is SyncAccountInfo) { + } else if (value is DownloadState) { buffer.putUint8(220); writeValue(buffer, value.encode()); - } else if (value is SyncDevice) { + } else if (value is ShareInternetResourceState) { buffer.putUint8(221); writeValue(buffer, value.encode()); - } else if (value is SyncIncomingTab) { + } else if (value is AddonCollection) { buffer.putUint8(222); writeValue(buffer, value.encode()); - } else if (value is SyncRemoteTab) { + } else if (value is SyncEngineStatus) { buffer.putUint8(223); writeValue(buffer, value.encode()); - } else if (value is SyncDeviceTabs) { + } else if (value is SyncAccountInfo) { buffer.putUint8(224); writeValue(buffer, value.encode()); - } else if (value is GeckoPref) { + } else if (value is SyncDevice) { buffer.putUint8(225); writeValue(buffer, value.encode()); - } else if (value is MlProgressData) { + } else if (value is SyncIncomingTab) { buffer.putUint8(226); writeValue(buffer, value.encode()); - } else if (value is ContainerSiteAssignment) { + } else if (value is SyncRemoteTab) { buffer.putUint8(227); writeValue(buffer, value.encode()); - } else if (value is GeckoHeader) { + } else if (value is SyncDeviceTabs) { buffer.putUint8(228); writeValue(buffer, value.encode()); - } else if (value is GeckoFetchRequest) { + } else if (value is GeckoPref) { buffer.putUint8(229); writeValue(buffer, value.encode()); - } else if (value is GeckoFetchResponse) { + } else if (value is MlProgressData) { buffer.putUint8(230); writeValue(buffer, value.encode()); - } else if (value is BookmarkNode) { + } else if (value is ContainerSiteAssignment) { buffer.putUint8(231); writeValue(buffer, value.encode()); - } else if (value is BookmarkInfo) { + } else if (value is GeckoHeader) { buffer.putUint8(232); writeValue(buffer, value.encode()); - } else if (value is SitePermissions) { + } else if (value is GeckoFetchRequest) { buffer.putUint8(233); writeValue(buffer, value.encode()); - } else if (value is TrackingProtectionException) { + } else if (value is GeckoFetchResponse) { buffer.putUint8(234); writeValue(buffer, value.encode()); - } else if (value is PwaIcon) { + } else if (value is BookmarkNode) { buffer.putUint8(235); writeValue(buffer, value.encode()); - } else if (value is ShareTargetFiles) { + } else if (value is BookmarkInfo) { buffer.putUint8(236); writeValue(buffer, value.encode()); - } else if (value is ShareTargetParams) { + } else if (value is SitePermissions) { buffer.putUint8(237); writeValue(buffer, value.encode()); - } else if (value is ShareTarget) { + } else if (value is TrackingProtectionException) { buffer.putUint8(238); writeValue(buffer, value.encode()); - } else if (value is ExternalApplicationResource) { + } else if (value is PwaIcon) { buffer.putUint8(239); writeValue(buffer, value.encode()); - } else if (value is PwaManifest) { + } else if (value is ShareTargetFiles) { buffer.putUint8(240); writeValue(buffer, value.encode()); + } else if (value is ShareTargetParams) { + buffer.putUint8(241); + writeValue(buffer, value.encode()); + } else if (value is ShareTarget) { + buffer.putUint8(242); + writeValue(buffer, value.encode()); + } else if (value is ExternalApplicationResource) { + buffer.putUint8(243); + writeValue(buffer, value.encode()); + } else if (value is PwaManifest) { + buffer.putUint8(244); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -5672,227 +5975,237 @@ class _PigeonCodec extends StandardMessageCodec { return value == null ? null : AddonUpdateStatus.values[value]; case 141: final value = readValue(buffer) as int?; - return value == null ? null : GeckoSuggestionType.values[value]; + return value == null ? null : AddonStoreApp.values[value]; case 142: final value = readValue(buffer) as int?; - return value == null ? null : TrackingProtectionPolicy.values[value]; + return value == null ? null : AddonStorePromoted.values[value]; case 143: final value = readValue(buffer) as int?; - return value == null ? null : HttpsOnlyMode.values[value]; + return value == null ? null : GeckoSuggestionType.values[value]; case 144: final value = readValue(buffer) as int?; - return value == null ? null : QueryParameterStripping.values[value]; + return value == null ? null : TrackingProtectionPolicy.values[value]; case 145: final value = readValue(buffer) as int?; - return value == null ? null : BounceTrackingProtectionMode.values[value]; + return value == null ? null : HttpsOnlyMode.values[value]; case 146: final value = readValue(buffer) as int?; - return value == null ? null : ColorScheme.values[value]; + return value == null ? null : QueryParameterStripping.values[value]; case 147: final value = readValue(buffer) as int?; - return value == null ? null : CookieBannerHandlingMode.values[value]; + return value == null ? null : BounceTrackingProtectionMode.values[value]; case 148: final value = readValue(buffer) as int?; - return value == null ? null : AppLinksMode.values[value]; + return value == null ? null : ColorScheme.values[value]; case 149: final value = readValue(buffer) as int?; - return value == null ? null : WebContentIsolationStrategy.values[value]; + return value == null ? null : CookieBannerHandlingMode.values[value]; case 150: final value = readValue(buffer) as int?; - return value == null ? null : CustomCookiePolicy.values[value]; + return value == null ? null : AppLinksMode.values[value]; case 151: final value = readValue(buffer) as int?; - return value == null ? null : TrackingScope.values[value]; + return value == null ? null : WebContentIsolationStrategy.values[value]; case 152: final value = readValue(buffer) as int?; - return value == null ? null : DohSettingsMode.values[value]; + return value == null ? null : CustomCookiePolicy.values[value]; case 153: final value = readValue(buffer) as int?; - return value == null ? null : DownloadStatus.values[value]; + return value == null ? null : TrackingScope.values[value]; case 154: final value = readValue(buffer) as int?; - return value == null ? null : LogLevel.values[value]; + return value == null ? null : DohSettingsMode.values[value]; case 155: final value = readValue(buffer) as int?; - return value == null ? null : SyncEngineValue.values[value]; + return value == null ? null : DownloadStatus.values[value]; case 156: final value = readValue(buffer) as int?; - return value == null ? null : MlProgressType.values[value]; + return value == null ? null : LogLevel.values[value]; case 157: final value = readValue(buffer) as int?; - return value == null ? null : MlProgressStatus.values[value]; + return value == null ? null : SyncEngineValue.values[value]; case 158: final value = readValue(buffer) as int?; - return value == null ? null : ClearDataType.values[value]; + return value == null ? null : MlProgressType.values[value]; case 159: final value = readValue(buffer) as int?; - return value == null ? null : GeckoFetchMethod.values[value]; + return value == null ? null : MlProgressStatus.values[value]; case 160: final value = readValue(buffer) as int?; - return value == null ? null : GeckoFetchRedircet.values[value]; + return value == null ? null : ClearDataType.values[value]; case 161: final value = readValue(buffer) as int?; - return value == null ? null : GeckoFetchCookiePolicy.values[value]; + return value == null ? null : GeckoFetchMethod.values[value]; case 162: final value = readValue(buffer) as int?; - return value == null ? null : BookmarkNodeType.values[value]; + return value == null ? null : GeckoFetchRedircet.values[value]; case 163: final value = readValue(buffer) as int?; - return value == null ? null : SitePermissionStatus.values[value]; + return value == null ? null : GeckoFetchCookiePolicy.values[value]; case 164: final value = readValue(buffer) as int?; - return value == null ? null : AutoplayStatus.values[value]; + return value == null ? null : BookmarkNodeType.values[value]; case 165: - return TranslationOptions.decode(readValue(buffer)!); + final value = readValue(buffer) as int?; + return value == null ? null : SitePermissionStatus.values[value]; case 166: - return TranslationLanguage.decode(readValue(buffer)!); + final value = readValue(buffer) as int?; + return value == null ? null : AutoplayStatus.values[value]; case 167: - return TranslationDetectedLanguages.decode(readValue(buffer)!); + return TranslationOptions.decode(readValue(buffer)!); case 168: - return TranslationPair.decode(readValue(buffer)!); + return TranslationLanguage.decode(readValue(buffer)!); case 169: - return TranslationEngineStateData.decode(readValue(buffer)!); + return TranslationDetectedLanguages.decode(readValue(buffer)!); case 170: - return TabTranslationStateData.decode(readValue(buffer)!); + return TranslationPair.decode(readValue(buffer)!); case 171: - return ReaderState.decode(readValue(buffer)!); + return TranslationEngineStateData.decode(readValue(buffer)!); case 172: - return AddTabParams.decode(readValue(buffer)!); + return TabTranslationStateData.decode(readValue(buffer)!); case 173: - return LastMediaAccessState.decode(readValue(buffer)!); + return ReaderState.decode(readValue(buffer)!); case 174: - return HistoryMetadataKey.decode(readValue(buffer)!); + return AddTabParams.decode(readValue(buffer)!); case 175: - return PackageCategoryValue.decode(readValue(buffer)!); + return LastMediaAccessState.decode(readValue(buffer)!); case 176: - return ExternalPackage.decode(readValue(buffer)!); + return HistoryMetadataKey.decode(readValue(buffer)!); case 177: - return LoadUrlFlagsValue.decode(readValue(buffer)!); + return PackageCategoryValue.decode(readValue(buffer)!); case 178: - return SourceValue.decode(readValue(buffer)!); + return ExternalPackage.decode(readValue(buffer)!); case 179: - return TabState.decode(readValue(buffer)!); + return LoadUrlFlagsValue.decode(readValue(buffer)!); case 180: - return RecoverableTab.decode(readValue(buffer)!); + return SourceValue.decode(readValue(buffer)!); case 181: - return IconRequest.decode(readValue(buffer)!); + return TabState.decode(readValue(buffer)!); case 182: - return ResourceSize.decode(readValue(buffer)!); + return RecoverableTab.decode(readValue(buffer)!); case 183: - return Resource.decode(readValue(buffer)!); + return IconRequest.decode(readValue(buffer)!); case 184: - return IconResult.decode(readValue(buffer)!); + return ResourceSize.decode(readValue(buffer)!); case 185: - return CookiePartitionKey.decode(readValue(buffer)!); + return Resource.decode(readValue(buffer)!); case 186: - return Cookie.decode(readValue(buffer)!); + return IconResult.decode(readValue(buffer)!); case 187: - return VisitInfo.decode(readValue(buffer)!); + return CookiePartitionKey.decode(readValue(buffer)!); case 188: - return HistoryHighlightWeights.decode(readValue(buffer)!); + return Cookie.decode(readValue(buffer)!); case 189: - return HistoryHighlight.decode(readValue(buffer)!); + return VisitInfo.decode(readValue(buffer)!); case 190: - return TopFrecentSiteInfo.decode(readValue(buffer)!); + return HistoryHighlightWeights.decode(readValue(buffer)!); case 191: - return HistoryItem.decode(readValue(buffer)!); + return HistoryHighlight.decode(readValue(buffer)!); case 192: - return HistoryState.decode(readValue(buffer)!); + return TopFrecentSiteInfo.decode(readValue(buffer)!); case 193: - return ReaderableState.decode(readValue(buffer)!); + return HistoryItem.decode(readValue(buffer)!); case 194: - return SecurityInfoState.decode(readValue(buffer)!); + return HistoryState.decode(readValue(buffer)!); case 195: - return TabContentState.decode(readValue(buffer)!); + return ReaderableState.decode(readValue(buffer)!); case 196: - return FindResultState.decode(readValue(buffer)!); + return SecurityInfoState.decode(readValue(buffer)!); case 197: - return CustomSelectionAction.decode(readValue(buffer)!); + return TabContentState.decode(readValue(buffer)!); case 198: - return WebExtensionData.decode(readValue(buffer)!); + return FindResultState.decode(readValue(buffer)!); case 199: - return AddonInfo.decode(readValue(buffer)!); + return CustomSelectionAction.decode(readValue(buffer)!); case 200: - return AddonStoreInfo.decode(readValue(buffer)!); + return WebExtensionData.decode(readValue(buffer)!); case 201: - return AddonUpdateAttemptInfo.decode(readValue(buffer)!); + return AddonInfo.decode(readValue(buffer)!); case 202: - return GeckoSuggestion.decode(readValue(buffer)!); + return AddonListingPreview.decode(readValue(buffer)!); case 203: - return TabContent.decode(readValue(buffer)!); + return AddonListing.decode(readValue(buffer)!); case 204: - return ContentBlocking.decode(readValue(buffer)!); + return AddonStoreInfo.decode(readValue(buffer)!); case 205: - return DohSettings.decode(readValue(buffer)!); + return AddonUpdateAttemptInfo.decode(readValue(buffer)!); case 206: - return GeckoEngineSettings.decode(readValue(buffer)!); + return GeckoSuggestion.decode(readValue(buffer)!); case 207: - return AutocompleteResult.decode(readValue(buffer)!); + return TabContent.decode(readValue(buffer)!); case 208: - return UnknownHitResult.decode(readValue(buffer)!); + return ContentBlocking.decode(readValue(buffer)!); case 209: - return ImageHitResult.decode(readValue(buffer)!); + return DohSettings.decode(readValue(buffer)!); case 210: - return VideoHitResult.decode(readValue(buffer)!); + return GeckoEngineSettings.decode(readValue(buffer)!); case 211: - return AudioHitResult.decode(readValue(buffer)!); + return AutocompleteResult.decode(readValue(buffer)!); case 212: - return ImageSrcHitResult.decode(readValue(buffer)!); + return UnknownHitResult.decode(readValue(buffer)!); case 213: - return PhoneHitResult.decode(readValue(buffer)!); + return ImageHitResult.decode(readValue(buffer)!); case 214: - return EmailHitResult.decode(readValue(buffer)!); + return VideoHitResult.decode(readValue(buffer)!); case 215: - return GeoHitResult.decode(readValue(buffer)!); + return AudioHitResult.decode(readValue(buffer)!); case 216: - return DownloadState.decode(readValue(buffer)!); + return ImageSrcHitResult.decode(readValue(buffer)!); case 217: - return ShareInternetResourceState.decode(readValue(buffer)!); + return PhoneHitResult.decode(readValue(buffer)!); case 218: - return AddonCollection.decode(readValue(buffer)!); + return EmailHitResult.decode(readValue(buffer)!); case 219: - return SyncEngineStatus.decode(readValue(buffer)!); + return GeoHitResult.decode(readValue(buffer)!); case 220: - return SyncAccountInfo.decode(readValue(buffer)!); + return DownloadState.decode(readValue(buffer)!); case 221: - return SyncDevice.decode(readValue(buffer)!); + return ShareInternetResourceState.decode(readValue(buffer)!); case 222: - return SyncIncomingTab.decode(readValue(buffer)!); + return AddonCollection.decode(readValue(buffer)!); case 223: - return SyncRemoteTab.decode(readValue(buffer)!); + return SyncEngineStatus.decode(readValue(buffer)!); case 224: - return SyncDeviceTabs.decode(readValue(buffer)!); + return SyncAccountInfo.decode(readValue(buffer)!); case 225: - return GeckoPref.decode(readValue(buffer)!); + return SyncDevice.decode(readValue(buffer)!); case 226: - return MlProgressData.decode(readValue(buffer)!); + return SyncIncomingTab.decode(readValue(buffer)!); case 227: - return ContainerSiteAssignment.decode(readValue(buffer)!); + return SyncRemoteTab.decode(readValue(buffer)!); case 228: - return GeckoHeader.decode(readValue(buffer)!); + return SyncDeviceTabs.decode(readValue(buffer)!); case 229: - return GeckoFetchRequest.decode(readValue(buffer)!); + return GeckoPref.decode(readValue(buffer)!); case 230: - return GeckoFetchResponse.decode(readValue(buffer)!); + return MlProgressData.decode(readValue(buffer)!); case 231: - return BookmarkNode.decode(readValue(buffer)!); + return ContainerSiteAssignment.decode(readValue(buffer)!); case 232: - return BookmarkInfo.decode(readValue(buffer)!); + return GeckoHeader.decode(readValue(buffer)!); case 233: - return SitePermissions.decode(readValue(buffer)!); + return GeckoFetchRequest.decode(readValue(buffer)!); case 234: - return TrackingProtectionException.decode(readValue(buffer)!); + return GeckoFetchResponse.decode(readValue(buffer)!); case 235: - return PwaIcon.decode(readValue(buffer)!); + return BookmarkNode.decode(readValue(buffer)!); case 236: - return ShareTargetFiles.decode(readValue(buffer)!); + return BookmarkInfo.decode(readValue(buffer)!); case 237: - return ShareTargetParams.decode(readValue(buffer)!); + return SitePermissions.decode(readValue(buffer)!); case 238: - return ShareTarget.decode(readValue(buffer)!); + return TrackingProtectionException.decode(readValue(buffer)!); case 239: - return ExternalApplicationResource.decode(readValue(buffer)!); + return PwaIcon.decode(readValue(buffer)!); case 240: + return ShareTargetFiles.decode(readValue(buffer)!); + case 241: + return ShareTargetParams.decode(readValue(buffer)!); + case 242: + return ShareTarget.decode(readValue(buffer)!); + case 243: + return ExternalApplicationResource.decode(readValue(buffer)!); + case 244: return PwaManifest.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -8517,6 +8830,44 @@ class GeckoAddonsApi { return pigeonVar_replyValue as AddonStoreInfo?; } + Future> searchAddonListings(String query, AddonStoreApp app, int page, int pageSize) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.searchAddonListings$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([query, app, page, pageSize]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return (pigeonVar_replyValue! as List).cast(); + } + + Future> getFeaturedAddonListings(AddonStoreApp app, int pageSize) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.getFeaturedAddonListings$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([app, pageSize]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return (pigeonVar_replyValue! as List).cast(); + } + Future invokeAddonAction(String extensionId, WebExtensionActionType actionType) async { final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index 84dd85ca..0c57324b 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -749,13 +749,113 @@ class AddonInfo { }); } +enum AddonStoreApp { android, firefox } + +enum AddonStorePromoted { none, recommended, line } + +class AddonListingPreview { + final String imageUrl; + final String? thumbnailUrl; + final String? caption; + + const AddonListingPreview({ + required this.imageUrl, + this.thumbnailUrl, + this.caption, + }); +} + +class AddonListing { + final String id; + final String name; + final String? summary; + final String? description; + final String? iconUrl; + final String latestVersion; + final String downloadUrl; + final double? ratingAverage; + final int? ratingReviews; + final String? authorName; + final String? authorUrl; + final String? homepageUrl; + final String detailUrl; + final String? ratingUrl; + final int? averageDailyUsers; + final AddonStorePromoted promoted; + final List previews; + final List permissions; + final List hostPermissions; + final List optionalPermissions; + final List dataCollectionPermissions; + final int? fileSize; + final String? lastUpdated; + final String? licenseName; + final String? licenseUrl; + final String? supportUrl; + final String? supportEmail; + final List categories; + final bool hasPrivacyPolicy; + final String? slug; + + const AddonListing({ + required this.id, + required this.name, + this.summary, + this.description, + this.iconUrl, + required this.latestVersion, + required this.downloadUrl, + this.ratingAverage, + this.ratingReviews, + this.authorName, + this.authorUrl, + this.homepageUrl, + required this.detailUrl, + this.ratingUrl, + this.averageDailyUsers, + this.promoted = AddonStorePromoted.none, + this.previews = const [], + this.permissions = const [], + this.hostPermissions = const [], + this.optionalPermissions = const [], + this.dataCollectionPermissions = const [], + this.fileSize, + this.lastUpdated, + this.licenseName, + this.licenseUrl, + this.supportUrl, + this.supportEmail, + this.categories = const [], + this.hasPrivacyPolicy = false, + this.slug, + }); +} + class AddonStoreInfo { final String latestVersion; final String latestXpiUrl; + final double? ratingAverage; + final int? ratingReviews; + final String? summary; + final String? description; + final String? homepageUrl; + final String? detailUrl; + final String? ratingUrl; + final String? authorName; + final String? authorUrl; const AddonStoreInfo({ required this.latestVersion, required this.latestXpiUrl, + this.ratingAverage, + this.ratingReviews, + this.summary, + this.description, + this.homepageUrl, + this.detailUrl, + this.ratingUrl, + this.authorName, + this.authorUrl, }); } @@ -1853,6 +1953,20 @@ abstract class GeckoAddonsApi { @async AddonStoreInfo? getAddonStoreInfo(String addonId); + @async + List searchAddonListings( + String query, + AddonStoreApp app, + int page, + int pageSize, + ); + + @async + List getFeaturedAddonListings( + AddonStoreApp app, + int pageSize, + ); + void invokeAddonAction(String extensionId, WebExtensionActionType actionType); @async