diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index fd9abd09..ef7a5d7d 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -41,7 +41,7 @@ android:icon="@mipmap/launcher_icon" android:usesCleartextTraffic="true" android:enableOnBackInvokedCallback="true"> - + + + + + + + + + + + + + @@ -113,10 +149,10 @@ - diff --git a/app/ios/Runner/GeneratedPluginRegistrant.m b/app/ios/Runner/GeneratedPluginRegistrant.m index f8d2398b..5eeaf75d 100644 --- a/app/ios/Runner/GeneratedPluginRegistrant.m +++ b/app/ios/Runner/GeneratedPluginRegistrant.m @@ -12,12 +12,6 @@ @import file_picker; #endif -#if __has_include() -#import -#else -@import flutter_inappwebview_ios; -#endif - #if __has_include() #import #else @@ -82,7 +76,6 @@ + (void)registerWithRegistry:(NSObject*)registry { [FilePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FilePickerPlugin"]]; - [InAppWebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"InAppWebViewFlutterPlugin"]]; [PdfTextPlugin registerWithRegistrar:[registry registrarForPlugin:@"PdfTextPlugin"]]; [FlutterSecureStoragePlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterSecureStoragePlugin"]]; [FlutterSharingIntentPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterSharingIntentPlugin"]]; diff --git a/app/lib/core/providers.dart b/app/lib/core/providers.dart index 5df5c93b..8d389292 100644 --- a/app/lib/core/providers.dart +++ b/app/lib/core/providers.dart @@ -1,11 +1,12 @@ import 'package:go_router/go_router.dart'; import 'package:lensai/core/routing/routes.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; @Riverpod(keepAlive: true) -GoRouter router(RouterRef ref) => GoRouter( +GoRouter router(Ref ref) => GoRouter( debugLogDiagnostics: true, routes: $appRoutes, ); diff --git a/app/lib/core/providers.g.dart b/app/lib/core/providers.g.dart index 5d5f133f..05734119 100644 --- a/app/lib/core/providers.g.dart +++ b/app/lib/core/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$routerHash() => r'4ad45f430b1f43e427e21122d16bd761ca04ae0e'; +String _$routerHash() => r'dcafae2924a2ad3d5d836c6756d2e4a4ead36b4e'; /// See also [router]. @ProviderFor(router) @@ -19,6 +19,8 @@ final routerProvider = Provider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef RouterRef = ProviderRef; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/domain/entities/equatable_image.dart b/app/lib/domain/entities/equatable_image.dart index 773a8ac4..ca85a5f9 100644 --- a/app/lib/domain/entities/equatable_image.dart +++ b/app/lib/domain/entities/equatable_image.dart @@ -4,11 +4,11 @@ import 'package:lensai/extensions/image.dart'; class EquatableImage { final Image value; - final String? _imageHash; + final int? _imageHash; EquatableImage( this.value, { - required String? hash, + required int? hash, }) : _imageHash = hash; static Future calculate(Image image) async { diff --git a/app/lib/domain/services/app_initialization.g.dart b/app/lib/domain/services/app_initialization.g.dart index 91541cf3..7d593514 100644 --- a/app/lib/domain/services/app_initialization.g.dart +++ b/app/lib/domain/services/app_initialization.g.dart @@ -31,4 +31,4 @@ final appInitializationServiceProvider = NotifierProvider< typedef _$AppInitializationService = Notifier< Result<({bool initialized, String? stage, List errors})>>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/domain/services/generic_website.g.dart b/app/lib/domain/services/generic_website.g.dart index a3ad8894..1eb9bf70 100644 --- a/app/lib/domain/services/generic_website.g.dart +++ b/app/lib/domain/services/generic_website.g.dart @@ -24,4 +24,4 @@ final genericWebsiteServiceProvider = typedef _$GenericWebsiteService = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/extensions/image.dart b/app/lib/extensions/image.dart index cf2db883..31ea2012 100644 --- a/app/lib/extensions/image.dart +++ b/app/lib/extensions/image.dart @@ -1,14 +1,14 @@ import 'dart:ui'; -import 'package:crypto/crypto.dart'; import 'package:lensai/domain/entities/equatable_image.dart'; +import 'package:xxh3/xxh3.dart'; extension ImageHash on Image { - Future calculateHash() async { + Future calculateHash() async { final bytes = await toByteData(); if (bytes != null) { - final digest = sha1.convert(bytes.buffer.asUint8List()); - return digest.toString(); + final digest = xxh3(bytes.buffer.asUint8List()); + return digest; } return null; diff --git a/app/lib/features/about/data/repositories/package_info_repository.dart b/app/lib/features/about/data/repositories/package_info_repository.dart index 25341f62..519e240a 100644 --- a/app/lib/features/about/data/repositories/package_info_repository.dart +++ b/app/lib/features/about/data/repositories/package_info_repository.dart @@ -1,9 +1,10 @@ import 'package:package_info_plus/package_info_plus.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'package_info_repository.g.dart'; @Riverpod(keepAlive: true) -Future packageInfo(PackageInfoRef ref) { +Future packageInfo(Ref ref) { return PackageInfo.fromPlatform(); } diff --git a/app/lib/features/about/data/repositories/package_info_repository.g.dart b/app/lib/features/about/data/repositories/package_info_repository.g.dart index d0b6c152..c9b08f86 100644 --- a/app/lib/features/about/data/repositories/package_info_repository.g.dart +++ b/app/lib/features/about/data/repositories/package_info_repository.g.dart @@ -6,7 +6,7 @@ part of 'package_info_repository.dart'; // RiverpodGenerator // ************************************************************************** -String _$packageInfoHash() => r'f7bd90882137aac7a9f28e9d10eae6efeee8c9d0'; +String _$packageInfoHash() => r'cc57db7b4684ab0d5df0f050b8ea045a3658e89a'; /// See also [packageInfo]. @ProviderFor(packageInfo) @@ -19,6 +19,8 @@ final packageInfoProvider = FutureProvider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef PackageInfoRef = FutureProviderRef; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/app_widget/domain/services/home_widget.dart b/app/lib/features/app_widget/domain/services/home_widget.dart index 7a12820c..c72ab589 100644 --- a/app/lib/features/app_widget/domain/services/home_widget.dart +++ b/app/lib/features/app_widget/domain/services/home_widget.dart @@ -1,18 +1,19 @@ import 'package:home_widget/home_widget.dart'; import 'package:lensai/data/models/received_parameter.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; part 'home_widget.g.dart'; @Riverpod(keepAlive: true) -FutureOr widgetPinnable(WidgetPinnableRef ref) async { +FutureOr widgetPinnable(Ref ref) async { return await HomeWidget.isRequestPinWidgetSupported() ?? false; } @Riverpod() Raw> appWidgetLaunchStream( - AppWidgetLaunchStreamRef ref, + Ref ref, ) { // ignore: discarded_futures final initialStream = HomeWidget.initiallyLaunchedFromHomeWidget().asStream(); diff --git a/app/lib/features/app_widget/domain/services/home_widget.g.dart b/app/lib/features/app_widget/domain/services/home_widget.g.dart index 6b87905e..b01c11c3 100644 --- a/app/lib/features/app_widget/domain/services/home_widget.g.dart +++ b/app/lib/features/app_widget/domain/services/home_widget.g.dart @@ -6,7 +6,7 @@ part of 'home_widget.dart'; // RiverpodGenerator // ************************************************************************** -String _$widgetPinnableHash() => r'c39a8b473a92ff81fa772d7c235b10736e4a49bd'; +String _$widgetPinnableHash() => r'f91041f0f051ad0f00dd765cafea5571ca8c8088'; /// See also [widgetPinnable]. @ProviderFor(widgetPinnable) @@ -20,9 +20,11 @@ final widgetPinnableProvider = FutureProvider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef WidgetPinnableRef = FutureProviderRef; String _$appWidgetLaunchStreamHash() => - r'8c5e21346d492f89f546b4e32135a283719e70e1'; + r'8eb98d88e74cc4f7573968563c106b8e6beee289'; /// See also [appWidgetLaunchStream]. @ProviderFor(appWidgetLaunchStream) @@ -37,7 +39,9 @@ final appWidgetLaunchStreamProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef AppWidgetLaunchStreamRef = AutoDisposeProviderRef>>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/bangs/data/providers.dart b/app/lib/features/bangs/data/providers.dart index 469c0265..57189c22 100644 --- a/app/lib/features/bangs/data/providers.dart +++ b/app/lib/features/bangs/data/providers.dart @@ -3,6 +3,7 @@ import 'package:drift/native.dart'; import 'package:lensai/features/bangs/data/database/database.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; @@ -11,7 +12,7 @@ import 'package:universal_io/io.dart'; part 'providers.g.dart'; @Riverpod(keepAlive: true) -BangDatabase bangDatabase(BangDatabaseRef ref) { +BangDatabase bangDatabase(Ref ref) { return BangDatabase( LazyDatabase(() async { // put the database file, called db.sqlite here, into the documents folder diff --git a/app/lib/features/bangs/data/providers.g.dart b/app/lib/features/bangs/data/providers.g.dart index 3086995b..c469e88c 100644 --- a/app/lib/features/bangs/data/providers.g.dart +++ b/app/lib/features/bangs/data/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$bangDatabaseHash() => r'bbca2edf32faa378dcb6270b12019758d5156662'; +String _$bangDatabaseHash() => r'd7964e9cdbb6e60fb6d2a78f07bfd4fa34bcb236'; /// See also [bangDatabase]. @ProviderFor(bangDatabase) @@ -19,6 +19,8 @@ final bangDatabaseProvider = Provider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef BangDatabaseRef = ProviderRef; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/bangs/data/services/source.g.dart b/app/lib/features/bangs/data/services/source.g.dart index 5e931684..2e60ab3e 100644 --- a/app/lib/features/bangs/data/services/source.g.dart +++ b/app/lib/features/bangs/data/services/source.g.dart @@ -23,4 +23,4 @@ final bangSourceServiceProvider = typedef _$BangSourceService = AutoDisposeNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/bangs/domain/providers.dart b/app/lib/features/bangs/domain/providers.dart index e869ef78..75b79622 100644 --- a/app/lib/features/bangs/domain/providers.dart +++ b/app/lib/features/bangs/domain/providers.dart @@ -2,31 +2,32 @@ import 'package:lensai/features/bangs/data/models/bang.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/bangs/domain/repositories/sync.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; @Riverpod() -Stream bangData(BangDataRef ref, String? trigger) { +Stream bangData(Ref ref, String? trigger) { final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.watchBang(trigger); } @Riverpod(keepAlive: true) -Stream kagiSearchBangData(KagiSearchBangDataRef ref) { +Stream kagiSearchBangData(Ref ref) { final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.watchBang('kagi'); } @Riverpod() -Stream>> bangCategories(BangCategoriesRef ref) { +Stream>> bangCategories(Ref ref) { final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.watchCategories(); } @Riverpod() Stream> bangDataList( - BangDataListRef ref, { + Ref ref, { ({ Iterable? groups, String? domain, @@ -44,14 +45,14 @@ Stream> bangDataList( } @Riverpod() -Stream> frequentBangDataList(FrequentBangDataListRef ref) { +Stream> frequentBangDataList(Ref ref) { final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.watchFrequentBangs(); } @Riverpod() Future bangDataEnsureIcon( - BangDataEnsureIconRef ref, + Ref ref, BangData bang, ) { final repository = ref.watch(bangDataRepositoryProvider.notifier); @@ -62,7 +63,7 @@ Future bangDataEnsureIcon( @Riverpod() Stream lastSyncOfGroup( - LastSyncOfGroupRef ref, + Ref ref, BangGroup group, ) { final repository = ref.watch(bangSyncRepositoryProvider.notifier); @@ -71,7 +72,7 @@ Stream lastSyncOfGroup( @Riverpod() Stream bangCountOfGroup( - BangCountOfGroupRef ref, + Ref ref, BangGroup group, ) { final repository = ref.watch(bangDataRepositoryProvider.notifier); @@ -79,7 +80,7 @@ Stream bangCountOfGroup( } @Riverpod() -Stream bangIconCacheSizeMegabytes(BangIconCacheSizeMegabytesRef ref) { +Stream bangIconCacheSizeMegabytes(Ref ref) { final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.watchIconCacheSize(); } diff --git a/app/lib/features/bangs/domain/providers.g.dart b/app/lib/features/bangs/domain/providers.g.dart index 22de6866..b1db6907 100644 --- a/app/lib/features/bangs/domain/providers.g.dart +++ b/app/lib/features/bangs/domain/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$bangDataHash() => r'd0e96885458be8893c6b19735b8792bdd1b9bcb8'; +String _$bangDataHash() => r'057a002de9789ab74c8f3b337a43faa26b788d8d'; /// Copied from Dart SDK class _SystemHash { @@ -141,6 +141,8 @@ class BangDataProvider extends AutoDisposeStreamProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin BangDataRef on AutoDisposeStreamProviderRef { /// The parameter `trigger` of this provider. String? get trigger; @@ -155,7 +157,7 @@ class _BangDataProviderElement } String _$kagiSearchBangDataHash() => - r'4dbeaab0541d7c8214d097523d139eede2e3f975'; + r'0bb5014a7af73ef6f5a36b0545d7a73e7ad0580a'; /// See also [kagiSearchBangData]. @ProviderFor(kagiSearchBangData) @@ -169,8 +171,10 @@ final kagiSearchBangDataProvider = StreamProvider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef KagiSearchBangDataRef = StreamProviderRef; -String _$bangCategoriesHash() => r'dbc63b78208be90c2aa3c3d61b08f98818f4b89d'; +String _$bangCategoriesHash() => r'947fcfd2dffcc7f585c6ed7379d319f4fe72293a'; /// See also [bangCategories]. @ProviderFor(bangCategories) @@ -185,9 +189,11 @@ final bangCategoriesProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef BangCategoriesRef = AutoDisposeStreamProviderRef>>; -String _$bangDataListHash() => r'c4fbc1676be933b29ec88f503110c119fe8b6d86'; +String _$bangDataListHash() => r'cd034fdf046b156ac452edd0857ac054ab477917'; /// See also [bangDataList]. @ProviderFor(bangDataList) @@ -317,6 +323,8 @@ class BangDataListProvider extends AutoDisposeStreamProvider> { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin BangDataListRef on AutoDisposeStreamProviderRef> { /// The parameter `filter` of this provider. ({ @@ -342,7 +350,7 @@ class _BangDataListProviderElement } String _$frequentBangDataListHash() => - r'adf8d0b72adfa8faffadd7f1a294ce3945e22fec'; + r'27bc10ddf502266458af3910fff09bc8282a8e5c'; /// See also [frequentBangDataList]. @ProviderFor(frequentBangDataList) @@ -357,9 +365,11 @@ final frequentBangDataListProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef FrequentBangDataListRef = AutoDisposeStreamProviderRef>; String _$bangDataEnsureIconHash() => - r'69b59b582a7b165b8076a1478d0d433ec0846ccb'; + r'423b8e92fd89ec328c2fcc8f9e55365c30633fd2'; /// See also [bangDataEnsureIcon]. @ProviderFor(bangDataEnsureIcon) @@ -474,6 +484,8 @@ class BangDataEnsureIconProvider extends AutoDisposeFutureProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin BangDataEnsureIconRef on AutoDisposeFutureProviderRef { /// The parameter `bang` of this provider. BangData get bang; @@ -488,7 +500,7 @@ class _BangDataEnsureIconProviderElement BangData get bang => (origin as BangDataEnsureIconProvider).bang; } -String _$lastSyncOfGroupHash() => r'4906dc24970eccda89d94697f98cf70e241ea4ec'; +String _$lastSyncOfGroupHash() => r'23d07f3132ba9bb35a31f74e3a69698d31d4c569'; /// See also [lastSyncOfGroup]. @ProviderFor(lastSyncOfGroup) @@ -603,6 +615,8 @@ class LastSyncOfGroupProvider extends AutoDisposeStreamProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin LastSyncOfGroupRef on AutoDisposeStreamProviderRef { /// The parameter `group` of this provider. BangGroup get group; @@ -617,7 +631,7 @@ class _LastSyncOfGroupProviderElement BangGroup get group => (origin as LastSyncOfGroupProvider).group; } -String _$bangCountOfGroupHash() => r'10b363679230c0abf6a6ba7d0be2d0367a7ed215'; +String _$bangCountOfGroupHash() => r'211ffcd7f49b637a7953f913dc5eafda344423f3'; /// See also [bangCountOfGroup]. @ProviderFor(bangCountOfGroup) @@ -732,6 +746,8 @@ class BangCountOfGroupProvider extends AutoDisposeStreamProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin BangCountOfGroupRef on AutoDisposeStreamProviderRef { /// The parameter `group` of this provider. BangGroup get group; @@ -746,7 +762,7 @@ class _BangCountOfGroupProviderElement } String _$bangIconCacheSizeMegabytesHash() => - r'2199439f1b8a2692607231836769a10a24405181'; + r'e08bfdc7bb47a56c079464478f0b8b5516b2effe'; /// See also [bangIconCacheSizeMegabytes]. @ProviderFor(bangIconCacheSizeMegabytes) @@ -761,6 +777,8 @@ final bangIconCacheSizeMegabytesProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef BangIconCacheSizeMegabytesRef = AutoDisposeStreamProviderRef; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/bangs/domain/repositories/data.g.dart b/app/lib/features/bangs/domain/repositories/data.g.dart index ad7e7412..b665682a 100644 --- a/app/lib/features/bangs/domain/repositories/data.g.dart +++ b/app/lib/features/bangs/domain/repositories/data.g.dart @@ -24,4 +24,4 @@ final bangDataRepositoryProvider = typedef _$BangDataRepository = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/bangs/domain/repositories/search.g.dart b/app/lib/features/bangs/domain/repositories/search.g.dart index 24d4be13..d44b2afa 100644 --- a/app/lib/features/bangs/domain/repositories/search.g.dart +++ b/app/lib/features/bangs/domain/repositories/search.g.dart @@ -22,4 +22,4 @@ final bangSearchProvider = typedef _$BangSearch = AutoDisposeStreamNotifier>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/bangs/domain/repositories/sync.g.dart b/app/lib/features/bangs/domain/repositories/sync.g.dart index a9cc707f..2a37a931 100644 --- a/app/lib/features/bangs/domain/repositories/sync.g.dart +++ b/app/lib/features/bangs/domain/repositories/sync.g.dart @@ -24,4 +24,4 @@ final bangSyncRepositoryProvider = typedef _$BangSyncRepository = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/chat_archive/data/providers.dart b/app/lib/features/chat_archive/data/providers.dart index a4ec489b..a73c3677 100644 --- a/app/lib/features/chat_archive/data/providers.dart +++ b/app/lib/features/chat_archive/data/providers.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:lensai/features/chat_archive/data/database/database.dart'; import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; @@ -10,7 +11,7 @@ import 'package:universal_io/io.dart'; part 'providers.g.dart'; @Riverpod() -ChatSearchDatabase chatSearchDatabase(ChatSearchDatabaseRef ref) { +ChatSearchDatabase chatSearchDatabase(Ref ref) { return ChatSearchDatabase( LazyDatabase(() async { // Also work around limitations on old Android versions diff --git a/app/lib/features/chat_archive/data/providers.g.dart b/app/lib/features/chat_archive/data/providers.g.dart index cb0de8a9..1c68568a 100644 --- a/app/lib/features/chat_archive/data/providers.g.dart +++ b/app/lib/features/chat_archive/data/providers.g.dart @@ -7,7 +7,7 @@ part of 'providers.dart'; // ************************************************************************** String _$chatSearchDatabaseHash() => - r'd7d2ad34264201d9196b28140b9b0ce0be6b7e1c'; + r'99700f1a2c25b24742c60fa8dd91fe744dcf21be'; /// See also [chatSearchDatabase]. @ProviderFor(chatSearchDatabase) @@ -22,6 +22,8 @@ final chatSearchDatabaseProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef ChatSearchDatabaseRef = AutoDisposeProviderRef; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/chat_archive/data/services/file.g.dart b/app/lib/features/chat_archive/data/services/file.g.dart index 37229728..c7cd9f09 100644 --- a/app/lib/features/chat_archive/data/services/file.g.dart +++ b/app/lib/features/chat_archive/data/services/file.g.dart @@ -24,4 +24,4 @@ final chatArchiveFileServiceProvider = AutoDisposeNotifierProvider< typedef _$ChatArchiveFileService = AutoDisposeNotifier>>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/chat_archive/domain/repositories/archive.dart b/app/lib/features/chat_archive/domain/repositories/archive.dart index 48fcbda7..fa81fb88 100644 --- a/app/lib/features/chat_archive/domain/repositories/archive.dart +++ b/app/lib/features/chat_archive/domain/repositories/archive.dart @@ -3,6 +3,7 @@ import 'package:lensai/features/chat_archive/data/services/file.dart'; import 'package:lensai/features/chat_archive/domain/entities/chat_entity.dart'; import 'package:lensai/features/kagi/data/services/chat.dart'; import 'package:path/path.dart' as path; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; @@ -62,10 +63,7 @@ class ChatArchiveRepository extends _$ChatArchiveRepository { } @Riverpod() -Future readArchivedChat( - ReadArchivedChatRef ref, - String fileName, -) async { +Future readArchivedChat(Ref ref, String fileName) async { final result = await ref.read(chatArchiveRepositoryProvider.notifier).readChat(fileName); diff --git a/app/lib/features/chat_archive/domain/repositories/archive.g.dart b/app/lib/features/chat_archive/domain/repositories/archive.g.dart index 45d59204..6d96a748 100644 --- a/app/lib/features/chat_archive/domain/repositories/archive.g.dart +++ b/app/lib/features/chat_archive/domain/repositories/archive.g.dart @@ -6,7 +6,7 @@ part of 'archive.dart'; // RiverpodGenerator // ************************************************************************** -String _$readArchivedChatHash() => r'e7d646a0ea22fe24d96e97f86c1d46fe83818508'; +String _$readArchivedChatHash() => r'beeadc38cf71f7dfb77a33a1202c685543a72b24'; /// Copied from Dart SDK class _SystemHash { @@ -142,6 +142,8 @@ class ReadArchivedChatProvider extends AutoDisposeFutureProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin ReadArchivedChatRef on AutoDisposeFutureProviderRef { /// The parameter `fileName` of this provider. String get fileName; @@ -173,4 +175,4 @@ final chatArchiveRepositoryProvider = AutoDisposeStreamNotifierProvider< typedef _$ChatArchiveRepository = AutoDisposeStreamNotifier>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/chat_archive/domain/repositories/search.g.dart b/app/lib/features/chat_archive/domain/repositories/search.g.dart index 97c2cf64..c8be5d47 100644 --- a/app/lib/features/chat_archive/domain/repositories/search.g.dart +++ b/app/lib/features/chat_archive/domain/repositories/search.g.dart @@ -25,4 +25,4 @@ final chatArchiveSearchRepositoryProvider = AutoDisposeStreamNotifierProvider< typedef _$ChatArchiveSearchRepository = AutoDisposeStreamNotifier>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/domain/entities/web_extension_state.dart b/app/lib/features/geckoview/domain/entities/web_extension_state.dart new file mode 100644 index 00000000..f0f09420 --- /dev/null +++ b/app/lib/features/geckoview/domain/entities/web_extension_state.dart @@ -0,0 +1,44 @@ +import 'dart:ui'; + +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:lensai/domain/entities/equatable_image.dart'; + +part 'web_extension_state.g.dart'; + +@CopyWith() +class WebExtensionState with FastEquatable { + String extensionId; + + String? title; + bool enabled; + String? badgeText; + Color? badgeTextColor; + Color? badgeBackgroundColor; + + final EquatableImage? icon; + + WebExtensionState({ + required this.extensionId, + required this.enabled, + this.title, + this.badgeText, + this.badgeTextColor, + this.badgeBackgroundColor, + this.icon, + }); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [ + extensionId, + title, + enabled, + badgeText, + badgeTextColor, + badgeBackgroundColor, + icon, + ]; +} diff --git a/app/lib/features/geckoview/domain/entities/web_extension_state.g.dart b/app/lib/features/geckoview/domain/entities/web_extension_state.g.dart new file mode 100644 index 00000000..1267377b --- /dev/null +++ b/app/lib/features/geckoview/domain/entities/web_extension_state.g.dart @@ -0,0 +1,127 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'web_extension_state.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$WebExtensionStateCWProxy { + WebExtensionState extensionId(String extensionId); + + WebExtensionState enabled(bool enabled); + + WebExtensionState title(String? title); + + WebExtensionState badgeText(String? badgeText); + + WebExtensionState badgeTextColor(Color? badgeTextColor); + + WebExtensionState badgeBackgroundColor(Color? badgeBackgroundColor); + + WebExtensionState icon(EquatableImage? icon); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebExtensionState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// WebExtensionState(...).copyWith(id: 12, name: "My name") + /// ```` + WebExtensionState call({ + String? extensionId, + bool? enabled, + String? title, + String? badgeText, + Color? badgeTextColor, + Color? badgeBackgroundColor, + EquatableImage? icon, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfWebExtensionState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfWebExtensionState.copyWith.fieldName(...)` +class _$WebExtensionStateCWProxyImpl implements _$WebExtensionStateCWProxy { + const _$WebExtensionStateCWProxyImpl(this._value); + + final WebExtensionState _value; + + @override + WebExtensionState extensionId(String extensionId) => + this(extensionId: extensionId); + + @override + WebExtensionState enabled(bool enabled) => this(enabled: enabled); + + @override + WebExtensionState title(String? title) => this(title: title); + + @override + WebExtensionState badgeText(String? badgeText) => this(badgeText: badgeText); + + @override + WebExtensionState badgeTextColor(Color? badgeTextColor) => + this(badgeTextColor: badgeTextColor); + + @override + WebExtensionState badgeBackgroundColor(Color? badgeBackgroundColor) => + this(badgeBackgroundColor: badgeBackgroundColor); + + @override + WebExtensionState icon(EquatableImage? icon) => this(icon: icon); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebExtensionState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// WebExtensionState(...).copyWith(id: 12, name: "My name") + /// ```` + WebExtensionState call({ + Object? extensionId = const $CopyWithPlaceholder(), + Object? enabled = const $CopyWithPlaceholder(), + Object? title = const $CopyWithPlaceholder(), + Object? badgeText = const $CopyWithPlaceholder(), + Object? badgeTextColor = const $CopyWithPlaceholder(), + Object? badgeBackgroundColor = const $CopyWithPlaceholder(), + Object? icon = const $CopyWithPlaceholder(), + }) { + return WebExtensionState( + extensionId: + extensionId == const $CopyWithPlaceholder() || extensionId == null + ? _value.extensionId + // ignore: cast_nullable_to_non_nullable + : extensionId as String, + enabled: enabled == const $CopyWithPlaceholder() || enabled == null + ? _value.enabled + // ignore: cast_nullable_to_non_nullable + : enabled as bool, + title: title == const $CopyWithPlaceholder() + ? _value.title + // ignore: cast_nullable_to_non_nullable + : title as String?, + badgeText: badgeText == const $CopyWithPlaceholder() + ? _value.badgeText + // ignore: cast_nullable_to_non_nullable + : badgeText as String?, + badgeTextColor: badgeTextColor == const $CopyWithPlaceholder() + ? _value.badgeTextColor + // ignore: cast_nullable_to_non_nullable + : badgeTextColor as Color?, + badgeBackgroundColor: badgeBackgroundColor == const $CopyWithPlaceholder() + ? _value.badgeBackgroundColor + // ignore: cast_nullable_to_non_nullable + : badgeBackgroundColor as Color?, + icon: icon == const $CopyWithPlaceholder() + ? _value.icon + // ignore: cast_nullable_to_non_nullable + : icon as EquatableImage?, + ); + } +} + +extension $WebExtensionStateCopyWith on WebExtensionState { + /// Returns a callable class that can be used as follows: `instanceOfWebExtensionState.copyWith(...)` or like so:`instanceOfWebExtensionState.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$WebExtensionStateCWProxy get copyWith => + _$WebExtensionStateCWProxyImpl(this); +} diff --git a/app/lib/features/geckoview/domain/providers.dart b/app/lib/features/geckoview/domain/providers.dart index 24a2bd37..1d597e93 100644 --- a/app/lib/features/geckoview/domain/providers.dart +++ b/app/lib/features/geckoview/domain/providers.dart @@ -5,6 +5,7 @@ import 'package:lensai/core/logger.dart'; import 'package:lensai/features/bangs/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -13,7 +14,7 @@ part 'providers.g.dart'; @Riverpod(keepAlive: true) GeckoSelectionActionService selectionActionService( - SelectionActionServiceRef ref, + Ref ref, ) { final service = GeckoSelectionActionService.setUp(); @@ -74,7 +75,7 @@ GeckoSelectionActionService selectionActionService( } @Riverpod(keepAlive: true) -GeckoEventService eventService(EventServiceRef ref) { +GeckoEventService eventService(Ref ref) { final service = GeckoEventService.setUp(); ref.onDispose(() { @@ -84,6 +85,17 @@ GeckoEventService eventService(EventServiceRef ref) { return service; } +@Riverpod(keepAlive: true) +GeckoAddonService addonService(Ref ref) { + final service = GeckoAddonService.setUp(); + + ref.onDispose(() { + service.dispose(); + }); + + return service; +} + @Riverpod(keepAlive: true) class EngineReadyState extends _$EngineReadyState { @override @@ -120,6 +132,6 @@ class EngineReadyState extends _$EngineReadyState { } @Riverpod() -Raw selectedTabSessionNotifier(SelectedTabSessionNotifierRef ref) { +Raw selectedTabSessionNotifier(Ref ref) { return ref.watch(tabSessionProvider(null).notifier); } diff --git a/app/lib/features/geckoview/domain/providers.g.dart b/app/lib/features/geckoview/domain/providers.g.dart index 0fc66e1a..2733fea3 100644 --- a/app/lib/features/geckoview/domain/providers.g.dart +++ b/app/lib/features/geckoview/domain/providers.g.dart @@ -7,7 +7,7 @@ part of 'providers.dart'; // ************************************************************************** String _$selectionActionServiceHash() => - r'c150f6003dcea80b18fc063dd998ab45ce27d1a1'; + r'b5bc6b2bc5496ae84d61a74cc6ff052b55d2a3d3'; /// See also [selectionActionService]. @ProviderFor(selectionActionService) @@ -22,8 +22,10 @@ final selectionActionServiceProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef SelectionActionServiceRef = ProviderRef; -String _$eventServiceHash() => r'5aa357fdf0d217677a9a66ecb50417ac18929cad'; +String _$eventServiceHash() => r'166b01f636fbdd4355dbc55a18ca4f83e0006de8'; /// See also [eventService]. @ProviderFor(eventService) @@ -36,9 +38,27 @@ final eventServiceProvider = Provider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef EventServiceRef = ProviderRef; +String _$addonServiceHash() => r'c7aca09b99c3810908176f3464f5f90a185aa5e7'; + +/// See also [addonService]. +@ProviderFor(addonService) +final addonServiceProvider = Provider.internal( + addonService, + name: r'addonServiceProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$addonServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef AddonServiceRef = ProviderRef; String _$selectedTabSessionNotifierHash() => - r'ea9959e871dd0c3a8b80152eafe726c925094106'; + r'62fcd98a915566d55b5bcb297d87d675fe12786d'; /// See also [selectedTabSessionNotifier]. @ProviderFor(selectedTabSessionNotifier) @@ -53,6 +73,8 @@ final selectedTabSessionNotifierProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef>; String _$engineReadyStateHash() => r'c682333e2e07cf0635aa7ae793a2088ca648c950'; @@ -71,4 +93,4 @@ final engineReadyStateProvider = typedef _$EngineReadyState = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/domain/providers/selected_tab.g.dart b/app/lib/features/geckoview/domain/providers/selected_tab.g.dart index 170e0b12..520b1fd7 100644 --- a/app/lib/features/geckoview/domain/providers/selected_tab.g.dart +++ b/app/lib/features/geckoview/domain/providers/selected_tab.g.dart @@ -21,4 +21,4 @@ final selectedTabProvider = NotifierProvider.internal( typedef _$SelectedTab = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/domain/providers/tab_list.dart b/app/lib/features/geckoview/domain/providers/tab_list.dart index d3b70b53..266f5b17 100644 --- a/app/lib/features/geckoview/domain/providers/tab_list.dart +++ b/app/lib/features/geckoview/domain/providers/tab_list.dart @@ -13,6 +13,7 @@ part 'tab_list.g.dart'; class TabList extends _$TabList { late TabDatabase _db; + @override List build() { final eventService = ref.watch(eventServiceProvider); diff --git a/app/lib/features/geckoview/domain/providers/tab_list.g.dart b/app/lib/features/geckoview/domain/providers/tab_list.g.dart index 4f84d3ac..84bdef8c 100644 --- a/app/lib/features/geckoview/domain/providers/tab_list.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_list.g.dart @@ -6,7 +6,7 @@ part of 'tab_list.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabListHash() => r'fd97d4692b1c74e91c5ebd4a75e6ff45ceb82ef9'; +String _$tabListHash() => r'3c17372cc467736ce1cfba937999acf4de7345ef'; /// See also [TabList]. @ProviderFor(TabList) @@ -21,4 +21,4 @@ final tabListProvider = NotifierProvider>.internal( typedef _$TabList = Notifier>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/domain/providers/tab_session.dart b/app/lib/features/geckoview/domain/providers/tab_session.dart index 4a3fb3df..02c04182 100644 --- a/app/lib/features/geckoview/domain/providers/tab_session.dart +++ b/app/lib/features/geckoview/domain/providers/tab_session.dart @@ -37,8 +37,8 @@ class TabSession extends _$TabSession { return _sessionService.exitFullscreen(); } - Future requestScreenshot() { - return _sessionService.requestScreenshot(); + Future requestScreenshot({bool requireImageResult = true}) { + return _sessionService.requestScreenshot(requireImageResult); } @override diff --git a/app/lib/features/geckoview/domain/providers/tab_session.g.dart b/app/lib/features/geckoview/domain/providers/tab_session.g.dart index 912970d3..a29c4f26 100644 --- a/app/lib/features/geckoview/domain/providers/tab_session.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_session.g.dart @@ -6,7 +6,7 @@ part of 'tab_session.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabSessionHash() => r'29b20f40af01e599ef80e198aec5f093d74efc6a'; +String _$tabSessionHash() => r'42b44a4d47b9a551d002fdbc6337c14b2c327dde'; /// Copied from Dart SDK class _SystemHash { @@ -155,6 +155,8 @@ class TabSessionProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin TabSessionRef on AutoDisposeNotifierProviderRef { /// The parameter `tabId` of this provider. String? get tabId; @@ -169,4 +171,4 @@ class _TabSessionProviderElement String? get tabId => (origin as TabSessionProvider).tabId; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/domain/providers/tab_state.dart b/app/lib/features/geckoview/domain/providers/tab_state.dart index 99296d05..5f8dcdf1 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.dart @@ -10,6 +10,7 @@ import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart'; import 'package:lensai/features/geckoview/utils/image_helper.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:synchronized/synchronized.dart'; @@ -190,7 +191,7 @@ class TabStates extends _$TabStates { } @Riverpod() -TabState? tabState(TabStateRef ref, String? tabId) { +TabState? tabState(Ref ref, String? tabId) { if (tabId == null) { return null; } @@ -201,7 +202,7 @@ TabState? tabState(TabStateRef ref, String? tabId) { } @Riverpod() -TabState? selectedTabState(SelectedTabStateRef ref) { +TabState? selectedTabState(Ref ref) { final tabId = ref.watch(selectedTabProvider); return ref.watch(tabStateProvider(tabId)); } diff --git a/app/lib/features/geckoview/domain/providers/tab_state.g.dart b/app/lib/features/geckoview/domain/providers/tab_state.g.dart index e982f993..eaca6134 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -6,7 +6,7 @@ part of 'tab_state.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabStateHash() => r'9fd7990804643fac180dd4fd50803bc7c9fa2827'; +String _$tabStateHash() => r'f57b2353acc99ca73670d504b45ea5ff19df38c7'; /// Copied from Dart SDK class _SystemHash { @@ -141,6 +141,8 @@ class TabStateProvider extends AutoDisposeProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin TabStateRef on AutoDisposeProviderRef { /// The parameter `tabId` of this provider. String? get tabId; @@ -154,7 +156,7 @@ class _TabStateProviderElement extends AutoDisposeProviderElement String? get tabId => (origin as TabStateProvider).tabId; } -String _$selectedTabStateHash() => r'22172913554e14e67f692eff0777ea015175906e'; +String _$selectedTabStateHash() => r'dbd36af7af286bd9d079f30e8e11bbda23bf7728'; /// See also [selectedTabState]. @ProviderFor(selectedTabState) @@ -168,6 +170,8 @@ final selectedTabStateProvider = AutoDisposeProvider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef SelectedTabStateRef = AutoDisposeProviderRef; String _$tabStatesHash() => r'dcd293279a901eeaab7728979e39e5ca3bee8c66'; @@ -185,4 +189,4 @@ final tabStatesProvider = typedef _$TabStates = Notifier>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/domain/providers/web_extensions_state.dart b/app/lib/features/geckoview/domain/providers/web_extensions_state.dart new file mode 100644 index 00000000..fd9b0115 --- /dev/null +++ b/app/lib/features/geckoview/domain/providers/web_extensions_state.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/domain/entities/equatable_image.dart'; +import 'package:lensai/extensions/image.dart'; +import 'package:lensai/features/geckoview/domain/entities/web_extension_state.dart'; +import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:lensai/features/geckoview/utils/image_helper.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:synchronized/synchronized.dart'; + +part 'web_extensions_state.g.dart'; + +@Riverpod(keepAlive: true) +class WebExtensionsState extends _$WebExtensionsState { + final _imageCache = {}; + + final _lock = Lock(); + + void _onExtensionUpdate(ExtensionDataEvent event) { + final ExtensionDataEvent(:extensionId, :data) = event; + + if (data != null) { + final current = state[extensionId] ?? + WebExtensionState( + extensionId: extensionId, + icon: _imageCache[extensionId], + enabled: false, + ); + + state = {...state}..[extensionId] = current.copyWith( + title: data.title, + enabled: data.enabled, + badgeText: data.badgeText, + badgeTextColor: (data.badgeTextColor != null) + ? Color(data.badgeTextColor!) + : null, + badgeBackgroundColor: (data.badgeBackgroundColor != null) + ? Color(data.badgeBackgroundColor!) + : null, + ); + } else { + if (state.containsKey(extensionId)) { + state = {...state}..remove(extensionId); + } + } + } + + Future _onIconChange(ExtensionIconEvent event) async { + final ExtensionIconEvent(:extensionId, :bytes) = event; + + final image = + await tryDecodeImage(bytes).then((image) async => image?.toEquatable()); + + if (image != null) { + if (_imageCache[extensionId] != image) { + _imageCache[extensionId] = image; + + if (state.containsKey(extensionId)) { + state = {...state}..[extensionId] = + state[extensionId]!.copyWith.icon(image); + } + } + } + } + + @override + Map build(WebExtensionActionType actionType) { + final addonService = ref.watch(addonServiceProvider); + + final subscriptions = switch (actionType) { + WebExtensionActionType.browser => [ + addonService.browserExtensionStream.listen( + (event) async { + await _lock.synchronized(() => _onExtensionUpdate(event)); + }, + ), + addonService.browserIconStream.listen( + (event) async { + await _lock.synchronized(() => _onIconChange(event)); + }, + ), + ], + WebExtensionActionType.page => [ + addonService.pageExtensionStream.listen( + (event) async { + await _lock.synchronized(() => _onExtensionUpdate(event)); + }, + ), + addonService.pageIconStream.listen( + (event) async { + await _lock.synchronized(() => _onIconChange(event)); + }, + ), + ], + }; + + ref.onDispose(() { + for (final sub in subscriptions) { + unawaited(sub.cancel()); + } + }); + + return {}; + } +} diff --git a/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart b/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart new file mode 100644 index 00000000..e543a578 --- /dev/null +++ b/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart @@ -0,0 +1,180 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'web_extensions_state.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$webExtensionsStateHash() => + r'9ac102b1d1740d69f9b0de590f08ff0774064845'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$WebExtensionsState + extends BuildlessNotifier> { + late final WebExtensionActionType actionType; + + Map build( + WebExtensionActionType actionType, + ); +} + +/// See also [WebExtensionsState]. +@ProviderFor(WebExtensionsState) +const webExtensionsStateProvider = WebExtensionsStateFamily(); + +/// See also [WebExtensionsState]. +class WebExtensionsStateFamily extends Family> { + /// See also [WebExtensionsState]. + const WebExtensionsStateFamily(); + + /// See also [WebExtensionsState]. + WebExtensionsStateProvider call( + WebExtensionActionType actionType, + ) { + return WebExtensionsStateProvider( + actionType, + ); + } + + @override + WebExtensionsStateProvider getProviderOverride( + covariant WebExtensionsStateProvider provider, + ) { + return call( + provider.actionType, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'webExtensionsStateProvider'; +} + +/// See also [WebExtensionsState]. +class WebExtensionsStateProvider extends NotifierProviderImpl< + WebExtensionsState, Map> { + /// See also [WebExtensionsState]. + WebExtensionsStateProvider( + WebExtensionActionType actionType, + ) : this._internal( + () => WebExtensionsState()..actionType = actionType, + from: webExtensionsStateProvider, + name: r'webExtensionsStateProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$webExtensionsStateHash, + dependencies: WebExtensionsStateFamily._dependencies, + allTransitiveDependencies: + WebExtensionsStateFamily._allTransitiveDependencies, + actionType: actionType, + ); + + WebExtensionsStateProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.actionType, + }) : super.internal(); + + final WebExtensionActionType actionType; + + @override + Map runNotifierBuild( + covariant WebExtensionsState notifier, + ) { + return notifier.build( + actionType, + ); + } + + @override + Override overrideWith(WebExtensionsState Function() create) { + return ProviderOverride( + origin: this, + override: WebExtensionsStateProvider._internal( + () => create()..actionType = actionType, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + actionType: actionType, + ), + ); + } + + @override + NotifierProviderElement> + createElement() { + return _WebExtensionsStateProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is WebExtensionsStateProvider && + other.actionType == actionType; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, actionType.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin WebExtensionsStateRef + on NotifierProviderRef> { + /// The parameter `actionType` of this provider. + WebExtensionActionType get actionType; +} + +class _WebExtensionsStateProviderElement extends NotifierProviderElement< + WebExtensionsState, + Map> with WebExtensionsStateRef { + _WebExtensionsStateProviderElement(super.provider); + + @override + WebExtensionActionType get actionType => + (origin as WebExtensionsStateProvider).actionType; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart index 91068fca..d9cd5ac4 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -22,4 +22,4 @@ final tabRepositoryProvider = NotifierProvider.internal( typedef _$TabRepository = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/browser/domain/providers.dart b/app/lib/features/geckoview/features/browser/domain/providers.dart index 0540701b..0096a336 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.dart @@ -7,6 +7,7 @@ import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_list.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; @@ -28,7 +29,7 @@ class SelectedBangTrigger extends _$SelectedBangTrigger { } @Riverpod() -Stream selectedBangData(SelectedBangDataRef ref, {String? domain}) { +Stream selectedBangData(Ref ref, {String? domain}) { final repository = ref.watch(bangDataRepositoryProvider.notifier); final selectedBangTrigger = ref.watch(selectedBangTriggerProvider(domain: domain)); @@ -85,7 +86,7 @@ class ShowFindInPage extends _$ShowFindInPage { @Riverpod() List availableTabIds( - AvailableTabIdsRef ref, + Ref ref, String? containerId, ) { final containerTabs = ref.watch( diff --git a/app/lib/features/geckoview/features/browser/domain/providers.g.dart b/app/lib/features/geckoview/features/browser/domain/providers.g.dart index f7b71994..bc98a966 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$selectedBangDataHash() => r'40dd3c51694394909fe83659ff3e19d51a06ec55'; +String _$selectedBangDataHash() => r'd43e679c40d35c4f4c692ee286dc21789d7287dd'; /// Copied from Dart SDK class _SystemHash { @@ -142,6 +142,8 @@ class SelectedBangDataProvider extends AutoDisposeStreamProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin SelectedBangDataRef on AutoDisposeStreamProviderRef { /// The parameter `domain` of this provider. String? get domain; @@ -156,7 +158,7 @@ class _SelectedBangDataProviderElement String? get domain => (origin as SelectedBangDataProvider).domain; } -String _$availableTabIdsHash() => r'427ea2e58c6bc27e2f16cffb850dc6621875e247'; +String _$availableTabIdsHash() => r'646d42c747d815496849c7342903caf48258a8ab'; /// See also [availableTabIds]. @ProviderFor(availableTabIds) @@ -271,6 +273,8 @@ class AvailableTabIdsProvider extends AutoDisposeProvider> { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin AvailableTabIdsRef on AutoDisposeProviderRef> { /// The parameter `containerId` of this provider. String? get containerId; @@ -413,6 +417,8 @@ class SelectedBangTriggerProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin SelectedBangTriggerRef on NotifierProviderRef { /// The parameter `domain` of this provider. String? get domain; @@ -493,4 +499,4 @@ final showFindInPageProvider = NotifierProvider.internal( typedef _$ShowFindInPage = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/browser/domain/services/create_tab.g.dart b/app/lib/features/geckoview/features/browser/domain/services/create_tab.g.dart index 090ab8af..fa9f416c 100644 --- a/app/lib/features/geckoview/features/browser/domain/services/create_tab.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/services/create_tab.g.dart @@ -23,4 +23,4 @@ final createTabStreamProvider = typedef _$CreateTabStream = AutoDisposeStreamNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/browser/domain/services/engine_settings.dart b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.dart index 90a8e7c2..644ee79a 100644 --- a/app/lib/features/geckoview/features/browser/domain/services/engine_settings.dart +++ b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.dart @@ -1,9 +1,10 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'engine_settings.g.dart'; @Riverpod(keepAlive: true) -GeckoEngineSettingsService engineSettingsService(EngineSettingsServiceRef ref) { +GeckoEngineSettingsService engineSettingsService(Ref ref) { return GeckoEngineSettingsService(); } diff --git a/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart index efbd907e..95183cff 100644 --- a/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart @@ -7,7 +7,7 @@ part of 'engine_settings.dart'; // ************************************************************************** String _$engineSettingsServiceHash() => - r'9d9abb3601a3782f1f3ef91e31049db40acbf64a'; + r'051d7b6f65eb5dcb07405b4af1bdadd8dd74fdc0'; /// See also [engineSettingsService]. @ProviderFor(engineSettingsService) @@ -22,6 +22,8 @@ final engineSettingsServiceProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef EngineSettingsServiceRef = ProviderRef; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart new file mode 100644 index 00000000..80867aab --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart @@ -0,0 +1,133 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/core/logger.dart'; +import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; +import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart'; + +class BrowserView extends StatefulHookConsumerWidget { + final Duration screenshotPeriod; + + const BrowserView({this.screenshotPeriod = const Duration(seconds: 10)}); + + @override + ConsumerState createState() => _BrowserViewState(); +} + +class _BrowserViewState extends ConsumerState + with WidgetsBindingObserver { + Timer? _periodicScreenshotUpdate; + + //This is managed by widget state changes to resume a timer + bool _timerPaused = false; + + Future _timerTick(Timer timer) async { + await ref + .read(selectedTabSessionNotifierProvider) + .requestScreenshot(requireImageResult: false) + .onError((error, stackTrace) { + logger.e(error, stackTrace: stackTrace); + timer.cancel(); + + return null; + }); + } + + @override + Widget build(BuildContext context) { + ref.listen( + selectedTabStateProvider.select( + (state) => (tabId: state?.id, isLoading: state?.isLoading), + ), + (previous, next) { + if (previous?.tabId != next.tabId || next.isLoading == true) { + _periodicScreenshotUpdate?.cancel(); + _periodicScreenshotUpdate = null; + _timerPaused = false; + } + + if (next.isLoading == false && + (_periodicScreenshotUpdate?.isActive ?? false) == false) { + _timerPaused = false; + _periodicScreenshotUpdate = + Timer.periodic(widget.screenshotPeriod, _timerTick); + } + }, + ); + + return GeckoView( + preInitializationStep: () async { + await ref + .read(eventServiceProvider) + .viewReadyStateEvents + .firstWhere((state) => state == true) + .timeout( + const Duration(seconds: 3), + onTimeout: () { + logger.e( + 'Browser fragement not reported ready, trying to intitialize anyways', + ); + return true; + }, + ); + }, + ); + } + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addObserver(this); + + //Initialize and register dependencies + ref.listenManual( + selectionActionServiceProvider, + (previous, next) {}, + ); + + ref.listenManual( + webExtensionsStateProvider(WebExtensionActionType.browser), + (previous, next) {}, + ); + + ref.listenManual( + webExtensionsStateProvider(WebExtensionActionType.page), + (previous, next) {}, + ); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.detached: + case AppLifecycleState.inactive: + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + if (_periodicScreenshotUpdate?.isActive == true) { + _periodicScreenshotUpdate?.cancel(); + _timerPaused = true; + } + case AppLifecycleState.resumed: + if (_timerPaused) { + _periodicScreenshotUpdate = + Timer.periodic(widget.screenshotPeriod, _timerTick); + _timerPaused = false; + } + } + } + + @override + Future dispose() async { + WidgetsBinding.instance.removeObserver(this); + + _periodicScreenshotUpdate?.cancel(); + + super.dispose(); + } +} diff --git a/app/lib/features/geckoview/features/browser/screens/browser.dart b/app/lib/features/geckoview/features/browser/screens/browser.dart index b393cd55..9310799b 100644 --- a/app/lib/features/geckoview/features/browser/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/screens/browser.dart @@ -6,18 +6,19 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/core/logger.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_list.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; +import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:lensai/features/geckoview/features/browser/domain/services/create_tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/services/engine_settings.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart'; @@ -219,7 +220,8 @@ class BrowserScreen extends HookConsumerWidget { onPressed: () { ref .read( - createTabStreamProvider.notifier) + createTabStreamProvider.notifier, + ) .createTab( CreateTabSheet( preferredTool: KagiTool.assistant, @@ -278,7 +280,8 @@ class BrowserScreen extends HookConsumerWidget { horizontal: 8.0, ), child: Icon( - quickActionVoice ? Icons.mic : quickAction.icon), + quickActionVoice ? Icons.mic : quickAction.icon, + ), ), ), TabsActionButton( @@ -310,13 +313,60 @@ class BrowserScreen extends HookConsumerWidget { }, child: const Padding( padding: EdgeInsets.symmetric( - vertical: 15.0, horizontal: 8.0), + vertical: 15.0, + horizontal: 8.0, + ), child: Icon(Icons.more_vert), ), ), ); }, menuChildren: [ + Consumer( + builder: (context, childRef, child) { + final pageExtensions = childRef.watch( + webExtensionsStateProvider( + WebExtensionActionType.page, + ).select((value) => value.values.toList()), + ); + + return Wrap( + alignment: WrapAlignment.center, + children: [ + ...pageExtensions.map( + (extension) => IconButton( + onPressed: () async { + //Use parents .ref because after onPressed this consumer gets disposed already + await ref + .read(addonServiceProvider) + .invokeAddonAction( + extension.extensionId, + WebExtensionActionType.page, + ); + }, + icon: Badge( + isLabelVisible: + extension.badgeText?.isNotEmpty ?? + false, + label: (extension.badgeText?.isNotEmpty == + true) + ? Text(extension.badgeText!) + : null, + textColor: extension.badgeTextColor, + backgroundColor: + extension.badgeBackgroundColor, + child: RawImage( + image: extension.icon?.value, + width: 24, + height: 24, + ), + ), + ), + ), + ], + ); + }, + ), MenuItemButton( onPressed: () async { await context.push(AboutRoute().location); @@ -332,6 +382,50 @@ class BrowserScreen extends HookConsumerWidget { leadingIcon: const Icon(Icons.settings), child: const Text('Settings'), ), + Consumer( + builder: (context, childRef, child) { + final browserExtensions = childRef.watch( + webExtensionsStateProvider( + WebExtensionActionType.browser, + ).select((value) => value.values.toList()), + ); + + return SubmenuButton( + menuChildren: [ + ...browserExtensions.map( + (extension) => MenuItemButton( + onPressed: () async { + //Use parents .ref because after onPressed this consumer gets disposed already + await ref + .read(addonServiceProvider) + .invokeAddonAction( + extension.extensionId, + WebExtensionActionType.browser, + ); + }, + leadingIcon: RawImage( + image: extension.icon?.value, + width: 24, + height: 24, + ), + child: Text(extension.title ?? ''), + ), + ), + MenuItemButton( + onPressed: () async { + await childRef + .read(addonServiceProvider) + .startAddonManagerActivity(); + }, + leadingIcon: const Icon(MdiIcons.puzzleEdit), + child: const Text('Addon Manager'), + ), + ], + leadingIcon: const Icon(MdiIcons.puzzle), + child: const Text('Addons'), + ); + }, + ), const Divider(), MenuItemButton( onPressed: () async { @@ -340,53 +434,56 @@ class BrowserScreen extends HookConsumerWidget { leadingIcon: const Icon(MdiIcons.exclamationThick), child: const Text('Bangs'), ), - if (showEarlyAccessFeatures) - MenuItemButton( - onPressed: () async { - await context - .push(ChatArchiveListRoute().location); - }, - leadingIcon: const Icon(MdiIcons.archive), - child: const Text('Chat Archive'), - ), - const Divider(), - if (showEarlyAccessFeatures) - MenuItemButton( - onPressed: () { - ref - .read(createTabStreamProvider.notifier) - .createTab( - CreateTabSheet( - preferredTool: KagiTool.assistant), - ); - }, - leadingIcon: Icon(KagiTool.assistant.icon), - child: const Text('Assistant'), - ), - MenuItemButton( - onPressed: () { - ref - .read(createTabStreamProvider.notifier) - .createTab( - CreateTabSheet( - preferredTool: KagiTool.summarizer), - ); - }, - leadingIcon: Icon(KagiTool.summarizer.icon), - child: const Text('Summarizer'), - ), - MenuItemButton( - onPressed: () { - ref - .read(createTabStreamProvider.notifier) - .createTab( - CreateTabSheet( - preferredTool: KagiTool.search), - ); - }, - leadingIcon: Icon(KagiTool.search.icon), - child: const Text('Search'), - ), + // if (showEarlyAccessFeatures) + // MenuItemButton( + // onPressed: () async { + // await context + // .push(ChatArchiveListRoute().location); + // }, + // leadingIcon: const Icon(MdiIcons.archive), + // child: const Text('Chat Archive'), + // ), + // const Divider(), + // if (showEarlyAccessFeatures) + // MenuItemButton( + // onPressed: () { + // ref + // .read(createTabStreamProvider.notifier) + // .createTab( + // CreateTabSheet( + // preferredTool: KagiTool.assistant, + // ), + // ); + // }, + // leadingIcon: Icon(KagiTool.assistant.icon), + // child: const Text('Assistant'), + // ), + // MenuItemButton( + // onPressed: () { + // ref + // .read(createTabStreamProvider.notifier) + // .createTab( + // CreateTabSheet( + // preferredTool: KagiTool.summarizer, + // ), + // ); + // }, + // leadingIcon: Icon(KagiTool.summarizer.icon), + // child: const Text('Summarizer'), + // ), + // MenuItemButton( + // onPressed: () { + // ref + // .read(createTabStreamProvider.notifier) + // .createTab( + // CreateTabSheet( + // preferredTool: KagiTool.search, + // ), + // ); + // }, + // leadingIcon: Icon(KagiTool.search.icon), + // child: const Text('Search'), + // ), const Divider(), if (selectedTabId != null) MenuItemButton( @@ -419,8 +516,10 @@ class BrowserScreen extends HookConsumerWidget { MenuItemButton( onPressed: () { ref - .read(findInPageVisibilityControllerProvider - .notifier) + .read( + findInPageVisibilityControllerProvider + .notifier, + ) .show(); }, leadingIcon: const Icon(Icons.search), @@ -431,7 +530,8 @@ class BrowserScreen extends HookConsumerWidget { MenuItemButton( onPressed: () async { final controller = ref.read( - tabSessionProvider(selectedTabId).notifier); + tabSessionProvider(selectedTabId).notifier, + ); await controller.reload(); menuController.close(); @@ -479,7 +579,9 @@ class BrowserScreen extends HookConsumerWidget { ), ), const SizedBox( - height: 48, child: VerticalDivider()), + height: 48, + child: VerticalDivider(), + ), Expanded( child: IconButton( onPressed: (history?.canGoForward == true) @@ -607,30 +709,7 @@ class BrowserScreen extends HookConsumerWidget { SafeArea( child: Stack( children: [ - Consumer( - builder: (context, ref, child) { - //Initialize dependencies - ref.watch(selectionActionServiceProvider); - - return GeckoView( - preInitializationStep: () async { - await ref - .read(eventServiceProvider) - .viewReadyStateEvents - .firstWhere((state) => state == true) - .timeout( - const Duration(seconds: 3), - onTimeout: () { - logger.e( - 'Browser fragement not reported ready, trying to intitialize anyways', - ); - return true; - }, - ); - }, - ); - }, - ), + const BrowserView(), Positioned( bottom: 0, left: 0, diff --git a/app/lib/features/geckoview/features/controllers/bottom_sheet.g.dart b/app/lib/features/geckoview/features/controllers/bottom_sheet.g.dart index 47563445..c3ad5cc2 100644 --- a/app/lib/features/geckoview/features/controllers/bottom_sheet.g.dart +++ b/app/lib/features/geckoview/features/controllers/bottom_sheet.g.dart @@ -40,4 +40,4 @@ final bottomSheetExtendProvider = typedef _$BottomSheetExtend = StreamNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/controllers/overlay_dialog.g.dart b/app/lib/features/geckoview/features/controllers/overlay_dialog.g.dart index 3935cbdf..ccfe7094 100644 --- a/app/lib/features/geckoview/features/controllers/overlay_dialog.g.dart +++ b/app/lib/features/geckoview/features/controllers/overlay_dialog.g.dart @@ -24,4 +24,4 @@ final overlayDialogControllerProvider = typedef _$OverlayDialogController = AutoDisposeNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart index 5d22ceaa..abfff6b4 100644 --- a/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart +++ b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart @@ -158,6 +158,8 @@ class FindInPageRepositoryProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin FindInPageRepositoryRef on AutoDisposeNotifierProviderRef { /// The parameter `tabId` of this provider. String? get tabId; @@ -172,4 +174,4 @@ class _FindInPageRepositoryProviderElement String? get tabId => (origin as FindInPageRepositoryProvider).tabId; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.g.dart b/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.g.dart index 65aeaece..0965297b 100644 --- a/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.g.dart +++ b/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.g.dart @@ -24,4 +24,4 @@ final findInPageVisibilityControllerProvider = typedef _$FindInPageVisibilityController = AutoDisposeNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart index e139a77f..79e24202 100644 --- a/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart +++ b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart @@ -1,10 +1,11 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'readerable.g.dart'; @Riverpod(keepAlive: true) -GeckoReaderableService readerableService(ReaderableServiceRef ref) { +GeckoReaderableService readerableService(Ref ref) { final service = GeckoReaderableService.setUp(); ref.onDispose(() { @@ -15,7 +16,7 @@ GeckoReaderableService readerableService(ReaderableServiceRef ref) { } @Riverpod() -Stream appearanceButtonVisibility(AppearanceButtonVisibilityRef ref) { +Stream appearanceButtonVisibility(Ref ref) { final service = ref.watch(readerableServiceProvider); return service.appearanceVisibility; } diff --git a/app/lib/features/geckoview/features/readerview/domain/providers/readerable.g.dart b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.g.dart index 92b0b715..f8e083bc 100644 --- a/app/lib/features/geckoview/features/readerview/domain/providers/readerable.g.dart +++ b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.g.dart @@ -6,7 +6,7 @@ part of 'readerable.dart'; // RiverpodGenerator // ************************************************************************** -String _$readerableServiceHash() => r'aad722c3869b4ce8dbe336fada733b7d102434c5'; +String _$readerableServiceHash() => r'03182c8afc41f5184322a3206473cbfa96df5819'; /// See also [readerableService]. @ProviderFor(readerableService) @@ -20,9 +20,11 @@ final readerableServiceProvider = Provider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef ReaderableServiceRef = ProviderRef; String _$appearanceButtonVisibilityHash() => - r'996ec32ee111cfa713230a50fea3ae3a9984df7c'; + r'e7102b63113937dd5d17028a5ec8dac428c38bbb'; /// See also [appearanceButtonVisibility]. @ProviderFor(appearanceButtonVisibility) @@ -37,6 +39,8 @@ final appearanceButtonVisibilityProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef AppearanceButtonVisibilityRef = AutoDisposeStreamProviderRef; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.g.dart b/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.g.dart index f9e250f6..a2981dd3 100644 --- a/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.g.dart +++ b/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.g.dart @@ -24,4 +24,4 @@ final readerableScreenControllerProvider = typedef _$ReaderableScreenController = AsyncNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/tabs/data/providers.dart b/app/lib/features/geckoview/features/tabs/data/providers.dart index 40c1f667..23d9df83 100644 --- a/app/lib/features/geckoview/features/tabs/data/providers.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.dart @@ -4,6 +4,7 @@ import 'package:lensai/data/database/functions/lexo_rank_functions.dart'; import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; @@ -12,7 +13,7 @@ import 'package:universal_io/io.dart'; part 'providers.g.dart'; @Riverpod(keepAlive: true) -TabDatabase tabDatabase(TabDatabaseRef ref) { +TabDatabase tabDatabase(Ref ref) { return TabDatabase( LazyDatabase(() async { // put the database file, called db.sqlite here, into the documents folder diff --git a/app/lib/features/geckoview/features/tabs/data/providers.g.dart b/app/lib/features/geckoview/features/tabs/data/providers.g.dart index 214caef0..62adeebc 100644 --- a/app/lib/features/geckoview/features/tabs/data/providers.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabDatabaseHash() => r'567872961683d21156a9af463466f8ddbdd02f1f'; +String _$tabDatabaseHash() => r'1379e222fe4d43c119a89ec89a779ef35fbe1563'; /// See also [tabDatabase]. @ProviderFor(tabDatabase) @@ -19,6 +19,8 @@ final tabDatabaseProvider = Provider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef TabDatabaseRef = ProviderRef; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/tabs/domain/providers.dart b/app/lib/features/geckoview/features/tabs/domain/providers.dart index 5b9ac59b..686fe48d 100644 --- a/app/lib/features/geckoview/features/tabs/domain/providers.dart +++ b/app/lib/features/geckoview/features/tabs/domain/providers.dart @@ -5,13 +5,14 @@ import 'package:lensai/features/geckoview/features/tabs/data/models/container_da import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:lensai/features/geckoview/features/tabs/utils/color_palette.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; @Riverpod() Future unusedRandomContainerColor( - UnusedRandomContainerColorRef ref, + Ref ref, ) async { final repository = ref.watch(containerRepositoryProvider.notifier); @@ -28,21 +29,21 @@ Future unusedRandomContainerColor( @Riverpod() Stream> containersWithCount( - ContainersWithCountRef ref, + Ref ref, ) { final db = ref.watch(tabDatabaseProvider); return db.containersWithCount().watch(); } @Riverpod() -Stream tabContainerId(TabContainerIdRef ref, String tabId) { +Stream tabContainerId(Ref ref, String tabId) { final db = ref.watch(tabDatabaseProvider); return db.tabDao.tabContainerId(tabId).watchSingle(); } @Riverpod() Stream> containerTabIds( - ContainerTabIdsRef ref, + Ref ref, String? containerId, ) { final db = ref.watch(tabDatabaseProvider); diff --git a/app/lib/features/geckoview/features/tabs/domain/providers.g.dart b/app/lib/features/geckoview/features/tabs/domain/providers.g.dart index a6f98e58..cf8ea4aa 100644 --- a/app/lib/features/geckoview/features/tabs/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/providers.g.dart @@ -7,7 +7,7 @@ part of 'providers.dart'; // ************************************************************************** String _$unusedRandomContainerColorHash() => - r'129cefaefa895012a6d7b35da69773e28e3289cc'; + r'913499b3de281ea0020db97ed67121652f53ec08'; /// See also [unusedRandomContainerColor]. @ProviderFor(unusedRandomContainerColor) @@ -22,9 +22,11 @@ final unusedRandomContainerColorProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef UnusedRandomContainerColorRef = AutoDisposeFutureProviderRef; String _$containersWithCountHash() => - r'e1bab28091dd1b9ecfd7d76e6393f459604c803a'; + r'ee0734e4b71472b79636816616a158f54fa6bc23'; /// See also [containersWithCount]. @ProviderFor(containersWithCount) @@ -39,9 +41,11 @@ final containersWithCountProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef ContainersWithCountRef = AutoDisposeStreamProviderRef>; -String _$tabContainerIdHash() => r'8adbfdc2ef46219569073fe9e58967f4935a45e5'; +String _$tabContainerIdHash() => r'726ddd000d23935a2c8ae39c7957412fab16016d'; /// Copied from Dart SDK class _SystemHash { @@ -177,6 +181,8 @@ class TabContainerIdProvider extends AutoDisposeStreamProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin TabContainerIdRef on AutoDisposeStreamProviderRef { /// The parameter `tabId` of this provider. String get tabId; @@ -190,7 +196,7 @@ class _TabContainerIdProviderElement String get tabId => (origin as TabContainerIdProvider).tabId; } -String _$containerTabIdsHash() => r'79e9bccfb17cf3ce802638c8b806c5a76fa8f732'; +String _$containerTabIdsHash() => r'7545a6c500b1832bf81c0838e257dfe5e051463d'; /// See also [containerTabIds]. @ProviderFor(containerTabIds) @@ -305,6 +311,8 @@ class ContainerTabIdsProvider extends AutoDisposeStreamProvider> { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin ContainerTabIdsRef on AutoDisposeStreamProviderRef> { /// The parameter `containerId` of this provider. String? get containerId; @@ -319,4 +327,4 @@ class _ContainerTabIdsProviderElement String? get containerId => (origin as ContainerTabIdsProvider).containerId; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart index 5b260c16..ddf8e18c 100644 --- a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart +++ b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart @@ -1,5 +1,6 @@ import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'selected_container.g.dart'; @@ -29,7 +30,7 @@ class SelectedContainer extends _$SelectedContainer { } @Riverpod() -Stream selectedContainerData(SelectedContainerDataRef ref) { +Stream selectedContainerData(Ref ref) { final db = ref.watch(tabDatabaseProvider); final selectedContainer = ref.watch(selectedContainerProvider); diff --git a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart index b113868e..2f170d39 100644 --- a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart @@ -7,7 +7,7 @@ part of 'selected_container.dart'; // ************************************************************************** String _$selectedContainerDataHash() => - r'dc930d1bb47b3c7989c6c119b0459f37d687af84'; + r'1ec86a82e1fc4823a867285f05036c903633a165'; /// See also [selectedContainerData]. @ProviderFor(selectedContainerData) @@ -22,6 +22,8 @@ final selectedContainerDataProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef; String _$selectedContainerHash() => r'e38e86db5bd0584af9561156c26ceaea3d23aebf'; @@ -40,4 +42,4 @@ final selectedContainerProvider = typedef _$SelectedContainer = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart index 9dfcd6e0..65c00e11 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart @@ -24,4 +24,4 @@ final containerRepositoryProvider = typedef _$ContainerRepository = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart index 4b4688fe..0bd5e2d3 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart @@ -23,4 +23,4 @@ final tabDataRepositoryProvider = typedef _$TabDataRepository = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/kagi/data/services/autosuggest.g.dart b/app/lib/features/kagi/data/services/autosuggest.g.dart index 43078ec2..38c6e48e 100644 --- a/app/lib/features/kagi/data/services/autosuggest.g.dart +++ b/app/lib/features/kagi/data/services/autosuggest.g.dart @@ -24,4 +24,4 @@ final kagiAutosuggestServiceProvider = typedef _$KagiAutosuggestService = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/kagi/data/services/chat.g.dart b/app/lib/features/kagi/data/services/chat.g.dart index aef3e736..5c3c7ff3 100644 --- a/app/lib/features/kagi/data/services/chat.g.dart +++ b/app/lib/features/kagi/data/services/chat.g.dart @@ -23,4 +23,4 @@ final kagiChatServiceProvider = typedef _$KagiChatService = Notifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/kagi/data/services/session.g.dart b/app/lib/features/kagi/data/services/session.g.dart index f2ee08a3..bcbe0833 100644 --- a/app/lib/features/kagi/data/services/session.g.dart +++ b/app/lib/features/kagi/data/services/session.g.dart @@ -24,4 +24,4 @@ final kagiSessionServiceProvider = typedef _$KagiSessionService = AutoDisposeNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/kagi/domain/repositories/autosuggest.g.dart b/app/lib/features/kagi/domain/repositories/autosuggest.g.dart index d5fdeb1f..64404555 100644 --- a/app/lib/features/kagi/domain/repositories/autosuggest.g.dart +++ b/app/lib/features/kagi/domain/repositories/autosuggest.g.dart @@ -24,4 +24,4 @@ final autosuggestRepositoryProvider = typedef _$AutosuggestRepository = Notifier>>>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/settings/data/models/settings.dart b/app/lib/features/settings/data/models/settings.dart index 98071da1..02dc5ad0 100644 --- a/app/lib/features/settings/data/models/settings.dart +++ b/app/lib/features/settings/data/models/settings.dart @@ -37,7 +37,6 @@ class Settings with FastEquatable { bool? incognitoMode, bool? enableJavascript, bool? launchUrlExternal, - bool? enableContentBlocking, bool? blockHttpProtocol, ThemeMode? themeMode, this.quickAction, diff --git a/app/lib/features/settings/data/repositories/settings_repository.g.dart b/app/lib/features/settings/data/repositories/settings_repository.g.dart index fe5c9b0a..cdcec015 100644 --- a/app/lib/features/settings/data/repositories/settings_repository.g.dart +++ b/app/lib/features/settings/data/repositories/settings_repository.g.dart @@ -24,4 +24,4 @@ final settingsRepositoryProvider = typedef _$SettingsRepository = AsyncNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/settings/presentation/controllers/save_settings.g.dart b/app/lib/features/settings/presentation/controllers/save_settings.g.dart index 8b5ea1f2..45ccd5b3 100644 --- a/app/lib/features/settings/presentation/controllers/save_settings.g.dart +++ b/app/lib/features/settings/presentation/controllers/save_settings.g.dart @@ -24,4 +24,4 @@ final saveSettingsControllerProvider = typedef _$SaveSettingsController = AsyncNotifier; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/share_intent/domain/services/sharing_intent.dart b/app/lib/features/share_intent/domain/services/sharing_intent.dart index 70751a2a..1e7fb74c 100644 --- a/app/lib/features/share_intent/domain/services/sharing_intent.dart +++ b/app/lib/features/share_intent/domain/services/sharing_intent.dart @@ -8,6 +8,7 @@ import 'package:lensai/core/logger.dart'; import 'package:lensai/data/models/received_parameter.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:mime/mime.dart' as mime; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; import 'package:uri_to_file/uri_to_file.dart' as uri_to_file; @@ -48,7 +49,7 @@ final _sharingIntentTransformer = ); @Riverpod() -Raw> sharingIntentStream(SharingIntentStreamRef ref) { +Raw> sharingIntentStream(Ref ref) { final initialStream = FlutterSharingIntent.instance // ignore: discarded_futures .getInitialSharing() diff --git a/app/lib/features/share_intent/domain/services/sharing_intent.g.dart b/app/lib/features/share_intent/domain/services/sharing_intent.g.dart index 6de1b8e0..eddee993 100644 --- a/app/lib/features/share_intent/domain/services/sharing_intent.g.dart +++ b/app/lib/features/share_intent/domain/services/sharing_intent.g.dart @@ -7,7 +7,7 @@ part of 'sharing_intent.dart'; // ************************************************************************** String _$sharingIntentStreamHash() => - r'ce060aadf885ab0a14d90acfe986447ecb05f4f9'; + r'd0634c3cbff4760eb637663716970b6ad35ccf36'; /// See also [sharingIntentStream]. @ProviderFor(sharingIntentStream) @@ -22,7 +22,9 @@ final sharingIntentStreamProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef SharingIntentStreamRef = AutoDisposeProviderRef>>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/presentation/controllers/website_title.dart b/app/lib/presentation/controllers/website_title.dart index c69a5342..d186992e 100644 --- a/app/lib/presentation/controllers/website_title.dart +++ b/app/lib/presentation/controllers/website_title.dart @@ -1,11 +1,12 @@ import 'package:lensai/data/models/web_page_info.dart'; import 'package:lensai/domain/services/generic_website.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'website_title.g.dart'; @Riverpod(keepAlive: true) -Future pageInfo(PageInfoRef ref, Uri url) { +Future pageInfo(Ref ref, Uri url) { final websiteService = ref.watch(genericWebsiteServiceProvider.notifier); return websiteService.getInfo(url).then((value) => value.value); } diff --git a/app/lib/presentation/controllers/website_title.g.dart b/app/lib/presentation/controllers/website_title.g.dart index 792f8ce4..9d96ed86 100644 --- a/app/lib/presentation/controllers/website_title.g.dart +++ b/app/lib/presentation/controllers/website_title.g.dart @@ -6,7 +6,7 @@ part of 'website_title.dart'; // RiverpodGenerator // ************************************************************************** -String _$pageInfoHash() => r'66800fc7e4863750fc9a3717a25e94ee95ec52a3'; +String _$pageInfoHash() => r'f83bf4b19670abedf0fac5cfe59f190c37544123'; /// Copied from Dart SDK class _SystemHash { @@ -141,6 +141,8 @@ class PageInfoProvider extends FutureProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin PageInfoRef on FutureProviderRef { /// The parameter `url` of this provider. Uri get url; @@ -154,4 +156,4 @@ class _PageInfoProviderElement extends FutureProviderElement Uri get url => (origin as PageInfoProvider).url; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/share_plus b/app/linux/flutter/ephemeral/.plugin_symlinks/share_plus index fafd0ea6..e38e0260 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/share_plus +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/share_plus @@ -1 +1 @@ -/home/fafre/.pub-cache/hosted/pub.dev/share_plus-10.1.0/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/share_plus-10.1.1/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs b/app/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs index 994c3a63..f2bcaa96 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs @@ -1 +1 @@ -/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.25/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.26/ \ No newline at end of file diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index 019aabe4..fcaab4df 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,6 @@ import FlutterMacOS import Foundation import dynamic_color -import flutter_inappwebview_macos import flutter_secure_storage_macos import package_info_plus import path_provider_foundation @@ -17,7 +16,6 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) - InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/app/pubspec.lock b/app/pubspec.lock index 219f9437..9c8c0f32 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -30,14 +30,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.3" - animations: - dependency: "direct main" - description: - name: animations - sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb - url: "https://pub.dev" - source: hosted - version: "2.0.11" archive: dependency: transitive description: @@ -231,7 +223,7 @@ packages: source: hosted version: "0.3.4+2" crypto: - dependency: "direct main" + dependency: transitive description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" @@ -298,10 +290,10 @@ packages: dependency: "direct dev" description: name: drift_dev - sha256: "27bab15e7869b69259663590381180117873b9b273a1ea9ebb21bb73133d1233" + sha256: "4592cea370f1dcd8afc67987c28402797d9ddb4aa3ae372cb675497c0741816c" url: "https://pub.dev" source: hosted - version: "2.21.0" + version: "2.21.1" dynamic_color: dependency: "direct main" description: @@ -403,70 +395,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.5" - flutter_inappwebview: - dependency: "direct main" - description: - name: flutter_inappwebview - sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" - url: "https://pub.dev" - source: hosted - version: "6.1.5" - flutter_inappwebview_android: - dependency: transitive - description: - name: flutter_inappwebview_android - sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" - url: "https://pub.dev" - source: hosted - version: "1.1.3" - flutter_inappwebview_internal_annotations: - dependency: transitive - description: - name: flutter_inappwebview_internal_annotations - sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter_inappwebview_ios: - dependency: transitive - description: - name: flutter_inappwebview_ios - sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_inappwebview_macos: - dependency: transitive - description: - name: flutter_inappwebview_macos - sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_inappwebview_platform_interface: - dependency: transitive - description: - name: flutter_inappwebview_platform_interface - sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 - url: "https://pub.dev" - source: hosted - version: "1.3.0+1" - flutter_inappwebview_web: - dependency: transitive - description: - name: flutter_inappwebview_web - sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_inappwebview_windows: - dependency: transitive - description: - name: flutter_inappwebview_windows - sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" - url: "https://pub.dev" - source: hosted - version: "0.6.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -479,10 +407,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: bd9c475d9aae256369edacafc29d1e74c81f78a10cdcdacbbbc9e3c43d009e4a + sha256: f0e599ba89c9946c8e051780f0ec99aba4ba15895e0380a7ab68f420046fc44e url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.4+1" flutter_material_design_icons: dependency: "direct main" description: @@ -518,10 +446,10 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: "711d916456563f715bde1e139d7cfdca009f8264befab3ac9f8ded8b6ec26405" + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.6.1" flutter_secure_storage: dependency: "direct main" description: @@ -656,10 +584,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "534ca8af331235f700f3916a74e4579cdfdd708e2fbed342fc8aa969d41a7af0" + sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.6.1" hotreloader: dependency: transitive description: @@ -1001,42 +929,42 @@ packages: dependency: "direct main" description: name: riverpod - sha256: c86fedfb45dd1da98ee6493dd9374325cdf494e7d523ebfb0c387eecc5f7b5c9 + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.6.1" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: ac28d7bc678471ec986b42d88e5a0893513382ff7542c7ac9634463b044ac72c + sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" url: "https://pub.dev" source: hosted - version: "0.5.4" + version: "0.5.6" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: "77fdedb87d09344809e8b514ab864d0537b1cb580a93d09bf579b0403aa6203a" + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.6.1" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "63311e361ffc578d655dfc31b48dfa4ed3bc76fd06f9be845e9bf97c5c11a429" + sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.6.1" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: a35a92f2c2a4b7a5d95671c96c5432b42c20f26bb3e985e83d0b186471b61a85 + sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8" url: "https://pub.dev" source: hosted - version: "2.3.13" + version: "2.6.1" rxdart: dependency: "direct main" description: @@ -1049,10 +977,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "334fcdf0ef9c0df0e3b428faebcac9568f35c747d59831474b2fc56e156d244e" + sha256: "3af2cda1752e5c24f2fc04b6083b40f013ffe84fb90472f30c6499a9213d5442" url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.1.1" share_plus_platform_interface: dependency: transitive description: @@ -1189,26 +1117,26 @@ packages: dependency: "direct main" description: name: sqlite3 - sha256: "45f168ae2213201b54e09429ed0c593dc2c88c924a1488d6f9c523a255d567cb" + sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: ccd29dd6cf6fb9351fa07cd6f92895809adbf0779c1d986acf5e3d53b3250e33 + sha256: "7ae52b23366e5295005022e62fa093f64bfe190810223ea0ebf733a4cd140bce" url: "https://pub.dev" source: hosted - version: "0.5.25" + version: "0.5.26" sqlparser: dependency: transitive description: name: sqlparser - sha256: c5f63dff8677407ddcddfa4744c176ea6dc44286c47ba9e69e76d8071398034d + sha256: d77749237609784e337ec36c979d41f6f38a7b279df98622ae23929c8eb954a4 url: "https://pub.dev" source: hosted - version: "0.39.1" + version: "0.39.2" stack_trace: dependency: transitive description: @@ -1332,10 +1260,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" + sha256: "0dea215895a4d254401730ca0ba8204b29109a34a99fb06ae559a2b60988d2de" url: "https://pub.dev" source: hosted - version: "6.3.12" + version: "6.3.13" url_launcher_ios: dependency: transitive description: @@ -1444,10 +1372,10 @@ packages: dependency: transitive description: name: win32 - sha256: "2735daae5150e8b1dfeb3eb0544b4d3af0061e9e82cef063adcd583bdae4306a" + sha256: e1d0cc62e65dc2561f5071fcbccecf58ff20c344f8f3dc7d4922df372a11df1f url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.7.1" xdg_directories: dependency: transitive description: @@ -1464,6 +1392,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + xxh3: + dependency: "direct main" + description: + name: xxh3 + sha256: cbeb0e1d10f4c6bf67b650f395eac0cc689425b5efc2ba0cc3d3e069a0beaeec + url: "https://pub.dev" + source: hosted + version: "1.1.0" yaml: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 41038dfc..52d753fc 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -7,10 +7,8 @@ environment: sdk: '>=3.4.1 <4.0.0' dependencies: - animations: ^2.0.11 collection: ^1.19.0 copy_with_extension: ^5.0.4 - crypto: ^3.0.6 drift: ^2.21.0 dynamic_color: ^1.7.0 exceptions: ^0.6.1 @@ -21,8 +19,7 @@ dependencies: flutter: sdk: flutter flutter_hooks: ^0.20.5 - flutter_inappwebview: ^6.1.5 - flutter_markdown: ^0.7.4 + flutter_markdown: ^0.7.4+1 flutter_material_design_icons: ^1.1.7447 flutter_mozilla_components: path: ../packages/flutter_mozilla_components @@ -32,7 +29,7 @@ dependencies: go_router: ^14.3.0 google_fonts: ^6.2.1 home_widget: ^0.7.0 - hooks_riverpod: ^2.5.4 + hooks_riverpod: ^2.6.1 html: ^0.15.4 http: ^1.2.2 json_annotation: ^4.9.0 @@ -46,16 +43,16 @@ dependencies: path: ^1.9.0 path_provider: ^2.1.4 reorderable_grid: ^1.0.10 - riverpod: ^2.5.3 - riverpod_annotation: ^2.5.3 + riverpod: ^2.6.1 + riverpod_annotation: ^2.6.1 rxdart: ^0.28.0 - share_plus: ^10.1.0 + share_plus: ^10.1.1 shared_preferences: ^2.3.2 skeletonizer: ^1.4.2 speech_to_text_google_dialog: path: ../../speech_to_text_google_dialog - sqlite3: ^2.4.6 - sqlite3_flutter_libs: ^0.5.25 + sqlite3: ^2.4.7 + sqlite3_flutter_libs: ^0.5.26 synchronized: ^3.3.0+3 text_scroll: ^0.2.0 timeago: ^3.7.0 @@ -65,20 +62,21 @@ dependencies: url_launcher: ^6.3.1 uuid: ^4.5.1 watcher: ^1.1.0 + xxh3: ^1.1.0 dev_dependencies: build_runner: ^2.4.13 copy_with_extension_gen: ^5.0.4 custom_lint: ^0.6.8 dependency_validator: ^4.1.1 - drift_dev: ^2.21.0 + drift_dev: ^2.21.1 fast_equatable_lint: ^0.3.0 flutter_launcher_icons: ^0.14.1 go_router_builder: ^2.7.1 json_serializable: ^6.8.0 lint: ^2.3.0 - riverpod_generator: ^2.4.3 - riverpod_lint: ^2.3.13 + riverpod_generator: ^2.6.1 + riverpod_lint: ^2.6.1 flutter: uses-material-design: true diff --git a/packages/flutter_mozilla_components/android/build.gradle b/packages/flutter_mozilla_components/android/build.gradle index 9f4c8aae..554e7fc1 100644 --- a/packages/flutter_mozilla_components/android/build.gradle +++ b/packages/flutter_mozilla_components/android/build.gradle @@ -128,3 +128,7 @@ android { } } } + +dependencies { + implementation 'androidx.preference:preference-ktx:1.2.1' +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BaseBrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BaseBrowserFragment.kt index f1483f66..7ac2c2e7 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BaseBrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BaseBrowserFragment.kt @@ -8,18 +8,23 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.CallSuper import androidx.fragment.app.Fragment -import eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity import eu.lensai.flutter_mozilla_components.addons.WebExtensionPromptFeature import eu.lensai.flutter_mozilla_components.databinding.FragmentBrowserBinding -import mozilla.components.browser.state.state.WebExtensionState +import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey +import eu.lensai.flutter_mozilla_components.services.DownloadService import mozilla.components.concept.engine.EngineView import mozilla.components.feature.app.links.AppLinksFeature import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.downloads.manager.FetchDownloadManager +import mozilla.components.feature.downloads.temporary.ShareDownloadFeature +import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature import mozilla.components.feature.privatemode.feature.SecureWindowFeature import mozilla.components.feature.prompts.PromptFeature +import mozilla.components.feature.session.FullScreenFeature import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SwipeRefreshFeature import mozilla.components.feature.sitepermissions.SitePermissionsFeature @@ -29,10 +34,9 @@ import mozilla.components.support.base.feature.ActivityResultHandler import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.ktx.android.arch.lifecycle.addObservers +import mozilla.components.support.ktx.android.view.enterImmersiveMode +import mozilla.components.support.ktx.android.view.exitImmersiveMode import mozilla.components.support.locale.ActivityContextWrapper -import mozilla.components.support.utils.ext.requestInPlacePermissions -import mozilla.components.support.webextensions.WebExtensionPopupObserver /** * Base fragment extended by [BrowserFragment] and [ExternalAppBrowserFragment]. @@ -42,153 +46,177 @@ import mozilla.components.support.webextensions.WebExtensionPopupObserver @SuppressWarnings("LargeClass") abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler { private val sessionFeature = ViewBoundFeatureWrapper() + private val shareDownloadsFeature = ViewBoundFeatureWrapper() private val downloadsFeature = ViewBoundFeatureWrapper() private val appLinksFeature = ViewBoundFeatureWrapper() private val promptFeature = ViewBoundFeatureWrapper() private val webExtensionPromptFeature = ViewBoundFeatureWrapper() private val sitePermissionsFeature = ViewBoundFeatureWrapper() private val swipeRefreshFeature = ViewBoundFeatureWrapper() + private val secureWindowFeature = ViewBoundFeatureWrapper() + private val fullScreenFeature = ViewBoundFeatureWrapper() + private val mediaSessionFullscreenFeature = + ViewBoundFeatureWrapper() - - protected val sessionId: String? + private val sessionId: String? get() = arguments?.getString(SESSION_ID_KEY) + private var _binding: FragmentBrowserBinding? = null + val binding get() = _binding!! + + protected val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + private val backButtonHandler: List> = listOf( + fullScreenFeature, + sessionFeature, + ) + private val activityResultHandler: List> = listOf( promptFeature, ) - private var _engineView: EngineView? = null - private var _binding: FragmentBrowserBinding? = null - private var _components: Components? = null - - val binding get() = _binding!! - val components get() = _components!! - val engineView get() = _engineView!! - protected abstract fun createEngine(components: Components) : EngineView + private lateinit var requestDownloadPermissionsLauncher: ActivityResultLauncher> + private lateinit var requestSitePermissionsLauncher: ActivityResultLauncher> + private lateinit var requestPromptsPermissionsLauncher: ActivityResultLauncher> + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + requestDownloadPermissionsLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results -> + val permissions = results.keys.toTypedArray() + val grantResults = + results.values.map { + if (it) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED + }.toIntArray() + downloadsFeature.withFeature { + it.onPermissionsResult(permissions, grantResults) + } + } + + requestSitePermissionsLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results -> + val permissions = results.keys.toTypedArray() + val grantResults = + results.values.map { + if (it) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED + }.toIntArray() + sitePermissionsFeature.withFeature { + it.onPermissionsResult(permissions, grantResults) + } + } + + requestPromptsPermissionsLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results -> + val permissions = results.keys.toTypedArray() + val grantResults = + results.values.map { + if (it) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED + }.toIntArray() + promptFeature.withFeature { + it.onPermissionsResult(permissions, grantResults) + } + } + } + @CallSuper @Suppress("LongMethod") - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = FragmentBrowserBinding.inflate(inflater, container, false) - _components = GlobalComponents.components - - _engineView = createEngine(components) - _components?.engineView = _engineView - - val engineNativeView = engineView.asView() - // Set layout parameters - val layoutParams = FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT - ) - engineNativeView.layoutParams = layoutParams - - binding.swipeToRefresh.addView(engineNativeView) - - val originalContext = ActivityContextWrapper.getOriginalContext(requireActivity()) - engineView.setActivityContext(originalContext) - + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { sessionFeature.set( feature = SessionFeature( - components.store, - components.sessionUseCases.goBack, - components.sessionUseCases.goForward, - engineView, + components.core.store, + components.useCases.sessionUseCases.goBack, + components.useCases.sessionUseCases.goForward, + components.engineView!!, sessionId, ), owner = this, - view = binding.root, + view = view, ) swipeRefreshFeature.set( feature = SwipeRefreshFeature( - components.store, - components.sessionUseCases.reload, + components.core.store, + components.useCases.sessionUseCases.reload, binding.swipeToRefresh, ), owner = this, - view = binding.root, + view = view, + ) + + shareDownloadsFeature.set( + ShareDownloadFeature( + context = requireContext().applicationContext, + httpClient = components.core.client, + store = components.core.store, + tabId = sessionId, + ), + owner = this, + view = view, ) downloadsFeature.set( feature = DownloadsFeature( requireContext().applicationContext, - store = components.store, - useCases = components.downloadsUseCases, + store = components.core.store, + useCases = components.useCases.downloadsUseCases, fragmentManager = childFragmentManager, onDownloadStopped = { download, id, status -> Logger.debug("Download done. ID#$id $download with status $status") }, downloadManager = FetchDownloadManager( requireContext().applicationContext, - components.store, + components.core.store, DownloadService::class, notificationsDelegate = components.notificationsDelegate, ), tabId = sessionId, onNeedToRequestPermissions = { permissions -> - requestInPlacePermissions(REQUEST_KEY_DOWNLOAD_PERMISSIONS, permissions) { result -> - downloadsFeature.get()?.onPermissionsResult( - result.keys.toTypedArray(), - result.values.map { - when (it) { - true -> PackageManager.PERMISSION_GRANTED - false -> PackageManager.PERMISSION_DENIED - } - }.toIntArray(), - ) - } + requestDownloadPermissionsLauncher.launch(permissions) }, ), owner = this, - view = binding.root, + view = view, ) appLinksFeature.set( feature = AppLinksFeature( context = requireContext(), - store = components.store, + store = components.core.store, sessionId = sessionId, fragmentManager = parentFragmentManager, - launchInApp = { components.preferences.getBoolean(DefaultComponents.PREF_LAUNCH_EXTERNAL_APP, false) }, - loadUrlUseCase = components.sessionUseCases.loadUrl, + launchInApp = { components.core.prefs.getBoolean(context?.getPreferenceKey(R.string.pref_key_launch_external_app), false) }, + loadUrlUseCase = components.useCases.sessionUseCases.loadUrl, ), owner = this, - view = binding.root, + view = view, ) promptFeature.set( feature = PromptFeature( fragment = this, - store = components.store, + store = components.core.store, customTabId = sessionId, - tabsUseCases = components.tabsUseCases, + tabsUseCases = components.useCases.tabsUseCases, fragmentManager = parentFragmentManager, - fileUploadsDirCleaner = components.fileUploadsDirCleaner, + fileUploadsDirCleaner = components.core.fileUploadsDirCleaner, onNeedToRequestPermissions = { permissions -> - requestInPlacePermissions(REQUEST_KEY_PROMPT_PERMISSIONS, permissions) { result -> - promptFeature.get()?.onPermissionsResult( - result.keys.toTypedArray(), - result.values.map { - when (it) { - true -> PackageManager.PERMISSION_GRANTED - false -> PackageManager.PERMISSION_DENIED - } - }.toIntArray(), - ) - } + requestPromptsPermissionsLauncher.launch(permissions) }, ), owner = this, - view = binding.root, + view = view, ) sitePermissionsFeature.set( feature = SitePermissionsFeature( context = requireContext(), sessionId = sessionId, - storage = components.permissionStorage, + storage = components.core.geckoSitePermissionsStorage, fragmentManager = parentFragmentManager, sitePermissionsRules = SitePermissionsRules( autoplayAudible = AutoplayAction.BLOCKED, @@ -202,63 +230,92 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit crossOriginStorageAccess = SitePermissionsRules.Action.ASK_TO_ALLOW, ), onNeedToRequestPermissions = { permissions -> - requestInPlacePermissions(REQUEST_KEY_SITE_PERMISSIONS, permissions) { result -> - sitePermissionsFeature.get()?.onPermissionsResult( - result.keys.toTypedArray(), - result.values.map { - when (it) { - true -> PackageManager.PERMISSION_GRANTED - false -> PackageManager.PERMISSION_DENIED - } - }.toIntArray(), - ) - } + requestSitePermissionsLauncher.launch(permissions) }, onShouldShowRequestPermissionRationale = { shouldShowRequestPermissionRationale(it) }, - store = components.store, + store = components.core.store, + ), + owner = this, + view = view, + ) + + webExtensionPromptFeature.set( + feature = WebExtensionPromptFeature( + store = components.core.store, + context = requireContext(), + fragmentManager = parentFragmentManager, + ), + owner = this, + view = view + ) + + fullScreenFeature.set( + feature = FullScreenFeature( + components.core.store, + components.useCases.sessionUseCases, + sessionId, + ) { inFullScreen -> + if (inFullScreen) { + activity?.enterImmersiveMode() + } else { + activity?.exitImmersiveMode() + } + }, + owner = this, + view = binding.root, + ) + + mediaSessionFullscreenFeature.set( + feature = MediaSessionFullscreenFeature( + requireActivity(), + components.core.store, + sessionId, ), owner = this, view = binding.root, ) - webExtensionPromptFeature.set( - feature = WebExtensionPromptFeature( - store = components.store, - context = requireContext(), - fragmentManager = parentFragmentManager, + secureWindowFeature.set( + feature = SecureWindowFeature( + window = requireActivity().window, + store = components.core.store, + customTabId = sessionId, ), owner = this, - view = binding.root + view = binding.root, + ) + } + + @CallSuper + @Suppress("LongMethod") + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentBrowserBinding.inflate(inflater, container, false) + + // Set layout parameters + val layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT ) - val secureWindowFeature = SecureWindowFeature( - window = requireActivity().window, - store = components.store, - customTabId = sessionId, - ) + val originalContext = ActivityContextWrapper.getOriginalContext(requireActivity()) - val webExtensionPopupObserver = WebExtensionPopupObserver(components.store, ::openPopup) + val engineView = createEngine(components) + components.engineView = engineView - // Observe the lifecycle for supported features - lifecycle.addObservers( - secureWindowFeature, - webExtensionPopupObserver, - ) + val engineNativeView = engineView.asView() + engineNativeView.layoutParams = layoutParams + + engineView.setActivityContext(originalContext) + + binding.swipeToRefresh.addView(engineNativeView) return binding.root } - private fun openPopup(webExtensionState: WebExtensionState) { - val intent = Intent(requireContext().applicationContext, WebExtensionActionPopupActivity::class.java) - intent.putExtra("web_extension_id", webExtensionState.id) - intent.putExtra("web_extension_name", webExtensionState.name) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(intent) - } - @CallSuper - override fun onBackPressed(): Boolean = - listOf(sessionFeature).any { it.onBackPressed() } + override fun onBackPressed(): Boolean { + return backButtonHandler.any { it.onBackPressed() } + } @CallSuper override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean { @@ -268,10 +325,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit companion object { private const val SESSION_ID_KEY = "session_id" - private const val REQUEST_KEY_DOWNLOAD_PERMISSIONS = "downloadFeature" - private const val REQUEST_KEY_PROMPT_PERMISSIONS = "promptFeature" - private const val REQUEST_KEY_SITE_PERMISSIONS = "sitePermissionsFeature" - @JvmStatic protected fun Bundle.putSessionId(sessionId: String?) { putString(SESSION_ID_KEY, sessionId) @@ -279,7 +332,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit } override fun onDestroyView() { super.onDestroyView() - engineView.setActivityContext(null) + + components.engineView?.setActivityContext(null) _binding = null } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BrowserFragment.kt index fc00dbc4..daa5550c 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BrowserFragment.kt @@ -2,107 +2,89 @@ package eu.lensai.flutter_mozilla_components import android.content.Context +import android.content.Intent import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate +import eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity +import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature import eu.lensai.flutter_mozilla_components.integration.ReaderViewIntegration +import mozilla.components.browser.state.state.WebExtensionState import mozilla.components.browser.thumbnails.BrowserThumbnails import mozilla.components.concept.engine.EngineView -import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature -import mozilla.components.feature.session.FullScreenFeature import mozilla.components.feature.tabs.WindowFeature import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper -import mozilla.components.support.ktx.android.view.enterImmersiveMode -import mozilla.components.support.ktx.android.view.exitImmersiveMode +import mozilla.components.support.webextensions.WebExtensionPopupObserver /** * Fragment used for browsing the web within the main app. */ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), UserInteractionHandler { + private val windowFeature = ViewBoundFeatureWrapper() private val thumbnailsFeature = ViewBoundFeatureWrapper() private val readerViewFeature = ViewBoundFeatureWrapper() - private val fullScreenFeature = ViewBoundFeatureWrapper() - private val mediaSessionFullscreenFeature = - ViewBoundFeatureWrapper() + private val webExtensionPopupObserver = ViewBoundFeatureWrapper() + private val webExtToolbarFeature = ViewBoundFeatureWrapper() override fun createEngine(components: Components): EngineView { - return components.engine.createView(context).apply { + return components.core.engine.createView(context).apply { selectionActionDelegate = components.selectionAction } } @Suppress("LongMethod") - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - super.onCreateView(inflater, container, savedInstanceState) - - val binding = super.binding + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) readerViewFeature.set( feature = ReaderViewIntegration( requireContext(), - components.engine, - components.store, + components.core.engine, + components.core.store, binding.readerViewBar, - components.readerViewEvents, + components.events.readerViewEvents, components.readerViewController, ), owner = this, - view = binding.root, + view = view, ) - fullScreenFeature.set( - feature = FullScreenFeature( - components.store, - components.sessionUseCases, - sessionId, - ) { inFullScreen -> - if (inFullScreen) { - activity?.enterImmersiveMode() - } else { - activity?.exitImmersiveMode() - } - }, + windowFeature.set( + feature = WindowFeature(components.core.store, components.useCases.tabsUseCases), owner = this, - view = binding.root, - ) - - mediaSessionFullscreenFeature.set( - feature = MediaSessionFullscreenFeature( - requireActivity(), - components.store, - sessionId, - ), - owner = this, - view = binding.root, + view = view, ) thumbnailsFeature.set( - feature = BrowserThumbnails(requireContext(), engineView, components.store), + feature = BrowserThumbnails(requireContext(), components.engineView!!, components.core.store), owner = this, - view = binding.root, + view = view, ) - val windowFeature = WindowFeature(components.store, components.tabsUseCases) - lifecycle.addObserver(windowFeature) + webExtensionPopupObserver.set( + feature = WebExtensionPopupObserver(components.core.store, ::openPopup), + owner = this, + view = view, + ) - return binding.root + webExtToolbarFeature.set( + feature = components.features.webExtensionToolbarFeature, + owner = this, + view = view, + ) } - - override fun onBackPressed(): Boolean { - return when { - fullScreenFeature.onBackPressed() -> true - else -> super.onBackPressed() - } + private fun openPopup(webExtensionState: WebExtensionState) { + val intent = Intent(requireContext().applicationContext, WebExtensionActionPopupActivity::class.java) + intent.putExtra("web_extension_id", webExtensionState.id) + intent.putExtra("web_extension_name", webExtensionState.name) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(intent) } + override fun onBackPressed(): Boolean = + readerViewFeature.onBackPressed() || super.onBackPressed() + companion object { fun create(context: Context, sessionId: String? = null) = BrowserFragment(context).apply { arguments = Bundle().apply { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/Components.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/Components.kt new file mode 100644 index 00000000..ae9cc6a5 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/Components.kt @@ -0,0 +1,38 @@ +package eu.lensai.flutter_mozilla_components + +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import eu.lensai.flutter_mozilla_components.components.Core +import eu.lensai.flutter_mozilla_components.components.Events +import eu.lensai.flutter_mozilla_components.components.Features +import eu.lensai.flutter_mozilla_components.components.Services +import eu.lensai.flutter_mozilla_components.components.UseCases +import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents +import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents +import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController +import mozilla.components.concept.engine.EngineView +import mozilla.components.concept.engine.selection.SelectionActionDelegate +import mozilla.components.support.base.android.NotificationsDelegate + +class Components(private val context: Context, + val flutterEvents: GeckoStateEvents, + val readerViewController: ReaderViewController, + val selectionAction: SelectionActionDelegate, + val addonEvents: GeckoAddonEvents +) { + val core by lazy { Core(context, this, flutterEvents) } + val events by lazy { Events(flutterEvents) } + val useCases by lazy { UseCases(context, core.engine, core.store) } + val services by lazy { Services(context, useCases.tabsUseCases) } + val features by lazy { Features(core.store, addonEvents) } + + var engineView: EngineView? = null + var engineReportedInitialized = false + + private val notificationManagerCompat = NotificationManagerCompat.from(context) + val notificationsDelegate: NotificationsDelegate by lazy { + NotificationsDelegate( + notificationManagerCompat, + ) + } +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/DefaultComponents.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/DefaultComponents.kt index aacd5571..8a89fb22 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/DefaultComponents.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/DefaultComponents.kt @@ -1,415 +1,415 @@ -package eu.lensai.flutter_mozilla_components - - -import android.annotation.SuppressLint -import android.content.Context -import android.content.SharedPreferences -import androidx.core.app.NotificationManagerCompat -import eu.lensai.flutter_mozilla_components.api.ReaderViewEventsImpl -import eu.lensai.flutter_mozilla_components.ext.toWebPBytes -import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware -import eu.lensai.flutter_mozilla_components.pigeons.FindResultState -import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents -import eu.lensai.flutter_mozilla_components.pigeons.HistoryItem -import eu.lensai.flutter_mozilla_components.pigeons.HistoryState -import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController -import eu.lensai.flutter_mozilla_components.pigeons.ReaderableState -import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState -import eu.lensai.flutter_mozilla_components.pigeons.TabContentState -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import mozilla.components.browser.engine.gecko.GeckoEngine -import mozilla.components.browser.icons.BrowserIcons -import mozilla.components.browser.session.storage.SessionStorage -import mozilla.components.browser.state.engine.EngineMiddleware -import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware -import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.browser.thumbnails.ThumbnailsMiddleware -import mozilla.components.browser.thumbnails.storage.ThumbnailStorage -import mozilla.components.concept.base.crash.Breadcrumb -import mozilla.components.concept.engine.DefaultSettings -import mozilla.components.concept.engine.Engine -import mozilla.components.concept.engine.EngineView -import mozilla.components.concept.engine.mediaquery.PreferredColorScheme -import mozilla.components.concept.engine.selection.SelectionActionDelegate -import mozilla.components.concept.fetch.Client -import mozilla.components.feature.addons.AddonManager -import mozilla.components.feature.addons.amo.AMOAddonsProvider -import mozilla.components.feature.addons.logger -import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker -import mozilla.components.feature.addons.update.DefaultAddonUpdater -import mozilla.components.feature.app.links.AppLinksInterceptor -import mozilla.components.feature.app.links.AppLinksUseCases -import mozilla.components.feature.downloads.DownloadMiddleware -import mozilla.components.feature.downloads.DownloadsUseCases -import mozilla.components.feature.media.MediaSessionFeature -import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware -import mozilla.components.feature.prompts.PromptMiddleware -import mozilla.components.feature.prompts.file.FileUploadsDirCleaner -import mozilla.components.feature.readerview.ReaderViewMiddleware -import mozilla.components.feature.session.SessionUseCases -import mozilla.components.feature.tabs.TabsUseCases -import mozilla.components.feature.session.middleware.LastAccessMiddleware -import mozilla.components.feature.session.middleware.undo.UndoMiddleware -import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage -import mozilla.components.feature.webnotifications.WebNotificationFeature -import mozilla.components.lib.crash.Crash -import mozilla.components.lib.crash.CrashReporter -import mozilla.components.lib.crash.service.CrashReporterService -import mozilla.components.lib.dataprotect.SecureAbove22Preferences -import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient -import mozilla.components.lib.publicsuffixlist.PublicSuffixList -import mozilla.components.lib.state.ext.flowScoped -import mozilla.components.service.digitalassetlinks.local.StatementApi -import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker -import mozilla.components.support.base.android.NotificationsDelegate -import mozilla.components.support.base.worker.Frequency -import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged -import java.util.concurrent.TimeUnit - -private const val DAY_IN_MINUTES = 24 * 60L - -@SuppressLint("NewApi") -@Suppress("LargeClass") -open class DefaultComponents( - private val applicationContext: Context, - val flutterEvents: GeckoStateEvents, - val readerViewController: ReaderViewController, - val selectionAction: SelectionActionDelegate, -) { - companion object { - const val SAMPLE_BROWSER_PREFERENCES = "sample_browser_preferences" - const val PREF_LAUNCH_EXTERNAL_APP = "sample_browser_launch_external_app" - const val PREF_GLOBAL_PRIVACY_CONTROL = "sample_browser_global_privacy_control" - } - - var engineView: EngineView? = null - var engineReportedInitialized = false - - val preferences: SharedPreferences = - applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE) - - private val securePreferences by lazy { SecureAbove22Preferences(applicationContext, "key_store") } - - val publicSuffixList by lazy { PublicSuffixList(applicationContext) } - - // Engine Settings - val engineSettings by lazy { - DefaultSettings().apply { - //historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage) - requestInterceptor = AppRequestInterceptor(applicationContext) - remoteDebuggingEnabled = true - supportMultipleWindows = true - preferredColorScheme = PreferredColorScheme.Dark - httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED - globalPrivacyControlEnabled = preferences.getBoolean( - PREF_GLOBAL_PRIVACY_CONTROL, - false, - ) - } - } - - private val notificationManagerCompat = NotificationManagerCompat.from(applicationContext) - - val notificationsDelegate: NotificationsDelegate by lazy { - NotificationsDelegate( - notificationManagerCompat, - ) - } - - val addonUpdater = - DefaultAddonUpdater(applicationContext, Frequency(1, TimeUnit.DAYS), notificationsDelegate) - - // Engine - open val engine: Engine by lazy { - GeckoEngine(applicationContext, engineSettings) - } - - val icons by lazy { BrowserIcons(applicationContext, client) } - - open val client: Client by lazy { HttpURLConnectionClient() } - - // Storage - //private val lazyHistoryStorage = lazy { PlacesHistoryStorage(applicationContext) } - //val historyStorage by lazy { lazyHistoryStorage.value } - - val sessionStorage by lazy { SessionStorage(applicationContext, engine) } - - val permissionStorage by lazy { OnDiskSitePermissionsStorage(applicationContext) } - - val thumbnailStorage by lazy { ThumbnailStorage(applicationContext) } - - val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy { - FileUploadsDirCleaner { applicationContext.cacheDir } - } - - @OptIn(FlowPreview::class) - val store by lazy { - BrowserStore( - middleware = listOf( - FlutterEventMiddleware(flutterEvents), - DownloadMiddleware(applicationContext, DownloadService::class.java), - ReaderViewMiddleware(), - ThumbnailsMiddleware(thumbnailStorage), - UndoMiddleware(), - RecordingDevicesMiddleware(applicationContext, notificationsDelegate), - LastAccessMiddleware(), - PromptMiddleware(), - SessionPrioritizationMiddleware(), - ) + EngineMiddleware.create(engine), - ).apply { - this.flowScoped { flow -> - flow.map { state -> state.selectedTabId } - .distinctUntilChanged() - .collect { tabId -> - flutterEvents.onSelectedTabChange( - System.currentTimeMillis(), - tabId - ) { _ -> } - } - } - - this.flowScoped { flow -> - flow.mapNotNull { state -> state.tabs } - .filterChanged { - it.content - } - .ifAnyChanged { arrayOf (it.content.icon) } - .debounce { 50 } - .collect { tab -> - val iconBytes = tab.content.icon?.toWebPBytes() - flutterEvents.onIconChange( - System.currentTimeMillis(), - tab.id, - iconBytes - ) { _ -> } - } - } - - this.flowScoped { flow -> - flow.mapNotNull { state -> state.tabs } - .filterChanged { - it.content.securityInfo - } - .debounce { 50 } - .collect { tab -> - flutterEvents.onSecurityInfoStateChange( - System.currentTimeMillis(), - tab.id, - SecurityInfoState( - tab.content.securityInfo.secure, - tab.content.securityInfo.host, - tab.content.securityInfo.issuer, - ) - ) { _ -> } - } - } - - this.flowScoped { flow -> - flow.mapNotNull { state -> state.tabs } - .filterChanged { - it.readerState - } - .ifAnyChanged { arrayOf( - it.readerState.readerable, - it.readerState.active, - ) - } - .debounce { 50 } - .collect { tab -> - flutterEvents.onReaderableStateChange( - System.currentTimeMillis(), - tab.id, - ReaderableState( - tab.readerState.readerable, - tab.readerState.active, - ) - ) { _ -> } - } - } - - this.flowScoped { flow -> - flow.mapNotNull { state -> state.tabs } - .filterChanged { - it.content - } - .ifAnyChanged { arrayOf( - it.content.history, - it.content.canGoBack, - it.content.canGoForward, - ) - } - .debounce { 50 } - .collect { tab -> - flutterEvents.onHistoryStateChange( - System.currentTimeMillis(), - tab.id, - HistoryState( - items = tab.content.history.items.map { item -> HistoryItem( - url = item.uri, - title = item.title - ) }, - currentIndex = tab.content.history.currentIndex.toLong(), - canGoBack = tab.content.canGoBack, - canGoForward = tab.content.canGoForward, - ) - ) { _ -> } - } - } - - this.flowScoped { flow -> - flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} } - .distinctUntilChanged() - .collect { tabs -> - flutterEvents.onTabListChange(System.currentTimeMillis(), tabs) { _ -> } - } - } - - this.flowScoped { flow -> - flow.mapNotNull { state -> state.tabs } - .filterChanged { - it.content - } - .ifAnyChanged { arrayOf( - it.content.url, - it.content.title, - it.content.private, - it.content.fullScreen, - it.content.progress, - it.content.loading) - } - .debounce { 50 } - .collect { tab -> - logger.info("title: ${tab.content.title} ${tab.content.url}") - flutterEvents.onTabContentStateChange( - System.currentTimeMillis(), - TabContentState( - id = tab.id, - contextId = tab.contextId, - url = tab.content.url, - title = tab.content.title, - progress = tab.content.progress.toLong(), - isPrivate = tab.content.private, - isFullScreen = tab.content.fullScreen, - isLoading = tab.content.loading - ) - ) { _ -> } - } - } - - this.flowScoped { flow -> - flow.mapNotNull { state -> state.tabs } - .filterChanged { - it.content.findResults - } - .distinctUntilChanged() - .collect { tab -> - tab.content.findResults - flutterEvents.onFindResults( - System.currentTimeMillis(), - tab.id, - tab.content.findResults.map { result -> FindResultState( - activeMatchOrdinal = result.activeMatchOrdinal.toLong(), - numberOfMatches = result.numberOfMatches.toLong(), - isDoneCounting = result.isDoneCounting, - ) } - ) { _ -> } - } - } - - icons.install(engine, this) - - WebNotificationFeature( - applicationContext, - engine, - icons, - R.drawable.ic_launcher_foreground, - permissionStorage, - NotificationActivity::class.java, - notificationsDelegate = notificationsDelegate, - ) - - MediaSessionFeature(applicationContext, MediaSessionService::class.java, this).start() - } - } - - val sessionUseCases by lazy { SessionUseCases(store) } - val tabsUseCases by lazy { TabsUseCases(store) } - - val readerViewEvents by lazy { ReaderViewEventsImpl() } - - // Addons - val addonManager by lazy { - AddonManager(store, engine, addonsProvider, addonUpdater) - } - - val addonsProvider by lazy { - AMOAddonsProvider( - applicationContext, - client, - collectionName = "7dfae8669acc4312a65e8ba5553036", - maxCacheAgeInMinutes = DAY_IN_MINUTES, - ) - } - - val supportedAddonsChecker by lazy { - DefaultSupportedAddonsChecker(applicationContext, Frequency(1, TimeUnit.DAYS)) - } - - val appLinksUseCases by lazy { AppLinksUseCases(applicationContext) } - - val appLinksInterceptor by lazy { - AppLinksInterceptor( - applicationContext, - interceptLinkClicks = true, - launchInApp = { - preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false) - }, - ) - } - - // Digital Asset Links checking - val relationChecker by lazy { - StatementRelationChecker(StatementApi(client)) - } - - val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) } - - val crashReporter: CrashReporter by lazy { - CrashReporter( - applicationContext, - services = listOf( - object : CrashReporterService { - override val id: String - get() = "xxx" - override val name: String - get() = "Test" - - override fun createCrashReportUrl(identifier: String): String? { - return null - } - - override fun report(crash: Crash.UncaughtExceptionCrash): String? { - return null - } - - override fun report(crash: Crash.NativeCodeCrash): String? { - return null - } - - override fun report( - throwable: Throwable, - breadcrumbs: ArrayList, - ): String? { - return null - } - }, - ), - notificationsDelegate = notificationsDelegate, - ).install(applicationContext) - } -} +//package eu.lensai.flutter_mozilla_components +// +// +//import android.annotation.SuppressLint +//import android.content.Context +//import android.content.SharedPreferences +//import androidx.core.app.NotificationManagerCompat +//import eu.lensai.flutter_mozilla_components.api.ReaderViewEventsImpl +//import eu.lensai.flutter_mozilla_components.ext.toWebPBytes +//import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware +//import eu.lensai.flutter_mozilla_components.pigeons.FindResultState +//import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents +//import eu.lensai.flutter_mozilla_components.pigeons.HistoryItem +//import eu.lensai.flutter_mozilla_components.pigeons.HistoryState +//import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController +//import eu.lensai.flutter_mozilla_components.pigeons.ReaderableState +//import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState +//import eu.lensai.flutter_mozilla_components.pigeons.TabContentState +//import kotlinx.coroutines.FlowPreview +//import kotlinx.coroutines.flow.debounce +//import kotlinx.coroutines.flow.distinctUntilChanged +//import kotlinx.coroutines.flow.map +//import kotlinx.coroutines.flow.mapNotNull +//import mozilla.components.browser.engine.gecko.GeckoEngine +//import mozilla.components.browser.icons.BrowserIcons +//import mozilla.components.browser.session.storage.SessionStorage +//import mozilla.components.browser.state.engine.EngineMiddleware +//import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware +//import mozilla.components.browser.state.store.BrowserStore +//import mozilla.components.browser.thumbnails.ThumbnailsMiddleware +//import mozilla.components.browser.thumbnails.storage.ThumbnailStorage +//import mozilla.components.concept.base.crash.Breadcrumb +//import mozilla.components.concept.engine.DefaultSettings +//import mozilla.components.concept.engine.Engine +//import mozilla.components.concept.engine.EngineView +//import mozilla.components.concept.engine.mediaquery.PreferredColorScheme +//import mozilla.components.concept.engine.selection.SelectionActionDelegate +//import mozilla.components.concept.fetch.Client +//import mozilla.components.feature.addons.AddonManager +//import mozilla.components.feature.addons.amo.AMOAddonsProvider +//import mozilla.components.feature.addons.logger +//import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker +//import mozilla.components.feature.addons.update.DefaultAddonUpdater +//import mozilla.components.feature.app.links.AppLinksInterceptor +//import mozilla.components.feature.app.links.AppLinksUseCases +//import mozilla.components.feature.downloads.DownloadMiddleware +//import mozilla.components.feature.downloads.DownloadsUseCases +//import mozilla.components.feature.media.MediaSessionFeature +//import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware +//import mozilla.components.feature.prompts.PromptMiddleware +//import mozilla.components.feature.prompts.file.FileUploadsDirCleaner +//import mozilla.components.feature.readerview.ReaderViewMiddleware +//import mozilla.components.feature.session.SessionUseCases +//import mozilla.components.feature.tabs.TabsUseCases +//import mozilla.components.feature.session.middleware.LastAccessMiddleware +//import mozilla.components.feature.session.middleware.undo.UndoMiddleware +//import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage +//import mozilla.components.feature.webnotifications.WebNotificationFeature +//import mozilla.components.lib.crash.Crash +//import mozilla.components.lib.crash.CrashReporter +//import mozilla.components.lib.crash.service.CrashReporterService +//import mozilla.components.lib.dataprotect.SecureAbove22Preferences +//import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient +//import mozilla.components.lib.publicsuffixlist.PublicSuffixList +//import mozilla.components.lib.state.ext.flowScoped +//import mozilla.components.service.digitalassetlinks.local.StatementApi +//import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker +//import mozilla.components.support.base.android.NotificationsDelegate +//import mozilla.components.support.base.worker.Frequency +//import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged +//import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged +//import java.util.concurrent.TimeUnit +// +//private const val DAY_IN_MINUTES = 24 * 60L +// +//@SuppressLint("NewApi") +//@Suppress("LargeClass") +//open class DefaultComponents( +// private val applicationContext: Context, +// val flutterEvents: GeckoStateEvents, +// val readerViewController: ReaderViewController, +// val selectionAction: SelectionActionDelegate, +//) { +// companion object { +// const val SAMPLE_BROWSER_PREFERENCES = "sample_browser_preferences" +// const val PREF_LAUNCH_EXTERNAL_APP = "sample_browser_launch_external_app" +// const val PREF_GLOBAL_PRIVACY_CONTROL = "sample_browser_global_privacy_control" +// } +// +// var engineView: EngineView? = null +// var engineReportedInitialized = false +// +// val preferences: SharedPreferences = +// applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE) +// +// private val securePreferences by lazy { SecureAbove22Preferences(applicationContext, "key_store") } +// +// val publicSuffixList by lazy { PublicSuffixList(applicationContext) } +// +// // Engine Settings +// val engineSettings by lazy { +// DefaultSettings().apply { +// //historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage) +// requestInterceptor = AppRequestInterceptor(applicationContext) +// remoteDebuggingEnabled = true +// supportMultipleWindows = true +// preferredColorScheme = PreferredColorScheme.Dark +// httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED +// globalPrivacyControlEnabled = preferences.getBoolean( +// PREF_GLOBAL_PRIVACY_CONTROL, +// false, +// ) +// } +// } +// +// private val notificationManagerCompat = NotificationManagerCompat.from(applicationContext) +// +// val notificationsDelegate: NotificationsDelegate by lazy { +// NotificationsDelegate( +// notificationManagerCompat, +// ) +// } +// +// val addonUpdater = +// DefaultAddonUpdater(applicationContext, Frequency(1, TimeUnit.DAYS), notificationsDelegate) +// +// // Engine +// open val engine: Engine by lazy { +// GeckoEngine(applicationContext, engineSettings) +// } +// +// val icons by lazy { BrowserIcons(applicationContext, client) } +// +// open val client: Client by lazy { HttpURLConnectionClient() } +// +// // Storage +// //private val lazyHistoryStorage = lazy { PlacesHistoryStorage(applicationContext) } +// //val historyStorage by lazy { lazyHistoryStorage.value } +// +// val sessionStorage by lazy { SessionStorage(applicationContext, engine) } +// +// val permissionStorage by lazy { OnDiskSitePermissionsStorage(applicationContext) } +// +// val thumbnailStorage by lazy { ThumbnailStorage(applicationContext) } +// +// val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy { +// FileUploadsDirCleaner { applicationContext.cacheDir } +// } +// +// @OptIn(FlowPreview::class) +// val store by lazy { +// BrowserStore( +// middleware = listOf( +// FlutterEventMiddleware(flutterEvents), +// DownloadMiddleware(applicationContext, DownloadService::class.java), +// ReaderViewMiddleware(), +// ThumbnailsMiddleware(thumbnailStorage), +// UndoMiddleware(), +// RecordingDevicesMiddleware(applicationContext, notificationsDelegate), +// LastAccessMiddleware(), +// PromptMiddleware(), +// SessionPrioritizationMiddleware(), +// ) + EngineMiddleware.create(engine), +// ).apply { +// this.flowScoped { flow -> +// flow.map { state -> state.selectedTabId } +// .distinctUntilChanged() +// .collect { tabId -> +// flutterEvents.onSelectedTabChange( +// System.currentTimeMillis(), +// tabId +// ) { _ -> } +// } +// } +// +// this.flowScoped { flow -> +// flow.mapNotNull { state -> state.tabs } +// .filterChanged { +// it.content +// } +// .ifAnyChanged { arrayOf (it.content.icon) } +// .debounce { 50 } +// .collect { tab -> +// val iconBytes = tab.content.icon?.toWebPBytes() +// flutterEvents.onIconChange( +// System.currentTimeMillis(), +// tab.id, +// iconBytes +// ) { _ -> } +// } +// } +// +// this.flowScoped { flow -> +// flow.mapNotNull { state -> state.tabs } +// .filterChanged { +// it.content.securityInfo +// } +// .debounce { 50 } +// .collect { tab -> +// flutterEvents.onSecurityInfoStateChange( +// System.currentTimeMillis(), +// tab.id, +// SecurityInfoState( +// tab.content.securityInfo.secure, +// tab.content.securityInfo.host, +// tab.content.securityInfo.issuer, +// ) +// ) { _ -> } +// } +// } +// +// this.flowScoped { flow -> +// flow.mapNotNull { state -> state.tabs } +// .filterChanged { +// it.readerState +// } +// .ifAnyChanged { arrayOf( +// it.readerState.readerable, +// it.readerState.active, +// ) +// } +// .debounce { 50 } +// .collect { tab -> +// flutterEvents.onReaderableStateChange( +// System.currentTimeMillis(), +// tab.id, +// ReaderableState( +// tab.readerState.readerable, +// tab.readerState.active, +// ) +// ) { _ -> } +// } +// } +// +// this.flowScoped { flow -> +// flow.mapNotNull { state -> state.tabs } +// .filterChanged { +// it.content +// } +// .ifAnyChanged { arrayOf( +// it.content.history, +// it.content.canGoBack, +// it.content.canGoForward, +// ) +// } +// .debounce { 50 } +// .collect { tab -> +// flutterEvents.onHistoryStateChange( +// System.currentTimeMillis(), +// tab.id, +// HistoryState( +// items = tab.content.history.items.map { item -> HistoryItem( +// url = item.uri, +// title = item.title +// ) }, +// currentIndex = tab.content.history.currentIndex.toLong(), +// canGoBack = tab.content.canGoBack, +// canGoForward = tab.content.canGoForward, +// ) +// ) { _ -> } +// } +// } +// +// this.flowScoped { flow -> +// flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} } +// .distinctUntilChanged() +// .collect { tabs -> +// flutterEvents.onTabListChange(System.currentTimeMillis(), tabs) { _ -> } +// } +// } +// +// this.flowScoped { flow -> +// flow.mapNotNull { state -> state.tabs } +// .filterChanged { +// it.content +// } +// .ifAnyChanged { arrayOf( +// it.content.url, +// it.content.title, +// it.content.private, +// it.content.fullScreen, +// it.content.progress, +// it.content.loading) +// } +// .debounce { 50 } +// .collect { tab -> +// logger.info("title: ${tab.content.title} ${tab.content.url}") +// flutterEvents.onTabContentStateChange( +// System.currentTimeMillis(), +// TabContentState( +// id = tab.id, +// contextId = tab.contextId, +// url = tab.content.url, +// title = tab.content.title, +// progress = tab.content.progress.toLong(), +// isPrivate = tab.content.private, +// isFullScreen = tab.content.fullScreen, +// isLoading = tab.content.loading +// ) +// ) { _ -> } +// } +// } +// +// this.flowScoped { flow -> +// flow.mapNotNull { state -> state.tabs } +// .filterChanged { +// it.content.findResults +// } +// .distinctUntilChanged() +// .collect { tab -> +// tab.content.findResults +// flutterEvents.onFindResults( +// System.currentTimeMillis(), +// tab.id, +// tab.content.findResults.map { result -> FindResultState( +// activeMatchOrdinal = result.activeMatchOrdinal.toLong(), +// numberOfMatches = result.numberOfMatches.toLong(), +// isDoneCounting = result.isDoneCounting, +// ) } +// ) { _ -> } +// } +// } +// +// icons.install(engine, this) +// +// WebNotificationFeature( +// applicationContext, +// engine, +// icons, +// R.drawable.ic_launcher_foreground, +// permissionStorage, +// NotificationActivity::class.java, +// notificationsDelegate = notificationsDelegate, +// ) +// +// MediaSessionFeature(applicationContext, MediaSessionService::class.java, this).start() +// } +// } +// +// val sessionUseCases by lazy { SessionUseCases(store) } +// val tabsUseCases by lazy { TabsUseCases(store) } +// +// val readerViewEvents by lazy { ReaderViewEventsImpl() } +// +// // Addons +// val addonManager by lazy { +// AddonManager(store, engine, addonsProvider, addonUpdater) +// } +// +// val addonsProvider by lazy { +// AMOAddonsProvider( +// applicationContext, +// client, +// collectionName = "7dfae8669acc4312a65e8ba5553036", +// maxCacheAgeInMinutes = DAY_IN_MINUTES, +// ) +// } +// +// val supportedAddonsChecker by lazy { +// DefaultSupportedAddonsChecker(applicationContext, Frequency(1, TimeUnit.DAYS)) +// } +// +// val appLinksUseCases by lazy { AppLinksUseCases(applicationContext) } +// +// val appLinksInterceptor by lazy { +// AppLinksInterceptor( +// applicationContext, +// interceptLinkClicks = true, +// launchInApp = { +// preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false) +// }, +// ) +// } +// +// // Digital Asset Links checking +// val relationChecker by lazy { +// StatementRelationChecker(StatementApi(client)) +// } +// +// val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) } +// +// val crashReporter: CrashReporter by lazy { +// CrashReporter( +// applicationContext, +// services = listOf( +// object : CrashReporterService { +// override val id: String +// get() = "xxx" +// override val name: String +// get() = "Test" +// +// override fun createCrashReportUrl(identifier: String): String? { +// return null +// } +// +// override fun report(crash: Crash.UncaughtExceptionCrash): String? { +// return null +// } +// +// override fun report(crash: Crash.NativeCodeCrash): String? { +// return null +// } +// +// override fun report( +// throwable: Throwable, +// breadcrumbs: ArrayList, +// ): String? { +// return null +// } +// }, +// ), +// notificationsDelegate = notificationsDelegate, +// ).install(applicationContext) +// } +//} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/DownloadService.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/DownloadService.kt deleted file mode 100644 index de3014e9..00000000 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/DownloadService.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.lensai.flutter_mozilla_components - -import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.feature.downloads.AbstractFetchDownloadService -import mozilla.components.support.base.android.NotificationsDelegate - -class DownloadService : AbstractFetchDownloadService() { - override val httpClient by lazy { GlobalComponents.components!!.client } - override val store: BrowserStore by lazy { GlobalComponents.components!!.store } - override val notificationsDelegate: NotificationsDelegate by lazy { GlobalComponents.components!!.notificationsDelegate } -} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/EngineProvider.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/EngineProvider.kt new file mode 100644 index 00000000..c13823e4 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/EngineProvider.kt @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package eu.lensai.flutter_mozilla_components + +import android.content.Context +import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature +import mozilla.components.browser.engine.gecko.GeckoEngine +import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient +import mozilla.components.concept.engine.DefaultSettings +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.fetch.Client +import mozilla.components.feature.webcompat.WebCompatFeature +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoRuntimeSettings + +object EngineProvider { + private var runtime: GeckoRuntime? = null + + @Synchronized + fun getOrCreateRuntime(context: Context): GeckoRuntime { + if (runtime == null) { + Logger.debug("Creating Runtime") + val builder = GeckoRuntimeSettings.Builder() + +// if (isCrashReportActive) { +// builder.crashHandler(CrashHandlerService::class.java) +// } + + // About config it's no longer enabled by default + builder.aboutConfigEnabled(true) + builder.extensionsWebAPIEnabled(true) + runtime = GeckoRuntime.create(context, builder.build()) + } + + return runtime!! + } + + fun createEngine(context: Context, defaultSettings: DefaultSettings): Engine { + Logger.debug("Creating Engine") + val runtime = getOrCreateRuntime(context) + + return GeckoEngine(context, defaultSettings, runtime).also { + WebCompatFeature.install(it) + CookieManagerFeature.install(it) + } + } + + fun createClient(context: Context): Client { + Logger.debug("Fetching Client") + val runtime = getOrCreateRuntime(context) + return GeckoViewFetchClient(context, runtime) + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt index 214f6969..5cd09b20 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt @@ -1,9 +1,10 @@ package eu.lensai.flutter_mozilla_components import android.app.Activity -import android.content.Context import android.content.Intent import androidx.fragment.app.FragmentActivity +import eu.lensai.flutter_mozilla_components.activities.NotificationActivity +import eu.lensai.flutter_mozilla_components.api.GeckoAddonsApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl @@ -12,8 +13,9 @@ import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerImpl import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl -import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate +import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents +import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi @@ -30,68 +32,16 @@ import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import mozilla.components.browser.engine.gecko.GeckoEngine -import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient -import mozilla.components.concept.engine.Engine -import mozilla.components.concept.engine.selection.SelectionActionDelegate -import mozilla.components.experiment.NimbusExperimentDelegate -import mozilla.components.feature.webcompat.WebCompatFeature -import mozilla.components.lib.crash.handler.CrashHandlerService import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.sink.AndroidLogSink -import org.mozilla.geckoview.GeckoRuntime -import org.mozilla.geckoview.GeckoRuntimeSettings - -/** - * Helper class for lazily instantiating components needed by the application. - */ -class Components( - private val applicationContext: Context, - flutterEvents: GeckoStateEvents, - readerViewController: ReaderViewController, - selectionAction: SelectionActionDelegate, -) : DefaultComponents( - applicationContext, - flutterEvents, - readerViewController, - selectionAction -) { - private val runtime by lazy { - // Allow for exfiltrating Gecko metrics through the Glean SDK. - val builder = GeckoRuntimeSettings.Builder() - .aboutConfigEnabled(true) - .extensionsWebAPIEnabled(true) - - builder.experimentDelegate(NimbusExperimentDelegate()) - builder.crashHandler(CrashHandlerService::class.java) - - GeckoRuntime.create(applicationContext, builder.build()) - } - - override val engine: Engine by lazy { - GeckoEngine(applicationContext, engineSettings, runtime).also { -// it.installBuiltInWebExtension("borderify@mozac.org", "resource://android/assets/extensions/borderify/") { -// throwable -> -// Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install borderify") -// } -// -// it.installBuiltInWebExtension("testext@mozac.org", "resource://android/assets/extensions/test/") { -// throwable -> -// Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install testext") -// } - - WebCompatFeature.install(it) - CookieManagerFeature.install(it) - //WebCompatReporterFeature.install(it) - } - } - - override val client by lazy { GeckoViewFetchClient(applicationContext, runtime) } -} /** FlutterMozillaComponentsPlugin */ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + private var activity: Activity? = null private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding; @@ -112,11 +62,14 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { val readerViewController = ReaderViewController(_flutterPluginBinding.binaryMessenger) + val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger) + GlobalComponents.setUp( flutterPluginBinding.applicationContext, _flutterEvents, readerViewController, - selectionActionDelegate + selectionActionDelegate, + addonEvents ) GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl { @@ -124,6 +77,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { }) GeckoEngineSettingsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoEngineSettingsApiImpl()) + GeckoAddonsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAddonsApiImpl(flutterPluginBinding.applicationContext)) GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl()) GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl()) GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl()) @@ -135,7 +89,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { ReaderViewEvents.setUp( _flutterPluginBinding.binaryMessenger, - GlobalComponents.components!!.readerViewEvents + components.events.readerViewEvents ) val intent = Intent(flutterPluginBinding.applicationContext, NotificationActivity::class.java) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GeckoPlatformView.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GeckoPlatformView.kt index 44c295cd..e77dfa1f 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GeckoPlatformView.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GeckoPlatformView.kt @@ -26,6 +26,10 @@ private class NativeFragmentView( containerId: Int, private val flutterEvents: GeckoStateEvents ) : PlatformView { + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + private val container: View init { @@ -41,7 +45,7 @@ private class NativeFragmentView( override fun onFlutterViewAttached(flutterView: View) { super.onFlutterViewAttached(flutterView) - GlobalComponents.components!!.engineReportedInitialized = false; + components.engineReportedInitialized = false; flutterEvents.onViewReadyStateChange(System.currentTimeMillis(),true) { _ -> } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GlobalComponents.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GlobalComponents.kt index 720cbe1a..63b0f7d3 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GlobalComponents.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GlobalComponents.kt @@ -1,6 +1,7 @@ package eu.lensai.flutter_mozilla_components import android.content.Context +import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController import kotlinx.coroutines.DelicateCoroutinesApi @@ -11,7 +12,9 @@ import mozilla.components.concept.engine.selection.SelectionActionDelegate import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider import mozilla.components.support.base.facts.Facts import mozilla.components.support.base.facts.processor.LogFactProcessor +import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.base.log.sink.AndroidLogSink import mozilla.components.support.webextensions.WebExtensionSupport import java.util.concurrent.TimeUnit @@ -23,60 +26,69 @@ object GlobalComponents { @DelicateCoroutinesApi private fun restoreBrowserState(newComponents: Components) = GlobalScope.launch(Dispatchers.Main) { - newComponents.tabsUseCases.restore(newComponents.sessionStorage) + newComponents.useCases.tabsUseCases.restore(newComponents.core.sessionStorage) - newComponents.sessionStorage.autoSave(newComponents.store) + newComponents.core.sessionStorage.autoSave(newComponents.core.store) .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS) .whenGoingToBackground() .whenSessionsChange() } + @OptIn(DelicateCoroutinesApi::class) fun setUp( applicationContext: Context, flutterEvents: GeckoStateEvents, readerViewController: ReaderViewController, - selectionAction: SelectionActionDelegate + selectionAction: SelectionActionDelegate, + addonEvents: GeckoAddonEvents ) { + Logger.debug("Creating new components") + val newComponents = Components( applicationContext, flutterEvents, readerViewController, - selectionAction + selectionAction, + addonEvents, ) - newComponents.crashReporter.install(applicationContext) - Facts.registerProcessor(LogFactProcessor()) + //newComponents.crashReporter.install(applicationContext) - newComponents.engine.warmUp() + //Facts.registerProcessor(LogFactProcessor()) + + //RustHttpConfig.setClient(lazy { newComponents.core.client }) + + newComponents.core.engine.warmUp() restoreBrowserState(newComponents) - newComponents.downloadsUseCases.restoreDownloads() + + //newComponents.useCases.downloadsUseCases.restoreDownloads() try { GlobalAddonDependencyProvider.initialize( - newComponents.addonManager, - newComponents.addonUpdater, + newComponents.core.addonManager, + newComponents.core.addonUpdater, ) WebExtensionSupport.initialize( - newComponents.engine, - newComponents.store, + newComponents.core.engine, + newComponents.core.store, onNewTabOverride = { _, engineSession, url -> - newComponents.tabsUseCases.addTab(url, selectTab = true, engineSession = engineSession) + newComponents.useCases.tabsUseCases.addTab(url, selectTab = true, engineSession = engineSession) }, onCloseTabOverride = { _, sessionId -> - newComponents.tabsUseCases.removeTab(sessionId) + newComponents.useCases.tabsUseCases.removeTab(sessionId) }, onSelectTabOverride = { _, sessionId -> - newComponents.tabsUseCases.selectTab(sessionId) + newComponents.useCases.tabsUseCases.selectTab(sessionId) }, - onUpdatePermissionRequest = newComponents.addonUpdater::onUpdatePermissionRequest, + onUpdatePermissionRequest = newComponents.core.addonUpdater::onUpdatePermissionRequest, onExtensionsLoaded = { extensions -> - newComponents.addonUpdater.registerForFutureUpdates(extensions) - newComponents.supportedAddonsChecker.registerForChecks() + newComponents.core.addonUpdater.registerForFutureUpdates(extensions) + newComponents.core.supportedAddonsChecker.registerForChecks() }, ) } catch (e: UnsupportedOperationException) { @@ -84,6 +96,10 @@ object GlobalComponents { Logger.error("Failed to initialize web extension support", e) } + GlobalScope.launch(Dispatchers.IO) { + newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory() + } + _components = newComponents } } \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/IntentReceiverActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/IntentReceiverActivity.kt deleted file mode 100644 index 073d2942..00000000 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/IntentReceiverActivity.kt +++ /dev/null @@ -1,2 +0,0 @@ -package eu.lensai.flutter_mozilla_components - diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/NotificationActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/NotificationActivity.kt deleted file mode 100644 index fbd50f46..00000000 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/NotificationActivity.kt +++ /dev/null @@ -1,14 +0,0 @@ -package eu.lensai.flutter_mozilla_components - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class NotificationActivity: AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - GlobalComponents.components!!.notificationsDelegate.bindToActivity(this) - - finish() - } -} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/activities/NotificationActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/activities/NotificationActivity.kt new file mode 100644 index 00000000..95a7a82e --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/activities/NotificationActivity.kt @@ -0,0 +1,19 @@ +package eu.lensai.flutter_mozilla_components.activities + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import eu.lensai.flutter_mozilla_components.GlobalComponents + +class NotificationActivity: AppCompatActivity() { + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + components.notificationsDelegate.bindToActivity(this) + + finish() + } +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/AddonSettingsActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/AddonSettingsActivity.kt index b66fad80..f1287df3 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/AddonSettingsActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/AddonSettingsActivity.kt @@ -21,13 +21,14 @@ import mozilla.components.feature.addons.ui.translateName import mozilla.components.support.utils.ext.getParcelableCompat import mozilla.components.support.utils.ext.getParcelableExtraCompat import eu.lensai.flutter_mozilla_components.R -import mozilla.components.browser.state.store.BrowserStore /** * An activity to show the settings of an add-on. */ class AddonSettingsActivity : AppCompatActivity() { - private val components: Components by lazy { GlobalComponents.components!! } + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -47,7 +48,7 @@ class AddonSettingsActivity : AppCompatActivity() { override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? = when (name) { - EngineView::class.java.name -> components.engine.createView(context, attrs).asView() + EngineView::class.java.name -> components.core.engine.createView(context, attrs).asView() else -> super.onCreateView(parent, name, context, attrs) } @@ -55,7 +56,9 @@ class AddonSettingsActivity : AppCompatActivity() { * A fragment to show the settings of an add-on with [EngineView]. */ class AddonSettingsFragment : Fragment() { - private val components: Components by lazy { GlobalComponents.components!! } + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } private lateinit var optionsPageUrl: String private lateinit var engineSession: EngineSession @@ -68,7 +71,7 @@ class AddonSettingsActivity : AppCompatActivity() { )?.installedState?.optionsPageUrl, ) - engineSession = components.engine.createSession() + engineSession = components.core.engine.createSession() return inflater.inflate(R.layout.fragment_add_on_settings, container, false) } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/AddonsFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/AddonsFragment.kt index b3bb7cef..a4d31635 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/AddonsFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/AddonsFragment.kt @@ -30,7 +30,9 @@ import mozilla.components.feature.addons.R as MozComp * Fragment use for managing add-ons. */ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate { - private val components: Components by lazy { GlobalComponents.components!! } + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } private val webExtensionPromptFeature = ViewBoundFeatureWrapper() private lateinit var recyclerView: RecyclerView @@ -54,7 +56,7 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate { bindRecyclerView(rootView) webExtensionPromptFeature.set( feature = WebExtensionPromptFeature( - store = components.store, + store = components.core.store, context = requireContext(), fragmentManager = parentFragmentManager, ), @@ -78,13 +80,13 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate { recyclerView.layoutManager = LinearLayoutManager(requireContext()) scope.launch { try { - addons = components.addonManager.getAddons() + addons = components.core.addonManager.getAddons() scope.launch(Dispatchers.Main) { adapter = AddonsManagerAdapter( this@AddonsFragment, addons, - store = components.store, + store = components.core.store, ) recyclerView.adapter = adapter } @@ -122,7 +124,7 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate { private val installAddon: ((Addon) -> Unit) = { addon -> addonProgressOverlay.visibility = View.VISIBLE isInstallationInProgress = true - components.addonManager.installAddon( + components.core.addonManager.installAddon( url = addon.downloadUrl, onSuccess = { runIfFragmentIsAttached { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/InstalledAddonDetailsActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/InstalledAddonDetailsActivity.kt index f0ee1635..73153fbf 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/InstalledAddonDetailsActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/InstalledAddonDetailsActivity.kt @@ -27,7 +27,9 @@ import mozilla.components.feature.addons.R as MozComp * An activity to show the details of a installed add-on. */ class InstalledAddonDetailsActivity : AppCompatActivity() { - private val components: Components by lazy { GlobalComponents.components!! } + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } private val scope = CoroutineScope(Dispatchers.IO) @@ -46,7 +48,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() { private fun bindAddon(addon: Addon) { scope.launch { try { - val addons = components.addonManager.getAddons() + val addons = components.core.addonManager.getAddons() scope.launch(Dispatchers.Main) { addons.find { addon.id == it.id }.let { if (it == null) { @@ -89,7 +91,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() { switch.setState(addon.isEnabled()) switch.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - components.addonManager.enableAddon( + components.core.addonManager.enableAddon( addon, onSuccess = { switch.setState(true) @@ -108,7 +110,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() { }, ) } else { - components.addonManager.disableAddon( + components.core.addonManager.disableAddon( addon, onSuccess = { switch.setState(false) @@ -161,7 +163,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() { val switch = findViewById(R.id.allow_in_private_browsing_switch) switch.isChecked = addon.isAllowedInPrivateBrowsing() switch.setOnCheckedChangeListener { _, isChecked -> - components.addonManager.setAddonAllowedInPrivateBrowsing( + components.core.addonManager.setAddonAllowedInPrivateBrowsing( addon, isChecked, onSuccess = { @@ -173,7 +175,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() { private fun bindRemoveButton(addon: Addon) { findViewById(R.id.remove_add_on).setOnClickListener { - components.addonManager.uninstallAddon( + components.core.addonManager.uninstallAddon( addon, onSuccess = { Toast.makeText( diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/WebExtensionActionPopupActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/WebExtensionActionPopupActivity.kt index 73a9efa8..31327ce9 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/WebExtensionActionPopupActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/WebExtensionActionPopupActivity.kt @@ -25,7 +25,9 @@ import eu.lensai.flutter_mozilla_components.R * An activity to show the pop up action of a web extension. */ class WebExtensionActionPopupActivity : AppCompatActivity() { - private val components: Components by lazy { GlobalComponents.components!! } + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } private lateinit var webExtensionId: String @@ -46,7 +48,7 @@ class WebExtensionActionPopupActivity : AppCompatActivity() { override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? = when (name) { - EngineView::class.java.name -> components.engine.createView(context, attrs).asView() + EngineView::class.java.name -> components.core.engine.createView(context, attrs).asView() else -> super.onCreateView(parent, name, context, attrs) } @@ -54,7 +56,9 @@ class WebExtensionActionPopupActivity : AppCompatActivity() { * A fragment to show the web extension action popup with [EngineView]. */ class WebExtensionActionPopupFragment : Fragment(), EngineSession.Observer { - private val components: Components by lazy { GlobalComponents.components!! } + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } private var engineSession: EngineSession? = null private lateinit var webExtensionId: String @@ -64,7 +68,7 @@ class WebExtensionActionPopupActivity : AppCompatActivity() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { webExtensionId = requireNotNull(arguments?.getString("web_extension_id")) - engineSession = components.store.state.extensions[webExtensionId]?.popupSession + engineSession = components.core.store.state.extensions[webExtensionId]?.popupSession return inflater.inflate(R.layout.fragment_add_on_settings, container, false) } @@ -77,7 +81,7 @@ class WebExtensionActionPopupActivity : AppCompatActivity() { addonSettingsEngineView.render(session) consumePopupSession() } else { - consumeFrom(components.store) { state -> + consumeFrom(components.core.store) { state -> state.extensions[webExtensionId]?.let { extState -> extState.popupSession?.let { if (engineSession == null) { @@ -108,7 +112,7 @@ class WebExtensionActionPopupActivity : AppCompatActivity() { } private fun consumePopupSession() { - components.store.dispatch( + components.core.store.dispatch( WebExtensionAction.UpdatePopupSessionAction(webExtensionId, popupSession = null), ) } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoAddonsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoAddonsApiImpl.kt new file mode 100644 index 00000000..50c67b17 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoAddonsApiImpl.kt @@ -0,0 +1,27 @@ +package eu.lensai.flutter_mozilla_components.api + +import android.content.Context +import android.content.Intent +import eu.lensai.flutter_mozilla_components.GlobalComponents +import eu.lensai.flutter_mozilla_components.addons.AddonsActivity +import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi +import eu.lensai.flutter_mozilla_components.pigeons.WebExtensionActionType + +class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi { + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + override fun startAddonManagerActivity() { + val intent = Intent(context, AddonsActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } + + override fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType) { + when(actionType) { + WebExtensionActionType.BROWSER -> components.features.webExtensionToolbarFeature.invokeAddonBrowserAction(extensionId) + WebExtensionActionType.PAGE -> components.features.webExtensionToolbarFeature.invokeAddonPageAction(extensionId) + } + } +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt index 71336acf..dbdb1262 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt @@ -5,17 +5,35 @@ import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi import mozilla.components.browser.state.action.SystemAction import mozilla.components.feature.addons.logger +/** + * Implementation of GeckoBrowserApi that handles browser-related operations + * @param showFragmentCallback Callback function to show native fragment + */ class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Unit) : GeckoBrowserApi { + companion object { + private const val TAG = "GeckoBrowserApiImpl" + } + override fun showNativeFragment() { - showFragmentCallback.invoke(); + try { + showFragmentCallback() + } catch (e: Exception) { + logger.error("Failed to show native fragment", e) + } } override fun onTrimMemory(level: Long) { - logger.debug("onTrimMemory: $level") + requireNotNull(GlobalComponents.components) { "Components not initialized" } - val components = GlobalComponents.components!! + logger.debug("$TAG: onTrimMemory called with level: $level") - components.store.dispatch(SystemAction.LowMemoryAction(level.toInt())) - components.icons.onTrimMemory(level.toInt()) + with(GlobalComponents.components!!) { + try { + core.store.dispatch(SystemAction.LowMemoryAction(level.toInt())) + core.icons.onTrimMemory(level.toInt()) + } catch (e: Exception) { + logger.error("$TAG: Failed to handle memory trim", e) + } + } } -} \ No newline at end of file +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoCookieApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoCookieApiImpl.kt index 03140651..00f90730 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoCookieApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoCookieApiImpl.kt @@ -2,64 +2,96 @@ package eu.lensai.flutter_mozilla_components.api import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature import eu.lensai.flutter_mozilla_components.feature.ResultConsumer -import eu.lensai.flutter_mozilla_components.pigeons.Cookie -import eu.lensai.flutter_mozilla_components.pigeons.CookiePartitionKey -import eu.lensai.flutter_mozilla_components.pigeons.CookieSameSiteStatus -import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi +import eu.lensai.flutter_mozilla_components.pigeons.* import org.json.JSONObject class GeckoCookieApiImpl : GeckoCookieApi { - private fun toValueOrNull(json: JSONObject, key: String): Any? { - if (!json.has(key)) { - throw RuntimeException("Invalid map key") + private companion object { + const val ERROR_INVALID_KEY = "Invalid map key" + } + + private fun JSONObject.putNullable(key: String, value: Any?) { + put(key, value ?: JSONObject.NULL) + } + + private fun JSONObject.getValueOrNull(key: String): Any? { + if (!has(key)) throw RuntimeException(ERROR_INVALID_KEY) + return if (!isNull(key)) get(key) else null + } + + private fun CookiePartitionKey.toJSON() = JSONObject().apply { + put("topLevelSite", topLevelSite) + } + + private fun cookiePartitionKeyFromJSON(json: JSONObject) = CookiePartitionKey( + topLevelSite = json.getString("topLevelSite") + ) + + private fun cookieFromJSON(json: JSONObject): Cookie { + val partitionKeyJson = json.getValueOrNull("partitionKey") as JSONObject? + val partitionKey = partitionKeyJson?.takeUnless { it.length() == 0 }?.let { + cookiePartitionKeyFromJSON(it) } - if (!json.isNull(key)) { - return json.get(key) - } - - return null - } - - private fun cookiePartitionKeyFromJSON(inputJSON: JSONObject): CookiePartitionKey { - return CookiePartitionKey( - topLevelSite = inputJSON.getString("topLevelSite") - ) - } - - private fun CookiePartitionKey.toJSON(): JSONObject { - val json = JSONObject() - json.put("topLevelSite", topLevelSite) - return json - } - - private fun cookieFromJSON(inputJSON: JSONObject): Cookie { - val partitionKeyRaw = toValueOrNull(inputJSON, "partitionKey") as JSONObject? - val partitionKey = if (partitionKeyRaw == null || partitionKeyRaw.length() == 0) null - else cookiePartitionKeyFromJSON(partitionKeyRaw) - - return Cookie ( - domain = inputJSON.getString("domain"), - expirationDate = (toValueOrNull(inputJSON, "expirationDate") as Int?)?.toLong(), - firstPartyDomain = inputJSON.getString("firstPartyDomain"), - hostOnly = inputJSON.getBoolean("hostOnly"), - httpOnly = inputJSON.getBoolean("httpOnly"), - name = inputJSON.getString("name"), + return Cookie( + domain = json.getString("domain"), + expirationDate = (json.getValueOrNull("expirationDate") as Int?)?.toLong(), + firstPartyDomain = json.getString("firstPartyDomain"), + hostOnly = json.getBoolean("hostOnly"), + httpOnly = json.getBoolean("httpOnly"), + name = json.getString("name"), partitionKey = partitionKey, - path = inputJSON.getString("path"), - secure = inputJSON.getBoolean("secure"), - session = inputJSON.getBoolean("session"), - sameSite = when(inputJSON.getString("sameSite")) { - "no_restriction" -> CookieSameSiteStatus.NO_RESTRICTION - "lax" -> CookieSameSiteStatus.LAX - "strict" -> CookieSameSiteStatus.STRICT - else -> CookieSameSiteStatus.UNSPECIFIED - }, - storeId = inputJSON.getString("storeId"), - value = inputJSON.getString("value") + path = json.getString("path"), + secure = json.getBoolean("secure"), + session = json.getBoolean("session"), + sameSite = parseSameSiteStatus(json.getString("sameSite")), + storeId = json.getString("storeId"), + value = json.getString("value") ) } + private fun parseSameSiteStatus(status: String) = when(status) { + "no_restriction" -> CookieSameSiteStatus.NO_RESTRICTION + "lax" -> CookieSameSiteStatus.LAX + "strict" -> CookieSameSiteStatus.STRICT + else -> CookieSameSiteStatus.UNSPECIFIED + } + + private fun sameSiteToString(status: CookieSameSiteStatus) = when(status) { + CookieSameSiteStatus.NO_RESTRICTION -> "no_restriction" + CookieSameSiteStatus.LAX -> "lax" + CookieSameSiteStatus.STRICT -> "strict" + CookieSameSiteStatus.UNSPECIFIED -> "" + } + + private fun createBaseArgs( + firstPartyDomain: String?, + partitionKey: CookiePartitionKey?, + storeId: String?, + url: String + ) = JSONObject().apply { + putNullable("firstPartyDomain", firstPartyDomain) + putNullable("partitionKey", partitionKey?.toJSON()) + putNullable("storeId", storeId) + put("url", url) + } + + private fun handleRequest( + action: String, + args: JSONObject, + callback: (Result) -> Unit + ) { + CookieManagerFeature.scheduleRequest(action, args, object : ResultConsumer { + override fun success(result: JSONObject) { + callback(Result.success(Unit)) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) + } + }) + } + override fun getCookie( firstPartyDomain: String?, name: String, @@ -68,30 +100,11 @@ class GeckoCookieApiImpl : GeckoCookieApi { url: String, callback: (Result) -> Unit ) { - val args = JSONObject() - - if (firstPartyDomain == null) { - args.put("firstPartyDomain", JSONObject.NULL) - } else { - args.put("firstPartyDomain", firstPartyDomain) - } - args.put("name", name) - - if (partitionKey == null) { - args.put("partitionKey", JSONObject.NULL) - } else { - args.put("partitionKey", partitionKey.toJSON()) + val args = createBaseArgs(firstPartyDomain, partitionKey, storeId, url).apply { + put("name", name) } - if (storeId == null) { - args.put("storeId", JSONObject.NULL) - } else { - args.put("storeId", storeId) - } - - args.put("url", url) - - CookieManagerFeature.scheduleRequest("get", args, object: ResultConsumer { + CookieManagerFeature.scheduleRequest("get", args, object : ResultConsumer { override fun success(result: JSONObject) { callback(Result.success(cookieFromJSON(result.getJSONObject("result")))) } @@ -99,7 +112,6 @@ class GeckoCookieApiImpl : GeckoCookieApi { override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) } - }) } @@ -112,53 +124,22 @@ class GeckoCookieApiImpl : GeckoCookieApi { url: String, callback: (Result>) -> Unit ) { - val args = JSONObject() - - if (domain == null) { - args.put("domain", JSONObject.NULL) - } else { - args.put("domain", domain) + val args = createBaseArgs(firstPartyDomain, partitionKey, storeId, url).apply { + putNullable("domain", domain) + putNullable("name", name) } - if (firstPartyDomain == null) { - args.put("firstPartyDomain", JSONObject.NULL) - } else { - args.put("firstPartyDomain", firstPartyDomain) - } - - args.put("name", name) - - if (partitionKey == null) { - args.put("partitionKey", JSONObject.NULL) - } else { - args.put("partitionKey", partitionKey.toJSON()) - } - - if (storeId == null) { - args.put("storeId", JSONObject.NULL) - } else { - args.put("storeId", storeId) - } - - args.put("url", url) - - CookieManagerFeature.scheduleRequest("getAll", args, object: ResultConsumer { + CookieManagerFeature.scheduleRequest("getAll", args, object : ResultConsumer { override fun success(result: JSONObject) { - val jsonArray = result.getJSONArray("result") - val cookies: MutableList = mutableListOf() - - repeat(jsonArray.length()) { - index -> - cookies.add(cookieFromJSON(jsonArray.getJSONObject(index))) + val cookies = result.getJSONArray("result").let { jsonArray -> + List(jsonArray.length()) { cookieFromJSON(jsonArray.getJSONObject(it)) } } - callback(Result.success(cookies)) } override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) } - }) } @@ -177,90 +158,18 @@ class GeckoCookieApiImpl : GeckoCookieApi { value: String?, callback: (Result) -> Unit ) { - val args = JSONObject() - - if (domain == null) { - args.put("domain", JSONObject.NULL) - } else { - args.put("domain", domain) + val args = createBaseArgs(firstPartyDomain, partitionKey, storeId, url).apply { + putNullable("domain", domain) + putNullable("expirationDate", expirationDate) + putNullable("httpOnly", httpOnly) + putNullable("name", name) + putNullable("path", path) + putNullable("sameSite", sameSite?.let { sameSiteToString(it) }) + putNullable("secure", secure) + putNullable("value", value) } - if (expirationDate == null) { - args.put("expirationDate", JSONObject.NULL) - } else { - args.put("expirationDate", domain) - } - - if (firstPartyDomain == null) { - args.put("firstPartyDomain", JSONObject.NULL) - } else { - args.put("firstPartyDomain", firstPartyDomain) - } - - if (httpOnly == null) { - args.put("httpOnly", JSONObject.NULL) - } else { - args.put("httpOnly", httpOnly) - } - - if (name == null) { - args.put("name", JSONObject.NULL) - } else { - args.put("name", name) - } - - if (partitionKey == null) { - args.put("partitionKey", JSONObject.NULL) - } else { - args.put("partitionKey", partitionKey.toJSON()) - } - - if (path == null) { - args.put("path", JSONObject.NULL) - } else { - args.put("path", path) - } - - if (sameSite == null) { - args.put("sameSite", JSONObject.NULL) - } else { - args.put("sameSite", when(sameSite) { - CookieSameSiteStatus.NO_RESTRICTION -> "no_restriction" - CookieSameSiteStatus.LAX -> "lax" - CookieSameSiteStatus.STRICT -> "strict" - CookieSameSiteStatus.UNSPECIFIED -> "" - }) - } - - if (secure == null) { - args.put("secure", JSONObject.NULL) - } else { - args.put("secure", secure) - } - - if (storeId == null) { - args.put("storeId", JSONObject.NULL) - } else { - args.put("storeId", storeId) - } - - args.put("url", url) - - if (value == null) { - args.put("value", JSONObject.NULL) - } else { - args.put("value", value) - } - - CookieManagerFeature.scheduleRequest("set", args, object: ResultConsumer { - override fun success(result: JSONObject) { - callback(Result.success(Unit)) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) - } - }) + handleRequest("set", args, callback) } override fun removeCookie( @@ -271,39 +180,10 @@ class GeckoCookieApiImpl : GeckoCookieApi { url: String, callback: (Result) -> Unit ) { - val args = JSONObject() - - if (firstPartyDomain == null) { - args.put("firstPartyDomain", JSONObject.NULL) - } else { - args.put("firstPartyDomain", firstPartyDomain) + val args = createBaseArgs(firstPartyDomain, partitionKey, storeId, url).apply { + put("name", name) } - args.put("name", name) - - if (partitionKey == null) { - args.put("partitionKey", JSONObject.NULL) - } else { - args.put("partitionKey", partitionKey.toJSON()) - } - - if (storeId == null) { - args.put("storeId", JSONObject.NULL) - } else { - args.put("storeId", storeId) - } - - args.put("url", url) - - CookieManagerFeature.scheduleRequest("remove", args, object: ResultConsumer { - override fun success(result: JSONObject) { - callback(Result.success(Unit)) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) - } - }) + handleRequest("remove", args, callback) } - -} \ No newline at end of file +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt index 59a4b212..1185780a 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt @@ -3,11 +3,27 @@ package eu.lensai.flutter_mozilla_components.api import eu.lensai.flutter_mozilla_components.GlobalComponents import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi import mozilla.components.concept.engine.Engine +import mozilla.components.feature.addons.logger +/** + * Implementation of GeckoEngineSettingsApi that manages engine-specific settings + */ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { - private val engine: Engine by lazy { GlobalComponents.components!!.engine } + companion object { + private const val TAG = "GeckoEngineSettingsApi" + } + + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } override fun javaScriptEnabled(state: Boolean) { - engine.settings.javascriptEnabled = state + try { + components.core.engine.settings.javascriptEnabled = state + logger.debug("$TAG: JavaScript enabled state changed to: $state") + } catch (e: Exception) { + logger.error("$TAG: Failed to set JavaScript enabled state", e) + throw IllegalStateException("Failed to set JavaScript enabled state", e) + } } -} \ No newline at end of file +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoFindApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoFindApiImpl.kt index 7994a40b..36afe55f 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoFindApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoFindApiImpl.kt @@ -7,17 +7,19 @@ import mozilla.components.browser.state.state.BrowserState import mozilla.components.concept.engine.EngineSession class GeckoFindApiImpl : GeckoFindApi { - private val state: BrowserState by lazy { GlobalComponents.components!!.store.state } + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } private fun sessionByTabId(tabId: String?) : EngineSession? { val loadSessionId = tabId - ?: state.selectedTabId + ?: components.core.store.state.selectedTabId if (loadSessionId == null) { return null } - val tab = state.findTabOrCustomTab(loadSessionId) + val tab = components.core.store.state.findTabOrCustomTab(loadSessionId) return tab?.engineState?.engineSession } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoIconsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoIconsApiImpl.kt index 467e13ce..22d61e48 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoIconsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoIconsApiImpl.kt @@ -7,47 +7,63 @@ import kotlinx.coroutines.* import mozilla.components.browser.icons.BrowserIcons import mozilla.components.browser.icons.Icon import mozilla.components.concept.engine.manifest.Size as HtmlSize +import mozilla.components.feature.addons.logger -typealias MozillaIconRequest = mozilla.components.browser.icons.IconRequest -typealias MozillaIconSize = mozilla.components.browser.icons.IconRequest.Size -typealias MozillaIconResource = mozilla.components.browser.icons.IconRequest.Resource -typealias MozillaIconResourceType = mozilla.components.browser.icons.IconRequest.Resource.Type +private typealias MozillaIconRequest = mozilla.components.browser.icons.IconRequest +private typealias MozillaIconSize = mozilla.components.browser.icons.IconRequest.Size +private typealias MozillaIconResource = mozilla.components.browser.icons.IconRequest.Resource +private typealias MozillaIconResourceType = mozilla.components.browser.icons.IconRequest.Resource.Type -class GeckoIconsApiImpl() : GeckoIconsApi { - private val icons: BrowserIcons by lazy { GlobalComponents.components!!.icons } +/** + * Implementation of GeckoIconsApi that handles icon loading and processing + */ +class GeckoIconsApiImpl : GeckoIconsApi { + companion object { + private const val TAG = "GeckoIconsApi" + private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + } + + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } override fun loadIcon(request: IconRequest, callback: (Result) -> Unit) { - CoroutineScope(Dispatchers.Default).launch { - runCatching { - loadIconAsync(request.toMozillaIconRequest()) - }.fold( - onSuccess = { result -> - withContext(Dispatchers.Main) { - callback(Result.success(result)) - } - }, - onFailure = { error -> - withContext(Dispatchers.Main) { - callback(Result.failure(error)) - } + coroutineScope.launch { + try { + val mozillaRequest = request.toMozillaIconRequest() + logger.debug("$TAG: Loading icon for URL: ${request.url}") + + val result = loadIconAsync(mozillaRequest) + withContext(Dispatchers.Main) { + callback(Result.success(result)) } - ) + } catch (e: Exception) { + logger.error("$TAG: Failed to load icon", e) + withContext(Dispatchers.Main) { + callback(Result.failure(e)) + } + } } } private suspend fun loadIconAsync(request: MozillaIconRequest): IconResult { - val result = icons.loadIcon(request).await() - val imageBytes = result.bitmap.toWebPBytes() - return IconResult( - image = imageBytes, - maskable = result.maskable, - color = result.color?.toLong(), - source = result.source.toApiSource() - ) + return try { + val result = components.core.icons.loadIcon(request).await() + val imageBytes = result.bitmap.toWebPBytes() + IconResult( + image = imageBytes, + maskable = result.maskable, + color = result.color?.toLong(), + source = result.source.toApiSource() + ) + } catch (e: Exception) { + logger.error("$TAG: Error in loadIconAsync", e) + throw e + } } - private fun IconRequest.toMozillaIconRequest(): MozillaIconRequest = - MozillaIconRequest( + private fun IconRequest.toMozillaIconRequest(): MozillaIconRequest { + return MozillaIconRequest( url = url, size = size.toMozillaSize(), color = color?.toInt(), @@ -55,6 +71,7 @@ class GeckoIconsApiImpl() : GeckoIconsApi { isPrivate = isPrivate, resources = resources.filterNotNull().map { it.toMozillaResource() } ) + } private fun IconSize.toMozillaSize(): MozillaIconSize = when (this) { IconSize.DEFAULT_SIZE -> MozillaIconSize.DEFAULT @@ -62,14 +79,17 @@ class GeckoIconsApiImpl() : GeckoIconsApi { IconSize.LAUNCHER_ADAPTIVE -> MozillaIconSize.LAUNCHER_ADAPTIVE } - private fun Resource.toMozillaResource(): MozillaIconResource = - MozillaIconResource( + private fun Resource.toMozillaResource(): MozillaIconResource { + return MozillaIconResource( url = url, mimeType = mimeType, maskable = maskable, type = type.toMozillaType(), - sizes = sizes.filterNotNull().map { HtmlSize(it.height.toInt(), it.width.toInt()) } + sizes = sizes.filterNotNull().map { + HtmlSize(it.height.toInt(), it.width.toInt()) + } ) + } private fun IconType.toMozillaType(): MozillaIconResourceType = when (this) { IconType.FAVICON -> MozillaIconResourceType.FAVICON diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSelectionActionControllerImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSelectionActionControllerImpl.kt index adf7770f..f093e6e5 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSelectionActionControllerImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSelectionActionControllerImpl.kt @@ -3,11 +3,37 @@ package eu.lensai.flutter_mozilla_components.api import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate import eu.lensai.flutter_mozilla_components.pigeons.CustomSelectionAction import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionController +import mozilla.components.feature.addons.logger +/** + * Implementation of GeckoSelectionActionController that manages custom text selection actions + * @property selectionActionDelegate Delegate handling selection actions + */ class GeckoSelectionActionControllerImpl( private val selectionActionDelegate: DefaultSelectionActionDelegate ) : GeckoSelectionActionController { - override fun setActions(actions: List) { - selectionActionDelegate.actions = actions.associateBy { it.id } + + companion object { + private const val TAG = "GeckoSelectionActionController" } -} \ No newline at end of file + + /** + * Sets the available custom selection actions + * @param actions List of custom selection actions to be registered + */ + override fun setActions(actions: List) { + try { + require(actions.distinctBy { it.id }.size == actions.size) { + "Duplicate action IDs found in selection actions" + } + + logger.debug("$TAG: Setting ${actions.size} custom selection actions") + selectionActionDelegate.actions = actions.associateBy { it.id }.also { actionMap -> + logger.debug("$TAG: Registered actions: ${actionMap.keys.joinToString()}") + } + } catch (e: Exception) { + logger.error("$TAG: Failed to set selection actions", e) + throw IllegalArgumentException("Failed to set selection actions", e) + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSessionApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSessionApiImpl.kt index c521ee00..a47000dd 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSessionApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSessionApiImpl.kt @@ -1,36 +1,36 @@ package eu.lensai.flutter_mozilla_components.api -import android.util.Log import eu.lensai.flutter_mozilla_components.GlobalComponents import eu.lensai.flutter_mozilla_components.ext.toWebPBytes -import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi -import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents -import eu.lensai.flutter_mozilla_components.pigeons.LoadUrlFlagsValue -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import eu.lensai.flutter_mozilla_components.pigeons.* +import kotlinx.coroutines.* import mozilla.components.browser.state.action.ContentAction -import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.selectedTab -import eu.lensai.flutter_mozilla_components.pigeons.TranslationOptions as PigeonTranslationOptions import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.thumbnails.storage.ThumbnailStorage -import mozilla.components.concept.base.images.ImageLoadRequest import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.translate.TranslationOptions import mozilla.components.feature.session.SessionUseCases -import org.mozilla.gecko.util.ThreadUtils.runOnUiThread +import mozilla.components.feature.addons.logger +import eu.lensai.flutter_mozilla_components.pigeons.TranslationOptions as PigeonTranslationOptions +/** + * Implementation of GeckoSessionApi that manages browser session operations + */ class GeckoSessionApiImpl : GeckoSessionApi { - private val sessionUseCases: SessionUseCases by lazy { GlobalComponents.components!!.sessionUseCases } - private val store: BrowserStore by lazy { GlobalComponents.components!!.store } - private val state: BrowserState by lazy { GlobalComponents.components!!.store.state } - private val thumbnailStorage: ThumbnailStorage by lazy { GlobalComponents.components!!.thumbnailStorage } - private val events: GeckoStateEvents by lazy { GlobalComponents.components!!.flutterEvents } - private val engineView: EngineView? by lazy { GlobalComponents.components!!.engineView } + companion object { + private const val TAG = "GeckoSessionApi" + private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + } + + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + private fun getTabId(tabId: String?) = tabId ?: components.core.store.state.selectedTabId + ?: throw IllegalStateException("No tab ID provided and no selected tab") override fun loadUrl( tabId: String?, @@ -38,72 +38,138 @@ class GeckoSessionApiImpl : GeckoSessionApi { flags: LoadUrlFlagsValue, additionalHeaders: Map? ) { - sessionUseCases.loadUrl( - url = url, - sessionId = tabId, - flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()), - additionalHeaders = additionalHeaders - ) + try { + logger.debug("$TAG: Loading URL: $url for tab: $tabId") + components.useCases.sessionUseCases.loadUrl( + url = url, + sessionId = getTabId(tabId), + flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()), + additionalHeaders = additionalHeaders + ) + } catch (e: Exception) { + logger.error("$TAG: Failed to load URL", e) + throw e + } } override fun loadData(tabId: String?, data: String, mimeType: String, encoding: String) { - sessionUseCases.loadData( - data = data, - tabId = tabId ?: state.selectedTabId, - mimeType = mimeType, - encoding = encoding - ) + try { + logger.debug("$TAG: Loading data with mimeType: $mimeType for tab: $tabId") + components.useCases.sessionUseCases.loadData( + data = data, + tabId = getTabId(tabId), + mimeType = mimeType, + encoding = encoding + ) + } catch (e: Exception) { + logger.error("$TAG: Failed to load data", e) + throw e + } } override fun reload(tabId: String?, flags: LoadUrlFlagsValue) { - sessionUseCases.reload( - tabId = tabId ?: state.selectedTabId, - flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()) - ) + try { + logger.debug("$TAG: Reloading tab: $tabId") + components.useCases.sessionUseCases.reload( + tabId = getTabId(tabId), + flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()) + ) + } catch (e: Exception) { + logger.error("$TAG: Failed to reload", e) + throw e + } } override fun stopLoading(tabId: String?) { - sessionUseCases.stopLoading(tabId = tabId ?: state.selectedTabId) + try { + logger.debug("$TAG: Stopping loading for tab: $tabId") + components.useCases.sessionUseCases.stopLoading(tabId = getTabId(tabId)) + } catch (e: Exception) { + logger.error("$TAG: Failed to stop loading", e) + throw e + } } override fun goBack(tabId: String?, userInteraction: Boolean) { - sessionUseCases.goBack( - tabId = tabId ?: state.selectedTabId, - userInteraction = userInteraction - ) + try { + logger.debug("$TAG: Going back in tab: $tabId") + components.useCases.sessionUseCases.goBack( + tabId = getTabId(tabId), + userInteraction = userInteraction + ) + } catch (e: Exception) { + logger.error("$TAG: Failed to go back", e) + throw e + } } override fun goForward(tabId: String?, userInteraction: Boolean) { - sessionUseCases.goForward( - tabId = tabId ?: state.selectedTabId, - userInteraction = userInteraction - ) + try { + logger.debug("$TAG: Going forward in tab: $tabId") + components.useCases.sessionUseCases.goForward( + tabId = getTabId(tabId), + userInteraction = userInteraction + ) + } catch (e: Exception) { + logger.error("$TAG: Failed to go forward", e) + throw e + } } override fun goToHistoryIndex(index: Long, tabId: String?) { - sessionUseCases.goToHistoryIndex( - tabId = tabId ?: state.selectedTabId, - index = index.toInt() - ) + try { + logger.debug("$TAG: Going to history index $index in tab: $tabId") + components.useCases.sessionUseCases.goToHistoryIndex( + tabId = getTabId(tabId), + index = index.toInt() + ) + } catch (e: Exception) { + logger.error("$TAG: Failed to go to history index", e) + throw e + } } override fun requestDesktopSite(tabId: String?, enable: Boolean) { - sessionUseCases.requestDesktopSite( - tabId = tabId ?: state.selectedTabId, - enable = enable - ) + try { + logger.debug("$TAG: Setting desktop site to $enable for tab: $tabId") + components.useCases.sessionUseCases.requestDesktopSite( + tabId = getTabId(tabId), + enable = enable + ) + } catch (e: Exception) { + logger.error("$TAG: Failed to request desktop site", e) + throw e + } } override fun exitFullscreen(tabId: String?) { - sessionUseCases.exitFullscreen(tabId = tabId ?: state.selectedTabId) + try { + logger.debug("$TAG: Exiting fullscreen for tab: $tabId") + components.useCases.sessionUseCases.exitFullscreen(tabId = getTabId(tabId)) + } catch (e: Exception) { + logger.error("$TAG: Failed to exit fullscreen", e) + throw e + } } override fun saveToPdf(tabId: String?) { - sessionUseCases.saveToPdf(tabId = tabId ?: state.selectedTabId) + try { + logger.debug("$TAG: Saving to PDF for tab: $tabId") + components.useCases.sessionUseCases.saveToPdf(tabId = getTabId(tabId)) + } catch (e: Exception) { + logger.error("$TAG: Failed to save to PDF", e) + throw e + } } override fun printContent(tabId: String?) { - sessionUseCases.printContent(tabId = tabId ?: state.selectedTabId) + try { + logger.debug("$TAG: Printing content for tab: $tabId") + components.useCases.sessionUseCases.printContent(tabId = getTabId(tabId)) + } catch (e: Exception) { + logger.error("$TAG: Failed to print content", e) + throw e + } } override fun translate( @@ -112,51 +178,103 @@ class GeckoSessionApiImpl : GeckoSessionApi { toLanguage: String, options: PigeonTranslationOptions? ) { - sessionUseCases.translate( - tabId = tabId ?: state.selectedTabId, - fromLanguage = fromLanguage, - toLanguage = toLanguage, - options = options?.let { TranslationOptions(downloadModel = it.downloadModel) } - ) + try { + logger.debug("$TAG: Translating from $fromLanguage to $toLanguage for tab: $tabId") + components.useCases.sessionUseCases.translate( + tabId = getTabId(tabId), + fromLanguage = fromLanguage, + toLanguage = toLanguage, + options = options?.let { TranslationOptions(downloadModel = it.downloadModel) } + ) + } catch (e: Exception) { + logger.error("$TAG: Failed to translate", e) + throw e + } } override fun translateRestore(tabId: String?) { - sessionUseCases.translateRestore(tabId = tabId ?: state.selectedTabId) + try { + logger.debug("$TAG: Restoring translation for tab: $tabId") + components.useCases.sessionUseCases.translateRestore(tabId = getTabId(tabId)) + } catch (e: Exception) { + logger.error("$TAG: Failed to restore translation", e) + throw e + } } override fun crashRecovery(tabIds: List?) { - if (tabIds != null) { - sessionUseCases.crashRecovery.invoke(tabIds = tabIds) - } else { - sessionUseCases.crashRecovery.invoke() + try { + logger.debug("$TAG: Performing crash recovery for tabs: $tabIds") + if (tabIds != null) { + components.useCases.sessionUseCases.crashRecovery.invoke(tabIds = tabIds) + } else { + components.useCases.sessionUseCases.crashRecovery.invoke() + } + } catch (e: Exception) { + logger.error("$TAG: Failed to perform crash recovery", e) + throw e } } override fun purgeHistory() { - sessionUseCases.purgeHistory() + try { + logger.debug("$TAG: Purging history") + components.useCases.sessionUseCases.purgeHistory() + } catch (e: Exception) { + logger.error("$TAG: Failed to purge history", e) + throw e + } } override fun updateLastAccess(tabId: String?, lastAccess: Long?) { - sessionUseCases.updateLastAccess( - tabId = tabId ?: state.selectedTabId, - lastAccess = lastAccess ?: System.currentTimeMillis() - ) + try { + val timestamp = lastAccess ?: System.currentTimeMillis() + logger.debug("$TAG: Updating last access time to $timestamp for tab: $tabId") + components.useCases.sessionUseCases.updateLastAccess( + tabId = getTabId(tabId), + lastAccess = timestamp + ) + } catch (e: Exception) { + logger.error("$TAG: Failed to update last access", e) + throw e + } } - override fun requestScreenshot(callback: (Result) -> Unit) { - val tab = state.selectedTab - if (tab != null) { - engineView?.captureThumbnail { bitmap -> - if (bitmap != null) { - store.dispatch(ContentAction.UpdateThumbnailAction(tab.id, bitmap)) - val compressed = bitmap.toWebPBytes() - callback(Result.success(compressed)) - } else { - callback(Result.success(null)) - } + override fun requestScreenshot(sendBack: Boolean, callback: (Result) -> Unit) { + try { + val tab = components.core.store.state.selectedTab + if (tab == null) { + logger.warn("$TAG: No selected tab for screenshot") + callback(Result.failure(IllegalStateException("No selected tab for screenshot"))) + return } - } else { - callback(Result.failure(Exception("No selected tab for screenshot"))) + + components.engineView?.captureThumbnail { bitmap -> + try { + if (bitmap != null) { + components.core.store.dispatch(ContentAction.UpdateThumbnailAction(tab.id, bitmap)) + if (sendBack) { + val compressed = bitmap.toWebPBytes() + logger.debug("$TAG: Screenshot captured successfully") + callback(Result.success(compressed)) + } else { + callback(Result.success(null)) + } + } else { + logger.warn("$TAG: Failed to capture screenshot - null bitmap") + callback(Result.success(null)) + } + } catch (e: Exception) { + logger.error("$TAG: Failed to process screenshot", e) + callback(Result.failure(e)) + } + } ?: run { + logger.warn("$TAG: No engine view available for screenshot") + callback(Result.failure(IllegalStateException("No engine view available"))) + } + } catch (e: Exception) { + logger.error("$TAG: Failed to request screenshot", e) + callback(Result.failure(e)) } } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt index 89b8420f..5177919d 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt @@ -37,80 +37,126 @@ import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.TabState import mozilla.components.browser.thumbnails.storage.ThumbnailStorage import mozilla.components.concept.base.images.ImageLoadRequest -import mozilla.components.concept.engine.Engine -import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.storage.HistoryMetadataKey import mozilla.components.feature.tabs.TabsUseCases import org.json.JSONObject import org.mozilla.gecko.util.ThreadUtils.runOnUiThread +import mozilla.components.feature.addons.logger +import mozilla.components.concept.engine.EngineSession -class GeckoTabsApiImpl() : GeckoTabsApi { - private val tabsUseCases: TabsUseCases by lazy { GlobalComponents.components!!.tabsUseCases } - private val engine: Engine by lazy { GlobalComponents.components!!.engine } - private val state: BrowserState by lazy { GlobalComponents.components!!.store.state } - private val thumbnailStorage: ThumbnailStorage by lazy { GlobalComponents.components!!.thumbnailStorage } - private val icons: BrowserIcons by lazy { GlobalComponents.components!!.icons } - private val events: GeckoStateEvents by lazy { GlobalComponents.components!!.flutterEvents } +class GeckoTabsApiImpl : GeckoTabsApi { + companion object { + private const val TAG = "GeckoTabsApi" + private val coroutineScope = CoroutineScope(Dispatchers.Default) + } - private fun restoreSource(source: SourceValue ) : SessionState.Source { + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + private fun restoreSource(source: SourceValue): SessionState.Source { return SessionState.Source.restore( source.id.toInt(), source.caller?.packageId, source.caller?.category?.value?.toInt() ) } - private fun mapTab(t: PigeonRecoverableTab) : RecoverableTab { + + private fun mapTab(tab: PigeonRecoverableTab): RecoverableTab { return RecoverableTab( - engineSessionState = if(t.engineSessionStateJson != null) - engine.createSessionState(JSONObject(t.engineSessionStateJson)) - else - null, + engineSessionState = tab.engineSessionStateJson?.let { json -> + components.core.engine.createSessionState(JSONObject(json)) + }, state = TabState( - id = t.state.id, - url = t.state.url, - parentId = t.state.parentId, - title = t.state.title, - searchTerm = t.state.searchTerm, - contextId = t.state.contextId, + id = tab.state.id, + url = tab.state.url, + parentId = tab.state.parentId, + title = tab.state.title, + searchTerm = tab.state.searchTerm, + contextId = tab.state.contextId, readerState = ReaderState( - readerable = t.state.readerState.readerable, - active = t.state.readerState.active, - checkRequired = t.state.readerState.checkRequired, - connectRequired = t.state.readerState.connectRequired, - baseUrl = t.state.readerState.baseUrl, - activeUrl = t.state.readerState.activeUrl, - scrollY = t.state.readerState.scrollY?.toInt(), + readerable = tab.state.readerState.readerable, + active = tab.state.readerState.active, + checkRequired = tab.state.readerState.checkRequired, + connectRequired = tab.state.readerState.connectRequired, + baseUrl = tab.state.readerState.baseUrl, + activeUrl = tab.state.readerState.activeUrl, + scrollY = tab.state.readerState.scrollY?.toInt() ), - lastAccess = t.state.lastAccess, - createdAt = t.state.createdAt, + lastAccess = tab.state.lastAccess, + createdAt = tab.state.createdAt, lastMediaAccessState = LastMediaAccessState( - lastMediaUrl = t.state.lastMediaAccessState.lastMediaUrl, - lastMediaAccess = t.state.lastMediaAccessState.lastMediaAccess, - mediaSessionActive = t.state.lastMediaAccessState.mediaSessionActive + lastMediaUrl = tab.state.lastMediaAccessState.lastMediaUrl, + lastMediaAccess = tab.state.lastMediaAccessState.lastMediaAccess, + mediaSessionActive = tab.state.lastMediaAccessState.mediaSessionActive ), - private = t.state.private, - historyMetadata = if(t.state.historyMetadata != null) + private = tab.state.private, + historyMetadata = tab.state.historyMetadata?.let { metadata -> HistoryMetadataKey( - url = t.state.historyMetadata.url, - searchTerm = t.state.historyMetadata.searchTerm, - referrerUrl = t.state.historyMetadata.referrerUrl + url = metadata.url, + searchTerm = metadata.searchTerm, + referrerUrl = metadata.referrerUrl ) - else null, - source = restoreSource(t.state.source), - index = t.state.index.toInt(), - hasFormData = t.state.hasFormData, + }, + source = restoreSource(tab.state.source), + index = tab.state.index.toInt(), + hasFormData = tab.state.hasFormData ) ) } - private fun mapRestoreLocation(t: PigeonRestoreLocation) : TabListAction.RestoreAction.RestoreLocation { - return when(t) { + private fun mapRestoreLocation(location: PigeonRestoreLocation): TabListAction.RestoreAction.RestoreLocation { + return when(location) { RestoreLocation.BEGINNING -> TabListAction.RestoreAction.RestoreLocation.BEGINNING RestoreLocation.END -> TabListAction.RestoreAction.RestoreLocation.END RestoreLocation.AT_INDEX -> TabListAction.RestoreAction.RestoreLocation.AT_INDEX } } + private suspend fun handleIconChange(tab: SessionState) { + try { + if (tab.content.icon != null) { + val iconBytes = tab.content.icon?.toWebPBytes() + withContext(Dispatchers.Main) { + components.flutterEvents.onIconChange( + System.currentTimeMillis(), + tab.id, + iconBytes + ) { } + } + } else { + val result = components.core.icons.loadIcon(IconRequest(url = tab.content.url)).await() + val iconBytes = result.bitmap.toWebPBytes() + withContext(Dispatchers.Main) { + components.flutterEvents.onIconChange(System.currentTimeMillis(), tab.id, iconBytes) { } + } + } + } catch (e: Exception) { + logger.error("$TAG: Failed to handle icon change for tab ${tab.id}", e) + } + } + + private suspend fun handleThumbnailChange(tab: SessionState) { + try { + val bitmap = components.core.thumbnailStorage.loadThumbnail( + ImageLoadRequest( + id = tab.id, + size = 1024, + isPrivate = tab.content.private + ) + ).await() + + bitmap?.let { + val bytes = it.toWebPBytes() + withContext(Dispatchers.Main) { + components.flutterEvents.onThumbnailChange(System.currentTimeMillis(), tab.id, bytes) { } + } + } + } catch (e: Exception) { + logger.error("$TAG: Failed to handle thumbnail change for tab ${tab.id}", e) + } + } + override fun syncEvents( onSelectedTabChange: Boolean, onTabListChange: Boolean, @@ -122,155 +168,118 @@ class GeckoTabsApiImpl() : GeckoTabsApi { onFindResults: Boolean, onThumbnailChange: Boolean, ) { - val tabs = state.tabs.map { x -> x.copy() }.toList() - val selectedTab = state.selectedTabId + try { + val tabs = components.core.store.state.tabs.map { it.copy() } + val selectedTab = components.core.store.state.selectedTabId - if(onSelectedTabChange) { - events.onSelectedTabChange( - System.currentTimeMillis(), - selectedTab - ) { _ -> } - } - - if(onTabListChange) { - events.onTabListChange( - System.currentTimeMillis(), - tabs.map {tab -> tab.id} - ) { _ -> } - } - - if(onTabContentStateChange) { - tabs.forEach { tab -> - events.onTabContentStateChange( - System.currentTimeMillis(), - TabContentState( - id = tab.id, - contextId = tab.contextId, - url = tab.content.url, - title = tab.content.title, - progress = tab.content.progress.toLong(), - isPrivate = tab.content.private, - isFullScreen = tab.content.fullScreen, - isLoading = tab.content.loading - ) - ) { _ -> } + if (onSelectedTabChange) { + components.flutterEvents.onSelectedTabChange(System.currentTimeMillis(), selectedTab) { } + } + + if (onTabListChange) { + components.flutterEvents.onTabListChange(System.currentTimeMillis(), tabs.map { it.id }) { } } - } - if(onIconChange) { tabs.forEach { tab -> - if(tab.content.icon != null) { - val iconBytes = tab.content.icon?.toWebPBytes() - events.onIconChange( + if (onTabContentStateChange) { + components.flutterEvents.onTabContentStateChange( + System.currentTimeMillis(), + TabContentState( + id = tab.id, + contextId = tab.contextId, + url = tab.content.url, + title = tab.content.title, + progress = tab.content.progress.toLong(), + isPrivate = tab.content.private, + isFullScreen = tab.content.fullScreen, + isLoading = tab.content.loading + ) + ) { } + } + + if (onIconChange) { + coroutineScope.launch { handleIconChange(tab) } + } + + if (onSecurityInfoStateChange) { + components.flutterEvents.onSecurityInfoStateChange( System.currentTimeMillis(), tab.id, - iconBytes - ) { _ -> } - } else { - CoroutineScope(Dispatchers.Default).launch { - val result = icons.loadIcon(IconRequest(url = tab.content.url)).await() - val iconBytes = result.bitmap.toWebPBytes() - runOnUiThread { - events.onIconChange( - System.currentTimeMillis(), - tab.id, - iconBytes - ) { _ -> } - } - } - - } - } - } - - if(onSecurityInfoStateChange) { - tabs.forEach { tab -> - events.onSecurityInfoStateChange( - System.currentTimeMillis(), - tab.id, - SecurityInfoState( - tab.content.securityInfo.secure, - tab.content.securityInfo.host, - tab.content.securityInfo.issuer, - ) - ) { _ -> } - } - } - - if(onReaderableStateChange) { - tabs.forEach { tab -> - events.onReaderableStateChange( - System.currentTimeMillis(), - tab.id, - ReaderableState( - tab.readerState.readerable, - tab.readerState.active, - ) - ) { _ -> } - } - } - - if(onHistoryStateChange) { - tabs.forEach { tab -> - events.onHistoryStateChange( - System.currentTimeMillis(), - tab.id, - HistoryState( - items = tab.content.history.items.map { item -> HistoryItem( - url = item.uri, - title = item.title - ) }, - currentIndex = tab.content.history.currentIndex.toLong(), - canGoBack = tab.content.canGoBack, - canGoForward = tab.content.canGoForward, - ) - ) { _ -> } - } - } - - if(onFindResults) { - tabs.forEach { tab -> - events.onFindResults( - System.currentTimeMillis(), - tab.id, - tab.content.findResults.map { result -> FindResultState( - activeMatchOrdinal = result.activeMatchOrdinal.toLong(), - numberOfMatches = result.numberOfMatches.toLong(), - isDoneCounting = result.isDoneCounting, - ) } - ) { _ -> } - } - } - - if(onThumbnailChange) { - tabs.forEach { tab -> - CoroutineScope(Dispatchers.Default).launch { - val bitmap = thumbnailStorage.loadThumbnail( - ImageLoadRequest( - id = tab.id, - //TODO: make this configurable - size = 1024, - isPrivate = tab.content.private + SecurityInfoState( + tab.content.securityInfo.secure, + tab.content.securityInfo.host, + tab.content.securityInfo.issuer ) - ).await() + ) { } + } - if(bitmap != null) { - val bytes = bitmap.toWebPBytes() - runOnUiThread { - events.onThumbnailChange(System.currentTimeMillis(), tab.id, bytes) { _ -> } + if (onReaderableStateChange) { + components.flutterEvents.onReaderableStateChange( + System.currentTimeMillis(), + tab.id, + ReaderableState( + tab.readerState.readerable, + tab.readerState.active + ) + ) { } + } + + if (onHistoryStateChange) { + components.flutterEvents.onHistoryStateChange( + System.currentTimeMillis(), + tab.id, + HistoryState( + items = tab.content.history.items.map { item -> + HistoryItem(url = item.uri, title = item.title) + }, + currentIndex = tab.content.history.currentIndex.toLong(), + canGoBack = tab.content.canGoBack, + canGoForward = tab.content.canGoForward + ) + ) { } + } + + if (onFindResults) { + components.flutterEvents.onFindResults( + System.currentTimeMillis(), + tab.id, + tab.content.findResults.map { result -> + FindResultState( + activeMatchOrdinal = result.activeMatchOrdinal.toLong(), + numberOfMatches = result.numberOfMatches.toLong(), + isDoneCounting = result.isDoneCounting + ) } - } + ) { } + } + + if (onThumbnailChange) { + coroutineScope.launch { handleThumbnailChange(tab) } } } + } catch (e: Exception) { + logger.error("$TAG: Failed to sync events", e) } } override fun selectTab(tabId: String) { - tabsUseCases.selectTab(tabId = tabId) + try { + components.useCases.tabsUseCases.selectTab(tabId = tabId) + logger.debug("$TAG: Selected tab $tabId") + } catch (e: Exception) { + logger.error("$TAG: Failed to select tab $tabId", e) + throw e + } } override fun removeTab(tabId: String) { - tabsUseCases.removeTab(tabId = tabId) + try { + components.useCases.tabsUseCases.removeTab(tabId = tabId) + logger.debug("$TAG: Removed tab $tabId") + } catch (e: Exception) { + logger.error("$TAG: Failed to remove tab $tabId", e) + throw e + } } override fun addTab( @@ -285,44 +294,81 @@ class GeckoTabsApiImpl() : GeckoTabsApi { historyMetadata: PigeonHistoryMetadataKey?, additionalHeaders: Map? ): String { - return tabsUseCases.addTab( - url = url, - selectTab = selectTab, - startLoading = startLoading, - parentId = parentId, - flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()), - contextId = contextId, - source = restoreSource(source), - private = private, - historyMetadata = if(historyMetadata != null) - HistoryMetadataKey( - url = historyMetadata.url, - searchTerm = historyMetadata.searchTerm, - referrerUrl = historyMetadata.referrerUrl - ) - else null, - additionalHeaders = additionalHeaders - ) + try { + return components.useCases.tabsUseCases.addTab( + url = url, + selectTab = selectTab, + startLoading = startLoading, + parentId = parentId, + flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()), + contextId = contextId, + source = restoreSource(source), + private = private, + historyMetadata = historyMetadata?.let { metadata -> + HistoryMetadataKey( + url = metadata.url, + searchTerm = metadata.searchTerm, + referrerUrl = metadata.referrerUrl + ) + }, + additionalHeaders = additionalHeaders + ).also { + logger.debug("$TAG: Added new tab with ID $it") + } + } catch (e: Exception) { + logger.error("$TAG: Failed to add tab", e) + throw e + } } override fun removeAllTabs(recoverable: Boolean) { - tabsUseCases.removeAllTabs(recoverable = recoverable) + try { + components.useCases.tabsUseCases.removeAllTabs(recoverable = recoverable) + logger.debug("$TAG: Removed all tabs, recoverable: $recoverable") + } catch (e: Exception) { + logger.error("$TAG: Failed to remove all tabs", e) + throw e + } } override fun removeTabs(ids: List) { - tabsUseCases.removeTabs(ids = ids) + try { + components.useCases.tabsUseCases.removeTabs(ids = ids) + logger.debug("$TAG: Removed tabs: ${ids.joinToString()}") + } catch (e: Exception) { + logger.error("$TAG: Failed to remove tabs", e) + throw e + } } override fun removeNormalTabs() { - tabsUseCases.removeNormalTabs() + try { + components.useCases.tabsUseCases.removeNormalTabs() + logger.debug("$TAG: Removed all normal tabs") + } catch (e: Exception) { + logger.error("$TAG: Failed to remove normal tabs", e) + throw e + } } override fun removePrivateTabs() { - tabsUseCases.removePrivateTabs() + try { + components.useCases.tabsUseCases.removePrivateTabs() + logger.debug("$TAG: Removed all private tabs") + } catch (e: Exception) { + logger.error("$TAG: Failed to remove private tabs", e) + throw e + } } override fun undo() { - tabsUseCases.undo() + try { + components.useCases.tabsUseCases.undo() + logger.debug("$TAG: Performed undo operation") + } catch (e: Exception) { + logger.error("$TAG: Failed to perform undo", e) + throw e + } } override fun restoreTabsByList( @@ -330,35 +376,57 @@ class GeckoTabsApiImpl() : GeckoTabsApi { selectTabId: String?, restoreLocation: PigeonRestoreLocation ) { - tabsUseCases.restore( - tabs = tabs.map { t -> mapTab(t) }, - restoreLocation = mapRestoreLocation(restoreLocation), - selectTabId = selectTabId, - ) + try { + components.useCases.tabsUseCases.restore( + tabs = tabs.map { mapTab(it) }, + restoreLocation = mapRestoreLocation(restoreLocation), + selectTabId = selectTabId + ) + logger.debug("$TAG: Restored ${tabs.size} tabs") + } catch (e: Exception) { + logger.error("$TAG: Failed to restore tabs", e) + throw e + } } override fun restoreTabsByBrowserState( state: PigeonRecoverableBrowserState, restoreLocation: PigeonRestoreLocation ) { - tabsUseCases.restore( - state = RecoverableBrowserState( - selectedTabId = state.selectedTabId, - tabs = state.tabs.filterNotNull().map { t -> mapTab(t) }, - ), - restoreLocation = mapRestoreLocation(restoreLocation), - ) + try { + components.useCases.tabsUseCases.restore( + state = RecoverableBrowserState( + selectedTabId = state.selectedTabId, + tabs = state.tabs.filterNotNull().map { mapTab(it) } + ), + restoreLocation = mapRestoreLocation(restoreLocation) + ) + logger.debug("$TAG: Restored browser state") + } catch (e: Exception) { + logger.error("$TAG: Failed to restore browser state", e) + throw e + } } - override fun selectOrAddTabByHistory(url: String, historyMetadata: PigeonHistoryMetadataKey): String { - return tabsUseCases.selectOrAddTab( - url = url, - historyMetadata = HistoryMetadataKey( - url = historyMetadata.url, - searchTerm = historyMetadata.searchTerm, - referrerUrl = historyMetadata.referrerUrl - ), - ) + override fun selectOrAddTabByHistory( + url: String, + historyMetadata: PigeonHistoryMetadataKey + ): String { + try { + return components.useCases.tabsUseCases.selectOrAddTab( + url = url, + historyMetadata = HistoryMetadataKey( + url = historyMetadata.url, + searchTerm = historyMetadata.searchTerm, + referrerUrl = historyMetadata.referrerUrl + ) + ).also { + logger.debug("$TAG: Selected or added tab by history with ID $it") + } + } catch (e: Exception) { + logger.error("$TAG: Failed to select or add tab by history", e) + throw e + } } override fun selectOrAddTabByUrl( @@ -368,36 +436,64 @@ class GeckoTabsApiImpl() : GeckoTabsApi { flags: LoadUrlFlagsValue, ignoreFragment: Boolean ): String { - return tabsUseCases.selectOrAddTab( - url = url, - private = private, - source = restoreSource(source), - flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()), - ignoreFragment = ignoreFragment, - ) + try { + return components.useCases.tabsUseCases.selectOrAddTab( + url = url, + private = private, + source = restoreSource(source), + flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()), + ignoreFragment = ignoreFragment + ).also { + logger.debug("$TAG: Selected or added tab by URL with ID $it") + } + } catch (e: Exception) { + logger.error("$TAG: Failed to select or add tab by URL", e) + throw e + } } override fun duplicateTab(selectTabId: String?, selectNewTab: Boolean): String { - val tabState = if(selectTabId != null) state.findTab(selectTabId) else null + try { + val tabState = selectTabId?.let { components.core.store.state.findTab(it) } + ?: throw IllegalArgumentException("Tab not found") - return tabsUseCases.duplicateTab( - tab = tabState!!, - selectNewTab = selectNewTab, - ) + return components.useCases.tabsUseCases.duplicateTab( + tab = tabState, + selectNewTab = selectNewTab + ).also { + logger.debug("$TAG: Duplicated tab $selectTabId to new tab $it") + } + } catch (e: Exception) { + logger.error("$TAG: Failed to duplicate tab", e) + throw e + } } override fun moveTabs(tabIds: List, targetTabId: String, placeAfter: Boolean) { - tabsUseCases.moveTabs( - tabIds = tabIds, - targetTabId = targetTabId, - placeAfter = placeAfter, - ) + try { + components.useCases.tabsUseCases.moveTabs( + tabIds = tabIds, + targetTabId = targetTabId, + placeAfter = placeAfter + ) + logger.debug("$TAG: Moved tabs ${tabIds.joinToString()} to $targetTabId") + } catch (e: Exception) { + logger.error("$TAG: Failed to move tabs", e) + throw e + } } override fun migratePrivateTabUseCase(tabId: String, alternativeUrl: String?): String { - return tabsUseCases.migratePrivateTabUseCase( - tabId = tabId, - alternativeUrl = alternativeUrl - ) + try { + return components.useCases.tabsUseCases.migratePrivateTabUseCase( + tabId = tabId, + alternativeUrl = alternativeUrl + ).also { + logger.debug("$TAG: Migrated private tab $tabId") + } + } catch (e: Exception) { + logger.error("$TAG: Failed to migrate private tab", e) + throw e + } } -} \ No newline at end of file +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt new file mode 100644 index 00000000..bf70382e --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt @@ -0,0 +1,187 @@ +package eu.lensai.flutter_mozilla_components.components + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import eu.lensai.flutter_mozilla_components.Components +import eu.lensai.flutter_mozilla_components.interceptor.AppRequestInterceptor +import eu.lensai.flutter_mozilla_components.services.DownloadService +import eu.lensai.flutter_mozilla_components.EngineProvider +import eu.lensai.flutter_mozilla_components.services.MediaSessionService +import eu.lensai.flutter_mozilla_components.activities.NotificationActivity +import eu.lensai.flutter_mozilla_components.R +import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey +import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware +import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents +import kotlinx.coroutines.FlowPreview +import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage +import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.session.storage.SessionStorage +import mozilla.components.browser.state.engine.EngineMiddleware +import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.thumbnails.ThumbnailsMiddleware +import mozilla.components.browser.thumbnails.storage.ThumbnailStorage +import mozilla.components.concept.engine.DefaultSettings +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy +import mozilla.components.concept.fetch.Client +import mozilla.components.feature.addons.AddonManager +import mozilla.components.feature.addons.amo.AMOAddonsProvider +import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker +import mozilla.components.feature.addons.update.DefaultAddonUpdater +import mozilla.components.feature.downloads.DownloadMiddleware +import mozilla.components.feature.media.MediaSessionFeature +import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware +import mozilla.components.feature.prompts.file.FileUploadsDirCleaner +import mozilla.components.feature.readerview.ReaderViewMiddleware +import mozilla.components.feature.session.middleware.LastAccessMiddleware +import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage +import mozilla.components.feature.webnotifications.WebNotificationFeature +import mozilla.components.support.base.worker.Frequency +import java.util.concurrent.TimeUnit + +private const val DAY_IN_MINUTES = 24 * 60L + +class Core(private val context: Context, + private val components: Components, + private val flutterEvents: GeckoStateEvents, +) { + val prefs by lazy { + PreferenceManager.getDefaultSharedPreferences(context) + } + + val engineSettings by lazy { + DefaultSettings().apply { + //historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage) + requestInterceptor = AppRequestInterceptor(context) + remoteDebuggingEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_remote_debugging), false) + testingModeEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_testing_mode), false) + trackingProtectionPolicy = createTrackingProtectionPolicy(prefs) + httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED + globalPrivacyControlEnabled = prefs.getBoolean( + context.getPreferenceKey(R.string.pref_key_global_privacy_control), + false, + ) + } + } + + val engine: Engine by lazy { + EngineProvider.createEngine(context, engineSettings) + } + + /** + * The [Client] implementation (`concept-fetch`) used for HTTP requests. + */ + val client: Client by lazy { + EngineProvider.createClient(context) + } + + val thumbnailStorage by lazy { ThumbnailStorage(context) } + + val icons by lazy { BrowserIcons(context, client) } + + /** + * A storage component for site permissions. + */ + val geckoSitePermissionsStorage by lazy { + val geckoRuntime = EngineProvider.getOrCreateRuntime(context) + GeckoSitePermissionsStorage(geckoRuntime, OnDiskSitePermissionsStorage(context)) + } + + // Addons + val addonManager by lazy { + AddonManager(store, engine, addonsProvider, addonUpdater) + } + + val addonUpdater by lazy { + DefaultAddonUpdater( + context, + Frequency(1, TimeUnit.DAYS), + components.notificationsDelegate + ) + } + + val addonsProvider by lazy { + AMOAddonsProvider( + context, + client, + collectionName = "7dfae8669acc4312a65e8ba5553036", + maxCacheAgeInMinutes = DAY_IN_MINUTES, + ) + } + + val supportedAddonsChecker by lazy { + DefaultSupportedAddonsChecker(context, Frequency(1, TimeUnit.DAYS)) + } + + val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy { + FileUploadsDirCleaner { context.cacheDir } + } + + @OptIn(FlowPreview::class) + val store by lazy { + BrowserStore( + middleware = listOf( + FlutterEventMiddleware(flutterEvents), + DownloadMiddleware(context, DownloadService::class.java), + ThumbnailsMiddleware(thumbnailStorage), + ReaderViewMiddleware(), +// UndoMiddleware(), + LastAccessMiddleware(), +// PromptMiddleware(), + SessionPrioritizationMiddleware(), + RecordingDevicesMiddleware(context, components.notificationsDelegate), + ) + EngineMiddleware.create(engine), + ).apply { + components.events.registerFlowEvents(this) + + icons.install(engine, this) + + WebNotificationFeature( + context, + engine, + icons, + R.drawable.ic_launcher_foreground, + geckoSitePermissionsStorage, + NotificationActivity::class.java, + notificationsDelegate = components.notificationsDelegate, + ) + + MediaSessionFeature(context, MediaSessionService::class.java, this).start() + } + } + + /** + * The storage component for persisting browser tab sessions. + */ + val sessionStorage: SessionStorage by lazy { + SessionStorage(context, engine) + } + + /** + * Constructs a [TrackingProtectionPolicy] based on current preferences. + * + * @param prefs the shared preferences to use when reading tracking + * protection settings. + * @param normalMode whether or not tracking protection should be enabled + * in normal browsing mode, defaults to the current preference value. + * @param privateMode whether or not tracking protection should be enabled + * in private browsing mode, default to the current preference value. + * @return the constructed tracking protection policy based on preferences. + */ + private fun createTrackingProtectionPolicy( + prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context), + normalMode: Boolean = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_tracking_protection_normal), true), + privateMode: Boolean = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_tracking_protection_private), true), + ): TrackingProtectionPolicy { + val trackingPolicy = TrackingProtectionPolicy.recommended() + return when { + normalMode && privateMode -> trackingPolicy + normalMode && !privateMode -> trackingPolicy.forRegularSessionsOnly() + !normalMode && privateMode -> trackingPolicy.forPrivateSessionsOnly() + else -> TrackingProtectionPolicy.none() + } + } + +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Events.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Events.kt new file mode 100644 index 00000000..ab2b909b --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Events.kt @@ -0,0 +1,193 @@ +package eu.lensai.flutter_mozilla_components.components + +import eu.lensai.flutter_mozilla_components.api.ReaderViewEventsImpl +import eu.lensai.flutter_mozilla_components.ext.toWebPBytes +import eu.lensai.flutter_mozilla_components.pigeons.FindResultState +import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents +import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents +import eu.lensai.flutter_mozilla_components.pigeons.HistoryItem +import eu.lensai.flutter_mozilla_components.pigeons.HistoryState +import eu.lensai.flutter_mozilla_components.pigeons.ReaderableState +import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState +import eu.lensai.flutter_mozilla_components.pigeons.TabContentState +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.feature.addons.logger +import mozilla.components.lib.state.Store +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged + +class Events( + private val flutterEvents: GeckoStateEvents, +) { + val readerViewEvents by lazy { ReaderViewEventsImpl() } + + @OptIn(FlowPreview::class) + fun registerFlowEvents(stateFlow: Store) { + stateFlow.flowScoped { flow -> + flow.map { state -> state.selectedTabId } + .distinctUntilChanged() + .collect { tabId -> + flutterEvents.onSelectedTabChange( + System.currentTimeMillis(), + tabId + ) { _ -> } + } + } + + stateFlow.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.content + } + .ifAnyChanged { arrayOf (it.content.icon) } + .debounce { 50 } + .collect { tab -> + val iconBytes = tab.content.icon?.toWebPBytes() + flutterEvents.onIconChange( + System.currentTimeMillis(), + tab.id, + iconBytes + ) { _ -> } + } + } + + stateFlow.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.content.securityInfo + } + .debounce { 50 } + .collect { tab -> + flutterEvents.onSecurityInfoStateChange( + System.currentTimeMillis(), + tab.id, + SecurityInfoState( + tab.content.securityInfo.secure, + tab.content.securityInfo.host, + tab.content.securityInfo.issuer, + ) + ) { _ -> } + } + } + + stateFlow.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.readerState + } + .ifAnyChanged { arrayOf( + it.readerState.readerable, + it.readerState.active, + ) + } + .debounce { 50 } + .collect { tab -> + flutterEvents.onReaderableStateChange( + System.currentTimeMillis(), + tab.id, + ReaderableState( + tab.readerState.readerable, + tab.readerState.active, + ) + ) { _ -> } + } + } + + stateFlow.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.content + } + .ifAnyChanged { arrayOf( + it.content.history, + it.content.canGoBack, + it.content.canGoForward, + ) + } + .debounce { 50 } + .collect { tab -> + flutterEvents.onHistoryStateChange( + System.currentTimeMillis(), + tab.id, + HistoryState( + items = tab.content.history.items.map { item -> HistoryItem( + url = item.uri, + title = item.title + ) }, + currentIndex = tab.content.history.currentIndex.toLong(), + canGoBack = tab.content.canGoBack, + canGoForward = tab.content.canGoForward, + ) + ) { _ -> } + } + } + + stateFlow.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} } + .distinctUntilChanged() + .collect { tabs -> + flutterEvents.onTabListChange(System.currentTimeMillis(), tabs) { _ -> } + } + } + + stateFlow.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.content + } + .ifAnyChanged { arrayOf( + it.content.url, + it.content.title, + it.content.private, + it.content.fullScreen, + it.content.progress, + it.content.loading) + } + .debounce { 50 } + .collect { tab -> + logger.info("title: ${tab.content.title} ${tab.content.url}") + flutterEvents.onTabContentStateChange( + System.currentTimeMillis(), + TabContentState( + id = tab.id, + contextId = tab.contextId, + url = tab.content.url, + title = tab.content.title, + progress = tab.content.progress.toLong(), + isPrivate = tab.content.private, + isFullScreen = tab.content.fullScreen, + isLoading = tab.content.loading + ) + ) { _ -> } + } + } + + stateFlow.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.content.findResults + } + .distinctUntilChanged() + .collect { tab -> + tab.content.findResults + flutterEvents.onFindResults( + System.currentTimeMillis(), + tab.id, + tab.content.findResults.map { result -> FindResultState( + activeMatchOrdinal = result.activeMatchOrdinal.toLong(), + numberOfMatches = result.numberOfMatches.toLong(), + isDoneCounting = result.isDoneCounting, + ) } + ) { _ -> } + } + } + } +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Features.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Features.kt new file mode 100644 index 00000000..2d362ade --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Features.kt @@ -0,0 +1,17 @@ +package eu.lensai.flutter_mozilla_components.components + +import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature +import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents +import mozilla.components.browser.state.store.BrowserStore + +class Features( + private val store: BrowserStore, + private val addonEvents: GeckoAddonEvents +) { + val webExtensionToolbarFeature by lazy { + WebExtensionToolbarFeature( + store, + addonEvents + ) + } +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Services.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Services.kt new file mode 100644 index 00000000..e54d5361 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Services.kt @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package eu.lensai.flutter_mozilla_components.components + +import android.content.Context +import androidx.preference.PreferenceManager +import eu.lensai.flutter_mozilla_components.R +import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey +import mozilla.components.feature.app.links.AppLinksInterceptor +import mozilla.components.feature.tabs.TabsUseCases + +/** + * Component group which encapsulates foreground-friendly services. + */ +class Services( + private val context: Context, + private val tabsUseCases: TabsUseCases, +) { + private val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + val appLinksInterceptor by lazy { + AppLinksInterceptor( + context, + interceptLinkClicks = true, + launchInApp = { + prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_launch_external_app), false) + }, + ) + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/UseCases.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/UseCases.kt new file mode 100644 index 00000000..998744b1 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/UseCases.kt @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package eu.lensai.flutter_mozilla_components.components + +import android.content.Context +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.app.links.AppLinksUseCases +import mozilla.components.feature.downloads.DownloadsUseCases +import mozilla.components.feature.session.SessionUseCases +import mozilla.components.feature.session.SettingsUseCases +import mozilla.components.feature.tabs.CustomTabsUseCases +import mozilla.components.feature.tabs.TabsUseCases + +/** + * Component group for all use cases. Use cases are provided by feature + * modules and can be triggered by UI interactions. + */ +class UseCases( + private val context: Context, + private val engine: Engine, + private val store: BrowserStore, +) { + /** + * Use cases that provide engine interactions for a given browser session. + */ + val sessionUseCases by lazy { SessionUseCases(store) } + + /** + * Use cases that provide tab management. + */ + val tabsUseCases: TabsUseCases by lazy { TabsUseCases(store) } + + /** + * Use cases that provide settings management. + */ + val settingsUseCases by lazy { SettingsUseCases(engine, store) } + + /** + * Use cases related to the downloads feature. + */ + val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) } + + /** + * Use cases related to Custom Tabs. + */ + val customTabsUseCases: CustomTabsUseCases by lazy { CustomTabsUseCases(store, sessionUseCases.loadUrl) } + + val appLinksUseCases by lazy { AppLinksUseCases(context) } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/ext/Context.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/ext/Context.kt new file mode 100644 index 00000000..ffe8a94c --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/ext/Context.kt @@ -0,0 +1,7 @@ +package eu.lensai.flutter_mozilla_components.ext + +import android.content.Context +import androidx.annotation.StringRes + +fun Context.getPreferenceKey(@StringRes resourceId: Int): String = + resources.getString(resourceId) \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/WebExtensionToolbarFeature.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/WebExtensionToolbarFeature.kt new file mode 100644 index 00000000..20cbde48 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/WebExtensionToolbarFeature.kt @@ -0,0 +1,217 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package eu.lensai.flutter_mozilla_components.feature + +import android.os.Handler +import android.os.HandlerThread +import androidx.annotation.VisibleForTesting +import eu.lensai.flutter_mozilla_components.ext.toWebPBytes +import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents +import eu.lensai.flutter_mozilla_components.pigeons.WebExtensionActionType +import eu.lensai.flutter_mozilla_components.pigeons.WebExtensionData +import kotlinx.coroutines.* +import kotlinx.coroutines.android.asCoroutineDispatcher +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.WebExtensionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.webextension.Action +import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.log.Log +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged +import org.mozilla.gecko.util.ThreadUtils.runOnUiThread + +/** + * Web extension toolbar implementation that updates the toolbar whenever the state of web + * extensions changes. + */ +class WebExtensionToolbarFeature( + private var store: BrowserStore, + private var addonEvents: GeckoAddonEvents, +) : LifecycleAwareFeature { + // This maps web extension ids to [WebExtensionToolbarAction]s for efficient + // updates of global and tab-specific browser/page actions within the same + // lifecycle. + @VisibleForTesting + internal val webExtensionBrowserActions = HashMap() + internal val webExtensionPageActions = HashMap() + + private var scope: CoroutineScope? = null + + internal val iconThread = HandlerThread("IconThread") + internal val iconHandler by lazy { + iconThread.start() + Handler(iconThread.looper) + } + + internal var iconJobDispatcher: CoroutineDispatcher = Dispatchers.Main + + init { + renderWebExtensionActions(store.state) + } + + /** + * Starts observing for the state of web extensions changes + */ + override fun start() { + // The feature could start with an existing view and toolbar so + // we have to check if any stale actions (from uninstalled or + // disabled extensions) are being displayed and remove them. + webExtensionBrowserActions + .filterKeys { !store.state.extensions.containsKey(it) || store.state.extensions[it]?.enabled == false } + .forEach { (extensionId, action) -> +// toolbar.removeBrowserAction(action) +// toolbar.invalidateActions() + webExtensionBrowserActions.remove(extensionId) + } + + webExtensionPageActions + .filterKeys { !store.state.extensions.containsKey(it) || store.state.extensions[it]?.enabled == false } + .forEach { (extensionId, action) -> +// toolbar.removePageAction(action) +// toolbar.invalidateActions() + webExtensionPageActions.remove(extensionId) + } + + iconJobDispatcher = iconHandler.asCoroutineDispatcher("WebExtensionIconDispatcher") + scope = store.flowScoped { flow -> + flow.ifAnyChanged { arrayOf(it.selectedTab, it.extensions) } + .collect { state -> + renderWebExtensionActions(state, state.selectedTab) + } + } + } + + override fun stop() { + iconJobDispatcher.cancel() + scope?.cancel() + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun renderWebExtensionActions(state: BrowserState, tab: SessionState? = null) { + val extensions = state.extensions.values.toList() + extensions.filter { it.enabled }.sortedBy { it.name }.forEach { extension -> + if (extensionNotAllowedInTab(extension, tab)) { + webExtensionPageActions[extension.id]?.let { + addonEvents.onRemoveWebExtensionAction( + timestampArg = System.currentTimeMillis(), + extensionIdArg = extension.id, + actionTypeArg = WebExtensionActionType.PAGE, + ) {} + webExtensionPageActions.remove(extension.id) + } + webExtensionBrowserActions[extension.id]?.let { + addonEvents.onRemoveWebExtensionAction( + timestampArg = System.currentTimeMillis(), + extensionIdArg = extension.id, + actionTypeArg = WebExtensionActionType.BROWSER, + ) {} + webExtensionBrowserActions.remove(extension.id) + } + return@forEach + } + + extension.browserAction?.let { browserAction -> + addOrUpdateAction( + extension = extension, + defaultAction = browserAction, + tabAction = tab?.extensionState?.get(extension.id)?.browserAction, + ) + } + + extension.pageAction?.let { pageAction -> + val tabPageAction = tab?.extensionState?.get(extension.id)?.pageAction + + // Unlike browser actions, page actions are not displayed by default (only if enabled): + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action + if (pageAction.copyWithOverride(tabPageAction).enabled == true) { + addOrUpdateAction( + extension = extension, + defaultAction = pageAction, + tabAction = tabPageAction, + isPageAction = true, + ) + } + } + } + } + + private fun extensionNotAllowedInTab( + extension: WebExtensionState?, + tab: SessionState?, + ): Boolean = extension?.allowedInPrivateBrowsing == false && tab?.content?.private == true + + fun invokeAddonBrowserAction(extensionId: String) { + webExtensionBrowserActions[extensionId]?.onClick?.invoke() + } + + fun invokeAddonPageAction(extensionId: String) { + webExtensionPageActions[extensionId]?.onClick?.invoke() + } + + private fun addOrUpdateAction( + extension: WebExtensionState, + defaultAction: Action, + tabAction: Action?, + isPageAction: Boolean = false, + ) { + var action = defaultAction + + // Apply tab-specific override of browser/page action + tabAction?.let { + action = action.copyWithOverride(it) + } + + CoroutineScope(iconJobDispatcher).launch { + try { + //TODO: make variable size + val icon = action.loadIcon?.invoke(128) + icon?.let { + val imageBytes = icon.toWebPBytes() + runOnUiThread { + addonEvents.onUpdateWebExtensionIcon( + timestampArg = System.currentTimeMillis(), + extensionIdArg = extension.id, + actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER, + iconArg = imageBytes + ) { } + } + } + } catch (throwable: Throwable) { + Log.log( + Log.Priority.ERROR, + "mozac-webextensions", + throwable, + "Failed to load browser action icon, falling back to default.", + ) + } + } + + val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions + // Add the global page/browser action if it doesn't exist + val toolbarAction = actionMap.getOrPut(extension.id) { + val toolbarAction = WebExtensionData( + extensionId = extension.id, + title = action.title, + enabled = action.enabled, + badgeText = action.badgeText, + badgeTextColor = action.badgeTextColor?.toLong(), + badgeBackgroundColor = action.badgeBackgroundColor?.toLong(), + ) + + addonEvents.onUpsertWebExtensionAction( + timestampArg = System.currentTimeMillis(), + extensionIdArg = extension.id, + actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER, + extensionDataArg = toolbarAction + ) { } + + action + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/AppRequestInterceptor.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/interceptor/AppRequestInterceptor.kt similarity index 86% rename from packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/AppRequestInterceptor.kt rename to packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/interceptor/AppRequestInterceptor.kt index 5f2c84d1..31d976bb 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/AppRequestInterceptor.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/interceptor/AppRequestInterceptor.kt @@ -1,15 +1,18 @@ -package eu.lensai.flutter_mozilla_components +package eu.lensai.flutter_mozilla_components.interceptor import android.content.Context -import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import eu.lensai.flutter_mozilla_components.GlobalComponents import mozilla.components.browser.errorpages.ErrorPages import mozilla.components.browser.errorpages.ErrorType import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.request.RequestInterceptor class AppRequestInterceptor(private val context: Context) : RequestInterceptor { + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + override fun onLoadRequest( engineSession: EngineSession, uri: String, @@ -20,7 +23,6 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor { isDirectNavigation: Boolean, isSubframeRequest: Boolean, ): RequestInterceptor.InterceptionResponse? { - val components = GlobalComponents.components!! return when (uri) { // "about:privatebrowsing" -> { @@ -37,7 +39,7 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor { // } else -> { - components.appLinksInterceptor.onLoadRequest( + components.services.appLinksInterceptor.onLoadRequest( engineSession, uri, lastUri, diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt index 353aa717..482d8bf9 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt @@ -29,6 +29,10 @@ import kotlin.reflect.typeOf * the thumbnail to the disk cache. */ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Middleware { + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + @Suppress("ComplexMethod") override fun invoke( context: MiddlewareContext, @@ -47,7 +51,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd //UpdateReaderConnectRequiredAction seems to be the only event that is called predictable //after a hot reload is ReaderAction.UpdateReaderConnectRequiredAction -> { - if(!GlobalComponents.components!!.engineReportedInitialized) { + if(!components.engineReportedInitialized) { runOnUiThread { flutterEvents.onEngineReadyStateChange( System.currentTimeMillis(), @@ -55,7 +59,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd ) { _ -> } } - GlobalComponents.components!!.engineReportedInitialized = true + components.engineReportedInitialized = true } } is TabListAction.AddTabAction -> { @@ -66,11 +70,8 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd ) { _ -> } } } - is WebExtensionAction.UpdatePromptRequestWebExtensionAction -> { - logger.debug("Event fired: " + action.javaClass.name) - } else -> { - logger.debug("Event fired: " + action.javaClass.name) + //logger.debug("Event fired: " + action.javaClass.name) } } next(action) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt index 612c02aa..e2ba20d0 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.5.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.0), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -143,6 +143,17 @@ enum class SelectionPattern(val raw: Int) { } } +enum class WebExtensionActionType(val raw: Int) { + BROWSER(0), + PAGE(1); + + companion object { + fun ofRaw(raw: Int): WebExtensionActionType? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** * Translation options that map to the Gecko Translations Options. * @@ -924,6 +935,39 @@ data class CustomSelectionAction ( ) } } + +/** Generated class from Pigeon that represents data sent in messages. */ +data class WebExtensionData ( + val extensionId: String, + val title: String? = null, + val enabled: Boolean? = null, + val badgeText: String? = null, + val badgeTextColor: Long? = null, + val badgeBackgroundColor: Long? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): WebExtensionData { + val extensionId = pigeonVar_list[0] as String + val title = pigeonVar_list[1] as String? + val enabled = pigeonVar_list[2] as Boolean? + val badgeText = pigeonVar_list[3] as String? + val badgeTextColor = pigeonVar_list[4] as Long? + val badgeBackgroundColor = pigeonVar_list[5] as Long? + return WebExtensionData(extensionId, title, enabled, badgeText, badgeTextColor, badgeBackgroundColor) + } + } + fun toList(): List { + return listOf( + extensionId, + title, + enabled, + badgeText, + badgeTextColor, + badgeBackgroundColor, + ) + } +} private open class GeckoPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -958,125 +1002,135 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { } } 135.toByte() -> { - return (readValue(buffer) as? List)?.let { - TranslationOptions.fromList(it) + return (readValue(buffer) as Long?)?.let { + WebExtensionActionType.ofRaw(it.toInt()) } } 136.toByte() -> { return (readValue(buffer) as? List)?.let { - ReaderState.fromList(it) + TranslationOptions.fromList(it) } } 137.toByte() -> { return (readValue(buffer) as? List)?.let { - LastMediaAccessState.fromList(it) + ReaderState.fromList(it) } } 138.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryMetadataKey.fromList(it) + LastMediaAccessState.fromList(it) } } 139.toByte() -> { return (readValue(buffer) as? List)?.let { - PackageCategoryValue.fromList(it) + HistoryMetadataKey.fromList(it) } } 140.toByte() -> { return (readValue(buffer) as? List)?.let { - ExternalPackage.fromList(it) + PackageCategoryValue.fromList(it) } } 141.toByte() -> { return (readValue(buffer) as? List)?.let { - LoadUrlFlagsValue.fromList(it) + ExternalPackage.fromList(it) } } 142.toByte() -> { return (readValue(buffer) as? List)?.let { - SourceValue.fromList(it) + LoadUrlFlagsValue.fromList(it) } } 143.toByte() -> { return (readValue(buffer) as? List)?.let { - TabState.fromList(it) + SourceValue.fromList(it) } } 144.toByte() -> { return (readValue(buffer) as? List)?.let { - RecoverableTab.fromList(it) + TabState.fromList(it) } } 145.toByte() -> { return (readValue(buffer) as? List)?.let { - RecoverableBrowserState.fromList(it) + RecoverableTab.fromList(it) } } 146.toByte() -> { return (readValue(buffer) as? List)?.let { - IconRequest.fromList(it) + RecoverableBrowserState.fromList(it) } } 147.toByte() -> { return (readValue(buffer) as? List)?.let { - ResourceSize.fromList(it) + IconRequest.fromList(it) } } 148.toByte() -> { return (readValue(buffer) as? List)?.let { - Resource.fromList(it) + ResourceSize.fromList(it) } } 149.toByte() -> { return (readValue(buffer) as? List)?.let { - IconResult.fromList(it) + Resource.fromList(it) } } 150.toByte() -> { return (readValue(buffer) as? List)?.let { - CookiePartitionKey.fromList(it) + IconResult.fromList(it) } } 151.toByte() -> { return (readValue(buffer) as? List)?.let { - Cookie.fromList(it) + CookiePartitionKey.fromList(it) } } 152.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryItem.fromList(it) + Cookie.fromList(it) } } 153.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryState.fromList(it) + HistoryItem.fromList(it) } } 154.toByte() -> { return (readValue(buffer) as? List)?.let { - ReaderableState.fromList(it) + HistoryState.fromList(it) } } 155.toByte() -> { return (readValue(buffer) as? List)?.let { - SecurityInfoState.fromList(it) + ReaderableState.fromList(it) } } 156.toByte() -> { return (readValue(buffer) as? List)?.let { - TabContentState.fromList(it) + SecurityInfoState.fromList(it) } } 157.toByte() -> { return (readValue(buffer) as? List)?.let { - FindResultState.fromList(it) + TabContentState.fromList(it) } } 158.toByte() -> { + return (readValue(buffer) as? List)?.let { + FindResultState.fromList(it) + } + } + 159.toByte() -> { return (readValue(buffer) as? List)?.let { CustomSelectionAction.fromList(it) } } + 160.toByte() -> { + return (readValue(buffer) as? List)?.let { + WebExtensionData.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -1106,102 +1160,110 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { stream.write(134) writeValue(stream, value.raw) } - is TranslationOptions -> { + is WebExtensionActionType -> { stream.write(135) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is ReaderState -> { + is TranslationOptions -> { stream.write(136) writeValue(stream, value.toList()) } - is LastMediaAccessState -> { + is ReaderState -> { stream.write(137) writeValue(stream, value.toList()) } - is HistoryMetadataKey -> { + is LastMediaAccessState -> { stream.write(138) writeValue(stream, value.toList()) } - is PackageCategoryValue -> { + is HistoryMetadataKey -> { stream.write(139) writeValue(stream, value.toList()) } - is ExternalPackage -> { + is PackageCategoryValue -> { stream.write(140) writeValue(stream, value.toList()) } - is LoadUrlFlagsValue -> { + is ExternalPackage -> { stream.write(141) writeValue(stream, value.toList()) } - is SourceValue -> { + is LoadUrlFlagsValue -> { stream.write(142) writeValue(stream, value.toList()) } - is TabState -> { + is SourceValue -> { stream.write(143) writeValue(stream, value.toList()) } - is RecoverableTab -> { + is TabState -> { stream.write(144) writeValue(stream, value.toList()) } - is RecoverableBrowserState -> { + is RecoverableTab -> { stream.write(145) writeValue(stream, value.toList()) } - is IconRequest -> { + is RecoverableBrowserState -> { stream.write(146) writeValue(stream, value.toList()) } - is ResourceSize -> { + is IconRequest -> { stream.write(147) writeValue(stream, value.toList()) } - is Resource -> { + is ResourceSize -> { stream.write(148) writeValue(stream, value.toList()) } - is IconResult -> { + is Resource -> { stream.write(149) writeValue(stream, value.toList()) } - is CookiePartitionKey -> { + is IconResult -> { stream.write(150) writeValue(stream, value.toList()) } - is Cookie -> { + is CookiePartitionKey -> { stream.write(151) writeValue(stream, value.toList()) } - is HistoryItem -> { + is Cookie -> { stream.write(152) writeValue(stream, value.toList()) } - is HistoryState -> { + is HistoryItem -> { stream.write(153) writeValue(stream, value.toList()) } - is ReaderableState -> { + is HistoryState -> { stream.write(154) writeValue(stream, value.toList()) } - is SecurityInfoState -> { + is ReaderableState -> { stream.write(155) writeValue(stream, value.toList()) } - is TabContentState -> { + is SecurityInfoState -> { stream.write(156) writeValue(stream, value.toList()) } - is FindResultState -> { + is TabContentState -> { stream.write(157) writeValue(stream, value.toList()) } - is CustomSelectionAction -> { + is FindResultState -> { stream.write(158) writeValue(stream, value.toList()) } + is CustomSelectionAction -> { + stream.write(159) + writeValue(stream, value.toList()) + } + is WebExtensionData -> { + stream.write(160) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -1311,7 +1373,7 @@ interface GeckoSessionApi { fun crashRecovery(tabIds: List?) fun purgeHistory() fun updateLastAccess(tabId: String?, lastAccess: Long?) - fun requestScreenshot(callback: (Result) -> Unit) + fun requestScreenshot(sendBack: Boolean, callback: (Result) -> Unit) companion object { /** The codec used by GeckoSessionApi. */ @@ -1626,8 +1688,10 @@ interface GeckoSessionApi { run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.requestScreenshot$separatedMessageChannelSuffix", codec) if (api != null) { - channel.setMessageHandler { _, reply -> - api.requestScreenshot{ result: Result -> + channel.setMessageHandler { message, reply -> + val args = message as List + val sendBackArg = args[0] as Boolean + api.requestScreenshot(sendBackArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { reply.reply(wrapError(error)) @@ -2567,3 +2631,115 @@ class GeckoSelectionActionEvents(private val binaryMessenger: BinaryMessenger, p } } } +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface GeckoAddonsApi { + fun startAddonManagerActivity() + fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType) + + companion object { + /** The codec used by GeckoAddonsApi. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoAddonsApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoAddonsApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.startAddonManagerActivity$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.startAddonManagerActivity() + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val extensionIdArg = args[0] as String + val actionTypeArg = args[1] as WebExtensionActionType + val wrapped: List = try { + api.invokeAddonAction(extensionIdArg, actionTypeArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +class GeckoAddonEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by GeckoAddonEvents. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + } + fun onUpsertWebExtensionAction(timestampArg: Long, extensionIdArg: String, actionTypeArg: WebExtensionActionType, extensionDataArg: WebExtensionData, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(timestampArg, extensionIdArg, actionTypeArg, extensionDataArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun onRemoveWebExtensionAction(timestampArg: Long, extensionIdArg: String, actionTypeArg: WebExtensionActionType, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onRemoveWebExtensionAction$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(timestampArg, extensionIdArg, actionTypeArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + fun onUpdateWebExtensionIcon(timestampArg: Long, extensionIdArg: String, actionTypeArg: WebExtensionActionType, iconArg: ByteArray, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(timestampArg, extensionIdArg, actionTypeArg, iconArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/services/DownloadService.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/services/DownloadService.kt new file mode 100644 index 00000000..684fc735 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/services/DownloadService.kt @@ -0,0 +1,16 @@ +package eu.lensai.flutter_mozilla_components.services + +import eu.lensai.flutter_mozilla_components.GlobalComponents +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.feature.downloads.AbstractFetchDownloadService +import mozilla.components.support.base.android.NotificationsDelegate + +class DownloadService : AbstractFetchDownloadService() { + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + override val httpClient by lazy { components.core.client } + override val store: BrowserStore by lazy { components.core.store } + override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate } +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/MediaSessionService.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/services/MediaSessionService.kt similarity index 50% rename from packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/MediaSessionService.kt rename to packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/services/MediaSessionService.kt index e2909b61..ab12aa56 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/MediaSessionService.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/services/MediaSessionService.kt @@ -1,5 +1,6 @@ -package eu.lensai.flutter_mozilla_components +package eu.lensai.flutter_mozilla_components.services +import eu.lensai.flutter_mozilla_components.GlobalComponents import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.base.crash.CrashReporting import mozilla.components.feature.media.service.AbstractMediaSessionService @@ -9,7 +10,11 @@ import mozilla.components.support.base.android.NotificationsDelegate * See [AbstractMediaSessionService]. */ class MediaSessionService : AbstractMediaSessionService() { - override val crashReporter: CrashReporting? by lazy { GlobalComponents.components!!.crashReporter } - override val store: BrowserStore by lazy { GlobalComponents.components!!.store } - override val notificationsDelegate: NotificationsDelegate by lazy { GlobalComponents.components!!.notificationsDelegate } + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + override val crashReporter: CrashReporting? by lazy { null } + override val store: BrowserStore by lazy { components.core.store } + override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate } } \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/res/values/preference_keys.xml b/packages/flutter_mozilla_components/android/src/main/res/values/preference_keys.xml new file mode 100644 index 00000000..93ca8672 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/res/values/preference_keys.xml @@ -0,0 +1,29 @@ + + + + pref_key_autofill + pref_key_sign_in + pref_key_pair_sign_in + pref_key_sign_out + pref_key_sync_now + pref_key_sync_manage_account + pref_key_firefox_account + pref_key_telemetry + pref_key_make_default_browser + pref_key_sync_history + pref_key_sync_tabs + pref_key_sync_passwords + pref_key_remote_debugging + pref_key_testing_mode + pref_key_about_page + pref_key_privacy + pref_key_tracking_protection_normal + pref_key_tracking_protection_private + pref_key_global_privacy_control + pref_key_launch_external_app + pref_key_override_amo_collection + pref_key_override_amo_user + pref_key_compose_ui + diff --git a/packages/flutter_mozilla_components/android/src/test/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPluginTest.kt b/packages/flutter_mozilla_components/android/src/test/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaContextPluginTest.kt similarity index 94% rename from packages/flutter_mozilla_components/android/src/test/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPluginTest.kt rename to packages/flutter_mozilla_components/android/src/test/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaContextPluginTest.kt index 2c91e256..3bdfd796 100644 --- a/packages/flutter_mozilla_components/android/src/test/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPluginTest.kt +++ b/packages/flutter_mozilla_components/android/src/test/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaContextPluginTest.kt @@ -13,7 +13,7 @@ import org.mockito.Mockito * you can run them directly from IDEs that support JUnit such as Android Studio. */ -internal class FlutterMozillaComponentsPluginTest { +internal class FlutterMozillaContextPluginTest { @Test fun onMethodCall_getPlatformVersion_returnsExpectedValue() { val plugin = FlutterMozillaComponentsPlugin() diff --git a/packages/flutter_mozilla_components/example/android/app/src/main/AndroidManifest.xml b/packages/flutter_mozilla_components/example/android/app/src/main/AndroidManifest.xml index 8ee851a1..4e5b5361 100644 --- a/packages/flutter_mozilla_components/example/android/app/src/main/AndroidManifest.xml +++ b/packages/flutter_mozilla_components/example/android/app/src/main/AndroidManifest.xml @@ -41,7 +41,7 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true"> - + - diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart index beec373a..84069066 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -2,6 +2,7 @@ export 'src/data/models/load_url_flags.dart'; export 'src/data/models/source.dart'; export 'src/domain/entities/default_selection_actions.dart'; +export 'src/domain/services/gecko_addon.dart'; export 'src/domain/services/gecko_cookie.dart'; export 'src/domain/services/gecko_engine_settings.dart'; export 'src/domain/services/gecko_event.dart'; @@ -21,4 +22,6 @@ export 'src/pigeons/gecko.g.dart' Resource, ResourceSize, SecurityInfoState, - TabContentState; + TabContentState, + WebExtensionActionType, + WebExtensionData; 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 new file mode 100644 index 00000000..2b5a0055 --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_addon.dart @@ -0,0 +1,128 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_mozilla_components/src/extensions/subject.dart'; +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; +import 'package:rxdart/rxdart.dart'; + +typedef ExtensionDataEvent = ({String extensionId, WebExtensionData? data}); +typedef ExtensionIconEvent = ({String extensionId, Uint8List bytes}); + +final _apiInstance = GeckoAddonsApi(); + +class GeckoAddonService extends GeckoAddonEvents { + final GeckoAddonsApi _api; + + final _browserExtensionSubject = ReplaySubject(); + final _pageExtensionSubject = ReplaySubject(); + + final _browserIconSubject = ReplaySubject(); + final _pageIconSubject = ReplaySubject(); + + Stream get browserExtensionStream => + _browserExtensionSubject.stream; + Stream get pageExtensionStream => + _pageExtensionSubject.stream; + + Stream get browserIconStream => + _browserIconSubject.stream; + Stream get pageIconStream => _pageIconSubject.stream; + + Future startAddonManagerActivity() { + return _api.startAddonManagerActivity(); + } + + Future invokeAddonAction( + String extensionId, + WebExtensionActionType actionType, + ) { + return _api.invokeAddonAction(extensionId, actionType); + } + + @override + void onRemoveWebExtensionAction( + int timestamp, + String extensionId, + WebExtensionActionType actionType, + ) { + switch (actionType) { + case WebExtensionActionType.browser: + _browserExtensionSubject.addWhenMoreRecent( + timestamp, + extensionId, + (extensionId: extensionId, data: null), + ); + case WebExtensionActionType.page: + _pageExtensionSubject.addWhenMoreRecent( + timestamp, + extensionId, + (extensionId: extensionId, data: null), + ); + } + } + + @override + void onUpdateWebExtensionIcon( + int timestamp, + String extensionId, + WebExtensionActionType actionType, + Uint8List icon, + ) { + switch (actionType) { + case WebExtensionActionType.browser: + _browserIconSubject.addWhenMoreRecent( + timestamp, + extensionId, + (extensionId: extensionId, bytes: icon), + ); + case WebExtensionActionType.page: + _pageIconSubject.addWhenMoreRecent( + timestamp, + extensionId, + (extensionId: extensionId, bytes: icon), + ); + } + } + + @override + void onUpsertWebExtensionAction( + int timestamp, + String extensionId, + WebExtensionActionType actionType, + WebExtensionData extensionData, + ) { + switch (actionType) { + case WebExtensionActionType.browser: + _browserExtensionSubject.addWhenMoreRecent( + timestamp, + extensionId, + (extensionId: extensionId, data: extensionData), + ); + case WebExtensionActionType.page: + _pageExtensionSubject.addWhenMoreRecent( + timestamp, + extensionId, + (extensionId: extensionId, data: extensionData), + ); + } + } + + GeckoAddonService.setUp({ + BinaryMessenger? binaryMessenger, + GeckoAddonsApi? api, + String messageChannelSuffix = '', + }) : _api = api ?? _apiInstance { + GeckoAddonEvents.setUp( + this, + binaryMessenger: binaryMessenger, + messageChannelSuffix: messageChannelSuffix, + ); + } + + void dispose() { + unawaited(_browserExtensionSubject.close()); + unawaited(_pageExtensionSubject.close()); + unawaited(_browserIconSubject.close()); + unawaited(_pageIconSubject.close()); + } +} diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart index 4f78310f..022e4425 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/services.dart'; +import 'package:flutter_mozilla_components/src/extensions/subject.dart'; import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; import 'package:rxdart/rxdart.dart'; @@ -12,8 +13,6 @@ typedef ThumbnailEvent = ({String tabId, Uint8List? bytes}); typedef FindResultsEvent = ({String tabId, List results}); class GeckoEventService extends GeckoStateEvents { - final _lastEventTimes = >{}; - // Stream controllers final _viewStateSubject = BehaviorSubject.seeded(false); final _engineStateSubject = BehaviorSubject.seeded(false); @@ -48,35 +47,20 @@ class GeckoEventService extends GeckoStateEvents { Stream get tabAddedStream => _tabAddedSubject.stream; - void _addWhenMoreRecent( - Subject subject, - int timestamp, - dynamic identifier, - T value, - ) { - _lastEventTimes[subject] ??= {}; - - if ((_lastEventTimes[subject]?[identifier] ?? 0) < timestamp) { - _lastEventTimes[subject]![identifier] = timestamp; - subject.add(value); - } - } - @override void onViewReadyStateChange(int timestamp, bool state) { - _addWhenMoreRecent(_viewStateSubject, timestamp, null, state); + _viewStateSubject.addWhenMoreRecent(timestamp, null, state); } @override void onEngineReadyStateChange(int timestamp, bool state) { - _addWhenMoreRecent(_engineStateSubject, timestamp, null, state); + _engineStateSubject.addWhenMoreRecent(timestamp, null, state); } // Overridden methods @override void onTabListChange(int timestamp, List tabIds) { - _addWhenMoreRecent( - _tabListSubject, + _tabListSubject.addWhenMoreRecent( timestamp, null, tabIds.nonNulls.toList(), @@ -85,18 +69,17 @@ class GeckoEventService extends GeckoStateEvents { @override void onSelectedTabChange(int timestamp, String? id) { - _addWhenMoreRecent(_selectedTabSubject, timestamp, id, id); + _selectedTabSubject.addWhenMoreRecent(timestamp, id, id); } @override void onTabContentStateChange(int timestamp, TabContentState state) { - _addWhenMoreRecent(_tabContentSubject, timestamp, state.id, state); + _tabContentSubject.addWhenMoreRecent(timestamp, state.id, state); } @override void onHistoryStateChange(int timestamp, String id, HistoryState state) { - _addWhenMoreRecent( - _historySubject, + _historySubject.addWhenMoreRecent( timestamp, id, (tabId: id, history: state), @@ -109,8 +92,7 @@ class GeckoEventService extends GeckoStateEvents { String id, ReaderableState state, ) { - _addWhenMoreRecent( - _readerableSubject, + _readerableSubject.addWhenMoreRecent( timestamp, id, (tabId: id, readerable: state), @@ -123,8 +105,7 @@ class GeckoEventService extends GeckoStateEvents { String id, SecurityInfoState state, ) { - _addWhenMoreRecent( - _securityInfoSubject, + _securityInfoSubject.addWhenMoreRecent( timestamp, id, (tabId: id, securityInfo: state), @@ -133,13 +114,12 @@ class GeckoEventService extends GeckoStateEvents { @override void onIconChange(int timestamp, String id, Uint8List? bytes) { - _addWhenMoreRecent(_iconSubject, timestamp, id, (tabId: id, bytes: bytes)); + _iconSubject.addWhenMoreRecent(timestamp, id, (tabId: id, bytes: bytes)); } @override void onThumbnailChange(int timestamp, String id, Uint8List? bytes) { - _addWhenMoreRecent( - _thumbnailSubject, + _thumbnailSubject.addWhenMoreRecent( timestamp, id, (tabId: id, bytes: bytes), @@ -148,8 +128,7 @@ class GeckoEventService extends GeckoStateEvents { @override void onFindResults(int timestamp, String id, List results) { - _addWhenMoreRecent( - _findResultsSubject, + _findResultsSubject.addWhenMoreRecent( timestamp, id, (tabId: id, results: results.nonNulls.toList()), @@ -158,8 +137,7 @@ class GeckoEventService extends GeckoStateEvents { @override void onTabAdded(int timestamp, String tabId) { - _addWhenMoreRecent( - _tabAddedSubject, + _tabAddedSubject.addWhenMoreRecent( timestamp, null, tabId, diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_session.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_session.dart index 9d9562ed..8498f71b 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_session.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_session.dart @@ -120,12 +120,12 @@ class GeckoSessionService { return _api.purgeHistory(); } - Future requestScreenshot() { + Future requestScreenshot(bool sendBack) { if (tabId != null) { throw Exception('Screenshot only allowed for selected (visible) tab.'); } - return _api.requestScreenshot(); + return _api.requestScreenshot(sendBack); } Future updateLastAccess({ diff --git a/packages/flutter_mozilla_components/lib/src/extensions/subject.dart b/packages/flutter_mozilla_components/lib/src/extensions/subject.dart new file mode 100644 index 00000000..4cbeb563 --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/extensions/subject.dart @@ -0,0 +1,33 @@ +import 'package:rxdart/rxdart.dart'; + +final _lastEventTimes = >{}; + +extension SubjectAddRecent on Subject { + void addWhenMoreRecent( + int timestamp, + dynamic identifier, + T value, + ) { + _lastEventTimes[this] ??= {}; + + if ((_lastEventTimes[this]?[identifier] ?? 0) < timestamp) { + _lastEventTimes[this]![identifier] = timestamp; + add(value); + } + } + + void updateWhenMoreRecent( + int timestamp, + dynamic identifier, + T Function(T? currentValue) update, + ) { + assert(this is BehaviorSubject, 'This only works with BehaviourSubject'); + + _lastEventTimes[this] ??= {}; + + if ((_lastEventTimes[this]?[identifier] ?? 0) < timestamp) { + _lastEventTimes[this]![identifier] = timestamp; + add(update((this as BehaviorSubject).valueOrNull)); + } + } +} 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 ce27e8ef..7896d407 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.5.0), do not edit directly. +// Autogenerated from Pigeon (v22.6.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -83,6 +83,11 @@ enum SelectionPattern { email, } +enum WebExtensionActionType { + browser, + page, +} + /// Translation options that map to the Gecko Translations Options. /// /// @property downloadModel If the necessary models should be downloaded on request. If false, then @@ -1022,6 +1027,52 @@ class CustomSelectionAction { } } +class WebExtensionData { + WebExtensionData({ + required this.extensionId, + this.title, + this.enabled, + this.badgeText, + this.badgeTextColor, + this.badgeBackgroundColor, + }); + + String extensionId; + + String? title; + + bool? enabled; + + String? badgeText; + + int? badgeTextColor; + + int? badgeBackgroundColor; + + Object encode() { + return [ + extensionId, + title, + enabled, + badgeText, + badgeTextColor, + badgeBackgroundColor, + ]; + } + + static WebExtensionData decode(Object result) { + result as List; + return WebExtensionData( + extensionId: result[0]! as String, + title: result[1] as String?, + enabled: result[2] as bool?, + badgeText: result[3] as String?, + badgeTextColor: result[4] as int?, + badgeBackgroundColor: result[5] as int?, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @@ -1048,78 +1099,84 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is SelectionPattern) { buffer.putUint8(134); writeValue(buffer, value.index); - } else if (value is TranslationOptions) { + } else if (value is WebExtensionActionType) { buffer.putUint8(135); - writeValue(buffer, value.encode()); - } else if (value is ReaderState) { + writeValue(buffer, value.index); + } else if (value is TranslationOptions) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is LastMediaAccessState) { + } else if (value is ReaderState) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is HistoryMetadataKey) { + } else if (value is LastMediaAccessState) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is PackageCategoryValue) { + } else if (value is HistoryMetadataKey) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is ExternalPackage) { + } else if (value is PackageCategoryValue) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is LoadUrlFlagsValue) { + } else if (value is ExternalPackage) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is SourceValue) { + } else if (value is LoadUrlFlagsValue) { buffer.putUint8(142); writeValue(buffer, value.encode()); - } else if (value is TabState) { + } else if (value is SourceValue) { buffer.putUint8(143); writeValue(buffer, value.encode()); - } else if (value is RecoverableTab) { + } else if (value is TabState) { buffer.putUint8(144); writeValue(buffer, value.encode()); - } else if (value is RecoverableBrowserState) { + } else if (value is RecoverableTab) { buffer.putUint8(145); writeValue(buffer, value.encode()); - } else if (value is IconRequest) { + } else if (value is RecoverableBrowserState) { buffer.putUint8(146); writeValue(buffer, value.encode()); - } else if (value is ResourceSize) { + } else if (value is IconRequest) { buffer.putUint8(147); writeValue(buffer, value.encode()); - } else if (value is Resource) { + } else if (value is ResourceSize) { buffer.putUint8(148); writeValue(buffer, value.encode()); - } else if (value is IconResult) { + } else if (value is Resource) { buffer.putUint8(149); writeValue(buffer, value.encode()); - } else if (value is CookiePartitionKey) { + } else if (value is IconResult) { buffer.putUint8(150); writeValue(buffer, value.encode()); - } else if (value is Cookie) { + } else if (value is CookiePartitionKey) { buffer.putUint8(151); writeValue(buffer, value.encode()); - } else if (value is HistoryItem) { + } else if (value is Cookie) { buffer.putUint8(152); writeValue(buffer, value.encode()); - } else if (value is HistoryState) { + } else if (value is HistoryItem) { buffer.putUint8(153); writeValue(buffer, value.encode()); - } else if (value is ReaderableState) { + } else if (value is HistoryState) { buffer.putUint8(154); writeValue(buffer, value.encode()); - } else if (value is SecurityInfoState) { + } else if (value is ReaderableState) { buffer.putUint8(155); writeValue(buffer, value.encode()); - } else if (value is TabContentState) { + } else if (value is SecurityInfoState) { buffer.putUint8(156); writeValue(buffer, value.encode()); - } else if (value is FindResultState) { + } else if (value is TabContentState) { buffer.putUint8(157); writeValue(buffer, value.encode()); - } else if (value is CustomSelectionAction) { + } else if (value is FindResultState) { buffer.putUint8(158); writeValue(buffer, value.encode()); + } else if (value is CustomSelectionAction) { + buffer.putUint8(159); + writeValue(buffer, value.encode()); + } else if (value is WebExtensionData) { + buffer.putUint8(160); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1147,53 +1204,58 @@ class _PigeonCodec extends StandardMessageCodec { final int? value = readValue(buffer) as int?; return value == null ? null : SelectionPattern.values[value]; case 135: - return TranslationOptions.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : WebExtensionActionType.values[value]; case 136: - return ReaderState.decode(readValue(buffer)!); + return TranslationOptions.decode(readValue(buffer)!); case 137: - return LastMediaAccessState.decode(readValue(buffer)!); + return ReaderState.decode(readValue(buffer)!); case 138: - return HistoryMetadataKey.decode(readValue(buffer)!); + return LastMediaAccessState.decode(readValue(buffer)!); case 139: - return PackageCategoryValue.decode(readValue(buffer)!); + return HistoryMetadataKey.decode(readValue(buffer)!); case 140: - return ExternalPackage.decode(readValue(buffer)!); + return PackageCategoryValue.decode(readValue(buffer)!); case 141: - return LoadUrlFlagsValue.decode(readValue(buffer)!); + return ExternalPackage.decode(readValue(buffer)!); case 142: - return SourceValue.decode(readValue(buffer)!); + return LoadUrlFlagsValue.decode(readValue(buffer)!); case 143: - return TabState.decode(readValue(buffer)!); + return SourceValue.decode(readValue(buffer)!); case 144: - return RecoverableTab.decode(readValue(buffer)!); + return TabState.decode(readValue(buffer)!); case 145: - return RecoverableBrowserState.decode(readValue(buffer)!); + return RecoverableTab.decode(readValue(buffer)!); case 146: - return IconRequest.decode(readValue(buffer)!); + return RecoverableBrowserState.decode(readValue(buffer)!); case 147: - return ResourceSize.decode(readValue(buffer)!); + return IconRequest.decode(readValue(buffer)!); case 148: - return Resource.decode(readValue(buffer)!); + return ResourceSize.decode(readValue(buffer)!); case 149: - return IconResult.decode(readValue(buffer)!); + return Resource.decode(readValue(buffer)!); case 150: - return CookiePartitionKey.decode(readValue(buffer)!); + return IconResult.decode(readValue(buffer)!); case 151: - return Cookie.decode(readValue(buffer)!); + return CookiePartitionKey.decode(readValue(buffer)!); case 152: - return HistoryItem.decode(readValue(buffer)!); + return Cookie.decode(readValue(buffer)!); case 153: - return HistoryState.decode(readValue(buffer)!); + return HistoryItem.decode(readValue(buffer)!); case 154: - return ReaderableState.decode(readValue(buffer)!); + return HistoryState.decode(readValue(buffer)!); case 155: - return SecurityInfoState.decode(readValue(buffer)!); + return ReaderableState.decode(readValue(buffer)!); case 156: - return TabContentState.decode(readValue(buffer)!); + return SecurityInfoState.decode(readValue(buffer)!); case 157: - return FindResultState.decode(readValue(buffer)!); + return TabContentState.decode(readValue(buffer)!); case 158: + return FindResultState.decode(readValue(buffer)!); + case 159: return CustomSelectionAction.decode(readValue(buffer)!); + case 160: + return WebExtensionData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -1659,7 +1721,7 @@ class GeckoSessionApi { } } - Future requestScreenshot() async { + Future requestScreenshot(bool sendBack) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.requestScreenshot$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -1667,7 +1729,7 @@ class GeckoSessionApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; + await pigeonVar_channel.send([sendBack]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -2855,3 +2917,174 @@ abstract class GeckoSelectionActionEvents { } } } + +class GeckoAddonsApi { + /// Constructor for [GeckoAddonsApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeckoAddonsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future startAddonManagerActivity() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.startAddonManagerActivity$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future invokeAddonAction(String extensionId, WebExtensionActionType actionType) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([extensionId, actionType]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } +} + +abstract class GeckoAddonEvents { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + void onUpsertWebExtensionAction(int timestamp, String extensionId, WebExtensionActionType actionType, WebExtensionData extensionData); + + void onRemoveWebExtensionAction(int timestamp, String extensionId, WebExtensionActionType actionType); + + void onUpdateWebExtensionIcon(int timestamp, String extensionId, WebExtensionActionType actionType, Uint8List icon); + + static void setUp(GeckoAddonEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction was null.'); + final List args = (message as List?)!; + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction was null, expected non-null int.'); + final String? arg_extensionId = (args[1] as String?); + assert(arg_extensionId != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction was null, expected non-null String.'); + final WebExtensionActionType? arg_actionType = (args[2] as WebExtensionActionType?); + assert(arg_actionType != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction was null, expected non-null WebExtensionActionType.'); + final WebExtensionData? arg_extensionData = (args[3] as WebExtensionData?); + assert(arg_extensionData != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction was null, expected non-null WebExtensionData.'); + try { + api.onUpsertWebExtensionAction(arg_timestamp!, arg_extensionId!, arg_actionType!, arg_extensionData!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onRemoveWebExtensionAction$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onRemoveWebExtensionAction was null.'); + final List args = (message as List?)!; + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onRemoveWebExtensionAction was null, expected non-null int.'); + final String? arg_extensionId = (args[1] as String?); + assert(arg_extensionId != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onRemoveWebExtensionAction was null, expected non-null String.'); + final WebExtensionActionType? arg_actionType = (args[2] as WebExtensionActionType?); + assert(arg_actionType != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onRemoveWebExtensionAction was null, expected non-null WebExtensionActionType.'); + try { + api.onRemoveWebExtensionAction(arg_timestamp!, arg_extensionId!, arg_actionType!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon was null.'); + final List args = (message as List?)!; + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon was null, expected non-null int.'); + final String? arg_extensionId = (args[1] as String?); + assert(arg_extensionId != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon was null, expected non-null String.'); + final WebExtensionActionType? arg_actionType = (args[2] as WebExtensionActionType?); + assert(arg_actionType != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon was null, expected non-null WebExtensionActionType.'); + final Uint8List? arg_icon = (args[3] as Uint8List?); + assert(arg_icon != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon was null, expected non-null Uint8List.'); + try { + api.onUpdateWebExtensionIcon(arg_timestamp!, arg_extensionId!, arg_actionType!, arg_icon!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index 4237e932..eb1b84a5 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -479,6 +479,26 @@ class CustomSelectionAction { CustomSelectionAction(this.id, this.title, this.pattern); } +enum WebExtensionActionType { browser, page } + +class WebExtensionData { + final String extensionId; + final String? title; + final bool? enabled; + final String? badgeText; + final int? badgeTextColor; + final int? badgeBackgroundColor; + + WebExtensionData( + this.extensionId, + this.title, + this.enabled, + this.badgeText, + this.badgeTextColor, + this.badgeBackgroundColor, + ); +} + // /// Represents all the different supported types of data that can be found from long clicking // /// an element. // sealed class HitResult { @@ -637,7 +657,7 @@ abstract class GeckoSessionApi { }); @async - Uint8List? requestScreenshot(); + Uint8List? requestScreenshot(bool sendBack); } @HostApi() @@ -835,3 +855,33 @@ abstract class GeckoSelectionActionController { abstract class GeckoSelectionActionEvents { void performSelectionAction(String id, String selectedText); } + +@HostApi() +abstract class GeckoAddonsApi { + void startAddonManagerActivity(); + + void invokeAddonAction(String extensionId, WebExtensionActionType actionType); +} + +@FlutterApi() +abstract class GeckoAddonEvents { + void onUpsertWebExtensionAction( + int timestamp, + String extensionId, + WebExtensionActionType actionType, + WebExtensionData extensionData, + ); + + void onRemoveWebExtensionAction( + int timestamp, + String extensionId, + WebExtensionActionType actionType, + ); + + void onUpdateWebExtensionIcon( + int timestamp, + String extensionId, + WebExtensionActionType actionType, + Uint8List icon, + ); +} diff --git a/packages/flutter_mozilla_components/pubspec.yaml b/packages/flutter_mozilla_components/pubspec.yaml index 426ab04e..6b22acc6 100644 --- a/packages/flutter_mozilla_components/pubspec.yaml +++ b/packages/flutter_mozilla_components/pubspec.yaml @@ -17,7 +17,7 @@ dev_dependencies: flutter_test: sdk: flutter lint: ^2.3.0 - pigeon: ^22.5.0 + pigeon: ^22.6.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec