From 8d351e55c063bd448746f42d9f72628dfb94c272 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Tue, 22 Oct 2024 07:27:37 +0200 Subject: [PATCH] one step closer --- app/analysis_options.yaml | 1 + app/android/app/src/main/AndroidManifest.xml | 3 +- .../movenext/bang_navigator/MainActivity.kt | 17 +- app/build.yaml | 4 + app/lib/core/routing/routes.dart | 12 +- app/lib/core/routing/routes.g.dart | 13 +- .../database}/converters/color.dart | 0 .../data/database/converters/icon_data.dart | 48 + .../database}/converters/uri.dart | 0 .../functions/lexo_rank_functions.dart | 58 + .../features/geckoview/domain/providers.dart | 38 +- .../geckoview/domain/providers.g.dart | 18 +- .../domain/providers/selected_tab.dart | 33 +- .../domain/providers/selected_tab.g.dart | 2 +- .../geckoview/domain/providers/tab_list.dart | 53 + .../domain/providers/tab_list.g.dart | 24 + .../domain/providers/tab_session.dart | 4 + .../domain/providers/tab_session.g.dart | 2 +- .../geckoview/domain/providers/tab_state.dart | 129 +- .../domain/providers/tab_state.g.dart | 2 +- .../geckoview/domain/repositories/tab.dart | 24 +- .../geckoview/domain/repositories/tab.g.dart | 2 +- .../features/browser/domain/providers.dart | 37 +- .../features/browser/domain/providers.g.dart | 43 +- .../presentation/dialogs/tab_action.dart | 42 +- .../presentation/widgets/app_bar_title.dart | 61 +- .../widgets/sheets/view_tabs.dart | 99 +- .../presentation/widgets/tab_preview.dart | 6 +- .../widgets/tabs_action_button.dart | 4 +- .../features/browser/screens/browser.dart | 1167 +++++++++-------- .../presentation/controllers/readerable.dart | 1 + .../tabs/data/database/daos/container.dart | 76 ++ .../tabs/data/database/daos/container.g.dart | 6 + .../features/tabs/data/database/daos/tab.dart | 128 ++ .../data/database/daos/tab.g.dart | 2 +- .../data/database/database.dart | 24 +- .../tabs/data/database/database.drift | 69 + .../tabs/data/database/database.g.dart | 996 ++++++++++++++ .../data/models/container_data.dart} | 23 +- .../{topics => tabs}/data/providers.dart | 12 +- .../{topics => tabs}/data/providers.g.dart | 2 +- .../features/tabs/domain/providers.dart | 50 + .../features/tabs/domain/providers.g.dart | 322 +++++ .../domain/providers/selected_container.dart | 43 + .../providers/selected_container.g.dart | 43 + .../domain/providers/selected_topic.g.dart | 0 .../tabs/domain/repositories/container.dart | 54 + .../domain/repositories/container.g.dart} | 21 +- .../tabs/domain/repositories/tab.dart | 41 + .../domain/repositories/tab.g.dart} | 20 +- .../presentation/screens/container_list.dart} | 92 +- .../presentation/widgets/container_chips.dart | 79 ++ .../widgets/container_dialog.dart} | 24 +- .../widgets/material_color_picker.dart | 2 +- .../{topics => tabs}/utils/color_palette.dart | 0 .../topics/data/database/daos/tab.dart | 63 - .../topics/data/database/daos/topic.dart | 54 - .../topics/data/database/daos/topic.g.dart | 6 - .../topics/data/database/database.drift | 30 - .../topics/data/database/database.g.dart | 826 ------------ .../features/topics/domain/providers.dart | 43 - .../features/topics/domain/providers.g.dart | 320 ----- .../domain/providers/selected_topic.dart | 41 - .../domain/repositories/selected_topic.dart | 27 - .../domain/repositories/selected_topic.g.dart | 27 - .../topics/domain/repositories/tab_link.dart | 42 - .../topics/domain/repositories/topic.dart | 40 - .../presentation/widgets/topic_chips.dart | 73 -- .../ephemeral/.plugin_symlinks/file_picker | 2 +- .../.plugin_symlinks/package_info_plus | 2 +- .../ephemeral/.plugin_symlinks/share_plus | 2 +- .../.plugin_symlinks/sqlite3_flutter_libs | 2 +- app/pubspec.lock | 81 +- app/pubspec.yaml | 15 +- .../android/build.gradle | 63 +- .../BaseBrowserFragment.kt | 16 - .../BrowserFragment.kt | 6 +- .../DefaultComponents.kt | 10 +- .../GeckoPlatformView.kt | 4 +- .../GlobalComponents.kt | 2 - .../api/GeckoBrowserApiImpl.kt | 12 + .../api/GeckoTabsApiImpl.kt | 56 +- .../flutter_mozilla_components/ext/Bitmap.kt | 17 + .../middleware/FlutterEventMiddleware.kt | 38 +- .../pigeons/Gecko.g.kt | 95 +- .../MainActivity.kt | 17 +- .../src/domain/services/gecko_browser.dart | 4 + .../lib/src/domain/services/gecko_event.dart | 141 +- .../lib/src/domain/services/gecko_tab.dart | 16 +- .../lib/src/geckoview_widget.dart | 36 +- .../lib/src/pigeons/gecko.g.dart | 190 ++- .../pigeons/gecko.dart | 28 +- 92 files changed, 3829 insertions(+), 2694 deletions(-) rename app/lib/{features/geckoview/features/topics/data/database/drift => data/database}/converters/color.dart (100%) create mode 100644 app/lib/data/database/converters/icon_data.dart rename app/lib/{features/geckoview/features/topics/data/database/drift => data/database}/converters/uri.dart (100%) create mode 100644 app/lib/data/database/functions/lexo_rank_functions.dart create mode 100644 app/lib/features/geckoview/domain/providers/tab_list.dart create mode 100644 app/lib/features/geckoview/domain/providers/tab_list.g.dart create mode 100644 app/lib/features/geckoview/features/tabs/data/database/daos/container.dart create mode 100644 app/lib/features/geckoview/features/tabs/data/database/daos/container.g.dart create mode 100644 app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart rename app/lib/features/geckoview/features/{topics => tabs}/data/database/daos/tab.g.dart (61%) rename app/lib/features/geckoview/features/{topics => tabs}/data/database/database.dart (59%) create mode 100644 app/lib/features/geckoview/features/tabs/data/database/database.drift create mode 100644 app/lib/features/geckoview/features/tabs/data/database/database.g.dart rename app/lib/features/geckoview/features/{topics/data/models/topic_data.dart => tabs/data/models/container_data.dart} (55%) rename app/lib/features/geckoview/features/{topics => tabs}/data/providers.dart (77%) rename app/lib/features/geckoview/features/{topics => tabs}/data/providers.g.dart (91%) create mode 100644 app/lib/features/geckoview/features/tabs/domain/providers.dart create mode 100644 app/lib/features/geckoview/features/tabs/domain/providers.g.dart create mode 100644 app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart create mode 100644 app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart rename app/lib/features/geckoview/features/{topics => tabs}/domain/providers/selected_topic.g.dart (100%) create mode 100644 app/lib/features/geckoview/features/tabs/domain/repositories/container.dart rename app/lib/features/geckoview/features/{topics/domain/repositories/tab_link.g.dart => tabs/domain/repositories/container.g.dart} (54%) create mode 100644 app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart rename app/lib/features/geckoview/features/{topics/domain/repositories/topic.g.dart => tabs/domain/repositories/tab.g.dart} (56%) rename app/lib/features/geckoview/features/{topics/presentation/screens/topic_list.dart => tabs/presentation/screens/container_list.dart} (62%) create mode 100644 app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart rename app/lib/features/geckoview/features/{topics/presentation/widgets/topic_dialog.dart => tabs/presentation/widgets/container_dialog.dart} (83%) rename app/lib/features/geckoview/features/{topics => tabs}/presentation/widgets/material_color_picker.dart (99%) rename app/lib/features/geckoview/features/{topics => tabs}/utils/color_palette.dart (100%) delete mode 100644 app/lib/features/geckoview/features/topics/data/database/daos/tab.dart delete mode 100644 app/lib/features/geckoview/features/topics/data/database/daos/topic.dart delete mode 100644 app/lib/features/geckoview/features/topics/data/database/daos/topic.g.dart delete mode 100644 app/lib/features/geckoview/features/topics/data/database/database.drift delete mode 100644 app/lib/features/geckoview/features/topics/data/database/database.g.dart delete mode 100644 app/lib/features/geckoview/features/topics/domain/providers.dart delete mode 100644 app/lib/features/geckoview/features/topics/domain/providers.g.dart delete mode 100644 app/lib/features/geckoview/features/topics/domain/providers/selected_topic.dart delete mode 100644 app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.dart delete mode 100644 app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.g.dart delete mode 100644 app/lib/features/geckoview/features/topics/domain/repositories/tab_link.dart delete mode 100644 app/lib/features/geckoview/features/topics/domain/repositories/topic.dart delete mode 100644 app/lib/features/geckoview/features/topics/presentation/widgets/topic_chips.dart diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml index d23dad1e..cdff5034 100644 --- a/app/analysis_options.yaml +++ b/app/analysis_options.yaml @@ -24,6 +24,7 @@ analyzer: plugins: - custom_lint exclude: + - "**.drift" - "**.g.dart" - "**.swagger.dart" - "**.freezed.dart" diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index bf29568b..fd9abd09 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -39,7 +39,8 @@ android:label="Lensai" android:name="${applicationName}" android:icon="@mipmap/launcher_icon" - android:usesCleartextTraffic="true"> + android:usesCleartextTraffic="true" + android:enableOnBackInvokedCallback="true"> ( - name: 'TopicsRoute', - path: 'topics', + TypedGoRoute( + name: 'ContainerListRoute', + path: 'containers', ), ], ) @@ -102,10 +102,10 @@ class BangSearchRoute extends GoRouteData { } } -class TopicListRoute extends GoRouteData { +class ContainerListRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const TopicListScreen(); + return const ContainerListScreen(); } } diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index 92edfc1e..9097d6ea 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -47,9 +47,9 @@ RouteBase get $browserRoute => GoRouteData.$route( ], ), GoRouteData.$route( - path: 'topics', - name: 'TopicsRoute', - factory: $TopicListRouteExtension._fromState, + path: 'containers', + name: 'ContainerListRoute', + factory: $ContainerListRouteExtension._fromState, ), ], ); @@ -163,11 +163,12 @@ extension $BangSubCategoryRouteExtension on BangSubCategoryRoute { void replace(BuildContext context) => context.replace(location); } -extension $TopicListRouteExtension on TopicListRoute { - static TopicListRoute _fromState(GoRouterState state) => TopicListRoute(); +extension $ContainerListRouteExtension on ContainerListRoute { + static ContainerListRoute _fromState(GoRouterState state) => + ContainerListRoute(); String get location => GoRouteData.$location( - '/topics', + '/containers', ); void go(BuildContext context) => context.go(location); diff --git a/app/lib/features/geckoview/features/topics/data/database/drift/converters/color.dart b/app/lib/data/database/converters/color.dart similarity index 100% rename from app/lib/features/geckoview/features/topics/data/database/drift/converters/color.dart rename to app/lib/data/database/converters/color.dart diff --git a/app/lib/data/database/converters/icon_data.dart b/app/lib/data/database/converters/icon_data.dart new file mode 100644 index 00000000..ee42abd4 --- /dev/null +++ b/app/lib/data/database/converters/icon_data.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:flutter/widgets.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class IconDataJsonConverter + implements JsonConverter> { + const IconDataJsonConverter(); + + @override + IconData fromJson(Map json) { + return IconData( + json['codePoint'] as int, + fontFamily: json['fontFamily'] as String, + fontPackage: json['fontPackage'] as String, + ); + } + + @override + Map toJson(IconData iconData) { + return { + 'codePoint': iconData.codePoint, + 'fontFamily': iconData.fontFamily, + 'fontPackage': iconData.fontPackage, + }; + } +} + +class IconDataTypeConverter implements TypeConverter { + const IconDataTypeConverter(); + + @override + IconData fromSql(String fromDb) { + final json = jsonDecode(fromDb) as Map; + return const IconDataJsonConverter().fromJson(json); + } + + @override + String toSql(IconData value) { + assert( + value.fontFamily != null, + 'Font family must be provided to identify icon', + ); + + return jsonEncode(const IconDataJsonConverter().toJson(value)); + } +} diff --git a/app/lib/features/geckoview/features/topics/data/database/drift/converters/uri.dart b/app/lib/data/database/converters/uri.dart similarity index 100% rename from app/lib/features/geckoview/features/topics/data/database/drift/converters/uri.dart rename to app/lib/data/database/converters/uri.dart diff --git a/app/lib/data/database/functions/lexo_rank_functions.dart b/app/lib/data/database/functions/lexo_rank_functions.dart new file mode 100644 index 00000000..76e3c9d3 --- /dev/null +++ b/app/lib/data/database/functions/lexo_rank_functions.dart @@ -0,0 +1,58 @@ +import 'package:lexo_rank/lexo_rank.dart'; +import 'package:lexo_rank/lexo_rank/lexo_rank_bucket.dart'; +import 'package:sqlite3/common.dart'; + +String _nextRankOrMiddle(List args) { + final parsedBucket = LexoRankBucket.resolve(args[0]! as int); + + if (args[1] != null) { + final parsedRank = LexoRank.parse(args[1]! as String); + + return parsedRank.genNext().value; + } else { + return LexoRank.middle(bucket: parsedBucket).value; + } +} + +String _previousRankOrMiddle(List args) { + final parsedBucket = LexoRankBucket.resolve(args[0]! as int); + + if (args[1] != null) { + final parsedRank = LexoRank.parse(args[1]! as String); + + return parsedRank.genPrev().value; + } else { + return LexoRank.middle(bucket: parsedBucket).value; + } +} + +String _reorderAfter(List args) { + final first = (args[0] != null) ? LexoRank.parse(args[0]! as String) : null; + final last = (args[1] != null) ? LexoRank.parse(args[1]! as String) : null; + + if (first == null) { + throw Exception('Tab not found'); + } else if (last == null) { + return first.genNext().value; + } else { + return first.genBetween(last).value; + } +} + +void registerLexorankFunctions(CommonDatabase database) { + database.createFunction( + functionName: 'lexo_rank_next', + argumentCount: const AllowedArgumentCount(2), + function: _nextRankOrMiddle, + ); + database.createFunction( + functionName: 'lexo_rank_previous', + argumentCount: const AllowedArgumentCount(2), + function: _previousRankOrMiddle, + ); + database.createFunction( + functionName: 'lexo_rank_reorder_after', + argumentCount: const AllowedArgumentCount(2), + function: _reorderAfter, + ); +} diff --git a/app/lib/features/geckoview/domain/providers.dart b/app/lib/features/geckoview/domain/providers.dart index 3864d151..3f6131e5 100644 --- a/app/lib/features/geckoview/domain/providers.dart +++ b/app/lib/features/geckoview/domain/providers.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/core/logger.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -10,8 +11,6 @@ part 'providers.g.dart'; GeckoEventService eventService(EventServiceRef ref) { final service = GeckoEventService.setUp(); - unawaited(GeckoTabService().syncEvents()); - ref.onDispose(() { service.dispose(); }); @@ -19,6 +18,41 @@ GeckoEventService eventService(EventServiceRef ref) { return service; } +@Riverpod(keepAlive: true) +class EngineReadyState extends _$EngineReadyState { + @override + bool build() { + final eventService = ref.watch(eventServiceProvider); + + final currentState = + eventService.engineReadyStateEvents.valueOrNull ?? false; + + if (!currentState) { + unawaited( + eventService.engineReadyStateEvents + .firstWhere((value) => value == true) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + logger.w('Waiting for engine ready state timed out'); + return true; + }, + ).whenComplete(() => state = true), + ); + } + + final sub = eventService.engineReadyStateEvents.listen((value) { + state = value; + }); + + ref.onDispose(() async { + await sub.cancel(); + }); + + return currentState; + } +} + @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 d0034243..d0d49cf3 100644 --- a/app/lib/features/geckoview/domain/providers.g.dart +++ b/app/lib/features/geckoview/domain/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$eventServiceHash() => r'c36c6d895629002945c7b4eafba8389771e2eb2d'; +String _$eventServiceHash() => r'5aa357fdf0d217677a9a66ecb50417ac18929cad'; /// See also [eventService]. @ProviderFor(eventService) @@ -37,5 +37,21 @@ final selectedTabSessionNotifierProvider = ); typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef; +String _$engineReadyStateHash() => r'c682333e2e07cf0635aa7ae793a2088ca648c950'; + +/// See also [EngineReadyState]. +@ProviderFor(EngineReadyState) +final engineReadyStateProvider = + NotifierProvider.internal( + EngineReadyState.new, + name: r'engineReadyStateProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$engineReadyStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$EngineReadyState = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/domain/providers/selected_tab.dart b/app/lib/features/geckoview/domain/providers/selected_tab.dart index bf2ea931..1b262995 100644 --- a/app/lib/features/geckoview/domain/providers/selected_tab.dart +++ b/app/lib/features/geckoview/domain/providers/selected_tab.dart @@ -1,8 +1,9 @@ import 'dart:async'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; -import 'package:lensai/features/geckoview/features/topics/data/database/database.dart'; -import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'selected_tab.g.dart'; @@ -13,21 +14,29 @@ class SelectedTab extends _$SelectedTab { @override String? build() { - _db = ref.watch(tabDatabaseProvider); - final eventSerivce = ref.watch(eventServiceProvider); - final selectedTabSub = eventSerivce.selectedTabEvents.listen( - (tabId) { - state = tabId; + _db = ref.watch(tabDatabaseProvider); + + ref.listen( + fireImmediately: true, + engineReadyStateProvider, + (previous, next) async { + if (next) { + await GeckoTabService().syncEvents(onSelectedTabChange: true); + } }, ); - ref.listenSelf((previous, next) async { - if (next != null) { - await _db.tabLinkDao.touchTabLink(next, timestamp: DateTime.now()); - } - }); + final selectedTabSub = eventSerivce.selectedTabEvents.listen( + (tabId) async { + state = tabId; + + if (tabId != null) { + await _db.tabDao.touchTab(tabId, timestamp: DateTime.now()); + } + }, + ); ref.onDispose(() { unawaited(selectedTabSub.cancel()); diff --git a/app/lib/features/geckoview/domain/providers/selected_tab.g.dart b/app/lib/features/geckoview/domain/providers/selected_tab.g.dart index 14c208a8..170e0b12 100644 --- a/app/lib/features/geckoview/domain/providers/selected_tab.g.dart +++ b/app/lib/features/geckoview/domain/providers/selected_tab.g.dart @@ -6,7 +6,7 @@ part of 'selected_tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$selectedTabHash() => r'd57003aa20d8a6f711918821ae355e5537a55685'; +String _$selectedTabHash() => r'e49fb1c3938a97f384673e158742e56479aa8a8f'; /// See also [SelectedTab]. @ProviderFor(SelectedTab) diff --git a/app/lib/features/geckoview/domain/providers/tab_list.dart b/app/lib/features/geckoview/domain/providers/tab_list.dart new file mode 100644 index 00000000..d3b70b53 --- /dev/null +++ b/app/lib/features/geckoview/domain/providers/tab_list.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'tab_list.g.dart'; + +@Riverpod(keepAlive: true) +class TabList extends _$TabList { + late TabDatabase _db; + + List build() { + final eventService = ref.watch(eventServiceProvider); + + _db = ref.watch(tabDatabaseProvider); + + ref.listen( + fireImmediately: true, + engineReadyStateProvider, + (previous, next) async { + if (next) { + await GeckoTabService().syncEvents(onTabListChange: true); + } + }, + ); + + final tabListSub = eventService.tabListEvents.listen( + (tabs) async { + if (!const DeepCollectionEquality().equals(state, tabs)) { + //Make sure we only syncing empty lists when there has been tabs + //before as security measurement + final syncTabs = tabs.isNotEmpty || state.isNotEmpty; + + state = tabs; + + if (syncTabs) { + await _db.tabDao.syncTabs(retainTabIds: tabs); + } + } + }, + ); + + ref.onDispose(() { + unawaited(tabListSub.cancel()); + }); + + return []; + } +} diff --git a/app/lib/features/geckoview/domain/providers/tab_list.g.dart b/app/lib/features/geckoview/domain/providers/tab_list.g.dart new file mode 100644 index 00000000..4f84d3ac --- /dev/null +++ b/app/lib/features/geckoview/domain/providers/tab_list.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tab_list.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$tabListHash() => r'fd97d4692b1c74e91c5ebd4a75e6ff45ceb82ef9'; + +/// See also [TabList]. +@ProviderFor(TabList) +final tabListProvider = NotifierProvider>.internal( + TabList.new, + name: r'tabListProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$tabListHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$TabList = Notifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/domain/providers/tab_session.dart b/app/lib/features/geckoview/domain/providers/tab_session.dart index 09244106..4a3fb3df 100644 --- a/app/lib/features/geckoview/domain/providers/tab_session.dart +++ b/app/lib/features/geckoview/domain/providers/tab_session.dart @@ -33,6 +33,10 @@ class TabSession extends _$TabSession { return _sessionService.goForward(); } + Future exitFullscreen() { + return _sessionService.exitFullscreen(); + } + Future requestScreenshot() { return _sessionService.requestScreenshot(); } diff --git a/app/lib/features/geckoview/domain/providers/tab_session.g.dart b/app/lib/features/geckoview/domain/providers/tab_session.g.dart index 4a1b8bf2..912970d3 100644 --- a/app/lib/features/geckoview/domain/providers/tab_session.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_session.g.dart @@ -6,7 +6,7 @@ part of 'tab_session.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabSessionHash() => r'25afbead5c1110ca865e48a5009da70eb20d3a28'; +String _$tabSessionHash() => r'29b20f40af01e599ef80e198aec5f093d74efc6a'; /// 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 index 36c55997..99296d05 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:collection/collection.dart'; 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'; @@ -10,33 +9,25 @@ 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/features/topics/data/database/database.dart'; -import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; import 'package:lensai/features/geckoview/utils/image_helper.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:synchronized/synchronized.dart'; part 'tab_state.g.dart'; @Riverpod(keepAlive: true) class TabStates extends _$TabStates { - final _tabsService = GeckoTabService(); - - late TabDatabase _db; - - void _onTabListChange(List tabs) { - state = { - for (final tabId in tabs) tabId: state[tabId] ?? TabState.$default(tabId), - }; - } + final _lock = Lock(); 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, + title: (contentState.title.isNotEmpty) + ? contentState.title + : current.title, progress: contentState.progress, isPrivate: contentState.isPrivate, isFullScreen: contentState.isFullScreen, @@ -47,26 +38,24 @@ class TabStates extends _$TabStates { 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; + final current = state[tabId] ?? TabState.$default(tabId); 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; + final current = state[tabId] ?? TabState.$default(tabId); state = {...state}..[tabId] = current.copyWith.thumbnail(image); } @@ -74,7 +63,6 @@ class TabStates extends _$TabStates { final SecurityInfoEvent(:tabId, :securityInfo) = event; final current = state[tabId] ?? TabState.$default(tabId); - state = {...state}..[tabId] = current.copyWith.securityInfoState( SecurityState( secure: securityInfo.secure, @@ -88,7 +76,6 @@ class TabStates extends _$TabStates { final HistoryEvent(:tabId, :history) = event; final current = state[tabId] ?? TabState.$default(tabId); - state = {...state}..[tabId] = current.copyWith.historyState( HistoryState( items: history.items.nonNulls @@ -108,7 +95,6 @@ class TabStates extends _$TabStates { final ReaderableEvent(:tabId, :readerable) = event; final current = state[tabId] ?? TabState.$default(tabId); - state = {...state}..[tabId] = current.copyWith.readerableState( ReaderableState( readerable: readerable.readerable, @@ -117,13 +103,13 @@ class TabStates extends _$TabStates { ); } - void _onFindResults(FindResultsEvent event) { + void _onFindResultsChange(FindResultsEvent event) { final FindResultsEvent(:tabId, :results) = event; if (results.isNotEmpty) { - final current = state[tabId] ?? TabState.$default(tabId); final result = results.last; + final current = state[tabId] ?? TabState.$default(tabId); state = {...state}..[tabId] = current.copyWith.findResultState( FindResultState( activeMatchOrdinal: result.activeMatchOrdinal, @@ -134,64 +120,61 @@ class TabStates extends _$TabStates { } } - 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); - _db = ref.watch(tabDatabaseProvider); 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), + eventService.tabContentEvents.listen( + (event) async { + await _lock.synchronized(() => _onTabContentStateChange(event)); + }, + ), + eventService.iconEvents.listen( + (event) async { + await _lock.synchronized(() => _onIconChange(event)); + }, + ), + eventService.thumbnailEvents.listen( + (event) async { + await _lock.synchronized(() => _onThumbnailChange(event)); + }, + ), + eventService.securityInfoEvents.listen( + (event) async { + await _lock.synchronized(() => _onSecurityInfoStateChange(event)); + }, + ), + eventService.historyEvents.listen( + (event) async { + await _lock.synchronized(() => _onHistoryStateChange(event)); + }, + ), + eventService.readerableEvents.listen( + (event) async { + await _lock.synchronized(() => _onReaderableStateChange(event)); + }, + ), + eventService.findResultsEvent.listen( + (event) async { + await _lock.synchronized(() => _onFindResultsChange(event)); + }, + ), ]; - ref.listenSelf( + ref.listen( + fireImmediately: true, + engineReadyStateProvider, (previous, next) async { - if (!const DeepCollectionEquality() - .equals(previous?.keys.toSet(), next.keys.toSet())) { - final ids = next.keys.toList(); - if (ids.isNotEmpty) { - await _db.tabLinkDao.syncTabLinks(retainTabIds: ids); - } + if (next) { + await GeckoTabService().syncEvents( + onTabContentStateChange: true, + onIconChange: true, + onThumbnailChange: true, + onSecurityInfoStateChange: true, + onHistoryStateChange: true, + onFindResults: true, + ); } }, ); diff --git a/app/lib/features/geckoview/domain/providers/tab_state.g.dart b/app/lib/features/geckoview/domain/providers/tab_state.g.dart index 04c3ebc5..e982f993 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -169,7 +169,7 @@ final selectedTabStateProvider = AutoDisposeProvider.internal( ); typedef SelectedTabStateRef = AutoDisposeProviderRef; -String _$tabStatesHash() => r'f3ae09e49150955dcb29ec89f0f3fe63155a1d6e'; +String _$tabStatesHash() => r'dcd293279a901eeaab7728979e39e5ca3bee8c66'; /// See also [TabStates]. @ProviderFor(TabStates) diff --git a/app/lib/features/geckoview/domain/repositories/tab.dart b/app/lib/features/geckoview/domain/repositories/tab.dart index 4e4fb0ac..4cae0c30 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.dart @@ -1,6 +1,11 @@ import 'dart:async'; +import 'package:drift/drift.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'tab.g.dart'; @@ -9,6 +14,8 @@ part 'tab.g.dart'; class TabRepository extends _$TabRepository { final _tabsService = GeckoTabService(); + late TabDatabase _db; + Future addTab({ Uri? url, bool selectTab = true, @@ -48,5 +55,20 @@ class TabRepository extends _$TabRepository { } @override - void build() {} + void build() { + final eventSerivce = ref.watch(eventServiceProvider); + + _db = ref.watch(tabDatabaseProvider); + + final tabAddedSub = eventSerivce.tabAddedStream.listen( + (tabId) async { + final containerId = ref.read(selectedContainerProvider); + await _db.tabDao.upsertTab(tabId, containerId: Value(containerId)); + }, + ); + + ref.onDispose(() { + unawaited(tabAddedSub.cancel()); + }); + } } diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart index 48a26f50..91068fca 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -6,7 +6,7 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabRepositoryHash() => r'0abf38ef42f29015d81ccd11eb5990b741ebc616'; +String _$tabRepositoryHash() => r'05040223b83cd5ff80afaf89407043a10e4777cb'; /// See also [TabRepository]. @ProviderFor(TabRepository) diff --git a/app/lib/features/geckoview/features/browser/domain/providers.dart b/app/lib/features/geckoview/features/browser/domain/providers.dart index 668b1d7a..0540701b 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.dart @@ -2,11 +2,10 @@ import 'dart:async'; -import 'package:lensai/data/models/equatable_iterable.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart'; -import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; -import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_list.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -85,27 +84,15 @@ class ShowFindInPage extends _$ShowFindInPage { } @Riverpod() -Stream> availableTabIds(AvailableTabIdsRef ref, String? topicId) { - final db = ref.watch(tabDatabaseProvider); - final openTabs = ref - .watch( - tabStatesProvider.select( - (states) => - EquatableCollection(states.keys.toSet(), immutable: false), - ), - ) - .collection; +List availableTabIds( + AvailableTabIdsRef ref, + String? containerId, +) { + final containerTabs = ref.watch( + containerTabIdsProvider(containerId).select((value) => value.valueOrNull), + ); + final tabStates = ref.watch(tabListProvider); - if (topicId != null) { - return db.tabLinkDao.topicTabIds(topicId).watch().map( - (tabIds) => - tabIds.where((tabId) => openTabs.contains(tabId)).toList(), - ); - } else { - return db.tabLinkDao.allTabIds().watch().map( - (assignedTabIds) => openTabs - .where((tabId) => !assignedTabIds.contains(tabId)) - .toList(), - ); - } + return containerTabs?.where((tabId) => tabStates.contains(tabId)).toList() ?? + []; } diff --git a/app/lib/features/geckoview/features/browser/domain/providers.g.dart b/app/lib/features/geckoview/features/browser/domain/providers.g.dart index 9b3059e2..f7b71994 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.g.dart @@ -156,23 +156,23 @@ class _SelectedBangDataProviderElement String? get domain => (origin as SelectedBangDataProvider).domain; } -String _$availableTabIdsHash() => r'bea231cdf6211fc876b8031b5d72999eae5d7b15'; +String _$availableTabIdsHash() => r'427ea2e58c6bc27e2f16cffb850dc6621875e247'; /// See also [availableTabIds]. @ProviderFor(availableTabIds) const availableTabIdsProvider = AvailableTabIdsFamily(); /// See also [availableTabIds]. -class AvailableTabIdsFamily extends Family>> { +class AvailableTabIdsFamily extends Family> { /// See also [availableTabIds]. const AvailableTabIdsFamily(); /// See also [availableTabIds]. AvailableTabIdsProvider call( - String? topicId, + String? containerId, ) { return AvailableTabIdsProvider( - topicId, + containerId, ); } @@ -181,7 +181,7 @@ class AvailableTabIdsFamily extends Family>> { covariant AvailableTabIdsProvider provider, ) { return call( - provider.topicId, + provider.containerId, ); } @@ -201,14 +201,14 @@ class AvailableTabIdsFamily extends Family>> { } /// See also [availableTabIds]. -class AvailableTabIdsProvider extends AutoDisposeStreamProvider> { +class AvailableTabIdsProvider extends AutoDisposeProvider> { /// See also [availableTabIds]. AvailableTabIdsProvider( - String? topicId, + String? containerId, ) : this._internal( (ref) => availableTabIds( ref as AvailableTabIdsRef, - topicId, + containerId, ), from: availableTabIdsProvider, name: r'availableTabIdsProvider', @@ -219,7 +219,7 @@ class AvailableTabIdsProvider extends AutoDisposeStreamProvider> { dependencies: AvailableTabIdsFamily._dependencies, allTransitiveDependencies: AvailableTabIdsFamily._allTransitiveDependencies, - topicId: topicId, + containerId: containerId, ); AvailableTabIdsProvider._internal( @@ -229,14 +229,14 @@ class AvailableTabIdsProvider extends AutoDisposeStreamProvider> { required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, - required this.topicId, + required this.containerId, }) : super.internal(); - final String? topicId; + final String? containerId; @override Override overrideWith( - Stream> Function(AvailableTabIdsRef provider) create, + List Function(AvailableTabIdsRef provider) create, ) { return ProviderOverride( origin: this, @@ -247,42 +247,41 @@ class AvailableTabIdsProvider extends AutoDisposeStreamProvider> { dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, - topicId: topicId, + containerId: containerId, ), ); } @override - AutoDisposeStreamProviderElement> createElement() { + AutoDisposeProviderElement> createElement() { return _AvailableTabIdsProviderElement(this); } @override bool operator ==(Object other) { - return other is AvailableTabIdsProvider && other.topicId == topicId; + return other is AvailableTabIdsProvider && other.containerId == containerId; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, topicId.hashCode); + hash = _SystemHash.combine(hash, containerId.hashCode); return _SystemHash.finish(hash); } } -mixin AvailableTabIdsRef on AutoDisposeStreamProviderRef> { - /// The parameter `topicId` of this provider. - String? get topicId; +mixin AvailableTabIdsRef on AutoDisposeProviderRef> { + /// The parameter `containerId` of this provider. + String? get containerId; } class _AvailableTabIdsProviderElement - extends AutoDisposeStreamProviderElement> - with AvailableTabIdsRef { + extends AutoDisposeProviderElement> with AvailableTabIdsRef { _AvailableTabIdsProviderElement(super.provider); @override - String? get topicId => (origin as AvailableTabIdsProvider).topicId; + String? get containerId => (origin as AvailableTabIdsProvider).containerId; } String _$selectedBangTriggerHash() => diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart index 7e86b704..46ad5750 100644 --- a/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart @@ -5,8 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/data/models/equatable_iterable.dart'; import 'package:lensai/features/geckoview/domain/entities/tab_state.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'; +import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart'; class TabActionDialog extends HookConsumerWidget { final TabState initialTab; @@ -22,20 +22,21 @@ class TabActionDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab; - final tabTopicId = ref.watch( - tabTopicIdProvider(initialTab.id).select((value) => value.valueOrNull), + final tabContainerId = ref.watch( + tabContainerIdProvider(initialTab.id) + .select((value) => value.valueOrNull), ); - final topics = ref + final containers = ref .watch( - topicsWithCountProvider.select( + containersWithCountProvider.select( (value) => EquatableCollection(value.valueOrNull, immutable: true), ), ) .collection; - final selectedTopic = - topics?.firstWhereOrNull((topic) => topic.id == tabTopicId); + final selectedContainer = containers + ?.firstWhereOrNull((container) => container.id == tabContainerId); final expansionController = useExpansionTileController(); @@ -68,22 +69,23 @@ class TabActionDialog extends HookConsumerWidget { width: double.maxFinite, child: ExpansionTile( controller: expansionController, - leading: (selectedTopic != null) - ? CircleAvatar(backgroundColor: selectedTopic.color) + leading: (selectedContainer != null) + ? CircleAvatar(backgroundColor: selectedContainer.color) : null, - title: (selectedTopic != null) - ? Text(selectedTopic.name ?? 'New Topic') - : const Text('Assign a Topic'), - children: topics - ?.where((topic) => topic.id != tabTopicId) + title: (selectedContainer != null) + ? Text(selectedContainer.name ?? 'New Container') + : const Text('Assign a Container'), + children: containers + ?.where((container) => container.id != tabContainerId) .map( - (topic) => ListTile( - leading: CircleAvatar(backgroundColor: topic.color), - title: Text(topic.name ?? 'New Topic'), + (container) => ListTile( + leading: + CircleAvatar(backgroundColor: container.color), + title: Text(container.name ?? 'New Container'), onTap: () async { await ref - .read(tabLinkRepositoryProvider.notifier) - .assignTab(initialTab.id, topic.id); + .read(tabDataRepositoryProvider.notifier) + .assignContainer(initialTab.id, container.id); expansionController.collapse(); }, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart index f48f9480..45545fbb 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:text_scroll/text_scroll.dart'; class AppBarTitle extends StatelessWidget { @@ -23,11 +24,16 @@ class AppBarTitle extends StatelessWidget { color: Theme.of(context).colorScheme.errorContainer, size: 14, ); - } else { + } else if (!tab.isLoading) { return const Icon( MdiIcons.lock, size: 14, ); + } else { + return const Icon( + MdiIcons.timerSand, + size: 14, + ); } } @@ -38,10 +44,18 @@ class AppBarTitle extends StatelessWidget { onTap: onTap, child: Row( children: [ - RawImage( - image: tab.icon?.value, - height: 16, - width: 16, + Skeletonizer( + enabled: tab.icon == null, + child: Skeleton.replace( + replacement: const Bone.icon( + size: 16, + ), + child: RawImage( + image: tab.icon?.value, + height: 16, + width: 16, + ), + ), ), const SizedBox( width: 8, @@ -51,20 +65,29 @@ class AppBarTitle extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - TextScroll( - key: ValueKey(tab.title), - tab.title, - style: theme.textTheme.bodyLarge - ?.copyWith(color: theme.colorScheme.onSurface), - // mode: TextScrollMode.bouncing, - velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), - delayBefore: const Duration(milliseconds: 500), - pauseBetween: const Duration(milliseconds: 5000), - fadedBorder: true, - fadeBorderSide: FadeBorderSide.right, - fadedBorderWidth: 0.05, - intervalSpaces: 4, - numberOfReps: 2, + Skeletonizer( + enabled: tab.title.isEmpty, + child: Skeleton.replace( + replacement: const Padding( + padding: EdgeInsets.only(right: 4, top: 1, bottom: 1), + child: Bone.text(), + ), + child: TextScroll( + key: ValueKey(tab.title), + tab.title, + style: theme.textTheme.bodyLarge + ?.copyWith(color: theme.colorScheme.onSurface), + // mode: TextScrollMode.bouncing, + velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), + delayBefore: const Duration(milliseconds: 500), + pauseBetween: const Duration(milliseconds: 5000), + fadedBorder: true, + fadeBorderSide: FadeBorderSide.right, + fadedBorderWidth: 0.05, + intervalSpaces: 4, + numberOfReps: 2, + ), + ), ), Row( children: [ diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart index 0df3ca10..6d33b768 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -11,9 +12,11 @@ import 'package:lensai/features/geckoview/features/browser/domain/providers.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/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/geckoview/features/tabs/domain/providers/selected_container.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_chips.dart'; +import 'package:reorderable_grid/reorderable_grid.dart'; class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { static const _headerSize = 104.0; @@ -52,10 +55,10 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { ), TextButton.icon( onPressed: () async { - final topic = ref.read(selectedTopicProvider); + final container = ref.read(selectedContainerProvider); await ref - .read(tabLinkRepositoryProvider.notifier) - .closeAllTabs(topic); + .read(tabDataRepositoryProvider.notifier) + .closeAllTabs(container); onClose(); }, @@ -64,7 +67,7 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { ), ], ), - TopicChips(), + ContainerChips(), const SizedBox(height: 8), ], ), @@ -125,14 +128,11 @@ class ViewTabsSheetWidget extends HookConsumerWidget { ), HookConsumer( builder: (context, ref, child) { - final topic = ref.watch(selectedTopicProvider); + final container = ref.watch(selectedContainerProvider); final availableTabs = ref .watch( - availableTabIdsProvider(topic).select( - (value) => EquatableCollection( - value.valueOrNull ?? [], - immutable: true, - ), + availableTabIdsProvider(container).select( + (value) => EquatableCollection(value, immutable: true), ), ) .collection; @@ -156,8 +156,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget { availableTabs.indexWhere((webView) => webView == activeTab); if (index > -1) { - final reversedIndex = availableTabs.length - 1 - index; - final offset = (reversedIndex ~/ 2) * itemHeight; + final offset = (index ~/ 2) * itemHeight; if (offset != sheetScrollController.offset) { unawaited( @@ -175,21 +174,15 @@ class ViewTabsSheetWidget extends HookConsumerWidget { [availableTabs, activeTab], ); - return SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - sliver: SliverGrid.count( - //Sync values for itemHeight calculation _calculateItemHeight - childAspectRatio: 0.75, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - crossAxisCount: 2, - children: availableTabs.reversed - .map( - (tabId) => Consumer( - key: ValueKey(tabId), + final tabs = useMemoized( + () => availableTabs + .mapIndexed( + (index, tabId) => ReorderableGridDelayedDragStartListener( + key: ValueKey(tabId), + index: index, + child: Consumer( builder: (context, ref, child) { final tab = ref.watch(tabStateProvider(tabId)); - return (tab != null) ? TabPreview( tab: tab, @@ -207,7 +200,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget { onClose(); } }, - onLongPress: () { + onDoubleTap: () { ref .read( overlayDialogControllerProvider @@ -236,8 +229,52 @@ class ViewTabsSheetWidget extends HookConsumerWidget { : const SizedBox.shrink(); }, ), - ) - .toList(), + ), + ) + .toList(), + [availableTabs, activeTab], + ); + + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + sliver: SliverReorderableGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + //Sync values for itemHeight calculation _calculateItemHeight + childAspectRatio: 0.75, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + crossAxisCount: 2, + ), + itemCount: tabs.length, + itemBuilder: (context, index) => tabs[index], + onReorder: (oldIndex, newIndex) async { + final containerRepository = + ref.read(containerRepositoryProvider.notifier); + + final tabId = availableTabs[oldIndex]; + final containerId = await ref + .read(tabDataRepositoryProvider.notifier) + .containerTabId(tabId); + + final String key; + if (newIndex <= 0) { + key = await containerRepository + .getLeadingOrderKey(containerId); + } else if (newIndex >= availableTabs.length - 1) { + key = await containerRepository + .getTrailingOrderKey(containerId); + } else { + final orderAfterIndex = newIndex - 1; + key = await containerRepository.getOrderKeyAfterTab( + availableTabs[orderAfterIndex], + containerId, + ); + } + + await ref + .read(tabDataRepositoryProvider.notifier) + .assignOrderKey(tabId, key); + }, ), ); }, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart index 203abd5a..8a903d54 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart @@ -6,14 +6,14 @@ class TabPreview extends StatelessWidget { final bool isActive; final VoidCallback? onTap; - final VoidCallback? onLongPress; + final VoidCallback? onDoubleTap; final VoidCallback? onDelete; const TabPreview({ required this.tab, required this.isActive, this.onTap, - this.onLongPress, + this.onDoubleTap, this.onDelete, super.key, }); @@ -35,7 +35,7 @@ class TabPreview extends StatelessWidget { child: InkWell( borderRadius: const BorderRadius.all(Radius.circular(16.0)), onTap: onTap, - onLongPress: onLongPress, + onDoubleTap: onDoubleTap, child: Column( children: [ Row( diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart index 466e116b..59431bec 100644 --- a/app/lib/features/geckoview/features/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/providers/tab_state.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_list.dart'; class TabsActionButton extends HookConsumerWidget { final bool isActive; @@ -16,7 +16,7 @@ class TabsActionButton extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final tabCount = ref.watch(tabStatesProvider.select((tabs) => tabs.length)); + final tabCount = ref.watch(tabListProvider.select((tabs) => tabs.length)); return InkWell( onTap: onTap, diff --git a/app/lib/features/geckoview/features/browser/screens/browser.dart b/app/lib/features/geckoview/features/browser/screens/browser.dart index 672387cd..59d0219a 100644 --- a/app/lib/features/geckoview/features/browser/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/screens/browser.dart @@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/logger.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_list.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; @@ -112,586 +113,652 @@ class BrowserScreen extends HookConsumerWidget { }, ); - return Scaffold( - bottomNavigationBar: BottomAppBar( - height: AppBar().preferredSize.height, - padding: EdgeInsets.zero, - child: AppBar( - automaticallyImplyLeading: false, - titleSpacing: 8.0, - title: (selectedTabId != null) - ? Consumer( - builder: (context, ref, child) { - final tabState = ref.watch(tabStateProvider(selectedTabId)); + return PopScope( + //We need this for BackButtonListener to work downstream + //No direct pop result will be handled here + canPop: false, + child: Scaffold( + bottomNavigationBar: Consumer( + builder: (context, ref, child) { + final tabInFullScreen = ref.watch( + selectedTabStateProvider + .select((value) => value?.isFullScreen ?? false), + ); - return (tabState != null) - ? AppBarTitle( - tab: tabState, - onTap: () { - ref - .read( - overlayDialogControllerProvider.notifier, - ) - .show( - WebPageDialog( - url: tabState.url, - precachedInfo: tabState, - onDismiss: ref + return Visibility( + visible: !tabInFullScreen, + child: BottomAppBar( + height: AppBar().preferredSize.height, + padding: EdgeInsets.zero, + child: AppBar( + automaticallyImplyLeading: false, + titleSpacing: 8.0, + title: (selectedTabId != null && + displayedSheet is! ViewTabsSheet) + ? Consumer( + builder: (context, ref, child) { + final tabState = + ref.watch(tabStateProvider(selectedTabId)); + + return (tabState != null) + ? AppBarTitle( + tab: tabState, + onTap: () { + ref .read( overlayDialogControllerProvider .notifier, ) - .dismiss, - ), - ); - }, - ) - : const SizedBox.shrink(); - }, - ) - : HookBuilder( - builder: (context) { - final activeTool = useListenableSelector( - activeKagiTool, - () { - if (displayedSheet is CreateTabSheetWidget) { - return null; - } - - return activeKagiTool.value; - }, - ); - - return Row( - children: [ - IconButton( - color: (activeTool == KagiTool.search) - ? Theme.of(context).colorScheme.primary - : null, - onPressed: () { - ref - .read(createTabStreamProvider.notifier) - .createTab( - CreateTabSheet( - preferredTool: KagiTool.search, - ), - ); + .show( + WebPageDialog( + url: tabState.url, + precachedInfo: tabState, + onDismiss: ref + .read( + overlayDialogControllerProvider + .notifier, + ) + .dismiss, + ), + ); + }, + ) + : const SizedBox.shrink(); + }, + ) + : HookBuilder( + builder: (context) { + final activeTool = useListenableSelector( + activeKagiTool, + () { + if (displayedSheet is CreateTabSheetWidget) { + return null; + } + + return activeKagiTool.value; + }, + ); + + return Row( + children: [ + IconButton( + color: (activeTool == KagiTool.search) + ? Theme.of(context).colorScheme.primary + : null, + onPressed: () { + ref + .read(createTabStreamProvider.notifier) + .createTab( + CreateTabSheet( + preferredTool: KagiTool.search, + ), + ); + }, + icon: Icon(KagiTool.search.icon), + ), + IconButton( + color: (activeTool == KagiTool.summarizer) + ? Theme.of(context).colorScheme.primary + : null, + onPressed: () { + ref + .read(createTabStreamProvider.notifier) + .createTab( + CreateTabSheet( + preferredTool: KagiTool.summarizer, + ), + ); + }, + icon: Icon(KagiTool.summarizer.icon), + ), + if (showEarlyAccessFeatures) + IconButton( + color: (activeTool == KagiTool.assistant) + ? Theme.of(context).colorScheme.primary + : null, + onPressed: () { + ref + .read( + createTabStreamProvider.notifier) + .createTab( + CreateTabSheet( + preferredTool: KagiTool.assistant, + ), + ); + }, + icon: Icon(KagiTool.assistant.icon), + ), + ], + ); }, - icon: Icon(KagiTool.search.icon), ), - IconButton( - color: (activeTool == KagiTool.summarizer) - ? Theme.of(context).colorScheme.primary - : null, - onPressed: () { - ref - .read(createTabStreamProvider.notifier) - .createTab( - CreateTabSheet( - preferredTool: KagiTool.summarizer, - ), + actions: [ + if (selectedTabId != null) ReaderButton(), + if (quickAction != null) + InkWell( + onTap: () async { + var tab = CreateTabSheet( + preferredTool: quickAction, + ); + + if (quickActionVoice) { + final completer = Completer(); + + final isServiceAvailable = + await SpeechToTextGoogleDialog.getInstance() + .showGoogleDialog( + onTextReceived: (data) { + completer.complete(data.toString()); + }, + // locale: "en-US", + ); + + if (!isServiceAvailable) { + if (context.mounted) { + ui_helper.showErrorMessage( + context, + 'Service is not available', ); + } + } + + tab = CreateTabSheet( + preferredTool: quickAction, + content: await completer.future, + ); + } + + ref + .read(createTabStreamProvider.notifier) + .createTab(tab); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 15.0, + horizontal: 8.0, + ), + child: Icon( + quickActionVoice ? Icons.mic : quickAction.icon), + ), + ), + TabsActionButton( + isActive: displayedSheet is ViewTabsSheet, + onTap: () { + if (displayedSheet case ViewTabsSheet()) { + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); + } else { + ref + .read(bottomSheetControllerProvider.notifier) + .show(ViewTabsSheet()); + } + }, + ), + MenuAnchor( + controller: menuController, + builder: (context, controller, child) { + return Padding( + padding: const EdgeInsets.only(right: 4.0), + child: InkWell( + onTap: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Padding( + padding: EdgeInsets.symmetric( + vertical: 15.0, horizontal: 8.0), + child: Icon(Icons.more_vert), + ), + ), + ); + }, + menuChildren: [ + MenuItemButton( + onPressed: () async { + await context.push(AboutRoute().location); }, - icon: Icon(KagiTool.summarizer.icon), + leadingIcon: const Icon(Icons.info), + child: const Text('About'), + ), + const Divider(), + MenuItemButton( + onPressed: () async { + await context.push(SettingsRoute().location); + }, + leadingIcon: const Icon(Icons.settings), + child: const Text('Settings'), + ), + const Divider(), + MenuItemButton( + onPressed: () async { + await context.push(BangCategoriesRoute().location); + }, + leadingIcon: const Icon(MdiIcons.exclamationThick), + child: const Text('Bangs'), ), if (showEarlyAccessFeatures) - IconButton( - color: (activeTool == KagiTool.assistant) - ? Theme.of(context).colorScheme.primary - : null, + MenuItemButton( + onPressed: () async { + await context + .push(ChatArchiveListRoute().location); + }, + leadingIcon: const Icon(MdiIcons.archive), + child: const Text('Chat Archive'), + ), + const Divider(), + if (showEarlyAccessFeatures) + MenuItemButton( onPressed: () { ref .read(createTabStreamProvider.notifier) .createTab( CreateTabSheet( - preferredTool: KagiTool.assistant, - ), + preferredTool: KagiTool.assistant), ); }, - icon: Icon(KagiTool.assistant.icon), + leadingIcon: Icon(KagiTool.assistant.icon), + child: const Text('Assistant'), ), - ], - ); - }, - ), - actions: [ - if (selectedTabId != null) ReaderButton(), - if (quickAction != null) - InkWell( - onTap: () async { - var tab = CreateTabSheet( - preferredTool: quickAction, - ); - - if (quickActionVoice) { - final completer = Completer(); - - final isServiceAvailable = - await SpeechToTextGoogleDialog.getInstance() - .showGoogleDialog( - onTextReceived: (data) { - completer.complete(data.toString()); - }, - // locale: "en-US", - ); - - if (!isServiceAvailable) { - if (context.mounted) { - ui_helper.showErrorMessage( - context, - 'Service is not available', - ); - } - } - - tab = CreateTabSheet( - preferredTool: quickAction, - content: await completer.future, - ); - } - - ref.read(createTabStreamProvider.notifier).createTab(tab); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 15.0, - horizontal: 8.0, - ), - child: Icon(quickActionVoice ? Icons.mic : quickAction.icon), - ), - ), - TabsActionButton( - isActive: displayedSheet is ViewTabsSheet, - onTap: () { - if (displayedSheet case ViewTabsSheet()) { - ref.read(bottomSheetControllerProvider.notifier).dismiss(); - } else { - ref - .read(bottomSheetControllerProvider.notifier) - .show(ViewTabsSheet()); - } - }, - ), - MenuAnchor( - controller: menuController, - builder: (context, controller, child) { - return Padding( - padding: const EdgeInsets.only(right: 4.0), - child: InkWell( - onTap: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: const Padding( - padding: - EdgeInsets.symmetric(vertical: 15.0, horizontal: 8.0), - child: Icon(Icons.more_vert), - ), - ), - ); - }, - menuChildren: [ - MenuItemButton( - onPressed: () async { - await context.push(AboutRoute().location); - }, - leadingIcon: const Icon(Icons.info), - child: const Text('About'), - ), - const Divider(), - MenuItemButton( - onPressed: () async { - await context.push(SettingsRoute().location); - }, - leadingIcon: const Icon(Icons.settings), - child: const Text('Settings'), - ), - const Divider(), - MenuItemButton( - onPressed: () async { - await context.push(BangCategoriesRoute().location); - }, - leadingIcon: const Icon(MdiIcons.exclamationThick), - child: const Text('Bangs'), - ), - if (showEarlyAccessFeatures) - MenuItemButton( - onPressed: () async { - await context.push(ChatArchiveListRoute().location); - }, - leadingIcon: const Icon(MdiIcons.archive), - child: const Text('Chat Archive'), - ), - const Divider(), - if (showEarlyAccessFeatures) - MenuItemButton( - onPressed: () { - ref.read(createTabStreamProvider.notifier).createTab( - CreateTabSheet(preferredTool: KagiTool.assistant), - ); - }, - leadingIcon: Icon(KagiTool.assistant.icon), - child: const Text('Assistant'), - ), - MenuItemButton( - onPressed: () { - ref.read(createTabStreamProvider.notifier).createTab( - CreateTabSheet(preferredTool: KagiTool.summarizer), - ); - }, - leadingIcon: Icon(KagiTool.summarizer.icon), - child: const Text('Summarizer'), - ), - MenuItemButton( - onPressed: () { - ref.read(createTabStreamProvider.notifier).createTab( - CreateTabSheet(preferredTool: KagiTool.search), - ); - }, - leadingIcon: Icon(KagiTool.search.icon), - child: const Text('Search'), - ), - const Divider(), - if (selectedTabId != null) - MenuItemButton( - onPressed: () async { - final tabState = - ref.read(tabStateProvider(selectedTabId)); - - 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 (selectedTabId != null) - MenuItemButton( - onPressed: () async { - final tabState = - ref.read(tabStateProvider(selectedTabId)); - - if (tabState?.url case final Uri url) { - await Share.shareUri(url); - } - }, - leadingIcon: const Icon(Icons.share), - child: const Text('Share'), - ), - if (selectedTabId != null) const Divider(), - if (selectedTabId != null) - MenuItemButton( - onPressed: () { - ref - .read(findInPageVisibilityControllerProvider.notifier) - .show(); - }, - leadingIcon: const Icon(Icons.search), - child: const Text('Find in page'), - ), - if (selectedTabId != null) const Divider(), - if (selectedTabId != null) - MenuItemButton( - onPressed: () async { - final controller = - ref.read(tabSessionProvider(selectedTabId).notifier); - - await controller.reload(); - menuController.close(); - }, - leadingIcon: const Icon(Icons.refresh), - child: const Text('Reload'), - ), - if (selectedTabId != null) const Divider(), - if (selectedTabId != null) - Consumer( - builder: (context, ref, child) { - final history = ref.watch( - tabStateProvider(selectedTabId) - .select((value) => value?.historyState), - ); - - return Row( - children: [ - Expanded( - child: IconButton( - onPressed: (history?.canGoBack == true) - ? () async { - final controller = ref.read( - tabSessionProvider( - selectedTabId, - ).notifier, - ); - - await controller.goBack(); - menuController.close(); - } - : null, - icon: const Icon(Icons.arrow_back), - ), - ), - const SizedBox(height: 48, child: VerticalDivider()), - Expanded( - child: IconButton( - onPressed: (history?.canGoForward == true) - ? () async { - final controller = ref.read( - tabSessionProvider( - selectedTabId, - ).notifier, - ); - - await controller.goForward(); - menuController.close(); - } - : null, - icon: const Icon(Icons.arrow_forward), - ), - ), - ], - ); - }, - ), - ], - ), - ], - ), - ), - body: OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return displayedOverlayDialog!; - }, - child: Listener( - onPointerDown: (displayedSheet != null) - ? (_) { - ref.read(bottomSheetControllerProvider.notifier).dismiss(); - } - : null, - child: HookConsumer( - builder: (context, ref, child) { - return BackButtonListener( - onBackButtonPressed: () async { - 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?.path != - BrowserRoute().location) { - return false; - } - - if (displayedSheet != null) { - ref.read(bottomSheetControllerProvider.notifier).dismiss(); - return true; - } - - if (displayedOverlayDialog != null) { - ref - .read(overlayDialogControllerProvider.notifier) - .dismiss(); - return true; - } - - if (tabState?.historyState.canGoBack == true) { - lastBackButtonPress.value = null; - - final controller = ref.read( - tabSessionProvider(selectedTabId).notifier, - ); - - await controller.goBack(); - return true; - } - - if (lastBackButtonPress.value != null && - DateTime.now().difference(lastBackButtonPress.value!) < - const Duration(seconds: 2)) { - lastBackButtonPress.value = null; - - if (tabState != null && tabCount > 1) { - await ref - .read(tabStatesProvider.notifier) - .closeTab(tabState.id); - return true; - } else { - //Mark back as unhandled and navigator will pop - return false; - } - } else { - lastBackButtonPress.value = DateTime.now(); - ScaffoldMessenger.of(context) - ..clearSnackBars() - ..showSnackBar( - SnackBar( - content: (tabCount > 1) - ? const Text( - 'Please click BACK again to close current tab', - ) - : const Text( - 'Please click BACK again to exit app', - ), + MenuItemButton( + onPressed: () { + ref + .read(createTabStreamProvider.notifier) + .createTab( + CreateTabSheet( + preferredTool: KagiTool.summarizer), + ); + }, + leadingIcon: Icon(KagiTool.summarizer.icon), + child: const Text('Summarizer'), ), - ); + MenuItemButton( + onPressed: () { + ref + .read(createTabStreamProvider.notifier) + .createTab( + CreateTabSheet( + preferredTool: KagiTool.search), + ); + }, + leadingIcon: Icon(KagiTool.search.icon), + child: const Text('Search'), + ), + const Divider(), + if (selectedTabId != null) + MenuItemButton( + onPressed: () async { + final tabState = + ref.read(tabStateProvider(selectedTabId)); - return true; - } - }, - child: Stack( - children: [ - SafeArea( - child: Stack( - children: [ - GeckoView( - preInitializationStep: () async { - await ref - .read(eventServiceProvider) - .fragmentReadyStateEvents - .firstWhere((state) => state == true) - .timeout( - const Duration(seconds: 3), - onTimeout: () { - logger.e( - 'Browser fragement not reported ready, trying to intitialize anyways', - ); - return true; - }, + 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 (selectedTabId != null) + MenuItemButton( + onPressed: () async { + final tabState = + ref.read(tabStateProvider(selectedTabId)); + + if (tabState?.url case final Uri url) { + await Share.shareUri(url); + } + }, + leadingIcon: const Icon(Icons.share), + child: const Text('Share'), + ), + if (selectedTabId != null) const Divider(), + if (selectedTabId != null) + MenuItemButton( + onPressed: () { + ref + .read(findInPageVisibilityControllerProvider + .notifier) + .show(); + }, + leadingIcon: const Icon(Icons.search), + child: const Text('Find in page'), + ), + if (selectedTabId != null) const Divider(), + if (selectedTabId != null) + MenuItemButton( + onPressed: () async { + final controller = ref.read( + tabSessionProvider(selectedTabId).notifier); + + await controller.reload(); + menuController.close(); + }, + leadingIcon: const Icon(Icons.refresh), + child: const Text('Reload'), + ), + if (selectedTabId != null) const Divider(), + if (selectedTabId != null) + Consumer( + builder: (context, ref, child) { + final history = ref.watch( + tabStateProvider(selectedTabId) + .select((value) => value?.historyState), + ); + + return Row( + children: [ + Expanded( + child: (history?.canGoBack == true) + ? IconButton( + onPressed: () async { + final controller = ref.read( + tabSessionProvider( + selectedTabId, + ).notifier, + ); + + await controller.goBack(); + menuController.close(); + }, + icon: const Icon(Icons.arrow_back), + ) + : IconButton( + onPressed: () async { + await ref + .read( + tabRepositoryProvider + .notifier, + ) + .closeTab(selectedTabId); + menuController.close(); + }, + icon: const Icon(Icons.close), + ), + ), + const SizedBox( + height: 48, child: VerticalDivider()), + Expanded( + child: IconButton( + onPressed: (history?.canGoForward == true) + ? () async { + final controller = ref.read( + tabSessionProvider( + selectedTabId, + ).notifier, + ); + + await controller.goForward(); + menuController.close(); + } + : null, + icon: const Icon(Icons.arrow_forward), + ), + ), + ], ); }, ), - 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) - ModalBarrier( - color: Theme.of(context).dialogTheme.barrierColor ?? - Colors.black54, - ), ], ), - ); - }, + ), + ); + }, + ), + body: OverlayPortal( + controller: overlayController, + overlayChildBuilder: (context) { + return displayedOverlayDialog!; + }, + child: Listener( + onPointerDown: (displayedSheet != null) + ? (_) { + ref.read(bottomSheetControllerProvider.notifier).dismiss(); + } + : null, + child: HookConsumer( + builder: (context, ref, child) { + return BackButtonListener( + onBackButtonPressed: () async { + final tabState = (selectedTabId != null) + ? ref.read(tabStateProvider(selectedTabId)) + : null; + + final tabCount = + ref.read(tabListProvider.select((tabs) => tabs.length)); + + //Don't do anything if a child route is active + if (GoRouterState.of(context).topRoute?.path != + BrowserRoute().location) { + return false; + } + + if (displayedSheet != null) { + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); + return true; + } + + if (displayedOverlayDialog != null) { + ref + .read(overlayDialogControllerProvider.notifier) + .dismiss(); + return true; + } + + if (tabState?.isFullScreen == true) { + await ref + .read(selectedTabSessionNotifierProvider) + .exitFullscreen(); + return true; + } + + if (tabState?.historyState.canGoBack == true) { + lastBackButtonPress.value = null; + + final controller = ref.read( + tabSessionProvider(selectedTabId).notifier, + ); + + await controller.goBack(); + return true; + } + + if (lastBackButtonPress.value != null && + DateTime.now().difference(lastBackButtonPress.value!) < + const Duration(seconds: 2)) { + lastBackButtonPress.value = null; + + if (tabState != null && tabCount > 1) { + await ref + .read(tabRepositoryProvider.notifier) + .closeTab(tabState.id); + return true; + } else { + //Mark back as unhandled and navigator will pop + return false; + } + } else { + lastBackButtonPress.value = DateTime.now(); + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar( + SnackBar( + content: (tabCount > 1) + ? const Text( + 'Please click BACK again to close current tab', + ) + : const Text( + 'Please click BACK again to exit app', + ), + ), + ); + + return true; + } + }, + child: Stack( + children: [ + SafeArea( + child: Stack( + children: [ + GeckoView( + preInitializationStep: () async { + await ref + .read(eventServiceProvider) + .viewReadyStateEvents + .firstWhere((state) => state == true) + .timeout( + const Duration(seconds: 3), + onTimeout: () { + logger.e( + 'Browser fragement not reported ready, trying to intitialize anyways', + ); + return true; + }, + ); + }, + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Consumer( + builder: (context, ref, child) { + final value = ref.watch( + selectedTabStateProvider.select( + (state) { + if (state?.isLoading == true) { + return state?.progress ?? 100; + } + + //When not loading we assumed finished + return 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?.isLoading == true && + state?.progress != null && + state!.progress < 100) + ? 4.0 + : 0.0, + ), + ), + ); + + return FindInPageWidget(padding: value); + }, + ), + ), + ], + ), + ), + if (displayedSheet != null) + ModalBarrier( + color: Theme.of(context).dialogTheme.barrierColor ?? + Colors.black54, + ), + ], + ), + ); + }, + ), ), ), + floatingActionButton: ReaderAppearanceButton(), + bottomSheet: (displayedSheet != null) + ? NotificationListener( + onNotification: (notification) { + if (notification.extent <= 0.1) { + ref.read(bottomSheetControllerProvider.notifier).dismiss(); + return true; + } else { + ref + .read(bottomSheetExtendProvider.notifier) + .add(notification.extent); + } + + return false; + }, + child: switch (displayedSheet) { + ViewTabsSheet() => DraggableScrollableSheet( + key: ValueKey(displayedSheet), + expand: false, + minChildSize: 0.1, + maxChildSize: _realtiveSafeArea(context), + builder: (context, scrollController) { + return ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), + ), + child: ViewTabsSheetWidget( + sheetScrollController: scrollController, + onClose: () { + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); + }, + ), + ); + }, + ), + final CreateTabSheet parameter => DraggableScrollableSheet( + key: ValueKey(displayedSheet), + expand: false, + initialChildSize: 0.8, + minChildSize: 0.1, + maxChildSize: _realtiveSafeArea(context), + builder: (context, scrollController) { + return SingleChildScrollView( + controller: scrollController, + child: CreateTabSheetWidget( + key: ValueKey(parameter), + parameter: parameter, + onSubmit: (url) async { + await ref + .read(tabRepositoryProvider.notifier) + .addTab(url: url); + + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); + }, + onActiveToolChanged: (tool) { + activeKagiTool.value = tool; + }, + ), + ); + }, + ), + }, + ) + : null, ), - floatingActionButton: ReaderAppearanceButton(), - bottomSheet: (displayedSheet != null) - ? NotificationListener( - onNotification: (notification) { - if (notification.extent <= 0.1) { - ref.read(bottomSheetControllerProvider.notifier).dismiss(); - return true; - } else { - ref - .read(bottomSheetExtendProvider.notifier) - .add(notification.extent); - } - - return false; - }, - child: switch (displayedSheet) { - ViewTabsSheet() => DraggableScrollableSheet( - key: ValueKey(displayedSheet), - expand: false, - minChildSize: 0.1, - maxChildSize: _realtiveSafeArea(context), - builder: (context, scrollController) { - return ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28), - ), - child: ViewTabsSheetWidget( - sheetScrollController: scrollController, - onClose: () { - ref - .read(bottomSheetControllerProvider.notifier) - .dismiss(); - }, - ), - ); - }, - ), - final CreateTabSheet parameter => DraggableScrollableSheet( - key: ValueKey(displayedSheet), - expand: false, - initialChildSize: 0.8, - minChildSize: 0.1, - maxChildSize: _realtiveSafeArea(context), - builder: (context, scrollController) { - return SingleChildScrollView( - controller: scrollController, - child: CreateTabSheetWidget( - key: ValueKey(parameter), - parameter: parameter, - onSubmit: (url) async { - await ref - .read(tabRepositoryProvider.notifier) - .addTab(url: url); - - ref - .read(bottomSheetControllerProvider.notifier) - .dismiss(); - }, - onActiveToolChanged: (tool) { - activeKagiTool.value = tool; - }, - ), - ); - }, - ), - }, - ) - : null, ); } } diff --git a/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.dart b/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.dart index bd465b2e..4e13d83d 100644 --- a/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.dart +++ b/app/lib/features/geckoview/features/readerview/presentation/controllers/readerable.dart @@ -11,6 +11,7 @@ class ReaderableScreenController extends _$ReaderableScreenController { Future toggleReaderView(bool enable) async { state = const AsyncValue.loading(); + final eventChange = ref.read(eventServiceProvider).readerableEvents.first; final toggle = _service.toggleReaderView(enable); diff --git a/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart b/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart new file mode 100644 index 00000000..051cc136 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart @@ -0,0 +1,76 @@ +import 'dart:ui'; + +import 'package:drift/drift.dart'; +import 'package:lensai/core/uuid.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; + +part 'container.g.dart'; + +@DriftAccessor() +class ContainerDao extends DatabaseAccessor + with _$ContainerDaoMixin { + ContainerDao(super.db); + + Future addContainer({String? name, required Color color}) { + return db.container.insertOne( + ContainerCompanion.insert( + id: uuid.v7(), + name: Value(name), + color: color, + ), + ); + } + + Future replaceContainer( + String id, { + required String? name, + required Color color, + }) { + return db.container.replaceOne( + ContainerCompanion( + id: Value(id), + name: Value(name), + color: Value(color), + ), + ); + } + + Future deleteContainer(String id) { + return db.container.deleteOne(ContainerCompanion.custom(id: Variable(id))); + } + + SingleOrNullSelectable getContainerData(String id) { + return select(db.container)..where((t) => t.id.equals(id)); + } + + Selectable getDistinctColors() { + final query = db.selectOnly(db.container, distinct: true) + ..addColumns([db.container.color]) + ..where(db.container.color.isNotNull()); + + return query + .map((row) => row.readWithConverter(db.container.color)!); + } + + SingleSelectable generateLeadingOrderKey( + String? containerId, { + int bucket = 0, + }) { + return db.leadingOrderKey(bucket: bucket, containerId: containerId); + } + + SingleSelectable generateTrailingOrderKey( + String? containerId, { + int bucket = 0, + }) { + return db.trailingOrderKey(bucket: bucket, containerId: containerId); + } + + SingleSelectable generateOrderKeyAfterTabId( + String? containerId, + String tabId, + ) { + return db.orderKeyAfterTab(containerId: containerId, tabId: tabId); + } +} diff --git a/app/lib/features/geckoview/features/tabs/data/database/daos/container.g.dart b/app/lib/features/geckoview/features/tabs/data/database/daos/container.g.dart new file mode 100644 index 00000000..55df7c87 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/data/database/daos/container.g.dart @@ -0,0 +1,6 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'container.dart'; + +// ignore_for_file: type=lint +mixin _$ContainerDaoMixin on DatabaseAccessor {} diff --git a/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart new file mode 100644 index 00000000..e7d51138 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart @@ -0,0 +1,128 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; +import 'package:lexo_rank/lexo_rank.dart'; + +part 'tab.g.dart'; + +@DriftAccessor() +class TabDao extends DatabaseAccessor with _$TabDaoMixin { + TabDao(super.db); + + Selectable containerTabIds(String? containerId) { + final query = selectOnly(db.tab) + ..addColumns([db.tab.id]) + ..where( + (containerId != null) + ? db.tab.containerId.equals(containerId) + : db.tab.containerId.isNull(), + ) + ..orderBy([OrderingTerm.asc(db.tab.orderKey)]); + + return query.map((row) => row.read(db.tab.id)!); + } + + Selectable allTabIds() { + final query = selectOnly(db.tab) + ..addColumns([db.tab.id]) + ..orderBy([OrderingTerm.asc(db.tab.orderKey)]); + + return query.map((row) => row.read(db.tab.id)!); + } + + SingleSelectable tabContainerId(String tabId) { + final query = selectOnly(db.tab) + ..addColumns([db.tab.containerId]) + ..where(db.tab.id.equals(tabId)); + + return query.map((row) => row.read(db.tab.containerId)); + } + + Future upsertTab( + String tabId, { + Value containerId = const Value.absent(), + Value orderKey = const Value.absent(), + }) { + return db.transaction(() async { + final currentOrderKey = orderKey.value ?? + await db.containerDao + .generateLeadingOrderKey(containerId.value) + .getSingle(); + + await db.tab.insertOne( + TabCompanion.insert( + id: tabId, + timestamp: DateTime.now(), + containerId: containerId, + orderKey: currentOrderKey, + ), + onConflict: DoUpdate( + (old) => TabCompanion.custom( + containerId: + (containerId.present) ? Variable(containerId.value) : null, + orderKey: (orderKey.present) ? Variable(orderKey.value) : null, + ), + ), + ); + + return tabId; + }); + } + + Future assignContainer( + String id, { + required String? containerId, + }) { + final statement = db.tab.update()..where((t) => t.id.equals(id)); + return statement.write( + TabCompanion(containerId: Value(containerId)), + ); + } + + Future assignOrderKey( + String id, { + required String orderKey, + }) { + final statement = db.tab.update()..where((t) => t.id.equals(id)); + return statement.write( + TabCompanion(orderKey: Value(orderKey)), + ); + } + + Future touchTab( + String id, { + required DateTime timestamp, + }) { + final statement = db.tab.update()..where((t) => t.id.equals(id)); + return statement.write( + TabCompanion(timestamp: Value(timestamp)), + ); + } + + Future syncTabs({required List retainTabIds}) async { + return db.transaction(() async { + await (db.tab.delete()..where((t) => t.id.isNotIn(retainTabIds))).go(); + + var currentOrderKey = + await db.containerDao.generateLeadingOrderKey(null).getSingle(); + + await db.tab.insertAll( + retainTabIds.map( + (id) { + final insertable = TabCompanion.insert( + id: id, + orderKey: currentOrderKey, + timestamp: DateTime.now(), + ); + + currentOrderKey = LexoRank.parse(currentOrderKey).genPrev().value; + + return insertable; + }, + ), + mode: InsertMode.insertOrIgnore, + ); + }); + } +} diff --git a/app/lib/features/geckoview/features/topics/data/database/daos/tab.g.dart b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.g.dart similarity index 61% rename from app/lib/features/geckoview/features/topics/data/database/daos/tab.g.dart rename to app/lib/features/geckoview/features/tabs/data/database/daos/tab.g.dart index df16e4ce..3da23453 100644 --- a/app/lib/features/geckoview/features/topics/data/database/daos/tab.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.g.dart @@ -3,4 +3,4 @@ part of 'tab.dart'; // ignore_for_file: type=lint -mixin _$TabLinkDaoMixin on DatabaseAccessor {} +mixin _$TabDaoMixin on DatabaseAccessor {} diff --git a/app/lib/features/geckoview/features/topics/data/database/database.dart b/app/lib/features/geckoview/features/tabs/data/database/database.dart similarity index 59% rename from app/lib/features/geckoview/features/topics/data/database/database.dart rename to app/lib/features/geckoview/features/tabs/data/database/database.dart index 254ff583..718fe419 100644 --- a/app/lib/features/geckoview/features/topics/data/database/database.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/database.dart @@ -1,17 +1,17 @@ -import 'dart:ui'; - import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; -import 'package:lensai/features/geckoview/features/topics/data/database/daos/tab.dart'; -import 'package:lensai/features/geckoview/features/topics/data/database/daos/topic.dart'; -import 'package:lensai/features/geckoview/features/topics/data/database/drift/converters/color.dart'; -import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart'; +import 'package:flutter/widgets.dart' show Color, IconData; +import 'package:lensai/data/database/converters/color.dart'; +import 'package:lensai/data/database/converters/icon_data.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/daos/container.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; part 'database.g.dart'; @DriftDatabase( include: {'database.drift'}, - daos: [TopicDao, TabLinkDao], + daos: [ContainerDao, TabDao], ) class TabDatabase extends _$TabDatabase { @override @@ -25,13 +25,11 @@ class TabDatabase extends _$TabDatabase { if (from < 2) { await transaction(() async { - await m.renameTable(tabLink, 'tab'); + // await m.dropColumn(tabLink, 'url'); + // await m.dropColumn(tabLink, 'title'); + // await m.dropColumn(tabLink, 'screenshot'); - await m.dropColumn(tabLink, 'url'); - await m.dropColumn(tabLink, 'title'); - await m.dropColumn(tabLink, 'screenshot'); - - await m.alterTable(TableMigration(tabLink)); + // await m.alterTable(TableMigration(tab)); }); } diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.drift b/app/lib/features/geckoview/features/tabs/data/database/database.drift new file mode 100644 index 00000000..02263fd7 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/data/database/database.drift @@ -0,0 +1,69 @@ +import 'package:lensai/data/database/converters/color.dart'; +import 'package:lensai/data/database/converters/icon_data.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; + +CREATE TABLE container ( + id TEXT PRIMARY KEY NOT NULL, + contextual_identity TEXT, + name TEXT, + color INTEGER NOT NULL MAPPED BY `const ColorConverter()`, + icon TEXT MAPPED BY `const IconDataTypeConverter()` +) WITH ContainerData; + +CREATE TABLE tab ( + id TEXT PRIMARY KEY NOT NULL, + container_id TEXT REFERENCES container (id) ON DELETE CASCADE, + order_key TEXT NOT NULL, + timestamp DATETIME NOT NULL +); + +containersWithCount WITH ContainerDataWithCount: + SELECT + container.*, + tab_agg.tab_count + FROM container + LEFT JOIN ( + SELECT + container_id, + COUNT(*) AS tab_count, + MAX(timestamp) AS last_updated + FROM tab + GROUP BY container_id + ) AS tab_agg ON container.id = tab_agg.container_id + ORDER BY tab_agg.last_updated DESC NULLS FIRST; + +leadingOrderKey(:container_id AS TEXT OR NULL, :bucket AS INTEGER): + SELECT lexo_rank_previous( + :bucket, + ( + SELECT order_key + FROM tab + WHERE container_id IS :container_id + ORDER BY order_key + LIMIT 1 + ) + ); + +trailingOrderKey(:container_id AS TEXT OR NULL, :bucket AS INTEGER): + SELECT lexo_rank_next( + :bucket, + ( + SELECT order_key + FROM tab + WHERE container_id IS :container_id + ORDER BY order_key DESC + LIMIT 1 + ) + ); + +orderKeyAfterTab(:tab_id AS TEXT, :container_id AS TEXT OR NULL): + WITH ordered_table AS ( + SELECT id, + order_key, + LEAD(order_key) OVER (ORDER BY order_key) AS next_order_key + FROM tab + WHERE container_id IS :container_id + ) + SELECT lexo_rank_reorder_after(order_key, next_order_key) + FROM ordered_table + WHERE id = :tab_id; \ No newline at end of file diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.g.dart b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart new file mode 100644 index 00000000..90611ece --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart @@ -0,0 +1,996 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class Container extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Container(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL'); + late final GeneratedColumn contextualIdentity = + GeneratedColumn('contextual_identity', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); + late final GeneratedColumnWithTypeConverter color = + GeneratedColumn('color', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL') + .withConverter(Container.$convertercolor); + late final GeneratedColumnWithTypeConverter icon = + GeneratedColumn('icon', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '') + .withConverter(Container.$convertericonn); + @override + List get $columns => + [id, contextualIdentity, name, color, icon]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'container'; + @override + Set get $primaryKey => {id}; + @override + ContainerData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ContainerData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + contextualIdentity: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}contextual_identity']), + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name']), + color: Container.$convertercolor.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}color'])!), + icon: Container.$convertericonn.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}icon'])), + ); + } + + @override + Container createAlias(String alias) { + return Container(attachedDatabase, alias); + } + + static TypeConverter $convertercolor = const ColorConverter(); + static TypeConverter $convertericon = + const IconDataTypeConverter(); + static TypeConverter $convertericonn = + NullAwareTypeConverter.wrap($convertericon); + @override + bool get dontWriteConstraints => true; +} + +class ContainerCompanion extends UpdateCompanion { + final Value id; + final Value contextualIdentity; + final Value name; + final Value color; + final Value icon; + final Value rowid; + const ContainerCompanion({ + this.id = const Value.absent(), + this.contextualIdentity = const Value.absent(), + this.name = const Value.absent(), + this.color = const Value.absent(), + this.icon = const Value.absent(), + this.rowid = const Value.absent(), + }); + ContainerCompanion.insert({ + required String id, + this.contextualIdentity = const Value.absent(), + this.name = const Value.absent(), + required Color color, + this.icon = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + color = Value(color); + static Insertable custom({ + Expression? id, + Expression? contextualIdentity, + Expression? name, + Expression? color, + Expression? icon, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (contextualIdentity != null) 'contextual_identity': contextualIdentity, + if (name != null) 'name': name, + if (color != null) 'color': color, + if (icon != null) 'icon': icon, + if (rowid != null) 'rowid': rowid, + }); + } + + ContainerCompanion copyWith( + {Value? id, + Value? contextualIdentity, + Value? name, + Value? color, + Value? icon, + Value? rowid}) { + return ContainerCompanion( + id: id ?? this.id, + contextualIdentity: contextualIdentity ?? this.contextualIdentity, + name: name ?? this.name, + color: color ?? this.color, + icon: icon ?? this.icon, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (contextualIdentity.present) { + map['contextual_identity'] = Variable(contextualIdentity.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (color.present) { + map['color'] = + Variable(Container.$convertercolor.toSql(color.value)); + } + if (icon.present) { + map['icon'] = + Variable(Container.$convertericonn.toSql(icon.value)); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ContainerCompanion(') + ..write('id: $id, ') + ..write('contextualIdentity: $contextualIdentity, ') + ..write('name: $name, ') + ..write('color: $color, ') + ..write('icon: $icon, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class Tab extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Tab(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL'); + late final GeneratedColumn containerId = GeneratedColumn( + 'container_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'REFERENCES container(id)ON DELETE CASCADE'); + late final GeneratedColumn orderKey = GeneratedColumn( + 'order_key', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + late final GeneratedColumn timestamp = GeneratedColumn( + 'timestamp', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + @override + List get $columns => [id, containerId, orderKey, timestamp]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'tab'; + @override + Set get $primaryKey => {id}; + @override + TabData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TabData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + containerId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}container_id']), + orderKey: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}order_key'])!, + timestamp: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}timestamp'])!, + ); + } + + @override + Tab createAlias(String alias) { + return Tab(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class TabData extends DataClass implements Insertable { + final String id; + final String? containerId; + final String orderKey; + final DateTime timestamp; + const TabData( + {required this.id, + this.containerId, + required this.orderKey, + required this.timestamp}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || containerId != null) { + map['container_id'] = Variable(containerId); + } + map['order_key'] = Variable(orderKey); + map['timestamp'] = Variable(timestamp); + return map; + } + + factory TabData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TabData( + id: serializer.fromJson(json['id']), + containerId: serializer.fromJson(json['container_id']), + orderKey: serializer.fromJson(json['order_key']), + timestamp: serializer.fromJson(json['timestamp']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'container_id': serializer.toJson(containerId), + 'order_key': serializer.toJson(orderKey), + 'timestamp': serializer.toJson(timestamp), + }; + } + + TabData copyWith( + {String? id, + Value containerId = const Value.absent(), + String? orderKey, + DateTime? timestamp}) => + TabData( + id: id ?? this.id, + containerId: containerId.present ? containerId.value : this.containerId, + orderKey: orderKey ?? this.orderKey, + timestamp: timestamp ?? this.timestamp, + ); + TabData copyWithCompanion(TabCompanion data) { + return TabData( + id: data.id.present ? data.id.value : this.id, + containerId: + data.containerId.present ? data.containerId.value : this.containerId, + orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey, + timestamp: data.timestamp.present ? data.timestamp.value : this.timestamp, + ); + } + + @override + String toString() { + return (StringBuffer('TabData(') + ..write('id: $id, ') + ..write('containerId: $containerId, ') + ..write('orderKey: $orderKey, ') + ..write('timestamp: $timestamp') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, containerId, orderKey, timestamp); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TabData && + other.id == this.id && + other.containerId == this.containerId && + other.orderKey == this.orderKey && + other.timestamp == this.timestamp); +} + +class TabCompanion extends UpdateCompanion { + final Value id; + final Value containerId; + final Value orderKey; + final Value timestamp; + final Value rowid; + const TabCompanion({ + this.id = const Value.absent(), + this.containerId = const Value.absent(), + this.orderKey = const Value.absent(), + this.timestamp = const Value.absent(), + this.rowid = const Value.absent(), + }); + TabCompanion.insert({ + required String id, + this.containerId = const Value.absent(), + required String orderKey, + required DateTime timestamp, + this.rowid = const Value.absent(), + }) : id = Value(id), + orderKey = Value(orderKey), + timestamp = Value(timestamp); + static Insertable custom({ + Expression? id, + Expression? containerId, + Expression? orderKey, + Expression? timestamp, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (containerId != null) 'container_id': containerId, + if (orderKey != null) 'order_key': orderKey, + if (timestamp != null) 'timestamp': timestamp, + if (rowid != null) 'rowid': rowid, + }); + } + + TabCompanion copyWith( + {Value? id, + Value? containerId, + Value? orderKey, + Value? timestamp, + Value? rowid}) { + return TabCompanion( + id: id ?? this.id, + containerId: containerId ?? this.containerId, + orderKey: orderKey ?? this.orderKey, + timestamp: timestamp ?? this.timestamp, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (containerId.present) { + map['container_id'] = Variable(containerId.value); + } + if (orderKey.present) { + map['order_key'] = Variable(orderKey.value); + } + if (timestamp.present) { + map['timestamp'] = Variable(timestamp.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TabCompanion(') + ..write('id: $id, ') + ..write('containerId: $containerId, ') + ..write('orderKey: $orderKey, ') + ..write('timestamp: $timestamp, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$TabDatabase extends GeneratedDatabase { + _$TabDatabase(QueryExecutor e) : super(e); + $TabDatabaseManager get managers => $TabDatabaseManager(this); + late final Container container = Container(this); + late final Tab tab = Tab(this); + late final ContainerDao containerDao = ContainerDao(this as TabDatabase); + late final TabDao tabDao = TabDao(this as TabDatabase); + Selectable containersWithCount() { + return customSelect( + 'SELECT container.*, tab_agg.tab_count FROM container LEFT JOIN (SELECT container_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated FROM tab GROUP BY container_id) AS tab_agg ON container.id = tab_agg.container_id ORDER BY tab_agg.last_updated DESC NULLS FIRST', + variables: [], + readsFrom: { + container, + tab, + }).map((QueryRow row) => ContainerDataWithCount( + id: row.read('id'), + contextualIdentity: row.readNullable('contextual_identity'), + name: row.readNullable('name'), + color: Container.$convertercolor.fromSql(row.read('color')), + icon: NullAwareTypeConverter.wrapFromSql( + Container.$convertericon, row.readNullable('icon')), + tabCount: row.readNullable('tab_count'), + )); + } + + Selectable leadingOrderKey( + {required int bucket, String? containerId}) { + return customSelect( + 'SELECT lexo_rank_previous(?1, (SELECT order_key FROM tab WHERE container_id IS ?2 ORDER BY order_key LIMIT 1)) AS _c0', + variables: [ + Variable(bucket), + Variable(containerId) + ], + readsFrom: { + tab, + }).map((QueryRow row) => row.read('_c0')); + } + + Selectable trailingOrderKey( + {required int bucket, String? containerId}) { + return customSelect( + 'SELECT lexo_rank_next(?1, (SELECT order_key FROM tab WHERE container_id IS ?2 ORDER BY order_key DESC LIMIT 1)) AS _c0', + variables: [ + Variable(bucket), + Variable(containerId) + ], + readsFrom: { + tab, + }).map((QueryRow row) => row.read('_c0')); + } + + Selectable orderKeyAfterTab( + {String? containerId, required String tabId}) { + return customSelect( + 'WITH ordered_table AS (SELECT id, order_key, LEAD(order_key)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS next_order_key FROM tab WHERE container_id IS ?1) SELECT lexo_rank_reorder_after(order_key, next_order_key) AS _c0 FROM ordered_table WHERE id = ?2', + variables: [ + Variable(containerId), + Variable(tabId) + ], + readsFrom: { + tab, + }).map((QueryRow row) => row.read('_c0')); + } + + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [container, tab]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( + [ + WritePropagation( + on: TableUpdateQuery.onTableName('container', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('tab', kind: UpdateKind.delete), + ], + ), + ], + ); +} + +typedef $ContainerCreateCompanionBuilder = ContainerCompanion Function({ + required String id, + Value contextualIdentity, + Value name, + required Color color, + Value icon, + Value rowid, +}); +typedef $ContainerUpdateCompanionBuilder = ContainerCompanion Function({ + Value id, + Value contextualIdentity, + Value name, + Value color, + Value icon, + Value rowid, +}); + +final class $ContainerReferences + extends BaseReferences<_$TabDatabase, Container, ContainerData> { + $ContainerReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey> _tabRefsTable( + _$TabDatabase db) => + MultiTypedResultKey.fromTable(db.tab, + aliasName: $_aliasNameGenerator(db.container.id, db.tab.containerId)); + + $TabProcessedTableManager get tabRefs { + final manager = $TabTableManager($_db, $_db.tab) + .filter((f) => f.containerId.id($_item.id)); + + final cache = $_typedResult.readTableOrNull(_tabRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $ContainerFilterComposer extends Composer<_$TabDatabase, Container> { + $ContainerFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get contextualIdentity => $composableBuilder( + column: $table.contextualIdentity, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters get color => + $composableBuilder( + column: $table.color, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters get icon => + $composableBuilder( + column: $table.icon, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + Expression tabRefs(Expression Function($TabFilterComposer f) f) { + final $TabFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tab, + getReferencedColumn: (t) => t.containerId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $TabFilterComposer( + $db: $db, + $table: $db.tab, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $ContainerOrderingComposer extends Composer<_$TabDatabase, Container> { + $ContainerOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contextualIdentity => $composableBuilder( + column: $table.contextualIdentity, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get color => $composableBuilder( + column: $table.color, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get icon => $composableBuilder( + column: $table.icon, builder: (column) => ColumnOrderings(column)); +} + +class $ContainerAnnotationComposer extends Composer<_$TabDatabase, Container> { + $ContainerAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get contextualIdentity => $composableBuilder( + column: $table.contextualIdentity, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumnWithTypeConverter get color => + $composableBuilder(column: $table.color, builder: (column) => column); + + GeneratedColumnWithTypeConverter get icon => + $composableBuilder(column: $table.icon, builder: (column) => column); + + Expression tabRefs( + Expression Function($TabAnnotationComposer a) f) { + final $TabAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tab, + getReferencedColumn: (t) => t.containerId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $TabAnnotationComposer( + $db: $db, + $table: $db.tab, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } +} + +class $ContainerTableManager extends RootTableManager< + _$TabDatabase, + Container, + ContainerData, + $ContainerFilterComposer, + $ContainerOrderingComposer, + $ContainerAnnotationComposer, + $ContainerCreateCompanionBuilder, + $ContainerUpdateCompanionBuilder, + (ContainerData, $ContainerReferences), + ContainerData, + PrefetchHooks Function({bool tabRefs})> { + $ContainerTableManager(_$TabDatabase db, Container table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $ContainerFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $ContainerOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $ContainerAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value contextualIdentity = const Value.absent(), + Value name = const Value.absent(), + Value color = const Value.absent(), + Value icon = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ContainerCompanion( + id: id, + contextualIdentity: contextualIdentity, + name: name, + color: color, + icon: icon, + rowid: rowid, + ), + createCompanionCallback: ({ + required String id, + Value contextualIdentity = const Value.absent(), + Value name = const Value.absent(), + required Color color, + Value icon = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ContainerCompanion.insert( + id: id, + contextualIdentity: contextualIdentity, + name: name, + color: color, + icon: icon, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $ContainerReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ({tabRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (tabRefs) db.tab], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (tabRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $ContainerReferences._tabRefsTable(db), + managerFromTypedResult: (p0) => + $ContainerReferences(db, table, p0).tabRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.containerId == item.id), + typedResults: items) + ]; + }, + ); + }, + )); +} + +typedef $ContainerProcessedTableManager = ProcessedTableManager< + _$TabDatabase, + Container, + ContainerData, + $ContainerFilterComposer, + $ContainerOrderingComposer, + $ContainerAnnotationComposer, + $ContainerCreateCompanionBuilder, + $ContainerUpdateCompanionBuilder, + (ContainerData, $ContainerReferences), + ContainerData, + PrefetchHooks Function({bool tabRefs})>; +typedef $TabCreateCompanionBuilder = TabCompanion Function({ + required String id, + Value containerId, + required String orderKey, + required DateTime timestamp, + Value rowid, +}); +typedef $TabUpdateCompanionBuilder = TabCompanion Function({ + Value id, + Value containerId, + Value orderKey, + Value timestamp, + Value rowid, +}); + +final class $TabReferences extends BaseReferences<_$TabDatabase, Tab, TabData> { + $TabReferences(super.$_db, super.$_table, super.$_typedResult); + + static Container _containerIdTable(_$TabDatabase db) => db.container + .createAlias($_aliasNameGenerator(db.tab.containerId, db.container.id)); + + $ContainerProcessedTableManager? get containerId { + if ($_item.containerId == null) return null; + final manager = $ContainerTableManager($_db, $_db.container) + .filter((f) => f.id($_item.containerId!)); + final item = $_typedResult.readTableOrNull(_containerIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $TabFilterComposer extends Composer<_$TabDatabase, Tab> { + $TabFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get orderKey => $composableBuilder( + column: $table.orderKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get timestamp => $composableBuilder( + column: $table.timestamp, builder: (column) => ColumnFilters(column)); + + $ContainerFilterComposer get containerId { + final $ContainerFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.containerId, + referencedTable: $db.container, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $ContainerFilterComposer( + $db: $db, + $table: $db.container, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $TabOrderingComposer extends Composer<_$TabDatabase, Tab> { + $TabOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get orderKey => $composableBuilder( + column: $table.orderKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get timestamp => $composableBuilder( + column: $table.timestamp, builder: (column) => ColumnOrderings(column)); + + $ContainerOrderingComposer get containerId { + final $ContainerOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.containerId, + referencedTable: $db.container, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $ContainerOrderingComposer( + $db: $db, + $table: $db.container, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $TabAnnotationComposer extends Composer<_$TabDatabase, Tab> { + $TabAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get orderKey => + $composableBuilder(column: $table.orderKey, builder: (column) => column); + + GeneratedColumn get timestamp => + $composableBuilder(column: $table.timestamp, builder: (column) => column); + + $ContainerAnnotationComposer get containerId { + final $ContainerAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.containerId, + referencedTable: $db.container, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $ContainerAnnotationComposer( + $db: $db, + $table: $db.container, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $TabTableManager extends RootTableManager< + _$TabDatabase, + Tab, + TabData, + $TabFilterComposer, + $TabOrderingComposer, + $TabAnnotationComposer, + $TabCreateCompanionBuilder, + $TabUpdateCompanionBuilder, + (TabData, $TabReferences), + TabData, + PrefetchHooks Function({bool containerId})> { + $TabTableManager(_$TabDatabase db, Tab table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $TabFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $TabOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $TabAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value containerId = const Value.absent(), + Value orderKey = const Value.absent(), + Value timestamp = const Value.absent(), + Value rowid = const Value.absent(), + }) => + TabCompanion( + id: id, + containerId: containerId, + orderKey: orderKey, + timestamp: timestamp, + rowid: rowid, + ), + createCompanionCallback: ({ + required String id, + Value containerId = const Value.absent(), + required String orderKey, + required DateTime timestamp, + Value rowid = const Value.absent(), + }) => + TabCompanion.insert( + id: id, + containerId: containerId, + orderKey: orderKey, + timestamp: timestamp, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), $TabReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ({containerId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (containerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.containerId, + referencedTable: $TabReferences._containerIdTable(db), + referencedColumn: $TabReferences._containerIdTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $TabProcessedTableManager = ProcessedTableManager< + _$TabDatabase, + Tab, + TabData, + $TabFilterComposer, + $TabOrderingComposer, + $TabAnnotationComposer, + $TabCreateCompanionBuilder, + $TabUpdateCompanionBuilder, + (TabData, $TabReferences), + TabData, + PrefetchHooks Function({bool containerId})>; + +class $TabDatabaseManager { + final _$TabDatabase _db; + $TabDatabaseManager(this._db); + $ContainerTableManager get container => + $ContainerTableManager(_db, _db.container); + $TabTableManager get tab => $TabTableManager(_db, _db.tab); +} diff --git a/app/lib/features/geckoview/features/topics/data/models/topic_data.dart b/app/lib/features/geckoview/features/tabs/data/models/container_data.dart similarity index 55% rename from app/lib/features/geckoview/features/topics/data/models/topic_data.dart rename to app/lib/features/geckoview/features/tabs/data/models/container_data.dart index cd1a2d14..9e0313d0 100644 --- a/app/lib/features/geckoview/features/topics/data/models/topic_data.dart +++ b/app/lib/features/geckoview/features/tabs/data/models/container_data.dart @@ -1,13 +1,20 @@ -import 'dart:ui'; - import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter/widgets.dart'; -class TopicData with FastEquatable { +class ContainerData with FastEquatable { final String id; + final String? contextualIdentity; final String? name; final Color color; + final IconData? icon; - TopicData({required this.id, this.name, required this.color}); + ContainerData({ + required this.id, + this.contextualIdentity, + this.name, + required this.color, + this.icon, + }); @override bool get cacheHash => true; @@ -15,18 +22,22 @@ class TopicData with FastEquatable { @override List get hashParameters => [ id, + contextualIdentity, name, color, + icon, ]; } -class TopicDataWithCount extends TopicData { +class ContainerDataWithCount extends ContainerData { final int? tabCount; - TopicDataWithCount({ + ContainerDataWithCount({ required super.id, + super.contextualIdentity, super.name, required super.color, + super.icon, required this.tabCount, }); diff --git a/app/lib/features/geckoview/features/topics/data/providers.dart b/app/lib/features/geckoview/features/tabs/data/providers.dart similarity index 77% rename from app/lib/features/geckoview/features/topics/data/providers.dart rename to app/lib/features/geckoview/features/tabs/data/providers.dart index ed1a78c3..40c1f667 100644 --- a/app/lib/features/geckoview/features/topics/data/providers.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.dart @@ -1,6 +1,7 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; -import 'package:lensai/features/geckoview/features/topics/data/database/database.dart'; +import 'package:lensai/data/database/functions/lexo_rank_functions.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart' as path_provider; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -17,7 +18,7 @@ TabDatabase tabDatabase(TabDatabaseRef ref) { // put the database file, called db.sqlite here, into the documents folder // for your app. final dbFolder = await path_provider.getApplicationDocumentsDirectory(); - final file = File(p.join(dbFolder.path, 'tab.db')); + final file = File(p.join(dbFolder.path, 'tab2.db')); // Also work around limitations on old Android versions if (Platform.isAndroid) { @@ -31,7 +32,12 @@ TabDatabase tabDatabase(TabDatabaseRef ref) { // Explicitly tell it about the correct temporary directory. sqlite3.tempDirectory = cachebase; - return NativeDatabase.createInBackground(file); + return NativeDatabase.createInBackground( + file, + setup: (database) { + registerLexorankFunctions(database); + }, + ); }), ); } diff --git a/app/lib/features/geckoview/features/topics/data/providers.g.dart b/app/lib/features/geckoview/features/tabs/data/providers.g.dart similarity index 91% rename from app/lib/features/geckoview/features/topics/data/providers.g.dart rename to app/lib/features/geckoview/features/tabs/data/providers.g.dart index 13b43baa..214caef0 100644 --- a/app/lib/features/geckoview/features/topics/data/providers.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabDatabaseHash() => r'b886b089ae5fdb9bdcc0982efe5fcbe9bc66486f'; +String _$tabDatabaseHash() => r'567872961683d21156a9af463466f8ddbdd02f1f'; /// See also [tabDatabase]. @ProviderFor(tabDatabase) diff --git a/app/lib/features/geckoview/features/tabs/domain/providers.dart b/app/lib/features/geckoview/features/tabs/domain/providers.dart new file mode 100644 index 00000000..5b9ac59b --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/domain/providers.dart @@ -0,0 +1,50 @@ +import 'dart:ui'; + +import 'package:collection/collection.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:lensai/features/geckoview/features/tabs/utils/color_palette.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'providers.g.dart'; + +@Riverpod() +Future unusedRandomContainerColor( + UnusedRandomContainerColorRef ref, +) async { + final repository = ref.watch(containerRepositoryProvider.notifier); + + final allColors = colorTypes.flattened.toList(); + final usedColors = await repository.getDistinctColors(); + + Color randomColor; + do { + randomColor = randomColorShade(allColors); + } while (usedColors.contains(randomColor)); + + return randomColor; +} + +@Riverpod() +Stream> containersWithCount( + ContainersWithCountRef ref, +) { + final db = ref.watch(tabDatabaseProvider); + return db.containersWithCount().watch(); +} + +@Riverpod() +Stream tabContainerId(TabContainerIdRef ref, String tabId) { + final db = ref.watch(tabDatabaseProvider); + return db.tabDao.tabContainerId(tabId).watchSingle(); +} + +@Riverpod() +Stream> containerTabIds( + ContainerTabIdsRef ref, + String? containerId, +) { + final db = ref.watch(tabDatabaseProvider); + return db.tabDao.containerTabIds(containerId).watch(); +} diff --git a/app/lib/features/geckoview/features/tabs/domain/providers.g.dart b/app/lib/features/geckoview/features/tabs/domain/providers.g.dart new file mode 100644 index 00000000..a6f98e58 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/domain/providers.g.dart @@ -0,0 +1,322 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$unusedRandomContainerColorHash() => + r'129cefaefa895012a6d7b35da69773e28e3289cc'; + +/// See also [unusedRandomContainerColor]. +@ProviderFor(unusedRandomContainerColor) +final unusedRandomContainerColorProvider = + AutoDisposeFutureProvider.internal( + unusedRandomContainerColor, + name: r'unusedRandomContainerColorProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$unusedRandomContainerColorHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef UnusedRandomContainerColorRef = AutoDisposeFutureProviderRef; +String _$containersWithCountHash() => + r'e1bab28091dd1b9ecfd7d76e6393f459604c803a'; + +/// See also [containersWithCount]. +@ProviderFor(containersWithCount) +final containersWithCountProvider = + AutoDisposeStreamProvider>.internal( + containersWithCount, + name: r'containersWithCountProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$containersWithCountHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef ContainersWithCountRef + = AutoDisposeStreamProviderRef>; +String _$tabContainerIdHash() => r'8adbfdc2ef46219569073fe9e58967f4935a45e5'; + +/// 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 [tabContainerId]. +@ProviderFor(tabContainerId) +const tabContainerIdProvider = TabContainerIdFamily(); + +/// See also [tabContainerId]. +class TabContainerIdFamily extends Family> { + /// See also [tabContainerId]. + const TabContainerIdFamily(); + + /// See also [tabContainerId]. + TabContainerIdProvider call( + String tabId, + ) { + return TabContainerIdProvider( + tabId, + ); + } + + @override + TabContainerIdProvider getProviderOverride( + covariant TabContainerIdProvider 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'tabContainerIdProvider'; +} + +/// See also [tabContainerId]. +class TabContainerIdProvider extends AutoDisposeStreamProvider { + /// See also [tabContainerId]. + TabContainerIdProvider( + String tabId, + ) : this._internal( + (ref) => tabContainerId( + ref as TabContainerIdRef, + tabId, + ), + from: tabContainerIdProvider, + name: r'tabContainerIdProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$tabContainerIdHash, + dependencies: TabContainerIdFamily._dependencies, + allTransitiveDependencies: + TabContainerIdFamily._allTransitiveDependencies, + tabId: tabId, + ); + + TabContainerIdProvider._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(TabContainerIdRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: TabContainerIdProvider._internal( + (ref) => create(ref as TabContainerIdRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + tabId: tabId, + ), + ); + } + + @override + AutoDisposeStreamProviderElement createElement() { + return _TabContainerIdProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is TabContainerIdProvider && 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 TabContainerIdRef on AutoDisposeStreamProviderRef { + /// The parameter `tabId` of this provider. + String get tabId; +} + +class _TabContainerIdProviderElement + extends AutoDisposeStreamProviderElement with TabContainerIdRef { + _TabContainerIdProviderElement(super.provider); + + @override + String get tabId => (origin as TabContainerIdProvider).tabId; +} + +String _$containerTabIdsHash() => r'79e9bccfb17cf3ce802638c8b806c5a76fa8f732'; + +/// See also [containerTabIds]. +@ProviderFor(containerTabIds) +const containerTabIdsProvider = ContainerTabIdsFamily(); + +/// See also [containerTabIds]. +class ContainerTabIdsFamily extends Family>> { + /// See also [containerTabIds]. + const ContainerTabIdsFamily(); + + /// See also [containerTabIds]. + ContainerTabIdsProvider call( + String? containerId, + ) { + return ContainerTabIdsProvider( + containerId, + ); + } + + @override + ContainerTabIdsProvider getProviderOverride( + covariant ContainerTabIdsProvider provider, + ) { + return call( + provider.containerId, + ); + } + + 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'containerTabIdsProvider'; +} + +/// See also [containerTabIds]. +class ContainerTabIdsProvider extends AutoDisposeStreamProvider> { + /// See also [containerTabIds]. + ContainerTabIdsProvider( + String? containerId, + ) : this._internal( + (ref) => containerTabIds( + ref as ContainerTabIdsRef, + containerId, + ), + from: containerTabIdsProvider, + name: r'containerTabIdsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$containerTabIdsHash, + dependencies: ContainerTabIdsFamily._dependencies, + allTransitiveDependencies: + ContainerTabIdsFamily._allTransitiveDependencies, + containerId: containerId, + ); + + ContainerTabIdsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.containerId, + }) : super.internal(); + + final String? containerId; + + @override + Override overrideWith( + Stream> Function(ContainerTabIdsRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: ContainerTabIdsProvider._internal( + (ref) => create(ref as ContainerTabIdsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + containerId: containerId, + ), + ); + } + + @override + AutoDisposeStreamProviderElement> createElement() { + return _ContainerTabIdsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ContainerTabIdsProvider && other.containerId == containerId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, containerId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin ContainerTabIdsRef on AutoDisposeStreamProviderRef> { + /// The parameter `containerId` of this provider. + String? get containerId; +} + +class _ContainerTabIdsProviderElement + extends AutoDisposeStreamProviderElement> + with ContainerTabIdsRef { + _ContainerTabIdsProviderElement(super.provider); + + @override + String? get containerId => (origin as ContainerTabIdsProvider).containerId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart new file mode 100644 index 00000000..5b260c16 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart @@ -0,0 +1,43 @@ +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'selected_container.g.dart'; + +@Riverpod(keepAlive: true) +class SelectedContainer extends _$SelectedContainer { + void setContainerId(String id) { + state = id; + } + + void toggleContainer(String id) { + if (state == id) { + clearContainer(); + } else { + setContainerId(id); + } + } + + void clearContainer() { + state = null; + } + + @override + String? build() { + return null; + } +} + +@Riverpod() +Stream selectedContainerData(SelectedContainerDataRef ref) { + final db = ref.watch(tabDatabaseProvider); + final selectedContainer = ref.watch(selectedContainerProvider); + + if (selectedContainer != null) { + return db.containerDao + .getContainerData(selectedContainer) + .watchSingleOrNull(); + } + + return Stream.value(null); +} diff --git a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart new file mode 100644 index 00000000..b113868e --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'selected_container.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$selectedContainerDataHash() => + r'dc930d1bb47b3c7989c6c119b0459f37d687af84'; + +/// See also [selectedContainerData]. +@ProviderFor(selectedContainerData) +final selectedContainerDataProvider = + AutoDisposeStreamProvider.internal( + selectedContainerData, + name: r'selectedContainerDataProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedContainerDataHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef; +String _$selectedContainerHash() => r'e38e86db5bd0584af9561156c26ceaea3d23aebf'; + +/// See also [SelectedContainer]. +@ProviderFor(SelectedContainer) +final selectedContainerProvider = + NotifierProvider.internal( + SelectedContainer.new, + name: r'selectedContainerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedContainerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SelectedContainer = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/features/topics/domain/providers/selected_topic.g.dart b/app/lib/features/geckoview/features/tabs/domain/providers/selected_topic.g.dart similarity index 100% rename from app/lib/features/geckoview/features/topics/domain/providers/selected_topic.g.dart rename to app/lib/features/geckoview/features/tabs/domain/providers/selected_topic.g.dart diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart new file mode 100644 index 00000000..f906d6f0 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart @@ -0,0 +1,54 @@ +import 'dart:ui'; + +import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'container.g.dart'; + +@Riverpod(keepAlive: true) +class ContainerRepository extends _$ContainerRepository { + late TabDatabase _db; + + @override + void build() { + _db = ref.watch(tabDatabaseProvider); + } + + Future addContainer({required String? name, required Color color}) { + return _db.containerDao.addContainer(name: name, color: color); + } + + Future replaceContainer({ + required String id, + required String? name, + required Color color, + }) { + return _db.containerDao.replaceContainer(id, name: name, color: color); + } + + Future deleteContainer(String id) { + return _db.containerDao.deleteContainer(id); + } + + Future> getDistinctColors() { + return _db.containerDao + .getDistinctColors() + .get() + .then((colors) => colors.toSet()); + } + + Future getLeadingOrderKey(String? containerId) { + return _db.containerDao.generateLeadingOrderKey(containerId).getSingle(); + } + + Future getTrailingOrderKey(String? containerId) { + return _db.containerDao.generateTrailingOrderKey(containerId).getSingle(); + } + + Future getOrderKeyAfterTab(String tabId, String? containerId) { + return _db.containerDao + .generateOrderKeyAfterTabId(containerId, tabId) + .getSingle(); + } +} diff --git a/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart similarity index 54% rename from app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart rename to app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart index 9b156e55..9dfcd6e0 100644 --- a/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart @@ -1,26 +1,27 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'tab_link.dart'; +part of 'container.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$tabLinkRepositoryHash() => r'cdc23ca6ea215925f39842b16f9abcc289b0efbb'; +String _$containerRepositoryHash() => + r'89ad15861fb213d6a3fa51232fd0ff9943f51e92'; -/// See also [TabLinkRepository]. -@ProviderFor(TabLinkRepository) -final tabLinkRepositoryProvider = - NotifierProvider.internal( - TabLinkRepository.new, - name: r'tabLinkRepositoryProvider', +/// See also [ContainerRepository]. +@ProviderFor(ContainerRepository) +final containerRepositoryProvider = + NotifierProvider.internal( + ContainerRepository.new, + name: r'containerRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$tabLinkRepositoryHash, + : _$containerRepositoryHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$TabLinkRepository = Notifier; +typedef _$ContainerRepository = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart new file mode 100644 index 00000000..9f14cef9 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart @@ -0,0 +1,41 @@ +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'tab.g.dart'; + +@Riverpod(keepAlive: true) +class TabDataRepository extends _$TabDataRepository { + late TabDatabase _db; + + @override + void build() { + _db = ref.watch(tabDatabaseProvider); + } + + Future assignContainer(String tabId, String? containerId) { + return _db.tabDao.assignContainer( + tabId, + containerId: containerId, + ); + } + + Future assignOrderKey(String tabId, String orderKey) { + return _db.tabDao.assignOrderKey( + tabId, + orderKey: orderKey, + ); + } + + Future closeAllTabs(String? containerId) async { + final tabIds = await _db.tabDao.containerTabIds(containerId).get(); + if (tabIds.isNotEmpty) { + await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds); + } + } + + Future containerTabId(String tabId) { + return _db.tabDao.tabContainerId(tabId).getSingle(); + } +} diff --git a/app/lib/features/geckoview/features/topics/domain/repositories/topic.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart similarity index 56% rename from app/lib/features/geckoview/features/topics/domain/repositories/topic.g.dart rename to app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart index 759fe11f..4b4688fe 100644 --- a/app/lib/features/geckoview/features/topics/domain/repositories/topic.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart @@ -1,26 +1,26 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'topic.dart'; +part of 'tab.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$topicRepositoryHash() => r'6c456c17af4ea7d7ef1747c92d0442459d061080'; +String _$tabDataRepositoryHash() => r'c067bbbf319cd95ad869655740fdf49d13159b94'; -/// See also [TopicRepository]. -@ProviderFor(TopicRepository) -final topicRepositoryProvider = - NotifierProvider.internal( - TopicRepository.new, - name: r'topicRepositoryProvider', +/// See also [TabDataRepository]. +@ProviderFor(TabDataRepository) +final tabDataRepositoryProvider = + NotifierProvider.internal( + TabDataRepository.new, + name: r'tabDataRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$topicRepositoryHash, + : _$tabDataRepositoryHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$TopicRepository = Notifier; +typedef _$TabDataRepository = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart b/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart similarity index 62% rename from app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart rename to app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart index b42ad83c..63e956c6 100644 --- a/app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart @@ -2,23 +2,23 @@ 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/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:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart'; import 'package:skeletonizer/skeletonizer.dart'; -class _TopicTile extends HookWidget { - final TopicData topic; +class _ContainerTile extends HookWidget { + final ContainerData container; final bool isSelected; - final void Function(TopicResult edited) onEdit; + final void Function(ContainerResult edited) onEdit; final void Function() onDelete; final void Function() onTap; - const _TopicTile( - this.topic, { + const _ContainerTile( + this.container, { required this.isSelected, required this.onEdit, required this.onDelete, @@ -32,8 +32,8 @@ class _TopicTile extends HookWidget { return ListTile( selected: isSelected, - leading: CircleAvatar(backgroundColor: topic.color), - title: Text(topic.name ?? 'New Topic'), + leading: CircleAvatar(backgroundColor: container.color), + title: Text(container.name ?? 'New Container'), trailing: MenuAnchor( controller: menuController, builder: (context, controller, child) { @@ -57,11 +57,11 @@ class _TopicTile extends HookWidget { menuChildren: [ MenuItemButton( onPressed: () async { - final result = await showDialog( + final result = await showDialog( context: context, - builder: (context) => TopicDialog.edit( - name: topic.name, - initialColor: topic.color, + builder: (context) => ContainerDialog.edit( + name: container.name, + initialColor: container.color, ), ); @@ -78,9 +78,9 @@ class _TopicTile extends HookWidget { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Delete Topic'), + title: const Text('Delete Container'), content: const Text( - 'Are you sure you want to delete this topic and all attached tabs?', + 'Are you sure you want to delete this container and close all attached tabs?', ), actions: [ TextButton( @@ -114,32 +114,32 @@ class _TopicTile extends HookWidget { } } -class TopicListScreen extends HookConsumerWidget { - const TopicListScreen(); +class ContainerListScreen extends HookConsumerWidget { + const ContainerListScreen(); @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar( - title: const Text('Topics'), + title: const Text('Containers'), actions: [ IconButton( onPressed: () async { final initialColor = - await ref.read(unusedRandomTopicColorProvider.future); + await ref.read(unusedRandomContainerColorProvider.future); if (context.mounted) { - final result = await showDialog( + final result = await showDialog( context: context, - builder: (context) => TopicDialog.create( + builder: (context) => ContainerDialog.create( initialColor: initialColor, ), ); if (result != null) { await ref - .read(topicRepositoryProvider.notifier) - .addTopic(name: result.name, color: result.color); + .read(containerRepositoryProvider.notifier) + .addContainer(name: result.name, color: result.color); } } }, @@ -149,42 +149,42 @@ class TopicListScreen extends HookConsumerWidget { ), body: HookConsumer( builder: (context, ref, child) { - final topicsAsync = ref.watch(topicsWithCountProvider); - final selectedTopic = ref.watch(selectedTopicProvider); + final containersAsync = ref.watch(containersWithCountProvider); + final selectedContainer = ref.watch(selectedContainerProvider); return Skeletonizer( - enabled: topicsAsync.isLoading, - child: topicsAsync.when( - data: (topics) => FadingScroll( + enabled: containersAsync.isLoading, + child: containersAsync.when( + data: (containers) => FadingScroll( fadingSize: 25, builder: (context, controller) { return ListView.builder( controller: controller, - itemCount: topics.length, + itemCount: containers.length, itemBuilder: (context, index) { - final topic = topics[index]; - return _TopicTile( - topic, - key: ValueKey(topic.id), - isSelected: topic.id == selectedTopic, + final container = containers[index]; + return _ContainerTile( + container, + key: ValueKey(container.id), + isSelected: container.id == selectedContainer, onEdit: (edited) async { await ref - .read(topicRepositoryProvider.notifier) - .replaceTopic( - id: topic.id, + .read(containerRepositoryProvider.notifier) + .replaceContainer( + id: container.id, name: edited.name, color: edited.color, ); }, onDelete: () async { await ref - .read(topicRepositoryProvider.notifier) - .deleteTopic(topic.id); + .read(containerRepositoryProvider.notifier) + .deleteContainer(container.id); }, onTap: () { ref - .read(selectedTopicProvider.notifier) - .toggleTopic(topic.id); + .read(selectedContainerProvider.notifier) + .toggleContainer(container.id); }, ); }, @@ -194,8 +194,8 @@ class TopicListScreen extends HookConsumerWidget { error: (error, stackTrace) => SizedBox.shrink(), loading: () => ListView.builder( itemCount: 3, - itemBuilder: (context, index) => _TopicTile( - TopicData(id: 'null', color: Colors.transparent), + itemBuilder: (context, index) => _ContainerTile( + ContainerData(id: 'null', color: Colors.transparent), isSelected: false, onEdit: (_) {}, onDelete: () {}, diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart new file mode 100644 index 00000000..2c2729f1 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.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/features/tabs/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart'; +import 'package:lensai/presentation/widgets/selectable_chips.dart'; + +class ContainerChips extends HookConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final containersAsync = ref.watch(containersWithCountProvider); + final selectedContainer = ref.watch( + selectedContainerDataProvider.select((value) => value.valueOrNull), + ); + + return containersAsync.when( + data: (availableContainers) => SizedBox( + height: 48, + child: Row( + children: [ + const SizedBox(width: 16), + if (selectedContainer != null || availableContainers.isNotEmpty) + Expanded( + child: SelectableChips( + deleteIcon: false, + itemId: (container) => container.id, + itemAvatar: (container) => Container( + width: 20.0, + height: 20.0, + decoration: BoxDecoration( + color: container.color, + shape: BoxShape.circle, + ), + ), + itemLabel: (container) => + Text(container.name ?? 'New Container'), + itemBadgeCount: (container) => container.tabCount, + availableItems: availableContainers, + selectedItem: selectedContainer, + onSelected: (container) { + ref + .read(selectedContainerProvider.notifier) + .setContainerId(container.id); + }, + onDeleted: (container) async { + ref + .read(selectedContainerProvider.notifier) + .clearContainer(); + }, + ), + ) + else + Expanded( + child: Text( + "Press '>' to manage Containers.", + style: TextStyle( + color: Theme.of(context).hintColor, + fontStyle: FontStyle.italic, + ), + ), + ), + IconButton( + onPressed: () async { + await context.push(ContainerListRoute().location); + }, + icon: const Icon(Icons.chevron_right), + ), + ], + ), + ), + error: (error, stackTrace) => const SizedBox.shrink(), + loading: () => const SizedBox( + height: 48, + width: double.infinity, + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/topics/presentation/widgets/topic_dialog.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart similarity index 83% rename from app/lib/features/geckoview/features/topics/presentation/widgets/topic_dialog.dart rename to app/lib/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart index 9503a9b0..820a9d8e 100644 --- a/app/lib/features/geckoview/features/topics/presentation/widgets/topic_dialog.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart @@ -1,35 +1,35 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:lensai/features/geckoview/features/topics/presentation/widgets/material_color_picker.dart'; +import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart'; -typedef TopicResult = ({String? name, Color color}); +typedef ContainerResult = ({String? name, Color color}); enum _DialogMode { create, edit } -class TopicDialog extends HookWidget { +class ContainerDialog extends HookWidget { final _DialogMode _mode; final String? initialName; final Color initialColor; - const TopicDialog._({ + const ContainerDialog._({ required _DialogMode mode, required this.initialColor, this.initialName, }) : _mode = mode; - factory TopicDialog.create({required Color initialColor}) { - return TopicDialog._( + factory ContainerDialog.create({required Color initialColor}) { + return ContainerDialog._( mode: _DialogMode.create, initialColor: initialColor, ); } - factory TopicDialog.edit({ + factory ContainerDialog.edit({ required String? name, required Color initialColor, }) { - return TopicDialog._( + return ContainerDialog._( mode: _DialogMode.edit, initialColor: initialColor, initialName: name, @@ -54,8 +54,8 @@ class TopicDialog extends HookWidget { ), title: Text( switch (_mode) { - _DialogMode.create => 'New Topic', - _DialogMode.edit => 'Edit Topic', + _DialogMode.create => 'New Container', + _DialogMode.edit => 'Edit Container', }, ), children: [ @@ -94,14 +94,14 @@ class TopicDialog extends HookWidget { children: [ TextButton( onPressed: () { - Navigator.pop(context); + Navigator.pop(context); }, child: const Text('Cancel'), ), TextButton( onPressed: () { final name = textController.text.trim(); - Navigator.pop( + Navigator.pop( context, ( name: name.isNotEmpty ? name : null, diff --git a/app/lib/features/geckoview/features/topics/presentation/widgets/material_color_picker.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart similarity index 99% rename from app/lib/features/geckoview/features/topics/presentation/widgets/material_color_picker.dart rename to app/lib/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart index 326f1aba..f69f3493 100644 --- a/app/lib/features/geckoview/features/topics/presentation/widgets/material_color_picker.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart @@ -2,7 +2,7 @@ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:lensai/features/geckoview/features/topics/utils/color_palette.dart'; +import 'package:lensai/features/geckoview/features/tabs/utils/color_palette.dart'; class MaterialPicker extends StatefulWidget { const MaterialPicker({ diff --git a/app/lib/features/geckoview/features/topics/utils/color_palette.dart b/app/lib/features/geckoview/features/tabs/utils/color_palette.dart similarity index 100% rename from app/lib/features/geckoview/features/topics/utils/color_palette.dart rename to app/lib/features/geckoview/features/tabs/utils/color_palette.dart 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 deleted file mode 100644 index 81b8cadc..00000000 --- a/app/lib/features/geckoview/features/topics/data/database/daos/tab.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:lensai/features/geckoview/features/topics/data/database/database.dart'; - -part 'tab.g.dart'; - -@DriftAccessor() -class TabLinkDao extends DatabaseAccessor with _$TabLinkDaoMixin { - TabLinkDao(super.db); - - Selectable topicTabIds(String topicId) { - final query = selectOnly(db.tabLink) - ..addColumns([db.tabLink.id]) - ..where(db.tabLink.topicId.equals(topicId)) - ..orderBy([OrderingTerm.asc(db.tabLink.id)]); - - return query.map((row) => row.read(db.tabLink.id)!); - } - - Selectable allTabIds() { - final query = selectOnly(db.tabLink) - ..addColumns([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.topicId)!); - } - - Future upsertTabLink( - String tabId, { - required DateTime timestamp, - required String topicId, - }) { - return db.tabLink.insertOne( - TabLinkCompanion.insert( - id: tabId, - timestamp: timestamp, - topicId: topicId, - ), - mode: InsertMode.insertOrReplace, - ); - } - - Future touchTabLink( - String id, { - required DateTime timestamp, - }) { - final statement = db.tabLink.update()..where((t) => t.id.equals(id)); - return statement.write( - TabLinkCompanion(timestamp: Value(timestamp)), - ); - } - - Future syncTabLinks({required List retainTabIds}) { - return (db.tabLink.delete()..where((t) => t.id.isNotIn(retainTabIds))).go(); - } -} diff --git a/app/lib/features/geckoview/features/topics/data/database/daos/topic.dart b/app/lib/features/geckoview/features/topics/data/database/daos/topic.dart deleted file mode 100644 index ee0d6cf6..00000000 --- a/app/lib/features/geckoview/features/topics/data/database/daos/topic.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:ui'; - -import 'package:drift/drift.dart'; -import 'package:lensai/core/uuid.dart'; -import 'package:lensai/features/geckoview/features/topics/data/database/database.dart'; -import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart'; - -part 'topic.g.dart'; - -@DriftAccessor() -class TopicDao extends DatabaseAccessor with _$TopicDaoMixin { - TopicDao(super.db); - - Future addTopic({String? name, required Color color}) { - return db.topic.insertOne( - TopicCompanion.insert( - id: uuid.v7(), - name: Value(name), - color: color, - ), - ); - } - - Future replaceTopic( - String id, { - required String? name, - required Color color, - }) { - return db.topic.replaceOne( - TopicCompanion( - id: Value(id), - name: Value(name), - color: Value(color), - ), - ); - } - - Future deleteTopic(String id) { - return db.topic.deleteOne(TopicCompanion.custom(id: Variable(id))); - } - - SingleOrNullSelectable getTopicData(String id) { - return select(db.topic)..where((t) => t.id.equals(id)); - } - - Selectable getDistinctColors() { - final query = db.selectOnly(db.topic, distinct: true) - ..addColumns([db.topic.color]) - ..where(db.topic.color.isNotNull()); - - return query - .map((row) => row.readWithConverter(db.topic.color)!); - } -} diff --git a/app/lib/features/geckoview/features/topics/data/database/daos/topic.g.dart b/app/lib/features/geckoview/features/topics/data/database/daos/topic.g.dart deleted file mode 100644 index 30a65165..00000000 --- a/app/lib/features/geckoview/features/topics/data/database/daos/topic.g.dart +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'topic.dart'; - -// ignore_for_file: type=lint -mixin _$TopicDaoMixin on DatabaseAccessor {} diff --git a/app/lib/features/geckoview/features/topics/data/database/database.drift b/app/lib/features/geckoview/features/topics/data/database/database.drift deleted file mode 100644 index 62314fc2..00000000 --- a/app/lib/features/geckoview/features/topics/data/database/database.drift +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:lensai/features/geckoview/features/topics/data/database/drift/converters/color.dart'; -import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart'; - -CREATE TABLE topic ( - id TEXT PRIMARY KEY NOT NULL, - name TEXT, - color INTEGER NOT NULL MAPPED BY `const ColorConverter()` -) WITH TopicData; - -CREATE TABLE tab_link ( - id TEXT PRIMARY KEY NOT NULL, - topic_id TEXT NOT NULL REFERENCES topic (id) ON DELETE CASCADE, - timestamp DATETIME NOT NULL -); - -topicsWithCount WITH TopicDataWithCount: - SELECT - topic.*, - tab_agg.tab_count - FROM topic - LEFT JOIN ( - SELECT - topic_id, - COUNT(*) AS tab_count, - MAX(timestamp) AS last_updated - FROM tab_link - GROUP BY topic_id - ) AS tab_agg ON topic.id = tab_agg.topic_id - ORDER BY tab_agg.last_updated DESC NULLS FIRST; - diff --git a/app/lib/features/geckoview/features/topics/data/database/database.g.dart b/app/lib/features/geckoview/features/topics/data/database/database.g.dart deleted file mode 100644 index f98df2f8..00000000 --- a/app/lib/features/geckoview/features/topics/data/database/database.g.dart +++ /dev/null @@ -1,826 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'database.dart'; - -// ignore_for_file: type=lint -class Topic extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - Topic(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'PRIMARY KEY NOT NULL'); - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: ''); - late final GeneratedColumnWithTypeConverter color = - GeneratedColumn('color', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL') - .withConverter(Topic.$convertercolor); - @override - List get $columns => [id, name, color]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'topic'; - @override - Set get $primaryKey => {id}; - @override - TopicData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return TopicData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name']), - color: Topic.$convertercolor.fromSql(attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}color'])!), - ); - } - - @override - Topic createAlias(String alias) { - return Topic(attachedDatabase, alias); - } - - static TypeConverter $convertercolor = const ColorConverter(); - @override - bool get dontWriteConstraints => true; -} - -class TopicCompanion extends UpdateCompanion { - final Value id; - final Value name; - final Value color; - final Value rowid; - const TopicCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.color = const Value.absent(), - this.rowid = const Value.absent(), - }); - TopicCompanion.insert({ - required String id, - this.name = const Value.absent(), - required Color color, - this.rowid = const Value.absent(), - }) : id = Value(id), - color = Value(color); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? color, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (color != null) 'color': color, - if (rowid != null) 'rowid': rowid, - }); - } - - TopicCompanion copyWith( - {Value? id, - Value? name, - Value? color, - Value? rowid}) { - return TopicCompanion( - id: id ?? this.id, - name: name ?? this.name, - color: color ?? this.color, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (color.present) { - map['color'] = Variable(Topic.$convertercolor.toSql(color.value)); - } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('TopicCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('color: $color, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class TabLink extends Table with TableInfo { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - TabLink(this.attachedDatabase, [this._alias]); - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'PRIMARY KEY NOT NULL'); - late final GeneratedColumn topicId = GeneratedColumn( - 'topic_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL REFERENCES topic(id)ON DELETE CASCADE'); - late final GeneratedColumn timestamp = GeneratedColumn( - 'timestamp', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL'); - @override - List get $columns => [id, topicId, timestamp]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'tab_link'; - @override - Set get $primaryKey => {id}; - @override - TabLinkData map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return TabLinkData( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - topicId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}topic_id'])!, - timestamp: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}timestamp'])!, - ); - } - - @override - TabLink createAlias(String alias) { - return TabLink(attachedDatabase, alias); - } - - @override - bool get dontWriteConstraints => true; -} - -class TabLinkData extends DataClass implements Insertable { - final String id; - final String topicId; - final DateTime timestamp; - const TabLinkData( - {required this.id, required this.topicId, required this.timestamp}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['topic_id'] = Variable(topicId); - map['timestamp'] = Variable(timestamp); - return map; - } - - factory TabLinkData.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return TabLinkData( - id: serializer.fromJson(json['id']), - topicId: serializer.fromJson(json['topic_id']), - timestamp: serializer.fromJson(json['timestamp']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'topic_id': serializer.toJson(topicId), - 'timestamp': serializer.toJson(timestamp), - }; - } - - TabLinkData copyWith({String? id, String? topicId, DateTime? timestamp}) => - TabLinkData( - id: id ?? this.id, - topicId: topicId ?? this.topicId, - timestamp: timestamp ?? this.timestamp, - ); - TabLinkData copyWithCompanion(TabLinkCompanion data) { - return TabLinkData( - id: data.id.present ? data.id.value : this.id, - topicId: data.topicId.present ? data.topicId.value : this.topicId, - timestamp: data.timestamp.present ? data.timestamp.value : this.timestamp, - ); - } - - @override - String toString() { - return (StringBuffer('TabLinkData(') - ..write('id: $id, ') - ..write('topicId: $topicId, ') - ..write('timestamp: $timestamp') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash(id, topicId, timestamp); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is TabLinkData && - other.id == this.id && - other.topicId == this.topicId && - other.timestamp == this.timestamp); -} - -class TabLinkCompanion extends UpdateCompanion { - final Value id; - final Value topicId; - final Value timestamp; - final Value rowid; - const TabLinkCompanion({ - this.id = const Value.absent(), - this.topicId = const Value.absent(), - this.timestamp = const Value.absent(), - this.rowid = const Value.absent(), - }); - TabLinkCompanion.insert({ - required String id, - required String topicId, - required DateTime timestamp, - this.rowid = const Value.absent(), - }) : id = Value(id), - topicId = Value(topicId), - timestamp = Value(timestamp); - static Insertable custom({ - Expression? id, - Expression? topicId, - Expression? timestamp, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (topicId != null) 'topic_id': topicId, - if (timestamp != null) 'timestamp': timestamp, - if (rowid != null) 'rowid': rowid, - }); - } - - TabLinkCompanion copyWith( - {Value? id, - Value? topicId, - Value? timestamp, - Value? rowid}) { - return TabLinkCompanion( - id: id ?? this.id, - topicId: topicId ?? this.topicId, - timestamp: timestamp ?? this.timestamp, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (topicId.present) { - map['topic_id'] = Variable(topicId.value); - } - if (timestamp.present) { - map['timestamp'] = Variable(timestamp.value); - } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('TabLinkCompanion(') - ..write('id: $id, ') - ..write('topicId: $topicId, ') - ..write('timestamp: $timestamp, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -abstract class _$TabDatabase extends GeneratedDatabase { - _$TabDatabase(QueryExecutor e) : super(e); - $TabDatabaseManager get managers => $TabDatabaseManager(this); - late final Topic topic = Topic(this); - late final TabLink tabLink = TabLink(this); - late final TopicDao topicDao = TopicDao(this as TabDatabase); - late final TabLinkDao tabLinkDao = TabLinkDao(this as TabDatabase); - Selectable topicsWithCount() { - return customSelect( - 'SELECT topic.*, tab_agg.tab_count FROM topic LEFT JOIN (SELECT topic_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated FROM tab_link GROUP BY topic_id) AS tab_agg ON topic.id = tab_agg.topic_id ORDER BY tab_agg.last_updated DESC NULLS FIRST', - variables: [], - readsFrom: { - topic, - tabLink, - }).map((QueryRow row) => TopicDataWithCount( - id: row.read('id'), - name: row.readNullable('name'), - color: Topic.$convertercolor.fromSql(row.read('color')), - tabCount: row.readNullable('tab_count'), - )); - } - - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [topic, tabLink]; - @override - StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( - [ - WritePropagation( - on: TableUpdateQuery.onTableName('topic', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('tab_link', kind: UpdateKind.delete), - ], - ), - ], - ); -} - -typedef $TopicCreateCompanionBuilder = TopicCompanion Function({ - required String id, - Value name, - required Color color, - Value rowid, -}); -typedef $TopicUpdateCompanionBuilder = TopicCompanion Function({ - Value id, - Value name, - Value color, - Value rowid, -}); - -final class $TopicReferences - extends BaseReferences<_$TabDatabase, Topic, TopicData> { - $TopicReferences(super.$_db, super.$_table, super.$_typedResult); - - static MultiTypedResultKey> _tabLinkRefsTable( - _$TabDatabase db) => - MultiTypedResultKey.fromTable(db.tabLink, - aliasName: $_aliasNameGenerator(db.topic.id, db.tabLink.topicId)); - - $TabLinkProcessedTableManager get tabLinkRefs { - final manager = $TabLinkTableManager($_db, $_db.tabLink) - .filter((f) => f.topicId.id($_item.id)); - - final cache = $_typedResult.readTableOrNull(_tabLinkRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); - } -} - -class $TopicFilterComposer extends Composer<_$TabDatabase, Topic> { - $TopicFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); - - ColumnFilters get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnFilters(column)); - - ColumnWithTypeConverterFilters get color => - $composableBuilder( - column: $table.color, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - Expression tabLinkRefs( - Expression Function($TabLinkFilterComposer f) f) { - final $TabLinkFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.tabLink, - getReferencedColumn: (t) => t.topicId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $TabLinkFilterComposer( - $db: $db, - $table: $db.tabLink, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } -} - -class $TopicOrderingComposer extends Composer<_$TabDatabase, Topic> { - $TopicOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get color => $composableBuilder( - column: $table.color, builder: (column) => ColumnOrderings(column)); -} - -class $TopicAnnotationComposer extends Composer<_$TabDatabase, Topic> { - $TopicAnnotationComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - - GeneratedColumn get name => - $composableBuilder(column: $table.name, builder: (column) => column); - - GeneratedColumnWithTypeConverter get color => - $composableBuilder(column: $table.color, builder: (column) => column); - - Expression tabLinkRefs( - Expression Function($TabLinkAnnotationComposer a) f) { - final $TabLinkAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.tabLink, - getReferencedColumn: (t) => t.topicId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $TabLinkAnnotationComposer( - $db: $db, - $table: $db.tabLink, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } -} - -class $TopicTableManager extends RootTableManager< - _$TabDatabase, - Topic, - TopicData, - $TopicFilterComposer, - $TopicOrderingComposer, - $TopicAnnotationComposer, - $TopicCreateCompanionBuilder, - $TopicUpdateCompanionBuilder, - (TopicData, $TopicReferences), - TopicData, - PrefetchHooks Function({bool tabLinkRefs})> { - $TopicTableManager(_$TabDatabase db, Topic table) - : super(TableManagerState( - db: db, - table: table, - createFilteringComposer: () => - $TopicFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $TopicOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $TopicAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value name = const Value.absent(), - Value color = const Value.absent(), - Value rowid = const Value.absent(), - }) => - TopicCompanion( - id: id, - name: name, - color: color, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - Value name = const Value.absent(), - required Color color, - Value rowid = const Value.absent(), - }) => - TopicCompanion.insert( - id: id, - name: name, - color: color, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), $TopicReferences(db, table, e))) - .toList(), - prefetchHooksCallback: ({tabLinkRefs = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [if (tabLinkRefs) db.tabLink], - addJoins: null, - getPrefetchedDataCallback: (items) async { - return [ - if (tabLinkRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: $TopicReferences._tabLinkRefsTable(db), - managerFromTypedResult: (p0) => - $TopicReferences(db, table, p0).tabLinkRefs, - referencedItemsForCurrentItem: (item, - referencedItems) => - referencedItems.where((e) => e.topicId == item.id), - typedResults: items) - ]; - }, - ); - }, - )); -} - -typedef $TopicProcessedTableManager = ProcessedTableManager< - _$TabDatabase, - Topic, - TopicData, - $TopicFilterComposer, - $TopicOrderingComposer, - $TopicAnnotationComposer, - $TopicCreateCompanionBuilder, - $TopicUpdateCompanionBuilder, - (TopicData, $TopicReferences), - TopicData, - PrefetchHooks Function({bool tabLinkRefs})>; -typedef $TabLinkCreateCompanionBuilder = TabLinkCompanion Function({ - required String id, - required String topicId, - required DateTime timestamp, - Value rowid, -}); -typedef $TabLinkUpdateCompanionBuilder = TabLinkCompanion Function({ - Value id, - Value topicId, - Value timestamp, - Value rowid, -}); - -final class $TabLinkReferences - extends BaseReferences<_$TabDatabase, TabLink, TabLinkData> { - $TabLinkReferences(super.$_db, super.$_table, super.$_typedResult); - - static Topic _topicIdTable(_$TabDatabase db) => db.topic - .createAlias($_aliasNameGenerator(db.tabLink.topicId, db.topic.id)); - - $TopicProcessedTableManager? get topicId { - if ($_item.topicId == null) return null; - final manager = $TopicTableManager($_db, $_db.topic) - .filter((f) => f.id($_item.topicId!)); - final item = $_typedResult.readTableOrNull(_topicIdTable($_db)); - if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); - } -} - -class $TabLinkFilterComposer extends Composer<_$TabDatabase, TabLink> { - $TabLinkFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); - - ColumnFilters get timestamp => $composableBuilder( - column: $table.timestamp, builder: (column) => ColumnFilters(column)); - - $TopicFilterComposer get topicId { - final $TopicFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.topicId, - referencedTable: $db.topic, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $TopicFilterComposer( - $db: $db, - $table: $db.topic, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } -} - -class $TabLinkOrderingComposer extends Composer<_$TabDatabase, TabLink> { - $TabLinkOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get timestamp => $composableBuilder( - column: $table.timestamp, builder: (column) => ColumnOrderings(column)); - - $TopicOrderingComposer get topicId { - final $TopicOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.topicId, - referencedTable: $db.topic, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $TopicOrderingComposer( - $db: $db, - $table: $db.topic, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } -} - -class $TabLinkAnnotationComposer extends Composer<_$TabDatabase, TabLink> { - $TabLinkAnnotationComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - - GeneratedColumn get timestamp => - $composableBuilder(column: $table.timestamp, builder: (column) => column); - - $TopicAnnotationComposer get topicId { - final $TopicAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.topicId, - referencedTable: $db.topic, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $TopicAnnotationComposer( - $db: $db, - $table: $db.topic, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return composer; - } -} - -class $TabLinkTableManager extends RootTableManager< - _$TabDatabase, - TabLink, - TabLinkData, - $TabLinkFilterComposer, - $TabLinkOrderingComposer, - $TabLinkAnnotationComposer, - $TabLinkCreateCompanionBuilder, - $TabLinkUpdateCompanionBuilder, - (TabLinkData, $TabLinkReferences), - TabLinkData, - PrefetchHooks Function({bool topicId})> { - $TabLinkTableManager(_$TabDatabase db, TabLink table) - : super(TableManagerState( - db: db, - table: table, - createFilteringComposer: () => - $TabLinkFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $TabLinkOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $TabLinkAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value topicId = const Value.absent(), - Value timestamp = const Value.absent(), - Value rowid = const Value.absent(), - }) => - TabLinkCompanion( - id: id, - topicId: topicId, - timestamp: timestamp, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - required String topicId, - required DateTime timestamp, - Value rowid = const Value.absent(), - }) => - TabLinkCompanion.insert( - id: id, - topicId: topicId, - timestamp: timestamp, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map( - (e) => (e.readTable(table), $TabLinkReferences(db, table, e))) - .toList(), - prefetchHooksCallback: ({topicId = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic>>(state) { - if (topicId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.topicId, - referencedTable: $TabLinkReferences._topicIdTable(db), - referencedColumn: $TabLinkReferences._topicIdTable(db).id, - ) as T; - } - - return state; - }, - getPrefetchedDataCallback: (items) async { - return []; - }, - ); - }, - )); -} - -typedef $TabLinkProcessedTableManager = ProcessedTableManager< - _$TabDatabase, - TabLink, - TabLinkData, - $TabLinkFilterComposer, - $TabLinkOrderingComposer, - $TabLinkAnnotationComposer, - $TabLinkCreateCompanionBuilder, - $TabLinkUpdateCompanionBuilder, - (TabLinkData, $TabLinkReferences), - TabLinkData, - PrefetchHooks Function({bool topicId})>; - -class $TabDatabaseManager { - final _$TabDatabase _db; - $TabDatabaseManager(this._db); - $TopicTableManager get topic => $TopicTableManager(_db, _db.topic); - $TabLinkTableManager get tabLink => $TabLinkTableManager(_db, _db.tabLink); -} diff --git a/app/lib/features/geckoview/features/topics/domain/providers.dart b/app/lib/features/geckoview/features/topics/domain/providers.dart deleted file mode 100644 index 3ea70025..00000000 --- a/app/lib/features/geckoview/features/topics/domain/providers.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:ui'; - -import 'package:collection/collection.dart'; -import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart'; -import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; -import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart'; -import 'package:lensai/features/geckoview/features/topics/utils/color_palette.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'providers.g.dart'; - -@Riverpod() -Future unusedRandomTopicColor(UnusedRandomTopicColorRef ref) async { - final repository = ref.watch(topicRepositoryProvider.notifier); - - final allColors = colorTypes.flattened.toList(); - final usedColors = await repository.getDistinctColors(); - - Color randomColor; - do { - randomColor = randomColorShade(allColors); - } while (usedColors.contains(randomColor)); - - return randomColor; -} - -@Riverpod() -Stream> topicsWithCount(TopicsWithCountRef ref) { - final db = ref.watch(tabDatabaseProvider); - return db.topicsWithCount().watch(); -} - -@Riverpod() -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 deleted file mode 100644 index 5dc58b27..00000000 --- a/app/lib/features/geckoview/features/topics/domain/providers.g.dart +++ /dev/null @@ -1,320 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'providers.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$unusedRandomTopicColorHash() => - r'099730b0d987cc37bcae4ef5c999cbebfa67d7da'; - -/// See also [unusedRandomTopicColor]. -@ProviderFor(unusedRandomTopicColor) -final unusedRandomTopicColorProvider = - AutoDisposeFutureProvider.internal( - unusedRandomTopicColor, - name: r'unusedRandomTopicColorProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$unusedRandomTopicColorHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef UnusedRandomTopicColorRef = AutoDisposeFutureProviderRef; -String _$topicsWithCountHash() => r'cd9792b0e0af12cd8796a3f0b76b42171a4200e6'; - -/// See also [topicsWithCount]. -@ProviderFor(topicsWithCount) -final topicsWithCountProvider = - AutoDisposeStreamProvider>.internal( - topicsWithCount, - name: r'topicsWithCountProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$topicsWithCountHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef TopicsWithCountRef - = AutoDisposeStreamProviderRef>; -String _$topicTabIdsHash() => r'43719c70e01cf9e030d75171405ab9650936ddf8'; - -/// 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 [topicTabIds]. -@ProviderFor(topicTabIds) -const topicTabIdsProvider = TopicTabIdsFamily(); - -/// See also [topicTabIds]. -class TopicTabIdsFamily extends Family>> { - /// See also [topicTabIds]. - const TopicTabIdsFamily(); - - /// See also [topicTabIds]. - TopicTabIdsProvider call( - String topicId, - ) { - return TopicTabIdsProvider( - topicId, - ); - } - - @override - TopicTabIdsProvider getProviderOverride( - covariant TopicTabIdsProvider provider, - ) { - return call( - provider.topicId, - ); - } - - 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'topicTabIdsProvider'; -} - -/// See also [topicTabIds]. -class TopicTabIdsProvider extends AutoDisposeStreamProvider> { - /// See also [topicTabIds]. - TopicTabIdsProvider( - String topicId, - ) : this._internal( - (ref) => topicTabIds( - ref as TopicTabIdsRef, - topicId, - ), - from: topicTabIdsProvider, - name: r'topicTabIdsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$topicTabIdsHash, - dependencies: TopicTabIdsFamily._dependencies, - allTransitiveDependencies: - TopicTabIdsFamily._allTransitiveDependencies, - topicId: topicId, - ); - - TopicTabIdsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.topicId, - }) : super.internal(); - - final String topicId; - - @override - Override overrideWith( - Stream> Function(TopicTabIdsRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: TopicTabIdsProvider._internal( - (ref) => create(ref as TopicTabIdsRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - topicId: topicId, - ), - ); - } - - @override - AutoDisposeStreamProviderElement> createElement() { - return _TopicTabIdsProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is TopicTabIdsProvider && other.topicId == topicId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, topicId.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin TopicTabIdsRef on AutoDisposeStreamProviderRef> { - /// The parameter `topicId` of this provider. - String get topicId; -} - -class _TopicTabIdsProviderElement - extends AutoDisposeStreamProviderElement> with TopicTabIdsRef { - _TopicTabIdsProviderElement(super.provider); - - @override - String get topicId => (origin as TopicTabIdsProvider).topicId; -} - -String _$tabTopicIdHash() => r'284164ac3acd9be3ba4e15eba99fc8d97602ee84'; - -/// See also [tabTopicId]. -@ProviderFor(tabTopicId) -const tabTopicIdProvider = TabTopicIdFamily(); - -/// 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 deleted file mode 100644 index d5d5ee3b..00000000 --- a/app/lib/features/geckoview/features/topics/domain/providers/selected_topic.dart +++ /dev/null @@ -1,41 +0,0 @@ -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/repositories/selected_topic.dart b/app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.dart deleted file mode 100644 index 2316570c..00000000 --- a/app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'selected_topic.g.dart'; - -@Riverpod(keepAlive: true) -class SelectedTopicRepository extends _$SelectedTopicRepository { - 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; - } -} diff --git a/app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.g.dart b/app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.g.dart deleted file mode 100644 index 68c586c4..00000000 --- a/app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'selected_topic.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$selectedTopicRepositoryHash() => - r'd7ffe4ff5501aff747e5155d74b07974784f26f9'; - -/// See also [SelectedTopicRepository]. -@ProviderFor(SelectedTopicRepository) -final selectedTopicRepositoryProvider = - NotifierProvider.internal( - SelectedTopicRepository.new, - name: r'selectedTopicRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$selectedTopicRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$SelectedTopicRepository = 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 deleted file mode 100644 index 97206be2..00000000 --- a/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.dart +++ /dev/null @@ -1,42 +0,0 @@ -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'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'tab_link.g.dart'; - -@Riverpod(keepAlive: true) -class TabLinkRepository extends _$TabLinkRepository { - late TabDatabase _db; - - @override - void build() { - _db = ref.watch(tabDatabaseProvider); - } - - Future assignTab(String tabId, String topicId) { - return _db.tabLinkDao.upsertTabLink( - tabId, - timestamp: DateTime.now(), - topicId: topicId, - ); - } - - Future closeAllTabs(String? topicId) async { - final List tabIds; - if (topicId != null) { - tabIds = await _db.tabLinkDao.topicTabIds(topicId).get(); - } else { - final openTabs = ref.read(tabStatesProvider).keys.toSet(); - final assignedTabIds = await _db.tabLinkDao.allTabIds().get(); - - tabIds = - openTabs.where((tabId) => !assignedTabIds.contains(tabId)).toList(); - } - - if (tabIds.isNotEmpty) { - await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds); - } - } -} diff --git a/app/lib/features/geckoview/features/topics/domain/repositories/topic.dart b/app/lib/features/geckoview/features/topics/domain/repositories/topic.dart deleted file mode 100644 index 7e3cc3f1..00000000 --- a/app/lib/features/geckoview/features/topics/domain/repositories/topic.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:ui'; - -import 'package:lensai/features/geckoview/features/topics/data/database/database.dart'; -import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'topic.g.dart'; - -@Riverpod(keepAlive: true) -class TopicRepository extends _$TopicRepository { - late TabDatabase _db; - - @override - void build() { - _db = ref.watch(tabDatabaseProvider); - } - - Future addTopic({required String? name, required Color color}) { - return _db.topicDao.addTopic(name: name, color: color); - } - - Future replaceTopic({ - required String id, - required String? name, - required Color color, - }) { - return _db.topicDao.replaceTopic(id, name: name, color: color); - } - - Future deleteTopic(String id) { - return _db.topicDao.deleteTopic(id); - } - - Future> getDistinctColors() { - return _db.topicDao - .getDistinctColors() - .get() - .then((colors) => colors.toSet()); - } -} 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 deleted file mode 100644 index e33fb8a6..00000000 --- a/app/lib/features/geckoview/features/topics/presentation/widgets/topic_chips.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.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/features/topics/domain/providers.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(topicsWithCountProvider); - final selectedTopic = ref - .watch(selectedTopicDataProvider.select((value) => value.valueOrNull)); - - return topicsAsync.when( - data: (availableTopics) => SizedBox( - height: 48, - child: Row( - children: [ - const SizedBox(width: 16), - if (selectedTopic != null || availableTopics.isNotEmpty) - Expanded( - child: SelectableChips( - deleteIcon: false, - itemId: (topic) => topic.id, - itemAvatar: (topic) => Container( - width: 20.0, - height: 20.0, - decoration: BoxDecoration( - color: topic.color, - shape: BoxShape.circle, - ), - ), - itemLabel: (topic) => Text(topic.name ?? 'New Topic'), - itemBadgeCount: (topic) => topic.tabCount, - availableItems: availableTopics, - selectedItem: selectedTopic, - onSelected: (topic) { - ref.read(selectedTopicProvider.notifier).setTopic(topic.id); - }, - onDeleted: (topic) async { - ref.read(selectedTopicProvider.notifier).clearTopic(); - }, - ), - ) - else - Expanded( - child: Text( - "Press '>' to manage Topics.", - style: TextStyle( - color: Theme.of(context).hintColor, - fontStyle: FontStyle.italic, - ), - ), - ), - IconButton( - onPressed: () async { - await context.push(TopicListRoute().location); - }, - icon: const Icon(Icons.chevron_right), - ), - ], - ), - ), - error: (error, stackTrace) => const SizedBox.shrink(), - loading: () => const SizedBox( - height: 48, - width: double.infinity, - ), - ); - } -} diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/file_picker b/app/linux/flutter/ephemeral/.plugin_symlinks/file_picker index 2609a00e..432e06dc 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/file_picker +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/file_picker @@ -1 +1 @@ -/home/fafre/.pub-cache/hosted/pub.dev/file_picker-8.1.2/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/file_picker-8.1.3/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus b/app/linux/flutter/ephemeral/.plugin_symlinks/package_info_plus index d7600695..2f111d03 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.3/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/package_info_plus-8.1.0/ \ 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 68934bd1..fafd0ea6 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.3/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/share_plus-10.1.0/ \ No newline at end of file diff --git a/app/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs b/app/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs index 03aad085..994c3a63 120000 --- a/app/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs +++ b/app/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs @@ -1 +1 @@ -/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.24/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.25/ \ No newline at end of file diff --git a/app/pubspec.lock b/app/pubspec.lock index 2604a200..219f9437 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -202,10 +202,10 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" copy_with_extension: dependency: "direct main" description: @@ -234,10 +234,10 @@ packages: dependency: "direct main" description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" csslib: dependency: transitive description: @@ -378,18 +378,18 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" + sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877 url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.3" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -696,18 +696,18 @@ packages: dependency: transitive description: name: http_parser - sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4" + sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" image: dependency: transitive description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.0" intl: dependency: transitive description: @@ -772,6 +772,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + lexo_rank: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "2922c07bc9d3865cc5566efe1976ce0955029512" + url: "https://github.com/FaFre/lexo_rank.git" + source: git + version: "0.1.0" lint: dependency: "direct dev" description: @@ -792,10 +801,10 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" macros: dependency: transitive description: @@ -856,10 +865,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef" + sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998 url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.1.0" package_info_plus_platform_interface: dependency: transitive description: @@ -936,10 +945,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -980,6 +989,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + reorderable_grid: + dependency: "direct main" + description: + name: reorderable_grid + sha256: "0b9cd95ef0f070ef99f92affe9cf85a4aa127099cd1334e5940950ce58cd981d" + url: "https://pub.dev" + source: hosted + version: "1.0.10" riverpod: dependency: "direct main" description: @@ -1032,10 +1049,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11 + sha256: "334fcdf0ef9c0df0e3b428faebcac9568f35c747d59831474b2fc56e156d244e" url: "https://pub.dev" source: hosted - version: "10.0.3" + version: "10.1.0" share_plus_platform_interface: dependency: transitive description: @@ -1180,10 +1197,10 @@ packages: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: ccd29dd6cf6fb9351fa07cd6f92895809adbf0779c1d986acf5e3d53b3250e33 url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.25" sqlparser: dependency: transitive description: @@ -1232,6 +1249,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + synchronized: + dependency: "direct main" + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" + source: hosted + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -1276,10 +1301,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: "direct main" description: @@ -1355,10 +1380,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" uuid: dependency: "direct main" description: @@ -1419,10 +1444,10 @@ packages: dependency: transitive description: name: win32 - sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd + sha256: "2735daae5150e8b1dfeb3eb0544b4d3af0061e9e82cef063adcd583bdae4306a" url: "https://pub.dev" source: hosted - version: "5.6.0" + version: "5.7.0" xdg_directories: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index cd892999..41038dfc 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -10,14 +10,14 @@ dependencies: animations: ^2.0.11 collection: ^1.19.0 copy_with_extension: ^5.0.4 - crypto: ^3.0.5 + crypto: ^3.0.6 drift: ^2.21.0 dynamic_color: ^1.7.0 exceptions: ^0.6.1 expandable_page_view: ^1.0.17 fading_scroll: ^0.9.1 fast_equatable: ^1.1.0 - file_picker: ^8.1.2 + file_picker: ^8.1.3 flutter: sdk: flutter flutter_hooks: ^0.20.5 @@ -36,22 +36,27 @@ dependencies: html: ^0.15.4 http: ^1.2.2 json_annotation: ^4.9.0 + lexo_rank: + git: + url: https://github.com/FaFre/lexo_rank.git logger: ^2.4.0 markdown: ^7.2.2 mime: ^2.0.0 - package_info_plus: ^8.0.3 + package_info_plus: ^8.1.0 path: ^1.9.0 path_provider: ^2.1.4 + reorderable_grid: ^1.0.10 riverpod: ^2.5.3 riverpod_annotation: ^2.5.3 rxdart: ^0.28.0 - share_plus: ^10.0.3 + share_plus: ^10.1.0 shared_preferences: ^2.3.2 skeletonizer: ^1.4.2 speech_to_text_google_dialog: path: ../../speech_to_text_google_dialog sqlite3: ^2.4.6 - sqlite3_flutter_libs: ^0.5.24 + sqlite3_flutter_libs: ^0.5.25 + synchronized: ^3.3.0+3 text_scroll: ^0.2.0 timeago: ^3.7.0 universal_io: ^2.2.2 diff --git a/packages/flutter_mozilla_components/android/build.gradle b/packages/flutter_mozilla_components/android/build.gradle index 5137af9f..478ba287 100644 --- a/packages/flutter_mozilla_components/android/build.gradle +++ b/packages/flutter_mozilla_components/android/build.gradle @@ -3,6 +3,8 @@ version = "1.0-SNAPSHOT" buildscript { ext.kotlin_version = "1.9.22" + ext.mozillaComponentsVersion = '131.0.3' + repositories { google() mavenCentral() @@ -56,37 +58,36 @@ android { } dependencies { - implementation 'org.mozilla.components:support-utils:131.0b1' - implementation 'org.mozilla.components:support-ktx:131.0b1' - implementation 'org.mozilla.components:support-webextensions:131.0b1' - implementation 'org.mozilla.components:support-locale:131.0b1' - implementation 'org.mozilla.components:lib-fetch-httpurlconnection:131.0b1' - implementation 'org.mozilla.components:lib-crash:131.0b1' - implementation 'org.mozilla.components:lib-publicsuffixlist:131.0b1' - implementation 'org.mozilla.components:lib-dataprotect:131.0b1' - implementation 'org.mozilla.components:concept-engine:131.0b1' - implementation 'org.mozilla.components:concept-fetch:131.0b1' - implementation 'org.mozilla.components:browser-engine-gecko:131.0b1' - implementation 'org.mozilla.components:browser-state:131.0b1' - implementation 'org.mozilla.components:browser-session-storage:131.0b1' - implementation 'org.mozilla.components:browser-icons:131.0b1' - //implementation 'org.mozilla.components:browser-storage-sync:131.0b1' - implementation 'org.mozilla.components:browser-thumbnails:131.0b1' - implementation 'org.mozilla.components:feature-addons:131.0b1' - implementation 'org.mozilla.components:feature-app-links:131.0b1' - implementation 'org.mozilla.components:feature-autofill:131.0b1' - implementation 'org.mozilla.components:feature-downloads:131.0b1' - implementation 'org.mozilla.components:feature-media:131.0b1' - implementation 'org.mozilla.components:feature-tabs:131.0b1' - implementation 'org.mozilla.components:feature-prompts:131.0b1' - implementation 'org.mozilla.components:feature-session:131.0b1' - implementation 'org.mozilla.components:feature-readerview:131.0b1' - implementation 'org.mozilla.components:feature-privatemode:131.0b1' - implementation 'org.mozilla.components:feature-sitepermissions:131.0b1' - implementation 'org.mozilla.components:feature-webcompat:131.0b1' - implementation 'org.mozilla.components:feature-webnotifications:131.0b1' - implementation 'org.mozilla.components:service-digitalassetlinks:131.0b1' - implementation 'org.mozilla.components:ui-widgets:131.0b1' + implementation "org.mozilla.components:support-utils:$mozillaComponentsVersion" + implementation "org.mozilla.components:support-ktx:$mozillaComponentsVersion" + implementation "org.mozilla.components:support-webextensions:$mozillaComponentsVersion" + implementation "org.mozilla.components:support-locale:$mozillaComponentsVersion" + implementation "org.mozilla.components:lib-fetch-httpurlconnection:$mozillaComponentsVersion" + implementation "org.mozilla.components:lib-crash:$mozillaComponentsVersion" + implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion" + implementation "org.mozilla.components:lib-dataprotect:$mozillaComponentsVersion" + implementation "org.mozilla.components:concept-engine:$mozillaComponentsVersion" + implementation "org.mozilla.components:concept-fetch:$mozillaComponentsVersion" + implementation "org.mozilla.components:browser-engine-gecko:$mozillaComponentsVersion" + implementation "org.mozilla.components:browser-state:$mozillaComponentsVersion" + implementation "org.mozilla.components:browser-session-storage:$mozillaComponentsVersion" + implementation "org.mozilla.components:browser-icons:$mozillaComponentsVersion" + implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-autofill:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-media:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-tabs:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion" + implementation "org.mozilla.components:feature-webnotifications:$mozillaComponentsVersion" + implementation "org.mozilla.components:service-digitalassetlinks:$mozillaComponentsVersion" + implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion" implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BaseBrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BaseBrowserFragment.kt index df15ffda..ca21d32d 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BaseBrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/BaseBrowserFragment.kt @@ -229,22 +229,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { -// consumeFlow(components.store) { flow -> -// flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(sessionId) } -// .ifAnyChanged { tab -> -// arrayOf( -// tab.content.loading, -// tab.content.canGoBack, -// tab.content.canGoForward, -// ) -// } -// .collect { -// binding.toolbar.invalidateActions() -// } -// } - } - @CallSuper override fun onBackPressed(): Boolean = listOf(sessionFeature).any { it.onBackPressed() } 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 99fe089c..70d1fab2 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 @@ -31,9 +31,9 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use override fun createEngine(components: Components): EngineView { return components.engine.createView(context).apply { - selectionActionDelegate = DefaultSelectionActionDelegate( - components.selectionAction - ) +// selectionActionDelegate = DefaultSelectionActionDelegate( +// components.selectionAction +// ) } } 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 6f6c5ffe..8447b55e 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 @@ -88,6 +88,7 @@ open class DefaultComponents( } var engineView: EngineView? = null + var engineReportedInitialized = false val preferences: SharedPreferences = applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE) @@ -166,6 +167,7 @@ open class DefaultComponents( .distinctUntilChanged() .collect { tabId -> flutterEvents.onSelectedTabChange( + System.currentTimeMillis(), tabId ) { _ -> } } @@ -181,6 +183,7 @@ open class DefaultComponents( .collect { tab -> val iconBytes = tab.content.icon?.toWebPBytes() flutterEvents.onIconChange( + System.currentTimeMillis(), tab.id, iconBytes ) { _ -> } @@ -195,6 +198,7 @@ open class DefaultComponents( .debounce { 50 } .collect { tab -> flutterEvents.onSecurityInfoStateChange( + System.currentTimeMillis(), tab.id, SecurityInfoState( tab.content.securityInfo.secure, @@ -218,6 +222,7 @@ open class DefaultComponents( .debounce { 50 } .collect { tab -> flutterEvents.onReaderableStateChange( + System.currentTimeMillis(), tab.id, ReaderableState( tab.readerState.readerable, @@ -241,6 +246,7 @@ open class DefaultComponents( .debounce { 50 } .collect { tab -> flutterEvents.onHistoryStateChange( + System.currentTimeMillis(), tab.id, HistoryState( items = tab.content.history.items.map { item -> HistoryItem( @@ -259,7 +265,7 @@ open class DefaultComponents( flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} } .distinctUntilChanged() .collect { tabs -> - flutterEvents.onTabListChange(tabs) { _ -> } + flutterEvents.onTabListChange(System.currentTimeMillis(), tabs) { _ -> } } } @@ -280,6 +286,7 @@ open class DefaultComponents( .collect { tab -> logger.info("title: ${tab.content.title} ${tab.content.url}") flutterEvents.onTabContentStateChange( + System.currentTimeMillis(), TabContentState( id = tab.id, contextId = tab.contextId, @@ -303,6 +310,7 @@ open class DefaultComponents( .collect { tab -> tab.content.findResults flutterEvents.onFindResults( + System.currentTimeMillis(), tab.id, tab.content.findResults.map { result -> FindResultState( activeMatchOrdinal = result.activeMatchOrdinal.toLong(), diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GeckoPlatformView.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GeckoPlatformView.kt index c02260e1..44c295cd 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GeckoPlatformView.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GeckoPlatformView.kt @@ -40,7 +40,9 @@ private class NativeFragmentView( override fun onFlutterViewAttached(flutterView: View) { super.onFlutterViewAttached(flutterView) - flutterEvents.onFragmentReadyStateChange(true) { _ -> } + + GlobalComponents.components!!.engineReportedInitialized = false; + flutterEvents.onViewReadyStateChange(System.currentTimeMillis(),true) { _ -> } } override fun getView(): View { 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 7503c1a9..9e8490df 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 @@ -84,8 +84,6 @@ object GlobalComponents { Logger.error("Failed to initialize web extension support", e) } - newComponents.tabsUseCases.addTab.invoke("https://google.com", selectTab = true) - _components = newComponents } } \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt index 8598fa41..71336acf 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt @@ -1,9 +1,21 @@ package eu.lensai.flutter_mozilla_components.api +import eu.lensai.flutter_mozilla_components.GlobalComponents import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi +import mozilla.components.browser.state.action.SystemAction +import mozilla.components.feature.addons.logger class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Unit) : GeckoBrowserApi { override fun showNativeFragment() { showFragmentCallback.invoke(); } + + override fun onTrimMemory(level: Long) { + logger.debug("onTrimMemory: $level") + + val components = GlobalComponents.components!! + + components.store.dispatch(SystemAction.LowMemoryAction(level.toInt())) + components.icons.onTrimMemory(level.toInt()) + } } \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt index 5e299b24..89b8420f 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt @@ -19,7 +19,13 @@ import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState import eu.lensai.flutter_mozilla_components.pigeons.TabContentState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.icons.IconRequest import mozilla.components.browser.session.storage.RecoverableBrowserState import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.selector.findTab @@ -43,6 +49,7 @@ class GeckoTabsApiImpl() : GeckoTabsApi { private val engine: Engine by lazy { GlobalComponents.components!!.engine } private val state: BrowserState by lazy { GlobalComponents.components!!.store.state } private val thumbnailStorage: ThumbnailStorage by lazy { GlobalComponents.components!!.thumbnailStorage } + private val icons: BrowserIcons by lazy { GlobalComponents.components!!.icons } private val events: GeckoStateEvents by lazy { GlobalComponents.components!!.flutterEvents } private fun restoreSource(source: SourceValue ) : SessionState.Source { @@ -115,19 +122,27 @@ class GeckoTabsApiImpl() : GeckoTabsApi { onFindResults: Boolean, onThumbnailChange: Boolean, ) { + val tabs = state.tabs.map { x -> x.copy() }.toList() + val selectedTab = state.selectedTabId + if(onSelectedTabChange) { events.onSelectedTabChange( - state.selectedTabId + System.currentTimeMillis(), + selectedTab ) { _ -> } } if(onTabListChange) { - events.onTabListChange(state.tabs.map {tab -> tab.id}) { _ -> } + events.onTabListChange( + System.currentTimeMillis(), + tabs.map {tab -> tab.id} + ) { _ -> } } if(onTabContentStateChange) { - state.tabs.forEach { tab -> + tabs.forEach { tab -> events.onTabContentStateChange( + System.currentTimeMillis(), TabContentState( id = tab.id, contextId = tab.contextId, @@ -143,21 +158,35 @@ class GeckoTabsApiImpl() : GeckoTabsApi { } if(onIconChange) { - state.tabs.forEach { tab -> + tabs.forEach { tab -> if(tab.content.icon != null) { val iconBytes = tab.content.icon?.toWebPBytes() events.onIconChange( + System.currentTimeMillis(), tab.id, iconBytes ) { _ -> } + } else { + CoroutineScope(Dispatchers.Default).launch { + val result = icons.loadIcon(IconRequest(url = tab.content.url)).await() + val iconBytes = result.bitmap.toWebPBytes() + runOnUiThread { + events.onIconChange( + System.currentTimeMillis(), + tab.id, + iconBytes + ) { _ -> } + } + } + } } } if(onSecurityInfoStateChange) { - state.tabs.forEach { tab -> - val iconBytes = tab.content.icon?.toWebPBytes() + tabs.forEach { tab -> events.onSecurityInfoStateChange( + System.currentTimeMillis(), tab.id, SecurityInfoState( tab.content.securityInfo.secure, @@ -169,8 +198,9 @@ class GeckoTabsApiImpl() : GeckoTabsApi { } if(onReaderableStateChange) { - state.tabs.forEach { tab -> + tabs.forEach { tab -> events.onReaderableStateChange( + System.currentTimeMillis(), tab.id, ReaderableState( tab.readerState.readerable, @@ -181,8 +211,9 @@ class GeckoTabsApiImpl() : GeckoTabsApi { } if(onHistoryStateChange) { - state.tabs.forEach { tab -> + tabs.forEach { tab -> events.onHistoryStateChange( + System.currentTimeMillis(), tab.id, HistoryState( items = tab.content.history.items.map { item -> HistoryItem( @@ -198,8 +229,9 @@ class GeckoTabsApiImpl() : GeckoTabsApi { } if(onFindResults) { - state.tabs.forEach { tab -> + tabs.forEach { tab -> events.onFindResults( + System.currentTimeMillis(), tab.id, tab.content.findResults.map { result -> FindResultState( activeMatchOrdinal = result.activeMatchOrdinal.toLong(), @@ -211,13 +243,13 @@ class GeckoTabsApiImpl() : GeckoTabsApi { } if(onThumbnailChange) { - state.tabs.forEach { tab -> + tabs.forEach { tab -> CoroutineScope(Dispatchers.Default).launch { val bitmap = thumbnailStorage.loadThumbnail( ImageLoadRequest( id = tab.id, //TODO: make this configurable - size = 600, + size = 1024, isPrivate = tab.content.private ) ).await() @@ -225,7 +257,7 @@ class GeckoTabsApiImpl() : GeckoTabsApi { if(bitmap != null) { val bytes = bitmap.toWebPBytes() runOnUiThread { - events.onThumbnailChange(tab.id, bytes) { _ -> } + events.onThumbnailChange(System.currentTimeMillis(), tab.id, bytes) { _ -> } } } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/ext/Bitmap.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/ext/Bitmap.kt index 96ff31a0..54a2a5bb 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/ext/Bitmap.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/ext/Bitmap.kt @@ -4,6 +4,23 @@ import android.graphics.Bitmap import android.os.Build import java.io.ByteArrayOutputStream +fun Bitmap.resize(maxWidth: Int, maxHeight: Int): Bitmap { + var width = this.width + var height = this.height + + val aspectRatio: Float = width.toFloat() / height.toFloat() + + if (width > height) { + width = maxWidth + height = (width / aspectRatio).toInt() + } else { + height = maxHeight + width = (height * aspectRatio).toInt() + } + + return Bitmap.createScaledBitmap(this, width, height, true) +} + fun Bitmap.toWebPBytes(): ByteArray { val stream = ByteArrayOutputStream() val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt index 60b81dfd..23e6acdd 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/middleware/FlutterEventMiddleware.kt @@ -5,16 +5,22 @@ package eu.lensai.flutter_mozilla_components.middleware import android.graphics.Bitmap import android.util.Log +import eu.lensai.flutter_mozilla_components.GlobalComponents +import eu.lensai.flutter_mozilla_components.ext.resize import eu.lensai.flutter_mozilla_components.ext.toWebPBytes import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.LastAccessAction +import mozilla.components.browser.state.action.ReaderAction +import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.state.BrowserState +import mozilla.components.feature.addons.logger import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext import org.mozilla.gecko.util.ThreadUtils.runOnUiThread import java.io.ByteArrayOutputStream - +import kotlin.reflect.typeOf /** @@ -30,13 +36,37 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd ) { when (action) { is ContentAction.UpdateThumbnailAction -> { - val bytes = action.thumbnail.toWebPBytes() + val resized = action.thumbnail.resize(maxWidth = 640, maxHeight = 480); + val bytes = resized.toWebPBytes() + runOnUiThread { - flutterEvents.onThumbnailChange(action.sessionId, bytes) { _ -> } + flutterEvents.onThumbnailChange(System.currentTimeMillis(), action.sessionId, bytes) { _ -> } + } + } + //UpdateReaderConnectRequiredAction seems to be the only event that is called predictable + //after a hot reload + is ReaderAction.UpdateReaderConnectRequiredAction -> { + if(!GlobalComponents.components!!.engineReportedInitialized) { + runOnUiThread { + flutterEvents.onEngineReadyStateChange( + System.currentTimeMillis(), + true + ) { _ -> } + } + + GlobalComponents.components!!.engineReportedInitialized = true + } + } + is TabListAction.AddTabAction -> { + runOnUiThread { + flutterEvents.onTabAdded( + System.currentTimeMillis(), + action.tab.id + ) { _ -> } } } else -> { - // no-op + //logger.debug("Event fired: " + action.javaClass.name) } } next(action) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt index 550bf59d..08e3f6fb 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 @@ -1158,6 +1158,7 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface GeckoBrowserApi { fun showNativeFragment() + fun onTrimMemory(level: Long) companion object { /** The codec used by GeckoBrowserApi. */ @@ -1184,6 +1185,24 @@ interface GeckoBrowserApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.onTrimMemory$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val levelArg = args[0] as Long + val wrapped: List = try { + api.onTrimMemory(levelArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } @@ -2153,12 +2172,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val GeckoPigeonCodec() } } - fun onFragmentReadyStateChange(stateArg: Boolean, callback: (Result) -> Unit) + fun onViewReadyStateChange(timestampArg: Long, stateArg: Boolean, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFragmentReadyStateChange$separatedMessageChannelSuffix" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(stateArg)) { + channel.send(listOf(timestampArg, stateArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) @@ -2170,12 +2189,46 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } - fun onTabListChange(tabIdsArg: List, callback: (Result) -> Unit) + fun onEngineReadyStateChange(timestampArg: Long, stateArg: Boolean, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(timestampArg, stateArg)) { + 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 onTabAdded(timestampArg: Long, tabIdArg: String, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(timestampArg, tabIdArg)) { + 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 onTabListChange(timestampArg: Long, tabIdsArg: List, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(tabIdsArg)) { + channel.send(listOf(timestampArg, tabIdsArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) @@ -2187,12 +2240,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } - fun onSelectedTabChange(idArg: String?, callback: (Result) -> Unit) + fun onSelectedTabChange(timestampArg: Long, idArg: String?, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(idArg)) { + channel.send(listOf(timestampArg, 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?))) @@ -2204,12 +2257,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } - fun onTabContentStateChange(stateArg: TabContentState, callback: (Result) -> Unit) + fun onTabContentStateChange(timestampArg: Long, stateArg: TabContentState, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(stateArg)) { + channel.send(listOf(timestampArg, stateArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) @@ -2221,12 +2274,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } - fun onHistoryStateChange(idArg: String, stateArg: HistoryState, callback: (Result) -> Unit) + fun onHistoryStateChange(timestampArg: Long, idArg: String, stateArg: HistoryState, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(idArg, stateArg)) { + channel.send(listOf(timestampArg, idArg, stateArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) @@ -2238,12 +2291,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } - fun onReaderableStateChange(idArg: String, stateArg: ReaderableState, callback: (Result) -> Unit) + fun onReaderableStateChange(timestampArg: Long, idArg: String, stateArg: ReaderableState, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(idArg, stateArg)) { + channel.send(listOf(timestampArg, idArg, stateArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) @@ -2255,12 +2308,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } - fun onSecurityInfoStateChange(idArg: String, stateArg: SecurityInfoState, callback: (Result) -> Unit) + fun onSecurityInfoStateChange(timestampArg: Long, idArg: String, stateArg: SecurityInfoState, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(idArg, stateArg)) { + channel.send(listOf(timestampArg, idArg, stateArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) @@ -2272,12 +2325,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } - fun onIconChange(idArg: String, bytesArg: ByteArray?, callback: (Result) -> Unit) + fun onIconChange(timestampArg: Long, idArg: String, bytesArg: ByteArray?, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(idArg, bytesArg)) { + channel.send(listOf(timestampArg, idArg, bytesArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) @@ -2289,12 +2342,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } - fun onThumbnailChange(idArg: String, bytesArg: ByteArray?, callback: (Result) -> Unit) + fun onThumbnailChange(timestampArg: Long, idArg: String, bytesArg: ByteArray?, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(idArg, bytesArg)) { + channel.send(listOf(timestampArg, idArg, bytesArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) @@ -2306,12 +2359,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val } } } - fun onFindResults(idArg: String, resultsArg: List, callback: (Result) -> Unit) + fun onFindResults(timestampArg: Long, 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)) { + channel.send(listOf(timestampArg, 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?))) diff --git a/packages/flutter_mozilla_components/example/android/app/src/main/kotlin/eu/lensai/flutter_mozilla_components_example/MainActivity.kt b/packages/flutter_mozilla_components/example/android/app/src/main/kotlin/eu/lensai/flutter_mozilla_components_example/MainActivity.kt index 16bf8d66..31582d7f 100644 --- a/packages/flutter_mozilla_components/example/android/app/src/main/kotlin/eu/lensai/flutter_mozilla_components_example/MainActivity.kt +++ b/packages/flutter_mozilla_components/example/android/app/src/main/kotlin/eu/lensai/flutter_mozilla_components_example/MainActivity.kt @@ -1,5 +1,20 @@ package eu.lensai.flutter_mozilla_components_example import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel -class MainActivity: FlutterFragmentActivity() +class MainActivity: FlutterFragmentActivity() { + private val CHANNEL = "me.movenext.flutter_mozilla_components/trim_memory" + private lateinit var channel: MethodChannel + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + } + + override fun onTrimMemory(level: Int) { + super.onTrimMemory(level) + channel.invokeMethod("onTrimMemory", level) + } +} diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart index 53978b40..5c67b46a 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart @@ -10,4 +10,8 @@ class GeckoBrowserService { Future showNativeFragment() { return _api.showNativeFragment(); } + + Future onTrimMemory(int level) { + return _api.onTrimMemory(level); + } } 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 eb01dce9..4f78310f 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 @@ -12,22 +12,31 @@ typedef ThumbnailEvent = ({String tabId, Uint8List? bytes}); typedef FindResultsEvent = ({String tabId, List results}); class GeckoEventService extends GeckoStateEvents { + final _lastEventTimes = >{}; + // Stream controllers - final _fragmentStateSubject = BehaviorSubject.seeded(false); + final _viewStateSubject = BehaviorSubject.seeded(false); + final _engineStateSubject = BehaviorSubject.seeded(false); final _tabListSubject = BehaviorSubject>(); final _selectedTabSubject = BehaviorSubject(); - final _tabContentSubject = BehaviorSubject(); - final _historySubject = BehaviorSubject(); - final _readerableSubject = BehaviorSubject(); - final _securityInfoSubject = BehaviorSubject(); - final _iconSubject = BehaviorSubject(); - final _thumbnailSubject = BehaviorSubject(); - final _findResultsSubject = BehaviorSubject(); + + final _tabContentSubject = ReplaySubject(); + final _historySubject = ReplaySubject(); + final _securityInfoSubject = ReplaySubject(); + final _readerableSubject = ReplaySubject(); + + final _iconSubject = PublishSubject(); + final _thumbnailSubject = PublishSubject(); + final _findResultsSubject = PublishSubject(); + + final _tabAddedSubject = PublishSubject(); // Event streams - Stream get fragmentReadyStateEvents => _fragmentStateSubject.stream; - Stream> get tabListEvents => _tabListSubject.stream; - Stream get selectedTabEvents => _selectedTabSubject.stream; + ValueStream get viewReadyStateEvents => _viewStateSubject.stream; + ValueStream get engineReadyStateEvents => _engineStateSubject.stream; + ValueStream> get tabListEvents => _tabListSubject.stream; + ValueStream get selectedTabEvents => _selectedTabSubject.stream; + Stream get tabContentEvents => _tabContentSubject.stream; Stream get historyEvents => _historySubject.stream; Stream get readerableEvents => _readerableSubject.stream; @@ -37,55 +46,124 @@ class GeckoEventService extends GeckoStateEvents { Stream get thumbnailEvents => _thumbnailSubject.stream; Stream get findResultsEvent => _findResultsSubject.stream; + Stream get tabAddedStream => _tabAddedSubject.stream; + + void _addWhenMoreRecent( + Subject subject, + int timestamp, + dynamic identifier, + T value, + ) { + _lastEventTimes[subject] ??= {}; + + if ((_lastEventTimes[subject]?[identifier] ?? 0) < timestamp) { + _lastEventTimes[subject]![identifier] = timestamp; + subject.add(value); + } + } + @override - void onFragmentReadyStateChange(bool state) { - _fragmentStateSubject.add(state); + void onViewReadyStateChange(int timestamp, bool state) { + _addWhenMoreRecent(_viewStateSubject, timestamp, null, state); + } + + @override + void onEngineReadyStateChange(int timestamp, bool state) { + _addWhenMoreRecent(_engineStateSubject, timestamp, null, state); } // Overridden methods @override - void onTabListChange(List tabIds) { - _tabListSubject.add(tabIds.nonNulls.toList()); + void onTabListChange(int timestamp, List tabIds) { + _addWhenMoreRecent( + _tabListSubject, + timestamp, + null, + tabIds.nonNulls.toList(), + ); } @override - void onSelectedTabChange(String? id) { - _selectedTabSubject.add(id); + void onSelectedTabChange(int timestamp, String? id) { + _addWhenMoreRecent(_selectedTabSubject, timestamp, id, id); } @override - void onTabContentStateChange(TabContentState state) { - _tabContentSubject.add(state); + void onTabContentStateChange(int timestamp, TabContentState state) { + _addWhenMoreRecent(_tabContentSubject, timestamp, state.id, state); } @override - void onHistoryStateChange(String id, HistoryState state) { - _historySubject.add((tabId: id, history: state)); + void onHistoryStateChange(int timestamp, String id, HistoryState state) { + _addWhenMoreRecent( + _historySubject, + timestamp, + id, + (tabId: id, history: state), + ); } @override - void onReaderableStateChange(String id, ReaderableState state) { - _readerableSubject.add((tabId: id, readerable: state)); + void onReaderableStateChange( + int timestamp, + String id, + ReaderableState state, + ) { + _addWhenMoreRecent( + _readerableSubject, + timestamp, + id, + (tabId: id, readerable: state), + ); } @override - void onSecurityInfoStateChange(String id, SecurityInfoState state) { - _securityInfoSubject.add((tabId: id, securityInfo: state)); + void onSecurityInfoStateChange( + int timestamp, + String id, + SecurityInfoState state, + ) { + _addWhenMoreRecent( + _securityInfoSubject, + timestamp, + id, + (tabId: id, securityInfo: state), + ); } @override - void onIconChange(String id, Uint8List? bytes) { - _iconSubject.add((tabId: id, bytes: bytes)); + void onIconChange(int timestamp, String id, Uint8List? bytes) { + _addWhenMoreRecent(_iconSubject, timestamp, id, (tabId: id, bytes: bytes)); } @override - void onThumbnailChange(String id, Uint8List? bytes) { - _thumbnailSubject.add((tabId: id, bytes: bytes)); + void onThumbnailChange(int timestamp, String id, Uint8List? bytes) { + _addWhenMoreRecent( + _thumbnailSubject, + timestamp, + id, + (tabId: id, bytes: bytes), + ); } @override - void onFindResults(String id, List results) { - _findResultsSubject.add((tabId: id, results: results.nonNulls.toList())); + void onFindResults(int timestamp, String id, List results) { + _addWhenMoreRecent( + _findResultsSubject, + timestamp, + id, + (tabId: id, results: results.nonNulls.toList()), + ); + } + + @override + void onTabAdded(int timestamp, String tabId) { + _addWhenMoreRecent( + _tabAddedSubject, + timestamp, + null, + tabId, + ); } GeckoEventService.setUp({ @@ -109,5 +187,6 @@ class GeckoEventService extends GeckoStateEvents { unawaited(_iconSubject.close()); unawaited(_thumbnailSubject.close()); unawaited(_findResultsSubject.close()); + unawaited(_tabAddedSubject.close()); } } 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 94c8d25b..0b1b4b3e 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 @@ -11,14 +11,14 @@ class GeckoTabService { GeckoTabService({GeckoTabsApi? api}) : _api = api ?? _apiInstance; Future syncEvents({ - bool onSelectedTabChange = true, - bool onTabListChange = true, - bool onTabContentStateChange = true, - bool onIconChange = true, - bool onSecurityInfoStateChange = true, - bool onHistoryStateChange = true, - bool onFindResults = true, - bool onThumbnailChange = true, + bool onSelectedTabChange = false, + bool onTabListChange = false, + bool onTabContentStateChange = false, + bool onIconChange = false, + bool onSecurityInfoStateChange = false, + bool onHistoryStateChange = false, + bool onFindResults = false, + bool onThumbnailChange = false, }) { return _api.syncEvents( onSelectedTabChange: onSelectedTabChange, diff --git a/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart b/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart index 4b93937d..a62836ea 100644 --- a/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart +++ b/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart @@ -8,11 +8,35 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mozilla_components/src/domain/services/gecko_browser.dart'; -class GeckoView extends StatelessWidget { +class GeckoView extends StatefulWidget { final FutureOr Function()? preInitializationStep; const GeckoView({super.key, this.preInitializationStep}); + @override + State createState() => _GeckoViewState(); +} + +class _GeckoViewState extends State { + static const platform = + MethodChannel('me.movenext.flutter_mozilla_components/trim_memory'); + + final browserService = GeckoBrowserService(); + + @override + void initState() { + super.initState(); + _setupMethodCallHandler(); + } + + void _setupMethodCallHandler() { + platform.setMethodCallHandler((MethodCall call) async { + if (call.method == 'onTrimMemory') { + await browserService.onTrimMemory(call.arguments as int); + } + }); + } + @override Widget build(BuildContext context) { return PlatformViewLink( @@ -39,8 +63,14 @@ class GeckoView extends StatelessWidget { params.onPlatformViewCreated(value); SchedulerBinding.instance.addPostFrameCallback((_) async { - await preInitializationStep?.call(); - await GeckoBrowserService().showNativeFragment(); + await widget.preInitializationStep?.call(); + + await Future.delayed( + //Wait for two more frames just to be sure view has been initialized + Duration(milliseconds: ((1000 / 60) * 2).toInt()), + ).whenComplete(() async { + await browserService.showNativeFragment(); + }); }); }) // ignore: discarded_futures 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 d175b3f4..2f63b1ea 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -1187,6 +1187,28 @@ class GeckoBrowserApi { return; } } + + Future onTrimMemory(int level) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.onTrimMemory$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([level]) 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 GeckoEngineSettingsApi { @@ -2243,44 +2265,107 @@ class GeckoCookieApi { abstract class GeckoStateEvents { static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - void onFragmentReadyStateChange(bool state); + void onViewReadyStateChange(int timestamp, bool state); - void onTabListChange(List tabIds); + void onEngineReadyStateChange(int timestamp, bool state); - void onSelectedTabChange(String? id); + void onTabAdded(int timestamp, String tabId); - void onTabContentStateChange(TabContentState state); + void onTabListChange(int timestamp, List tabIds); - void onHistoryStateChange(String id, HistoryState state); + void onSelectedTabChange(int timestamp, String? id); - void onReaderableStateChange(String id, ReaderableState state); + void onTabContentStateChange(int timestamp, TabContentState state); - void onSecurityInfoStateChange(String id, SecurityInfoState state); + void onHistoryStateChange(int timestamp, String id, HistoryState state); - void onIconChange(String id, Uint8List? bytes); + void onReaderableStateChange(int timestamp, String id, ReaderableState state); - void onThumbnailChange(String id, Uint8List? bytes); + void onSecurityInfoStateChange(int timestamp, String id, SecurityInfoState state); - void onFindResults(String id, List results); + void onIconChange(int timestamp, String id, Uint8List? bytes); + + void onThumbnailChange(int timestamp, String id, Uint8List? bytes); + + void onFindResults(int timestamp, String id, List results); static void setUp(GeckoStateEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFragmentReadyStateChange$messageChannelSuffix', pigeonChannelCodec, + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange$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.onFragmentReadyStateChange was null.'); + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange was null.'); final List args = (message as List?)!; - final bool? arg_state = (args[0] as bool?); + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange was null, expected non-null int.'); + final bool? arg_state = (args[1] as bool?); assert(arg_state != null, - 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFragmentReadyStateChange was null, expected non-null bool.'); + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange was null, expected non-null bool.'); try { - api.onFragmentReadyStateChange(arg_state!); + api.onViewReadyStateChange(arg_timestamp!, arg_state!); + 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.GeckoStateEvents.onEngineReadyStateChange$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.onEngineReadyStateChange was null.'); + final List args = (message as List?)!; + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange was null, expected non-null int.'); + final bool? arg_state = (args[1] as bool?); + assert(arg_state != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange was null, expected non-null bool.'); + try { + api.onEngineReadyStateChange(arg_timestamp!, arg_state!); + 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.GeckoStateEvents.onTabAdded$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.onTabAdded was null.'); + final List args = (message as List?)!; + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded was null, expected non-null int.'); + final String? arg_tabId = (args[1] as String?); + assert(arg_tabId != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded was null, expected non-null String.'); + try { + api.onTabAdded(arg_timestamp!, arg_tabId!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -2301,11 +2386,14 @@ 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 int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null, expected non-null int.'); + final List? arg_tabIds = (args[1] as List?)?.cast(); assert(arg_tabIds != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null, expected non-null List.'); try { - api.onTabListChange(arg_tabIds!); + api.onTabListChange(arg_timestamp!, arg_tabIds!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -2326,9 +2414,12 @@ abstract class GeckoStateEvents { assert(message != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange was null.'); final List args = (message as List?)!; - final String? arg_id = (args[0] as String?); + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange was null, expected non-null int.'); + final String? arg_id = (args[1] as String?); try { - api.onSelectedTabChange(arg_id); + api.onSelectedTabChange(arg_timestamp!, arg_id); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -2349,11 +2440,14 @@ abstract class GeckoStateEvents { assert(message != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange was null.'); final List args = (message as List?)!; - final TabContentState? arg_state = (args[0] as TabContentState?); + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange was null, expected non-null int.'); + final TabContentState? arg_state = (args[1] as TabContentState?); assert(arg_state != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange was null, expected non-null TabContentState.'); try { - api.onTabContentStateChange(arg_state!); + api.onTabContentStateChange(arg_timestamp!, arg_state!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -2374,14 +2468,17 @@ abstract class GeckoStateEvents { assert(message != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null.'); final List args = (message as List?)!; - final String? arg_id = (args[0] as String?); + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null, expected non-null int.'); + final String? arg_id = (args[1] as String?); assert(arg_id != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null, expected non-null String.'); - final HistoryState? arg_state = (args[1] as HistoryState?); + final HistoryState? arg_state = (args[2] as HistoryState?); assert(arg_state != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null, expected non-null HistoryState.'); try { - api.onHistoryStateChange(arg_id!, arg_state!); + api.onHistoryStateChange(arg_timestamp!, arg_id!, arg_state!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -2402,14 +2499,17 @@ abstract class GeckoStateEvents { assert(message != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null.'); final List args = (message as List?)!; - final String? arg_id = (args[0] as String?); + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null, expected non-null int.'); + final String? arg_id = (args[1] as String?); assert(arg_id != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null, expected non-null String.'); - final ReaderableState? arg_state = (args[1] as ReaderableState?); + final ReaderableState? arg_state = (args[2] as ReaderableState?); assert(arg_state != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null, expected non-null ReaderableState.'); try { - api.onReaderableStateChange(arg_id!, arg_state!); + api.onReaderableStateChange(arg_timestamp!, arg_id!, arg_state!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -2430,14 +2530,17 @@ abstract class GeckoStateEvents { assert(message != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null.'); final List args = (message as List?)!; - final String? arg_id = (args[0] as String?); + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null, expected non-null int.'); + final String? arg_id = (args[1] as String?); assert(arg_id != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null, expected non-null String.'); - final SecurityInfoState? arg_state = (args[1] as SecurityInfoState?); + final SecurityInfoState? arg_state = (args[2] as SecurityInfoState?); assert(arg_state != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null, expected non-null SecurityInfoState.'); try { - api.onSecurityInfoStateChange(arg_id!, arg_state!); + api.onSecurityInfoStateChange(arg_timestamp!, arg_id!, arg_state!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -2458,12 +2561,15 @@ abstract class GeckoStateEvents { assert(message != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange was null.'); final List args = (message as List?)!; - final String? arg_id = (args[0] as String?); + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange was null, expected non-null int.'); + final String? arg_id = (args[1] as String?); assert(arg_id != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange was null, expected non-null String.'); - final Uint8List? arg_bytes = (args[1] as Uint8List?); + final Uint8List? arg_bytes = (args[2] as Uint8List?); try { - api.onIconChange(arg_id!, arg_bytes); + api.onIconChange(arg_timestamp!, arg_id!, arg_bytes); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -2484,12 +2590,15 @@ abstract class GeckoStateEvents { assert(message != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange was null.'); final List args = (message as List?)!; - final String? arg_id = (args[0] as String?); + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange was null, expected non-null int.'); + final String? arg_id = (args[1] as String?); assert(arg_id != null, 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange was null, expected non-null String.'); - final Uint8List? arg_bytes = (args[1] as Uint8List?); + final Uint8List? arg_bytes = (args[2] as Uint8List?); try { - api.onThumbnailChange(arg_id!, arg_bytes); + api.onThumbnailChange(arg_timestamp!, arg_id!, arg_bytes); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); @@ -2510,14 +2619,17 @@ abstract class GeckoStateEvents { 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?); + final int? arg_timestamp = (args[0] as int?); + assert(arg_timestamp != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null, expected non-null int.'); + final String? arg_id = (args[1] 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(); + final List? arg_results = (args[2] 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!); + api.onFindResults(arg_timestamp!, arg_id!, arg_results!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index 8c4db937..a9d9299d 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -539,6 +539,7 @@ class FindResultState { @HostApi() abstract class GeckoBrowserApi { void showNativeFragment(); + void onTrimMemory(int level); } @HostApi() @@ -782,19 +783,26 @@ abstract class GeckoCookieApi { @FlutterApi() abstract class GeckoStateEvents { - void onFragmentReadyStateChange(bool state); + void onViewReadyStateChange(int timestamp, bool state); + void onEngineReadyStateChange(int timestamp, bool state); - void onTabListChange(List tabIds); - void onSelectedTabChange(String? id); + void onTabAdded(int timestamp, String tabId); - void onTabContentStateChange(TabContentState state); - void onHistoryStateChange(String id, HistoryState state); - void onReaderableStateChange(String id, ReaderableState state); - void onSecurityInfoStateChange(String id, SecurityInfoState state); - void onIconChange(String id, Uint8List? bytes); - void onThumbnailChange(String id, Uint8List? bytes); + void onTabListChange(int timestamp, List tabIds); + void onSelectedTabChange(int timestamp, String? id); - void onFindResults(String id, List results); + void onTabContentStateChange(int timestamp, TabContentState state); + void onHistoryStateChange(int timestamp, String id, HistoryState state); + void onReaderableStateChange(int timestamp, String id, ReaderableState state); + void onSecurityInfoStateChange( + int timestamp, + String id, + SecurityInfoState state, + ); + void onIconChange(int timestamp, String id, Uint8List? bytes); + void onThumbnailChange(int timestamp, String id, Uint8List? bytes); + + void onFindResults(int timestamp, String id, List results); } @HostApi()