diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 5448e536..7cf87bed 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -25,7 +25,7 @@ if (flutterVersionName == null) { android { namespace = "me.movenext.bang_navigator" - compileSdk = 34 + compileSdk = 35 ndkVersion = flutter.ndkVersion compileOptions { @@ -43,7 +43,7 @@ android { // https://api.flutter.dev/flutter/widgets/AndroidView-class.html //https://inappwebview.dev/docs/intro/#setup-android minSdk = 23 - targetSdk = 34 + targetSdk = 35 versionCode = flutterVersionCode.toInteger() versionName = flutterVersionName } @@ -51,7 +51,7 @@ android { dependencies { // implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // implementation 'androidx.glance:glance-appwidget:1.0.0' - implementation 'com.google.android.material:material:1.11.0' + implementation 'com.google.android.material:material:1.12.0' } signingConfigs { diff --git a/app/android/settings.gradle b/app/android/settings.gradle index 9151bc04..00cb4c30 100644 --- a/app/android/settings.gradle +++ b/app/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false + id "com.android.application" version "8.3.1" apply false id "org.jetbrains.kotlin.android" version "1.9.22" apply false } diff --git a/app/assets/scripts/readability.min.js.gz b/app/assets/scripts/readability.min.js.gz deleted file mode 120000 index 93576576..00000000 --- a/app/assets/scripts/readability.min.js.gz +++ /dev/null @@ -1 +0,0 @@ -../../js/readability/dist/readability.min.js.gz \ No newline at end of file diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index 8b63db74..f9ddeb94 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -8,14 +8,14 @@ import 'package:lensai/features/bangs/presentation/screens/search.dart'; import 'package:lensai/features/chat_archive/presentation/screens/detail.dart'; import 'package:lensai/features/chat_archive/presentation/screens/list.dart'; import 'package:lensai/features/chat_archive/presentation/screens/search.dart'; -import 'package:lensai/features/search_browser/presentation/screens/browser.dart'; -import 'package:lensai/features/settings/presentation/screens/settings.dart'; +import 'package:lensai/features/geckoview/features/browser/screens/browser.dart'; import 'package:lensai/features/geckoview/features/topics/presentation/screens/topic_list.dart'; +import 'package:lensai/features/settings/presentation/screens/settings.dart'; part 'routes.g.dart'; -@TypedGoRoute( - name: 'KagiRoute', +@TypedGoRoute( + name: 'BrowserRoute', path: '/', routes: [ TypedGoRoute( @@ -48,10 +48,10 @@ part 'routes.g.dart'; ), ], ) -class KagiRoute extends GoRouteData { +class BrowserRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const KagiScreen(); + return const BrowserScreen(); } } diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index 4e7a1c44..92edfc1e 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -7,15 +7,15 @@ part of 'routes.dart'; // ************************************************************************** List get $appRoutes => [ - $kagiRoute, + $browserRoute, $settingsRoute, $chatArchiveListRoute, ]; -RouteBase get $kagiRoute => GoRouteData.$route( +RouteBase get $browserRoute => GoRouteData.$route( path: '/', - name: 'KagiRoute', - factory: $KagiRouteExtension._fromState, + name: 'BrowserRoute', + factory: $BrowserRouteExtension._fromState, routes: [ GoRouteData.$route( path: 'about', @@ -54,8 +54,8 @@ RouteBase get $kagiRoute => GoRouteData.$route( ], ); -extension $KagiRouteExtension on KagiRoute { - static KagiRoute _fromState(GoRouterState state) => KagiRoute(); +extension $BrowserRouteExtension on BrowserRoute { + static BrowserRoute _fromState(GoRouterState state) => BrowserRoute(); String get location => GoRouteData.$location( '/', diff --git a/app/lib/domain/services/app_initialization.dart b/app/lib/domain/services/app_initialization.dart index 3d31d6f3..b9ff6476 100644 --- a/app/lib/domain/services/app_initialization.dart +++ b/app/lib/domain/services/app_initialization.dart @@ -6,10 +6,9 @@ import 'package:lensai/features/bangs/data/models/bang.dart'; import 'package:lensai/features/bangs/domain/repositories/sync.dart'; import 'package:lensai/features/content_block/data/models/host.dart'; import 'package:lensai/features/content_block/domain/repositories/sync.dart'; -import 'package:lensai/features/search_browser/domain/services/session.dart'; +import 'package:lensai/features/kagi/data/services/session.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; -import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'app_initialization.g.dart'; @@ -77,15 +76,11 @@ class AppInitializationService extends _$AppInitializationService { ), ); - if (settings.incognitoMode) { - await ref.read(sessionServiceProvider.notifier).clearAllData(); - //Delete all unsasigned tabs - await ref.read(tabDatabaseProvider).tabDao.deleteTopicTabs(null); - } - if (settings.kagiSession case final String session) { if (session.isNotEmpty) { - await ref.read(sessionServiceProvider.notifier).setKagiSession(session); + await ref + .read(kagiSessionServiceProvider.notifier) + .setKagiSession(session); } } } diff --git a/app/lib/domain/services/app_initialization.g.dart b/app/lib/domain/services/app_initialization.g.dart index 43c61b14..43c3ec57 100644 --- a/app/lib/domain/services/app_initialization.g.dart +++ b/app/lib/domain/services/app_initialization.g.dart @@ -7,7 +7,7 @@ part of 'app_initialization.dart'; // ************************************************************************** String _$appInitializationServiceHash() => - r'eee9c8eadc793fb435af1032ad2099dfd381a034'; + r'0d7dc3eb3ed2e17643f4c34e4e172734ed8d09e2'; /// See also [AppInitializationService]. @ProviderFor(AppInitializationService) diff --git a/app/lib/features/bangs/data/database/daos/bang.dart b/app/lib/features/bangs/data/database/daos/bang.dart index e4dc9e04..d379d35b 100644 --- a/app/lib/features/bangs/data/database/daos/bang.dart +++ b/app/lib/features/bangs/data/database/daos/bang.dart @@ -1,7 +1,4 @@ -import 'dart:ui'; - import 'package:drift/drift.dart'; -import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:lensai/features/bangs/data/database/database.dart'; import 'package:lensai/features/bangs/data/models/bang.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; diff --git a/app/lib/features/bangs/presentation/screens/list.dart b/app/lib/features/bangs/presentation/screens/list.dart index 5a749ea7..43575c3f 100644 --- a/app/lib/features/bangs/presentation/screens/list.dart +++ b/app/lib/features/bangs/presentation/screens/list.dart @@ -4,9 +4,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/bangs/domain/providers.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; -import 'package:lensai/features/search_browser/domain/providers.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; class BangListScreen extends HookConsumerWidget { @@ -49,15 +50,16 @@ class BangListScreen extends HookConsumerWidget { .read(selectedBangTriggerProvider().notifier) .setTrigger(bang.trigger); - if (ref.read(bottomSheetProvider) is! CreateTabSheet) { - ref.read(bottomSheetProvider.notifier).show( + if (ref.read(bottomSheetControllerProvider) + is! CreateTabSheet) { + ref.read(bottomSheetControllerProvider.notifier).show( CreateTabSheet( preferredTool: KagiTool.search, ), ); } - context.go(KagiRoute().location); + context.go(BrowserRoute().location); }, ); }, diff --git a/app/lib/features/bangs/presentation/screens/search.dart b/app/lib/features/bangs/presentation/screens/search.dart index 8c159ae2..0efcee60 100644 --- a/app/lib/features/bangs/presentation/screens/search.dart +++ b/app/lib/features/bangs/presentation/screens/search.dart @@ -8,9 +8,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/bangs/domain/repositories/search.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; -import 'package:lensai/features/search_browser/domain/providers.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/presentation/hooks/listenable_callback.dart'; @@ -77,13 +78,14 @@ class BangSearchScreen extends HookConsumerWidget { .read(selectedBangTriggerProvider().notifier) .setTrigger(bang.trigger); - if (ref.read(bottomSheetProvider) is! CreateTabSheet) { - ref.read(bottomSheetProvider.notifier).show( + if (ref.read(bottomSheetControllerProvider) + is! CreateTabSheet) { + ref.read(bottomSheetControllerProvider.notifier).show( CreateTabSheet(preferredTool: KagiTool.search), ); } - context.go(KagiRoute().location); + context.go(BrowserRoute().location); }, ); }, diff --git a/app/lib/features/bangs/presentation/widgets/bang_details.dart b/app/lib/features/bangs/presentation/widgets/bang_details.dart index 452bd1dc..f3c3faf3 100644 --- a/app/lib/features/bangs/presentation/widgets/bang_details.dart +++ b/app/lib/features/bangs/presentation/widgets/bang_details.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/utils/ui_helper.dart' as ui_helper; @@ -82,11 +83,11 @@ class BangDetails extends HookConsumerWidget { await ui_helper.launchUrlFeedback(context, url); } else { await ref - .read(switchNewTabControllerProvider.notifier) - .add(url); + .read(tabRepositoryProvider.notifier) + .addTab(url: url); if (context.mounted) { - context.go(KagiRoute().location); + context.go(BrowserRoute().location); } } }, diff --git a/app/lib/features/bangs/presentation/widgets/search_field.dart b/app/lib/features/bangs/presentation/widgets/search_field.dart index 9647764c..addd392c 100644 --- a/app/lib/features/bangs/presentation/widgets/search_field.dart +++ b/app/lib/features/bangs/presentation/widgets/search_field.dart @@ -5,8 +5,8 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart'; import 'package:lensai/features/kagi/domain/repositories/autosuggest.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/speech_to_text_button.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/presentation/widgets/autocomplete.dart'; diff --git a/app/lib/features/bangs/presentation/widgets/site_search.dart b/app/lib/features/bangs/presentation/widgets/site_search.dart index a545a05f..96cca82b 100644 --- a/app/lib/features/bangs/presentation/widgets/site_search.dart +++ b/app/lib/features/bangs/presentation/widgets/site_search.dart @@ -6,7 +6,9 @@ import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; import 'package:lensai/features/bangs/presentation/widgets/search_field.dart'; -import 'package:lensai/features/search_browser/domain/providers.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; class SiteSearch extends HookConsumerWidget { @@ -39,10 +41,10 @@ class SiteSearch extends HookConsumerWidget { .increaseFrequency(activeBang.trigger); await ref - .read(switchNewTabControllerProvider.notifier) - .add(activeBang.getUrl(textController.text)); + .read(tabRepositoryProvider.notifier) + .addTab(url: activeBang.getUrl(textController.text)); - ref.read(overlayDialogProvider.notifier).dismiss(); + ref.read(overlayDialogControllerProvider.notifier).dismiss(); } } diff --git a/app/lib/features/chat_archive/domain/repositories/search.dart b/app/lib/features/chat_archive/domain/repositories/search.dart index fe9df7c9..24167f79 100644 --- a/app/lib/features/chat_archive/domain/repositories/search.dart +++ b/app/lib/features/chat_archive/domain/repositories/search.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:lensai/core/logger.dart'; import 'package:lensai/features/chat_archive/data/database/database.dart'; import 'package:lensai/features/chat_archive/data/providers.dart'; @@ -53,7 +52,7 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository { ), ); - return availableChats.whereNotNull(); + return availableChats.nonNulls; } Future search( 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 707fb37a..97c2cf64 100644 --- a/app/lib/features/chat_archive/domain/repositories/search.g.dart +++ b/app/lib/features/chat_archive/domain/repositories/search.g.dart @@ -7,7 +7,7 @@ part of 'search.dart'; // ************************************************************************** String _$chatArchiveSearchRepositoryHash() => - r'6cb31ab2c4a37561ec17a60dae3bb4ecd3893744'; + r'a3decc7975bf49995b20ea578db3c08b69f7c03c'; /// See also [ChatArchiveSearchRepository]. @ProviderFor(ChatArchiveSearchRepository) diff --git a/app/lib/features/chat_archive/presentation/screens/detail.dart b/app/lib/features/chat_archive/presentation/screens/detail.dart index c0068282..5b342657 100644 --- a/app/lib/features/chat_archive/presentation/screens/detail.dart +++ b/app/lib/features/chat_archive/presentation/screens/detail.dart @@ -10,6 +10,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/chat_archive/domain/repositories/archive.dart'; import 'package:lensai/features/chat_archive/utils/markdown_to_text.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; @@ -113,11 +114,11 @@ class ChatArchiveDetailScreen extends HookConsumerWidget { await ui_helper.launchUrlFeedback(context, Uri.parse(href)); } else { await ref - .read(switchNewTabControllerProvider.notifier) - .add(url); + .read(tabRepositoryProvider.notifier) + .addTab(url: url); if (context.mounted) { - context.go(KagiRoute().location); + context.go(BrowserRoute().location); } } } diff --git a/app/lib/features/chat_archive/presentation/screens/list.dart b/app/lib/features/chat_archive/presentation/screens/list.dart index 2e0f7ae0..c4c8a94e 100644 --- a/app/lib/features/chat_archive/presentation/screens/list.dart +++ b/app/lib/features/chat_archive/presentation/screens/list.dart @@ -2,8 +2,8 @@ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/extensions/date_time.dart'; import 'package:lensai/core/routing/routes.dart'; +import 'package:lensai/extensions/date_time.dart'; import 'package:lensai/features/chat_archive/domain/repositories/archive.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:skeletonizer/skeletonizer.dart'; diff --git a/app/lib/features/chat_archive/presentation/screens/search.dart b/app/lib/features/chat_archive/presentation/screens/search.dart index 9b4d3e8b..d737c8b1 100644 --- a/app/lib/features/chat_archive/presentation/screens/search.dart +++ b/app/lib/features/chat_archive/presentation/screens/search.dart @@ -6,8 +6,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/extensions/date_time.dart'; import 'package:lensai/core/routing/routes.dart'; +import 'package:lensai/extensions/date_time.dart'; import 'package:lensai/features/chat_archive/domain/entities/chat_entity.dart'; import 'package:lensai/features/chat_archive/domain/repositories/search.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; diff --git a/app/lib/features/content_block/data/services/source.dart b/app/lib/features/content_block/data/services/source.dart index 74cb48e3..b3637a27 100644 --- a/app/lib/features/content_block/data/services/source.dart +++ b/app/lib/features/content_block/data/services/source.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:collection/collection.dart'; import 'package:exceptions/exceptions.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; @@ -30,10 +29,7 @@ class HostSourceService extends _$HostSourceService { ); final matches = hostRegex.allMatches(utf8.decode(args[0])); - return matches - .map((match) => match.group(2)) - .whereNotNull() - .toSet(); + return matches.map((match) => match.group(2)).nonNulls.toSet(); }, [response.bodyBytes], ); diff --git a/app/lib/features/content_block/data/services/source.g.dart b/app/lib/features/content_block/data/services/source.g.dart index 78ecceeb..46fa2c75 100644 --- a/app/lib/features/content_block/data/services/source.g.dart +++ b/app/lib/features/content_block/data/services/source.g.dart @@ -6,7 +6,7 @@ part of 'source.dart'; // RiverpodGenerator // ************************************************************************** -String _$hostSourceServiceHash() => r'26ac09fcd34cc37440eb643075984707e0e9bba3'; +String _$hostSourceServiceHash() => r'6c1ee7bfde02cc542a4f55db666d49037fda187e'; /// See also [HostSourceService]. @ProviderFor(HostSourceService) diff --git a/app/lib/features/geckoview/domain/entities/browser_icon.dart b/app/lib/features/geckoview/domain/entities/browser_icon.dart index 72bc3f39..84b3664f 100644 --- a/app/lib/features/geckoview/domain/entities/browser_icon.dart +++ b/app/lib/features/geckoview/domain/entities/browser_icon.dart @@ -21,14 +21,14 @@ class BrowserIcon with FastEquatable { final image = await tryDecodeImage(bytes).then((image) => image!.toEquatable()); - return BrowserIcon._( + return BrowserIcon( image: image, dominantColor: dominantColor, source: source, ); } - BrowserIcon._({ + BrowserIcon({ required this.image, required this.dominantColor, required this.source, diff --git a/app/lib/features/geckoview/domain/entities/find_result_state.dart b/app/lib/features/geckoview/domain/entities/find_result_state.dart new file mode 100644 index 00000000..9ee3fb6b --- /dev/null +++ b/app/lib/features/geckoview/domain/entities/find_result_state.dart @@ -0,0 +1,29 @@ +import 'package:fast_equatable/fast_equatable.dart'; + +class FindResultState with FastEquatable { + final int activeMatchOrdinal; + final int numberOfMatches; + final bool isDoneCounting; + + FindResultState({ + required this.activeMatchOrdinal, + required this.numberOfMatches, + required this.isDoneCounting, + }); + + factory FindResultState.$default() => FindResultState( + activeMatchOrdinal: -1, + numberOfMatches: 0, + isDoneCounting: false, + ); + + @override + List get hashParameters => [ + activeMatchOrdinal, + numberOfMatches, + isDoneCounting, + ]; + + @override + bool get cacheHash => true; +} diff --git a/app/lib/features/geckoview/domain/entities/tab_state.dart b/app/lib/features/geckoview/domain/entities/tab_state.dart index 41f9b32a..8dbcff85 100644 --- a/app/lib/features/geckoview/domain/entities/tab_state.dart +++ b/app/lib/features/geckoview/domain/entities/tab_state.dart @@ -1,6 +1,10 @@ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/data/models/web_page_info.dart'; import 'package:lensai/domain/entities/equatable_image.dart'; +import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart'; +import 'package:lensai/features/geckoview/domain/entities/find_result_state.dart'; import 'package:lensai/features/geckoview/domain/entities/history_state.dart'; import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; import 'package:lensai/features/geckoview/domain/entities/security_state.dart'; @@ -8,16 +12,28 @@ import 'package:lensai/features/geckoview/domain/entities/security_state.dart'; part 'tab_state.g.dart'; @CopyWith() -class TabState with FastEquatable { +class TabState with FastEquatable implements WebPageInfo { @CopyWithField(immutable: true) final String id; final String? contextId; + @override final Uri url; + @override final String title; final EquatableImage? icon; + + @override + BrowserIcon? get favicon => (icon != null) + ? BrowserIcon( + image: icon!, + dominantColor: null, + source: IconSource.memory, + ) + : null; + final EquatableImage? thumbnail; final int progress; @@ -29,6 +45,7 @@ class TabState with FastEquatable { final SecurityState securityInfoState; final HistoryState historyState; final ReaderableState readerableState; + final FindResultState findResultState; TabState({ required this.id, @@ -44,6 +61,7 @@ class TabState with FastEquatable { required this.securityInfoState, required this.historyState, required this.readerableState, + required this.findResultState, }); factory TabState.$default(String tabId) => TabState( @@ -60,6 +78,7 @@ class TabState with FastEquatable { securityInfoState: SecurityState.$default(), historyState: HistoryState.$default(), readerableState: ReaderableState.$default(), + findResultState: FindResultState.$default(), ); @override @@ -80,5 +99,6 @@ class TabState with FastEquatable { securityInfoState, historyState, readerableState, + findResultState, ]; } diff --git a/app/lib/features/geckoview/domain/entities/tab_state.g.dart b/app/lib/features/geckoview/domain/entities/tab_state.g.dart index 06e32e8d..6a435af2 100644 --- a/app/lib/features/geckoview/domain/entities/tab_state.g.dart +++ b/app/lib/features/geckoview/domain/entities/tab_state.g.dart @@ -31,6 +31,8 @@ abstract class _$TabStateCWProxy { TabState readerableState(ReaderableState readerableState); + TabState findResultState(FindResultState findResultState); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TabState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -50,6 +52,7 @@ abstract class _$TabStateCWProxy { SecurityState? securityInfoState, HistoryState? historyState, ReaderableState? readerableState, + FindResultState? findResultState, }); } @@ -98,6 +101,10 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy { TabState readerableState(ReaderableState readerableState) => this(readerableState: readerableState); + @override + TabState findResultState(FindResultState findResultState) => + this(findResultState: findResultState); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TabState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -119,6 +126,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy { Object? securityInfoState = const $CopyWithPlaceholder(), Object? historyState = const $CopyWithPlaceholder(), Object? readerableState = const $CopyWithPlaceholder(), + Object? findResultState = const $CopyWithPlaceholder(), }) { return TabState( id: _value.id, @@ -174,6 +182,11 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy { ? _value.readerableState // ignore: cast_nullable_to_non_nullable : readerableState as ReaderableState, + findResultState: findResultState == const $CopyWithPlaceholder() || + findResultState == null + ? _value.findResultState + // ignore: cast_nullable_to_non_nullable + : findResultState as FindResultState, ); } } diff --git a/app/lib/features/geckoview/domain/providers.dart b/app/lib/features/geckoview/domain/providers.dart index 6959964e..baffd4f1 100644 --- a/app/lib/features/geckoview/domain/providers.dart +++ b/app/lib/features/geckoview/domain/providers.dart @@ -1,8 +1,5 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; -import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; -import 'package:lensai/features/geckoview/domain/repositories/selected_tab.dart'; -import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; -import 'package:lensai/features/geckoview/domain/repositories/tab_session.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; @@ -18,23 +15,6 @@ GeckoEventService eventService(EventServiceRef ref) { return service; } -@Riverpod() -TabState? tabState(TabStateRef ref, String? tabId) { - if (tabId == null) { - return null; - } - - return ref.watch( - tabRepositoryProvider.select((tabs) => tabs[tabId]), - ); -} - -@Riverpod() -TabState? selectedTabState(SelectedTabStateRef ref) { - final tabId = ref.watch(selectedTabControllerProvider); - return ref.watch(tabStateProvider(tabId)); -} - @Riverpod() TabSession selectedTabSessionNotifier(SelectedTabSessionNotifierRef 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 dd0a64c1..f060d01e 100644 --- a/app/lib/features/geckoview/domain/providers.g.dart +++ b/app/lib/features/geckoview/domain/providers.g.dart @@ -20,169 +20,6 @@ final eventServiceProvider = Provider.internal( ); typedef EventServiceRef = ProviderRef; -String _$tabStateHash() => r'6985f5472625a9cf44c8bbc61e61b7577eb60e71'; - -/// 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)); - } -} - -/// See also [tabState]. -@ProviderFor(tabState) -const tabStateProvider = TabStateFamily(); - -/// See also [tabState]. -class TabStateFamily extends Family { - /// See also [tabState]. - const TabStateFamily(); - - /// See also [tabState]. - TabStateProvider call( - String? tabId, - ) { - return TabStateProvider( - tabId, - ); - } - - @override - TabStateProvider getProviderOverride( - covariant TabStateProvider provider, - ) { - return call( - provider.tabId, - ); - } - - 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'tabStateProvider'; -} - -/// See also [tabState]. -class TabStateProvider extends AutoDisposeProvider { - /// See also [tabState]. - TabStateProvider( - String? tabId, - ) : this._internal( - (ref) => tabState( - ref as TabStateRef, - tabId, - ), - from: tabStateProvider, - name: r'tabStateProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$tabStateHash, - dependencies: TabStateFamily._dependencies, - allTransitiveDependencies: TabStateFamily._allTransitiveDependencies, - tabId: tabId, - ); - - TabStateProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.tabId, - }) : super.internal(); - - final String? tabId; - - @override - Override overrideWith( - TabState? Function(TabStateRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: TabStateProvider._internal( - (ref) => create(ref as TabStateRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - tabId: tabId, - ), - ); - } - - @override - AutoDisposeProviderElement createElement() { - return _TabStateProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is TabStateProvider && other.tabId == tabId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, tabId.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin TabStateRef on AutoDisposeProviderRef { - /// The parameter `tabId` of this provider. - String? get tabId; -} - -class _TabStateProviderElement extends AutoDisposeProviderElement - with TabStateRef { - _TabStateProviderElement(super.provider); - - @override - String? get tabId => (origin as TabStateProvider).tabId; -} - -String _$selectedTabStateHash() => r'd5240ade9010f5c9f00d7a4f419e1b776f75fac1'; - -/// See also [selectedTabState]. -@ProviderFor(selectedTabState) -final selectedTabStateProvider = AutoDisposeProvider.internal( - selectedTabState, - name: r'selectedTabStateProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$selectedTabStateHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef SelectedTabStateRef = AutoDisposeProviderRef; String _$selectedTabSessionNotifierHash() => r'1b0fe1afa87c0b09fc8407ceb91946fffaef5214'; diff --git a/app/lib/features/geckoview/domain/repositories/selected_tab.dart b/app/lib/features/geckoview/domain/providers/selected_tab.dart similarity index 65% rename from app/lib/features/geckoview/domain/repositories/selected_tab.dart rename to app/lib/features/geckoview/domain/providers/selected_tab.dart index cf9cfc59..ab323e6f 100644 --- a/app/lib/features/geckoview/domain/repositories/selected_tab.dart +++ b/app/lib/features/geckoview/domain/providers/selected_tab.dart @@ -1,19 +1,12 @@ import 'dart:async'; -import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'selected_tab.g.dart'; @Riverpod(keepAlive: true) -class SelectedTabController extends _$SelectedTabController { - final _tabsService = GeckoTabService(); - - Future selectTab(String tabId) { - return _tabsService.selectTab(tabId: tabId); - } - +class SelectedTab extends _$SelectedTab { @override String? build() { final eventSerivce = ref.watch(eventServiceProvider); diff --git a/app/lib/features/geckoview/domain/providers/selected_tab.g.dart b/app/lib/features/geckoview/domain/providers/selected_tab.g.dart new file mode 100644 index 00000000..3c315bda --- /dev/null +++ b/app/lib/features/geckoview/domain/providers/selected_tab.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'selected_tab.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$selectedTabHash() => r'd30f0490c7924b7de30e23f3d4d132311bf336d0'; + +/// See also [SelectedTab]. +@ProviderFor(SelectedTab) +final selectedTabProvider = NotifierProvider.internal( + SelectedTab.new, + name: r'selectedTabProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$selectedTabHash, + dependencies: null, + allTransitiveDependencies: null, +); + +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 diff --git a/app/lib/features/geckoview/domain/repositories/tab_session.dart b/app/lib/features/geckoview/domain/providers/tab_session.dart similarity index 77% rename from app/lib/features/geckoview/domain/repositories/tab_session.dart rename to app/lib/features/geckoview/domain/providers/tab_session.dart index ac2cbcf7..c4a2aa61 100644 --- a/app/lib/features/geckoview/domain/repositories/tab_session.dart +++ b/app/lib/features/geckoview/domain/providers/tab_session.dart @@ -19,6 +19,18 @@ class TabSession extends _$TabSession { ); } + Future reload() { + return _sessionService.reload(); + } + + Future goBack() { + return _sessionService.goBack(); + } + + Future goForward() { + return _sessionService.goForward(); + } + @override void build(String? tabId) { _sessionService = (tabId != null) diff --git a/app/lib/features/geckoview/domain/repositories/tab_session.g.dart b/app/lib/features/geckoview/domain/providers/tab_session.g.dart similarity index 98% rename from app/lib/features/geckoview/domain/repositories/tab_session.g.dart rename to app/lib/features/geckoview/domain/providers/tab_session.g.dart index 3f9d9430..2ae93a15 100644 --- a/app/lib/features/geckoview/domain/repositories/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'4aab49dcb544cfc9e7a61b498d2ccf2c28ef729b'; +String _$tabSessionHash() => r'cdde9c7426178f50dc430284939e215f158e8535'; /// Copied from Dart SDK class _SystemHash { diff --git a/app/lib/features/geckoview/domain/providers/tab_state.dart b/app/lib/features/geckoview/domain/providers/tab_state.dart new file mode 100644 index 00000000..774c1afe --- /dev/null +++ b/app/lib/features/geckoview/domain/providers/tab_state.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/extensions/image.dart'; +import 'package:lensai/features/geckoview/domain/entities/find_result_state.dart'; +import 'package:lensai/features/geckoview/domain/entities/history_state.dart'; +import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; +import 'package:lensai/features/geckoview/domain/entities/security_state.dart'; +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_annotation/riverpod_annotation.dart'; + +part 'tab_state.g.dart'; + +@Riverpod(keepAlive: true) +class TabStates extends _$TabStates { + final _tabsService = GeckoTabService(); + + void _onTabListChange(List tabs) { + state = { + for (final tabId in tabs) tabId: state[tabId] ?? TabState.$default(tabId), + }; + } + + void _onTabContentStateChange(TabContentState contentState) { + final current = + state[contentState.id] ?? TabState.$default(contentState.id); + + state = {...state}..[contentState.id] = current.copyWith( + contextId: contentState.contextId, + url: Uri.parse(contentState.url), + title: contentState.title, + progress: contentState.progress, + isPrivate: contentState.isPrivate, + isFullScreen: contentState.isFullScreen, + isLoading: contentState.isLoading, + ); + } + + Future _onIconChange(IconEvent event) async { + final IconEvent(:tabId, :bytes) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + final image = (bytes != null) + ? (await tryDecodeImage(bytes) + .then((image) async => image?.toEquatable())) + : null; + + state = {...state}..[tabId] = current.copyWith.icon(image); + } + + Future _onThumbnailChange(ThumbnailEvent event) async { + final ThumbnailEvent(:tabId, :bytes) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + final image = (bytes != null) + ? (await tryDecodeImage(bytes) + .then((image) async => image?.toEquatable())) + : null; + + state = {...state}..[tabId] = current.copyWith.thumbnail(image); + } + + void _onSecurityInfoStateChange(SecurityInfoEvent event) { + final SecurityInfoEvent(:tabId, :securityInfo) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + state = {...state}..[tabId] = current.copyWith.securityInfoState( + SecurityState( + secure: securityInfo.secure, + host: securityInfo.host, + issuer: securityInfo.issuer, + ), + ); + } + + void _onHistoryStateChange(HistoryEvent event) { + final HistoryEvent(:tabId, :history) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + state = {...state}..[tabId] = current.copyWith.historyState( + HistoryState( + items: history.items.nonNulls + .map( + (item) => + HistoryItem(url: Uri.parse(item.url), title: item.title), + ) + .toList(), + currentIndex: history.currentIndex, + canGoBack: history.canGoBack, + canGoForward: history.canGoForward, + ), + ); + } + + void _onReaderableStateChange(ReaderableEvent event) { + final ReaderableEvent(:tabId, :readerable) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + state = {...state}..[tabId] = current.copyWith.readerableState( + ReaderableState( + readerable: readerable.readerable, + active: readerable.active, + ), + ); + } + + void _onFindResults(FindResultsEvent event) { + final FindResultsEvent(:tabId, :results) = event; + + if (results.isNotEmpty) { + final current = state[tabId] ?? TabState.$default(tabId); + final result = results.last; + + state = {...state}..[tabId] = current.copyWith.findResultState( + FindResultState( + activeMatchOrdinal: result.activeMatchOrdinal, + numberOfMatches: result.numberOfMatches, + isDoneCounting: result.isDoneCounting, + ), + ); + } + } + + Future addTab({ + Uri? url, + bool selectTab = true, + bool startLoading = true, + String? parentId, + LoadUrlFlags flags = LoadUrlFlags.NONE, + String? contextId, + Source source = Internal.newTab, + bool private = false, + HistoryMetadataKey? historyMetadata, + Map? additionalHeaders, + }) { + return _tabsService.addTab( + url: url, + selectTab: selectTab, + startLoading: startLoading, + parentId: parentId, + flags: flags, + contextId: contextId, + source: source, + private: private, + historyMetadata: historyMetadata, + additionalHeaders: additionalHeaders, + ); + } + + Future closeTabs(List tabIds) { + return _tabsService.removeTabs(ids: tabIds); + } + + Future closeTab(String tabIds) { + return _tabsService.removeTab(tabId: tabIds); + } + + @override + Map build() { + final eventService = ref.watch(eventServiceProvider); + + final subscriptions = [ + eventService.tabListEvents.listen(_onTabListChange), + eventService.tabContentEvents.listen(_onTabContentStateChange), + eventService.iconEvents.listen(_onIconChange), + eventService.thumbnailEvents.listen(_onThumbnailChange), + eventService.securityInfoEvents.listen(_onSecurityInfoStateChange), + eventService.historyEvents.listen(_onHistoryStateChange), + eventService.readerableEvents.listen(_onReaderableStateChange), + eventService.findResultsEvent.listen(_onFindResults), + ]; + + ref.onDispose(() { + for (final sub in subscriptions) { + unawaited(sub.cancel()); + } + }); + + return {}; + } +} + +@Riverpod() +TabState? tabState(TabStateRef ref, String? tabId) { + if (tabId == null) { + return null; + } + + return ref.watch( + tabStatesProvider.select((tabs) => tabs[tabId]), + ); +} + +@Riverpod() +TabState? selectedTabState(SelectedTabStateRef 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 new file mode 100644 index 00000000..2c3eadd4 --- /dev/null +++ b/app/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -0,0 +1,188 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tab_state.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$tabStateHash() => r'9fd7990804643fac180dd4fd50803bc7c9fa2827'; + +/// 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)); + } +} + +/// See also [tabState]. +@ProviderFor(tabState) +const tabStateProvider = TabStateFamily(); + +/// See also [tabState]. +class TabStateFamily extends Family { + /// See also [tabState]. + const TabStateFamily(); + + /// See also [tabState]. + TabStateProvider call( + String? tabId, + ) { + return TabStateProvider( + tabId, + ); + } + + @override + TabStateProvider getProviderOverride( + covariant TabStateProvider provider, + ) { + return call( + provider.tabId, + ); + } + + 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'tabStateProvider'; +} + +/// See also [tabState]. +class TabStateProvider extends AutoDisposeProvider { + /// See also [tabState]. + TabStateProvider( + String? tabId, + ) : this._internal( + (ref) => tabState( + ref as TabStateRef, + tabId, + ), + from: tabStateProvider, + name: r'tabStateProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$tabStateHash, + dependencies: TabStateFamily._dependencies, + allTransitiveDependencies: TabStateFamily._allTransitiveDependencies, + tabId: tabId, + ); + + TabStateProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.tabId, + }) : super.internal(); + + final String? tabId; + + @override + Override overrideWith( + TabState? Function(TabStateRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: TabStateProvider._internal( + (ref) => create(ref as TabStateRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + tabId: tabId, + ), + ); + } + + @override + AutoDisposeProviderElement createElement() { + return _TabStateProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is TabStateProvider && other.tabId == tabId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, tabId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin TabStateRef on AutoDisposeProviderRef { + /// The parameter `tabId` of this provider. + String? get tabId; +} + +class _TabStateProviderElement extends AutoDisposeProviderElement + with TabStateRef { + _TabStateProviderElement(super.provider); + + @override + String? get tabId => (origin as TabStateProvider).tabId; +} + +String _$selectedTabStateHash() => r'22172913554e14e67f692eff0777ea015175906e'; + +/// See also [selectedTabState]. +@ProviderFor(selectedTabState) +final selectedTabStateProvider = AutoDisposeProvider.internal( + selectedTabState, + name: r'selectedTabStateProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedTabStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef SelectedTabStateRef = AutoDisposeProviderRef; +String _$tabStatesHash() => r'22fd36fe2e443fd2dfb703dc3be72716fbccef6d'; + +/// See also [TabStates]. +@ProviderFor(TabStates) +final tabStatesProvider = + NotifierProvider>.internal( + TabStates.new, + name: r'tabStatesProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$tabStatesHash, + dependencies: null, + allTransitiveDependencies: null, +); + +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 diff --git a/app/lib/features/geckoview/domain/repositories/tab.dart b/app/lib/features/geckoview/domain/repositories/tab.dart index 892405fc..4e4fb0ac 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.dart @@ -1,13 +1,6 @@ import 'dart:async'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; -import 'package:lensai/extensions/image.dart'; -import 'package:lensai/features/geckoview/domain/entities/history_state.dart'; -import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; -import 'package:lensai/features/geckoview/domain/entities/security_state.dart'; -import 'package:lensai/features/geckoview/domain/entities/tab_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'; part 'tab.g.dart'; @@ -16,100 +9,6 @@ part 'tab.g.dart'; class TabRepository extends _$TabRepository { final _tabsService = GeckoTabService(); - void _onTabListChange(List tabs) { - state = { - for (final tabId in tabs) tabId: state[tabId] ?? TabState.$default(tabId), - }; - } - - void _onTabContentStateChange(TabContentState contentState) { - final current = - state[contentState.id] ?? TabState.$default(contentState.id); - - state = {...state}..[contentState.id] = current.copyWith( - contextId: contentState.contextId, - url: Uri.parse(contentState.url), - title: contentState.title, - progress: contentState.progress, - isPrivate: contentState.isPrivate, - isFullScreen: contentState.isFullScreen, - isLoading: contentState.isLoading, - ); - } - - Future _onIconChange(IconEvent event) async { - final IconEvent(:tabId, :bytes) = event; - - final current = state[tabId] ?? TabState.$default(tabId); - - final image = (bytes != null) - ? (await tryDecodeImage(bytes) - .then((image) async => image?.toEquatable())) - : null; - - state = {...state}..[tabId] = current.copyWith.icon(image); - } - - Future _onThumbnailChange(ThumbnailEvent event) async { - final ThumbnailEvent(:tabId, :bytes) = event; - - final current = state[tabId] ?? TabState.$default(tabId); - - final image = (bytes != null) - ? (await tryDecodeImage(bytes) - .then((image) async => image?.toEquatable())) - : null; - - state = {...state}..[tabId] = current.copyWith.thumbnail(image); - } - - void _onSecurityInfoStateChange(SecurityInfoEvent event) { - final SecurityInfoEvent(:tabId, :securityInfo) = event; - - final current = state[tabId] ?? TabState.$default(tabId); - - state = {...state}..[tabId] = current.copyWith.securityInfoState( - SecurityState( - secure: securityInfo.secure, - host: securityInfo.host, - issuer: securityInfo.issuer, - ), - ); - } - - void _onHistoryStateChange(HistoryEvent event) { - final HistoryEvent(:tabId, :history) = event; - - final current = state[tabId] ?? TabState.$default(tabId); - - state = {...state}..[tabId] = current.copyWith.historyState( - HistoryState( - items: history.items.nonNulls - .map( - (item) => - HistoryItem(url: Uri.parse(item.url), title: item.title), - ) - .toList(), - currentIndex: history.currentIndex, - canGoBack: history.canGoBack, - canGoForward: history.canGoForward, - ), - ); - } - - void _onReaderableStateChange(ReaderableEvent event) { - final ReaderableEvent(:tabId, :readerable) = event; - - final current = state[tabId] ?? TabState.$default(tabId); - - state = {...state}..[tabId] = current.copyWith.readerableState( - ReaderableState( - readerable: readerable.readerable, - active: readerable.active, - ), - ); - } - Future addTab({ Uri? url, bool selectTab = true, @@ -136,30 +35,18 @@ class TabRepository extends _$TabRepository { ); } + Future selectTab(String tabId) { + return _tabsService.selectTab(tabId: tabId); + } + + Future closeTab(String tabId) { + return _tabsService.removeTab(tabId: tabId); + } + Future closeTabs(List tabIds) { return _tabsService.removeTabs(ids: tabIds); } @override - Map build() { - final eventService = ref.watch(eventServiceProvider); - - final subscriptions = [ - eventService.tabListEvents.listen(_onTabListChange), - eventService.tabContentEvents.listen(_onTabContentStateChange), - eventService.iconEvents.listen(_onIconChange), - eventService.thumbnailEvents.listen(_onThumbnailChange), - eventService.securityInfoEvents.listen(_onSecurityInfoStateChange), - eventService.historyEvents.listen(_onHistoryStateChange), - eventService.readerableEvents.listen(_onReaderableStateChange), - ]; - - ref.onDispose(() { - for (final sub in subscriptions) { - unawaited(sub.cancel()); - } - }); - - return {}; - } + void build() {} } diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart index 5f1f0897..48a26f50 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -6,12 +6,11 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabRepositoryHash() => r'64724c0215624753c5c6ff8d6827b890ad9a6f2c'; +String _$tabRepositoryHash() => r'0abf38ef42f29015d81ccd11eb5990b741ebc616'; /// See also [TabRepository]. @ProviderFor(TabRepository) -final tabRepositoryProvider = - NotifierProvider>.internal( +final tabRepositoryProvider = NotifierProvider.internal( TabRepository.new, name: r'tabRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -21,6 +20,6 @@ final tabRepositoryProvider = allTransitiveDependencies: null, ); -typedef _$TabRepository = Notifier>; +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 diff --git a/app/lib/features/search_browser/domain/entities/sheet.dart b/app/lib/features/geckoview/features/browser/domain/entities/sheet.dart similarity index 100% rename from app/lib/features/search_browser/domain/entities/sheet.dart rename to app/lib/features/geckoview/features/browser/domain/entities/sheet.dart diff --git a/app/lib/features/search_browser/domain/providers.dart b/app/lib/features/geckoview/features/browser/domain/providers.dart similarity index 93% rename from app/lib/features/search_browser/domain/providers.dart rename to app/lib/features/geckoview/features/browser/domain/providers.dart index cc6c12fd..123f8834 100644 --- a/app/lib/features/search_browser/domain/providers.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.dart @@ -2,12 +2,9 @@ import 'dart:async'; -import 'package:flutter/widgets.dart'; - import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; diff --git a/app/lib/features/search_browser/domain/providers.g.dart b/app/lib/features/geckoview/features/browser/domain/providers.g.dart similarity index 100% rename from app/lib/features/search_browser/domain/providers.g.dart rename to app/lib/features/geckoview/features/browser/domain/providers.g.dart diff --git a/app/lib/features/search_browser/domain/services/create_tab.dart b/app/lib/features/geckoview/features/browser/domain/services/create_tab.dart similarity index 94% rename from app/lib/features/search_browser/domain/services/create_tab.dart rename to app/lib/features/geckoview/features/browser/domain/services/create_tab.dart index 67a33e45..afc6dd54 100644 --- a/app/lib/features/search_browser/domain/services/create_tab.dart +++ b/app/lib/features/geckoview/features/browser/domain/services/create_tab.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:lensai/data/models/received_parameter.dart'; import 'package:lensai/features/app_widget/domain/services/home_widget.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; import 'package:lensai/features/share_intent/domain/services/sharing_intent.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/app/lib/features/search_browser/domain/services/create_tab.g.dart b/app/lib/features/geckoview/features/browser/domain/services/create_tab.g.dart similarity index 100% rename from app/lib/features/search_browser/domain/services/create_tab.g.dart rename to app/lib/features/geckoview/features/browser/domain/services/create_tab.g.dart 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 new file mode 100644 index 00000000..90a8e7c2 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.dart @@ -0,0 +1,9 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'engine_settings.g.dart'; + +@Riverpod(keepAlive: true) +GeckoEngineSettingsService engineSettingsService(EngineSettingsServiceRef ref) { + return GeckoEngineSettingsService(); +} diff --git a/app/lib/features/geckoview/domain/repositories/selected_tab.g.dart b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart similarity index 52% rename from app/lib/features/geckoview/domain/repositories/selected_tab.g.dart rename to app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart index 37efb507..efbd907e 100644 --- a/app/lib/features/geckoview/domain/repositories/selected_tab.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart @@ -1,27 +1,27 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'selected_tab.dart'; +part of 'engine_settings.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$selectedTabControllerHash() => - r'b5d79d4d5c2dc08d504e601fa29c8b83d51c9716'; +String _$engineSettingsServiceHash() => + r'9d9abb3601a3782f1f3ef91e31049db40acbf64a'; -/// See also [SelectedTabController]. -@ProviderFor(SelectedTabController) -final selectedTabControllerProvider = - NotifierProvider.internal( - SelectedTabController.new, - name: r'selectedTabControllerProvider', +/// See also [engineSettingsService]. +@ProviderFor(engineSettingsService) +final engineSettingsServiceProvider = + Provider.internal( + engineSettingsService, + name: r'engineSettingsServiceProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$selectedTabControllerHash, + : _$engineSettingsServiceHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$SelectedTabController = Notifier; +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 diff --git a/app/lib/features/search_browser/presentation/dialogs/tab_action.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart similarity index 76% rename from app/lib/features/search_browser/presentation/dialogs/tab_action.dart rename to app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart index 6f835bd6..c74bc06e 100644 --- a/app/lib/features/search_browser/presentation/dialogs/tab_action.dart +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart @@ -3,28 +3,32 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; -import 'package:lensai/features/geckoview/domain/providers.dart'; -import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; class TabActionDialog extends HookConsumerWidget { - final String tabId; + final TabState initialTab; final void Function()? onDismiss; const TabActionDialog({ - required this.tabId, + required this.initialTab, this.onDismiss, super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { - final tabState = - ref.watch(tabStateProvider(tabId)) ?? TabState.$default(tabId); + final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab; + final tabTopicId = ref.watch( + tabTopicIdProvider(initialTab.id).select((value) => value.valueOrNull), + ); + final topics = - ref.watch(topicRepositoryProvider.select((value) => value.valueOrNull)); + ref.watch(topicsWithCountProvider.select((value) => value.valueOrNull)); final selectedTopic = - topics?.firstWhereOrNull((topic) => topic.id == tabState.topicId); + topics?.firstWhereOrNull((topic) => topic.id == tabTopicId); final expansionController = useExpansionTileController(); @@ -64,17 +68,15 @@ class TabActionDialog extends HookConsumerWidget { ? Text(selectedTopic.name ?? 'New Topic') : const Text('Assign a Topic'), children: topics - ?.where((topic) => topic.id != tabState.topicId) + ?.where((topic) => topic.id != tabTopicId) .map( (topic) => ListTile( leading: CircleAvatar(backgroundColor: topic.color), title: Text(topic.name ?? 'New Topic'), onTap: () async { await ref - .read( - tabStateProvider(initialTab.id).notifier, - ) - .copyWith(topicId: topic.id); + .read(tabLinkRepositoryProvider.notifier) + .assignTab(initialTab.id, topic.id); expansionController.collapse(); }, diff --git a/app/lib/features/web_view/presentation/widgets/web_page_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart similarity index 81% rename from app/lib/features/web_view/presentation/widgets/web_page_dialog.dart rename to app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart index aea7557e..38078143 100644 --- a/app/lib/features/web_view/presentation/widgets/web_page_dialog.dart +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; - import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/data/models/web_page_info.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/providers.dart'; import 'package:lensai/features/bangs/presentation/widgets/site_search.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/utils/url_builder.dart' - as uri_builder; +import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; -import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/presentation/widgets/website_title_tile.dart'; import 'package:lensai/utils/ui_helper.dart' as ui_helper; @@ -84,27 +83,23 @@ class WebPageDialog extends HookConsumerWidget { enableIMEPersonalizedLearning: !incognitoEnabled, decoration: InputDecoration( labelText: 'URL', - suffixIcon: (webViewController != null) - ? IconButton( - onPressed: () async { - if (formKey.currentState?.validate() ?? false) { - await webViewController!.loadUrl( - urlRequest: URLRequest( - url: WebUri.uri( - uri_parser.tryParseUrl( - urlTextController.text, - eagerParsing: true, - )!, - ), - ), - ); + suffixIcon: IconButton( + onPressed: () async { + if (formKey.currentState?.validate() ?? false) { + await ref + .read(tabSessionProvider(null).notifier) + .loadUrl( + url: uri_parser.tryParseUrl( + urlTextController.text, + eagerParsing: true, + )!, + ); - onDismiss?.call(); - } - }, - icon: const Icon(Icons.send), - ) - : null, + onDismiss?.call(); + } + }, + icon: const Icon(Icons.send), + ), ), validator: (value) { if (uri_parser.tryParseUrl(value, eagerParsing: true) != @@ -174,9 +169,7 @@ class WebPageDialog extends HookConsumerWidget { leading: const Icon(MdiIcons.tabPlus), title: const Text('Open in new tab'), onTap: () async { - await ref - .read(switchNewTabControllerProvider.notifier) - .add(url); + await ref.read(tabRepositoryProvider.notifier).addTab(url: url); onDismiss?.call(); }, @@ -201,8 +194,8 @@ class WebPageDialog extends HookConsumerWidget { ); await ref - .read(switchNewTabControllerProvider.notifier) - .add(summarizerUrl); + .read(tabRepositoryProvider.notifier) + .addTab(url: summarizerUrl); onDismiss?.call(); }, diff --git a/app/lib/features/search_browser/presentation/widgets/app_bar_title.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart similarity index 100% rename from app/lib/features/search_browser/presentation/widgets/app_bar_title.dart rename to app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart diff --git a/app/lib/features/search_browser/presentation/widgets/error_container.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/error_container.dart similarity index 100% rename from app/lib/features/search_browser/presentation/widgets/error_container.dart rename to app/lib/features/geckoview/features/browser/presentation/widgets/error_container.dart diff --git a/app/lib/features/search_browser/presentation/widgets/landing/action.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/landing/action.dart similarity index 92% rename from app/lib/features/search_browser/presentation/widgets/landing/action.dart rename to app/lib/features/geckoview/features/browser/presentation/widgets/landing/action.dart index c5e33955..c9966737 100644 --- a/app/lib/features/search_browser/presentation/widgets/landing/action.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/landing/action.dart @@ -3,7 +3,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/error_container.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/error_container.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; class LandingAction extends HookConsumerWidget { diff --git a/app/lib/features/search_browser/presentation/widgets/landing/content.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/landing/content.dart similarity index 91% rename from app/lib/features/search_browser/presentation/widgets/landing/content.dart rename to app/lib/features/geckoview/features/browser/presentation/widgets/landing/content.dart index 0fe09084..dab6236f 100644 --- a/app/lib/features/search_browser/presentation/widgets/landing/content.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/landing/content.dart @@ -4,8 +4,9 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/domain/services/app_initialization.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/error_container.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/landing/action.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/error_container.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/landing/action.dart'; import 'package:lensai/presentation/hooks/cached_future.dart'; class LandingContent extends HookConsumerWidget { @@ -99,7 +100,8 @@ class LandingContent extends HookConsumerWidget { onPressed: () async { await ref .read( - appInitializationServiceProvider.notifier) + appInitializationServiceProvider.notifier, + ) .reinitialize(); }, style: OutlinedButton.styleFrom( @@ -119,8 +121,8 @@ class LandingContent extends HookConsumerWidget { onTapLink: (text, href, title) async { if (href != null) { await ref - .read(switchNewTabControllerProvider.notifier) - .add(Uri.parse(href)); + .read(tabRepositoryProvider.notifier) + .addTab(url: Uri.parse(href)); } }, selectable: true, diff --git a/app/lib/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart similarity index 89% rename from app/lib/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart rename to app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart index ddbc2c91..2c4bd774 100644 --- a/app/lib/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart @@ -2,11 +2,11 @@ import 'package:expandable_page_view/expandable_page_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/tabs/assistant_tab.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/tabs/search_tab.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/tabs/summarize_tab.dart'; +import 'package:lensai/features/kagi/presentation/tabs/assistant_tab.dart'; +import 'package:lensai/features/kagi/presentation/tabs/search_tab.dart'; +import 'package:lensai/features/kagi/presentation/tabs/summarize_tab.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; @@ -14,12 +14,12 @@ import 'package:lensai/presentation/hooks/sync_page_tab.dart'; typedef OnSubmitUri = void Function(Uri url); -class SharedContentSheet extends HookConsumerWidget { +class CreateTabSheetWidget extends HookConsumerWidget { final CreateTabSheet parameter; final OnSubmitUri onSubmit; final void Function(KagiTool tool)? onActiveToolChanged; - const SharedContentSheet({ + const CreateTabSheetWidget({ required this.parameter, required this.onSubmit, this.onActiveToolChanged, diff --git a/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart similarity index 83% rename from app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart rename to app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart index 62c480fb..8940956b 100644 --- a/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart @@ -4,10 +4,16 @@ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/tab_action.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tab_preview.dart'; +import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/providers/selected_topic.dart'; import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; import 'package:lensai/features/geckoview/features/topics/presentation/widgets/topic_chips.dart'; -import 'package:lensai/features/search_browser/presentation/dialogs/tab_action.dart'; class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { static const _headerSize = 104.0; @@ -36,8 +42,8 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { TextButton.icon( onPressed: () async { await ref - .read(switchNewTabControllerProvider.notifier) - .add(Uri.https('kagi.com')); + .read(tabRepositoryProvider.notifier) + .addTab(url: Uri.https('kagi.com')); onClose(); }, @@ -48,8 +54,8 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { onPressed: () async { final topic = ref.read(selectedTopicProvider); await ref - .read(topicTabRepositoryProvider(topic).notifier) - .closeAllTabs(); + .read(tabLinkRepositoryProvider.notifier) + .closeAllTabs(topic); onClose(); }, @@ -79,11 +85,11 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { false; } -class ViewTabsSheet extends HookConsumerWidget { +class ViewTabsSheetWidget extends HookConsumerWidget { final ScrollController sheetScrollController; final VoidCallback onClose; - const ViewTabsSheet({ + const ViewTabsSheetWidget({ required this.onClose, required this.sheetScrollController, super.key, @@ -127,10 +133,10 @@ class ViewTabsSheet extends HookConsumerWidget { builder: (context, ref, child) { final topic = ref.watch(selectedTopicProvider); final availableTabs = ref.watch( - topicTabRepositoryProvider(topic) + topicTabIdsProvider(topic) .select((value) => value.valueOrNull ?? []), ); - final activeTab = ref.watch(webViewTabControllerProvider); + final activeTab = ref.watch(selectedTabProvider); final itemHeight = useMemoized( () => _calculateItemHeight( @@ -185,19 +191,18 @@ class ViewTabsSheet extends HookConsumerWidget { final tab = ref.watch(tabStateProvider(tabId)); return (tab != null) - ? WebViewTab( + ? TabPreview( tab: tab, isActive: tabId == activeTab, - onTap: () { + onTap: () async { if (tabId != activeTab) { //Close first to avoid rebuilds onClose(); - ref + await ref .read( - webViewTabControllerProvider - .notifier, + tabRepositoryProvider.notifier, ) - .showTab(tab.id); + .selectTab(tab.id); } else { onClose(); } @@ -205,14 +210,15 @@ class ViewTabsSheet extends HookConsumerWidget { onLongPress: () { ref .read( - overlayDialogProvider.notifier, + overlayDialogControllerProvider + .notifier, ) .show( TabActionDialog( initialTab: tab, onDismiss: ref .read( - overlayDialogProvider + overlayDialogControllerProvider .notifier, ) .dismiss, @@ -224,7 +230,7 @@ class ViewTabsSheet extends HookConsumerWidget { .read( tabRepositoryProvider.notifier, ) - .deleteTab(tab.id); + .closeTab(tab.id); }, ) : const SizedBox.shrink(); diff --git a/app/lib/features/search_browser/presentation/widgets/speech_to_text_button.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart similarity index 100% rename from app/lib/features/search_browser/presentation/widgets/speech_to_text_button.dart rename to app/lib/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart diff --git a/app/lib/features/web_view/presentation/widgets/web_view_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart similarity index 84% rename from app/lib/features/web_view/presentation/widgets/web_view_tab.dart rename to app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart index a319da21..203abd5a 100644 --- a/app/lib/features/web_view/presentation/widgets/web_view_tab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/favicon.dart'; +import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; -class WebViewTab extends StatelessWidget { - final ITab tab; +class TabPreview extends StatelessWidget { + final TabState tab; final bool isActive; final VoidCallback? onTap; final VoidCallback? onLongPress; final VoidCallback? onDelete; - const WebViewTab({ + const TabPreview({ required this.tab, required this.isActive, this.onTap, @@ -46,7 +45,7 @@ class WebViewTab extends StatelessWidget { padding: const EdgeInsets.only(left: 6.0), child: Text( overflow: TextOverflow.ellipsis, - tab.title ?? 'New Tab', + tab.title, maxLines: 2, ), ), @@ -64,9 +63,10 @@ class WebViewTab extends StatelessWidget { const SizedBox( width: 6.0, ), - FaviconImage( - url: tab.url, - favicon: tab.favicon, + RawImage( + image: tab.icon?.value, + height: 16, + width: 16, ), const SizedBox( width: 6.0, @@ -82,7 +82,7 @@ class WebViewTab extends StatelessWidget { const SizedBox( height: 6, ), - if (tab.screenshot != null) + if (tab.thumbnail != null) Expanded( child: ClipRRect( borderRadius: const BorderRadius.only( @@ -91,9 +91,9 @@ class WebViewTab extends StatelessWidget { ), child: SizedBox( width: double.infinity, - child: Image.memory( + child: RawImage( + image: tab.thumbnail!.value, fit: BoxFit.fitWidth, - tab.screenshot!, ), ), ), diff --git a/app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart similarity index 88% rename from app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart rename to app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart index f4c23328..466e116b 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; class TabsActionButton extends HookConsumerWidget { final bool isActive; @@ -16,8 +16,7 @@ class TabsActionButton extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final tabCount = - ref.watch(tabRepositoryProvider.select((tabs) => tabs.length)); + final tabCount = ref.watch(tabStatesProvider.select((tabs) => tabs.length)); return InkWell( onTap: onTap, diff --git a/app/lib/features/search_browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/screens/browser.dart similarity index 65% rename from app/lib/features/search_browser/presentation/screens/browser.dart rename to app/lib/features/geckoview/features/browser/screens/browser.dart index 7fd2c767..1ff882b2 100644 --- a/app/lib/features/search_browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/screens/browser.dart @@ -1,34 +1,39 @@ import 'dart:async'; -import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; 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/routing/routes.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/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/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'; +import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart'; +import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart'; +import 'package:lensai/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart'; +import 'package:lensai/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart'; +import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_button.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; -import 'package:lensai/features/search_browser/domain/providers.dart'; -import 'package:lensai/features/search_browser/domain/services/create_tab.dart'; -import 'package:lensai/features/search_browser/presentation/controllers/bottom_sheet.dart'; -import 'package:lensai/features/search_browser/presentation/controllers/overlay_dialog.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/app_bar_title.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/landing/content.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/tabs_action_button.dart'; +import 'package:lensai/features/kagi/data/services/session.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/presentation/hooks/overlay_portal_controller.dart'; -import 'package:lensai/presentation/widgets/animate_gradient_shader.dart'; -import 'package:lensai/presentation/widgets/animated_indexed_stack.dart'; import 'package:lensai/utils/ui_helper.dart' as ui_helper; import 'package:share_plus/share_plus.dart'; import 'package:speech_to_text_google_dialog/speech_to_text_google_dialog.dart'; -class KagiScreen extends HookConsumerWidget { - const KagiScreen({super.key}); +class BrowserScreen extends HookConsumerWidget { + const BrowserScreen({super.key}); double _realtiveSafeArea(BuildContext context) { final mediaQuery = MediaQuery.of(context); @@ -60,7 +65,8 @@ class KagiScreen extends HookConsumerWidget { ), ); - final activeTabId = ref.watch(webViewTabControllerProvider); + final selectedTabId = + ref.watch(selectedTabStateProvider.select((value) => value?.id)); final menuController = useMemoized(() => MenuController()); @@ -81,12 +87,24 @@ class KagiScreen extends HookConsumerWidget { }, ); + ref.listen( + settingsRepositoryProvider + .select((value) => value.valueOrNull?.enableJavascript), + (previous, next) async { + if (next != null) { + await ref.read(engineSettingsServiceProvider).javaScriptEnabled(next); + } + }, + ); + ref.listen( settingsRepositoryProvider .select((value) => value.valueOrNull?.kagiSession), (previous, next) async { if (next != null && next.isNotEmpty) { - await ref.read(sessionServiceProvider.notifier).setKagiSession(next); + await ref + .read(kagiSessionServiceProvider.notifier) + .setKagiSession(next); } }, ); @@ -98,22 +116,28 @@ class KagiScreen extends HookConsumerWidget { child: AppBar( automaticallyImplyLeading: false, titleSpacing: 8.0, - title: (activeTabId != null) + title: (selectedTabId != null) ? Consumer( builder: (context, ref, child) { - final page = ref.watch(tabStateProvider(activeTabId)); + final tabState = ref.watch(tabStateProvider(selectedTabId)); - return (page != null) + return (tabState != null) ? AppBarTitle( - tab: page, + tab: tabState, onTap: () { - ref.read(overlayDialogProvider.notifier).show( + ref + .read( + overlayDialogControllerProvider.notifier, + ) + .show( WebPageDialog( - url: page.url, - precachedInfo: page, - webViewController: page.controller, + url: tabState.url, + precachedInfo: tabState, onDismiss: ref - .read(overlayDialogProvider.notifier) + .read( + overlayDialogControllerProvider + .notifier, + ) .dismiss, ), ); @@ -127,7 +151,7 @@ class KagiScreen extends HookConsumerWidget { final activeTool = useListenableSelector( activeKagiTool, () { - if (displayedSheet is SharedContentSheet) { + if (displayedSheet is CreateTabSheetWidget) { return null; } @@ -146,7 +170,8 @@ class KagiScreen extends HookConsumerWidget { .read(createTabStreamProvider.notifier) .createTab( CreateTabSheet( - preferredTool: KagiTool.search), + preferredTool: KagiTool.search, + ), ); }, icon: Icon(KagiTool.search.icon), @@ -160,7 +185,8 @@ class KagiScreen extends HookConsumerWidget { .read(createTabStreamProvider.notifier) .createTab( CreateTabSheet( - preferredTool: KagiTool.summarizer), + preferredTool: KagiTool.summarizer, + ), ); }, icon: Icon(KagiTool.summarizer.icon), @@ -186,93 +212,7 @@ class KagiScreen extends HookConsumerWidget { }, ), actions: [ - if (activeTabId != null) - HookConsumer( - builder: (context, ref, child) { - final colorScheme = Theme.of(context).colorScheme; - - final readerabilityState = ref.watch( - readerabilityControllerProvider(activeTabId), - ); - - final enableReadability = ref.watch( - settingsRepositoryProvider.select( - (value) => (value.valueOrNull ?? Settings.withDefaults()) - .enableReadability, - ), - ); - - final isReaderable = - readerabilityState.valueOrNull?.readerable ?? false; - - final readerableApplied = - readerabilityState.valueOrNull?.applied ?? false; - - final icon = useMemoized( - () => readerableApplied - ? Icon( - MdiIcons.bookOpen, - color: Theme.of(context).colorScheme.primary, - ) - : const Icon( - MdiIcons.bookOpenOutline, - color: Colors.white, - ), - [readerableApplied], - ); - - return Visibility( - visible: enableReadability, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 15.0, - horizontal: 8.0, - ), - child: readerabilityState.when( - data: (_) => Visibility( - visible: isReaderable == true || readerableApplied, - child: InkWell( - onTap: readerabilityState.isLoading - ? null - : () async { - final controller = ref.read( - webViewControllerProvider(activeTabId), - ); - - if (controller != null) { - final readabilityNotifier = ref.read( - readerabilityControllerProvider( - activeTabId, - ).notifier, - ); - - await readabilityNotifier - .toggleReaderable(); - } - }, - child: icon, - ), - ), - error: (error, stackTrace) => SizedBox.shrink(), - loading: () => AnimateGradientShader( - duration: const Duration(milliseconds: 500), - primaryEnd: Alignment.bottomLeft, - secondaryEnd: Alignment.topRight, - primaryColors: [ - colorScheme.primary, - colorScheme.primaryContainer, - ], - secondaryColors: [ - colorScheme.secondary, - colorScheme.secondaryContainer, - ], - child: icon, - ), - ), - ), - ); - }, - ), + if (selectedTabId != null) ReaderButton(), if (quickAction != null) InkWell( onTap: () async { @@ -321,9 +261,11 @@ class KagiScreen extends HookConsumerWidget { isActive: displayedSheet is ViewTabsSheet, onTap: () { if (displayedSheet case ViewTabsSheet()) { - ref.read(bottomSheetProvider.notifier).dismiss(); + ref.read(bottomSheetControllerProvider.notifier).dismiss(); } else { - ref.read(bottomSheetProvider.notifier).show(ViewTabsSheet()); + ref + .read(bottomSheetControllerProvider.notifier) + .show(ViewTabsSheet()); } }, ), @@ -410,63 +352,63 @@ class KagiScreen extends HookConsumerWidget { child: const Text('Search'), ), const Divider(), - if (activeTabId != null) + if (selectedTabId != null) MenuItemButton( onPressed: () async { - final controller = - ref.read(webViewControllerProvider(activeTabId)); + final tabState = + ref.read(tabStateProvider(selectedTabId)); - final url = await controller?.getUrl(); - if (url != null) { - // ignore: use_build_context_synchronously + if (tabState?.url case final Uri url) { await ui_helper.launchUrlFeedback(context, url); } }, leadingIcon: const Icon(Icons.open_in_browser), child: const Text('Launch External'), ), - if (activeTabId != null) + if (selectedTabId != null) MenuItemButton( onPressed: () async { - final controller = - ref.read(webViewControllerProvider(activeTabId)); + final tabState = + ref.read(tabStateProvider(selectedTabId)); - final url = await controller?.getUrl(); - if (url != null) { + if (tabState?.url case final Uri url) { await Share.shareUri(url); } }, leadingIcon: const Icon(Icons.share), child: const Text('Share'), ), - if (activeTabId != null) const Divider(), - if (activeTabId != null) + if (selectedTabId != null) const Divider(), + if (selectedTabId != null) MenuItemButton( - onPressed: () async { - ref.read(showFindInPageProvider.notifier).update(true); + onPressed: () { + ref + .read(findInPageVisibilityControllerProvider.notifier) + .show(); }, leadingIcon: const Icon(Icons.search), child: const Text('Find in page'), ), - if (activeTabId != null) const Divider(), - if (activeTabId != null) + if (selectedTabId != null) const Divider(), + if (selectedTabId != null) MenuItemButton( onPressed: () async { final controller = - ref.read(webViewControllerProvider(activeTabId)); + ref.read(tabSessionProvider(selectedTabId).notifier); - await controller?.reload(); + await controller.reload(); + menuController.close(); }, leadingIcon: const Icon(Icons.refresh), child: const Text('Reload'), ), - if (activeTabId != null) const Divider(), - if (activeTabId != null) + if (selectedTabId != null) const Divider(), + if (selectedTabId != null) Consumer( builder: (context, ref, child) { final history = ref.watch( - tabStateProvider(activeTabId) - .select((value) => value?.pageHistory), + tabStateProvider(selectedTabId) + .select((value) => value?.historyState), ); return Row( @@ -476,12 +418,12 @@ class KagiScreen extends HookConsumerWidget { onPressed: (history?.canGoBack == true) ? () async { final controller = ref.read( - webViewControllerProvider( - activeTabId, - ), + tabSessionProvider( + selectedTabId, + ).notifier, ); - await controller?.goBack(); + await controller.goBack(); menuController.close(); } : null, @@ -494,12 +436,12 @@ class KagiScreen extends HookConsumerWidget { onPressed: (history?.canGoForward == true) ? () async { final controller = ref.read( - webViewControllerProvider( - activeTabId, - ), + tabSessionProvider( + selectedTabId, + ).notifier, ); - await controller?.goForward(); + await controller.goForward(); menuController.close(); } : null, @@ -523,42 +465,46 @@ class KagiScreen extends HookConsumerWidget { child: Listener( onPointerDown: (displayedSheet != null) ? (_) { - ref.read(bottomSheetProvider.notifier).dismiss(); + ref.read(bottomSheetControllerProvider.notifier).dismiss(); } : null, child: HookConsumer( builder: (context, ref, child) { - final webViewWidgets = ref.watch(webViewWidgetCacheProvider); - final activeWebViewStackIndex = (activeTabId != null) - ? webViewWidgets - .indexWhere((view) => view.tabId == activeTabId) - : null; - return BackButtonListener( onBackButtonPressed: () async { - final page = (activeTabId != null) - ? ref.read(tabStateProvider(activeTabId)) + final tabState = (selectedTabId != null) + ? ref.read(tabStateProvider(selectedTabId)) : null; + final tabCount = + ref.read(tabStatesProvider.select((tabs) => tabs.length)); + //Don't do anything if a child route is active - if (GoRouterState.of(context).topRoute?.name != 'KagiRoute') { + if (GoRouterState.of(context).topRoute?.path != + BrowserRoute().location) { return false; } if (displayedSheet != null) { - ref.read(bottomSheetProvider.notifier).dismiss(); + ref.read(bottomSheetControllerProvider.notifier).dismiss(); return true; } if (displayedOverlayDialog != null) { - ref.read(overlayDialogProvider.notifier).dismiss(); + ref + .read(overlayDialogControllerProvider.notifier) + .dismiss(); return true; } - if (page?.pageHistory.canGoBack == true) { + if (tabState?.historyState.canGoBack == true) { lastBackButtonPress.value = null; - await page?.controller?.goBack(); + final controller = ref.read( + tabSessionProvider(selectedTabId).notifier, + ); + + await controller.goBack(); return true; } @@ -567,10 +513,10 @@ class KagiScreen extends HookConsumerWidget { const Duration(seconds: 2)) { lastBackButtonPress.value = null; - if (page != null && webViewWidgets.length > 1) { + if (tabState != null && tabCount > 1) { await ref - .read(tabRepositoryProvider.notifier) - .deleteTab(page.id); + .read(tabStatesProvider.notifier) + .closeTab(tabState.id); return true; } else { //Mark back as unhandled and navigator will pop @@ -582,7 +528,7 @@ class KagiScreen extends HookConsumerWidget { ..clearSnackBars() ..showSnackBar( SnackBar( - content: (webViewWidgets.length > 1) + content: (tabCount > 1) ? const Text( 'Please click BACK again to close current tab', ) @@ -598,26 +544,52 @@ class KagiScreen extends HookConsumerWidget { child: Stack( children: [ SafeArea( - child: AnimatedIndexedStack( - duration: const Duration(milliseconds: 250), - transitionBuilder: - (child, animation, secondaryAnimation) => - SharedAxisTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.horizontal, - child: child, - ), - // key: ValueKey( - // (activeTabId != null) - // ? activeWebViewStackIndex - // : null, - // ), - index: (activeWebViewStackIndex != null && - activeWebViewStackIndex > -1) - ? activeWebViewStackIndex + 1 - : 0, - children: [const LandingContent(), ...webViewWidgets], + child: Stack( + children: [ + const GeckoView(), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Consumer( + builder: (context, ref, child) { + final value = ref.watch( + selectedTabStateProvider.select( + (state) => state?.progress ?? 100, + ), + ); + + return Visibility( + visible: value < 100, + child: LinearProgressIndicator( + value: value / 100, + ), + ); + }, + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Consumer( + builder: (context, ref, child) { + final value = ref.watch( + selectedTabStateProvider.select( + (state) => EdgeInsets.only( + bottom: (state?.progress != null && + state!.progress < 100) + ? 4.0 + : 0.0, + ), + ), + ); + + return FindInPageWidget(padding: value); + }, + ), + ), + ], ), ), if (displayedSheet != null) @@ -636,7 +608,7 @@ class KagiScreen extends HookConsumerWidget { ? NotificationListener( onNotification: (notification) { if (notification.extent <= 0.1) { - ref.read(bottomSheetProvider.notifier).dismiss(); + ref.read(bottomSheetControllerProvider.notifier).dismiss(); return true; } else { ref @@ -658,10 +630,12 @@ class KagiScreen extends HookConsumerWidget { topLeft: Radius.circular(28), topRight: Radius.circular(28), ), - child: ViewTabsSheet( + child: ViewTabsSheetWidget( sheetScrollController: scrollController, onClose: () { - ref.read(bottomSheetProvider.notifier).dismiss(); + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); }, ), ); @@ -676,15 +650,17 @@ class KagiScreen extends HookConsumerWidget { builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, - child: SharedContentSheet( + child: CreateTabSheetWidget( key: ValueKey(parameter), parameter: parameter, onSubmit: (url) async { await ref - .read(switchNewTabControllerProvider.notifier) - .add(url); + .read(tabRepositoryProvider.notifier) + .addTab(url: url); - ref.read(bottomSheetProvider.notifier).dismiss(); + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); }, onActiveToolChanged: (tool) { activeKagiTool.value = tool; diff --git a/app/lib/features/search_browser/presentation/controllers/bottom_sheet.dart b/app/lib/features/geckoview/features/controllers/bottom_sheet.dart similarity index 84% rename from app/lib/features/search_browser/presentation/controllers/bottom_sheet.dart rename to app/lib/features/geckoview/features/controllers/bottom_sheet.dart index 0e5ca53b..bef1c226 100644 --- a/app/lib/features/search_browser/presentation/controllers/bottom_sheet.dart +++ b/app/lib/features/geckoview/features/controllers/bottom_sheet.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; -import 'package:lensai/features/search_browser/domain/services/create_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:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/app/lib/features/search_browser/presentation/controllers/bottom_sheet.g.dart b/app/lib/features/geckoview/features/controllers/bottom_sheet.g.dart similarity index 100% rename from app/lib/features/search_browser/presentation/controllers/bottom_sheet.g.dart rename to app/lib/features/geckoview/features/controllers/bottom_sheet.g.dart diff --git a/app/lib/features/search_browser/presentation/controllers/overlay_dialog.dart b/app/lib/features/geckoview/features/controllers/overlay_dialog.dart similarity index 100% rename from app/lib/features/search_browser/presentation/controllers/overlay_dialog.dart rename to app/lib/features/geckoview/features/controllers/overlay_dialog.dart diff --git a/app/lib/features/search_browser/presentation/controllers/overlay_dialog.g.dart b/app/lib/features/geckoview/features/controllers/overlay_dialog.g.dart similarity index 100% rename from app/lib/features/search_browser/presentation/controllers/overlay_dialog.g.dart rename to app/lib/features/geckoview/features/controllers/overlay_dialog.g.dart diff --git a/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart new file mode 100644 index 00000000..37947488 --- /dev/null +++ b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart @@ -0,0 +1,28 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'find_in_page.g.dart'; + +@Riverpod() +class FindInPageRepository extends _$FindInPageRepository { + late GeckoFindInPageService _service; + + @override + void build(String? tabId) { + _service = (tabId != null) + ? GeckoFindInPageService(tabId: tabId) + : GeckoFindInPageService.forActiveTab(); + } + + Future findAll({required String text}) { + return _service.findAll(text); + } + + Future findNext({bool forward = true}) { + return _service.findNext(forward); + } + + Future clearMatches() { + return _service.clearMatches(); + } +} diff --git a/app/lib/features/web_view/presentation/services/readerability_script.g.dart b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart similarity index 50% rename from app/lib/features/web_view/presentation/services/readerability_script.g.dart rename to app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart index dc540b77..5d22ceaa 100644 --- a/app/lib/features/web_view/presentation/services/readerability_script.g.dart +++ b/app/lib/features/geckoview/features/find_in_page/domain/repositories/find_in_page.g.dart @@ -1,13 +1,13 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'readerability_script.dart'; +part of 'find_in_page.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$readerabilityScriptServiceHash() => - r'85f0de50516cadb1f2d1efa8161a8cf45868b9d9'; +String _$findInPageRepositoryHash() => + r'a307a7169cfc559a687483962bd595d808c159bf'; /// Copied from Dart SDK class _SystemHash { @@ -30,36 +30,36 @@ class _SystemHash { } } -abstract class _$ReaderabilityScriptService - extends BuildlessAutoDisposeAsyncNotifier { - late final String tabId; +abstract class _$FindInPageRepository + extends BuildlessAutoDisposeNotifier { + late final String? tabId; - FutureOr build( - String tabId, + void build( + String? tabId, ); } -/// See also [ReaderabilityScriptService]. -@ProviderFor(ReaderabilityScriptService) -const readerabilityScriptServiceProvider = ReaderabilityScriptServiceFamily(); +/// See also [FindInPageRepository]. +@ProviderFor(FindInPageRepository) +const findInPageRepositoryProvider = FindInPageRepositoryFamily(); -/// See also [ReaderabilityScriptService]. -class ReaderabilityScriptServiceFamily extends Family> { - /// See also [ReaderabilityScriptService]. - const ReaderabilityScriptServiceFamily(); +/// See also [FindInPageRepository]. +class FindInPageRepositoryFamily extends Family { + /// See also [FindInPageRepository]. + const FindInPageRepositoryFamily(); - /// See also [ReaderabilityScriptService]. - ReaderabilityScriptServiceProvider call( - String tabId, + /// See also [FindInPageRepository]. + FindInPageRepositoryProvider call( + String? tabId, ) { - return ReaderabilityScriptServiceProvider( + return FindInPageRepositoryProvider( tabId, ); } @override - ReaderabilityScriptServiceProvider getProviderOverride( - covariant ReaderabilityScriptServiceProvider provider, + FindInPageRepositoryProvider getProviderOverride( + covariant FindInPageRepositoryProvider provider, ) { return call( provider.tabId, @@ -78,31 +78,30 @@ class ReaderabilityScriptServiceFamily extends Family> { _allTransitiveDependencies; @override - String? get name => r'readerabilityScriptServiceProvider'; + String? get name => r'findInPageRepositoryProvider'; } -/// See also [ReaderabilityScriptService]. -class ReaderabilityScriptServiceProvider - extends AutoDisposeAsyncNotifierProviderImpl { - /// See also [ReaderabilityScriptService]. - ReaderabilityScriptServiceProvider( - String tabId, +/// See also [FindInPageRepository]. +class FindInPageRepositoryProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [FindInPageRepository]. + FindInPageRepositoryProvider( + String? tabId, ) : this._internal( - () => ReaderabilityScriptService()..tabId = tabId, - from: readerabilityScriptServiceProvider, - name: r'readerabilityScriptServiceProvider', + () => FindInPageRepository()..tabId = tabId, + from: findInPageRepositoryProvider, + name: r'findInPageRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$readerabilityScriptServiceHash, - dependencies: ReaderabilityScriptServiceFamily._dependencies, + : _$findInPageRepositoryHash, + dependencies: FindInPageRepositoryFamily._dependencies, allTransitiveDependencies: - ReaderabilityScriptServiceFamily._allTransitiveDependencies, + FindInPageRepositoryFamily._allTransitiveDependencies, tabId: tabId, ); - ReaderabilityScriptServiceProvider._internal( + FindInPageRepositoryProvider._internal( super._createNotifier, { required super.name, required super.dependencies, @@ -112,11 +111,11 @@ class ReaderabilityScriptServiceProvider required this.tabId, }) : super.internal(); - final String tabId; + final String? tabId; @override - FutureOr runNotifierBuild( - covariant ReaderabilityScriptService notifier, + void runNotifierBuild( + covariant FindInPageRepository notifier, ) { return notifier.build( tabId, @@ -124,10 +123,10 @@ class ReaderabilityScriptServiceProvider } @override - Override overrideWith(ReaderabilityScriptService Function() create) { + Override overrideWith(FindInPageRepository Function() create) { return ProviderOverride( origin: this, - override: ReaderabilityScriptServiceProvider._internal( + override: FindInPageRepositoryProvider._internal( () => create()..tabId = tabId, from: from, name: null, @@ -140,14 +139,14 @@ class ReaderabilityScriptServiceProvider } @override - AutoDisposeAsyncNotifierProviderElement + AutoDisposeNotifierProviderElement createElement() { - return _ReaderabilityScriptServiceProviderElement(this); + return _FindInPageRepositoryProviderElement(this); } @override bool operator ==(Object other) { - return other is ReaderabilityScriptServiceProvider && other.tabId == tabId; + return other is FindInPageRepositoryProvider && other.tabId == tabId; } @override @@ -159,19 +158,18 @@ class ReaderabilityScriptServiceProvider } } -mixin ReaderabilityScriptServiceRef - on AutoDisposeAsyncNotifierProviderRef { +mixin FindInPageRepositoryRef on AutoDisposeNotifierProviderRef { /// The parameter `tabId` of this provider. - String get tabId; + String? get tabId; } -class _ReaderabilityScriptServiceProviderElement - extends AutoDisposeAsyncNotifierProviderElement with ReaderabilityScriptServiceRef { - _ReaderabilityScriptServiceProviderElement(super.provider); +class _FindInPageRepositoryProviderElement + extends AutoDisposeNotifierProviderElement + with FindInPageRepositoryRef { + _FindInPageRepositoryProviderElement(super.provider); @override - String get tabId => (origin as ReaderabilityScriptServiceProvider).tabId; + 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 diff --git a/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart b/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart new file mode 100644 index 00000000..daccf25c --- /dev/null +++ b/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart @@ -0,0 +1,19 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'find_in_page_visibility.g.dart'; + +@Riverpod() +class FindInPageVisibilityController extends _$FindInPageVisibilityController { + void show() { + state = true; + } + + void hide() { + state = false; + } + + @override + bool build() { + return false; + } +} 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 new file mode 100644 index 00000000..65aeaece --- /dev/null +++ b/app/lib/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'find_in_page_visibility.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$findInPageVisibilityControllerHash() => + r'365819c6b181ff2cefc022360413a918457e06d7'; + +/// See also [FindInPageVisibilityController]. +@ProviderFor(FindInPageVisibilityController) +final findInPageVisibilityControllerProvider = + AutoDisposeNotifierProvider.internal( + FindInPageVisibilityController.new, + name: r'findInPageVisibilityControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$findInPageVisibilityControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +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 diff --git a/app/lib/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart b/app/lib/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart new file mode 100644 index 00000000..00f29a48 --- /dev/null +++ b/app/lib/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; +import 'package:lensai/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart'; +import 'package:lensai/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart'; + +class FindInPageWidget extends HookConsumerWidget { + final EdgeInsetsGeometry padding; + + const FindInPageWidget({this.padding = EdgeInsets.zero}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final showFindInPage = ref.watch(findInPageVisibilityControllerProvider); + final findInteractionController = + ref.watch(findInPageRepositoryProvider(null).notifier); + + final textController = useTextEditingController(); + + return Visibility( + visible: showFindInPage, + child: Padding( + padding: padding, + child: Material( + child: Row( + children: [ + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: textController, + autofocus: true, + autocorrect: false, + decoration: const InputDecoration.collapsed( + hintText: 'Find in page', + ), + keyboardType: TextInputType.text, + onSubmitted: (value) async { + if (value == '') { + await findInteractionController.clearMatches(); + } else { + await findInteractionController.findAll(text: value); + } + }, + ), + ), + HookConsumer( + builder: (context, ref, child) { + final searchResult = ref.watch( + selectedTabStateProvider + .select((state) => state?.findResultState), + ); + + if (searchResult != null) { + if (searchResult.numberOfMatches == 0) { + return const Text('Not found'); + } + + return Text( + '${searchResult.activeMatchOrdinal + 1} of ${searchResult.numberOfMatches}', + ); + } + + return const SizedBox.shrink(); + }, + ), + IconButton( + icon: const Icon(Icons.arrow_upward), + onPressed: () async { + await findInteractionController.findNext(forward: false); + }, + ), + IconButton( + icon: const Icon(Icons.arrow_downward), + onPressed: () async { + await findInteractionController.findNext(); + }, + ), + IconButton( + icon: const Icon(Icons.clear), + onPressed: () async { + ref + .read(findInPageVisibilityControllerProvider.notifier) + .hide(); + + await findInteractionController.clearMatches(); + textController.clear(); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart new file mode 100644 index 00000000..2110e5e4 --- /dev/null +++ b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.dart @@ -0,0 +1,27 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'readerable.g.dart'; + +@Riverpod(keepAlive: true) +GeckoReaderableService readerableService(ReaderableServiceRef ref) { + final service = GeckoReaderableService.setUp(); + + ref.onDispose(() { + service.dispose(); + }); + + return service; +} + +@Riverpod() +Stream appearanceButtonVisibility(AppearanceButtonVisibilityRef ref) { + final service = ref.watch(readerableServiceProvider); + return service.appearanceVisibility; +} + +@Riverpod() +Stream readerButtonVisibility(ReaderButtonVisibilityRef ref) { + final service = ref.watch(readerableServiceProvider); + return service.readerVisibility; +} 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 new file mode 100644 index 00000000..64c024cd --- /dev/null +++ b/app/lib/features/geckoview/features/readerview/domain/providers/readerable.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'readerable.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$readerableServiceHash() => r'aad722c3869b4ce8dbe336fada733b7d102434c5'; + +/// See also [readerableService]. +@ProviderFor(readerableService) +final readerableServiceProvider = Provider.internal( + readerableService, + name: r'readerableServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$readerableServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef ReaderableServiceRef = ProviderRef; +String _$appearanceButtonVisibilityHash() => + r'996ec32ee111cfa713230a50fea3ae3a9984df7c'; + +/// See also [appearanceButtonVisibility]. +@ProviderFor(appearanceButtonVisibility) +final appearanceButtonVisibilityProvider = + AutoDisposeStreamProvider.internal( + appearanceButtonVisibility, + name: r'appearanceButtonVisibilityProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$appearanceButtonVisibilityHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef AppearanceButtonVisibilityRef = AutoDisposeStreamProviderRef; +String _$readerButtonVisibilityHash() => + r'd58132aa4e9a5ba311a0b5d99b9f8416e952b1ab'; + +/// See also [readerButtonVisibility]. +@ProviderFor(readerButtonVisibility) +final readerButtonVisibilityProvider = AutoDisposeStreamProvider.internal( + readerButtonVisibility, + name: r'readerButtonVisibilityProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$readerButtonVisibilityHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef ReaderButtonVisibilityRef = 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 diff --git a/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.dart b/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.dart new file mode 100644 index 00000000..bd465b2e --- /dev/null +++ b/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.dart @@ -0,0 +1,26 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/readerview/domain/providers/readerable.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'readerable.g.dart'; + +@Riverpod(keepAlive: true) +class ReaderableScreenController extends _$ReaderableScreenController { + late GeckoReaderableService _service; + + Future toggleReaderView(bool enable) async { + state = const AsyncValue.loading(); + final eventChange = ref.read(eventServiceProvider).readerableEvents.first; + final toggle = _service.toggleReaderView(enable); + + state = await AsyncValue.guard(() => Future.wait([toggle, eventChange])); + } + + @override + FutureOr build() { + _service = ref.watch(readerableServiceProvider); + + return null; + } +} diff --git a/app/lib/features/web_view/presentation/controllers/switch_new_tab.g.dart b/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.g.dart similarity index 50% rename from app/lib/features/web_view/presentation/controllers/switch_new_tab.g.dart rename to app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.g.dart index df257786..f9e250f6 100644 --- a/app/lib/features/web_view/presentation/controllers/switch_new_tab.g.dart +++ b/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.g.dart @@ -1,27 +1,27 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'switch_new_tab.dart'; +part of 'readerable.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$switchNewTabControllerHash() => - r'7ebfea953aeb81c279ea11254ff0ad558e6a949f'; +String _$readerableScreenControllerHash() => + r'463c032a6d07936954fead58589101ccb31c2765'; -/// See also [SwitchNewTabController]. -@ProviderFor(SwitchNewTabController) -final switchNewTabControllerProvider = - AsyncNotifierProvider.internal( - SwitchNewTabController.new, - name: r'switchNewTabControllerProvider', +/// See also [ReaderableScreenController]. +@ProviderFor(ReaderableScreenController) +final readerableScreenControllerProvider = + AsyncNotifierProvider.internal( + ReaderableScreenController.new, + name: r'readerableScreenControllerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$switchNewTabControllerHash, + : _$readerableScreenControllerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$SwitchNewTabController = AsyncNotifier; +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 diff --git a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart new file mode 100644 index 00000000..0dfd2e00 --- /dev/null +++ b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/geckoview/features/readerview/domain/providers/readerable.dart'; + +class ReaderAppearanceButton extends HookConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final buttonVisible = ref.watch( + appearanceButtonVisibilityProvider + .select((value) => value.valueOrNull ?? false), + ); + + return FloatingActionButton( + onPressed: buttonVisible + ? () async { + await ref.read(readerableServiceProvider).onAppearanceButtonTap(); + } + : null, + child: const Icon(MdiIcons.formatFont), + ); + } +} diff --git a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart new file mode 100644 index 00000000..11edb0ee --- /dev/null +++ b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; +import 'package:lensai/features/geckoview/features/readerview/domain/providers/readerable.dart'; +import 'package:lensai/features/geckoview/features/readerview/presentation/controllers/readerable.dart'; +import 'package:lensai/presentation/widgets/animate_gradient_shader.dart'; + +class ReaderButton extends HookConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + + final readerChanging = ref.watch(readerableScreenControllerProvider); + + final readerabilityState = ref.watch( + selectedTabStateProvider.select( + (state) => state?.readerableState ?? ReaderableState.$default(), + ), + ); + + final readerButtonVisible = ref.watch( + readerButtonVisibilityProvider + .select((value) => value.valueOrNull ?? false), + ); + + final icon = useMemoized( + () => readerabilityState.active + ? Icon( + MdiIcons.bookOpen, + color: Theme.of(context).colorScheme.primary, + ) + : const Icon( + MdiIcons.bookOpenOutline, + color: Colors.white, + ), + [readerabilityState.active], + ); + + return Visibility( + visible: readerButtonVisible, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 15.0, + horizontal: 8.0, + ), + child: readerChanging.when( + data: (_) => Visibility( + visible: readerButtonVisible, + child: InkWell( + onTap: readerChanging.isLoading + ? null + : () async { + await ref + .read(readerableScreenControllerProvider.notifier) + .toggleReaderView(!readerabilityState.active); + }, + child: icon, + ), + ), + error: (error, stackTrace) => SizedBox.shrink(), + loading: () => AnimateGradientShader( + duration: const Duration(milliseconds: 500), + primaryEnd: Alignment.bottomLeft, + secondaryEnd: Alignment.topRight, + primaryColors: [ + colorScheme.primary, + colorScheme.primaryContainer, + ], + secondaryColors: [ + colorScheme.secondary, + colorScheme.secondaryContainer, + ], + child: icon, + ), + ), + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/topics/data/database/daos/tab.dart b/app/lib/features/geckoview/features/topics/data/database/daos/tab.dart index ddc4b345..41980423 100644 --- a/app/lib/features/geckoview/features/topics/data/database/daos/tab.dart +++ b/app/lib/features/geckoview/features/topics/data/database/daos/tab.dart @@ -8,14 +8,22 @@ class TabLinkDao extends DatabaseAccessor with _$TabLinkDaoMixin { TabLinkDao(super.db); Selectable topicTabIds(String? topicId) { - final query = (selectOnly(db.tabLink) + final query = selectOnly(db.tabLink) ..addColumns([db.tabLink.id]) ..where( (topicId == null) ? db.tabLink.topicId.isNull() : db.tabLink.topicId.equals(topicId), ) - ..orderBy([OrderingTerm.asc(db.tabLink.id)])); + ..orderBy([OrderingTerm.asc(db.tabLink.id)]); + + return query.map((row) => row.read(db.tabLink.id)!); + } + + SingleOrNullSelectable tabTopicId(String tabId) { + final query = selectOnly(db.tabLink) + ..addColumns([db.tabLink.topicId]) + ..where(db.tabLink.id.equals(tabId)); return query.map((row) => row.read(db.tabLink.id)!); } diff --git a/app/lib/features/geckoview/features/topics/domain/providers.dart b/app/lib/features/geckoview/features/topics/domain/providers.dart index 11a6a08a..2a293158 100644 --- a/app/lib/features/geckoview/features/topics/domain/providers.dart +++ b/app/lib/features/geckoview/features/topics/domain/providers.dart @@ -9,42 +9,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; -@Riverpod(keepAlive: true) -class SelectedTopic extends _$SelectedTopic { - void setTopic(String id) { - state = id; - } - - void toggleTopic(String id) { - if (state == id) { - clearTopic(); - } else { - setTopic(id); - } - } - - void clearTopic() { - state = null; - } - - @override - String? build() { - return null; - } -} - -@Riverpod() -Stream selectedTopicData(SelectedTopicDataRef ref) { - final db = ref.watch(tabDatabaseProvider); - final selectedTopic = ref.watch(selectedTopicProvider); - - if (selectedTopic != null) { - return db.topicDao.getTopicData(selectedTopic).watchSingleOrNull(); - } - - return Stream.value(null); -} - @Riverpod() Future unusedRandomTopicColor(UnusedRandomTopicColorRef ref) async { final repository = ref.watch(topicRepositoryProvider.notifier); @@ -67,7 +31,13 @@ Stream> topicsWithCount(TopicsWithCountRef ref) { } @Riverpod() -Stream> topicTabIds(TopicTabIdsRef ref, String topicId) { +Stream> topicTabIds(TopicTabIdsRef ref, String? topicId) { final db = ref.watch(tabDatabaseProvider); return db.tabLinkDao.topicTabIds(topicId).watch(); } + +@Riverpod() +Stream tabTopicId(TabTopicIdRef ref, String tabId) { + final db = ref.watch(tabDatabaseProvider); + return db.tabLinkDao.tabTopicId(tabId).watchSingleOrNull(); +} diff --git a/app/lib/features/geckoview/features/topics/domain/providers.g.dart b/app/lib/features/geckoview/features/topics/domain/providers.g.dart index 351ff739..ea695315 100644 --- a/app/lib/features/geckoview/features/topics/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/topics/domain/providers.g.dart @@ -6,22 +6,6 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$selectedTopicDataHash() => r'fa450784052498014f592c356bde4ff5ec675281'; - -/// See also [selectedTopicData]. -@ProviderFor(selectedTopicData) -final selectedTopicDataProvider = - AutoDisposeStreamProvider.internal( - selectedTopicData, - name: r'selectedTopicDataProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$selectedTopicDataHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef SelectedTopicDataRef = AutoDisposeStreamProviderRef; String _$unusedRandomTopicColorHash() => r'099730b0d987cc37bcae4ef5c999cbebfa67d7da'; @@ -56,7 +40,7 @@ final topicsWithCountProvider = typedef TopicsWithCountRef = AutoDisposeStreamProviderRef>; -String _$topicTabIdsHash() => r'43719c70e01cf9e030d75171405ab9650936ddf8'; +String _$topicTabIdsHash() => r'f7ce2633804421a22df1efc42eb4d19d440d9fd3'; /// Copied from Dart SDK class _SystemHash { @@ -90,7 +74,7 @@ class TopicTabIdsFamily extends Family>> { /// See also [topicTabIds]. TopicTabIdsProvider call( - String topicId, + String? topicId, ) { return TopicTabIdsProvider( topicId, @@ -125,7 +109,7 @@ class TopicTabIdsFamily extends Family>> { class TopicTabIdsProvider extends AutoDisposeStreamProvider> { /// See also [topicTabIds]. TopicTabIdsProvider( - String topicId, + String? topicId, ) : this._internal( (ref) => topicTabIds( ref as TopicTabIdsRef, @@ -153,7 +137,7 @@ class TopicTabIdsProvider extends AutoDisposeStreamProvider> { required this.topicId, }) : super.internal(); - final String topicId; + final String? topicId; @override Override overrideWith( @@ -194,7 +178,7 @@ class TopicTabIdsProvider extends AutoDisposeStreamProvider> { mixin TopicTabIdsRef on AutoDisposeStreamProviderRef> { /// The parameter `topicId` of this provider. - String get topicId; + String? get topicId; } class _TopicTabIdsProviderElement @@ -202,23 +186,135 @@ class _TopicTabIdsProviderElement _TopicTabIdsProviderElement(super.provider); @override - String get topicId => (origin as TopicTabIdsProvider).topicId; + String? get topicId => (origin as TopicTabIdsProvider).topicId; } -String _$selectedTopicHash() => r'f4e1c0620971a0b7501ee9aff0d5b41da3130ba0'; +String _$tabTopicIdHash() => r'284164ac3acd9be3ba4e15eba99fc8d97602ee84'; -/// See also [SelectedTopic]. -@ProviderFor(SelectedTopic) -final selectedTopicProvider = NotifierProvider.internal( - SelectedTopic.new, - name: r'selectedTopicProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$selectedTopicHash, - dependencies: null, - allTransitiveDependencies: null, -); +/// See also [tabTopicId]. +@ProviderFor(tabTopicId) +const tabTopicIdProvider = TabTopicIdFamily(); -typedef _$SelectedTopic = Notifier; +/// See also [tabTopicId]. +class TabTopicIdFamily extends Family> { + /// See also [tabTopicId]. + const TabTopicIdFamily(); + + /// See also [tabTopicId]. + TabTopicIdProvider call( + String tabId, + ) { + return TabTopicIdProvider( + tabId, + ); + } + + @override + TabTopicIdProvider getProviderOverride( + covariant TabTopicIdProvider provider, + ) { + return call( + provider.tabId, + ); + } + + 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'tabTopicIdProvider'; +} + +/// See also [tabTopicId]. +class TabTopicIdProvider extends AutoDisposeStreamProvider { + /// See also [tabTopicId]. + TabTopicIdProvider( + String tabId, + ) : this._internal( + (ref) => tabTopicId( + ref as TabTopicIdRef, + tabId, + ), + from: tabTopicIdProvider, + name: r'tabTopicIdProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$tabTopicIdHash, + dependencies: TabTopicIdFamily._dependencies, + allTransitiveDependencies: + TabTopicIdFamily._allTransitiveDependencies, + tabId: tabId, + ); + + TabTopicIdProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.tabId, + }) : super.internal(); + + final String tabId; + + @override + Override overrideWith( + Stream Function(TabTopicIdRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: TabTopicIdProvider._internal( + (ref) => create(ref as TabTopicIdRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + tabId: tabId, + ), + ); + } + + @override + AutoDisposeStreamProviderElement createElement() { + return _TabTopicIdProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is TabTopicIdProvider && other.tabId == tabId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, tabId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin TabTopicIdRef on AutoDisposeStreamProviderRef { + /// The parameter `tabId` of this provider. + String get tabId; +} + +class _TabTopicIdProviderElement + extends AutoDisposeStreamProviderElement with TabTopicIdRef { + _TabTopicIdProviderElement(super.provider); + + @override + String get tabId => (origin as TabTopicIdProvider).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 diff --git a/app/lib/features/geckoview/features/topics/domain/providers/selected_topic.dart b/app/lib/features/geckoview/features/topics/domain/providers/selected_topic.dart new file mode 100644 index 00000000..d5d5ee3b --- /dev/null +++ b/app/lib/features/geckoview/features/topics/domain/providers/selected_topic.dart @@ -0,0 +1,41 @@ +import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart'; +import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'selected_topic.g.dart'; + +@Riverpod(keepAlive: true) +class SelectedTopic extends _$SelectedTopic { + void setTopic(String id) { + state = id; + } + + void toggleTopic(String id) { + if (state == id) { + clearTopic(); + } else { + setTopic(id); + } + } + + void clearTopic() { + state = null; + } + + @override + String? build() { + return null; + } +} + +@Riverpod() +Stream selectedTopicData(SelectedTopicDataRef ref) { + final db = ref.watch(tabDatabaseProvider); + final selectedTopic = ref.watch(selectedTopicProvider); + + if (selectedTopic != null) { + return db.topicDao.getTopicData(selectedTopic).watchSingleOrNull(); + } + + return Stream.value(null); +} diff --git a/app/lib/features/geckoview/features/topics/domain/providers/selected_topic.g.dart b/app/lib/features/geckoview/features/topics/domain/providers/selected_topic.g.dart new file mode 100644 index 00000000..895a1f8c --- /dev/null +++ b/app/lib/features/geckoview/features/topics/domain/providers/selected_topic.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'selected_topic.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$selectedTopicDataHash() => r'fa450784052498014f592c356bde4ff5ec675281'; + +/// See also [selectedTopicData]. +@ProviderFor(selectedTopicData) +final selectedTopicDataProvider = + AutoDisposeStreamProvider.internal( + selectedTopicData, + name: r'selectedTopicDataProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedTopicDataHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef SelectedTopicDataRef = AutoDisposeStreamProviderRef; +String _$selectedTopicHash() => r'f4e1c0620971a0b7501ee9aff0d5b41da3130ba0'; + +/// See also [SelectedTopic]. +@ProviderFor(SelectedTopic) +final selectedTopicProvider = NotifierProvider.internal( + SelectedTopic.new, + name: r'selectedTopicProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedTopicHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SelectedTopic = 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 diff --git a/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.dart b/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.dart index 57911cbe..15f36eea 100644 --- a/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.dart +++ b/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.dart @@ -1,5 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:lensai/features/geckoview/domain/repositories/selected_tab.dart'; +import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/topics/data/database/database.dart'; import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; @@ -17,7 +18,7 @@ class TabLinkRepository extends _$TabLinkRepository { //Sync existing tabs ref.listen( - tabRepositoryProvider.select((tabs) => tabs.keys.toList()), + tabStatesProvider.select((tabs) => tabs.keys.toList()), (previous, next) async { //No empty list to avoid data loss if (next.isNotEmpty) { @@ -27,7 +28,7 @@ class TabLinkRepository extends _$TabLinkRepository { ); ref.listen( - selectedTabControllerProvider, + selectedTabProvider, (previous, next) async { if (next != null) { await _db.tabLinkDao.touchTabLink(next, timestamp: DateTime.now()); diff --git a/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart b/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart index 55a36a6b..9e564ed1 100644 --- a/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart +++ b/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart @@ -6,7 +6,7 @@ part of 'tab_link.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabLinkRepositoryHash() => r'e85d88fadb8133c3ef1e38f0176694a12a749397'; +String _$tabLinkRepositoryHash() => r'c4944c1f6792bd725f889b1d6e12946b1439be11'; /// See also [TabLinkRepository]. @ProviderFor(TabLinkRepository) diff --git a/app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart b/app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart index 70dcf476..b42ad83c 100644 --- a/app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart +++ b/app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart'; import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/providers/selected_topic.dart'; import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart'; import 'package:lensai/features/geckoview/features/topics/presentation/widgets/topic_dialog.dart'; import 'package:skeletonizer/skeletonizer.dart'; @@ -148,7 +149,7 @@ class TopicListScreen extends HookConsumerWidget { ), body: HookConsumer( builder: (context, ref, child) { - final topicsAsync = ref.watch(topicRepositoryProvider); + final topicsAsync = ref.watch(topicsWithCountProvider); final selectedTopic = ref.watch(selectedTopicProvider); return Skeletonizer( diff --git a/app/lib/features/geckoview/features/topics/presentation/widgets/topic_chips.dart b/app/lib/features/geckoview/features/topics/presentation/widgets/topic_chips.dart index eafb1ed3..e33fb8a6 100644 --- a/app/lib/features/geckoview/features/topics/presentation/widgets/topic_chips.dart +++ b/app/lib/features/geckoview/features/topics/presentation/widgets/topic_chips.dart @@ -3,13 +3,13 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; -import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/providers/selected_topic.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; class TopicChips extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final topicsAsync = ref.watch(topicRepositoryProvider); + final topicsAsync = ref.watch(topicsWithCountProvider); final selectedTopic = ref .watch(selectedTopicDataProvider.select((value) => value.valueOrNull)); diff --git a/app/lib/features/geckoview/features/topics/utils/color_palette.dart b/app/lib/features/geckoview/features/topics/utils/color_palette.dart index 07bd9487..d38eca6b 100644 --- a/app/lib/features/geckoview/features/topics/utils/color_palette.dart +++ b/app/lib/features/geckoview/features/topics/utils/color_palette.dart @@ -18,9 +18,9 @@ bool useWhiteForeground(Color backgroundColor, {double bias = 0.0}) { // New: final v = sqrt( - pow(backgroundColor.red, 2) * 0.299 + - pow(backgroundColor.green, 2) * 0.587 + - pow(backgroundColor.blue, 2) * 0.114, + pow(backgroundColor.r, 2) * 0.299 + + pow(backgroundColor.g, 2) * 0.587 + + pow(backgroundColor.b, 2) * 0.114, ).round(); return v < (130 + bias); } diff --git a/app/lib/features/search_browser/presentation/widgets/tabs/assistant_tab.dart b/app/lib/features/kagi/presentation/tabs/assistant_tab.dart similarity index 95% rename from app/lib/features/search_browser/presentation/widgets/tabs/assistant_tab.dart rename to app/lib/features/kagi/presentation/tabs/assistant_tab.dart index ec1c51a4..0c05d3e1 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs/assistant_tab.dart +++ b/app/lib/features/kagi/presentation/tabs/assistant_tab.dart @@ -3,12 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/domain/providers.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/speech_to_text_button.dart'; -import 'package:lensai/features/search_browser/utils/url_builder.dart' - as uri_builder; +import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; diff --git a/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart b/app/lib/features/kagi/presentation/tabs/search_tab.dart similarity index 96% rename from app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart rename to app/lib/features/kagi/presentation/tabs/search_tab.dart index f9917084..716605da 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart +++ b/app/lib/features/kagi/presentation/tabs/search_tab.dart @@ -8,9 +8,9 @@ import 'package:lensai/features/bangs/domain/providers.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; import 'package:lensai/features/bangs/presentation/widgets/search_field.dart'; -import 'package:lensai/features/search_browser/domain/providers.dart'; -import 'package:lensai/features/search_browser/presentation/controllers/bottom_sheet.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart'; +import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart'; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; diff --git a/app/lib/features/search_browser/presentation/widgets/tabs/summarize_tab.dart b/app/lib/features/kagi/presentation/tabs/summarize_tab.dart similarity index 96% rename from app/lib/features/search_browser/presentation/widgets/tabs/summarize_tab.dart rename to app/lib/features/kagi/presentation/tabs/summarize_tab.dart index aac8f6e0..c794860d 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs/summarize_tab.dart +++ b/app/lib/features/kagi/presentation/tabs/summarize_tab.dart @@ -6,10 +6,9 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_pdf_text/flutter_pdf_text.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart'; -import 'package:lensai/features/search_browser/utils/url_builder.dart' - as uri_builder; +import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; diff --git a/app/lib/features/search_browser/utils/url_builder.dart b/app/lib/features/kagi/utils/url_builder.dart similarity index 100% rename from app/lib/features/search_browser/utils/url_builder.dart rename to app/lib/features/kagi/utils/url_builder.dart diff --git a/app/lib/features/settings/utils/preference_parser.dart b/app/lib/features/settings/utils/preference_parser.dart index 674997cd..fdb66db5 100644 --- a/app/lib/features/settings/utils/preference_parser.dart +++ b/app/lib/features/settings/utils/preference_parser.dart @@ -8,7 +8,7 @@ Set? parseHostSources(List? input) => input (list) => HostSource.values.firstWhereOrNull((source) => source.name == list), ) - .whereNotNull() + .nonNulls .toSet(); ThemeMode? parseThemeMode(int? index) { diff --git a/app/lib/features/web_view/domain/entities/abstract/tab.dart b/app/lib/features/web_view/domain/entities/abstract/tab.dart deleted file mode 100644 index f7909508..00000000 --- a/app/lib/features/web_view/domain/entities/abstract/tab.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:drift/drift.dart'; - -abstract interface class ITab { - String get id; - Uri get url; - String? get title; - String? get topicId; - Favicon? get favicon; - Uint8List? get screenshot; -} diff --git a/app/lib/features/web_view/domain/entities/consistent_controller.dart b/app/lib/features/web_view/domain/entities/consistent_controller.dart deleted file mode 100644 index 9631d866..00000000 --- a/app/lib/features/web_view/domain/entities/consistent_controller.dart +++ /dev/null @@ -1,16 +0,0 @@ -class ConsistentController { - final InAppWebViewController? value; - - const ConsistentController(this.value); - - @override - // ignore: avoid_dynamic_calls - int get hashCode => value?.platform.id.hashCode ?? -1; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! ConsistentController) return false; - return hashCode == other.hashCode; - } -} diff --git a/app/lib/features/web_view/domain/entities/web_view_page.dart b/app/lib/features/web_view/domain/entities/web_view_page.dart deleted file mode 100644 index 25d04a9c..00000000 --- a/app/lib/features/web_view/domain/entities/web_view_page.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:fast_equatable/fast_equatable.dart'; -import 'package:flutter/foundation.dart'; - -import 'package:lensai/core/uuid.dart'; -import 'package:lensai/data/models/web_page_info.dart'; - -part 'web_view_page.g.dart'; - -typedef PageHistory = ({bool canGoBack, bool canGoForward}); - -@CopyWith() -class WebViewPage extends WebPageInfo with FastEquatable implements ITab { - @override - @CopyWithField(immutable: true) - final String id; - - final InAppWebViewController? controller; - - // ignore: missing_field_in_equatable_props - final SslError? sslError; - - @override - final String? topicId; - - @override - final Uint8List? screenshot; - - final PageHistory pageHistory; - - WebViewPage({ - required this.id, - required this.controller, - required super.url, - required this.sslError, - required super.title, - required this.topicId, - required super.favicon, - required this.screenshot, - required this.pageHistory, - }); - - WebViewPage.create({ - String? id, - this.controller, - required super.url, - this.sslError, - super.title, - this.topicId, - super.favicon, - this.screenshot, - this.pageHistory = (canGoBack: false, canGoForward: false), - }) : id = id ?? uuid.v7(); - - @override - bool get cacheHash => true; - - @override - List get hashParameters => [ - id, - controller, - super.url, - sslError?.toString(), - title, - topicId, - favicon?.toString(), - screenshot, - pageHistory, - ]; -} diff --git a/app/lib/features/web_view/domain/entities/web_view_page.g.dart b/app/lib/features/web_view/domain/entities/web_view_page.g.dart deleted file mode 100644 index ab3ea306..00000000 --- a/app/lib/features/web_view/domain/entities/web_view_page.g.dart +++ /dev/null @@ -1,138 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'web_view_page.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$WebViewPageCWProxy { - WebViewPage controller(InvalidType controller); - - WebViewPage url(Uri url); - - WebViewPage sslError(InvalidType sslError); - - WebViewPage title(String? title); - - WebViewPage topicId(String? topicId); - - WebViewPage favicon(BrowserIcon? favicon); - - WebViewPage screenshot(Uint8List? screenshot); - - WebViewPage pageHistory(({bool canGoBack, bool canGoForward}) pageHistory); - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebViewPage(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// WebViewPage(...).copyWith(id: 12, name: "My name") - /// ```` - WebViewPage call({ - InvalidType? controller, - Uri? url, - InvalidType? sslError, - String? title, - String? topicId, - BrowserIcon? favicon, - Uint8List? screenshot, - ({bool canGoBack, bool canGoForward})? pageHistory, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfWebViewPage.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfWebViewPage.copyWith.fieldName(...)` -class _$WebViewPageCWProxyImpl implements _$WebViewPageCWProxy { - const _$WebViewPageCWProxyImpl(this._value); - - final WebViewPage _value; - - @override - WebViewPage controller(InvalidType controller) => - this(controller: controller); - - @override - WebViewPage url(Uri url) => this(url: url); - - @override - WebViewPage sslError(InvalidType sslError) => this(sslError: sslError); - - @override - WebViewPage title(String? title) => this(title: title); - - @override - WebViewPage topicId(String? topicId) => this(topicId: topicId); - - @override - WebViewPage favicon(BrowserIcon? favicon) => this(favicon: favicon); - - @override - WebViewPage screenshot(Uint8List? screenshot) => this(screenshot: screenshot); - - @override - WebViewPage pageHistory(({bool canGoBack, bool canGoForward}) pageHistory) => - this(pageHistory: pageHistory); - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebViewPage(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// WebViewPage(...).copyWith(id: 12, name: "My name") - /// ```` - WebViewPage call({ - Object? controller = const $CopyWithPlaceholder(), - Object? url = const $CopyWithPlaceholder(), - Object? sslError = const $CopyWithPlaceholder(), - Object? title = const $CopyWithPlaceholder(), - Object? topicId = const $CopyWithPlaceholder(), - Object? favicon = const $CopyWithPlaceholder(), - Object? screenshot = const $CopyWithPlaceholder(), - Object? pageHistory = const $CopyWithPlaceholder(), - }) { - return WebViewPage( - id: _value.id, - controller: - controller == const $CopyWithPlaceholder() || controller == null - ? _value.controller - // ignore: cast_nullable_to_non_nullable - : controller as InvalidType, - url: url == const $CopyWithPlaceholder() || url == null - ? _value.url - // ignore: cast_nullable_to_non_nullable - : url as Uri, - sslError: sslError == const $CopyWithPlaceholder() || sslError == null - ? _value.sslError - // ignore: cast_nullable_to_non_nullable - : sslError as InvalidType, - title: title == const $CopyWithPlaceholder() - ? _value.title - // ignore: cast_nullable_to_non_nullable - : title as String?, - topicId: topicId == const $CopyWithPlaceholder() - ? _value.topicId - // ignore: cast_nullable_to_non_nullable - : topicId as String?, - favicon: favicon == const $CopyWithPlaceholder() - ? _value.favicon - // ignore: cast_nullable_to_non_nullable - : favicon as BrowserIcon?, - screenshot: screenshot == const $CopyWithPlaceholder() - ? _value.screenshot - // ignore: cast_nullable_to_non_nullable - : screenshot as Uint8List?, - pageHistory: - pageHistory == const $CopyWithPlaceholder() || pageHistory == null - ? _value.pageHistory - // ignore: cast_nullable_to_non_nullable - : pageHistory as ({bool canGoBack, bool canGoForward}), - ); - } -} - -extension $WebViewPageCopyWith on WebViewPage { - /// Returns a callable class that can be used as follows: `instanceOfWebViewPage.copyWith(...)` or like so:`instanceOfWebViewPage.copyWith.fieldName(...)`. - // ignore: library_private_types_in_public_api - _$WebViewPageCWProxy get copyWith => _$WebViewPageCWProxyImpl(this); -} diff --git a/app/lib/features/web_view/domain/providers.dart b/app/lib/features/web_view/domain/providers.dart deleted file mode 100644 index 02922b64..00000000 --- a/app/lib/features/web_view/domain/providers.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart' show rootBundle; -import 'package:lensai/features/content_block/domain/repositories/host.dart'; -import 'package:lensai/features/settings/data/models/settings.dart'; -import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; -import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; -import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:universal_io/io.dart'; - -part 'providers.g.dart'; - -@Riverpod(keepAlive: true) -Stream?> blockContentHosts(BlockContentHostsRef ref) { - final hostRepository = ref.watch(hostRepositoryProvider.notifier); - final enableContentBlocking = ref.watch( - settingsRepositoryProvider.select( - (value) => - (value.valueOrNull ?? Settings.withDefaults()).enableContentBlocking, - ), - ); - final enableHostList = ref.watch( - settingsRepositoryProvider.select( - (value) => (value.valueOrNull ?? Settings.withDefaults()).enableHostList, - ), - ); - - if (!enableContentBlocking || enableHostList.isEmpty) { - return Stream.value(null); - } - - return hostRepository - .watchHosts(sources: enableHostList) - .map((hosts) => hosts.toSet()); -} - -@Riverpod(keepAlive: true) -Future readerabilityScript(ReaderabilityScriptRef ref) { - return rootBundle.load('assets/scripts/readability.min.js.gz').then( - (value) => compute( - (message) => utf8.decode(gzip.decode(message)), - value.buffer.asUint8List(), - ), - ); -} - -@Riverpod() -List activeTabs(ActiveTabsRef ref) { - final topic = ref.watch(selectedTopicProvider); - return ref.watch( - topicTabRepositoryProvider(topic) - .select((value) => value.valueOrNull ?? []), - ); -} diff --git a/app/lib/features/web_view/domain/providers.g.dart b/app/lib/features/web_view/domain/providers.g.dart deleted file mode 100644 index 21429702..00000000 --- a/app/lib/features/web_view/domain/providers.g.dart +++ /dev/null @@ -1,55 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'providers.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$blockContentHostsHash() => r'41855093d8376152244ae37f98e66eea7c74aa32'; - -/// See also [blockContentHosts]. -@ProviderFor(blockContentHosts) -final blockContentHostsProvider = StreamProvider?>.internal( - blockContentHosts, - name: r'blockContentHostsProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$blockContentHostsHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef BlockContentHostsRef = StreamProviderRef?>; -String _$readerabilityScriptHash() => - r'6fdd1aa920bc58c35ef9888158e572e25793870a'; - -/// See also [readerabilityScript]. -@ProviderFor(readerabilityScript) -final readerabilityScriptProvider = FutureProvider.internal( - readerabilityScript, - name: r'readerabilityScriptProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$readerabilityScriptHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef ReaderabilityScriptRef = FutureProviderRef; -String _$activeTabsHash() => r'd1ef2ae32d8707f7cc0902dd031e8302a63f50d4'; - -/// See also [activeTabs]. -@ProviderFor(activeTabs) -final activeTabsProvider = AutoDisposeProvider>.internal( - activeTabs, - name: r'activeTabsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$activeTabsHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef ActiveTabsRef = 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 diff --git a/app/lib/features/web_view/domain/repositories/web_view.dart b/app/lib/features/web_view/domain/repositories/web_view.dart deleted file mode 100644 index 9dba3361..00000000 --- a/app/lib/features/web_view/domain/repositories/web_view.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:drift/drift.dart'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/core/logger.dart'; -import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; -import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; -import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; -import 'package:lensai/features/web_view/domain/entities/consistent_controller.dart'; -import 'package:lensai/features/web_view/domain/entities/web_view_page.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'web_view.g.dart'; - -abstract class _TabStateCopyProxy { - Future copyWith({ - InAppWebViewController? controller, - Uri? url, - SslError? sslError, - String? title, - String? topicId, - Favicon? favicon, - Uint8List? screenshot, - ({bool canGoBack, bool canGoForward})? pageHistory, - }); -} - -@Riverpod() -class WebViewTabController extends _$WebViewTabController { - String? _requestedTab; - - @override - String? build() { - final webViewTabs = ref.watch( - tabRepositoryProvider.select( - (value) => value.value ?? [], - ), - ); - - if (_requestedTab != null) { - if (webViewTabs.any((tab) => tab.id == _requestedTab)) { - return _requestedTab; - } else if (webViewTabs.isNotEmpty) { - final topic = ref.read(selectedTopicProvider); - return webViewTabs.where((tab) => tab.topicId == topic).lastOrNull?.id; - } - } - - return null; - } - - void showTab(String? id) { - _requestedTab = id; - ref.invalidateSelf(); - } -} - -@Riverpod() -class TabState extends _$TabState implements _TabStateCopyProxy { - KeepAliveLink? _aliveLink; - - @override - WebViewPage? build(String tabId) { - final tabAsync = ref.watch(tabDataProvider(tabId)); - - if (tabAsync.hasValue) { - final tab = tabAsync.valueOrNull; - - final current = stateOrNull ?? - WebViewPage.create( - id: tabId, - url: tab?.url ?? Uri.https('localhost'), - ); - - if (tab != null) { - _aliveLink ??= ref.keepAlive(); - - return current.copyWith( - topicId: tab.topicId, - url: tab.url, - title: tab.title, - screenshot: tab.screenshot, - ); - } else { - _aliveLink?.close(); - _aliveLink = null; - - return current; - } - } - - return null; - } - - Future updateScreenshot() async { - final screenshot = await state?.controller - ?.takeScreenshot( - screenshotConfiguration: ScreenshotConfiguration( - compressFormat: CompressFormat.JPEG, - quality: 20, - ), - ) - .timeout( - const Duration(milliseconds: 1500), - onTimeout: () { - logger.w('Screenshot timed out'); - return null; - }, - ); - - await copyWith(screenshot: screenshot); - } - - @override - Future copyWith({ - Object? controller = const $CopyWithPlaceholder(), - Object? url = const $CopyWithPlaceholder(), - Object? sslError = const $CopyWithPlaceholder(), - Object? title = const $CopyWithPlaceholder(), - Object? topicId = const $CopyWithPlaceholder(), - Object? favicon = const $CopyWithPlaceholder(), - Object? screenshot = const $CopyWithPlaceholder(), - Object? pageHistory = const $CopyWithPlaceholder(), - }) async { - if (state != null) { - final current = state!; - - state = WebViewPage( - id: current.id, - controller: controller == const $CopyWithPlaceholder() - ? current.controller - : controller as InAppWebViewController?, - url: current.url, - sslError: sslError == const $CopyWithPlaceholder() - ? current.sslError - : sslError as SslError?, - title: current.title, - topicId: current.topicId, - favicon: favicon == const $CopyWithPlaceholder() - ? current.favicon - : favicon as Favicon?, - screenshot: current.screenshot, - pageHistory: - pageHistory == const $CopyWithPlaceholder() || pageHistory == null - ? current.pageHistory - : pageHistory as ({bool canGoBack, bool canGoForward}), - ); - - await ref.read(tabRepositoryProvider.notifier).updateTab( - current.id, - url: url == const $CopyWithPlaceholder() || url == null - ? const Value.absent() - : Value(url as Uri), - title: title == const $CopyWithPlaceholder() - ? const Value.absent() - : Value(title as String?), - topicId: topicId == const $CopyWithPlaceholder() - ? const Value.absent() - : Value(topicId as String?), - screenshot: screenshot == const $CopyWithPlaceholder() - ? const Value.absent() - : Value(screenshot as Uint8List?), - ); - } - } -} - -@Riverpod() -InAppWebViewController? webViewController( - WebViewControllerRef ref, - String tabId, -) { - return ref - .watch( - tabStateProvider(tabId) - .select((value) => ConsistentController(value?.controller)), - ) - .value; -} diff --git a/app/lib/features/web_view/domain/repositories/web_view.g.dart b/app/lib/features/web_view/domain/repositories/web_view.g.dart deleted file mode 100644 index 2d636a34..00000000 --- a/app/lib/features/web_view/domain/repositories/web_view.g.dart +++ /dev/null @@ -1,318 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'web_view.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$webViewControllerHash() => r'f35fb13d46fcb04cee5676089f0cb8d5ca21e89b'; - -/// 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)); - } -} - -/// See also [webViewController]. -@ProviderFor(webViewController) -const webViewControllerProvider = WebViewControllerFamily(); - -/// See also [webViewController]. -class WebViewControllerFamily extends Family { - /// See also [webViewController]. - const WebViewControllerFamily(); - - /// See also [webViewController]. - WebViewControllerProvider call( - String tabId, - ) { - return WebViewControllerProvider( - tabId, - ); - } - - @override - WebViewControllerProvider getProviderOverride( - covariant WebViewControllerProvider provider, - ) { - return call( - provider.tabId, - ); - } - - 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'webViewControllerProvider'; -} - -/// See also [webViewController]. -class WebViewControllerProvider - extends AutoDisposeProvider { - /// See also [webViewController]. - WebViewControllerProvider( - String tabId, - ) : this._internal( - (ref) => webViewController( - ref as WebViewControllerRef, - tabId, - ), - from: webViewControllerProvider, - name: r'webViewControllerProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$webViewControllerHash, - dependencies: WebViewControllerFamily._dependencies, - allTransitiveDependencies: - WebViewControllerFamily._allTransitiveDependencies, - tabId: tabId, - ); - - WebViewControllerProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.tabId, - }) : super.internal(); - - final String tabId; - - @override - Override overrideWith( - InAppWebViewController? Function(WebViewControllerRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: WebViewControllerProvider._internal( - (ref) => create(ref as WebViewControllerRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - tabId: tabId, - ), - ); - } - - @override - AutoDisposeProviderElement createElement() { - return _WebViewControllerProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is WebViewControllerProvider && other.tabId == tabId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, tabId.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin WebViewControllerRef on AutoDisposeProviderRef { - /// The parameter `tabId` of this provider. - String get tabId; -} - -class _WebViewControllerProviderElement - extends AutoDisposeProviderElement - with WebViewControllerRef { - _WebViewControllerProviderElement(super.provider); - - @override - String get tabId => (origin as WebViewControllerProvider).tabId; -} - -String _$webViewTabControllerHash() => - r'cf1b766aa84472040b56a7b4d9ce1fd1606ed5e1'; - -/// See also [WebViewTabController]. -@ProviderFor(WebViewTabController) -final webViewTabControllerProvider = - AutoDisposeNotifierProvider.internal( - WebViewTabController.new, - name: r'webViewTabControllerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$webViewTabControllerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$WebViewTabController = AutoDisposeNotifier; -String _$tabStateHash() => r'9ce6b0d99e5032f534c97482d6140dda4b14fb07'; - -abstract class _$TabState extends BuildlessAutoDisposeNotifier { - late final String tabId; - - WebViewPage? build( - String tabId, - ); -} - -/// See also [TabState]. -@ProviderFor(TabState) -const tabStateProvider = TabStateFamily(); - -/// See also [TabState]. -class TabStateFamily extends Family { - /// See also [TabState]. - const TabStateFamily(); - - /// See also [TabState]. - TabStateProvider call( - String tabId, - ) { - return TabStateProvider( - tabId, - ); - } - - @override - TabStateProvider getProviderOverride( - covariant TabStateProvider provider, - ) { - return call( - provider.tabId, - ); - } - - 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'tabStateProvider'; -} - -/// See also [TabState]. -class TabStateProvider - extends AutoDisposeNotifierProviderImpl { - /// See also [TabState]. - TabStateProvider( - String tabId, - ) : this._internal( - () => TabState()..tabId = tabId, - from: tabStateProvider, - name: r'tabStateProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$tabStateHash, - dependencies: TabStateFamily._dependencies, - allTransitiveDependencies: TabStateFamily._allTransitiveDependencies, - tabId: tabId, - ); - - TabStateProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.tabId, - }) : super.internal(); - - final String tabId; - - @override - WebViewPage? runNotifierBuild( - covariant TabState notifier, - ) { - return notifier.build( - tabId, - ); - } - - @override - Override overrideWith(TabState Function() create) { - return ProviderOverride( - origin: this, - override: TabStateProvider._internal( - () => create()..tabId = tabId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - tabId: tabId, - ), - ); - } - - @override - AutoDisposeNotifierProviderElement createElement() { - return _TabStateProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is TabStateProvider && other.tabId == tabId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, tabId.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin TabStateRef on AutoDisposeNotifierProviderRef { - /// The parameter `tabId` of this provider. - String get tabId; -} - -class _TabStateProviderElement - extends AutoDisposeNotifierProviderElement - with TabStateRef { - _TabStateProviderElement(super.provider); - - @override - String get tabId => (origin as TabStateProvider).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 diff --git a/app/lib/features/web_view/presentation/controllers/readerability.dart b/app/lib/features/web_view/presentation/controllers/readerability.dart deleted file mode 100644 index 2edaa3c2..00000000 --- a/app/lib/features/web_view/presentation/controllers/readerability.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; -import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; -import 'package:lensai/features/web_view/presentation/services/readerability_script.dart'; -import 'package:riverpod/riverpod.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'readerability.g.dart'; - -@Riverpod() -class ReaderabilityController extends _$ReaderabilityController { - late ReaderabilityScriptService _service; - late KeepAliveLink _aliveLink; - - @override - AsyncValue<({bool readerable, bool applied})> build( - String tabId, - ) { - _service = ref.watch(readerabilityScriptServiceProvider(tabId).notifier); - _aliveLink = ref.keepAlive(); - - ref.listen( - isTabExistingProvider(tabId), - (previous, next) { - if (next.valueOrNull == false) { - _aliveLink.close(); - } - }, - ); - - return const AsyncLoading(); - } - - Future checkReaderable() async { - final applied = state.valueOrNull?.applied ?? false; - - state = const AsyncLoading(); - final result = await AsyncValue.guard(() async { - return (readerable: await _service.isReaderable(), applied: applied); - }); - state = result; - - return result.valueOrNull?.readerable ?? false; - } - - Future toggleReaderable() async { - final applied = state.valueOrNull?.applied ?? false; - final readerable = state.valueOrNull?.readerable ?? false; - - state = const AsyncLoading(); - state = await AsyncValue.guard(() async { - if (applied) { - await ref.read(webViewControllerProvider(tabId))?.reload(); - } else { - await _service.applyReaderable(); - } - - return (readerable: readerable, applied: !applied); - }); - } - - void reset() { - state = const AsyncLoading(); - } -} diff --git a/app/lib/features/web_view/presentation/controllers/readerability.g.dart b/app/lib/features/web_view/presentation/controllers/readerability.g.dart deleted file mode 100644 index 17e86e6d..00000000 --- a/app/lib/features/web_view/presentation/controllers/readerability.g.dart +++ /dev/null @@ -1,178 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'readerability.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$readerabilityControllerHash() => - r'a1c8da3a9cf48300089e390a4fcae89862a6ec1a'; - -/// 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 _$ReaderabilityController extends BuildlessAutoDisposeNotifier< - AsyncValue<({bool readerable, bool applied})>> { - late final String tabId; - - AsyncValue<({bool readerable, bool applied})> build( - String tabId, - ); -} - -/// See also [ReaderabilityController]. -@ProviderFor(ReaderabilityController) -const readerabilityControllerProvider = ReaderabilityControllerFamily(); - -/// See also [ReaderabilityController]. -class ReaderabilityControllerFamily - extends Family> { - /// See also [ReaderabilityController]. - const ReaderabilityControllerFamily(); - - /// See also [ReaderabilityController]. - ReaderabilityControllerProvider call( - String tabId, - ) { - return ReaderabilityControllerProvider( - tabId, - ); - } - - @override - ReaderabilityControllerProvider getProviderOverride( - covariant ReaderabilityControllerProvider provider, - ) { - return call( - provider.tabId, - ); - } - - 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'readerabilityControllerProvider'; -} - -/// See also [ReaderabilityController]. -class ReaderabilityControllerProvider extends AutoDisposeNotifierProviderImpl< - ReaderabilityController, AsyncValue<({bool readerable, bool applied})>> { - /// See also [ReaderabilityController]. - ReaderabilityControllerProvider( - String tabId, - ) : this._internal( - () => ReaderabilityController()..tabId = tabId, - from: readerabilityControllerProvider, - name: r'readerabilityControllerProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$readerabilityControllerHash, - dependencies: ReaderabilityControllerFamily._dependencies, - allTransitiveDependencies: - ReaderabilityControllerFamily._allTransitiveDependencies, - tabId: tabId, - ); - - ReaderabilityControllerProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.tabId, - }) : super.internal(); - - final String tabId; - - @override - AsyncValue<({bool readerable, bool applied})> runNotifierBuild( - covariant ReaderabilityController notifier, - ) { - return notifier.build( - tabId, - ); - } - - @override - Override overrideWith(ReaderabilityController Function() create) { - return ProviderOverride( - origin: this, - override: ReaderabilityControllerProvider._internal( - () => create()..tabId = tabId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - tabId: tabId, - ), - ); - } - - @override - AutoDisposeNotifierProviderElement> createElement() { - return _ReaderabilityControllerProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is ReaderabilityControllerProvider && other.tabId == tabId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, tabId.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin ReaderabilityControllerRef on AutoDisposeNotifierProviderRef< - AsyncValue<({bool readerable, bool applied})>> { - /// The parameter `tabId` of this provider. - String get tabId; -} - -class _ReaderabilityControllerProviderElement - extends AutoDisposeNotifierProviderElement> - with ReaderabilityControllerRef { - _ReaderabilityControllerProviderElement(super.provider); - - @override - String get tabId => (origin as ReaderabilityControllerProvider).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 diff --git a/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart b/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart deleted file mode 100644 index 495bf5b3..00000000 --- a/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; -import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; -import 'package:lensai/features/web_view/domain/entities/web_view_page.dart'; -import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'switch_new_tab.g.dart'; - -@Riverpod(keepAlive: true) -class SwitchNewTabController extends _$SwitchNewTabController { - @override - FutureOr build() {} - - Future add(Uri url) async { - state = const AsyncLoading(); - state = await AsyncValue.guard( - () async { - final topic = ref.read(selectedTopicProvider); - final newTab = WebViewPage.create( - url: WebUri.uri(url), - topicId: topic, - ); - - await ref - .read(topicTabRepositoryProvider(topic).notifier) - .addTab(newTab); - - ref.read(webViewTabControllerProvider.notifier).showTab(newTab.id); - }, - ); - } -} diff --git a/app/lib/features/web_view/presentation/services/readerability_script.dart b/app/lib/features/web_view/presentation/services/readerability_script.dart deleted file mode 100644 index f1e55c80..00000000 --- a/app/lib/features/web_view/presentation/services/readerability_script.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:lensai/features/settings/data/models/settings.dart'; -import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; -import 'package:lensai/features/web_view/domain/providers.dart'; -import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'readerability_script.g.dart'; - -@Riverpod() -class ReaderabilityScriptService extends _$ReaderabilityScriptService { - late InAppWebViewController? _controller; - late Future _readerabilityScript; - late bool _enableReadability; - - @override - Future build(String tabId) async { - _controller = ref.watch(webViewControllerProvider(tabId)); - _readerabilityScript = ref.watch(readerabilityScriptProvider.future); - _enableReadability = ref.watch( - settingsRepositoryProvider.select( - (value) => - (value.valueOrNull ?? Settings.withDefaults()).enableReadability, - ), - ); - } - - Future _injectScript() async { - if (_controller != null) { - await _readerabilityScript - .then((script) => _controller!.evaluateJavascript(source: script)); - } - } - - Future _ensureScriptInjected() async { - if (_controller != null) { - final injected = await _controller!.evaluateJavascript( - source: - "(typeof window !== 'undefined' && typeof window.isReaderable === 'function')", - ) as bool; - - if (!injected) { - await _injectScript(); - } - } - } - - Future isReaderable() async { - if (_enableReadability) { - await _ensureScriptInjected(); - - if (_controller != null) { - return await _controller!.evaluateJavascript( - source: 'isReaderable();', - ) as bool; - } - } - - return false; - } - - Future applyReaderable() async { - if (_enableReadability) { - await _ensureScriptInjected(); - - if (_controller != null) { - await _controller!.evaluateJavascript(source: 'applyReaderable();'); - } - } - } -} diff --git a/app/lib/features/web_view/presentation/widgets/web_view.dart b/app/lib/features/web_view/presentation/widgets/web_view.dart index 7be3b58f..5b34647c 100644 --- a/app/lib/features/web_view/presentation/widgets/web_view.dart +++ b/app/lib/features/web_view/presentation/widgets/web_view.dart @@ -1,612 +1,612 @@ -import 'dart:async'; -import 'dart:convert'; +// import 'dart:async'; +// import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/core/logger.dart'; -import 'package:lensai/domain/services/generic_website.dart'; -import 'package:lensai/features/bangs/domain/providers.dart'; -import 'package:lensai/features/chat_archive/domain/entities/chat_entity.dart'; -import 'package:lensai/features/chat_archive/domain/repositories/archive.dart'; -import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; -import 'package:lensai/features/search_browser/domain/providers.dart'; -import 'package:lensai/features/settings/data/models/settings.dart'; -import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; -import 'package:lensai/features/web_view/domain/providers.dart'; -import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; -import 'package:lensai/features/web_view/presentation/controllers/readerability.dart'; -import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart'; -import 'package:lensai/features/web_view/presentation/widgets/web_page_dialog.dart'; -import 'package:lensai/features/web_view/utils/download_helper.dart'; -import 'package:lensai/utils/platform_util.dart' as platform_util; -import 'package:lensai/utils/ui_helper.dart' as ui_helper; -import 'package:url_launcher/url_launcher.dart'; +// import 'package:hooks_riverpod/hooks_riverpod.dart'; +// import 'package:lensai/core/logger.dart'; +// import 'package:lensai/domain/services/generic_website.dart'; +// import 'package:lensai/features/bangs/domain/providers.dart'; +// import 'package:lensai/features/chat_archive/domain/entities/chat_entity.dart'; +// import 'package:lensai/features/chat_archive/domain/repositories/archive.dart'; +// import 'package:lensai/features/kagi/data/entities/modes.dart'; +// import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; +// import 'package:lensai/features/search_browser/domain/providers.dart'; +// import 'package:lensai/features/settings/data/models/settings.dart'; +// import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; +// import 'package:lensai/features/web_view/domain/providers.dart'; +// import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; +// import 'package:lensai/features/web_view/presentation/controllers/readerability.dart'; +// import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart'; +// import 'package:lensai/features/web_view/presentation/widgets/web_page_dialog.dart'; +// import 'package:lensai/features/web_view/utils/download_helper.dart'; +// import 'package:lensai/utils/platform_util.dart' as platform_util; +// import 'package:lensai/utils/ui_helper.dart' as ui_helper; +// import 'package:url_launcher/url_launcher.dart'; -const _webViewSupportedSchemes = [ - "http", - "https", - "file", - "chrome", - "data", - "javascript", - "about", -]; +// const _webViewSupportedSchemes = [ +// "http", +// "https", +// "file", +// "chrome", +// "data", +// "javascript", +// "about", +// ]; -class WebView extends StatefulHookConsumerWidget { - final String tabId; - final URLRequest? initialUrlRequest; +// class WebView extends StatefulHookConsumerWidget { +// final String tabId; +// final URLRequest? initialUrlRequest; - const WebView({ - required this.tabId, - required super.key, - this.initialUrlRequest, - }); +// const WebView({ +// required this.tabId, +// required super.key, +// this.initialUrlRequest, +// }); - @override - ConsumerState createState() => _WebViewState(); -} +// @override +// ConsumerState createState() => _WebViewState(); +// } -class _WebViewState extends ConsumerState { - Timer? _onLoadStopDebounce; - Timer? _periodicScreenshotUpdate; +// class _WebViewState extends ConsumerState { +// Timer? _onLoadStopDebounce; +// Timer? _periodicScreenshotUpdate; - Future _downloadChat( - DownloadStartRequest downloadStartRequest, - BuildContext context, - ) async { - final fileName = getDispositionFileName( - downloadStartRequest.contentDisposition!, - ); +// Future _downloadChat( +// DownloadStartRequest downloadStartRequest, +// BuildContext context, +// ) async { +// final fileName = getDispositionFileName( +// downloadStartRequest.contentDisposition!, +// ); - if (fileName != null) { - final entity = ChatEntity.fromFileName(fileName); - if (entity.name != null) { - final fileWrite = await ref - .read(chatArchiveRepositoryProvider.notifier) - .archiveChat(fileName, downloadStartRequest.url); +// if (fileName != null) { +// final entity = ChatEntity.fromFileName(fileName); +// if (entity.name != null) { +// final fileWrite = await ref +// .read(chatArchiveRepositoryProvider.notifier) +// .archiveChat(fileName, downloadStartRequest.url); - return fileWrite.fold( - (_) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: Colors.green, - content: Text( - 'Conversation "$entity" saved successfully!', - style: const TextStyle(color: Colors.white), - ), - ), - ); +// return fileWrite.fold( +// (_) { +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// backgroundColor: Colors.green, +// content: Text( +// 'Conversation "$entity" saved successfully!', +// style: const TextStyle(color: Colors.white), +// ), +// ), +// ); - return true; - }, - onFailure: (errorMessage) { - ui_helper.showErrorMessage( - context, - errorMessage.toString(), - ); +// return true; +// }, +// onFailure: (errorMessage) { +// ui_helper.showErrorMessage( +// context, +// errorMessage.toString(), +// ); - return false; - }, - ); - } - } +// return false; +// }, +// ); +// } +// } - return false; - } +// return false; +// } - @override - Future dispose() async { - super.dispose(); +// @override +// Future dispose() async { +// super.dispose(); - _onLoadStopDebounce?.cancel(); - _periodicScreenshotUpdate?.cancel(); +// _onLoadStopDebounce?.cancel(); +// _periodicScreenshotUpdate?.cancel(); - logger.i('Disposed ${widget.key} ${widget.tabId}'); - } +// logger.i('Disposed ${widget.key} ${widget.tabId}'); +// } - @override - Widget build(BuildContext context) { - final initialSettings = useMemoized( - () => InAppWebViewSettings( - // isInspectable: kDebugMode, - useOnDownloadStart: true, - allowsLinkPreview: false, - disableLongPressContextMenuOnLinks: true, - useShouldOverrideUrlLoading: true, - javaScriptEnabled: (ref.read(settingsRepositoryProvider).valueOrNull ?? - Settings.withDefaults()) - .enableJavascript, - saveFormData: false, - disabledActionModeMenuItems: ActionModeMenuItem.MENU_ITEM_WEB_SEARCH, - ), - ); +// @override +// Widget build(BuildContext context) { +// final initialSettings = useMemoized( +// () => InAppWebViewSettings( +// // isInspectable: kDebugMode, +// useOnDownloadStart: true, +// allowsLinkPreview: false, +// disableLongPressContextMenuOnLinks: true, +// useShouldOverrideUrlLoading: true, +// javaScriptEnabled: (ref.read(settingsRepositoryProvider).valueOrNull ?? +// Settings.withDefaults()) +// .enableJavascript, +// saveFormData: false, +// disabledActionModeMenuItems: ActionModeMenuItem.MENU_ITEM_WEB_SEARCH, +// ), +// ); - final showEarlyAccessFeatures = ref.watch( - settingsRepositoryProvider.select( - (value) => (value.valueOrNull ?? Settings.withDefaults()) - .showEarlyAccessFeatures, - ), - ); +// final showEarlyAccessFeatures = ref.watch( +// settingsRepositoryProvider.select( +// (value) => (value.valueOrNull ?? Settings.withDefaults()) +// .showEarlyAccessFeatures, +// ), +// ); - // We keep listening to changes and cache them in this ref. - // This is important, since our WebView will not have the main scope anymore - // to react to updates from ref.watch - // - // With this solution we dont need to read on every interception request - // and can just obtain the value from here - final blockContentHosts = useRef?>(null); - ref.listen( - blockContentHostsProvider, - (previous, next) { - blockContentHosts.value = next.valueOrNull; - }, - ); +// // We keep listening to changes and cache them in this ref. +// // This is important, since our WebView will not have the main scope anymore +// // to react to updates from ref.watch +// // +// // With this solution we dont need to read on every interception request +// // and can just obtain the value from here +// final blockContentHosts = useRef?>(null); +// ref.listen( +// blockContentHostsProvider, +// (previous, next) { +// blockContentHosts.value = next.valueOrNull; +// }, +// ); - ref.listen( - settingsRepositoryProvider.select( - (value) => - (value.valueOrNull ?? Settings.withDefaults()).enableJavascript, - ), - (previous, next) async { - final controller = ref.read(webViewControllerProvider(widget.tabId)); - await controller?.setSettings( - settings: initialSettings.copy()..javaScriptEnabled = next, - ); - }, - ); +// ref.listen( +// settingsRepositoryProvider.select( +// (value) => +// (value.valueOrNull ?? Settings.withDefaults()).enableJavascript, +// ), +// (previous, next) async { +// final controller = ref.read(webViewControllerProvider(widget.tabId)); +// await controller?.setSettings( +// settings: initialSettings.copy()..javaScriptEnabled = next, +// ); +// }, +// ); - final webViewProgress = useValueNotifier(100); - final inPageSearchResult = - useValueNotifier<({int activeMatchOrdinal, int numberOfMatches})?>( - null, - ); +// final webViewProgress = useValueNotifier(100); +// final inPageSearchResult = +// useValueNotifier<({int activeMatchOrdinal, int numberOfMatches})?>( +// null, +// ); - final findInteractionController = useMemoized( - () => FindInteractionController( - onFindResultReceived: ( - controller, - activeMatchOrdinal, - numberOfMatches, - isDoneCounting, - ) { - if (isDoneCounting) { - inPageSearchResult.value = ( - activeMatchOrdinal: activeMatchOrdinal, - numberOfMatches: numberOfMatches, - ); - } - }, - ), - ); +// final findInteractionController = useMemoized( +// () => FindInteractionController( +// onFindResultReceived: ( +// controller, +// activeMatchOrdinal, +// numberOfMatches, +// isDoneCounting, +// ) { +// if (isDoneCounting) { +// inPageSearchResult.value = ( +// activeMatchOrdinal: activeMatchOrdinal, +// numberOfMatches: numberOfMatches, +// ); +// } +// }, +// ), +// ); - useOnAppLifecycleStateChange((previous, current) async { - final controller = ref.read(webViewControllerProvider(widget.tabId)); +// useOnAppLifecycleStateChange((previous, current) async { +// final controller = ref.read(webViewControllerProvider(widget.tabId)); - switch (current) { - case AppLifecycleState.paused: - if (platform_util.isAndroid()) { - await controller?.pause(); - } - if (platform_util.isAndroid() || platform_util.isIOS()) { - await controller?.pauseTimers(); - } - case AppLifecycleState.resumed: - if (platform_util.isAndroid()) { - await controller?.resume(); - } - if (platform_util.isAndroid() || platform_util.isIOS()) { - await controller?.resumeTimers(); - } - default: - } - }); +// switch (current) { +// case AppLifecycleState.paused: +// if (platform_util.isAndroid()) { +// await controller?.pause(); +// } +// if (platform_util.isAndroid() || platform_util.isIOS()) { +// await controller?.pauseTimers(); +// } +// case AppLifecycleState.resumed: +// if (platform_util.isAndroid()) { +// await controller?.resume(); +// } +// if (platform_util.isAndroid() || platform_util.isIOS()) { +// await controller?.resumeTimers(); +// } +// default: +// } +// }); - return Stack( - children: [ - InAppWebView( - initialUrlRequest: widget.initialUrlRequest, - initialSettings: initialSettings, - findInteractionController: findInteractionController, - contextMenu: ContextMenu( - menuItems: [ - ContextMenuItem( - id: 1, - title: "Search", - action: () async { - final controller = - ref.read(webViewControllerProvider(widget.tabId)); +// return Stack( +// children: [ +// InAppWebView( +// initialUrlRequest: widget.initialUrlRequest, +// initialSettings: initialSettings, +// findInteractionController: findInteractionController, +// contextMenu: ContextMenu( +// menuItems: [ +// ContextMenuItem( +// id: 1, +// title: "Search", +// action: () async { +// final controller = +// ref.read(webViewControllerProvider(widget.tabId)); - final selectedText = await controller?.getSelectedText(); +// final selectedText = await controller?.getSelectedText(); - if (selectedText != null && selectedText.isNotEmpty) { - final searchBang = - await ref.read(kagiSearchBangDataProvider.future); +// if (selectedText != null && selectedText.isNotEmpty) { +// final searchBang = +// await ref.read(kagiSearchBangDataProvider.future); - await ref - .read(switchNewTabControllerProvider.notifier) - .add(searchBang!.getUrl(selectedText)); - } - }, - ), - if (showEarlyAccessFeatures) - ContextMenuItem( - id: 2, - title: "Assistant", - action: () async { - final controller = - ref.read(webViewControllerProvider(widget.tabId)); +// await ref +// .read(switchNewTabControllerProvider.notifier) +// .add(searchBang!.getUrl(selectedText)); +// } +// }, +// ), +// if (showEarlyAccessFeatures) +// ContextMenuItem( +// id: 2, +// title: "Assistant", +// action: () async { +// final controller = +// ref.read(webViewControllerProvider(widget.tabId)); - final selectedText = await controller?.getSelectedText(); +// final selectedText = await controller?.getSelectedText(); - if (selectedText != null && selectedText.isNotEmpty) { - ref.read(bottomSheetProvider.notifier).show( - CreateTabSheet( - content: selectedText, - preferredTool: KagiTool.assistant, - ), - ); - } - }, - ), - ], - ), - onWebViewCreated: (controller) async { - if (platform_util.isAndroid()) { - await controller.startSafeBrowsing(); - } +// if (selectedText != null && selectedText.isNotEmpty) { +// ref.read(bottomSheetProvider.notifier).show( +// CreateTabSheet( +// content: selectedText, +// preferredTool: KagiTool.assistant, +// ), +// ); +// } +// }, +// ), +// ], +// ), +// onWebViewCreated: (controller) async { +// if (platform_util.isAndroid()) { +// await controller.startSafeBrowsing(); +// } - await ref - .read(tabStateProvider(widget.tabId).notifier) - .copyWith(controller: controller); - }, - onReceivedServerTrustAuthRequest: (controller, challenge) async { - final sslError = challenge.protectionSpace.sslError; +// await ref +// .read(tabStateProvider(widget.tabId).notifier) +// .copyWith(controller: controller); +// }, +// onReceivedServerTrustAuthRequest: (controller, challenge) async { +// final sslError = challenge.protectionSpace.sslError; - if (sslError != null && sslError.code != null) { - final url = await controller.getUrl(); - if (challenge.protectionSpace.host == url?.host) { - await ref - .read(tabStateProvider(widget.tabId).notifier) - .copyWith(sslError: sslError); +// if (sslError != null && sslError.code != null) { +// final url = await controller.getUrl(); +// if (challenge.protectionSpace.host == url?.host) { +// await ref +// .read(tabStateProvider(widget.tabId).notifier) +// .copyWith(sslError: sslError); - if (context.mounted) { - ui_helper.showErrorMessage( - context, - 'We detected an security issue and did not continue to ${url?.authority}: ${sslError.message}', - ); - } - } +// if (context.mounted) { +// ui_helper.showErrorMessage( +// context, +// 'We detected an security issue and did not continue to ${url?.authority}: ${sslError.message}', +// ); +// } +// } - return ServerTrustAuthResponse( - // ignore: avoid_redundant_argument_values - action: ServerTrustAuthResponseAction.CANCEL, - ); - } +// return ServerTrustAuthResponse( +// // ignore: avoid_redundant_argument_values +// action: ServerTrustAuthResponseAction.CANCEL, +// ); +// } - await ref - .read(tabStateProvider(widget.tabId).notifier) - .copyWith(sslError: null); +// await ref +// .read(tabStateProvider(widget.tabId).notifier) +// .copyWith(sslError: null); - return ServerTrustAuthResponse( - action: ServerTrustAuthResponseAction.PROCEED, - ); - }, - shouldInterceptRequest: (controller, request) async { - if (blockContentHosts.value?.contains(request.url.host) == true) { - return WebResourceResponse( - contentType: "text/plain", - data: utf8.encode("Blocked"), - statusCode: 403, - reasonPhrase: "Forbidden", - ); - } +// return ServerTrustAuthResponse( +// action: ServerTrustAuthResponseAction.PROCEED, +// ); +// }, +// shouldInterceptRequest: (controller, request) async { +// if (blockContentHosts.value?.contains(request.url.host) == true) { +// return WebResourceResponse( +// contentType: "text/plain", +// data: utf8.encode("Blocked"), +// statusCode: 403, +// reasonPhrase: "Forbidden", +// ); +// } - //Check if main page aka main frame is upgradeable - if (request.url.isScheme('http') && - request.isForMainFrame == true) { - final upgradedUri = await ref - .read(genericWebsiteServiceProvider.notifier) - .tryUpgradeToHttps(request.url); +// //Check if main page aka main frame is upgradeable +// if (request.url.isScheme('http') && +// request.isForMainFrame == true) { +// final upgradedUri = await ref +// .read(genericWebsiteServiceProvider.notifier) +// .tryUpgradeToHttps(request.url); - if (upgradedUri != null) { - unawaited( - controller.loadUrl( - urlRequest: URLRequest(url: WebUri.uri(upgradedUri)), - ), - ); +// if (upgradedUri != null) { +// unawaited( +// controller.loadUrl( +// urlRequest: URLRequest(url: WebUri.uri(upgradedUri)), +// ), +// ); - return WebResourceResponse( - statusCode: 200, - reasonPhrase: "Redirecting...", - ); - } - } +// return WebResourceResponse( +// statusCode: 200, +// reasonPhrase: "Redirecting...", +// ); +// } +// } - if (request.url.isScheme('http')) { - if ((ref.read(settingsRepositoryProvider).valueOrNull ?? - Settings.withDefaults()) - .blockHttpProtocol) { - return WebResourceResponse( - contentType: "text/plain", - data: utf8.encode("HTTP protocol is blocked"), - statusCode: 403, - reasonPhrase: "Forbidden", - ); - } - } +// if (request.url.isScheme('http')) { +// if ((ref.read(settingsRepositoryProvider).valueOrNull ?? +// Settings.withDefaults()) +// .blockHttpProtocol) { +// return WebResourceResponse( +// contentType: "text/plain", +// data: utf8.encode("HTTP protocol is blocked"), +// statusCode: 403, +// reasonPhrase: "Forbidden", +// ); +// } +// } - return null; - }, - onProgressChanged: (controller, progress) { - webViewProgress.value = progress; - }, - onLoadStart: (controller, url) async { - final readabilityNotifier = ref.read( - readerabilityControllerProvider(widget.tabId).notifier, - ); +// return null; +// }, +// onProgressChanged: (controller, progress) { +// webViewProgress.value = progress; +// }, +// onLoadStart: (controller, url) async { +// final readabilityNotifier = ref.read( +// readerabilityControllerProvider(widget.tabId).notifier, +// ); - if (url != null) { - await ref.read(tabStateProvider(widget.tabId).notifier).copyWith( - url: url, - // ignore: avoid_redundant_argument_values - sslError: null, - ); - } +// if (url != null) { +// await ref.read(tabStateProvider(widget.tabId).notifier).copyWith( +// url: url, +// // ignore: avoid_redundant_argument_values +// sslError: null, +// ); +// } - readabilityNotifier.reset(); - }, - onLoadStop: (controller, url) async { - if (url != null) { - await ref - .read(tabStateProvider(widget.tabId).notifier) - .copyWith(url: url); - } +// readabilityNotifier.reset(); +// }, +// onLoadStop: (controller, url) async { +// if (url != null) { +// await ref +// .read(tabStateProvider(widget.tabId).notifier) +// .copyWith(url: url); +// } - _onLoadStopDebounce?.cancel(); - _onLoadStopDebounce = - Timer(const Duration(milliseconds: 150), () async { - final readabilityNotifier = ref.read( - readerabilityControllerProvider(widget.tabId).notifier, - ); +// _onLoadStopDebounce?.cancel(); +// _onLoadStopDebounce = +// Timer(const Duration(milliseconds: 150), () async { +// final readabilityNotifier = ref.read( +// readerabilityControllerProvider(widget.tabId).notifier, +// ); - await readabilityNotifier.checkReaderable(); +// await readabilityNotifier.checkReaderable(); - await ref - .read(tabStateProvider(widget.tabId).notifier) - .updateScreenshot() - .whenComplete(() { - _periodicScreenshotUpdate?.cancel(); - _periodicScreenshotUpdate = - Timer.periodic(const Duration(seconds: 5), (timer) async { - await ref - .read(tabStateProvider(widget.tabId).notifier) - .updateScreenshot() - .onError((error, stackTrace) { - logger.e(error, stackTrace: stackTrace); - timer.cancel(); - }); - }); - }); - }); - }, - onUpdateVisitedHistory: (controller, url, isReload) async { - if (isReload != true) { - final history = ( - canGoBack: await controller.canGoBack(), - canGoForward: await controller.canGoForward() - ); +// await ref +// .read(tabStateProvider(widget.tabId).notifier) +// .updateScreenshot() +// .whenComplete(() { +// _periodicScreenshotUpdate?.cancel(); +// _periodicScreenshotUpdate = +// Timer.periodic(const Duration(seconds: 5), (timer) async { +// await ref +// .read(tabStateProvider(widget.tabId).notifier) +// .updateScreenshot() +// .onError((error, stackTrace) { +// logger.e(error, stackTrace: stackTrace); +// timer.cancel(); +// }); +// }); +// }); +// }); +// }, +// onUpdateVisitedHistory: (controller, url, isReload) async { +// if (isReload != true) { +// final history = ( +// canGoBack: await controller.canGoBack(), +// canGoForward: await controller.canGoForward() +// ); - await ref - .read(tabStateProvider(widget.tabId).notifier) - .copyWith(pageHistory: history); - } - }, - shouldOverrideUrlLoading: (controller, navigationAction) async { - final url = navigationAction.request.url; - if (url != null) { - final launchExternal = ref.read( - settingsRepositoryProvider.select( - (value) => (value.valueOrNull ?? Settings.withDefaults()) - .launchUrlExternal, - ), - ); +// await ref +// .read(tabStateProvider(widget.tabId).notifier) +// .copyWith(pageHistory: history); +// } +// }, +// shouldOverrideUrlLoading: (controller, navigationAction) async { +// final url = navigationAction.request.url; +// if (url != null) { +// final launchExternal = ref.read( +// settingsRepositoryProvider.select( +// (value) => (value.valueOrNull ?? Settings.withDefaults()) +// .launchUrlExternal, +// ), +// ); - final unhandledScheme = - !_webViewSupportedSchemes.contains(url.scheme); +// final unhandledScheme = +// !_webViewSupportedSchemes.contains(url.scheme); - if (unhandledScheme || - (launchExternal && url.host != 'kagi.com')) { - if (await canLaunchUrl(url)) { - var success = false; - if (unhandledScheme) { - success = await launchUrl(url); - } else if (launchExternal) { - success = await launchUrl( - url, - mode: LaunchMode.externalApplication, - ); - } +// if (unhandledScheme || +// (launchExternal && url.host != 'kagi.com')) { +// if (await canLaunchUrl(url)) { +// var success = false; +// if (unhandledScheme) { +// success = await launchUrl(url); +// } else if (launchExternal) { +// success = await launchUrl( +// url, +// mode: LaunchMode.externalApplication, +// ); +// } - if (!success) { - if (context.mounted) { - ui_helper.showErrorMessage( - context, - 'Could not launch URL ($url)', - ); - } - } - // and cancel the request - return NavigationActionPolicy.CANCEL; - } - } - } +// if (!success) { +// if (context.mounted) { +// ui_helper.showErrorMessage( +// context, +// 'Could not launch URL ($url)', +// ); +// } +// } +// // and cancel the request +// return NavigationActionPolicy.CANCEL; +// } +// } +// } - return NavigationActionPolicy.ALLOW; - }, - onLongPressHitTestResult: (controller, hitTestResult) async { - if (switch (hitTestResult.type) { - InAppWebViewHitTestResultType.SRC_IMAGE_ANCHOR_TYPE || - InAppWebViewHitTestResultType.SRC_ANCHOR_TYPE || - InAppWebViewHitTestResultType.IMAGE_TYPE => - true, - _ => false, - }) { - final requestFocusNodeHrefResult = - await controller.requestFocusNodeHref(); +// return NavigationActionPolicy.ALLOW; +// }, +// onLongPressHitTestResult: (controller, hitTestResult) async { +// if (switch (hitTestResult.type) { +// InAppWebViewHitTestResultType.SRC_IMAGE_ANCHOR_TYPE || +// InAppWebViewHitTestResultType.SRC_ANCHOR_TYPE || +// InAppWebViewHitTestResultType.IMAGE_TYPE => +// true, +// _ => false, +// }) { +// final requestFocusNodeHrefResult = +// await controller.requestFocusNodeHref(); - final url = requestFocusNodeHrefResult?.url ?? - Uri.tryParse(requestFocusNodeHrefResult?.src ?? ''); - if (url?.hasScheme == true && url?.hasAuthority == true) { - ref.read(overlayDialogProvider.notifier).show( - WebPageDialog( - url: url!, - onDismiss: () { - ref.read(overlayDialogProvider.notifier).dismiss(); - }, - ), - ); - } - } - }, - onTitleChanged: (controller, title) async { - await ref - .read(tabStateProvider(widget.tabId).notifier) - .copyWith(title: title); - }, - onDownloadStartRequest: (controller, downloadStartRequest) async { - final handled = switch (downloadStartRequest.mimeType) { - 'text/markdown' => - await _downloadChat(downloadStartRequest, context), - _ => false - }; +// final url = requestFocusNodeHrefResult?.url ?? +// Uri.tryParse(requestFocusNodeHrefResult?.src ?? ''); +// if (url?.hasScheme == true && url?.hasAuthority == true) { +// ref.read(overlayDialogProvider.notifier).show( +// WebPageDialog( +// url: url!, +// onDismiss: () { +// ref.read(overlayDialogProvider.notifier).dismiss(); +// }, +// ), +// ); +// } +// } +// }, +// onTitleChanged: (controller, title) async { +// await ref +// .read(tabStateProvider(widget.tabId).notifier) +// .copyWith(title: title); +// }, +// onDownloadStartRequest: (controller, downloadStartRequest) async { +// final handled = switch (downloadStartRequest.mimeType) { +// 'text/markdown' => +// await _downloadChat(downloadStartRequest, context), +// _ => false +// }; - if (!handled) { - if (context.mounted) { - await ui_helper.launchUrlFeedback( - context, - downloadStartRequest.url, - mode: LaunchMode.platformDefault, - ); - } - } +// if (!handled) { +// if (context.mounted) { +// await ui_helper.launchUrlFeedback( +// context, +// downloadStartRequest.url, +// mode: LaunchMode.platformDefault, +// ); +// } +// } - webViewProgress.value = 100; - }, - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: HookBuilder( - builder: (context) { - final value = useValueListenable(webViewProgress); +// webViewProgress.value = 100; +// }, +// ), +// Positioned( +// bottom: 0, +// left: 0, +// right: 0, +// child: HookBuilder( +// builder: (context) { +// final value = useValueListenable(webViewProgress); - return Visibility( - visible: value < 100, - child: LinearProgressIndicator( - value: value / 100, - ), - ); - }, - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: HookConsumer( - builder: (context, ref, child) { - final showFindInPage = ref.watch(showFindInPageProvider); +// return Visibility( +// visible: value < 100, +// child: LinearProgressIndicator( +// value: value / 100, +// ), +// ); +// }, +// ), +// ), +// Positioned( +// bottom: 0, +// left: 0, +// right: 0, +// child: HookConsumer( +// builder: (context, ref, child) { +// final showFindInPage = ref.watch(showFindInPageProvider); - final textController = useTextEditingController(); - final progress = useValueListenable(webViewProgress); +// final textController = useTextEditingController(); +// final progress = useValueListenable(webViewProgress); - return Visibility( - visible: showFindInPage, - child: Padding( - padding: - EdgeInsets.only(bottom: (progress < 100) ? 4.0 : 0.0), - child: Material( - child: Row( - children: [ - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: textController, - autofocus: true, - autocorrect: false, - decoration: const InputDecoration.collapsed( - hintText: 'Find in page', - ), - keyboardType: TextInputType.text, - onSubmitted: (value) async { - if (value == '') { - inPageSearchResult.value = null; - await findInteractionController.clearMatches(); - } else { - await findInteractionController.findAll( - find: value, - ); - } - }, - ), - ), - HookConsumer( - builder: (context, ref, child) { - final searchResult = - useValueListenable(inPageSearchResult); +// return Visibility( +// visible: showFindInPage, +// child: Padding( +// padding: +// EdgeInsets.only(bottom: (progress < 100) ? 4.0 : 0.0), +// child: Material( +// child: Row( +// children: [ +// const SizedBox(width: 8), +// Expanded( +// child: TextField( +// controller: textController, +// autofocus: true, +// autocorrect: false, +// decoration: const InputDecoration.collapsed( +// hintText: 'Find in page', +// ), +// keyboardType: TextInputType.text, +// onSubmitted: (value) async { +// if (value == '') { +// inPageSearchResult.value = null; +// await findInteractionController.clearMatches(); +// } else { +// await findInteractionController.findAll( +// find: value, +// ); +// } +// }, +// ), +// ), +// HookConsumer( +// builder: (context, ref, child) { +// final searchResult = +// useValueListenable(inPageSearchResult); - if (searchResult != null) { - if (searchResult.numberOfMatches == 0) { - return const Text('Not found'); - } +// if (searchResult != null) { +// if (searchResult.numberOfMatches == 0) { +// return const Text('Not found'); +// } - return Text( - '${searchResult.activeMatchOrdinal + 1} of ${searchResult.numberOfMatches}', - ); - } +// return Text( +// '${searchResult.activeMatchOrdinal + 1} of ${searchResult.numberOfMatches}', +// ); +// } - return const SizedBox.shrink(); - }, - ), - IconButton( - icon: const Icon(Icons.arrow_upward), - onPressed: () async { - await findInteractionController.findNext( - forward: false, - ); - }, - ), - IconButton( - icon: const Icon(Icons.arrow_downward), - onPressed: () async { - await findInteractionController.findNext(); - }, - ), - IconButton( - icon: const Icon(Icons.clear), - onPressed: () async { - ref - .read(showFindInPageProvider.notifier) - .update(false); +// return const SizedBox.shrink(); +// }, +// ), +// IconButton( +// icon: const Icon(Icons.arrow_upward), +// onPressed: () async { +// await findInteractionController.findNext( +// forward: false, +// ); +// }, +// ), +// IconButton( +// icon: const Icon(Icons.arrow_downward), +// onPressed: () async { +// await findInteractionController.findNext(); +// }, +// ), +// IconButton( +// icon: const Icon(Icons.clear), +// onPressed: () async { +// ref +// .read(showFindInPageProvider.notifier) +// .update(false); - inPageSearchResult.value = null; - await findInteractionController.clearMatches(); - textController.clear(); - }, - ), - ], - ), - ), - ), - ); - }, - ), - ), - ], - ); - } -} +// inPageSearchResult.value = null; +// await findInteractionController.clearMatches(); +// textController.clear(); +// }, +// ), +// ], +// ), +// ), +// ), +// ); +// }, +// ), +// ), +// ], +// ); +// } +// } diff --git a/app/lib/presentation/widgets/website_title_tile.dart b/app/lib/presentation/widgets/website_title_tile.dart index f0753d43..012492df 100644 --- a/app/lib/presentation/widgets/website_title_tile.dart +++ b/app/lib/presentation/widgets/website_title_tile.dart @@ -20,10 +20,10 @@ class WebsiteTitleTile extends HookConsumerWidget { child: pageInfoAsync.when( data: (info) { return ListTile( - leading: FaviconImage( - favicon: info.favicon, - url: info.url, - size: 24, + leading: RawImage( + image: info.favicon?.image.value, + height: 24, + width: 24, ), contentPadding: EdgeInsets.zero, title: Text(info.title ?? 'Unknown Title'), @@ -38,10 +38,10 @@ class WebsiteTitleTile extends HookConsumerWidget { }, loading: () => (precachedInfo != null) ? ListTile( - leading: FaviconImage( - favicon: precachedInfo!.favicon, - url: precachedInfo!.url, - size: 24, + leading: RawImage( + image: precachedInfo!.favicon?.image.value, + height: 24, + width: 24, ), contentPadding: EdgeInsets.zero, title: Text(precachedInfo!.title ?? 'Unknown Title'), diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus b/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus index e2873900..d7600695 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus @@ -1 +1 @@ -/home/fafre/.pub-cache/hosted/pub.dev/package_info_plus-8.0.2/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/package_info_plus-8.0.3/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/share_plus b/app/linux/flutter/ephemeral/.plugin_symlinks/share_plus index 44a0fca4..68934bd1 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.0.2/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/share_plus-10.0.3/ \ No newline at end of file diff --git a/app/pubspec.lock b/app/pubspec.lock index 5e9860ca..e374d665 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "73.0.0" _macros: dependency: transitive description: dart @@ -18,10 +18,10 @@ packages: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.8.0" analyzer_plugin: dependency: transitive description: @@ -106,10 +106,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.13" build_runner_core: dependency: transitive description: @@ -250,18 +250,18 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "4939d89e580c36215e48a7de8fd92f22c79dcc3eb11fda84f3402b3b45aec663" + sha256: "832fcdc676171205201c9cffafd6b5add19393962f6598af8472b48b413026e6" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.8" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: d9e5bb63ed52c1d006f5a1828992ba6de124c27a531e8fba0a31afffa81621b3 + sha256: c3d82779026f91b8e00c9ac18934595cbc9b490094ea682052beeafdb2bd50ac url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.8" custom_lint_core: dependency: transitive description: @@ -274,34 +274,34 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" dependency_validator: dependency: "direct dev" description: name: dependency_validator - sha256: "16906f2137a312d0ffe754aee156436563a63d6ab7faaded83d401b5385d5d35" + sha256: "81b5dc4cc34a1c05d2fa24aa8d658cb8f048ca23e63d5aaec420200190f1c4b0" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" drift: dependency: "direct main" description: name: drift - sha256: "15b51e0ee1970455c0c3f7e560f3ac02ecb9c04711a9657586e470b234659dba" + sha256: d6ff1ec6a0f3fa097dda6b776cf601f1f3d88b53b287288e09c1306f394fb1b3 url: "https://pub.dev" source: hosted - version: "2.20.0" + version: "2.20.3" drift_dev: dependency: "direct dev" description: name: drift_dev - sha256: b9ec6159a731288e805a44225ccbebad507dd84d52ab71352c52584f13199d2d + sha256: "3ee987578ca2281b5ff91eadd757cd6dd36001458d6e33784f990d67ff38f756" url: "https://pub.dev" source: hosted - version: "2.20.1" + version: "2.20.3" dynamic_color: dependency: "direct main" description: @@ -370,10 +370,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" file_picker: dependency: "direct main" description: @@ -407,18 +407,18 @@ packages: dependency: "direct main" description: name: flutter_inappwebview - sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.5" flutter_inappwebview_android: dependency: transitive description: name: flutter_inappwebview_android - sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" url: "https://pub.dev" source: hosted - version: "1.0.13" + version: "1.1.3" flutter_inappwebview_internal_annotations: dependency: transitive description: @@ -431,50 +431,58 @@ packages: dependency: transitive description: name: flutter_inappwebview_ios - sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" url: "https://pub.dev" source: hosted - version: "1.0.13" + version: "1.1.2" flutter_inappwebview_macos: dependency: transitive description: name: flutter_inappwebview_macos - sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "1.1.2" flutter_inappwebview_platform_interface: dependency: transitive description: name: flutter_inappwebview_platform_interface - sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 url: "https://pub.dev" source: hosted - version: "1.0.10" + version: "1.3.0+1" flutter_inappwebview_web: dependency: transitive description: name: flutter_inappwebview_web - sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" url: "https://pub.dev" source: hosted - version: "1.0.8" + 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: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.1" flutter_markdown: dependency: "direct main" description: name: flutter_markdown - sha256: a23c41ee57573e62fc2190a1f36a0480c4d90bde3a8a8d7126e5d5992fb53fb7 + sha256: e17575ca576a34b46c58c91f9948891117a1bd97815d2e661813c7f90c647a78 url: "https://pub.dev" source: hosted - version: "0.7.3+1" + version: "0.7.3+2" flutter_material_design_icons: dependency: "direct main" description: @@ -493,19 +501,18 @@ packages: flutter_pdf_text: dependency: "direct main" description: - name: flutter_pdf_text - sha256: "97a49ecba6cf711a607642785758bf74fe0aaa3a2223524fa0c142e3966c56a0" - url: "https://pub.dev" - source: hosted + path: "/home/fafre/Repos/flutter-pdf-text" + relative: false + source: path version: "0.6.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.23" flutter_riverpod: dependency: transitive description: @@ -608,10 +615,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc" url: "https://pub.dev" source: hosted - version: "14.2.7" + version: "14.3.0" go_router_builder: dependency: "direct dev" description: @@ -640,10 +647,10 @@ packages: dependency: "direct main" description: name: home_widget - sha256: "2a0fdd6267ff975bd07bedf74686bd5577200f504f5de36527ac1b56bdbe68e3" + sha256: b313e3304c0429669fddf1286e1fbf61a64b873f38ba30b3eb890ef0d7560b12 url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.0" hooks_riverpod: dependency: "direct main" description: @@ -688,10 +695,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.0" image: dependency: transitive description: @@ -832,10 +839,10 @@ packages: dependency: "direct main" description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" package_config: dependency: transitive description: @@ -848,10 +855,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 + sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.0.3" package_info_plus_platform_interface: dependency: transitive description: @@ -880,10 +887,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.12" path_provider_foundation: dependency: transitive description: @@ -1024,18 +1031,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" + sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11 url: "https://pub.dev" source: hosted - version: "10.0.2" + version: "10.0.3" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" + sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.1" shared_preferences: dependency: "direct main" description: @@ -1048,18 +1055,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: @@ -1096,10 +1103,10 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: @@ -1148,10 +1155,9 @@ packages: speech_to_text_google_dialog: dependency: "direct main" description: - name: speech_to_text_google_dialog - sha256: f156aaf3b39416161310fb27a33a4e4da1b4bfeecd643635f28d5cac405a00da - url: "https://pub.dev" - source: hosted + path: "../../speech_to_text_google_dialog" + relative: true + source: path version: "0.0.6" sprintf: dependency: transitive @@ -1181,10 +1187,10 @@ packages: dependency: transitive description: name: sqlparser - sha256: "3be52b4968fc2f098ba735863404756d2fe3ea0729cf006a5b5612618f74ca04" + sha256: "852cf80f9e974ac8e1b613758a8aa640215f7701352b66a7f468e95711eb570b" url: "https://pub.dev" source: hosted - version: "0.37.1" + version: "0.38.1" stack_trace: dependency: transitive description: @@ -1284,27 +1290,26 @@ packages: uri_to_file: dependency: "direct main" description: - name: uri_to_file - sha256: cbbb38f975d22311efacc9a53cdcb806708606666d8092314b81c5cb7983b700 - url: "https://pub.dev" - source: hosted + path: "../../uri-to-file" + relative: true + source: path version: "1.0.0" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab + sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" url: "https://pub.dev" source: hosted - version: "6.3.10" + version: "6.3.12" url_launcher_ios: dependency: transitive description: @@ -1325,10 +1330,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -1357,10 +1362,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_math: dependency: transitive description: @@ -1389,10 +1394,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket: dependency: transitive description: @@ -1413,18 +1418,18 @@ packages: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.5.5" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index c44ad1ad..e39847f7 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -8,65 +8,68 @@ environment: dependencies: animations: ^2.0.11 - collection: ^1.18.0 + collection: ^1.19.0 copy_with_extension: ^5.0.4 crypto: ^3.0.5 - drift: ^2.20.0 + drift: ^2.20.3 dynamic_color: ^1.7.0 exceptions: ^0.6.1 expandable_page_view: ^1.0.17 - fading_scroll: ^0.9.0 + fading_scroll: ^0.9.1 fast_equatable: ^1.1.0 file_picker: ^8.1.2 flutter: sdk: flutter flutter_hooks: ^0.20.5 - flutter_inappwebview: ^6.0.0 - flutter_markdown: ^0.7.3+1 + flutter_inappwebview: ^6.1.5 + flutter_markdown: ^0.7.3+2 flutter_material_design_icons: ^1.1.7447 flutter_mozilla_components: path: ../packages/flutter_mozilla_components - flutter_pdf_text: ^0.6.0 + flutter_pdf_text: + path: ../../flutter-pdf-text flutter_secure_storage: ^9.2.2 flutter_sharing_intent: ^1.1.1 - go_router: ^14.2.3 + go_router: ^14.3.0 google_fonts: ^6.2.1 - home_widget: ^0.6.0 + home_widget: ^0.7.0 hooks_riverpod: ^2.5.2 html: ^0.15.4 http: ^1.2.2 json_annotation: ^4.9.0 logger: ^2.4.0 markdown: ^7.2.2 - mime: ^1.0.5 - package_info_plus: ^8.0.2 + mime: ^2.0.0 + package_info_plus: ^8.0.3 path: ^1.9.0 path_provider: ^2.1.4 riverpod: ^2.5.1 riverpod_annotation: ^2.3.5 rxdart: ^0.28.0 - share_plus: ^10.0.2 + share_plus: ^10.0.3 shared_preferences: ^2.3.2 skeletonizer: ^1.4.2 - speech_to_text_google_dialog: ^0.0.6 + speech_to_text_google_dialog: + path: ../../speech_to_text_google_dialog sqlite3: ^2.4.6 sqlite3_flutter_libs: ^0.5.24 text_scroll: ^0.2.0 timeago: ^3.7.0 universal_io: ^2.2.2 - uri_to_file: ^1.0.0 - url_launcher: ^6.3.0 - uuid: ^4.4.2 + uri_to_file: + path: ../../uri-to-file + url_launcher: ^6.3.1 + uuid: ^4.5.1 watcher: ^1.1.0 dev_dependencies: - build_runner: ^2.4.12 + build_runner: ^2.4.13 copy_with_extension_gen: ^5.0.4 - custom_lint: ^0.6.5 - dependency_validator: ^4.1.0 - drift_dev: ^2.20.1 + custom_lint: ^0.6.8 + dependency_validator: ^4.1.1 + drift_dev: ^2.20.3 fast_equatable_lint: ^0.3.0 - flutter_launcher_icons: ^0.13.1 + flutter_launcher_icons: ^0.14.1 go_router_builder: ^2.7.1 json_serializable: ^6.8.0 lint: ^2.3.0 @@ -79,7 +82,6 @@ flutter: - assets/icon/ - assets/landing/ - assets/fonts/ - - assets/scripts/ flutter_launcher_icons: android: "launcher_icon" diff --git a/melos.yaml b/melos.yaml index d15aa9ec..a1746169 100644 --- a/melos.yaml +++ b/melos.yaml @@ -1,7 +1,28 @@ name: lensai_project scripts: + upgrade-major-tighten: + run: | + dart pub upgrade --major-versions --tighten + exec: + concurrency: 1 + build-watch: + description: Generate and watch files with build_runner in the workspace + packageFilters: + dependsOn: build_runner + exec: | + dart run build_runner watch --delete-conflicting-outputs + build: + description: Generate files with build_runner in the workspace + packageFilters: + dependsOn: build_runner + run: | + dart run build_runner build --delete-conflicting-outputs + exec: + failFast: true + concurrency: 1 build-pigeons: + description: Running pigeon for files in pigeons directory exec: | echo "Running pigeon for files in pigeons directory:" for file in ./pigeons/*; do diff --git a/packages/flutter_mozilla_components/analysis_options.yaml b/packages/flutter_mozilla_components/analysis_options.yaml index a5744c1c..4d829f60 100644 --- a/packages/flutter_mozilla_components/analysis_options.yaml +++ b/packages/flutter_mozilla_components/analysis_options.yaml @@ -1,4 +1,35 @@ -include: package:flutter_lints/flutter.yaml +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. -# Additional information about this file can be found at +include: package:lint/package.yaml +# Uncomment the following section to specify additional rules. + +linter: + rules: + - unawaited_futures + - discarded_futures + - collection_methods_unrelated_type + +analyzer: + plugins: + - custom_lint + exclude: + - "**.g.dart" + - "**.swagger.dart" + - "**.freezed.dart" + - "**.chopper.dart" +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see # https://dart.dev/guides/language/analysis-options diff --git a/packages/flutter_mozilla_components/android/build.gradle b/packages/flutter_mozilla_components/android/build.gradle index e33929d4..5137af9f 100644 --- a/packages/flutter_mozilla_components/android/build.gradle +++ b/packages/flutter_mozilla_components/android/build.gradle @@ -2,7 +2,7 @@ group = "eu.lensai.flutter_mozilla_components" version = "1.0-SNAPSHOT" buildscript { - ext.kotlin_version = "1.9.20" + ext.kotlin_version = "1.9.22" repositories { google() mavenCentral() 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 cfa3cd40..99fe089c 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,13 +2,13 @@ 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 android.widget.FrameLayout -import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents +import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate +import eu.lensai.flutter_mozilla_components.integration.ReaderViewIntegration +import eu.lensai.flutter_mozilla_components.pigeons.SelectionAction import mozilla.components.browser.thumbnails.BrowserThumbnails import mozilla.components.concept.engine.EngineView import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature @@ -24,16 +24,16 @@ import mozilla.components.support.ktx.android.view.exitImmersiveMode */ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), UserInteractionHandler { private val thumbnailsFeature = ViewBoundFeatureWrapper() + private val readerViewFeature = ViewBoundFeatureWrapper() private val fullScreenFeature = ViewBoundFeatureWrapper() private val mediaSessionFullscreenFeature = ViewBoundFeatureWrapper() override fun createEngine(components: Components): EngineView { return components.engine.createView(context).apply { -// selectionActionDelegate = DefaultSelectionActionDelegate( -// store = components.store, -// context = context, -// ) + selectionActionDelegate = DefaultSelectionActionDelegate( + components.selectionAction + ) } } @@ -47,6 +47,19 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use val binding = super.binding + readerViewFeature.set( + feature = ReaderViewIntegration( + requireContext(), + components.engine, + components.store, + binding.readerViewBar, + components.readerViewEvents, + components.readerViewController, + ), + owner = this, + view = binding.root, + ) + fullScreenFeature.set( feature = FullScreenFeature( components.store, 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 6d9dd470..b3ed4d32 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 @@ -3,32 +3,28 @@ package eu.lensai.flutter_mozilla_components import android.annotation.SuppressLint import android.content.Context -import android.content.Intent import android.content.SharedPreferences -import android.util.Log -import android.widget.Toast import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat +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.SelectionAction import eu.lensai.flutter_mozilla_components.pigeons.TabContentState -import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.launch 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.selector.selectedTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.thumbnails.ThumbnailsMiddleware import mozilla.components.browser.thumbnails.storage.ThumbnailStorage @@ -75,7 +71,12 @@ private const val DAY_IN_MINUTES = 24 * 60L @SuppressLint("NewApi") @Suppress("LargeClass") -open class DefaultComponents(private val applicationContext: Context, private val flutterEvents: GeckoStateEvents) { +open class DefaultComponents( + private val applicationContext: Context, + private val flutterEvents: GeckoStateEvents, + val readerViewController: ReaderViewController, + val selectionAction: SelectionAction, +) { companion object { const val SAMPLE_BROWSER_PREFERENCES = "sample_browser_preferences" const val PREF_LAUNCH_EXTERNAL_APP = "sample_browser_launch_external_app" @@ -277,6 +278,24 @@ open class DefaultComponents(private val applicationContext: Context, private va } } + this.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.content.findResults + } + .collect { tab -> + tab.content.findResults + flutterEvents.onFindResults( + tab.id, + tab.content.findResults.map { result -> FindResultState( + activeMatchOrdinal = result.activeMatchOrdinal.toLong(), + numberOfMatches = result.numberOfMatches.toLong(), + isDoneCounting = result.isDoneCounting, + ) } + ) { _ -> } + } + } + WebNotificationFeature( applicationContext, engine, @@ -294,6 +313,8 @@ open class DefaultComponents(private val applicationContext: Context, private va 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) 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 643c1916..c8c56216 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 @@ -3,28 +3,30 @@ package eu.lensai.flutter_mozilla_components import android.app.Activity import android.content.Context import android.content.Intent -import androidx.annotation.NonNull -import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl +import eu.lensai.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl +import eu.lensai.flutter_mozilla_components.api.GeckoFindApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl 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.pigeons.GeckoBrowserApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi +import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi +import eu.lensai.flutter_mozilla_components.pigeons.GeckoFindApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi +import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController +import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents +import eu.lensai.flutter_mozilla_components.pigeons.SelectionAction import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler import mozilla.components.browser.engine.gecko.GeckoEngine import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient import mozilla.components.concept.engine.Engine @@ -32,7 +34,6 @@ 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.logger.Logger import mozilla.components.support.base.log.sink.AndroidLogSink import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntimeSettings @@ -40,7 +41,17 @@ import org.mozilla.geckoview.GeckoRuntimeSettings /** * Helper class for lazily instantiating components needed by the application. */ -class Components(private val applicationContext: Context, flutterEvents: GeckoStateEvents) : DefaultComponents(applicationContext, flutterEvents) { +class Components( + private val applicationContext: Context, + flutterEvents: GeckoStateEvents, + readerViewController: ReaderViewController, + selectionAction: SelectionAction, +) : 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) @@ -85,8 +96,16 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { _flutterPluginBinding = flutterPluginBinding val flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger) + val readerViewController = + ReaderViewController(_flutterPluginBinding.binaryMessenger) + val selectionActionDelegate = SelectionAction(_flutterPluginBinding.binaryMessenger) - GlobalComponents.setUp(flutterPluginBinding.applicationContext, flutterEvents) + GlobalComponents.setUp( + flutterPluginBinding.applicationContext, + flutterEvents, + readerViewController, + selectionActionDelegate + ) val intent = Intent(flutterPluginBinding.applicationContext, NotificationActivity::class.java) intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -96,10 +115,17 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { showNativeFragment() }) + GeckoEngineSettingsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoEngineSettingsApiImpl()) GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl()) GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl()) GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl()) GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl()) + GeckoFindApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFindApiImpl()) + + ReaderViewEvents.setUp( + _flutterPluginBinding.binaryMessenger, + GlobalComponents.components!!.readerViewEvents + ) } private fun showNativeFragment() { 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 c681dd74..7503c1a9 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 @@ -2,6 +2,8 @@ package eu.lensai.flutter_mozilla_components import android.content.Context import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents +import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController +import eu.lensai.flutter_mozilla_components.pigeons.SelectionAction import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -29,8 +31,18 @@ object GlobalComponents { .whenSessionsChange() } - fun setUp(applicationContext: Context, flutterEvents: GeckoStateEvents) { - val newComponents = Components(applicationContext, flutterEvents) + fun setUp( + applicationContext: Context, + flutterEvents: GeckoStateEvents, + readerViewController: ReaderViewController, + selectionAction: SelectionAction + ) { + val newComponents = Components( + applicationContext, + flutterEvents, + readerViewController, + selectionAction + ) newComponents.crashReporter.install(applicationContext) Facts.registerProcessor(LogFactProcessor()) 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 new file mode 100644 index 00000000..59a4b212 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt @@ -0,0 +1,13 @@ +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 + +class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { + private val engine: Engine by lazy { GlobalComponents.components!!.engine } + + override fun javaScriptEnabled(state: Boolean) { + engine.settings.javascriptEnabled = state + } +} \ 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 new file mode 100644 index 00000000..7994a40b --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoFindApiImpl.kt @@ -0,0 +1,38 @@ +package eu.lensai.flutter_mozilla_components.api + +import eu.lensai.flutter_mozilla_components.GlobalComponents +import eu.lensai.flutter_mozilla_components.pigeons.GeckoFindApi +import mozilla.components.browser.state.selector.findTabOrCustomTab +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 fun sessionByTabId(tabId: String?) : EngineSession? { + val loadSessionId = tabId + ?: state.selectedTabId + + if (loadSessionId == null) { + return null + } + + val tab = state.findTabOrCustomTab(loadSessionId) + return tab?.engineState?.engineSession + } + + override fun findAll(tabId: String?, text: String) { + val engineSession = sessionByTabId(tabId) + engineSession?.findAll(text) + } + + override fun findNext(tabId: String?, forward: Boolean) { + val engineSession = sessionByTabId(tabId) + engineSession?.findNext(forward) + } + + override fun clearMatches(tabId: String?) { + val engineSession = sessionByTabId(tabId) + engineSession?.clearFindMatches() + } +} \ No newline at end of file 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 929f4c44..467e13ce 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 @@ -1,7 +1,5 @@ package eu.lensai.flutter_mozilla_components.api -import android.graphics.Bitmap -import android.os.Build import eu.lensai.flutter_mozilla_components.GlobalComponents import eu.lensai.flutter_mozilla_components.ext.toWebPBytes import eu.lensai.flutter_mozilla_components.pigeons.* @@ -9,7 +7,6 @@ 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 java.io.ByteArrayOutputStream typealias MozillaIconRequest = mozilla.components.browser.icons.IconRequest typealias MozillaIconSize = mozilla.components.browser.icons.IconRequest.Size @@ -86,11 +83,11 @@ class GeckoIconsApiImpl() : GeckoIconsApi { IconType.MANIFEST_ICON -> MozillaIconResourceType.MANIFEST_ICON } - private fun Icon.Source.toApiSource(): Source = when (this) { - Icon.Source.GENERATOR -> Source.GENERATOR - Icon.Source.DOWNLOAD -> Source.DOWNLOAD - Icon.Source.INLINE -> Source.INLINE - Icon.Source.MEMORY -> Source.MEMORY - Icon.Source.DISK -> Source.DISK + private fun Icon.Source.toApiSource(): IconSource = when (this) { + Icon.Source.GENERATOR -> IconSource.GENERATOR + Icon.Source.DOWNLOAD -> IconSource.DOWNLOAD + Icon.Source.INLINE -> IconSource.INLINE + Icon.Source.MEMORY -> IconSource.MEMORY + Icon.Source.DISK -> IconSource.DISK } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/ReaderViewEventsImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/ReaderViewEventsImpl.kt new file mode 100644 index 00000000..4b161b45 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/ReaderViewEventsImpl.kt @@ -0,0 +1,32 @@ +package eu.lensai.flutter_mozilla_components.api + +import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents + +// Interface for the listener +interface ReaderViewControllerListener { + fun onReaderViewToggled(enabled: Boolean) + fun onAppearanceButtonTap() +} + +class ReaderViewEventsImpl : ReaderViewEvents { + + private val listeners = mutableSetOf() + + override fun onToggleReaderView(enable: Boolean) { + listeners.forEach { it.onReaderViewToggled(enable) } + } + + override fun onAppearanceButtonTap() { + listeners.forEach { it.onAppearanceButtonTap() } + } + + // Method to add a listener + fun addListener(listener: ReaderViewControllerListener) { + listeners.add(listener) + } + + // Method to remove a listener + fun removeListener(listener: ReaderViewControllerListener) { + listeners.remove(listener) + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/DefaultSelectionActionDelegate.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/DefaultSelectionActionDelegate.kt new file mode 100644 index 00000000..cd61a07d --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/DefaultSelectionActionDelegate.kt @@ -0,0 +1,51 @@ +package eu.lensai.flutter_mozilla_components.feature + +import eu.lensai.flutter_mozilla_components.pigeons.SelectionAction +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine +import mozilla.components.concept.engine.selection.SelectionActionDelegate +import kotlin.coroutines.resume + +class DefaultSelectionActionDelegate( + private val selectionAction: SelectionAction +) : SelectionActionDelegate { + override fun getActionTitle(id: String): CharSequence? = runBlocking { + suspendCancellableCoroutine { continuation -> + selectionAction.getActionTitle(id) { result -> + continuation.resume(result.getOrNull()) + } + } + } + + override fun getAllActions(): Array = runBlocking { + suspendCancellableCoroutine { continuation -> + selectionAction.getAllActions() { result -> + continuation.resume(result.getOrNull()!!.toTypedArray()) + } + } + } + + override fun isActionAvailable(id: String, selectedText: String): Boolean = runBlocking { + suspendCancellableCoroutine { continuation -> + selectionAction.isActionAvailable(id, selectedText) { result -> + continuation.resume(result.getOrNull()!!) + } + } + } + + override fun performAction(id: String, selectedText: String): Boolean = runBlocking { + suspendCancellableCoroutine { continuation -> + selectionAction.performAction(id, selectedText) { result -> + continuation.resume(result.getOrNull()!!) + } + } + } + + override fun sortedActions(actions: Array): Array = runBlocking { + suspendCancellableCoroutine { continuation -> + selectionAction.sortedActions(actions.toList()) { result -> + continuation.resume(result.getOrNull()!!.toTypedArray()) + } + } + } +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/integration/ReaderViewIntegration.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/integration/ReaderViewIntegration.kt new file mode 100644 index 00000000..cfeec442 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/integration/ReaderViewIntegration.kt @@ -0,0 +1,72 @@ +package eu.lensai.flutter_mozilla_components.integration + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.core.content.ContextCompat +import eu.lensai.flutter_mozilla_components.GlobalComponents +import eu.lensai.flutter_mozilla_components.api.ReaderViewEventsImpl +import eu.lensai.flutter_mozilla_components.api.ReaderViewControllerListener +import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.readerview.ReaderViewFeature +import mozilla.components.feature.readerview.view.ReaderViewControlsView +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.feature.UserInteractionHandler + +@Suppress("UndocumentedPublicClass") +class ReaderViewIntegration( + context: Context, + engine: Engine, + store: BrowserStore, + view: ReaderViewControlsView, + readerViewEvents: ReaderViewEventsImpl, + readerViewController: ReaderViewController +) : LifecycleAwareFeature, UserInteractionHandler { + + private val controllerListener = object : ReaderViewControllerListener { + override fun onReaderViewToggled(enabled: Boolean) { + if (enabled) { + feature.showReaderView() + + readerViewController.appearanceButtonVisibility(true) { _ -> }; + } else { + feature.hideReaderView() + feature.hideControls() + + readerViewController.appearanceButtonVisibility(false) { _ -> }; + } + } + + override fun onAppearanceButtonTap() { + feature.showControls() + } + } + + init { + readerViewEvents.addListener(controllerListener) + } + + private val feature = ReaderViewFeature(context, engine, store, view) + // Will be event based in flutter +// { available, active -> +// readerViewButtonVisible = available +// readerViewButton.setSelected(active) +// +// if (active) readerViewAppearanceButton.show() else readerViewAppearanceButton.hide() +// toolbar.invalidateActions() +// } + + override fun start() { + feature.start() + } + + override fun stop() { + feature.stop() + } + + override fun onBackPressed(): Boolean { + return feature.onBackPressed() + } +} 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 2fd48e4f..8bf3b6b1 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.3.0), do not edit directly. +// Autogenerated from Pigeon (v22.4.2), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -865,6 +865,30 @@ data class TabContentState ( ) } } + +/** Generated class from Pigeon that represents data sent in messages. */ +data class FindResultState ( + val activeMatchOrdinal: Long, + val numberOfMatches: Long, + val isDoneCounting: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): FindResultState { + val activeMatchOrdinal = pigeonVar_list[0] as Long + val numberOfMatches = pigeonVar_list[1] as Long + val isDoneCounting = pigeonVar_list[2] as Boolean + return FindResultState(activeMatchOrdinal, numberOfMatches, isDoneCounting) + } + } + fun toList(): List { + return listOf( + activeMatchOrdinal, + numberOfMatches, + isDoneCounting, + ) + } +} private open class GeckoPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -1003,6 +1027,11 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { TabContentState.fromList(it) } } + 156.toByte() -> { + return (readValue(buffer) as? List)?.let { + FindResultState.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -1116,6 +1145,10 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { stream.write(155) writeValue(stream, value.toList()) } + is FindResultState -> { + stream.write(156) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -1155,6 +1188,40 @@ interface GeckoBrowserApi { } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface GeckoEngineSettingsApi { + fun javaScriptEnabled(state: Boolean) + + companion object { + /** The codec used by GeckoEngineSettingsApi. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoEngineSettingsApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoEngineSettingsApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.javaScriptEnabled$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val stateArg = args[0] as Boolean + val wrapped: List = try { + api.javaScriptEnabled(stateArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface GeckoSessionApi { fun loadUrl(tabId: String?, url: String, flags: LoadUrlFlagsValue, additionalHeaders: Map?) fun loadData(tabId: String?, data: String, mimeType: String, encoding: String) @@ -1802,6 +1869,80 @@ interface GeckoTabsApi { } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface GeckoFindApi { + fun findAll(tabId: String?, text: String) + fun findNext(tabId: String?, forward: Boolean) + fun clearMatches(tabId: String?) + + companion object { + /** The codec used by GeckoFindApi. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoFindApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoFindApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.findAll$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val tabIdArg = args[0] as String? + val textArg = args[1] as String + val wrapped: List = try { + api.findAll(tabIdArg, textArg) + 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.GeckoFindApi.findNext$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val tabIdArg = args[0] as String? + val forwardArg = args[1] as Boolean + val wrapped: List = try { + api.findNext(tabIdArg, forwardArg) + 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.GeckoFindApi.clearMatches$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val tabIdArg = args[0] as String? + val wrapped: List = try { + api.clearMatches(tabIdArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface GeckoIconsApi { fun loadIcon(request: IconRequest, callback: (Result) -> Unit) @@ -2102,4 +2243,249 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } + fun onFindResults(idArg: String, resultsArg: List, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(idArg, resultsArg)) { + 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))) + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface ReaderViewEvents { + fun onToggleReaderView(enable: Boolean) + fun onAppearanceButtonTap() + + companion object { + /** The codec used by ReaderViewEvents. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `ReaderViewEvents` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: ReaderViewEvents?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.ReaderViewEvents.onToggleReaderView$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val enableArg = args[0] as Boolean + val wrapped: List = try { + api.onToggleReaderView(enableArg) + 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.ReaderViewEvents.onAppearanceButtonTap$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.onAppearanceButtonTap() + 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 ReaderViewController(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by ReaderViewController. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + } + fun appearanceButtonVisibility(visibleArg: Boolean, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(visibleArg)) { + 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 readerViewButtonVisibility(visibleArg: Boolean, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.readerViewButtonVisibility$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(visibleArg)) { + 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))) + } + } + } +} +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +class SelectionAction(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by SelectionAction. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + } + /** + * Gets Strings representing all possible selection actions. + * + * @returns String IDs for each action that could possibly be shown in the context menu. This + * array must include all actions, available or not, and must not change over the class lifetime. + */ + fun getAllActions(callback: (Result>) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.getAllActions$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + 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 if (it[0] == null) { + callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", ""))) + } else { + val output = it[0] as List + callback(Result.success(output)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + /** + * Checks if an action can be shown on a new selection context menu. + * + * @returns whether or not the the custom action with the id of [id] is currently available + * which may be informed by [selectedText]. + */ + fun isActionAvailable(idArg: String, selectedTextArg: String, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.isActionAvailable$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(idArg, selectedTextArg)) { + 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 if (it[0] == null) { + callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", ""))) + } else { + val output = it[0] as Boolean + callback(Result.success(output)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + /** + * Gets a title to be shown in the selection context menu. + * + * @returns the text that should be shown on the action. + */ + fun getActionTitle(idArg: String, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.getActionTitle$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(idArg)) { + 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 { + val output = it[0] as String? + callback(Result.success(output)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + /** + * Should perform the action with the id of [id]. + * + * @returns [true] if the action was consumed. + */ + fun performAction(idArg: String, selectedTextArg: String, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.performAction$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(idArg, selectedTextArg)) { + 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 if (it[0] == null) { + callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", ""))) + } else { + val output = it[0] as Boolean + callback(Result.success(output)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } + /** + * Takes in a list of actions and sorts them. + * + * @returns the sorted list. + */ + fun sortedActions(actionsArg: List, callback: (Result>) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.sortedActions$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(actionsArg)) { + 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 if (it[0] == null) { + callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", ""))) + } else { + val output = it[0] as List + callback(Result.success(output)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } } diff --git a/packages/flutter_mozilla_components/android/src/main/res/layout/fragment_browser.xml b/packages/flutter_mozilla_components/android/src/main/res/layout/fragment_browser.xml index 05bd34cb..16917cdb 100644 --- a/packages/flutter_mozilla_components/android/src/main/res/layout/fragment_browser.xml +++ b/packages/flutter_mozilla_components/android/src/main/res/layout/fragment_browser.xml @@ -20,6 +20,16 @@ android:layout_height="match_parent"> + + \ No newline at end of file diff --git a/packages/flutter_mozilla_components/example/android/settings.gradle b/packages/flutter_mozilla_components/example/android/settings.gradle index e6da251d..00cb4c30 100644 --- a/packages/flutter_mozilla_components/example/android/settings.gradle +++ b/packages/flutter_mozilla_components/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false - id "org.jetbrains.kotlin.android" version "1.9.20" apply false + id "com.android.application" version "8.3.1" apply false + id "org.jetbrains.kotlin.android" version "1.9.22" apply false } include ":app" diff --git a/packages/flutter_mozilla_components/example/pubspec.yaml b/packages/flutter_mozilla_components/example/pubspec.yaml index 933e6de0..386cccee 100644 --- a/packages/flutter_mozilla_components/example/pubspec.yaml +++ b/packages/flutter_mozilla_components/example/pubspec.yaml @@ -40,7 +40,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^4.0.0 + flutter_lints: ^5.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart index 347d6b1b..41682535 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -1,19 +1,22 @@ -export 'src/geckoview_widget.dart'; //export 'src/domain/services/gecko_browser.dart'; export 'src/data/models/load_url_flags.dart'; export 'src/data/models/source.dart'; +export 'src/domain/services/gecko_cookie.dart'; +export 'src/domain/services/gecko_engine_settings.dart'; +export 'src/domain/services/gecko_event.dart'; +export 'src/domain/services/gecko_find_in_page.dart'; +export 'src/domain/services/gecko_icon.dart'; +export 'src/domain/services/gecko_readerable.dart'; export 'src/domain/services/gecko_session.dart'; export 'src/domain/services/gecko_tab.dart'; -export 'src/domain/services/gecko_icon.dart'; -export 'src/domain/services/gecko_cookie.dart'; -export 'src/domain/services/gecko_event.dart'; +export 'src/geckoview_widget.dart'; export 'src/pigeons/gecko.g.dart' show - Resource, - IconType, - ResourceSize, - IconSource, CookieSameSiteStatus, - TabContentState, + HistoryMetadataKey, + IconSource, + IconType, + Resource, + ResourceSize, SecurityInfoState, - HistoryMetadataKey; + TabContentState; diff --git a/packages/flutter_mozilla_components/lib/src/data/models/load_url_flags.dart b/packages/flutter_mozilla_components/lib/src/data/models/load_url_flags.dart index ced2af11..0ce7d89c 100644 --- a/packages/flutter_mozilla_components/lib/src/data/models/load_url_flags.dart +++ b/packages/flutter_mozilla_components/lib/src/data/models/load_url_flags.dart @@ -39,8 +39,8 @@ class LoadUrlFlags { ALLOW_JAVASCRIPT_URL.value, ); - static LoadUrlFlags select(List flags) { - int combinedValue = flags.fold(0, (sum, flag) => sum | flag.value); + factory LoadUrlFlags.select(List flags) { + final combinedValue = flags.fold(0, (sum, flag) => sum | flag.value); return LoadUrlFlags._(combinedValue); } } diff --git a/packages/flutter_mozilla_components/lib/src/data/models/source.dart b/packages/flutter_mozilla_components/lib/src/data/models/source.dart index 89ebe544..7c32e2f1 100644 --- a/packages/flutter_mozilla_components/lib/src/data/models/source.dart +++ b/packages/flutter_mozilla_components/lib/src/data/models/source.dart @@ -38,11 +38,15 @@ abstract class Source { /// Initializes a [Source] of a correct type from its component properties. /// Intended use is for restoring persisted state. static Source restore( - int? sourceId, String? packageId, int? packageCategory) { + int? sourceId, + String? packageId, + int? packageCategory, + ) { final caller = packageId != null ? ExternalPackage( packageId: packageId, - category: PackageCategory.fromInt(packageCategory).toValue()) + category: PackageCategory.fromInt(packageCategory).toValue(), + ) : null; switch (sourceId) { diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart new file mode 100644 index 00000000..6ba9caaf --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart @@ -0,0 +1,14 @@ +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; + +final _apiInstance = GeckoEngineSettingsApi(); + +class GeckoEngineSettingsService { + final GeckoEngineSettingsApi _api; + + GeckoEngineSettingsService({GeckoEngineSettingsApi? api}) + : _api = api ?? _apiInstance; + + Future javaScriptEnabled(bool state) { + return _api.javaScriptEnabled(state); + } +} 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 2871ed97..88d5663a 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 @@ -8,6 +8,7 @@ typedef ReaderableEvent = ({String tabId, ReaderableState readerable}); typedef SecurityInfoEvent = ({String tabId, SecurityInfoState securityInfo}); typedef IconEvent = ({String tabId, Uint8List? bytes}); typedef ThumbnailEvent = ({String tabId, Uint8List? bytes}); +typedef FindResultsEvent = ({String tabId, List results}); class GeckoEventService extends GeckoStateEvents { // Stream controllers @@ -20,6 +21,7 @@ class GeckoEventService extends GeckoStateEvents { StreamController.broadcast(); final _iconController = StreamController.broadcast(); final _thumbnailController = StreamController.broadcast(); + final _findResultsController = StreamController.broadcast(); // Event streams Stream> get tabListEvents => _tabListController.stream; @@ -31,6 +33,8 @@ class GeckoEventService extends GeckoStateEvents { _securityInfoController.stream; Stream get iconEvents => _iconController.stream; Stream get thumbnailEvents => _thumbnailController.stream; + Stream get findResultsEvent => + _findResultsController.stream; // Overridden methods @override @@ -73,21 +77,31 @@ class GeckoEventService extends GeckoStateEvents { _thumbnailController.add((tabId: id, bytes: bytes)); } + @override + void onFindResults(String id, List results) { + _findResultsController.add((tabId: id, results: results.nonNulls.toList())); + } + GeckoEventService.setUp({ BinaryMessenger? binaryMessenger, String messageChannelSuffix = '', }) { - GeckoStateEvents.setUp(this); + GeckoStateEvents.setUp( + this, + binaryMessenger: binaryMessenger, + messageChannelSuffix: messageChannelSuffix, + ); } void dispose() { - _tabListController.close(); - _selectedTabController.close(); - _tabContentController.close(); - _historyController.close(); - _readerableController.close(); - _securityInfoController.close(); - _iconController.close(); - _thumbnailController.close(); + unawaited(_tabListController.close()); + unawaited(_selectedTabController.close()); + unawaited(_tabContentController.close()); + unawaited(_historyController.close()); + unawaited(_readerableController.close()); + unawaited(_securityInfoController.close()); + unawaited(_iconController.close()); + unawaited(_thumbnailController.close()); + unawaited(_findResultsController.close()); } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_find_in_page.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_find_in_page.dart new file mode 100644 index 00000000..7d210a39 --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_find_in_page.dart @@ -0,0 +1,27 @@ +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; + +final _apiInstance = GeckoFindApi(); + +class GeckoFindInPageService { + final GeckoFindApi _api; + final String? tabId; + + GeckoFindInPageService({required this.tabId, GeckoFindApi? api}) + : _api = api ?? _apiInstance; + + GeckoFindInPageService.forActiveTab({GeckoFindApi? api}) + : _api = api ?? _apiInstance, + tabId = null; + + Future findAll(String text) { + return _api.findAll(tabId, text); + } + + Future findNext(bool forward) { + return _api.findNext(tabId, forward); + } + + Future clearMatches() { + return _api.clearMatches(tabId); + } +} diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_icon.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_icon.dart index 3d9475b3..4ff99368 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_icon.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_icon.dart @@ -7,14 +7,18 @@ class GeckoIconService { GeckoIconService({GeckoIconsApi? api}) : _api = api ?? _apiInstance; - Future loadIcon( - {required Uri url, List resources = const []}) async { - return _api.loadIcon(IconRequest( - url: url.toString(), - size: IconSize.defaultSize, - resources: resources, - isPrivate: false, - waitOnNetworkLoad: true, - )); + Future loadIcon({ + required Uri url, + List resources = const [], + }) async { + return _api.loadIcon( + IconRequest( + url: url.toString(), + size: IconSize.defaultSize, + resources: resources, + isPrivate: false, + waitOnNetworkLoad: true, + ), + ); } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_readerable.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_readerable.dart new file mode 100644 index 00000000..20ecc076 --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_readerable.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; + +class GeckoReaderableService extends ReaderViewController { + final ReaderViewEvents _events; + + final _appearanceVisibility = StreamController.broadcast(); + final _readerVisibility = StreamController.broadcast(); + + Stream get appearanceVisibility => _appearanceVisibility.stream; + Stream get readerVisibility => _readerVisibility.stream; + + Future toggleReaderView(bool enable) { + return _events.onToggleReaderView(enable); + } + + Future onAppearanceButtonTap() { + return _events.onAppearanceButtonTap(); + } + + @override + void appearanceButtonVisibility(bool visible) { + _appearanceVisibility.add(visible); + } + + @override + void readerViewButtonVisibility(bool visible) { + _readerVisibility.add(visible); + } + + GeckoReaderableService.setUp({ + ReaderViewEvents? readerEvents, + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : _events = readerEvents ?? + ReaderViewEvents( + binaryMessenger: binaryMessenger, + messageChannelSuffix: messageChannelSuffix, + ) { + ReaderViewController.setUp( + this, + binaryMessenger: binaryMessenger, + messageChannelSuffix: messageChannelSuffix, + ); + } + + void dispose() { + unawaited(_appearanceVisibility.close()); + unawaited(_readerVisibility.close()); + } +} 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 3a94b96e..6815c845 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 @@ -7,13 +7,13 @@ class GeckoSessionService { final String? tabId; final GeckoSessionApi _api; + GeckoSessionService({required String this.tabId, GeckoSessionApi? api}) + : _api = api ?? _apiInstance; + GeckoSessionService.forActiveTab({GeckoSessionApi? api}) : _api = api ?? _apiInstance, tabId = null; - GeckoSessionService({required String this.tabId, GeckoSessionApi? api}) - : _api = api ?? _apiInstance; - Future loadUrl({ required Uri url, LoadUrlFlags flags = LoadUrlFlags.NONE, diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart index 3dfe9ff7..3a10d78c 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart @@ -71,7 +71,10 @@ class GeckoTabService { RestoreLocation restoreLocation = RestoreLocation.end, }) { return _api.restoreTabsByList( - tabs: tabs, selectTabId: selectTabId, restoreLocation: restoreLocation); + tabs: tabs, + selectTabId: selectTabId, + restoreLocation: restoreLocation, + ); } Future restoreTabsByBrowserState({ @@ -79,7 +82,9 @@ class GeckoTabService { RestoreLocation restoreLocation = RestoreLocation.end, }) { return _api.restoreTabsByBrowserState( - state: state, restoreLocation: restoreLocation); + state: state, + restoreLocation: restoreLocation, + ); } //The calls with engin storage for restore are not supported at the moment @@ -91,7 +96,9 @@ class GeckoTabService { required HistoryMetadataKey historyMetadata, }) { return _api.selectOrAddTabByHistory( - url: url.toString(), historyMetadata: historyMetadata); + url: url.toString(), + historyMetadata: historyMetadata, + ); } /// Selects an already existing tab displaying [url] or otherwise creates a new tab. @@ -116,7 +123,9 @@ class GeckoTabService { bool selectNewTab = true, }) { return _api.duplicateTab( - selectTabId: selectTabId, selectNewTab: selectNewTab); + selectTabId: selectTabId, + selectNewTab: selectNewTab, + ); } Future moveTabs({ @@ -125,7 +134,10 @@ class GeckoTabService { required bool placeAfter, }) { return _api.moveTabs( - tabIds: tabIds, targetTabId: targetTabId, placeAfter: placeAfter); + tabIds: tabIds, + targetTabId: targetTabId, + placeAfter: placeAfter, + ); } Future migratePrivateTabUseCase({ @@ -133,6 +145,8 @@ class GeckoTabService { Uri? alternativeUrl, }) { return _api.migratePrivateTabUseCase( - tabId: tabId, alternativeUrl: alternativeUrl?.toString()); + tabId: tabId, + alternativeUrl: alternativeUrl?.toString(), + ); } } diff --git a/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart b/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart index 9f6f76cd..74096498 100644 --- a/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart +++ b/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart @@ -38,6 +38,7 @@ class GeckoView extends StatelessWidget { await GeckoBrowserService().showNativeFragment(); }); }) + // ignore: discarded_futures ..create(); }, ); 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 11a9e66f..f68e4ba3 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.3.0), do not edit directly. +// Autogenerated from Pigeon (v22.4.2), 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 @@ -955,6 +955,37 @@ class TabContentState { } } +class FindResultState { + FindResultState({ + required this.activeMatchOrdinal, + required this.numberOfMatches, + required this.isDoneCounting, + }); + + int activeMatchOrdinal; + + int numberOfMatches; + + bool isDoneCounting; + + Object encode() { + return [ + activeMatchOrdinal, + numberOfMatches, + isDoneCounting, + ]; + } + + static FindResultState decode(Object result) { + result as List; + return FindResultState( + activeMatchOrdinal: result[0]! as int, + numberOfMatches: result[1]! as int, + isDoneCounting: result[2]! as bool, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @@ -1044,6 +1075,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is TabContentState) { buffer.putUint8(155); writeValue(buffer, value.encode()); + } else if (value is FindResultState) { + buffer.putUint8(156); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1111,6 +1145,8 @@ class _PigeonCodec extends StandardMessageCodec { return SecurityInfoState.decode(readValue(buffer)!); case 155: return TabContentState.decode(readValue(buffer)!); + case 156: + return FindResultState.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -1153,6 +1189,42 @@ class GeckoBrowserApi { } } +class GeckoEngineSettingsApi { + /// Constructor for [GeckoEngineSettingsApi]. 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. + GeckoEngineSettingsApi({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 javaScriptEnabled(bool state) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.javaScriptEnabled$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([state]) 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; + } + } +} + class GeckoSessionApi { /// Constructor for [GeckoSessionApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -1166,7 +1238,7 @@ class GeckoSessionApi { final String pigeonVar_messageChannelSuffix; - Future loadUrl({required String? tabId, required String url, required LoadUrlFlagsValue flags, required Map? additionalHeaders,}) async { + Future loadUrl({required String? tabId, required String url, required LoadUrlFlagsValue flags, required Map? additionalHeaders,}) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.loadUrl$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -1452,7 +1524,7 @@ class GeckoSessionApi { } } - Future crashRecovery({required List? tabIds}) async { + Future crashRecovery({required List? tabIds}) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.crashRecovery$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -1576,7 +1648,7 @@ class GeckoTabsApi { } } - Future addTab({required String url, required bool selectTab, required bool startLoading, required String? parentId, required LoadUrlFlagsValue flags, required String? contextId, required SourceValue source, required bool private, required HistoryMetadataKey? historyMetadata, required Map? additionalHeaders,}) async { + Future addTab({required String url, required bool selectTab, required bool startLoading, required String? parentId, required LoadUrlFlagsValue flags, required String? contextId, required SourceValue source, required bool private, required HistoryMetadataKey? historyMetadata, required Map? additionalHeaders,}) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.addTab$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -1625,7 +1697,7 @@ class GeckoTabsApi { } } - Future removeTabs({required List ids}) async { + Future removeTabs({required List ids}) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.removeTabs$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -1713,7 +1785,7 @@ class GeckoTabsApi { } } - Future restoreTabsByList({required List tabs, required String? selectTabId, required RestoreLocation restoreLocation,}) async { + Future restoreTabsByList({required List tabs, required String? selectTabId, required RestoreLocation restoreLocation,}) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.restoreTabsByList$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -1841,7 +1913,7 @@ class GeckoTabsApi { } } - Future moveTabs({required List tabIds, required String targetTabId, required bool placeAfter,}) async { + Future moveTabs({required List tabIds, required String targetTabId, required bool placeAfter,}) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.moveTabs$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -1891,6 +1963,86 @@ class GeckoTabsApi { } } +class GeckoFindApi { + /// Constructor for [GeckoFindApi]. 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. + GeckoFindApi({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 findAll(String? tabId, String text) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.findAll$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([tabId, text]) 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 findNext(String? tabId, bool forward) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.findNext$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([tabId, forward]) 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 clearMatches(String? tabId) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoFindApi.clearMatches$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([tabId]) 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; + } + } +} + class GeckoIconsApi { /// Constructor for [GeckoIconsApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -1972,7 +2124,7 @@ class GeckoCookieApi { } } - Future> getAllCookies(String? domain, String? firstPartyDomain, String? name, CookiePartitionKey? partitionKey, String? storeId, String url) async { + Future> getAllCookies(String? domain, String? firstPartyDomain, String? name, CookiePartitionKey? partitionKey, String? storeId, String url) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.getAllCookies$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -1995,7 +2147,7 @@ class GeckoCookieApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)!.cast(); + return (pigeonVar_replyList[0] as List?)!.cast(); } } @@ -2047,7 +2199,7 @@ class GeckoCookieApi { abstract class GeckoStateEvents { static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - void onTabListChange(List tabIds); + void onTabListChange(List tabIds); void onSelectedTabChange(String? id); @@ -2063,6 +2215,8 @@ abstract class GeckoStateEvents { void onThumbnailChange(String id, Uint8List? bytes); + void onFindResults(String id, List results); + static void setUp(GeckoStateEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { @@ -2076,9 +2230,9 @@ abstract class GeckoStateEvents { assert(message != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null.'); final List args = (message as List?)!; - final List? arg_tabIds = (args[0] as List?)?.cast(); + final List? arg_tabIds = (args[0] as List?)?.cast(); assert(arg_tabIds != null, - 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null, expected non-null List.'); + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null, expected non-null List.'); try { api.onTabListChange(arg_tabIds!); return wrapResponse(empty: true); @@ -2274,5 +2428,313 @@ abstract class GeckoStateEvents { }); } } + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults$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.GeckoStateEvents.onFindResults was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + assert(arg_id != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null, expected non-null String.'); + final List? arg_results = (args[1] as List?)?.cast(); + assert(arg_results != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null, expected non-null List.'); + try { + api.onFindResults(arg_id!, arg_results!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} + +class ReaderViewEvents { + /// Constructor for [ReaderViewEvents]. 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. + ReaderViewEvents({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 onToggleReaderView(bool enable) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.ReaderViewEvents.onToggleReaderView$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([enable]) 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 onAppearanceButtonTap() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.ReaderViewEvents.onAppearanceButtonTap$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; + } + } +} + +abstract class ReaderViewController { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + void appearanceButtonVisibility(bool visible); + + void readerViewButtonVisibility(bool visible); + + static void setUp(ReaderViewController? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility$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.ReaderViewController.appearanceButtonVisibility was null.'); + final List args = (message as List?)!; + final bool? arg_visible = (args[0] as bool?); + assert(arg_visible != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility was null, expected non-null bool.'); + try { + api.appearanceButtonVisibility(arg_visible!); + 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.ReaderViewController.readerViewButtonVisibility$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.ReaderViewController.readerViewButtonVisibility was null.'); + final List args = (message as List?)!; + final bool? arg_visible = (args[0] as bool?); + assert(arg_visible != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.readerViewButtonVisibility was null, expected non-null bool.'); + try { + api.readerViewButtonVisibility(arg_visible!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} + +abstract class SelectionAction { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Gets Strings representing all possible selection actions. + /// + /// @returns String IDs for each action that could possibly be shown in the context menu. This + /// array must include all actions, available or not, and must not change over the class lifetime. + List getAllActions(); + + /// Checks if an action can be shown on a new selection context menu. + /// + /// @returns whether or not the the custom action with the id of [id] is currently available + /// which may be informed by [selectedText]. + bool isActionAvailable(String id, String selectedText); + + /// Gets a title to be shown in the selection context menu. + /// + /// @returns the text that should be shown on the action. + String? getActionTitle(String id); + + /// Should perform the action with the id of [id]. + /// + /// @returns [true] if the action was consumed. + bool performAction(String id, String selectedText); + + /// Takes in a list of actions and sorts them. + /// + /// @returns the sorted list. + List sortedActions(List actions); + + static void setUp(SelectionAction? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.getAllActions$messageChannelSuffix', pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + final List output = api.getAllActions(); + return wrapResponse(result: output); + } 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.SelectionAction.isActionAvailable$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.SelectionAction.isActionAvailable was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + assert(arg_id != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.isActionAvailable was null, expected non-null String.'); + final String? arg_selectedText = (args[1] as String?); + assert(arg_selectedText != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.isActionAvailable was null, expected non-null String.'); + try { + final bool output = api.isActionAvailable(arg_id!, arg_selectedText!); + return wrapResponse(result: output); + } 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.SelectionAction.getActionTitle$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.SelectionAction.getActionTitle was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + assert(arg_id != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.getActionTitle was null, expected non-null String.'); + try { + final String? output = api.getActionTitle(arg_id!); + return wrapResponse(result: output); + } 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.SelectionAction.performAction$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.SelectionAction.performAction was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + assert(arg_id != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.performAction was null, expected non-null String.'); + final String? arg_selectedText = (args[1] as String?); + assert(arg_selectedText != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.performAction was null, expected non-null String.'); + try { + final bool output = api.performAction(arg_id!, arg_selectedText!); + return wrapResponse(result: output); + } 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.SelectionAction.sortedActions$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.SelectionAction.sortedActions was null.'); + final List args = (message as List?)!; + final List? arg_actions = (args[0] as List?)?.cast(); + assert(arg_actions != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.SelectionAction.sortedActions was null, expected non-null List.'); + try { + final List output = api.sortedActions(arg_actions!); + return wrapResponse(result: output); + } 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 1a6869fa..14efb24e 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -375,19 +375,20 @@ class Cookie { final String value; Cookie( - this.domain, - this.expirationDate, - this.firstPartyDomain, - this.hostOnly, - this.httpOnly, - this.name, - this.partitionKey, - this.path, - this.secure, - this.session, - this.sameSite, - this.storeId, - this.value); + this.domain, + this.expirationDate, + this.firstPartyDomain, + this.hostOnly, + this.httpOnly, + this.name, + this.partitionKey, + this.path, + this.secure, + this.session, + this.sameSite, + this.storeId, + this.value, + ); } class HistoryItem { @@ -456,20 +457,95 @@ class TabContentState { ); } -@ConfigurePigeon(PigeonOptions( - dartOut: 'lib/src/pigeons/gecko.g.dart', - dartOptions: DartOptions(), - kotlinOut: - 'android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt', - kotlinOptions: - KotlinOptions(package: 'eu.lensai.flutter_mozilla_components.pigeons'), - dartPackageName: 'flutter_mozilla_components', -)) +class FindResultState { + final int activeMatchOrdinal; + final int numberOfMatches; + final bool isDoneCounting; + + FindResultState( + this.activeMatchOrdinal, + this.numberOfMatches, + this.isDoneCounting, + ); +} + +// /// Represents all the different supported types of data that can be found from long clicking +// /// an element. +// sealed class HitResult { +// final String src; + +// HitResult(this.src); +// } + +// /// Default type if we're unable to match the type to anything. It may or may not have a src. +// class UnknownHitResult extends HitResult { +// UnknownHitResult(super.src); +// } + +// /// If the HTML element was of type 'HTMLImageElement'. +// class ImageHitResult extends HitResult { +// final String? title; + +// ImageHitResult(super.src, {this.title}); +// } + +// /// If the HTML element was of type 'HTMLVideoElement'. +// class VideoHitResult extends HitResult { +// final String? title; + +// VideoHitResult(super.src, {this.title}); +// } + +// /// If the HTML element was of type 'HTMLAudioElement'. +// class AudioHitResult extends HitResult { +// final String? title; + +// AudioHitResult(super.src, {this.title}); +// } + +// /// If the HTML element was of type 'HTMLImageElement' and contained a URI. +// class ImageSrcHitResult extends HitResult { +// final String uri; + +// ImageSrcHitResult(super.src, this.uri); +// } + +// /// The type used if the URI is prepended with 'tel:'. +// class PhoneHitResult extends HitResult { +// PhoneHitResult(super.src); +// } + +// /// The type used if the URI is prepended with 'mailto:'. +// class EmailHitResult extends HitResult { +// EmailHitResult(super.src); +// } + +// /// The type used if the URI is prepended with 'geo:'. +// class GeoHitResult extends HitResult { +// GeoHitResult(super.src); +// } + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/pigeons/gecko.g.dart', + dartOptions: DartOptions(), + kotlinOut: + 'android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt', + kotlinOptions: + KotlinOptions(package: 'eu.lensai.flutter_mozilla_components.pigeons'), + dartPackageName: 'flutter_mozilla_components', + ), +) @HostApi() abstract class GeckoBrowserApi { void showNativeFragment(); } +@HostApi() +abstract class GeckoEngineSettingsApi { + void javaScriptEnabled(bool state); +} + @HostApi() abstract class GeckoSessionApi { void loadUrl({ @@ -629,6 +705,13 @@ abstract class GeckoTabsApi { }); } +@HostApi() +abstract class GeckoFindApi { + void findAll(String? tabId, String text); + void findNext(String? tabId, bool forward); + void clearMatches(String? tabId); +} + @HostApi() abstract class GeckoIconsApi { @async @@ -693,4 +776,48 @@ abstract class GeckoStateEvents { void onSecurityInfoStateChange(String id, SecurityInfoState state); void onIconChange(String id, Uint8List? bytes); void onThumbnailChange(String id, Uint8List? bytes); + + void onFindResults(String id, List results); +} + +@HostApi() +abstract class ReaderViewEvents { + void onToggleReaderView(bool enable); + void onAppearanceButtonTap(); +} + +@FlutterApi() +abstract class ReaderViewController { + void appearanceButtonVisibility(bool visible); + void readerViewButtonVisibility(bool visible); +} + +@FlutterApi() +abstract class SelectionAction { + /// Gets Strings representing all possible selection actions. + /// + /// @returns String IDs for each action that could possibly be shown in the context menu. This + /// array must include all actions, available or not, and must not change over the class lifetime. + List getAllActions(); + + /// Checks if an action can be shown on a new selection context menu. + /// + /// @returns whether or not the the custom action with the id of [id] is currently available + /// which may be informed by [selectedText]. + bool isActionAvailable(String id, String selectedText); + + /// Gets a title to be shown in the selection context menu. + /// + /// @returns the text that should be shown on the action. + String? getActionTitle(String id); + + /// Should perform the action with the id of [id]. + /// + /// @returns [true] if the action was consumed. + bool performAction(String id, String selectedText); + + /// Takes in a list of actions and sorts them. + /// + /// @returns the sorted list. + List sortedActions(List actions); } diff --git a/packages/flutter_mozilla_components/pubspec.yaml b/packages/flutter_mozilla_components/pubspec.yaml index e7fed88f..aa06fe7b 100644 --- a/packages/flutter_mozilla_components/pubspec.yaml +++ b/packages/flutter_mozilla_components/pubspec.yaml @@ -10,13 +10,13 @@ environment: dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.0.2 + plugin_platform_interface: ^2.1.8 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 - pigeon: ^22.3.0 + lint: ^2.3.0 + pigeon: ^22.4.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec