diff --git a/app/ios/Flutter/Generated.xcconfig b/app/ios/Flutter/Generated.xcconfig index a351d19c..095fc7c8 100644 --- a/app/ios/Flutter/Generated.xcconfig +++ b/app/ios/Flutter/Generated.xcconfig @@ -4,8 +4,8 @@ FLUTTER_APPLICATION_PATH=/home/fafre/Repos/kagi_bang_bang/app COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_TARGET=lib/main.dart FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=0.6.2 -FLUTTER_BUILD_NUMBER=14 +FLUTTER_BUILD_NAME=0.7.0 +FLUTTER_BUILD_NUMBER=15 EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 DART_OBFUSCATION=false diff --git a/app/ios/Flutter/flutter_export_environment.sh b/app/ios/Flutter/flutter_export_environment.sh index 3925d609..454b2c9d 100755 --- a/app/ios/Flutter/flutter_export_environment.sh +++ b/app/ios/Flutter/flutter_export_environment.sh @@ -5,8 +5,8 @@ export "FLUTTER_APPLICATION_PATH=/home/fafre/Repos/kagi_bang_bang/app" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=0.6.2" -export "FLUTTER_BUILD_NUMBER=14" +export "FLUTTER_BUILD_NAME=0.7.0" +export "FLUTTER_BUILD_NUMBER=15" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" diff --git a/app/lib/core/extension/result_async.dart b/app/lib/core/extension/result_async.dart deleted file mode 100644 index e9448764..00000000 --- a/app/lib/core/extension/result_async.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:exceptions/exceptions.dart'; -import 'package:riverpod/riverpod.dart'; - -extension AsyncExtension on Result { - AsyncValue toAsyncValue() { - return switch (this) { - Success(value: final value) => AsyncValue.data(value), - // TODO: Handle this case. - Failure( - error: final error, - ) => - AsyncError(error.message, error.stackTrace ?? StackTrace.empty), - }; - } -} diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index c006a4f8..8b63db74 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -10,7 +10,7 @@ import 'package:lensai/features/chat_archive/presentation/screens/list.dart'; import 'package:lensai/features/chat_archive/presentation/screens/search.dart'; import 'package:lensai/features/search_browser/presentation/screens/browser.dart'; import 'package:lensai/features/settings/presentation/screens/settings.dart'; -import 'package:lensai/features/topics/presentation/screens/topic_list.dart'; +import 'package:lensai/features/geckoview/features/topics/presentation/screens/topic_list.dart'; part 'routes.g.dart'; diff --git a/app/lib/domain/entities/received_parameter.dart b/app/lib/data/models/received_parameter.dart similarity index 100% rename from app/lib/domain/entities/received_parameter.dart rename to app/lib/data/models/received_parameter.dart diff --git a/app/lib/data/models/web_page_info.dart b/app/lib/data/models/web_page_info.dart new file mode 100644 index 00000000..100047aa --- /dev/null +++ b/app/lib/data/models/web_page_info.dart @@ -0,0 +1,17 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart'; + +part 'web_page_info.g.dart'; + +@CopyWith() +class WebPageInfo { + final Uri url; + final String? title; + final BrowserIcon? favicon; + + WebPageInfo({ + required this.url, + required this.title, + required this.favicon, + }); +} diff --git a/app/lib/data/models/web_page_info.g.dart b/app/lib/data/models/web_page_info.g.dart new file mode 100644 index 00000000..1e66b8eb --- /dev/null +++ b/app/lib/data/models/web_page_info.g.dart @@ -0,0 +1,78 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'web_page_info.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$WebPageInfoCWProxy { + WebPageInfo url(Uri url); + + WebPageInfo title(String? title); + + WebPageInfo favicon(BrowserIcon? favicon); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebPageInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// WebPageInfo(...).copyWith(id: 12, name: "My name") + /// ```` + WebPageInfo call({ + Uri? url, + String? title, + BrowserIcon? favicon, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfWebPageInfo.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfWebPageInfo.copyWith.fieldName(...)` +class _$WebPageInfoCWProxyImpl implements _$WebPageInfoCWProxy { + const _$WebPageInfoCWProxyImpl(this._value); + + final WebPageInfo _value; + + @override + WebPageInfo url(Uri url) => this(url: url); + + @override + WebPageInfo title(String? title) => this(title: title); + + @override + WebPageInfo favicon(BrowserIcon? favicon) => this(favicon: favicon); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebPageInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// WebPageInfo(...).copyWith(id: 12, name: "My name") + /// ```` + WebPageInfo call({ + Object? url = const $CopyWithPlaceholder(), + Object? title = const $CopyWithPlaceholder(), + Object? favicon = const $CopyWithPlaceholder(), + }) { + return WebPageInfo( + url: url == const $CopyWithPlaceholder() || url == null + ? _value.url + // ignore: cast_nullable_to_non_nullable + : url as Uri, + title: title == const $CopyWithPlaceholder() + ? _value.title + // ignore: cast_nullable_to_non_nullable + : title as String?, + favicon: favicon == const $CopyWithPlaceholder() + ? _value.favicon + // ignore: cast_nullable_to_non_nullable + : favicon as BrowserIcon?, + ); + } +} + +extension $WebPageInfoCopyWith on WebPageInfo { + /// Returns a callable class that can be used as follows: `instanceOfWebPageInfo.copyWith(...)` or like so:`instanceOfWebPageInfo.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$WebPageInfoCWProxy get copyWith => _$WebPageInfoCWProxyImpl(this); +} diff --git a/app/lib/domain/entities/equatable_image.dart b/app/lib/domain/entities/equatable_image.dart new file mode 100644 index 00000000..773a8ac4 --- /dev/null +++ b/app/lib/domain/entities/equatable_image.dart @@ -0,0 +1,27 @@ +import 'dart:ui'; + +import 'package:lensai/extensions/image.dart'; + +class EquatableImage { + final Image value; + final String? _imageHash; + + EquatableImage( + this.value, { + required String? hash, + }) : _imageHash = hash; + + static Future calculate(Image image) async { + final imageHash = await image.calculateHash(); + + return EquatableImage(image, hash: imageHash); + } + + @override + int get hashCode => _imageHash.hashCode; + + @override + bool operator ==(Object other) { + return other is EquatableImage && other._imageHash == _imageHash; + } +} diff --git a/app/lib/domain/entities/web_page_info.dart b/app/lib/domain/entities/web_page_info.dart deleted file mode 100644 index 89464a2d..00000000 --- a/app/lib/domain/entities/web_page_info.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; - -class WebPageInfo { - final Uri url; - final String? title; - final Favicon? favicon; - - WebPageInfo({ - required this.url, - required this.title, - required this.favicon, - }); - - factory WebPageInfo.fromJson(Map json) { - return WebPageInfo( - url: Uri.parse(json['url'] as String), - title: json['title'] as String?, - favicon: switch (json['favicon']) { - final Map favicon => Favicon.fromMap(favicon), - _ => null - }, - ); - } - - Map toJson() { - return { - 'url': url.toString(), - 'title': title, - 'favicon': favicon?.toJson(), - }; - } -} diff --git a/app/lib/domain/services/app_initialization.dart b/app/lib/domain/services/app_initialization.dart index 30e22a2c..3d31d6f3 100644 --- a/app/lib/domain/services/app_initialization.dart +++ b/app/lib/domain/services/app_initialization.dart @@ -9,8 +9,7 @@ import 'package:lensai/features/content_block/domain/repositories/sync.dart'; import 'package:lensai/features/search_browser/domain/services/session.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; -import 'package:lensai/features/topics/data/providers.dart'; -import 'package:lensai/features/web_view/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'app_initialization.g.dart'; @@ -96,8 +95,6 @@ class AppInitializationService extends _$AppInitializationService { final settings = await ref.read(settingsRepositoryProvider.future); final errors = []; - unawaited(ref.read(readerabilityScriptProvider.future)); - await _initPackageInfo(); final bangSyncResults = await _initBangs(); diff --git a/app/lib/domain/services/app_initialization.g.dart b/app/lib/domain/services/app_initialization.g.dart index 809183f6..43c61b14 100644 --- a/app/lib/domain/services/app_initialization.g.dart +++ b/app/lib/domain/services/app_initialization.g.dart @@ -7,7 +7,7 @@ part of 'app_initialization.dart'; // ************************************************************************** String _$appInitializationServiceHash() => - r'dadcf205d32efa61eb2a0ff1c9b3d49658b76a56'; + r'eee9c8eadc793fb435af1032ad2099dfd381a034'; /// See also [AppInitializationService]. @ProviderFor(AppInitializationService) diff --git a/app/lib/domain/services/generic_website.dart b/app/lib/domain/services/generic_website.dart index 24f01f35..646c5a98 100644 --- a/app/lib/domain/services/generic_website.dart +++ b/app/lib/domain/services/generic_website.dart @@ -1,57 +1,169 @@ +import 'dart:ui'; + import 'package:exceptions/exceptions.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; + import 'package:html/dom.dart'; import 'package:html/parser.dart' as html_parser; import 'package:http/http.dart' as http; import 'package:lensai/core/http_error_handler.dart'; -import 'package:lensai/domain/entities/web_page_info.dart'; -import 'package:lensai/extensions/web_uri_favicon.dart'; -import 'package:lensai/features/web_view/utils/favicon_helper.dart'; +import 'package:lensai/data/models/web_page_info.dart'; +import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:universal_io/io.dart'; part 'generic_website.g.dart'; +const _typeMap = { + "manifest": IconType.manifestIcon, + "icon": IconType.favicon, + "shortcut icon": IconType.favicon, + "fluid-icon": IconType.fluidIcon, + "apple-touch-icon": IconType.appleTouchIcon, + "image_src": IconType.imageSrc, + "apple-touch-icon image_src": IconType.appleTouchIcon, + "apple-touch-icon-precomposed": IconType.appleTouchIcon, + "og:image": IconType.openGraph, + "og:image:url": IconType.openGraph, + "og:image:secure_url": IconType.openGraph, + "twitter:image": IconType.twitter, + "msapplication-TileImage": IconType.microsoftTile, +}; + +Iterable sizesToList(String? sizes) sync* { + if (sizes != null) { + final splitted = + sizes.split(' ').where((size) => size.contains('x')).toList(); + + for (final size in splitted) { + final dimensions = size.split('x'); + if (dimensions.length == 2) { + final height = int.tryParse(dimensions[0]); + final width = int.tryParse(dimensions[1]); + + if (width != null && height != null) { + yield ResourceSize(height: height, width: width); + } + } + } + } +} + @Riverpod(keepAlive: true) class GenericWebsiteService extends _$GenericWebsiteService { + final GeckoIconService _iconsService; + late http.Client _client; + final Map _httpsCache; - GenericWebsiteService() : _httpsCache = {}; + GenericWebsiteService() + : _httpsCache = {}, + _iconsService = GeckoIconService(); @override void build() { _client = http.Client(); } - static Iterable _extractFavicons(Uri url, Document document) sync* { - final links = document.querySelectorAll( - 'link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]', + static Map _serializeResource(Resource resource) { + return { + 'url': resource.url, + 'type': resource.type, + 'sizes': resource.sizes.nonNulls.map((s) => [s.height, s.width]).toList(), + 'mimeType': resource.mimeType, + 'maskable': resource.maskable, + }; + } + + static Resource _deserializeResource(Map resource) { + return Resource( + url: resource['url'] as String, + type: resource['type'] as IconType, + mimeType: resource['mimeType'] as String?, + sizes: (resource['sizes'] as List>) + .map((s) => ResourceSize(height: s[0], width: s[1])) + .toList(), + maskable: resource['maskable'] as bool, ); + } - for (final link in links) { - final href = link.attributes['href']; - if (href != null) { - // Attempt to parse height and width if available - int? height; - int? width; - if (link.attributes['sizes'] case final String sizes) { - final dimensions = sizes.split('x'); - if (dimensions.length == 2) { - height = int.tryParse(dimensions[0]); - width = int.tryParse(dimensions[1]); - } + static List _extractIcons(Document document) { + final List icons = []; + + void collectLinkIcons(String rel) { + final links = document.querySelectorAll('link[rel="$rel"]'); + for (final link in links) { + final href = link.attributes['href']; + final type = _typeMap[rel]; + final mimeType = link.attributes['type']; + if (href != null && type != null) { + icons.add( + Resource( + url: href, + type: type, + sizes: sizesToList(link.attributes['sizes']).toList(), + mimeType: (mimeType?.isNotEmpty ?? true) ? null : mimeType, + maskable: false, + ), + ); } - - yield Favicon( - url: WebUri.uri(url.resolve(href)), - rel: link.attributes['rel'], - width: width, - height: height, - ); } } + + void collectMetaPropertyIcons(String property) { + final metas = document.querySelectorAll('meta[property="$property"]'); + for (final meta in metas) { + final content = meta.attributes['content']; + final type = _typeMap[property]; + if (content != null && type != null) { + icons.add( + Resource( + type: type, + url: content, + sizes: [], + maskable: false, + ), + ); + } + } + } + + void collectMetaNameIcons(String name) { + final metas = document.querySelectorAll('meta[name="$name"]'); + for (final meta in metas) { + final content = meta.attributes['content']; + final type = _typeMap[name]; + if (content != null && type != null) { + icons.add( + Resource( + type: type, + url: content, + sizes: [], + maskable: false, + ), + ); + } + } + } + + collectLinkIcons("icon"); + collectLinkIcons("shortcut icon"); + collectLinkIcons("fluid-icon"); + collectLinkIcons("apple-touch-icon"); + collectLinkIcons("image_src"); + collectLinkIcons("apple-touch-icon image_src"); + collectLinkIcons("apple-touch-icon-precomposed"); + + collectMetaPropertyIcons("og:image"); + collectMetaPropertyIcons("og:image:url"); + collectMetaPropertyIcons("og:image:secure_url"); + + collectMetaNameIcons("twitter:image"); + collectMetaNameIcons("msapplication-TileImage"); + + return icons; } Future> getInfo(Uri url) async { @@ -59,50 +171,47 @@ class GenericWebsiteService extends _$GenericWebsiteService { () async { final response = await _client.get(url).timeout(const Duration(seconds: 10)); - return await compute( - (args) { + + final result = await compute( + (args) async { final document = html_parser.parse(args[0]); - final url = Uri.parse(args[1]); final title = document.querySelector('title')?.text; - final favicon = choseFavicon(_extractFavicons(url, document)) ?? - //In case the icon can not get extracted, we use the resolver - //of duckduckgo - Favicon( - url: WebUri.uri(url.genericFavicon()), - ); + final resources = _extractIcons(document); - return WebPageInfo(url: url, title: title, favicon: favicon) - .toJson(); + return { + 'title': title, + 'resources': resources.map(_serializeResource).toList(), + }; }, - [response.body, url.toString()], - ).then(WebPageInfo.fromJson); + [response.body], + ); + + final resources = (result['resources']! as List>) + .map(_deserializeResource) + .toList(); + + final favicon = await getUrlFavicon(url: url, resources: resources); + + return WebPageInfo( + url: url, + title: result['title'] as String?, + favicon: favicon, + ); }, exceptionHandler: handleHttpError, ); } - Future> getFaviconBytes(Uri url) { - return getInfo(url).then( - (result) => result.flatMapAsync( - (info) async { - if (info.favicon != null) { - return _client - .get(info.favicon!.url) - .timeout(const Duration(seconds: 10)) - .then((response) { - if (response.statusCode == 200) { - return response.bodyBytes; - } else { - return null; - } - }); - } - - return null; - }, - exceptionHandler: handleHttpError, - ), + Future getUrlFavicon({ + required Uri url, + List resources = const [], + }) async { + final result = await _iconsService.loadIcon(url: url, resources: resources); + return BrowserIcon.fromBytes( + result.image, + dominantColor: (result.color != null) ? Color(result.color!) : null, + source: result.source, ); } diff --git a/app/lib/domain/services/generic_website.g.dart b/app/lib/domain/services/generic_website.g.dart index 1ac6b5ae..a3ad8894 100644 --- a/app/lib/domain/services/generic_website.g.dart +++ b/app/lib/domain/services/generic_website.g.dart @@ -7,7 +7,7 @@ part of 'generic_website.dart'; // ************************************************************************** String _$genericWebsiteServiceHash() => - r'517304ca95e1ef8377d4c1a3bd06fef9c68c92cd'; + r'0b1d953fe1b30b9dd0396df34e6176424cdaf941'; /// See also [GenericWebsiteService]. @ProviderFor(GenericWebsiteService) diff --git a/app/lib/core/extension/date_time.dart b/app/lib/extensions/date_time.dart similarity index 100% rename from app/lib/core/extension/date_time.dart rename to app/lib/extensions/date_time.dart diff --git a/app/lib/extensions/image.dart b/app/lib/extensions/image.dart new file mode 100644 index 00000000..cf2db883 --- /dev/null +++ b/app/lib/extensions/image.dart @@ -0,0 +1,20 @@ +import 'dart:ui'; + +import 'package:crypto/crypto.dart'; +import 'package:lensai/domain/entities/equatable_image.dart'; + +extension ImageHash on Image { + Future calculateHash() async { + final bytes = await toByteData(); + if (bytes != null) { + final digest = sha1.convert(bytes.buffer.asUint8List()); + return digest.toString(); + } + + return null; + } + + Future toEquatable() { + return EquatableImage.calculate(this); + } +} diff --git a/app/lib/extensions/web_uri_favicon.dart b/app/lib/extensions/web_uri_favicon.dart deleted file mode 100644 index 7689db60..00000000 --- a/app/lib/extensions/web_uri_favicon.dart +++ /dev/null @@ -1,4 +0,0 @@ -extension UriFavicon on Uri { - Uri genericFavicon() => - Uri.parse('https://icons.duckduckgo.com/ip3/$host.ico'); -} diff --git a/app/lib/features/app_widget/domain/services/home_widget.dart b/app/lib/features/app_widget/domain/services/home_widget.dart index 1ad71638..7a12820c 100644 --- a/app/lib/features/app_widget/domain/services/home_widget.dart +++ b/app/lib/features/app_widget/domain/services/home_widget.dart @@ -1,5 +1,5 @@ import 'package:home_widget/home_widget.dart'; -import 'package:lensai/domain/entities/received_parameter.dart'; +import 'package:lensai/data/models/received_parameter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/app/lib/features/bangs/data/database/daos/bang.dart b/app/lib/features/bangs/data/database/daos/bang.dart index 0c8b8137..e4dc9e04 100644 --- a/app/lib/features/bangs/data/database/daos/bang.dart +++ b/app/lib/features/bangs/data/database/daos/bang.dart @@ -1,4 +1,7 @@ +import 'dart:ui'; + import 'package:drift/drift.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:lensai/features/bangs/data/database/database.dart'; import 'package:lensai/features/bangs/data/models/bang.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; @@ -94,14 +97,17 @@ class BangDao extends DatabaseAccessor with _$BangDaoMixin { return db.bangQuery(query: db.buildQuery(searchString)); } - Future upsertBangIcon(String trigger, Uint8List iconData) { - return db.bangIcon.insertOne( - BangIconCompanion.insert( - trigger: trigger, - iconData: iconData, - fetchDate: DateTime.now(), - ), - mode: InsertMode.insertOrReplace, - ); - } + // Future upsertBangIcon(String trigger, BrowserIcon icon) async { + // //TODO: swithc to WEBP in future, requires 3rd party + // final compressed = await icon.image.toByteData(format: ImageByteFormat.png); + + // return db.bangIcon.insertOne( + // BangIconCompanion.insert( + // trigger: trigger, + // iconData: compressed!.buffer.asUint8List(), + // fetchDate: DateTime.now(), + // ), + // mode: InsertMode.insertOrReplace, + // ); + // } } diff --git a/app/lib/features/bangs/data/database/database.g.dart b/app/lib/features/bangs/data/database/database.g.dart index 02e423f3..533fe931 100644 --- a/app/lib/features/bangs/data/database/database.g.dart +++ b/app/lib/features/bangs/data/database/database.g.dart @@ -1021,8 +1021,6 @@ class BangDataView extends ViewInfo .read(DriftSqlType.int, data['${effectivePrefix}frequency']), lastUsed: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}last_used']), - iconData: attachedDatabase.typeMapping - .read(DriftSqlType.blob, data['${effectivePrefix}icon_data']), ); } @@ -1121,7 +1119,6 @@ abstract class _$BangDatabase extends GeneratedDatabase { .fromSql(row.readNullable('format')), frequency: row.readNullable('frequency'), lastUsed: row.readNullable('last_used'), - iconData: row.readNullable('icon_data'), )); } diff --git a/app/lib/features/bangs/data/models/bang_data.dart b/app/lib/features/bangs/data/models/bang_data.dart index 282b1c42..6ae12e96 100644 --- a/app/lib/features/bangs/data/models/bang_data.dart +++ b/app/lib/features/bangs/data/models/bang_data.dart @@ -1,6 +1,6 @@ import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:drift/drift.dart'; import 'package:lensai/features/bangs/data/models/bang.dart'; +import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart'; part 'bang_data.g.dart'; @@ -9,7 +9,7 @@ class BangData extends Bang { final int frequency; final DateTime? lastUsed; - final Uint8List? iconData; + final BrowserIcon? icon; BangData({ required super.websiteName, @@ -21,7 +21,7 @@ class BangData extends Bang { super.format, int? frequency, this.lastUsed, - this.iconData, + this.icon, }) : frequency = frequency ?? 0; @override @@ -32,6 +32,6 @@ class BangData extends Bang { ...super.hashParameters, frequency, lastUsed, - iconData, + icon, ]; } diff --git a/app/lib/features/bangs/data/models/bang_data.g.dart b/app/lib/features/bangs/data/models/bang_data.g.dart index d657568e..ad31ce98 100644 --- a/app/lib/features/bangs/data/models/bang_data.g.dart +++ b/app/lib/features/bangs/data/models/bang_data.g.dart @@ -25,7 +25,7 @@ abstract class _$BangDataCWProxy { BangData lastUsed(DateTime? lastUsed); - BangData iconData(Uint8List? iconData); + BangData icon(BrowserIcon? icon); /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `BangData(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// @@ -43,7 +43,7 @@ abstract class _$BangDataCWProxy { Set? format, int? frequency, DateTime? lastUsed, - Uint8List? iconData, + BrowserIcon? icon, }); } @@ -81,7 +81,7 @@ class _$BangDataCWProxyImpl implements _$BangDataCWProxy { BangData lastUsed(DateTime? lastUsed) => this(lastUsed: lastUsed); @override - BangData iconData(Uint8List? iconData) => this(iconData: iconData); + BangData icon(BrowserIcon? icon) => this(icon: icon); @override @@ -101,7 +101,7 @@ class _$BangDataCWProxyImpl implements _$BangDataCWProxy { Object? format = const $CopyWithPlaceholder(), Object? frequency = const $CopyWithPlaceholder(), Object? lastUsed = const $CopyWithPlaceholder(), - Object? iconData = const $CopyWithPlaceholder(), + Object? icon = const $CopyWithPlaceholder(), }) { return BangData( websiteName: @@ -142,10 +142,10 @@ class _$BangDataCWProxyImpl implements _$BangDataCWProxy { ? _value.lastUsed // ignore: cast_nullable_to_non_nullable : lastUsed as DateTime?, - iconData: iconData == const $CopyWithPlaceholder() - ? _value.iconData + icon: icon == const $CopyWithPlaceholder() + ? _value.icon // ignore: cast_nullable_to_non_nullable - : iconData as Uint8List?, + : icon as BrowserIcon?, ); } } diff --git a/app/lib/features/bangs/domain/repositories/data.dart b/app/lib/features/bangs/domain/repositories/data.dart index 9c957fe7..bfa31594 100644 --- a/app/lib/features/bangs/domain/repositories/data.dart +++ b/app/lib/features/bangs/domain/repositories/data.dart @@ -8,7 +8,6 @@ import 'package:lensai/features/bangs/data/database/database.dart'; import 'package:lensai/features/bangs/data/models/bang.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/data/providers.dart'; -import 'package:lensai/utils/image_helper.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'data.g.dart'; @@ -69,24 +68,16 @@ class BangDataRepository extends _$BangDataRepository { } Future> ensureIconAvailable(BangData bang) async { - if (bang.iconData != null) { + if (bang.icon != null) { return Result.success(bang); } - final result = await ref + final icon = await ref .read(genericWebsiteServiceProvider.notifier) - .getFaviconBytes(Uri.parse(bang.getUrl('').origin)); + .getUrlFavicon(url: Uri.parse(bang.getUrl('').origin)); - return result.flatMapAsync( - (faviconBytes) async { - if (faviconBytes != null && await isImageValid(faviconBytes)) { - await _db.bangDao.upsertBangIcon(bang.trigger, faviconBytes); - return bang.copyWith.iconData(faviconBytes); - } - - return bang; - }, - ); + // await _db.bangDao.upsertBangIcon(bang.trigger, icon); + return Result.success(bang.copyWith.icon(icon)); } Stream watchIconCacheSize() { diff --git a/app/lib/features/bangs/domain/repositories/data.g.dart b/app/lib/features/bangs/domain/repositories/data.g.dart index f70405ce..ad7e7412 100644 --- a/app/lib/features/bangs/domain/repositories/data.g.dart +++ b/app/lib/features/bangs/domain/repositories/data.g.dart @@ -7,7 +7,7 @@ part of 'data.dart'; // ************************************************************************** String _$bangDataRepositoryHash() => - r'ef06babcaa07361e37f5cc62a268eb10aed0fc82'; + r'abacc6db3a61958cae67a91cbfd312ae87c641b3'; /// See also [BangDataRepository]. @ProviderFor(BangDataRepository) diff --git a/app/lib/features/bangs/presentation/screens/list.dart b/app/lib/features/bangs/presentation/screens/list.dart index feeae86b..5a749ea7 100644 --- a/app/lib/features/bangs/presentation/screens/list.dart +++ b/app/lib/features/bangs/presentation/screens/list.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/bangs/domain/providers.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; import 'package:lensai/features/search_browser/domain/providers.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; @@ -49,9 +49,9 @@ class BangListScreen extends HookConsumerWidget { .read(selectedBangTriggerProvider().notifier) .setTrigger(bang.trigger); - if (ref.read(bottomSheetProvider) is! CreateTab) { + if (ref.read(bottomSheetProvider) is! CreateTabSheet) { ref.read(bottomSheetProvider.notifier).show( - CreateTab( + CreateTabSheet( preferredTool: KagiTool.search, ), ); diff --git a/app/lib/features/bangs/presentation/screens/search.dart b/app/lib/features/bangs/presentation/screens/search.dart index aa8a4838..8c159ae2 100644 --- a/app/lib/features/bangs/presentation/screens/search.dart +++ b/app/lib/features/bangs/presentation/screens/search.dart @@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/bangs/domain/repositories/search.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; import 'package:lensai/features/search_browser/domain/providers.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; @@ -77,9 +77,9 @@ class BangSearchScreen extends HookConsumerWidget { .read(selectedBangTriggerProvider().notifier) .setTrigger(bang.trigger); - if (ref.read(bottomSheetProvider) is! CreateTab) { + if (ref.read(bottomSheetProvider) is! CreateTabSheet) { ref.read(bottomSheetProvider.notifier).show( - CreateTab(preferredTool: KagiTool.search), + CreateTabSheet(preferredTool: KagiTool.search), ); } diff --git a/app/lib/features/bangs/presentation/widgets/bang_details.dart b/app/lib/features/bangs/presentation/widgets/bang_details.dart index 362e18a9..452bd1dc 100644 --- a/app/lib/features/bangs/presentation/widgets/bang_details.dart +++ b/app/lib/features/bangs/presentation/widgets/bang_details.dart @@ -6,7 +6,6 @@ import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; -import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart'; import 'package:lensai/utils/ui_helper.dart' as ui_helper; class BangDetails extends HookConsumerWidget { diff --git a/app/lib/features/bangs/presentation/widgets/bang_icon.dart b/app/lib/features/bangs/presentation/widgets/bang_icon.dart index 5b09001a..81767104 100644 --- a/app/lib/features/bangs/presentation/widgets/bang_icon.dart +++ b/app/lib/features/bangs/presentation/widgets/bang_icon.dart @@ -20,18 +20,12 @@ class BangIcon extends HookConsumerWidget { enabled: bangAsync.isLoading, child: SizedBox.square( dimension: iconSize, - child: (bang.iconData != null) - ? Image.memory( - bang.iconData!, + child: (bang.icon != null) + ? RawImage( + image: bang.icon?.image.value, height: iconSize, width: iconSize, fit: BoxFit.fill, - errorBuilder: (context, error, stackTrace) { - return Icon( - MdiIcons.web, - size: iconSize, - ); - }, ) : Icon( MdiIcons.web, diff --git a/app/lib/features/bangs/presentation/widgets/site_search.dart b/app/lib/features/bangs/presentation/widgets/site_search.dart index f597defc..a545a05f 100644 --- a/app/lib/features/bangs/presentation/widgets/site_search.dart +++ b/app/lib/features/bangs/presentation/widgets/site_search.dart @@ -7,7 +7,6 @@ import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; import 'package:lensai/features/bangs/presentation/widgets/search_field.dart'; import 'package:lensai/features/search_browser/domain/providers.dart'; -import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; class SiteSearch extends HookConsumerWidget { diff --git a/app/lib/features/chat_archive/presentation/screens/detail.dart b/app/lib/features/chat_archive/presentation/screens/detail.dart index a62180b4..c0068282 100644 --- a/app/lib/features/chat_archive/presentation/screens/detail.dart +++ b/app/lib/features/chat_archive/presentation/screens/detail.dart @@ -12,7 +12,6 @@ import 'package:lensai/features/chat_archive/domain/repositories/archive.dart'; import 'package:lensai/features/chat_archive/utils/markdown_to_text.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; -import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/utils/ui_helper.dart' as ui_helper; import 'package:skeletonizer/skeletonizer.dart'; @@ -51,7 +50,7 @@ class ChatArchiveDetailScreen extends HookConsumerWidget { if (chatAsync.valueOrNull != null) { await Clipboard.setData( ClipboardData( - text: await Future.microtask( + text: await Future( () => markdownToText(chatAsync.valueOrNull!), ), ), diff --git a/app/lib/features/chat_archive/presentation/screens/list.dart b/app/lib/features/chat_archive/presentation/screens/list.dart index 03488f2c..2e0f7ae0 100644 --- a/app/lib/features/chat_archive/presentation/screens/list.dart +++ b/app/lib/features/chat_archive/presentation/screens/list.dart @@ -2,7 +2,7 @@ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/core/extension/date_time.dart'; +import 'package:lensai/extensions/date_time.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/chat_archive/domain/repositories/archive.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; diff --git a/app/lib/features/chat_archive/presentation/screens/search.dart b/app/lib/features/chat_archive/presentation/screens/search.dart index af914209..9b4d3e8b 100644 --- a/app/lib/features/chat_archive/presentation/screens/search.dart +++ b/app/lib/features/chat_archive/presentation/screens/search.dart @@ -6,7 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/core/extension/date_time.dart'; +import 'package:lensai/extensions/date_time.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/features/chat_archive/domain/entities/chat_entity.dart'; import 'package:lensai/features/chat_archive/domain/repositories/search.dart'; diff --git a/app/lib/features/geckoview/domain/entities/browser_icon.dart b/app/lib/features/geckoview/domain/entities/browser_icon.dart new file mode 100644 index 00000000..72bc3f39 --- /dev/null +++ b/app/lib/features/geckoview/domain/entities/browser_icon.dart @@ -0,0 +1,46 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/domain/entities/equatable_image.dart'; +import 'package:lensai/extensions/image.dart'; +import 'package:lensai/features/geckoview/utils/image_helper.dart'; + +class BrowserIcon with FastEquatable { + final EquatableImage image; + + final Color? dominantColor; + final IconSource source; + + static Future fromBytes( + Uint8List bytes, { + required Color? dominantColor, + required IconSource source, + }) async { + final image = + await tryDecodeImage(bytes).then((image) => image!.toEquatable()); + + return BrowserIcon._( + image: image, + dominantColor: dominantColor, + source: source, + ); + } + + BrowserIcon._({ + required this.image, + required this.dominantColor, + required this.source, + }); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [ + image, + dominantColor, + source, + ]; +} diff --git a/app/lib/features/geckoview/domain/entities/history_state.dart b/app/lib/features/geckoview/domain/entities/history_state.dart new file mode 100644 index 00000000..47519b73 --- /dev/null +++ b/app/lib/features/geckoview/domain/entities/history_state.dart @@ -0,0 +1,43 @@ +import 'package:fast_equatable/fast_equatable.dart'; + +class HistoryItem with FastEquatable { + final Uri url; + final String title; + + HistoryItem({required this.url, required this.title}); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [url, title]; +} + +class HistoryState with FastEquatable { + final List items; + final int currentIndex; + + final bool canGoBack; + final bool canGoForward; + + HistoryState({ + required this.items, + required this.currentIndex, + required this.canGoBack, + required this.canGoForward, + }); + + factory HistoryState.$default() => HistoryState( + items: const [], + currentIndex: 0, + canGoBack: false, + canGoForward: false, + ); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => + [items, currentIndex, canGoBack, canGoForward]; +} diff --git a/app/lib/features/geckoview/domain/entities/readerable_state.dart b/app/lib/features/geckoview/domain/entities/readerable_state.dart new file mode 100644 index 00000000..13ea1fd3 --- /dev/null +++ b/app/lib/features/geckoview/domain/entities/readerable_state.dart @@ -0,0 +1,23 @@ +import 'package:fast_equatable/fast_equatable.dart'; + +class ReaderableState with FastEquatable { + final bool readerable; + + final bool active; + + ReaderableState({ + required this.readerable, + required this.active, + }); + + factory ReaderableState.$default() => ReaderableState( + readerable: false, + active: false, + ); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [readerable, active]; +} diff --git a/app/lib/features/geckoview/domain/entities/security_state.dart b/app/lib/features/geckoview/domain/entities/security_state.dart new file mode 100644 index 00000000..bb3b2c06 --- /dev/null +++ b/app/lib/features/geckoview/domain/entities/security_state.dart @@ -0,0 +1,25 @@ +import 'package:fast_equatable/fast_equatable.dart'; + +class SecurityState with FastEquatable { + final bool secure; + final String host; + final String issuer; + + SecurityState({ + required this.secure, + required this.host, + required this.issuer, + }); + + factory SecurityState.$default() => SecurityState( + secure: false, + host: "", + issuer: "", + ); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [secure, host, issuer]; +} diff --git a/app/lib/features/geckoview/domain/entities/tab_state.dart b/app/lib/features/geckoview/domain/entities/tab_state.dart new file mode 100644 index 00000000..41f9b32a --- /dev/null +++ b/app/lib/features/geckoview/domain/entities/tab_state.dart @@ -0,0 +1,84 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:lensai/domain/entities/equatable_image.dart'; +import 'package:lensai/features/geckoview/domain/entities/history_state.dart'; +import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; +import 'package:lensai/features/geckoview/domain/entities/security_state.dart'; + +part 'tab_state.g.dart'; + +@CopyWith() +class TabState with FastEquatable { + @CopyWithField(immutable: true) + final String id; + + final String? contextId; + + final Uri url; + final String title; + + final EquatableImage? icon; + final EquatableImage? thumbnail; + + final int progress; + + final bool isPrivate; + final bool isFullScreen; + final bool isLoading; + + final SecurityState securityInfoState; + final HistoryState historyState; + final ReaderableState readerableState; + + TabState({ + required this.id, + required this.contextId, + required this.url, + required this.title, + required this.icon, + required this.thumbnail, + required this.progress, + required this.isPrivate, + required this.isFullScreen, + required this.isLoading, + required this.securityInfoState, + required this.historyState, + required this.readerableState, + }); + + factory TabState.$default(String tabId) => TabState( + id: tabId, + contextId: null, + url: Uri.parse('about:blank'), + title: "", + icon: null, + thumbnail: null, + progress: 0, + isPrivate: false, + isFullScreen: false, + isLoading: false, + securityInfoState: SecurityState.$default(), + historyState: HistoryState.$default(), + readerableState: ReaderableState.$default(), + ); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [ + id, + contextId, + url, + title, + icon, + thumbnail, + progress, + isPrivate, + isFullScreen, + isLoading, + securityInfoState, + historyState, + readerableState, + ]; +} diff --git a/app/lib/features/geckoview/domain/entities/tab_state.g.dart b/app/lib/features/geckoview/domain/entities/tab_state.g.dart new file mode 100644 index 00000000..06e32e8d --- /dev/null +++ b/app/lib/features/geckoview/domain/entities/tab_state.g.dart @@ -0,0 +1,185 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tab_state.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$TabStateCWProxy { + TabState contextId(String? contextId); + + TabState url(Uri url); + + TabState title(String title); + + TabState icon(EquatableImage? icon); + + TabState thumbnail(EquatableImage? thumbnail); + + TabState progress(int progress); + + TabState isPrivate(bool isPrivate); + + TabState isFullScreen(bool isFullScreen); + + TabState isLoading(bool isLoading); + + TabState securityInfoState(SecurityState securityInfoState); + + TabState historyState(HistoryState historyState); + + TabState readerableState(ReaderableState readerableState); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TabState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// TabState(...).copyWith(id: 12, name: "My name") + /// ```` + TabState call({ + String? contextId, + Uri? url, + String? title, + EquatableImage? icon, + EquatableImage? thumbnail, + int? progress, + bool? isPrivate, + bool? isFullScreen, + bool? isLoading, + SecurityState? securityInfoState, + HistoryState? historyState, + ReaderableState? readerableState, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTabState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfTabState.copyWith.fieldName(...)` +class _$TabStateCWProxyImpl implements _$TabStateCWProxy { + const _$TabStateCWProxyImpl(this._value); + + final TabState _value; + + @override + TabState contextId(String? contextId) => this(contextId: contextId); + + @override + TabState url(Uri url) => this(url: url); + + @override + TabState title(String title) => this(title: title); + + @override + TabState icon(EquatableImage? icon) => this(icon: icon); + + @override + TabState thumbnail(EquatableImage? thumbnail) => this(thumbnail: thumbnail); + + @override + TabState progress(int progress) => this(progress: progress); + + @override + TabState isPrivate(bool isPrivate) => this(isPrivate: isPrivate); + + @override + TabState isFullScreen(bool isFullScreen) => this(isFullScreen: isFullScreen); + + @override + TabState isLoading(bool isLoading) => this(isLoading: isLoading); + + @override + TabState securityInfoState(SecurityState securityInfoState) => + this(securityInfoState: securityInfoState); + + @override + TabState historyState(HistoryState historyState) => + this(historyState: historyState); + + @override + TabState readerableState(ReaderableState readerableState) => + this(readerableState: readerableState); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TabState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// TabState(...).copyWith(id: 12, name: "My name") + /// ```` + TabState call({ + Object? contextId = const $CopyWithPlaceholder(), + Object? url = const $CopyWithPlaceholder(), + Object? title = const $CopyWithPlaceholder(), + Object? icon = const $CopyWithPlaceholder(), + Object? thumbnail = const $CopyWithPlaceholder(), + Object? progress = const $CopyWithPlaceholder(), + Object? isPrivate = const $CopyWithPlaceholder(), + Object? isFullScreen = const $CopyWithPlaceholder(), + Object? isLoading = const $CopyWithPlaceholder(), + Object? securityInfoState = const $CopyWithPlaceholder(), + Object? historyState = const $CopyWithPlaceholder(), + Object? readerableState = const $CopyWithPlaceholder(), + }) { + return TabState( + id: _value.id, + contextId: contextId == const $CopyWithPlaceholder() + ? _value.contextId + // ignore: cast_nullable_to_non_nullable + : contextId as String?, + url: url == const $CopyWithPlaceholder() || url == null + ? _value.url + // ignore: cast_nullable_to_non_nullable + : url as Uri, + title: title == const $CopyWithPlaceholder() || title == null + ? _value.title + // ignore: cast_nullable_to_non_nullable + : title as String, + icon: icon == const $CopyWithPlaceholder() + ? _value.icon + // ignore: cast_nullable_to_non_nullable + : icon as EquatableImage?, + thumbnail: thumbnail == const $CopyWithPlaceholder() + ? _value.thumbnail + // ignore: cast_nullable_to_non_nullable + : thumbnail as EquatableImage?, + progress: progress == const $CopyWithPlaceholder() || progress == null + ? _value.progress + // ignore: cast_nullable_to_non_nullable + : progress as int, + isPrivate: isPrivate == const $CopyWithPlaceholder() || isPrivate == null + ? _value.isPrivate + // ignore: cast_nullable_to_non_nullable + : isPrivate as bool, + isFullScreen: + isFullScreen == const $CopyWithPlaceholder() || isFullScreen == null + ? _value.isFullScreen + // ignore: cast_nullable_to_non_nullable + : isFullScreen as bool, + isLoading: isLoading == const $CopyWithPlaceholder() || isLoading == null + ? _value.isLoading + // ignore: cast_nullable_to_non_nullable + : isLoading as bool, + securityInfoState: securityInfoState == const $CopyWithPlaceholder() || + securityInfoState == null + ? _value.securityInfoState + // ignore: cast_nullable_to_non_nullable + : securityInfoState as SecurityState, + historyState: + historyState == const $CopyWithPlaceholder() || historyState == null + ? _value.historyState + // ignore: cast_nullable_to_non_nullable + : historyState as HistoryState, + readerableState: readerableState == const $CopyWithPlaceholder() || + readerableState == null + ? _value.readerableState + // ignore: cast_nullable_to_non_nullable + : readerableState as ReaderableState, + ); + } +} + +extension $TabStateCopyWith on TabState { + /// Returns a callable class that can be used as follows: `instanceOfTabState.copyWith(...)` or like so:`instanceOfTabState.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$TabStateCWProxy get copyWith => _$TabStateCWProxyImpl(this); +} diff --git a/app/lib/features/geckoview/domain/providers.dart b/app/lib/features/geckoview/domain/providers.dart new file mode 100644 index 00000000..6959964e --- /dev/null +++ b/app/lib/features/geckoview/domain/providers.dart @@ -0,0 +1,41 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; +import 'package:lensai/features/geckoview/domain/repositories/selected_tab.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab_session.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'providers.g.dart'; + +@Riverpod(keepAlive: true) +GeckoEventService eventService(EventServiceRef ref) { + final service = GeckoEventService.setUp(); + + ref.onDispose(() { + service.dispose(); + }); + + return service; +} + +@Riverpod() +TabState? tabState(TabStateRef ref, String? tabId) { + if (tabId == null) { + return null; + } + + return ref.watch( + tabRepositoryProvider.select((tabs) => tabs[tabId]), + ); +} + +@Riverpod() +TabState? selectedTabState(SelectedTabStateRef ref) { + final tabId = ref.watch(selectedTabControllerProvider); + return ref.watch(tabStateProvider(tabId)); +} + +@Riverpod() +TabSession selectedTabSessionNotifier(SelectedTabSessionNotifierRef ref) { + return ref.watch(tabSessionProvider(null).notifier); +} diff --git a/app/lib/features/geckoview/domain/providers.g.dart b/app/lib/features/geckoview/domain/providers.g.dart new file mode 100644 index 00000000..dd0a64c1 --- /dev/null +++ b/app/lib/features/geckoview/domain/providers.g.dart @@ -0,0 +1,204 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$eventServiceHash() => r'5aa357fdf0d217677a9a66ecb50417ac18929cad'; + +/// See also [eventService]. +@ProviderFor(eventService) +final eventServiceProvider = Provider.internal( + eventService, + name: r'eventServiceProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$eventServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef EventServiceRef = ProviderRef; +String _$tabStateHash() => r'6985f5472625a9cf44c8bbc61e61b7577eb60e71'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [tabState]. +@ProviderFor(tabState) +const tabStateProvider = TabStateFamily(); + +/// See also [tabState]. +class TabStateFamily extends Family { + /// See also [tabState]. + const TabStateFamily(); + + /// See also [tabState]. + TabStateProvider call( + String? tabId, + ) { + return TabStateProvider( + tabId, + ); + } + + @override + TabStateProvider getProviderOverride( + covariant TabStateProvider provider, + ) { + return call( + provider.tabId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'tabStateProvider'; +} + +/// See also [tabState]. +class TabStateProvider extends AutoDisposeProvider { + /// See also [tabState]. + TabStateProvider( + String? tabId, + ) : this._internal( + (ref) => tabState( + ref as TabStateRef, + tabId, + ), + from: tabStateProvider, + name: r'tabStateProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$tabStateHash, + dependencies: TabStateFamily._dependencies, + allTransitiveDependencies: TabStateFamily._allTransitiveDependencies, + tabId: tabId, + ); + + TabStateProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.tabId, + }) : super.internal(); + + final String? tabId; + + @override + Override overrideWith( + TabState? Function(TabStateRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: TabStateProvider._internal( + (ref) => create(ref as TabStateRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + tabId: tabId, + ), + ); + } + + @override + AutoDisposeProviderElement createElement() { + return _TabStateProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is TabStateProvider && other.tabId == tabId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, tabId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin TabStateRef on AutoDisposeProviderRef { + /// The parameter `tabId` of this provider. + String? get tabId; +} + +class _TabStateProviderElement extends AutoDisposeProviderElement + with TabStateRef { + _TabStateProviderElement(super.provider); + + @override + String? get tabId => (origin as TabStateProvider).tabId; +} + +String _$selectedTabStateHash() => r'd5240ade9010f5c9f00d7a4f419e1b776f75fac1'; + +/// See also [selectedTabState]. +@ProviderFor(selectedTabState) +final selectedTabStateProvider = AutoDisposeProvider.internal( + selectedTabState, + name: r'selectedTabStateProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedTabStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef SelectedTabStateRef = AutoDisposeProviderRef; +String _$selectedTabSessionNotifierHash() => + r'1b0fe1afa87c0b09fc8407ceb91946fffaef5214'; + +/// See also [selectedTabSessionNotifier]. +@ProviderFor(selectedTabSessionNotifier) +final selectedTabSessionNotifierProvider = + AutoDisposeProvider.internal( + selectedTabSessionNotifier, + name: r'selectedTabSessionNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedTabSessionNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/domain/repositories/selected_tab.dart b/app/lib/features/geckoview/domain/repositories/selected_tab.dart new file mode 100644 index 00000000..cf9cfc59 --- /dev/null +++ b/app/lib/features/geckoview/domain/repositories/selected_tab.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'selected_tab.g.dart'; + +@Riverpod(keepAlive: true) +class SelectedTabController extends _$SelectedTabController { + final _tabsService = GeckoTabService(); + + Future selectTab(String tabId) { + return _tabsService.selectTab(tabId: tabId); + } + + @override + String? build() { + final eventSerivce = ref.watch(eventServiceProvider); + + final selectedTabSub = eventSerivce.selectedTabEvents.listen( + (tabId) { + state = tabId; + }, + ); + + ref.onDispose(() { + unawaited(selectedTabSub.cancel()); + }); + + return null; + } +} diff --git a/app/lib/features/geckoview/domain/repositories/selected_tab.g.dart b/app/lib/features/geckoview/domain/repositories/selected_tab.g.dart new file mode 100644 index 00000000..37efb507 --- /dev/null +++ b/app/lib/features/geckoview/domain/repositories/selected_tab.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'selected_tab.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$selectedTabControllerHash() => + r'b5d79d4d5c2dc08d504e601fa29c8b83d51c9716'; + +/// See also [SelectedTabController]. +@ProviderFor(SelectedTabController) +final selectedTabControllerProvider = + NotifierProvider.internal( + SelectedTabController.new, + name: r'selectedTabControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedTabControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SelectedTabController = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/domain/repositories/tab.dart b/app/lib/features/geckoview/domain/repositories/tab.dart new file mode 100644 index 00000000..892405fc --- /dev/null +++ b/app/lib/features/geckoview/domain/repositories/tab.dart @@ -0,0 +1,165 @@ +import 'dart:async'; + +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/extensions/image.dart'; +import 'package:lensai/features/geckoview/domain/entities/history_state.dart'; +import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; +import 'package:lensai/features/geckoview/domain/entities/security_state.dart'; +import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; +import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:lensai/features/geckoview/utils/image_helper.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'tab.g.dart'; + +@Riverpod(keepAlive: true) +class TabRepository extends _$TabRepository { + final _tabsService = GeckoTabService(); + + void _onTabListChange(List tabs) { + state = { + for (final tabId in tabs) tabId: state[tabId] ?? TabState.$default(tabId), + }; + } + + void _onTabContentStateChange(TabContentState contentState) { + final current = + state[contentState.id] ?? TabState.$default(contentState.id); + + state = {...state}..[contentState.id] = current.copyWith( + contextId: contentState.contextId, + url: Uri.parse(contentState.url), + title: contentState.title, + progress: contentState.progress, + isPrivate: contentState.isPrivate, + isFullScreen: contentState.isFullScreen, + isLoading: contentState.isLoading, + ); + } + + Future _onIconChange(IconEvent event) async { + final IconEvent(:tabId, :bytes) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + final image = (bytes != null) + ? (await tryDecodeImage(bytes) + .then((image) async => image?.toEquatable())) + : null; + + state = {...state}..[tabId] = current.copyWith.icon(image); + } + + Future _onThumbnailChange(ThumbnailEvent event) async { + final ThumbnailEvent(:tabId, :bytes) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + final image = (bytes != null) + ? (await tryDecodeImage(bytes) + .then((image) async => image?.toEquatable())) + : null; + + state = {...state}..[tabId] = current.copyWith.thumbnail(image); + } + + void _onSecurityInfoStateChange(SecurityInfoEvent event) { + final SecurityInfoEvent(:tabId, :securityInfo) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + state = {...state}..[tabId] = current.copyWith.securityInfoState( + SecurityState( + secure: securityInfo.secure, + host: securityInfo.host, + issuer: securityInfo.issuer, + ), + ); + } + + void _onHistoryStateChange(HistoryEvent event) { + final HistoryEvent(:tabId, :history) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + state = {...state}..[tabId] = current.copyWith.historyState( + HistoryState( + items: history.items.nonNulls + .map( + (item) => + HistoryItem(url: Uri.parse(item.url), title: item.title), + ) + .toList(), + currentIndex: history.currentIndex, + canGoBack: history.canGoBack, + canGoForward: history.canGoForward, + ), + ); + } + + void _onReaderableStateChange(ReaderableEvent event) { + final ReaderableEvent(:tabId, :readerable) = event; + + final current = state[tabId] ?? TabState.$default(tabId); + + state = {...state}..[tabId] = current.copyWith.readerableState( + ReaderableState( + readerable: readerable.readerable, + active: readerable.active, + ), + ); + } + + Future addTab({ + Uri? url, + bool selectTab = true, + 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); + } + + @override + Map build() { + final eventService = ref.watch(eventServiceProvider); + + final subscriptions = [ + eventService.tabListEvents.listen(_onTabListChange), + eventService.tabContentEvents.listen(_onTabContentStateChange), + eventService.iconEvents.listen(_onIconChange), + eventService.thumbnailEvents.listen(_onThumbnailChange), + eventService.securityInfoEvents.listen(_onSecurityInfoStateChange), + eventService.historyEvents.listen(_onHistoryStateChange), + eventService.readerableEvents.listen(_onReaderableStateChange), + ]; + + ref.onDispose(() { + for (final sub in subscriptions) { + unawaited(sub.cancel()); + } + }); + + return {}; + } +} diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart new file mode 100644 index 00000000..5f1f0897 --- /dev/null +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tab.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$tabRepositoryHash() => r'64724c0215624753c5c6ff8d6827b890ad9a6f2c'; + +/// See also [TabRepository]. +@ProviderFor(TabRepository) +final tabRepositoryProvider = + NotifierProvider>.internal( + TabRepository.new, + name: r'tabRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$tabRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$TabRepository = Notifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/domain/repositories/tab_session.dart b/app/lib/features/geckoview/domain/repositories/tab_session.dart new file mode 100644 index 00000000..ac2cbcf7 --- /dev/null +++ b/app/lib/features/geckoview/domain/repositories/tab_session.dart @@ -0,0 +1,28 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'tab_session.g.dart'; + +@Riverpod() +class TabSession extends _$TabSession { + late GeckoSessionService _sessionService; + + Future loadUrl({ + required Uri url, + LoadUrlFlags flags = LoadUrlFlags.NONE, + Map? additionalHeaders, + }) { + return _sessionService.loadUrl( + url: url, + flags: flags, + additionalHeaders: additionalHeaders, + ); + } + + @override + void build(String? tabId) { + _sessionService = (tabId != null) + ? GeckoSessionService(tabId: tabId) + : GeckoSessionService.forActiveTab(); + } +} diff --git a/app/lib/features/geckoview/domain/repositories/tab_session.g.dart b/app/lib/features/geckoview/domain/repositories/tab_session.g.dart new file mode 100644 index 00000000..3f9d9430 --- /dev/null +++ b/app/lib/features/geckoview/domain/repositories/tab_session.g.dart @@ -0,0 +1,172 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tab_session.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$tabSessionHash() => r'4aab49dcb544cfc9e7a61b498d2ccf2c28ef729b'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$TabSession extends BuildlessAutoDisposeNotifier { + late final String? tabId; + + void build( + String? tabId, + ); +} + +/// See also [TabSession]. +@ProviderFor(TabSession) +const tabSessionProvider = TabSessionFamily(); + +/// See also [TabSession]. +class TabSessionFamily extends Family { + /// See also [TabSession]. + const TabSessionFamily(); + + /// See also [TabSession]. + TabSessionProvider call( + String? tabId, + ) { + return TabSessionProvider( + tabId, + ); + } + + @override + TabSessionProvider getProviderOverride( + covariant TabSessionProvider 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'tabSessionProvider'; +} + +/// See also [TabSession]. +class TabSessionProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [TabSession]. + TabSessionProvider( + String? tabId, + ) : this._internal( + () => TabSession()..tabId = tabId, + from: tabSessionProvider, + name: r'tabSessionProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$tabSessionHash, + dependencies: TabSessionFamily._dependencies, + allTransitiveDependencies: + TabSessionFamily._allTransitiveDependencies, + tabId: tabId, + ); + + TabSessionProvider._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 + void runNotifierBuild( + covariant TabSession notifier, + ) { + return notifier.build( + tabId, + ); + } + + @override + Override overrideWith(TabSession Function() create) { + return ProviderOverride( + origin: this, + override: TabSessionProvider._internal( + () => create()..tabId = tabId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + tabId: tabId, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement createElement() { + return _TabSessionProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is TabSessionProvider && 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 TabSessionRef on AutoDisposeNotifierProviderRef { + /// The parameter `tabId` of this provider. + String? get tabId; +} + +class _TabSessionProviderElement + extends AutoDisposeNotifierProviderElement + with TabSessionRef { + _TabSessionProviderElement(super.provider); + + @override + String? get tabId => (origin as TabSessionProvider).tabId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 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 new file mode 100644 index 00000000..ddc4b345 --- /dev/null +++ b/app/lib/features/geckoview/features/topics/data/database/daos/tab.dart @@ -0,0 +1,51 @@ +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( + (topicId == null) + ? db.tabLink.topicId.isNull() + : db.tabLink.topicId.equals(topicId), + ) + ..orderBy([OrderingTerm.asc(db.tabLink.id)])); + + return query.map((row) => row.read(db.tabLink.id)!); + } + + Future upsertTabLink( + String tabId, { + required DateTime timestamp, + Value topicId = const Value.absent(), + }) { + 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/topics/data/database/daos/tab.g.dart b/app/lib/features/geckoview/features/topics/data/database/daos/tab.g.dart similarity index 61% rename from app/lib/features/topics/data/database/daos/tab.g.dart rename to app/lib/features/geckoview/features/topics/data/database/daos/tab.g.dart index 3da23453..df16e4ce 100644 --- a/app/lib/features/topics/data/database/daos/tab.g.dart +++ b/app/lib/features/geckoview/features/topics/data/database/daos/tab.g.dart @@ -3,4 +3,4 @@ part of 'tab.dart'; // ignore_for_file: type=lint -mixin _$TabDaoMixin on DatabaseAccessor {} +mixin _$TabLinkDaoMixin on DatabaseAccessor {} diff --git a/app/lib/features/topics/data/database/daos/topic.dart b/app/lib/features/geckoview/features/topics/data/database/daos/topic.dart similarity index 87% rename from app/lib/features/topics/data/database/daos/topic.dart rename to app/lib/features/geckoview/features/topics/data/database/daos/topic.dart index d8e38a45..ee0d6cf6 100644 --- a/app/lib/features/topics/data/database/daos/topic.dart +++ b/app/lib/features/geckoview/features/topics/data/database/daos/topic.dart @@ -2,8 +2,8 @@ import 'dart:ui'; import 'package:drift/drift.dart'; import 'package:lensai/core/uuid.dart'; -import 'package:lensai/features/topics/data/database/database.dart'; -import 'package:lensai/features/topics/data/models/topic_data.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'; diff --git a/app/lib/features/topics/data/database/daos/topic.g.dart b/app/lib/features/geckoview/features/topics/data/database/daos/topic.g.dart similarity index 100% rename from app/lib/features/topics/data/database/daos/topic.g.dart rename to app/lib/features/geckoview/features/topics/data/database/daos/topic.g.dart diff --git a/app/lib/features/geckoview/features/topics/data/database/database.dart b/app/lib/features/geckoview/features/topics/data/database/database.dart new file mode 100644 index 00000000..8307046a --- /dev/null +++ b/app/lib/features/geckoview/features/topics/data/database/database.dart @@ -0,0 +1,52 @@ +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'; + +part 'database.g.dart'; + +@DriftDatabase( + include: {'database.drift'}, + daos: [TopicDao, TabLinkDao], +) +class TabDatabase extends _$TabDatabase { + @override + final int schemaVersion = 2; + + @override + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + // disable foreign_keys before migrations + await customStatement('PRAGMA foreign_keys = OFF'); + + 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'); + }); + } + + // Assert that the schema is valid after migrations + if (kDebugMode) { + final wrongForeignKeys = + await customSelect('PRAGMA foreign_key_check').get(); + assert( + wrongForeignKeys.isEmpty, + '${wrongForeignKeys.map((e) => e.data)}', + ); + } + }, + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON;'); + }, + ); + + TabDatabase(super.e); +} diff --git a/app/lib/features/topics/data/database/database.drift b/app/lib/features/geckoview/features/topics/data/database/database.drift similarity index 59% rename from app/lib/features/topics/data/database/database.drift rename to app/lib/features/geckoview/features/topics/data/database/database.drift index ada2d0c5..47730b04 100644 --- a/app/lib/features/topics/data/database/database.drift +++ b/app/lib/features/geckoview/features/topics/data/database/database.drift @@ -1,6 +1,5 @@ -import 'package:lensai/features/topics/data/database/drift/converters/color.dart'; -import 'package:lensai/features/topics/data/database/drift/converters/uri.dart'; -import 'package:lensai/features/topics/data/models/topic_data.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'; CREATE TABLE topic ( id TEXT PRIMARY KEY NOT NULL, @@ -8,13 +7,10 @@ CREATE TABLE topic ( color INTEGER NOT NULL MAPPED BY `const ColorConverter()` ) WITH TopicData; -CREATE TABLE tab ( +CREATE TABLE tab_link ( id TEXT PRIMARY KEY NOT NULL, topic_id TEXT REFERENCES topic (id) ON DELETE CASCADE, - timestamp DATETIME NOT NULL, - url TEXT NOT NULL MAPPED BY `const UriConverter()`, - title TEXT, - screenshot BLOB + timestamp DATETIME NOT NULL ); topicsWithCount WITH TopicDataWithCount: @@ -27,7 +23,7 @@ topicsWithCount WITH TopicDataWithCount: topic_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated - FROM tab + 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/topics/data/database/database.g.dart b/app/lib/features/geckoview/features/topics/data/database/database.g.dart similarity index 65% rename from app/lib/features/topics/data/database/database.g.dart rename to app/lib/features/geckoview/features/topics/data/database/database.g.dart index cc0200e9..347a212e 100644 --- a/app/lib/features/topics/data/database/database.g.dart +++ b/app/lib/features/geckoview/features/topics/data/database/database.g.dart @@ -131,11 +131,11 @@ class TopicCompanion extends UpdateCompanion { } } -class Tab extends Table with TableInfo { +class TabLink extends Table with TableInfo { @override final GeneratedDatabase attachedDatabase; final String? _alias; - Tab(this.attachedDatabase, [this._alias]); + TabLink(this.attachedDatabase, [this._alias]); late final GeneratedColumn id = GeneratedColumn( 'id', aliasedName, false, type: DriftSqlType.string, @@ -151,75 +151,42 @@ class Tab extends Table with TableInfo { type: DriftSqlType.dateTime, requiredDuringInsert: true, $customConstraints: 'NOT NULL'); - late final GeneratedColumnWithTypeConverter url = - GeneratedColumn('url', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - $customConstraints: 'NOT NULL') - .withConverter(Tab.$converterurl); - late final GeneratedColumn title = GeneratedColumn( - 'title', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: ''); - late final GeneratedColumn screenshot = GeneratedColumn( - 'screenshot', aliasedName, true, - type: DriftSqlType.blob, - requiredDuringInsert: false, - $customConstraints: ''); @override - List get $columns => - [id, topicId, timestamp, url, title, screenshot]; + List get $columns => [id, topicId, timestamp]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'tab'; + static const String $name = 'tab_link'; @override Set get $primaryKey => {id}; @override - TabData map(Map data, {String? tablePrefix}) { + TabLinkData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return TabData( + 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'])!, - url: Tab.$converterurl.fromSql(attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}url'])!), - title: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}title']), - screenshot: attachedDatabase.typeMapping - .read(DriftSqlType.blob, data['${effectivePrefix}screenshot']), ); } @override - Tab createAlias(String alias) { - return Tab(attachedDatabase, alias); + TabLink createAlias(String alias) { + return TabLink(attachedDatabase, alias); } - static TypeConverter $converterurl = const UriConverter(); @override bool get dontWriteConstraints => true; } -class TabData extends DataClass implements Insertable { +class TabLinkData extends DataClass implements Insertable { final String id; final String? topicId; final DateTime timestamp; - final Uri url; - final String? title; - final Uint8List? screenshot; - const TabData( - {required this.id, - this.topicId, - required this.timestamp, - required this.url, - this.title, - this.screenshot}); + const TabLinkData({required this.id, this.topicId, required this.timestamp}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -228,28 +195,16 @@ class TabData extends DataClass implements Insertable { map['topic_id'] = Variable(topicId); } map['timestamp'] = Variable(timestamp); - { - map['url'] = Variable(Tab.$converterurl.toSql(url)); - } - if (!nullToAbsent || title != null) { - map['title'] = Variable(title); - } - if (!nullToAbsent || screenshot != null) { - map['screenshot'] = Variable(screenshot); - } return map; } - factory TabData.fromJson(Map json, + factory TabLinkData.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return TabData( + return TabLinkData( id: serializer.fromJson(json['id']), topicId: serializer.fromJson(json['topic_id']), timestamp: serializer.fromJson(json['timestamp']), - url: serializer.fromJson(json['url']), - title: serializer.fromJson(json['title']), - screenshot: serializer.fromJson(json['screenshot']), ); } @override @@ -259,130 +214,88 @@ class TabData extends DataClass implements Insertable { 'id': serializer.toJson(id), 'topic_id': serializer.toJson(topicId), 'timestamp': serializer.toJson(timestamp), - 'url': serializer.toJson(url), - 'title': serializer.toJson(title), - 'screenshot': serializer.toJson(screenshot), }; } - TabData copyWith( + TabLinkData copyWith( {String? id, Value topicId = const Value.absent(), - DateTime? timestamp, - Uri? url, - Value title = const Value.absent(), - Value screenshot = const Value.absent()}) => - TabData( + DateTime? timestamp}) => + TabLinkData( id: id ?? this.id, topicId: topicId.present ? topicId.value : this.topicId, timestamp: timestamp ?? this.timestamp, - url: url ?? this.url, - title: title.present ? title.value : this.title, - screenshot: screenshot.present ? screenshot.value : this.screenshot, ); - TabData copyWithCompanion(TabCompanion data) { - return TabData( + 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, - url: data.url.present ? data.url.value : this.url, - title: data.title.present ? data.title.value : this.title, - screenshot: - data.screenshot.present ? data.screenshot.value : this.screenshot, ); } @override String toString() { - return (StringBuffer('TabData(') + return (StringBuffer('TabLinkData(') ..write('id: $id, ') ..write('topicId: $topicId, ') - ..write('timestamp: $timestamp, ') - ..write('url: $url, ') - ..write('title: $title, ') - ..write('screenshot: $screenshot') + ..write('timestamp: $timestamp') ..write(')')) .toString(); } @override - int get hashCode => Object.hash( - id, topicId, timestamp, url, title, $driftBlobEquality.hash(screenshot)); + int get hashCode => Object.hash(id, topicId, timestamp); @override bool operator ==(Object other) => identical(this, other) || - (other is TabData && + (other is TabLinkData && other.id == this.id && other.topicId == this.topicId && - other.timestamp == this.timestamp && - other.url == this.url && - other.title == this.title && - $driftBlobEquality.equals(other.screenshot, this.screenshot)); + other.timestamp == this.timestamp); } -class TabCompanion extends UpdateCompanion { +class TabLinkCompanion extends UpdateCompanion { final Value id; final Value topicId; final Value timestamp; - final Value url; - final Value title; - final Value screenshot; final Value rowid; - const TabCompanion({ + const TabLinkCompanion({ this.id = const Value.absent(), this.topicId = const Value.absent(), this.timestamp = const Value.absent(), - this.url = const Value.absent(), - this.title = const Value.absent(), - this.screenshot = const Value.absent(), this.rowid = const Value.absent(), }); - TabCompanion.insert({ + TabLinkCompanion.insert({ required String id, this.topicId = const Value.absent(), required DateTime timestamp, - required Uri url, - this.title = const Value.absent(), - this.screenshot = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), - timestamp = Value(timestamp), - url = Value(url); - static Insertable custom({ + timestamp = Value(timestamp); + static Insertable custom({ Expression? id, Expression? topicId, Expression? timestamp, - Expression? url, - Expression? title, - Expression? screenshot, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (topicId != null) 'topic_id': topicId, if (timestamp != null) 'timestamp': timestamp, - if (url != null) 'url': url, - if (title != null) 'title': title, - if (screenshot != null) 'screenshot': screenshot, if (rowid != null) 'rowid': rowid, }); } - TabCompanion copyWith( + TabLinkCompanion copyWith( {Value? id, Value? topicId, Value? timestamp, - Value? url, - Value? title, - Value? screenshot, Value? rowid}) { - return TabCompanion( + return TabLinkCompanion( id: id ?? this.id, topicId: topicId ?? this.topicId, timestamp: timestamp ?? this.timestamp, - url: url ?? this.url, - title: title ?? this.title, - screenshot: screenshot ?? this.screenshot, rowid: rowid ?? this.rowid, ); } @@ -399,15 +312,6 @@ class TabCompanion extends UpdateCompanion { if (timestamp.present) { map['timestamp'] = Variable(timestamp.value); } - if (url.present) { - map['url'] = Variable(Tab.$converterurl.toSql(url.value)); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (screenshot.present) { - map['screenshot'] = Variable(screenshot.value); - } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -416,13 +320,10 @@ class TabCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('TabCompanion(') + return (StringBuffer('TabLinkCompanion(') ..write('id: $id, ') ..write('topicId: $topicId, ') ..write('timestamp: $timestamp, ') - ..write('url: $url, ') - ..write('title: $title, ') - ..write('screenshot: $screenshot, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -433,16 +334,16 @@ abstract class _$TabDatabase extends GeneratedDatabase { _$TabDatabase(QueryExecutor e) : super(e); $TabDatabaseManager get managers => $TabDatabaseManager(this); late final Topic topic = Topic(this); - late final Tab tab = Tab(this); + late final TabLink tabLink = TabLink(this); late final TopicDao topicDao = TopicDao(this as TabDatabase); - late final TabDao tabDao = TabDao(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 GROUP BY topic_id) AS tab_agg ON topic.id = tab_agg.topic_id ORDER BY tab_agg.last_updated DESC NULLS FIRST', + '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, - tab, + tabLink, }).map((QueryRow row) => TopicDataWithCount( id: row.read('id'), name: row.readNullable('name'), @@ -455,7 +356,7 @@ abstract class _$TabDatabase extends GeneratedDatabase { Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [topic, tab]; + List get allSchemaEntities => [topic, tabLink]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( [ @@ -463,7 +364,7 @@ abstract class _$TabDatabase extends GeneratedDatabase { on: TableUpdateQuery.onTableName('topic', limitUpdateKind: UpdateKind.delete), result: [ - TableUpdate('tab', kind: UpdateKind.delete), + TableUpdate('tab_link', kind: UpdateKind.delete), ], ), ], @@ -487,16 +388,16 @@ final class $TopicReferences extends BaseReferences<_$TabDatabase, Topic, TopicData> { $TopicReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey> _tabRefsTable( + static MultiTypedResultKey> _tabLinkRefsTable( _$TabDatabase db) => - MultiTypedResultKey.fromTable(db.tab, - aliasName: $_aliasNameGenerator(db.topic.id, db.tab.topicId)); + MultiTypedResultKey.fromTable(db.tabLink, + aliasName: $_aliasNameGenerator(db.topic.id, db.tabLink.topicId)); - $TabProcessedTableManager get tabRefs { - final manager = - $TabTableManager($_db, $_db.tab).filter((f) => f.topicId.id($_item.id)); + $TabLinkProcessedTableManager get tabLinkRefs { + final manager = $TabLinkTableManager($_db, $_db.tabLink) + .filter((f) => f.topicId.id($_item.id)); - final cache = $_typedResult.readTableOrNull(_tabRefsTable($_db)); + final cache = $_typedResult.readTableOrNull(_tabLinkRefsTable($_db)); return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } @@ -521,15 +422,16 @@ class $TopicFilterComposer extends FilterComposer<_$TabDatabase, Topic> { column, joinBuilders: joinBuilders)); - ComposableFilter tabRefs(ComposableFilter Function($TabFilterComposer f) f) { - final $TabFilterComposer composer = $state.composerBuilder( + ComposableFilter tabLinkRefs( + ComposableFilter Function($TabLinkFilterComposer f) f) { + final $TabLinkFilterComposer composer = $state.composerBuilder( composer: this, getCurrentColumn: (t) => t.id, - referencedTable: $state.db.tab, + referencedTable: $state.db.tabLink, getReferencedColumn: (t) => t.topicId, - builder: (joinBuilder, parentComposers) => $TabFilterComposer( + builder: (joinBuilder, parentComposers) => $TabLinkFilterComposer( ComposerState( - $state.db, $state.db.tab, joinBuilder, parentComposers))); + $state.db, $state.db.tabLink, joinBuilder, parentComposers))); return f(composer); } } @@ -562,7 +464,7 @@ class $TopicTableManager extends RootTableManager< $TopicUpdateCompanionBuilder, (TopicData, $TopicReferences), TopicData, - PrefetchHooks Function({bool tabRefs})> { + PrefetchHooks Function({bool tabLinkRefs})> { $TopicTableManager(_$TabDatabase db, Topic table) : super(TableManagerState( db: db, @@ -596,19 +498,19 @@ class $TopicTableManager extends RootTableManager< withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), $TopicReferences(db, table, e))) .toList(), - prefetchHooksCallback: ({tabRefs = false}) { + prefetchHooksCallback: ({tabLinkRefs = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [if (tabRefs) db.tab], + explicitlyWatchedTables: [if (tabLinkRefs) db.tabLink], addJoins: null, getPrefetchedDataCallback: (items) async { return [ - if (tabRefs) + if (tabLinkRefs) await $_getPrefetchedData( currentTable: table, - referencedTable: $TopicReferences._tabRefsTable(db), + referencedTable: $TopicReferences._tabLinkRefsTable(db), managerFromTypedResult: (p0) => - $TopicReferences(db, table, p0).tabRefs, + $TopicReferences(db, table, p0).tabLinkRefs, referencedItemsForCurrentItem: (item, referencedItems) => referencedItems.where((e) => e.topicId == item.id), @@ -630,31 +532,26 @@ typedef $TopicProcessedTableManager = ProcessedTableManager< $TopicUpdateCompanionBuilder, (TopicData, $TopicReferences), TopicData, - PrefetchHooks Function({bool tabRefs})>; -typedef $TabCreateCompanionBuilder = TabCompanion Function({ + PrefetchHooks Function({bool tabLinkRefs})>; +typedef $TabLinkCreateCompanionBuilder = TabLinkCompanion Function({ required String id, Value topicId, required DateTime timestamp, - required Uri url, - Value title, - Value screenshot, Value rowid, }); -typedef $TabUpdateCompanionBuilder = TabCompanion Function({ +typedef $TabLinkUpdateCompanionBuilder = TabLinkCompanion Function({ Value id, Value topicId, Value timestamp, - Value url, - Value title, - Value screenshot, Value rowid, }); -final class $TabReferences extends BaseReferences<_$TabDatabase, Tab, TabData> { - $TabReferences(super.$_db, super.$_table, super.$_typedResult); +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.tab.topicId, db.topic.id)); + static Topic _topicIdTable(_$TabDatabase db) => db.topic + .createAlias($_aliasNameGenerator(db.tabLink.topicId, db.topic.id)); $TopicProcessedTableManager? get topicId { if ($_item.topicId == null) return null; @@ -667,8 +564,8 @@ final class $TabReferences extends BaseReferences<_$TabDatabase, Tab, TabData> { } } -class $TabFilterComposer extends FilterComposer<_$TabDatabase, Tab> { - $TabFilterComposer(super.$state); +class $TabLinkFilterComposer extends FilterComposer<_$TabDatabase, TabLink> { + $TabLinkFilterComposer(super.$state); ColumnFilters get id => $state.composableBuilder( column: $state.table.id, builder: (column, joinBuilders) => @@ -679,23 +576,6 @@ class $TabFilterComposer extends FilterComposer<_$TabDatabase, Tab> { builder: (column, joinBuilders) => ColumnFilters(column, joinBuilders: joinBuilders)); - ColumnWithTypeConverterFilters get url => - $state.composableBuilder( - column: $state.table.url, - builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( - column, - joinBuilders: joinBuilders)); - - ColumnFilters get title => $state.composableBuilder( - column: $state.table.title, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - - ColumnFilters get screenshot => $state.composableBuilder( - column: $state.table.screenshot, - builder: (column, joinBuilders) => - ColumnFilters(column, joinBuilders: joinBuilders)); - $TopicFilterComposer get topicId { final $TopicFilterComposer composer = $state.composerBuilder( composer: this, @@ -709,8 +589,9 @@ class $TabFilterComposer extends FilterComposer<_$TabDatabase, Tab> { } } -class $TabOrderingComposer extends OrderingComposer<_$TabDatabase, Tab> { - $TabOrderingComposer(super.$state); +class $TabLinkOrderingComposer + extends OrderingComposer<_$TabDatabase, TabLink> { + $TabLinkOrderingComposer(super.$state); ColumnOrderings get id => $state.composableBuilder( column: $state.table.id, builder: (column, joinBuilders) => @@ -721,21 +602,6 @@ class $TabOrderingComposer extends OrderingComposer<_$TabDatabase, Tab> { builder: (column, joinBuilders) => ColumnOrderings(column, joinBuilders: joinBuilders)); - ColumnOrderings get url => $state.composableBuilder( - column: $state.table.url, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get title => $state.composableBuilder( - column: $state.table.title, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - - ColumnOrderings get screenshot => $state.composableBuilder( - column: $state.table.screenshot, - builder: (column, joinBuilders) => - ColumnOrderings(column, joinBuilders: joinBuilders)); - $TopicOrderingComposer get topicId { final $TopicOrderingComposer composer = $state.composerBuilder( composer: this, @@ -749,61 +615,50 @@ class $TabOrderingComposer extends OrderingComposer<_$TabDatabase, Tab> { } } -class $TabTableManager extends RootTableManager< +class $TabLinkTableManager extends RootTableManager< _$TabDatabase, - Tab, - TabData, - $TabFilterComposer, - $TabOrderingComposer, - $TabCreateCompanionBuilder, - $TabUpdateCompanionBuilder, - (TabData, $TabReferences), - TabData, + TabLink, + TabLinkData, + $TabLinkFilterComposer, + $TabLinkOrderingComposer, + $TabLinkCreateCompanionBuilder, + $TabLinkUpdateCompanionBuilder, + (TabLinkData, $TabLinkReferences), + TabLinkData, PrefetchHooks Function({bool topicId})> { - $TabTableManager(_$TabDatabase db, Tab table) + $TabLinkTableManager(_$TabDatabase db, TabLink table) : super(TableManagerState( db: db, table: table, - filteringComposer: $TabFilterComposer(ComposerState(db, table)), - orderingComposer: $TabOrderingComposer(ComposerState(db, table)), + filteringComposer: $TabLinkFilterComposer(ComposerState(db, table)), + orderingComposer: $TabLinkOrderingComposer(ComposerState(db, table)), updateCompanionCallback: ({ Value id = const Value.absent(), Value topicId = const Value.absent(), Value timestamp = const Value.absent(), - Value url = const Value.absent(), - Value title = const Value.absent(), - Value screenshot = const Value.absent(), Value rowid = const Value.absent(), }) => - TabCompanion( + TabLinkCompanion( id: id, topicId: topicId, timestamp: timestamp, - url: url, - title: title, - screenshot: screenshot, rowid: rowid, ), createCompanionCallback: ({ required String id, Value topicId = const Value.absent(), required DateTime timestamp, - required Uri url, - Value title = const Value.absent(), - Value screenshot = const Value.absent(), Value rowid = const Value.absent(), }) => - TabCompanion.insert( + TabLinkCompanion.insert( id: id, topicId: topicId, timestamp: timestamp, - url: url, - title: title, - screenshot: screenshot, rowid: rowid, ), withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), $TabReferences(db, table, e))) + .map( + (e) => (e.readTable(table), $TabLinkReferences(db, table, e))) .toList(), prefetchHooksCallback: ({topicId = false}) { return PrefetchHooks( @@ -825,8 +680,8 @@ class $TabTableManager extends RootTableManager< state = state.withJoin( currentTable: table, currentColumn: table.topicId, - referencedTable: $TabReferences._topicIdTable(db), - referencedColumn: $TabReferences._topicIdTable(db).id, + referencedTable: $TabLinkReferences._topicIdTable(db), + referencedColumn: $TabLinkReferences._topicIdTable(db).id, ) as T; } @@ -840,21 +695,21 @@ class $TabTableManager extends RootTableManager< )); } -typedef $TabProcessedTableManager = ProcessedTableManager< +typedef $TabLinkProcessedTableManager = ProcessedTableManager< _$TabDatabase, - Tab, - TabData, - $TabFilterComposer, - $TabOrderingComposer, - $TabCreateCompanionBuilder, - $TabUpdateCompanionBuilder, - (TabData, $TabReferences), - TabData, + TabLink, + TabLinkData, + $TabLinkFilterComposer, + $TabLinkOrderingComposer, + $TabLinkCreateCompanionBuilder, + $TabLinkUpdateCompanionBuilder, + (TabLinkData, $TabLinkReferences), + TabLinkData, PrefetchHooks Function({bool topicId})>; class $TabDatabaseManager { final _$TabDatabase _db; $TabDatabaseManager(this._db); $TopicTableManager get topic => $TopicTableManager(_db, _db.topic); - $TabTableManager get tab => $TabTableManager(_db, _db.tab); + $TabLinkTableManager get tabLink => $TabLinkTableManager(_db, _db.tabLink); } diff --git a/app/lib/features/topics/data/database/drift/converters/color.dart b/app/lib/features/geckoview/features/topics/data/database/drift/converters/color.dart similarity index 100% rename from app/lib/features/topics/data/database/drift/converters/color.dart rename to app/lib/features/geckoview/features/topics/data/database/drift/converters/color.dart diff --git a/app/lib/features/topics/data/database/drift/converters/uri.dart b/app/lib/features/geckoview/features/topics/data/database/drift/converters/uri.dart similarity index 100% rename from app/lib/features/topics/data/database/drift/converters/uri.dart rename to app/lib/features/geckoview/features/topics/data/database/drift/converters/uri.dart diff --git a/app/lib/features/topics/data/models/topic_data.dart b/app/lib/features/geckoview/features/topics/data/models/topic_data.dart similarity index 100% rename from app/lib/features/topics/data/models/topic_data.dart rename to app/lib/features/geckoview/features/topics/data/models/topic_data.dart diff --git a/app/lib/features/topics/data/providers.dart b/app/lib/features/geckoview/features/topics/data/providers.dart similarity index 78% rename from app/lib/features/topics/data/providers.dart rename to app/lib/features/geckoview/features/topics/data/providers.dart index ab56083a..ed1a78c3 100644 --- a/app/lib/features/topics/data/providers.dart +++ b/app/lib/features/geckoview/features/topics/data/providers.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; -import 'package:lensai/features/topics/data/database/database.dart'; +import 'package:lensai/features/geckoview/features/topics/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'; @@ -35,15 +35,3 @@ TabDatabase tabDatabase(TabDatabaseRef ref) { }), ); } - -@Riverpod() -Stream isTabExisting(IsTabExistingRef ref, String tabId) { - final db = ref.watch(tabDatabaseProvider); - return db.tabDao.watchTabExisiting(tabId); -} - -@Riverpod() -Stream tabData(TabDataRef ref, String tabId) { - final db = ref.watch(tabDatabaseProvider); - return db.tabDao.watchTab(tabId); -} diff --git a/app/lib/features/geckoview/features/topics/data/providers.g.dart b/app/lib/features/geckoview/features/topics/data/providers.g.dart new file mode 100644 index 00000000..13b43baa --- /dev/null +++ b/app/lib/features/geckoview/features/topics/data/providers.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$tabDatabaseHash() => r'b886b089ae5fdb9bdcc0982efe5fcbe9bc66486f'; + +/// See also [tabDatabase]. +@ProviderFor(tabDatabase) +final tabDatabaseProvider = Provider.internal( + tabDatabase, + name: r'tabDatabaseProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$tabDatabaseHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef TabDatabaseRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/topics/domain/providers.dart b/app/lib/features/geckoview/features/topics/domain/providers.dart similarity index 63% rename from app/lib/features/topics/domain/providers.dart rename to app/lib/features/geckoview/features/topics/domain/providers.dart index d58dea5a..11a6a08a 100644 --- a/app/lib/features/topics/domain/providers.dart +++ b/app/lib/features/geckoview/features/topics/domain/providers.dart @@ -1,10 +1,10 @@ import 'dart:ui'; import 'package:collection/collection.dart'; -import 'package:lensai/features/topics/data/models/topic_data.dart'; -import 'package:lensai/features/topics/data/providers.dart'; -import 'package:lensai/features/topics/domain/repositories/topic.dart'; -import 'package:lensai/features/topics/utils/color_palette.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'; @@ -59,3 +59,15 @@ Future unusedRandomTopicColor(UnusedRandomTopicColorRef ref) async { 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(); +} diff --git a/app/lib/features/geckoview/features/topics/domain/providers.g.dart b/app/lib/features/geckoview/features/topics/domain/providers.g.dart new file mode 100644 index 00000000..351ff739 --- /dev/null +++ b/app/lib/features/geckoview/features/topics/domain/providers.g.dart @@ -0,0 +1,224 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$selectedTopicDataHash() => r'fa450784052498014f592c356bde4ff5ec675281'; + +/// See also [selectedTopicData]. +@ProviderFor(selectedTopicData) +final selectedTopicDataProvider = + AutoDisposeStreamProvider.internal( + selectedTopicData, + name: r'selectedTopicDataProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedTopicDataHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef SelectedTopicDataRef = AutoDisposeStreamProviderRef; +String _$unusedRandomTopicColorHash() => + r'099730b0d987cc37bcae4ef5c999cbebfa67d7da'; + +/// 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 _$selectedTopicHash() => r'f4e1c0620971a0b7501ee9aff0d5b41da3130ba0'; + +/// See also [SelectedTopic]. +@ProviderFor(SelectedTopic) +final selectedTopicProvider = NotifierProvider.internal( + SelectedTopic.new, + name: r'selectedTopicProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$selectedTopicHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SelectedTopic = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.dart b/app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.dart new file mode 100644 index 00000000..2316570c --- /dev/null +++ b/app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.dart @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000..68c586c4 --- /dev/null +++ b/app/lib/features/geckoview/features/topics/domain/repositories/selected_topic.g.dart @@ -0,0 +1,27 @@ +// 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 new file mode 100644 index 00000000..57911cbe --- /dev/null +++ b/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.dart @@ -0,0 +1,53 @@ +import 'package:drift/drift.dart'; +import 'package:lensai/features/geckoview/domain/repositories/selected_tab.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/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); + + //Sync existing tabs + ref.listen( + tabRepositoryProvider.select((tabs) => tabs.keys.toList()), + (previous, next) async { + //No empty list to avoid data loss + if (next.isNotEmpty) { + await _db.tabLinkDao.syncTabLinks(retainTabIds: next); + } + }, + ); + + ref.listen( + selectedTabControllerProvider, + (previous, next) async { + if (next != null) { + await _db.tabLinkDao.touchTabLink(next, timestamp: DateTime.now()); + } + }, + ); + } + + Future assignTab(String tabId, String? topicId) { + return _db.tabLinkDao.upsertTabLink( + tabId, + timestamp: DateTime.now(), + topicId: Value(topicId), + ); + } + + Future closeAllTabs(String? topicId) async { + final tabIds = await _db.tabLinkDao.topicTabIds(topicId).get(); + if (tabIds.isNotEmpty) { + await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds); + } + } +} diff --git a/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart b/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart new file mode 100644 index 00000000..55a36a6b --- /dev/null +++ b/app/lib/features/geckoview/features/topics/domain/repositories/tab_link.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tab_link.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$tabLinkRepositoryHash() => r'e85d88fadb8133c3ef1e38f0176694a12a749397'; + +/// See also [TabLinkRepository]. +@ProviderFor(TabLinkRepository) +final tabLinkRepositoryProvider = + NotifierProvider.internal( + TabLinkRepository.new, + name: r'tabLinkRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$tabLinkRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$TabLinkRepository = 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/topics/domain/repositories/topic.dart b/app/lib/features/geckoview/features/topics/domain/repositories/topic.dart similarity index 74% rename from app/lib/features/topics/domain/repositories/topic.dart rename to app/lib/features/geckoview/features/topics/domain/repositories/topic.dart index 5321cb12..7e3cc3f1 100644 --- a/app/lib/features/topics/domain/repositories/topic.dart +++ b/app/lib/features/geckoview/features/topics/domain/repositories/topic.dart @@ -1,8 +1,7 @@ import 'dart:ui'; -import 'package:lensai/features/topics/data/database/database.dart'; -import 'package:lensai/features/topics/data/models/topic_data.dart'; -import 'package:lensai/features/topics/data/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:riverpod_annotation/riverpod_annotation.dart'; part 'topic.g.dart'; @@ -12,9 +11,8 @@ class TopicRepository extends _$TopicRepository { late TabDatabase _db; @override - Stream> build() { + void build() { _db = ref.watch(tabDatabaseProvider); - return _db.topicsWithCount().watch(); } Future addTopic({required String? name, required Color color}) { diff --git a/app/lib/features/topics/domain/repositories/topic.g.dart b/app/lib/features/geckoview/features/topics/domain/repositories/topic.g.dart similarity index 76% rename from app/lib/features/topics/domain/repositories/topic.g.dart rename to app/lib/features/geckoview/features/topics/domain/repositories/topic.g.dart index d1cff0fa..759fe11f 100644 --- a/app/lib/features/topics/domain/repositories/topic.g.dart +++ b/app/lib/features/geckoview/features/topics/domain/repositories/topic.g.dart @@ -6,12 +6,12 @@ part of 'topic.dart'; // RiverpodGenerator // ************************************************************************** -String _$topicRepositoryHash() => r'3b75c5fb1a081ba6d90e2ccb781469d0aa87c415'; +String _$topicRepositoryHash() => r'6c456c17af4ea7d7ef1747c92d0442459d061080'; /// See also [TopicRepository]. @ProviderFor(TopicRepository) final topicRepositoryProvider = - StreamNotifierProvider>.internal( + NotifierProvider.internal( TopicRepository.new, name: r'topicRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -21,6 +21,6 @@ final topicRepositoryProvider = allTransitiveDependencies: null, ); -typedef _$TopicRepository = StreamNotifier>; +typedef _$TopicRepository = 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/topics/presentation/screens/topic_list.dart b/app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart similarity index 94% rename from app/lib/features/topics/presentation/screens/topic_list.dart rename to app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart index 3d6ab4d3..70dcf476 100644 --- a/app/lib/features/topics/presentation/screens/topic_list.dart +++ b/app/lib/features/geckoview/features/topics/presentation/screens/topic_list.dart @@ -2,10 +2,10 @@ 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/topics/data/models/topic_data.dart'; -import 'package:lensai/features/topics/domain/providers.dart'; -import 'package:lensai/features/topics/domain/repositories/topic.dart'; -import 'package:lensai/features/topics/presentation/widgets/topic_dialog.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/repositories/topic.dart'; +import 'package:lensai/features/geckoview/features/topics/presentation/widgets/topic_dialog.dart'; import 'package:skeletonizer/skeletonizer.dart'; class _TopicTile extends HookWidget { diff --git a/app/lib/features/topics/presentation/widgets/material_color_picker.dart b/app/lib/features/geckoview/features/topics/presentation/widgets/material_color_picker.dart similarity index 99% rename from app/lib/features/topics/presentation/widgets/material_color_picker.dart rename to app/lib/features/geckoview/features/topics/presentation/widgets/material_color_picker.dart index 58848ee3..326f1aba 100644 --- a/app/lib/features/topics/presentation/widgets/material_color_picker.dart +++ b/app/lib/features/geckoview/features/topics/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/topics/utils/color_palette.dart'; +import 'package:lensai/features/geckoview/features/topics/utils/color_palette.dart'; class MaterialPicker extends StatefulWidget { const MaterialPicker({ diff --git a/app/lib/features/topics/presentation/widgets/topic_chips.dart b/app/lib/features/geckoview/features/topics/presentation/widgets/topic_chips.dart similarity index 93% rename from app/lib/features/topics/presentation/widgets/topic_chips.dart rename to app/lib/features/geckoview/features/topics/presentation/widgets/topic_chips.dart index 14939f92..eafb1ed3 100644 --- a/app/lib/features/topics/presentation/widgets/topic_chips.dart +++ b/app/lib/features/geckoview/features/topics/presentation/widgets/topic_chips.dart @@ -2,8 +2,8 @@ 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/topics/domain/providers.dart'; -import 'package:lensai/features/topics/domain/repositories/topic.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; class TopicChips extends HookConsumerWidget { diff --git a/app/lib/features/topics/presentation/widgets/topic_dialog.dart b/app/lib/features/geckoview/features/topics/presentation/widgets/topic_dialog.dart similarity index 96% rename from app/lib/features/topics/presentation/widgets/topic_dialog.dart rename to app/lib/features/geckoview/features/topics/presentation/widgets/topic_dialog.dart index 6ecc4c7b..9503a9b0 100644 --- a/app/lib/features/topics/presentation/widgets/topic_dialog.dart +++ b/app/lib/features/geckoview/features/topics/presentation/widgets/topic_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:lensai/features/topics/presentation/widgets/material_color_picker.dart'; +import 'package:lensai/features/geckoview/features/topics/presentation/widgets/material_color_picker.dart'; typedef TopicResult = ({String? name, Color color}); diff --git a/app/lib/features/topics/utils/color_palette.dart b/app/lib/features/geckoview/features/topics/utils/color_palette.dart similarity index 100% rename from app/lib/features/topics/utils/color_palette.dart rename to app/lib/features/geckoview/features/topics/utils/color_palette.dart diff --git a/app/lib/features/geckoview/utils/image_helper.dart b/app/lib/features/geckoview/utils/image_helper.dart new file mode 100644 index 00000000..b8d69c59 --- /dev/null +++ b/app/lib/features/geckoview/utils/image_helper.dart @@ -0,0 +1,29 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +Future tryDecodeImage( + Uint8List bytes, { + int? targetWidth, + int? targetHeight, + bool allowUpscaling = true, +}) async { + try { + final codec = await instantiateImageCodec( + bytes, + targetWidth: targetWidth, + targetHeight: targetHeight, + allowUpscaling: allowUpscaling, + ); + + final frameInfo = await codec.getNextFrame(); + final image = frameInfo.image; + + if (image.width > 0) { + return image; + } + } catch (e) { + //swallow + } + + return null; +} diff --git a/app/lib/features/search_browser/domain/entities/modes.dart b/app/lib/features/kagi/data/entities/modes.dart similarity index 100% rename from app/lib/features/search_browser/domain/entities/modes.dart rename to app/lib/features/kagi/data/entities/modes.dart diff --git a/app/lib/features/kagi/data/services/session.dart b/app/lib/features/kagi/data/services/session.dart new file mode 100644 index 00000000..bddba1d2 --- /dev/null +++ b/app/lib/features/kagi/data/services/session.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'session.g.dart'; + +@Riverpod() +class KagiSessionService extends _$KagiSessionService { + final _cookieManager = GeckoCookieService(); + + Future setKagiSession(String session) async { + await _cookieManager.setCookie( + url: Uri.https('kagi.com'), + name: 'kagi_session', + value: session, + domain: Uri.https('kagi.com'), + httpOnly: true, + secure: true, + sameSite: CookieSameSiteStatus.lax, + ); + } + + @override + void build() {} +} diff --git a/app/lib/features/search_browser/domain/services/session.g.dart b/app/lib/features/kagi/data/services/session.g.dart similarity index 56% rename from app/lib/features/search_browser/domain/services/session.g.dart rename to app/lib/features/kagi/data/services/session.g.dart index 3c81fd49..f2ee08a3 100644 --- a/app/lib/features/search_browser/domain/services/session.g.dart +++ b/app/lib/features/kagi/data/services/session.g.dart @@ -6,21 +6,22 @@ part of 'session.dart'; // RiverpodGenerator // ************************************************************************** -String _$sessionServiceHash() => r'fd2cd03bbf2dd254bb19e541c71a6fb249f5a7bb'; +String _$kagiSessionServiceHash() => + r'8bed5d74d319ff2f874f42bc8596d02f05347c1f'; -/// See also [SessionService]. -@ProviderFor(SessionService) -final sessionServiceProvider = - AutoDisposeNotifierProvider.internal( - SessionService.new, - name: r'sessionServiceProvider', +/// See also [KagiSessionService]. +@ProviderFor(KagiSessionService) +final kagiSessionServiceProvider = + AutoDisposeNotifierProvider.internal( + KagiSessionService.new, + name: r'kagiSessionServiceProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$sessionServiceHash, + : _$kagiSessionServiceHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$SessionService = AutoDisposeNotifier; +typedef _$KagiSessionService = AutoDisposeNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/search_browser/domain/entities/sheet.dart b/app/lib/features/search_browser/domain/entities/sheet.dart index 501b323f..67241394 100644 --- a/app/lib/features/search_browser/domain/entities/sheet.dart +++ b/app/lib/features/search_browser/domain/entities/sheet.dart @@ -1,16 +1,16 @@ import 'package:fast_equatable/fast_equatable.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; // ignore: missing_override_of_must_be_overridden sealed class Sheet with FastEquatable {} -class CreateTab extends Sheet { +class CreateTabSheet extends Sheet { final String? content; final KagiTool? preferredTool; bool get hasParameters => content != null || preferredTool != null; - CreateTab({this.content, this.preferredTool}); + CreateTabSheet({this.content, this.preferredTool}); @override bool get cacheHash => true; @@ -19,7 +19,7 @@ class CreateTab extends Sheet { List get hashParameters => [content, preferredTool]; } -class ViewTabs extends Sheet { +class ViewTabsSheet extends Sheet { @override bool get cacheHash => true; diff --git a/app/lib/features/search_browser/domain/providers.dart b/app/lib/features/search_browser/domain/providers.dart index 818a9044..cc6c12fd 100644 --- a/app/lib/features/search_browser/domain/providers.dart +++ b/app/lib/features/search_browser/domain/providers.dart @@ -2,76 +2,16 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; -import 'package:lensai/features/search_browser/domain/services/create_tab.dart'; -import 'package:lensai/features/topics/data/database/database.dart'; -import 'package:lensai/features/topics/domain/repositories/tab.dart'; -import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; -import 'package:lensai/features/web_view/presentation/widgets/web_view.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:rxdart/rxdart.dart'; part 'providers.g.dart'; -@Riverpod() -class OverlayDialog extends _$OverlayDialog { - @override - Widget? build() { - return null; - } - - void show(Widget dialog) { - state = dialog; - } - - void dismiss() { - state = null; - } -} - -@Riverpod() -class BottomSheet extends _$BottomSheet { - @override - Sheet? build() { - //Don't use select here because it will only fire on new != previous - return ref.watch(createTabStreamProvider).valueOrNull; - } - - void show(Sheet sheet) { - state = sheet; - } - - void dismiss() { - state = null; - } -} - -@Riverpod(keepAlive: true) -class BottomSheetExtend extends _$BottomSheetExtend { - late StreamController _extentStreamController; - - void add(double extent) { - _extentStreamController.add(extent); - } - - @override - Stream build() { - _extentStreamController = StreamController(); - ref.onDispose(() async { - await _extentStreamController.close(); - }); - - return _extentStreamController.stream - .sampleTime(const Duration(milliseconds: 50)); - } -} - @Riverpod(keepAlive: true) class SelectedBangTrigger extends _$SelectedBangTrigger { void setTrigger(String trigger) { @@ -143,65 +83,3 @@ class ShowFindInPage extends _$ShowFindInPage { return false; } } - -@Riverpod() -class WebViewWidgetCache extends _$WebViewWidgetCache { - final _requestedWidgets = {}; - - List _availableTabs = []; - - @override - List build() { - ref.listen( - tabRepositoryProvider, - (previous, next) { - if (next.hasValue && next.valueOrNull != null) { - _availableTabs = next.valueOrNull!; - - _removeClosedWidgets(); - _requestedWidgets.removeWhere(_ensureWidgetCached); - } - }, - ); - - ref.listen( - webViewTabControllerProvider, - (_, tabId) { - if (tabId != null && !_ensureWidgetCached(tabId)) { - _requestedWidgets.add(tabId); - } - }, - ); - - return stateOrNull ?? []; - } - - void _removeClosedWidgets() { - state = [...state]..removeWhere( - (element) => !_availableTabs.any((tab) => tab.id == element.tabId), - ); - } - - bool _ensureWidgetCached(String tabId) { - final index = state.indexWhere((tab) => tab.tabId == tabId); - if (index < 0) { - final tab = _availableTabs.firstWhereOrNull((tab) => tab.id == tabId); - if (tab != null) { - state = [ - ...state, - WebView( - key: GlobalKey(), - tabId: tabId, - initialUrlRequest: URLRequest(url: WebUri.uri(tab.url)), - ), - ]; - - return true; - } - - return false; - } - - return true; - } -} diff --git a/app/lib/features/search_browser/domain/providers.g.dart b/app/lib/features/search_browser/domain/providers.g.dart index 877ca964..2752869a 100644 --- a/app/lib/features/search_browser/domain/providers.g.dart +++ b/app/lib/features/search_browser/domain/providers.g.dart @@ -156,53 +156,6 @@ class _SelectedBangDataProviderElement String? get domain => (origin as SelectedBangDataProvider).domain; } -String _$overlayDialogHash() => r'7a0376fff0ba12c70a257a47391e7c049dabd36d'; - -/// See also [OverlayDialog]. -@ProviderFor(OverlayDialog) -final overlayDialogProvider = - AutoDisposeNotifierProvider.internal( - OverlayDialog.new, - name: r'overlayDialogProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$overlayDialogHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$OverlayDialog = AutoDisposeNotifier; -String _$bottomSheetHash() => r'4040ec50a488b4b12123bedceab6d8620e6abe3d'; - -/// See also [BottomSheet]. -@ProviderFor(BottomSheet) -final bottomSheetProvider = - AutoDisposeNotifierProvider.internal( - BottomSheet.new, - name: r'bottomSheetProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$bottomSheetHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$BottomSheet = AutoDisposeNotifier; -String _$bottomSheetExtendHash() => r'81ae254499d1d7f57309274b0f18747b94d6dda3'; - -/// See also [BottomSheetExtend]. -@ProviderFor(BottomSheetExtend) -final bottomSheetExtendProvider = - StreamNotifierProvider.internal( - BottomSheetExtend.new, - name: r'bottomSheetExtendProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$bottomSheetExtendHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$BottomSheetExtend = StreamNotifier; String _$selectedBangTriggerHash() => r'b0e12bc95e93d50d04f1c658230cea9f15ff0385'; @@ -411,22 +364,5 @@ final showFindInPageProvider = NotifierProvider.internal( ); typedef _$ShowFindInPage = Notifier; -String _$webViewWidgetCacheHash() => - r'7d6f769e8c314d9dc579d700d36d6d4c944e600f'; - -/// See also [WebViewWidgetCache]. -@ProviderFor(WebViewWidgetCache) -final webViewWidgetCacheProvider = - AutoDisposeNotifierProvider>.internal( - WebViewWidgetCache.new, - name: r'webViewWidgetCacheProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$webViewWidgetCacheHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$WebViewWidgetCache = AutoDisposeNotifier>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/search_browser/domain/services/create_tab.dart b/app/lib/features/search_browser/domain/services/create_tab.dart index 704c1702..67a33e45 100644 --- a/app/lib/features/search_browser/domain/services/create_tab.dart +++ b/app/lib/features/search_browser/domain/services/create_tab.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:lensai/domain/entities/received_parameter.dart'; +import 'package:lensai/data/models/received_parameter.dart'; import 'package:lensai/features/app_widget/domain/services/home_widget.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; import 'package:lensai/features/share_intent/domain/services/sharing_intent.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -11,10 +11,10 @@ import 'package:rxdart/rxdart.dart'; part 'create_tab.g.dart'; -final _parameterToCreateTabTransformer = - StreamTransformer.fromHandlers( +final _parameterToCreateTabSheetTransformer = + StreamTransformer.fromHandlers( handleData: (parameter, sink) { - final createTab = CreateTab( + final createTab = CreateTabSheet( preferredTool: KagiTool.values .firstWhereOrNull((tool) => tool.name == parameter.tool), content: parameter.content, @@ -28,10 +28,10 @@ final _parameterToCreateTabTransformer = @Riverpod() class CreateTabStream extends _$CreateTabStream { - late StreamController _streamController; + late StreamController _streamController; @override - Stream build() { + Stream build() { _streamController = StreamController(); ref.onDispose(() async { await _streamController.close(); @@ -41,13 +41,13 @@ class CreateTabStream extends _$CreateTabStream { final appWidgetLaunchStream = ref.watch(appWidgetLaunchStreamProvider); return MergeStream([ - sharingItentStream.transform(_parameterToCreateTabTransformer), - appWidgetLaunchStream.transform(_parameterToCreateTabTransformer), + sharingItentStream.transform(_parameterToCreateTabSheetTransformer), + appWidgetLaunchStream.transform(_parameterToCreateTabSheetTransformer), _streamController.stream, ]); } - void createTab(CreateTab parameter) { + void createTab(CreateTabSheet parameter) { _streamController.add(parameter); } } diff --git a/app/lib/features/search_browser/domain/services/create_tab.g.dart b/app/lib/features/search_browser/domain/services/create_tab.g.dart index 5423f2b0..090ab8af 100644 --- a/app/lib/features/search_browser/domain/services/create_tab.g.dart +++ b/app/lib/features/search_browser/domain/services/create_tab.g.dart @@ -6,12 +6,12 @@ part of 'create_tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$createTabStreamHash() => r'3e85020edbebf3ec9927dba6d05af9f06a360a14'; +String _$createTabStreamHash() => r'05ad914e41e0d346e082e4638afe5fe5a04052f1'; /// See also [CreateTabStream]. @ProviderFor(CreateTabStream) final createTabStreamProvider = - AutoDisposeStreamNotifierProvider.internal( + AutoDisposeStreamNotifierProvider.internal( CreateTabStream.new, name: r'createTabStreamProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -21,6 +21,6 @@ final createTabStreamProvider = allTransitiveDependencies: null, ); -typedef _$CreateTabStream = AutoDisposeStreamNotifier; +typedef _$CreateTabStream = AutoDisposeStreamNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/search_browser/domain/services/session.dart b/app/lib/features/search_browser/domain/services/session.dart deleted file mode 100644 index 60710093..00000000 --- a/app/lib/features/search_browser/domain/services/session.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'session.g.dart'; - -@Riverpod() -class SessionService extends _$SessionService { - final _cookieManager = CookieManager.instance(); - - Future setKagiSession(String session) async { - await _cookieManager.setCookie( - url: WebUri.uri(Uri.https('kagi.com')), - name: 'kagi_session', - value: session, - domain: 'kagi.com', - isHttpOnly: true, - isSecure: true, - sameSite: HTTPCookieSameSitePolicy.LAX, - ); - } - - Future clearAllData() async { - await _cookieManager.deleteAllCookies(); - - final webViewLoaded = Completer(); - final headlessWebView = HeadlessInAppWebView( - initialSettings: InAppWebViewSettings( - //Clears all data on (just) Android - incognito: true, - //Clear other stuff - clearCache: true, - clearSessionCache: true, - ), - onWebViewCreated: (controller) { - //wait until settings are applied for sure - webViewLoaded.complete(); - }, - ); - - unawaited(headlessWebView.run()); - - await webViewLoaded.future.whenComplete(() => headlessWebView..dispose()); - } - - @override - void build() {} -} diff --git a/app/lib/features/search_browser/presentation/controllers/bottom_sheet.dart b/app/lib/features/search_browser/presentation/controllers/bottom_sheet.dart new file mode 100644 index 00000000..0e5ca53b --- /dev/null +++ b/app/lib/features/search_browser/presentation/controllers/bottom_sheet.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; +import 'package:lensai/features/search_browser/domain/services/create_tab.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:rxdart/rxdart.dart'; + +part 'bottom_sheet.g.dart'; + +@Riverpod() +class BottomSheetController extends _$BottomSheetController { + @override + Sheet? build() { + //Don't use select here because it will only fire on new != previous + return ref.watch(createTabStreamProvider).valueOrNull; + } + + void show(Sheet sheet) { + state = sheet; + } + + void dismiss() { + state = null; + } +} + +@Riverpod(keepAlive: true) +class BottomSheetExtend extends _$BottomSheetExtend { + late StreamController _extentStreamController; + + void add(double extent) { + _extentStreamController.add(extent); + } + + @override + Stream build() { + _extentStreamController = StreamController(); + ref.onDispose(() async { + await _extentStreamController.close(); + }); + + return _extentStreamController.stream + .sampleTime(const Duration(milliseconds: 50)); + } +} diff --git a/app/lib/features/search_browser/presentation/controllers/bottom_sheet.g.dart b/app/lib/features/search_browser/presentation/controllers/bottom_sheet.g.dart new file mode 100644 index 00000000..47563445 --- /dev/null +++ b/app/lib/features/search_browser/presentation/controllers/bottom_sheet.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'bottom_sheet.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$bottomSheetControllerHash() => + r'ae91f911fc35fdc0dfb14149ac2295e58ad13a86'; + +/// See also [BottomSheetController]. +@ProviderFor(BottomSheetController) +final bottomSheetControllerProvider = + AutoDisposeNotifierProvider.internal( + BottomSheetController.new, + name: r'bottomSheetControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$bottomSheetControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$BottomSheetController = AutoDisposeNotifier; +String _$bottomSheetExtendHash() => r'81ae254499d1d7f57309274b0f18747b94d6dda3'; + +/// See also [BottomSheetExtend]. +@ProviderFor(BottomSheetExtend) +final bottomSheetExtendProvider = + StreamNotifierProvider.internal( + BottomSheetExtend.new, + name: r'bottomSheetExtendProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$bottomSheetExtendHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$BottomSheetExtend = StreamNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/search_browser/presentation/controllers/overlay_dialog.dart b/app/lib/features/search_browser/presentation/controllers/overlay_dialog.dart new file mode 100644 index 00000000..233f0bc2 --- /dev/null +++ b/app/lib/features/search_browser/presentation/controllers/overlay_dialog.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'overlay_dialog.g.dart'; + +@Riverpod() +class OverlayDialogController extends _$OverlayDialogController { + @override + Widget? build() { + return null; + } + + void show(Widget dialog) { + state = dialog; + } + + void dismiss() { + state = null; + } +} diff --git a/app/lib/features/search_browser/presentation/controllers/overlay_dialog.g.dart b/app/lib/features/search_browser/presentation/controllers/overlay_dialog.g.dart new file mode 100644 index 00000000..3935cbdf --- /dev/null +++ b/app/lib/features/search_browser/presentation/controllers/overlay_dialog.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'overlay_dialog.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$overlayDialogControllerHash() => + r'53b44d9567bcb5bd9faecbfb8bb1af5aafb369c9'; + +/// See also [OverlayDialogController]. +@ProviderFor(OverlayDialogController) +final overlayDialogControllerProvider = + AutoDisposeNotifierProvider.internal( + OverlayDialogController.new, + name: r'overlayDialogControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$overlayDialogControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$OverlayDialogController = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/search_browser/presentation/dialogs/tab_action.dart b/app/lib/features/search_browser/presentation/dialogs/tab_action.dart index eee12fa1..6f835bd6 100644 --- a/app/lib/features/search_browser/presentation/dialogs/tab_action.dart +++ b/app/lib/features/search_browser/presentation/dialogs/tab_action.dart @@ -2,25 +2,25 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/features/topics/domain/repositories/topic.dart'; -import 'package:lensai/features/web_view/domain/entities/web_view_page.dart'; -import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; -import 'package:lensai/features/web_view/presentation/widgets/favicon.dart'; +import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; +import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart'; class TabActionDialog extends HookConsumerWidget { - final WebViewPage initialTab; + final String tabId; final void Function()? onDismiss; const TabActionDialog({ - required this.initialTab, + required this.tabId, this.onDismiss, super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { - final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab; + final tabState = + ref.watch(tabStateProvider(tabId)) ?? TabState.$default(tabId); final topics = ref.watch(topicRepositoryProvider.select((value) => value.valueOrNull)); final selectedTopic = @@ -42,13 +42,13 @@ class TabActionDialog extends HookConsumerWidget { vertical: 24.0, ), title: ListTile( - leading: FaviconImage( - favicon: tabState.favicon, - url: tabState.url, - size: 24, + leading: RawImage( + image: tabState.icon?.value, + width: 24, + height: 24, ), contentPadding: EdgeInsets.zero, - title: Text(tabState.title ?? 'Unknown Title'), + title: Text(tabState.title), subtitle: Text(tabState.url.authority), ), children: [ diff --git a/app/lib/features/search_browser/presentation/screens/browser.dart b/app/lib/features/search_browser/presentation/screens/browser.dart index 49cf2575..7fd2c767 100644 --- a/app/lib/features/search_browser/presentation/screens/browser.dart +++ b/app/lib/features/search_browser/presentation/screens/browser.dart @@ -7,11 +7,12 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; import 'package:lensai/features/search_browser/domain/providers.dart'; import 'package:lensai/features/search_browser/domain/services/create_tab.dart'; -import 'package:lensai/features/search_browser/domain/services/session.dart'; +import 'package:lensai/features/search_browser/presentation/controllers/bottom_sheet.dart'; +import 'package:lensai/features/search_browser/presentation/controllers/overlay_dialog.dart'; import 'package:lensai/features/search_browser/presentation/widgets/app_bar_title.dart'; import 'package:lensai/features/search_browser/presentation/widgets/landing/content.dart'; import 'package:lensai/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart'; @@ -19,11 +20,6 @@ import 'package:lensai/features/search_browser/presentation/widgets/sheets/view_ import 'package:lensai/features/search_browser/presentation/widgets/tabs_action_button.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; -import 'package:lensai/features/topics/domain/repositories/tab.dart'; -import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; -import 'package:lensai/features/web_view/presentation/controllers/readerability.dart'; -import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart'; -import 'package:lensai/features/web_view/presentation/widgets/web_page_dialog.dart'; import 'package:lensai/presentation/hooks/overlay_portal_controller.dart'; import 'package:lensai/presentation/widgets/animate_gradient_shader.dart'; import 'package:lensai/presentation/widgets/animated_indexed_stack.dart'; @@ -41,8 +37,8 @@ class KagiScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final displayedSheet = ref.watch(bottomSheetProvider); - final displayedOverlayDialog = ref.watch(overlayDialogProvider); + final displayedSheet = ref.watch(bottomSheetControllerProvider); + final displayedOverlayDialog = ref.watch(overlayDialogControllerProvider); final showEarlyAccessFeatures = ref.watch( settingsRepositoryProvider.select( @@ -75,7 +71,7 @@ class KagiScreen extends HookConsumerWidget { final activeKagiTool = useValueNotifier(null, [displayedSheet]); ref.listen( - overlayDialogProvider, + overlayDialogControllerProvider, (previous, next) { if (next != null) { overlayController.show(); @@ -109,7 +105,7 @@ class KagiScreen extends HookConsumerWidget { return (page != null) ? AppBarTitle( - page: page, + tab: page, onTap: () { ref.read(overlayDialogProvider.notifier).show( WebPageDialog( @@ -149,7 +145,8 @@ class KagiScreen extends HookConsumerWidget { ref .read(createTabStreamProvider.notifier) .createTab( - CreateTab(preferredTool: KagiTool.search), + CreateTabSheet( + preferredTool: KagiTool.search), ); }, icon: Icon(KagiTool.search.icon), @@ -162,7 +159,8 @@ class KagiScreen extends HookConsumerWidget { ref .read(createTabStreamProvider.notifier) .createTab( - CreateTab(preferredTool: KagiTool.summarizer), + CreateTabSheet( + preferredTool: KagiTool.summarizer), ); }, icon: Icon(KagiTool.summarizer.icon), @@ -176,7 +174,7 @@ class KagiScreen extends HookConsumerWidget { ref .read(createTabStreamProvider.notifier) .createTab( - CreateTab( + CreateTabSheet( preferredTool: KagiTool.assistant, ), ); @@ -278,7 +276,7 @@ class KagiScreen extends HookConsumerWidget { if (quickAction != null) InkWell( onTap: () async { - var tab = CreateTab( + var tab = CreateTabSheet( preferredTool: quickAction, ); @@ -303,7 +301,7 @@ class KagiScreen extends HookConsumerWidget { } } - tab = CreateTab( + tab = CreateTabSheet( preferredTool: quickAction, content: await completer.future, ); @@ -320,12 +318,12 @@ class KagiScreen extends HookConsumerWidget { ), ), TabsActionButton( - isActive: displayedSheet is ViewTabs, + isActive: displayedSheet is ViewTabsSheet, onTap: () { - if (displayedSheet case ViewTabs()) { + if (displayedSheet case ViewTabsSheet()) { ref.read(bottomSheetProvider.notifier).dismiss(); } else { - ref.read(bottomSheetProvider.notifier).show(ViewTabs()); + ref.read(bottomSheetProvider.notifier).show(ViewTabsSheet()); } }, ), @@ -387,7 +385,7 @@ class KagiScreen extends HookConsumerWidget { MenuItemButton( onPressed: () { ref.read(createTabStreamProvider.notifier).createTab( - CreateTab(preferredTool: KagiTool.assistant), + CreateTabSheet(preferredTool: KagiTool.assistant), ); }, leadingIcon: Icon(KagiTool.assistant.icon), @@ -396,7 +394,7 @@ class KagiScreen extends HookConsumerWidget { MenuItemButton( onPressed: () { ref.read(createTabStreamProvider.notifier).createTab( - CreateTab(preferredTool: KagiTool.summarizer), + CreateTabSheet(preferredTool: KagiTool.summarizer), ); }, leadingIcon: Icon(KagiTool.summarizer.icon), @@ -405,7 +403,7 @@ class KagiScreen extends HookConsumerWidget { MenuItemButton( onPressed: () { ref.read(createTabStreamProvider.notifier).createTab( - CreateTab(preferredTool: KagiTool.search), + CreateTabSheet(preferredTool: KagiTool.search), ); }, leadingIcon: Icon(KagiTool.search.icon), @@ -649,7 +647,7 @@ class KagiScreen extends HookConsumerWidget { return false; }, child: switch (displayedSheet) { - ViewTabs() => DraggableScrollableSheet( + ViewTabsSheet() => DraggableScrollableSheet( key: ValueKey(displayedSheet), expand: false, minChildSize: 0.1, @@ -669,7 +667,7 @@ class KagiScreen extends HookConsumerWidget { ); }, ), - final CreateTab parameter => DraggableScrollableSheet( + final CreateTabSheet parameter => DraggableScrollableSheet( key: ValueKey(displayedSheet), expand: false, initialChildSize: 0.8, diff --git a/app/lib/features/search_browser/presentation/widgets/app_bar_title.dart b/app/lib/features/search_browser/presentation/widgets/app_bar_title.dart index 6d3bb807..f48f9480 100644 --- a/app/lib/features/search_browser/presentation/widgets/app_bar_title.dart +++ b/app/lib/features/search_browser/presentation/widgets/app_bar_title.dart @@ -1,24 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; -import 'package:lensai/features/web_view/domain/entities/web_view_page.dart'; -import 'package:lensai/features/web_view/presentation/widgets/favicon.dart'; +import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; import 'package:text_scroll/text_scroll.dart'; class AppBarTitle extends StatelessWidget { - final WebViewPage page; + final TabState tab; final void Function()? onTap; - const AppBarTitle({required this.page, this.onTap, super.key}); + const AppBarTitle({required this.tab, this.onTap, super.key}); - Icon _securityStatusIcon(BuildContext context, WebViewPage page) { - if (page.url.isScheme('http')) { + Icon _securityStatusIcon(BuildContext context) { + if (tab.url.isScheme('http')) { return Icon( MdiIcons.lockOff, color: Theme.of(context).colorScheme.error, size: 14, ); - } else if (page.sslError != null) { + } else if (!tab.securityInfoState.secure) { return Icon( MdiIcons.lockAlert, color: Theme.of(context).colorScheme.errorContainer, @@ -39,9 +38,10 @@ class AppBarTitle extends StatelessWidget { onTap: onTap, child: Row( children: [ - FaviconImage( - favicon: page.favicon, - url: page.url, + RawImage( + image: tab.icon?.value, + height: 16, + width: 16, ), const SizedBox( width: 8, @@ -52,8 +52,8 @@ class AppBarTitle extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ TextScroll( - key: ValueKey(page.title), - page.title ?? 'New Tab', + key: ValueKey(tab.title), + tab.title, style: theme.textTheme.bodyLarge ?.copyWith(color: theme.colorScheme.onSurface), // mode: TextScrollMode.bouncing, @@ -68,13 +68,13 @@ class AppBarTitle extends StatelessWidget { ), Row( children: [ - _securityStatusIcon(context, page), + _securityStatusIcon(context), const SizedBox( width: 4, ), Expanded( child: Text( - page.url.authority, + tab.url.authority, style: theme.textTheme.bodyMedium ?.copyWith(color: theme.colorScheme.onSurface), ), diff --git a/app/lib/features/search_browser/presentation/widgets/landing/content.dart b/app/lib/features/search_browser/presentation/widgets/landing/content.dart index 26f74f87..0fe09084 100644 --- a/app/lib/features/search_browser/presentation/widgets/landing/content.dart +++ b/app/lib/features/search_browser/presentation/widgets/landing/content.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/domain/services/app_initialization.dart'; import 'package:lensai/features/search_browser/presentation/widgets/error_container.dart'; import 'package:lensai/features/search_browser/presentation/widgets/landing/action.dart'; -import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart'; import 'package:lensai/presentation/hooks/cached_future.dart'; class LandingContent extends HookConsumerWidget { diff --git a/app/lib/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart b/app/lib/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart index ae8380c3..ddbc2c91 100644 --- a/app/lib/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart +++ b/app/lib/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart @@ -2,7 +2,7 @@ import 'package:expandable_page_view/expandable_page_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; import 'package:lensai/features/search_browser/presentation/widgets/tabs/assistant_tab.dart'; import 'package:lensai/features/search_browser/presentation/widgets/tabs/search_tab.dart'; @@ -15,7 +15,7 @@ import 'package:lensai/presentation/hooks/sync_page_tab.dart'; typedef OnSubmitUri = void Function(Uri url); class SharedContentSheet extends HookConsumerWidget { - final CreateTab parameter; + final CreateTabSheet parameter; final OnSubmitUri onSubmit; final void Function(KagiTool tool)? onActiveToolChanged; diff --git a/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart b/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart index a32259ab..62c480fb 100644 --- a/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart +++ b/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart @@ -4,14 +4,10 @@ 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/search_browser/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; +import 'package:lensai/features/geckoview/features/topics/presentation/widgets/topic_chips.dart'; import 'package:lensai/features/search_browser/presentation/dialogs/tab_action.dart'; -import 'package:lensai/features/topics/domain/providers.dart'; -import 'package:lensai/features/topics/domain/repositories/tab.dart'; -import 'package:lensai/features/topics/presentation/widgets/topic_chips.dart'; -import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; -import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart'; -import 'package:lensai/features/web_view/presentation/widgets/web_view_tab.dart'; class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { static const _headerSize = 104.0; diff --git a/app/lib/features/search_browser/presentation/widgets/tabs/assistant_tab.dart b/app/lib/features/search_browser/presentation/widgets/tabs/assistant_tab.dart index e0b555e8..ec1c51a4 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs/assistant_tab.dart +++ b/app/lib/features/search_browser/presentation/widgets/tabs/assistant_tab.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/domain/providers.dart'; import 'package:lensai/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart'; import 'package:lensai/features/search_browser/presentation/widgets/speech_to_text_button.dart'; diff --git a/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart b/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart index 0c4fa894..f9917084 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart +++ b/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart @@ -9,6 +9,7 @@ import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; import 'package:lensai/features/bangs/presentation/widgets/search_field.dart'; import 'package:lensai/features/search_browser/domain/providers.dart'; +import 'package:lensai/features/search_browser/presentation/controllers/bottom_sheet.dart'; import 'package:lensai/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart'; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; diff --git a/app/lib/features/search_browser/presentation/widgets/tabs/summarize_tab.dart b/app/lib/features/search_browser/presentation/widgets/tabs/summarize_tab.dart index 9bdafadb..aac8f6e0 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs/summarize_tab.dart +++ b/app/lib/features/search_browser/presentation/widgets/tabs/summarize_tab.dart @@ -6,7 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_pdf_text/flutter_pdf_text.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart'; import 'package:lensai/features/search_browser/utils/url_builder.dart' as uri_builder; diff --git a/app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart b/app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart index 71e8629a..f4c23328 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart +++ b/app/lib/features/search_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/web_view/domain/providers.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; class TabsActionButton extends HookConsumerWidget { final bool isActive; @@ -17,7 +17,7 @@ class TabsActionButton extends HookConsumerWidget { final theme = Theme.of(context); final tabCount = - ref.watch(activeTabsProvider.select((tabs) => tabs.length)); + ref.watch(tabRepositoryProvider.select((tabs) => tabs.length)); return InkWell( onTap: onTap, diff --git a/app/lib/features/search_browser/utils/url_builder.dart b/app/lib/features/search_browser/utils/url_builder.dart index 7ccc86ef..60439fed 100644 --- a/app/lib/features/search_browser/utils/url_builder.dart +++ b/app/lib/features/search_browser/utils/url_builder.dart @@ -1,4 +1,4 @@ -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; final _baseUrl = Uri.https('kagi.com'); diff --git a/app/lib/features/settings/data/models/settings.dart b/app/lib/features/settings/data/models/settings.dart index 50156eec..e366a574 100644 --- a/app/lib/features/settings/data/models/settings.dart +++ b/app/lib/features/settings/data/models/settings.dart @@ -2,7 +2,7 @@ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter/material.dart'; import 'package:lensai/features/content_block/data/models/host.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; part 'settings.g.dart'; diff --git a/app/lib/features/settings/presentation/screens/settings.dart b/app/lib/features/settings/presentation/screens/settings.dart index e00fc66c..dff8acc6 100644 --- a/app/lib/features/settings/presentation/screens/settings.dart +++ b/app/lib/features/settings/presentation/screens/settings.dart @@ -8,7 +8,7 @@ import 'package:lensai/features/bangs/data/models/bang.dart'; import 'package:lensai/features/bangs/domain/providers.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/content_block/data/models/host.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/features/settings/presentation/controllers/save_settings.dart'; diff --git a/app/lib/features/settings/utils/preference_parser.dart b/app/lib/features/settings/utils/preference_parser.dart index 7616d2ae..674997cd 100644 --- a/app/lib/features/settings/utils/preference_parser.dart +++ b/app/lib/features/settings/utils/preference_parser.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:lensai/features/content_block/data/models/host.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; Set? parseHostSources(List? input) => input ?.map( diff --git a/app/lib/features/share_intent/domain/services/sharing_intent.dart b/app/lib/features/share_intent/domain/services/sharing_intent.dart index a45fea1b..70751a2a 100644 --- a/app/lib/features/share_intent/domain/services/sharing_intent.dart +++ b/app/lib/features/share_intent/domain/services/sharing_intent.dart @@ -5,8 +5,8 @@ import 'package:flutter_pdf_text/flutter_pdf_text.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:lensai/core/logger.dart'; -import 'package:lensai/domain/entities/received_parameter.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/data/models/received_parameter.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:mime/mime.dart' as mime; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/app/lib/features/topics/data/database/daos/tab.dart b/app/lib/features/topics/data/database/daos/tab.dart deleted file mode 100644 index 148292fa..00000000 --- a/app/lib/features/topics/data/database/daos/tab.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:lensai/features/topics/data/database/database.dart'; -import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart'; - -part 'tab.g.dart'; - -@DriftAccessor() -class TabDao extends DatabaseAccessor with _$TabDaoMixin { - TabDao(super.db); - - Stream> watchTabs() { - return (db.tab.select()..orderBy([(u) => OrderingTerm.asc(u.id)])).watch(); - } - - Stream watchTab(String tabId) { - return (db.tab.select()..where((t) => t.id.equals(tabId))) - .watchSingleOrNull(); - } - - Stream watchTabExisiting(String tabId) { - final existsStatement = - existsQuery(db.tab.select()..where((t) => t.id.equals(tabId))); - - return selectExpressions([existsStatement]) - .map((row) => row.read(existsStatement)!) - .watchSingle(); - } - - Stream> watchTopicTabs(String? topicId) { - final query = (selectOnly(db.tab) - ..addColumns([db.tab.id]) - ..where( - (topicId == null) - ? db.tab.topicId.isNull() - : db.tab.topicId.equals(topicId), - ) - ..orderBy([OrderingTerm.asc(db.tab.id)])); - - return query.map((row) => row.read(db.tab.id)!).watch(); - } - - Future upsertTab(ITab tab, DateTime timestamp) { - return db.tab.insertOne( - TabCompanion.insert( - id: tab.id, - timestamp: timestamp, - url: tab.url, - topicId: Value(tab.topicId), - title: Value(tab.title), - screenshot: Value(tab.screenshot), - ), - mode: InsertMode.insertOrReplace, - ); - } - - Future updateTab( - String id, { - required DateTime timestamp, - Value url = const Value.absent(), - Value title = const Value.absent(), - Value topicId = const Value.absent(), - Value screenshot = const Value.absent(), - }) { - final statement = db.tab.update()..where((t) => t.id.equals(id)); - - return statement.write( - TabCompanion( - timestamp: Value(timestamp), - url: url, - title: title, - topicId: topicId, - screenshot: screenshot, - ), - ); - } - - Future deleteTab(String id) { - return db.tab.deleteOne(TabCompanion.custom(id: Variable(id))); - } - - Future deleteTopicTabs(String? topicId) { - return (db.tab.delete() - ..where( - (t) => (topicId == null) - ? t.topicId.isNull() - : t.topicId.equals(topicId), - )) - .go(); - } -} diff --git a/app/lib/features/topics/data/database/database.dart b/app/lib/features/topics/data/database/database.dart deleted file mode 100644 index 258969dd..00000000 --- a/app/lib/features/topics/data/database/database.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:ui'; - -import 'package:drift/drift.dart'; -import 'package:lensai/features/topics/data/database/daos/tab.dart'; -import 'package:lensai/features/topics/data/database/daos/topic.dart'; -import 'package:lensai/features/topics/data/database/drift/converters/color.dart'; -import 'package:lensai/features/topics/data/database/drift/converters/uri.dart'; -import 'package:lensai/features/topics/data/models/topic_data.dart'; - -part 'database.g.dart'; - -@DriftDatabase( - include: {'database.drift'}, - daos: [TopicDao, TabDao], -) -class TabDatabase extends _$TabDatabase { - @override - final int schemaVersion = 1; - - @override - MigrationStrategy get migration => MigrationStrategy( - beforeOpen: (details) async { - await customStatement('PRAGMA foreign_keys = ON;'); - }, - ); - - TabDatabase(super.e); -} diff --git a/app/lib/features/topics/data/providers.g.dart b/app/lib/features/topics/data/providers.g.dart deleted file mode 100644 index 4682778d..00000000 --- a/app/lib/features/topics/data/providers.g.dart +++ /dev/null @@ -1,299 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'providers.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$tabDatabaseHash() => r'b886b089ae5fdb9bdcc0982efe5fcbe9bc66486f'; - -/// See also [tabDatabase]. -@ProviderFor(tabDatabase) -final tabDatabaseProvider = Provider.internal( - tabDatabase, - name: r'tabDatabaseProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$tabDatabaseHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef TabDatabaseRef = ProviderRef; -String _$isTabExistingHash() => r'98fc7bf073ba8f73c20df6ba7076672ff497246f'; - -/// 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 [isTabExisting]. -@ProviderFor(isTabExisting) -const isTabExistingProvider = IsTabExistingFamily(); - -/// See also [isTabExisting]. -class IsTabExistingFamily extends Family> { - /// See also [isTabExisting]. - const IsTabExistingFamily(); - - /// See also [isTabExisting]. - IsTabExistingProvider call( - String tabId, - ) { - return IsTabExistingProvider( - tabId, - ); - } - - @override - IsTabExistingProvider getProviderOverride( - covariant IsTabExistingProvider 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'isTabExistingProvider'; -} - -/// See also [isTabExisting]. -class IsTabExistingProvider extends AutoDisposeStreamProvider { - /// See also [isTabExisting]. - IsTabExistingProvider( - String tabId, - ) : this._internal( - (ref) => isTabExisting( - ref as IsTabExistingRef, - tabId, - ), - from: isTabExistingProvider, - name: r'isTabExistingProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$isTabExistingHash, - dependencies: IsTabExistingFamily._dependencies, - allTransitiveDependencies: - IsTabExistingFamily._allTransitiveDependencies, - tabId: tabId, - ); - - IsTabExistingProvider._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(IsTabExistingRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: IsTabExistingProvider._internal( - (ref) => create(ref as IsTabExistingRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - tabId: tabId, - ), - ); - } - - @override - AutoDisposeStreamProviderElement createElement() { - return _IsTabExistingProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is IsTabExistingProvider && 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 IsTabExistingRef on AutoDisposeStreamProviderRef { - /// The parameter `tabId` of this provider. - String get tabId; -} - -class _IsTabExistingProviderElement - extends AutoDisposeStreamProviderElement with IsTabExistingRef { - _IsTabExistingProviderElement(super.provider); - - @override - String get tabId => (origin as IsTabExistingProvider).tabId; -} - -String _$tabDataHash() => r'8b799386305d0c31103bc48a5a467df41d57d065'; - -/// See also [tabData]. -@ProviderFor(tabData) -const tabDataProvider = TabDataFamily(); - -/// See also [tabData]. -class TabDataFamily extends Family> { - /// See also [tabData]. - const TabDataFamily(); - - /// See also [tabData]. - TabDataProvider call( - String tabId, - ) { - return TabDataProvider( - tabId, - ); - } - - @override - TabDataProvider getProviderOverride( - covariant TabDataProvider 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'tabDataProvider'; -} - -/// See also [tabData]. -class TabDataProvider extends AutoDisposeStreamProvider { - /// See also [tabData]. - TabDataProvider( - String tabId, - ) : this._internal( - (ref) => tabData( - ref as TabDataRef, - tabId, - ), - from: tabDataProvider, - name: r'tabDataProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$tabDataHash, - dependencies: TabDataFamily._dependencies, - allTransitiveDependencies: TabDataFamily._allTransitiveDependencies, - tabId: tabId, - ); - - TabDataProvider._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(TabDataRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: TabDataProvider._internal( - (ref) => create(ref as TabDataRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - tabId: tabId, - ), - ); - } - - @override - AutoDisposeStreamProviderElement createElement() { - return _TabDataProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is TabDataProvider && 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 TabDataRef on AutoDisposeStreamProviderRef { - /// The parameter `tabId` of this provider. - String get tabId; -} - -class _TabDataProviderElement extends AutoDisposeStreamProviderElement - with TabDataRef { - _TabDataProviderElement(super.provider); - - @override - String get tabId => (origin as TabDataProvider).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/topics/domain/providers.g.dart b/app/lib/features/topics/domain/providers.g.dart deleted file mode 100644 index 89808504..00000000 --- a/app/lib/features/topics/domain/providers.g.dart +++ /dev/null @@ -1,58 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'providers.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$selectedTopicDataHash() => r'fa450784052498014f592c356bde4ff5ec675281'; - -/// See also [selectedTopicData]. -@ProviderFor(selectedTopicData) -final selectedTopicDataProvider = - AutoDisposeStreamProvider.internal( - selectedTopicData, - name: r'selectedTopicDataProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$selectedTopicDataHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef SelectedTopicDataRef = AutoDisposeStreamProviderRef; -String _$unusedRandomTopicColorHash() => - r'099730b0d987cc37bcae4ef5c999cbebfa67d7da'; - -/// 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 _$selectedTopicHash() => r'f4e1c0620971a0b7501ee9aff0d5b41da3130ba0'; - -/// See also [SelectedTopic]. -@ProviderFor(SelectedTopic) -final selectedTopicProvider = NotifierProvider.internal( - SelectedTopic.new, - name: r'selectedTopicProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$selectedTopicHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$SelectedTopic = Notifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/topics/domain/repositories/tab.dart b/app/lib/features/topics/domain/repositories/tab.dart deleted file mode 100644 index 4e6b5b94..00000000 --- a/app/lib/features/topics/domain/repositories/tab.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:lensai/features/topics/data/database/database.dart'; -import 'package:lensai/features/topics/data/providers.dart'; -import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'tab.g.dart'; - -@Riverpod(keepAlive: true) -class TabRepository extends _$TabRepository { - late TabDatabase _db; - - @override - Stream> build() { - _db = ref.watch(tabDatabaseProvider); - return _db.tabDao.watchTabs(); - } - - Future updateTab( - String id, { - Value url = const Value.absent(), - Value title = const Value.absent(), - Value topicId = const Value.absent(), - Value screenshot = const Value.absent(), - }) { - return _db.tabDao.updateTab( - id, - timestamp: DateTime.now(), - url: url, - title: title, - topicId: topicId, - screenshot: screenshot, - ); - } - - Future deleteTab(String id) { - return _db.tabDao.deleteTab(id); - } -} - -@Riverpod() -class TopicTabRepository extends _$TopicTabRepository { - late TabDatabase _db; - - @override - Stream> build(String? topicId) { - _db = ref.watch(tabDatabaseProvider); - return _db.tabDao.watchTopicTabs(topicId); - } - - Future addTab(ITab tab) { - assert(tab.topicId == topicId); - return _db.tabDao.upsertTab(tab, DateTime.now()); - } - - Future closeAllTabs() { - return _db.tabDao.deleteTopicTabs(topicId); - } -} diff --git a/app/lib/features/topics/domain/repositories/tab.g.dart b/app/lib/features/topics/domain/repositories/tab.g.dart deleted file mode 100644 index 36169d8c..00000000 --- a/app/lib/features/topics/domain/repositories/tab.g.dart +++ /dev/null @@ -1,192 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'tab.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$tabRepositoryHash() => r'454e182a99910512be72279f68caa0af125ed165'; - -/// See also [TabRepository]. -@ProviderFor(TabRepository) -final tabRepositoryProvider = - StreamNotifierProvider>.internal( - TabRepository.new, - name: r'tabRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$tabRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$TabRepository = StreamNotifier>; -String _$topicTabRepositoryHash() => - r'52ceb013e0b31046fdd0a00d031392ba84b9ecda'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$TopicTabRepository - extends BuildlessAutoDisposeStreamNotifier> { - late final String? topicId; - - Stream> build( - String? topicId, - ); -} - -/// See also [TopicTabRepository]. -@ProviderFor(TopicTabRepository) -const topicTabRepositoryProvider = TopicTabRepositoryFamily(); - -/// See also [TopicTabRepository]. -class TopicTabRepositoryFamily extends Family>> { - /// See also [TopicTabRepository]. - const TopicTabRepositoryFamily(); - - /// See also [TopicTabRepository]. - TopicTabRepositoryProvider call( - String? topicId, - ) { - return TopicTabRepositoryProvider( - topicId, - ); - } - - @override - TopicTabRepositoryProvider getProviderOverride( - covariant TopicTabRepositoryProvider 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'topicTabRepositoryProvider'; -} - -/// See also [TopicTabRepository]. -class TopicTabRepositoryProvider extends AutoDisposeStreamNotifierProviderImpl< - TopicTabRepository, List> { - /// See also [TopicTabRepository]. - TopicTabRepositoryProvider( - String? topicId, - ) : this._internal( - () => TopicTabRepository()..topicId = topicId, - from: topicTabRepositoryProvider, - name: r'topicTabRepositoryProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$topicTabRepositoryHash, - dependencies: TopicTabRepositoryFamily._dependencies, - allTransitiveDependencies: - TopicTabRepositoryFamily._allTransitiveDependencies, - topicId: topicId, - ); - - TopicTabRepositoryProvider._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 - Stream> runNotifierBuild( - covariant TopicTabRepository notifier, - ) { - return notifier.build( - topicId, - ); - } - - @override - Override overrideWith(TopicTabRepository Function() create) { - return ProviderOverride( - origin: this, - override: TopicTabRepositoryProvider._internal( - () => create()..topicId = topicId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - topicId: topicId, - ), - ); - } - - @override - AutoDisposeStreamNotifierProviderElement> - createElement() { - return _TopicTabRepositoryProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is TopicTabRepositoryProvider && 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 TopicTabRepositoryRef - on AutoDisposeStreamNotifierProviderRef> { - /// The parameter `topicId` of this provider. - String? get topicId; -} - -class _TopicTabRepositoryProviderElement - extends AutoDisposeStreamNotifierProviderElement> with TopicTabRepositoryRef { - _TopicTabRepositoryProviderElement(super.provider); - - @override - String? get topicId => (origin as TopicTabRepositoryProvider).topicId; -} -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/web_view/domain/entities/abstract/tab.dart b/app/lib/features/web_view/domain/entities/abstract/tab.dart index 144f703d..f7909508 100644 --- a/app/lib/features/web_view/domain/entities/abstract/tab.dart +++ b/app/lib/features/web_view/domain/entities/abstract/tab.dart @@ -1,5 +1,4 @@ import 'package:drift/drift.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; abstract interface class ITab { String get id; diff --git a/app/lib/features/web_view/domain/entities/consistent_controller.dart b/app/lib/features/web_view/domain/entities/consistent_controller.dart index eca5ed21..9631d866 100644 --- a/app/lib/features/web_view/domain/entities/consistent_controller.dart +++ b/app/lib/features/web_view/domain/entities/consistent_controller.dart @@ -1,5 +1,3 @@ -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; - class ConsistentController { final InAppWebViewController? value; diff --git a/app/lib/features/web_view/domain/entities/web_view_page.dart b/app/lib/features/web_view/domain/entities/web_view_page.dart index 8b5ab8ec..25d04a9c 100644 --- a/app/lib/features/web_view/domain/entities/web_view_page.dart +++ b/app/lib/features/web_view/domain/entities/web_view_page.dart @@ -1,10 +1,9 @@ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + import 'package:lensai/core/uuid.dart'; -import 'package:lensai/domain/entities/web_page_info.dart'; -import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart'; +import 'package:lensai/data/models/web_page_info.dart'; part 'web_view_page.g.dart'; diff --git a/app/lib/features/web_view/domain/entities/web_view_page.g.dart b/app/lib/features/web_view/domain/entities/web_view_page.g.dart index 8d2799c4..ab3ea306 100644 --- a/app/lib/features/web_view/domain/entities/web_view_page.g.dart +++ b/app/lib/features/web_view/domain/entities/web_view_page.g.dart @@ -7,17 +7,17 @@ part of 'web_view_page.dart'; // ************************************************************************** abstract class _$WebViewPageCWProxy { - WebViewPage controller(InAppWebViewController? controller); + WebViewPage controller(InvalidType controller); WebViewPage url(Uri url); - WebViewPage sslError(SslError? sslError); + WebViewPage sslError(InvalidType sslError); WebViewPage title(String? title); WebViewPage topicId(String? topicId); - WebViewPage favicon(Favicon? favicon); + WebViewPage favicon(BrowserIcon? favicon); WebViewPage screenshot(Uint8List? screenshot); @@ -30,12 +30,12 @@ abstract class _$WebViewPageCWProxy { /// WebViewPage(...).copyWith(id: 12, name: "My name") /// ```` WebViewPage call({ - InAppWebViewController? controller, + InvalidType? controller, Uri? url, - SslError? sslError, + InvalidType? sslError, String? title, String? topicId, - Favicon? favicon, + BrowserIcon? favicon, Uint8List? screenshot, ({bool canGoBack, bool canGoForward})? pageHistory, }); @@ -48,14 +48,14 @@ class _$WebViewPageCWProxyImpl implements _$WebViewPageCWProxy { final WebViewPage _value; @override - WebViewPage controller(InAppWebViewController? controller) => + WebViewPage controller(InvalidType controller) => this(controller: controller); @override WebViewPage url(Uri url) => this(url: url); @override - WebViewPage sslError(SslError? sslError) => this(sslError: sslError); + WebViewPage sslError(InvalidType sslError) => this(sslError: sslError); @override WebViewPage title(String? title) => this(title: title); @@ -64,7 +64,7 @@ class _$WebViewPageCWProxyImpl implements _$WebViewPageCWProxy { WebViewPage topicId(String? topicId) => this(topicId: topicId); @override - WebViewPage favicon(Favicon? favicon) => this(favicon: favicon); + WebViewPage favicon(BrowserIcon? favicon) => this(favicon: favicon); @override WebViewPage screenshot(Uint8List? screenshot) => this(screenshot: screenshot); @@ -93,18 +93,19 @@ class _$WebViewPageCWProxyImpl implements _$WebViewPageCWProxy { }) { return WebViewPage( id: _value.id, - controller: controller == const $CopyWithPlaceholder() - ? _value.controller - // ignore: cast_nullable_to_non_nullable - : controller as InAppWebViewController?, + controller: + controller == const $CopyWithPlaceholder() || controller == null + ? _value.controller + // ignore: cast_nullable_to_non_nullable + : controller as InvalidType, url: url == const $CopyWithPlaceholder() || url == null ? _value.url // ignore: cast_nullable_to_non_nullable : url as Uri, - sslError: sslError == const $CopyWithPlaceholder() + sslError: sslError == const $CopyWithPlaceholder() || sslError == null ? _value.sslError // ignore: cast_nullable_to_non_nullable - : sslError as SslError?, + : sslError as InvalidType, title: title == const $CopyWithPlaceholder() ? _value.title // ignore: cast_nullable_to_non_nullable @@ -116,7 +117,7 @@ class _$WebViewPageCWProxyImpl implements _$WebViewPageCWProxy { favicon: favicon == const $CopyWithPlaceholder() ? _value.favicon // ignore: cast_nullable_to_non_nullable - : favicon as Favicon?, + : favicon as BrowserIcon?, screenshot: screenshot == const $CopyWithPlaceholder() ? _value.screenshot // ignore: cast_nullable_to_non_nullable diff --git a/app/lib/features/web_view/domain/providers.dart b/app/lib/features/web_view/domain/providers.dart index d02d6420..02922b64 100644 --- a/app/lib/features/web_view/domain/providers.dart +++ b/app/lib/features/web_view/domain/providers.dart @@ -5,8 +5,8 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:lensai/features/content_block/domain/repositories/host.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; -import 'package:lensai/features/topics/domain/providers.dart'; -import 'package:lensai/features/topics/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:universal_io/io.dart'; diff --git a/app/lib/features/web_view/domain/repositories/web_view.dart b/app/lib/features/web_view/domain/repositories/web_view.dart index 071ab293..9dba3361 100644 --- a/app/lib/features/web_view/domain/repositories/web_view.dart +++ b/app/lib/features/web_view/domain/repositories/web_view.dart @@ -1,11 +1,11 @@ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:drift/drift.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/logger.dart'; -import 'package:lensai/features/topics/data/providers.dart'; -import 'package:lensai/features/topics/domain/providers.dart'; -import 'package:lensai/features/topics/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; import 'package:lensai/features/web_view/domain/entities/consistent_controller.dart'; import 'package:lensai/features/web_view/domain/entities/web_view_page.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/app/lib/features/web_view/presentation/controllers/readerability.dart b/app/lib/features/web_view/presentation/controllers/readerability.dart index d94e1749..2edaa3c2 100644 --- a/app/lib/features/web_view/presentation/controllers/readerability.dart +++ b/app/lib/features/web_view/presentation/controllers/readerability.dart @@ -1,4 +1,4 @@ -import 'package:lensai/features/topics/data/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; import 'package:lensai/features/web_view/presentation/services/readerability_script.dart'; import 'package:riverpod/riverpod.dart'; diff --git a/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart b/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart index 4838a727..495bf5b3 100644 --- a/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart +++ b/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart @@ -1,6 +1,5 @@ -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:lensai/features/topics/domain/providers.dart'; -import 'package:lensai/features/topics/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; import 'package:lensai/features/web_view/domain/entities/web_view_page.dart'; import 'package:lensai/features/web_view/domain/repositories/web_view.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/app/lib/features/web_view/presentation/services/readerability_script.dart b/app/lib/features/web_view/presentation/services/readerability_script.dart index 79167da9..f1e55c80 100644 --- a/app/lib/features/web_view/presentation/services/readerability_script.dart +++ b/app/lib/features/web_view/presentation/services/readerability_script.dart @@ -1,4 +1,3 @@ -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; import 'package:lensai/features/settings/data/repositories/settings_repository.dart'; import 'package:lensai/features/web_view/domain/providers.dart'; diff --git a/app/lib/features/web_view/presentation/widgets/favicon.dart b/app/lib/features/web_view/presentation/widgets/favicon.dart deleted file mode 100644 index 2a449dfc..00000000 --- a/app/lib/features/web_view/presentation/widgets/favicon.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; -import 'package:lensai/extensions/web_uri_favicon.dart'; - -class FaviconImage extends StatelessWidget { - final double size; - final Icon _iconPlaceholder; - - final Favicon? favicon; - final Uri url; - - FaviconImage({ - required this.url, - required this.favicon, - this.size = 16, - super.key, - }) : _iconPlaceholder = Icon( - MdiIcons.webBox, - size: size, - ); - - @override - Widget build(BuildContext context) { - if (favicon != null) { - return FadeInImage( - placeholder: NetworkImage( - url.genericFavicon().toString(), - ), - image: NetworkImage(favicon!.url.toString()), - placeholderErrorBuilder: (_, __, ___) => _iconPlaceholder, - imageErrorBuilder: (_, __, ___) => Image.network( - url.genericFavicon().toString(), - errorBuilder: (_, __, ___) => _iconPlaceholder, - height: size, - width: size, - ), - height: size, - width: size, - ); - } else { - return Image.network( - url.genericFavicon().toString(), - errorBuilder: (_, __, ___) => _iconPlaceholder, - height: size, - width: size, - ); - } - } -} diff --git a/app/lib/features/web_view/presentation/widgets/web_page_dialog.dart b/app/lib/features/web_view/presentation/widgets/web_page_dialog.dart index 22f9b231..aea7557e 100644 --- a/app/lib/features/web_view/presentation/widgets/web_page_dialog.dart +++ b/app/lib/features/web_view/presentation/widgets/web_page_dialog.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/domain/entities/web_page_info.dart'; +import 'package:lensai/data/models/web_page_info.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/providers.dart'; import 'package:lensai/features/bangs/presentation/widgets/site_search.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/utils/url_builder.dart' as uri_builder; import 'package:lensai/features/settings/data/models/settings.dart'; @@ -24,14 +24,12 @@ import 'package:share_plus/share_plus.dart'; class WebPageDialog extends HookConsumerWidget { final Uri url; final WebPageInfo? precachedInfo; - final InAppWebViewController? webViewController; final void Function()? onDismiss; const WebPageDialog({ required this.url, this.precachedInfo, - this.webViewController, this.onDismiss, super.key, }); diff --git a/app/lib/features/web_view/presentation/widgets/web_view.dart b/app/lib/features/web_view/presentation/widgets/web_view.dart index 7fc1d775..7be3b58f 100644 --- a/app/lib/features/web_view/presentation/widgets/web_view.dart +++ b/app/lib/features/web_view/presentation/widgets/web_view.dart @@ -3,14 +3,14 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/logger.dart'; import 'package:lensai/domain/services/generic_website.dart'; import 'package:lensai/features/bangs/domain/providers.dart'; import 'package:lensai/features/chat_archive/domain/entities/chat_entity.dart'; import 'package:lensai/features/chat_archive/domain/repositories/archive.dart'; -import 'package:lensai/features/search_browser/domain/entities/modes.dart'; +import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; import 'package:lensai/features/search_browser/domain/providers.dart'; import 'package:lensai/features/settings/data/models/settings.dart'; @@ -243,7 +243,7 @@ class _WebViewState extends ConsumerState { if (selectedText != null && selectedText.isNotEmpty) { ref.read(bottomSheetProvider.notifier).show( - CreateTab( + CreateTabSheet( content: selectedText, preferredTool: KagiTool.assistant, ), diff --git a/app/lib/features/web_view/presentation/widgets/web_view_tab.dart b/app/lib/features/web_view/presentation/widgets/web_view_tab.dart index 49a4b977..a319da21 100644 --- a/app/lib/features/web_view/presentation/widgets/web_view_tab.dart +++ b/app/lib/features/web_view/presentation/widgets/web_view_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart'; -import 'package:lensai/features/web_view/presentation/widgets/favicon.dart'; +import 'package:lensai/features/search_browser/presentation/widgets/favicon.dart'; class WebViewTab extends StatelessWidget { final ITab tab; diff --git a/app/lib/features/web_view/utils/favicon_helper.dart b/app/lib/features/web_view/utils/favicon_helper.dart deleted file mode 100644 index 7c5781fb..00000000 --- a/app/lib/features/web_view/utils/favicon_helper.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; - -Favicon? choseFavicon(Iterable? favicons, [Favicon? currentIcon]) { - var selectedIcon = currentIcon; - - if (favicons != null) { - for (final icon in favicons) { - if (selectedIcon == null) { - selectedIcon = icon; - } else { - if ((selectedIcon.width == null && - !selectedIcon.url.toString().endsWith("favicon.ico")) || - (icon.width != null && - selectedIcon.width != null && - icon.width! > selectedIcon.width!)) { - selectedIcon = icon; - } - } - } - } - - return selectedIcon; -} diff --git a/app/lib/presentation/controllers/website_title.dart b/app/lib/presentation/controllers/website_title.dart index ff28478e..c69a5342 100644 --- a/app/lib/presentation/controllers/website_title.dart +++ b/app/lib/presentation/controllers/website_title.dart @@ -1,4 +1,4 @@ -import 'package:lensai/domain/entities/web_page_info.dart'; +import 'package:lensai/data/models/web_page_info.dart'; import 'package:lensai/domain/services/generic_website.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/app/lib/presentation/widgets/website_title_tile.dart b/app/lib/presentation/widgets/website_title_tile.dart index f69cb9cc..f0753d43 100644 --- a/app/lib/presentation/widgets/website_title_tile.dart +++ b/app/lib/presentation/widgets/website_title_tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/domain/entities/web_page_info.dart'; -import 'package:lensai/features/web_view/presentation/widgets/favicon.dart'; +import 'package:lensai/data/models/web_page_info.dart'; import 'package:lensai/presentation/controllers/website_title.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:skeletonizer/skeletonizer.dart'; diff --git a/app/lib/utils/image_helper.dart b/app/lib/utils/image_helper.dart deleted file mode 100644 index 2a9582e8..00000000 --- a/app/lib/utils/image_helper.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:typed_data'; -import 'dart:ui'; - -Future isImageValid(Uint8List bytes) async { - try { - final codec = await instantiateImageCodec(bytes, targetWidth: 32); - final frameInfo = await codec.getNextFrame(); - return frameInfo.image.width > 0; - } catch (e) { - return false; - } -} diff --git a/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig index 8c880c4d..5e631464 100644 --- a/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ b/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -3,8 +3,8 @@ FLUTTER_ROOT=/home/fafre/Software/flutter FLUTTER_APPLICATION_PATH=/home/fafre/Repos/kagi_bang_bang/app COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=0.6.2 -FLUTTER_BUILD_NUMBER=14 +FLUTTER_BUILD_NAME=0.7.0 +FLUTTER_BUILD_NUMBER=15 DART_OBFUSCATION=false TRACK_WIDGET_CREATION=true TREE_SHAKE_ICONS=false diff --git a/app/macos/Flutter/ephemeral/flutter_export_environment.sh b/app/macos/Flutter/ephemeral/flutter_export_environment.sh index 1a43578c..008f9762 100755 --- a/app/macos/Flutter/ephemeral/flutter_export_environment.sh +++ b/app/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -4,8 +4,8 @@ export "FLUTTER_ROOT=/home/fafre/Software/flutter" export "FLUTTER_APPLICATION_PATH=/home/fafre/Repos/kagi_bang_bang/app" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=0.6.2" -export "FLUTTER_BUILD_NUMBER=14" +export "FLUTTER_BUILD_NAME=0.7.0" +export "FLUTTER_BUILD_NUMBER=15" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" diff --git a/app/pubspec.lock b/app/pubspec.lock index 9478de5d..5e9860ca 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -194,10 +194,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -231,7 +231,7 @@ packages: source: hosted version: "0.3.4+2" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 @@ -483,6 +483,13 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.7447" + flutter_mozilla_components: + dependency: "direct main" + description: + path: "../packages/flutter_mozilla_components" + relative: true + source: path + version: "0.0.1" flutter_pdf_text: dependency: "direct main" description: @@ -737,18 +744,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -1113,7 +1120,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -1214,10 +1221,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -1230,10 +1237,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" text_scroll: dependency: "direct main" description: @@ -1366,10 +1373,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 80d610d8..c44ad1ad 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: animations: ^2.0.11 collection: ^1.18.0 copy_with_extension: ^5.0.4 + crypto: ^3.0.5 drift: ^2.20.0 dynamic_color: ^1.7.0 exceptions: ^0.6.1 @@ -23,6 +24,8 @@ dependencies: flutter_inappwebview: ^6.0.0 flutter_markdown: ^0.7.3+1 flutter_material_design_icons: ^1.1.7447 + flutter_mozilla_components: + path: ../packages/flutter_mozilla_components flutter_pdf_text: ^0.6.0 flutter_secure_storage: ^9.2.2 flutter_sharing_intent: ^1.1.1 diff --git a/app/pubspec_overrides.yaml b/app/pubspec_overrides.yaml new file mode 100644 index 00000000..d146a556 --- /dev/null +++ b/app/pubspec_overrides.yaml @@ -0,0 +1,4 @@ +# melos_managed_dependency_overrides: flutter_mozilla_components +dependency_overrides: + flutter_mozilla_components: + path: ../packages/flutter_mozilla_components diff --git a/melos.yaml b/melos.yaml index 304124e6..d15aa9ec 100644 --- a/melos.yaml +++ b/melos.yaml @@ -1,5 +1,18 @@ name: lensai_project +scripts: + build-pigeons: + exec: | + echo "Running pigeon for files in pigeons directory:" + for file in ./pigeons/*; do + if [ -f "\$file" ]; then + echo "Processing \$file" + flutter pub run pigeon --input "\$file" + fi + done + packageFilters: + dirExists: 'pigeons' + packages: - app/ - packages/** \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/cookie-manager/background.js b/packages/flutter_mozilla_components/android/src/main/assets/extensions/cookie-manager/background.js new file mode 100644 index 00000000..e5dd5776 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/cookie-manager/background.js @@ -0,0 +1,85 @@ +'use strict'; + +console.log("Connecting to native port...") +const port = browser.runtime.connectNative("cookieManager"); +console.log("Native port connected!") + +function cookieToMap(cookie) { + let partitionKey = {} + if (cookie.partitionKey) { + partitionKey["topLevelSite"] = cookie.partitionKey.topLevelSite + } + + return { + "domain": cookie.domain, + "expirationDate": cookie.expirationDate, + "firstPartyDomain": cookie.firstPartyDomain, + "hostOnly": cookie.hostOnly, + "httpOnly": cookie.httpOnly, + "name": cookie.name, + "partitionKey": partitionKey, + "path": cookie.path, + "secure": cookie.secure, + "session": cookie.session, + "sameSite": cookie.sameSite, + "storeId": cookie.storeId, + "value": cookie.value + } +} + +function sendCookieResultForRequest(id) { + return function (cookie) { + port.postMessage({ + "id": id, + "status": "success", + "result": cookieToMap(cookie) + }) + } +} + +function sendCookieListResultForRequest(id) { + return function (cookies) { + port.postMessage({ + "id": id, + "status": "success", + "result": cookies.map((cookie) => cookieToMap(cookie)) + }) + } +} + +function sendErrorForRequest(id) { + return function (error) { + console.error(error); + port.postMessage({ + "id": id, + "status": "error", + "error": error + }); + } +} + +port.onMessage.addListener(message => { + let requestId = message["id"] + switch (message["action"]) { + case "get": + browser.cookies.get(message["args"]) + .then(sendCookieResultForRequest(requestId)) + .catch(sendErrorForRequest(requestId)) + break + case "getAll": + browser.cookies.getAll(message["args"]) + .then(sendCookieListResultForRequest(requestId)) + .catch(sendErrorForRequest(requestId)) + break + case "remove": + browser.cookies.remove(message["args"]) + .then(sendCookieResultForRequest(requestId)) + .catch(sendErrorForRequest(requestId)) + break + case "set": + browser.cookies.set(message["args"]) + .then(sendCookieResultForRequest(requestId)) + .catch(sendErrorForRequest(requestId)) + break + } +}); diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/cookie-manager/manifest.json b/packages/flutter_mozilla_components/android/src/main/assets/extensions/cookie-manager/manifest.json new file mode 100644 index 00000000..afcccd6e --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/cookie-manager/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 2, + "name": "cookie-manager", + "version": "1.0", + "description": "Cookie manager", + "browser_specific_settings": { + "gecko": { + "id": "cookie-manager@lensai.eu" + } + }, + "background": { + "scripts": [ + "background.js" + ] + }, + "permissions": [ + "nativeMessaging", + "nativeMessagingFromContent", + "geckoViewAddons", + "cookies", + "" + ] +} \ No newline at end of file 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 da6029e8..cfa3cd40 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 @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents import mozilla.components.browser.thumbnails.BrowserThumbnails import mozilla.components.concept.engine.EngineView import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature 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 76d76f5c..6d9dd470 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 @@ -9,9 +9,18 @@ import android.util.Log import android.widget.Toast import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import eu.lensai.flutter_mozilla_components.ext.toWebPBytes import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware +import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents +import eu.lensai.flutter_mozilla_components.pigeons.HistoryItem +import eu.lensai.flutter_mozilla_components.pigeons.HistoryState +import eu.lensai.flutter_mozilla_components.pigeons.ReaderableState +import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState +import eu.lensai.flutter_mozilla_components.pigeons.TabContentState import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import mozilla.components.browser.engine.gecko.GeckoEngine @@ -66,7 +75,7 @@ private const val DAY_IN_MINUTES = 24 * 60L @SuppressLint("NewApi") @Suppress("LargeClass") -open class DefaultComponents(private val applicationContext: Context) { +open class DefaultComponents(private val applicationContext: Context, private val flutterEvents: GeckoStateEvents) { companion object { const val SAMPLE_BROWSER_PREFERENCES = "sample_browser_preferences" const val PREF_LAUNCH_EXTERNAL_APP = "sample_browser_launch_external_app" @@ -133,7 +142,7 @@ open class DefaultComponents(private val applicationContext: Context) { val store by lazy { BrowserStore( middleware = listOf( - FlutterEventMiddleware(), + FlutterEventMiddleware(flutterEvents), DownloadMiddleware(applicationContext, DownloadService::class.java), ReaderViewMiddleware(), ThumbnailsMiddleware(thumbnailStorage), @@ -145,12 +154,63 @@ open class DefaultComponents(private val applicationContext: Context) { ) + EngineMiddleware.create(engine), ).apply { this.flowScoped { flow -> - flow.mapNotNull { state -> state.selectedTab } - .distinctUntilChangedBy { - it.content.url + flow.map { state -> state.selectedTabId } + .collect { tabId -> + flutterEvents.onSelectedTabChange( + tabId + ) { _ -> } + } + } + + this.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.content.icon } .collect { tab -> - Log.d("URL_CHANGE", tab.content.url) + val iconBytes = tab.content.icon?.toWebPBytes() + flutterEvents.onIconChange( + tab.id, + iconBytes + ) { _ -> } + } + } + + this.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.content.securityInfo + } + .collect { tab -> + flutterEvents.onSecurityInfoStateChange( + tab.id, + SecurityInfoState( + tab.content.securityInfo.secure, + tab.content.securityInfo.host, + tab.content.securityInfo.issuer, + ) + ) { _ -> } + } + } + + this.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.readerState + } + .ifAnyChanged { arrayOf( + it.readerState.readerable, + it.readerState.active, + ) + } + .collect { tab -> + flutterEvents.onReaderableStateChange( + tab.id, + ReaderableState( + tab.readerState.readerable, + tab.readerState.active, + ) + ) { _ -> } } } @@ -159,9 +219,61 @@ open class DefaultComponents(private val applicationContext: Context) { .filterChanged { it.content } - .ifAnyChanged { arrayOf(it.content.url) } + .ifAnyChanged { arrayOf( + it.content.history, + it.content.canGoBack, + it.content.canGoForward, + ) + } .collect { tab -> - Log.d("URL_CHANGE2", tab.content.url) + flutterEvents.onHistoryStateChange( + tab.id, + HistoryState( + items = tab.content.history.items.map { item -> HistoryItem( + url = item.uri, + title = item.title + ) }, + currentIndex = tab.content.history.currentIndex.toLong(), + canGoBack = tab.content.canGoBack, + canGoForward = tab.content.canGoForward, + ) + ) { _ -> } + } + } + + this.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} } + .distinctUntilChanged() + .collect { tabs -> + flutterEvents.onTabListChange(tabs) { _ -> } + } + } + + this.flowScoped { flow -> + flow.mapNotNull { state -> state.tabs } + .filterChanged { + it.content + } + .ifAnyChanged { arrayOf( + it.content.url, + it.content.title, + it.content.private, + it.content.fullScreen, + it.content.progress, + it.content.loading) } + .collect { tab -> + flutterEvents.onTabContentStateChange( + TabContentState( + id = tab.id, + contextId = tab.contextId, + url = tab.content.url, + title = tab.content.title, + progress = tab.content.progress.toLong(), + isPrivate = tab.content.private, + isFullScreen = tab.content.fullScreen, + isLoading = tab.content.loading + ) + ) { _ -> } } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt index 61359d76..643c1916 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt @@ -1,8 +1,5 @@ package eu.lensai.flutter_mozilla_components -import GeckoBrowserApi -import GeckoSessionApi -import GeckoTabsApi import android.app.Activity import android.content.Context import android.content.Intent @@ -10,8 +7,17 @@ import androidx.annotation.NonNull import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl +import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl +import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl +import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature +import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi +import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi +import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi +import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi +import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents +import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -34,7 +40,7 @@ import org.mozilla.geckoview.GeckoRuntimeSettings /** * Helper class for lazily instantiating components needed by the application. */ -class Components(private val applicationContext: Context) : DefaultComponents(applicationContext) { +class Components(private val applicationContext: Context, flutterEvents: GeckoStateEvents) : DefaultComponents(applicationContext, flutterEvents) { private val runtime by lazy { // Allow for exfiltrating Gecko metrics through the Glean SDK. val builder = GeckoRuntimeSettings.Builder().aboutConfigEnabled(true) @@ -56,6 +62,7 @@ class Components(private val applicationContext: Context) : DefaultComponents(ap // } WebCompatFeature.install(it) + CookieManagerFeature.install(it) //WebCompatReporterFeature.install(it) } } @@ -77,7 +84,9 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { _flutterPluginBinding = flutterPluginBinding - GlobalComponents.setUp(flutterPluginBinding.applicationContext) + val flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger) + + GlobalComponents.setUp(flutterPluginBinding.applicationContext, flutterEvents) val intent = Intent(flutterPluginBinding.applicationContext, NotificationActivity::class.java) intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -89,6 +98,8 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl()) GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl()) + GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl()) + GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl()) } private fun showNativeFragment() { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GlobalComponents.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GlobalComponents.kt index 8788b51b..c681dd74 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GlobalComponents.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/GlobalComponents.kt @@ -1,6 +1,7 @@ package eu.lensai.flutter_mozilla_components import android.content.Context +import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -28,8 +29,8 @@ object GlobalComponents { .whenSessionsChange() } - fun setUp(applicationContext: Context) { - val newComponents = Components(applicationContext) + fun setUp(applicationContext: Context, flutterEvents: GeckoStateEvents) { + val newComponents = Components(applicationContext, flutterEvents) newComponents.crashReporter.install(applicationContext) Facts.registerProcessor(LogFactProcessor()) 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 9104f1b7..8598fa41 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,10 +1,8 @@ package eu.lensai.flutter_mozilla_components.api -import GeckoBrowserApi +import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi -class GeckoBrowserApiImpl( - private val showFragmentCallback: () -> Unit -) : GeckoBrowserApi { +class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Unit) : GeckoBrowserApi { override fun showNativeFragment() { showFragmentCallback.invoke(); } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoCookieApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoCookieApiImpl.kt new file mode 100644 index 00000000..03140651 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoCookieApiImpl.kt @@ -0,0 +1,309 @@ +package eu.lensai.flutter_mozilla_components.api + +import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature +import eu.lensai.flutter_mozilla_components.feature.ResultConsumer +import eu.lensai.flutter_mozilla_components.pigeons.Cookie +import eu.lensai.flutter_mozilla_components.pigeons.CookiePartitionKey +import eu.lensai.flutter_mozilla_components.pigeons.CookieSameSiteStatus +import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi +import org.json.JSONObject + +class GeckoCookieApiImpl : GeckoCookieApi { + private fun toValueOrNull(json: JSONObject, key: String): Any? { + if (!json.has(key)) { + throw RuntimeException("Invalid map key") + } + + if (!json.isNull(key)) { + return json.get(key) + } + + return null + } + + private fun cookiePartitionKeyFromJSON(inputJSON: JSONObject): CookiePartitionKey { + return CookiePartitionKey( + topLevelSite = inputJSON.getString("topLevelSite") + ) + } + + private fun CookiePartitionKey.toJSON(): JSONObject { + val json = JSONObject() + json.put("topLevelSite", topLevelSite) + return json + } + + private fun cookieFromJSON(inputJSON: JSONObject): Cookie { + val partitionKeyRaw = toValueOrNull(inputJSON, "partitionKey") as JSONObject? + val partitionKey = if (partitionKeyRaw == null || partitionKeyRaw.length() == 0) null + else cookiePartitionKeyFromJSON(partitionKeyRaw) + + return Cookie ( + domain = inputJSON.getString("domain"), + expirationDate = (toValueOrNull(inputJSON, "expirationDate") as Int?)?.toLong(), + firstPartyDomain = inputJSON.getString("firstPartyDomain"), + hostOnly = inputJSON.getBoolean("hostOnly"), + httpOnly = inputJSON.getBoolean("httpOnly"), + name = inputJSON.getString("name"), + partitionKey = partitionKey, + path = inputJSON.getString("path"), + secure = inputJSON.getBoolean("secure"), + session = inputJSON.getBoolean("session"), + sameSite = when(inputJSON.getString("sameSite")) { + "no_restriction" -> CookieSameSiteStatus.NO_RESTRICTION + "lax" -> CookieSameSiteStatus.LAX + "strict" -> CookieSameSiteStatus.STRICT + else -> CookieSameSiteStatus.UNSPECIFIED + }, + storeId = inputJSON.getString("storeId"), + value = inputJSON.getString("value") + ) + } + + override fun getCookie( + firstPartyDomain: String?, + name: String, + partitionKey: CookiePartitionKey?, + storeId: String?, + url: String, + callback: (Result) -> Unit + ) { + val args = JSONObject() + + if (firstPartyDomain == null) { + args.put("firstPartyDomain", JSONObject.NULL) + } else { + args.put("firstPartyDomain", firstPartyDomain) + } + args.put("name", name) + + if (partitionKey == null) { + args.put("partitionKey", JSONObject.NULL) + } else { + args.put("partitionKey", partitionKey.toJSON()) + } + + if (storeId == null) { + args.put("storeId", JSONObject.NULL) + } else { + args.put("storeId", storeId) + } + + args.put("url", url) + + CookieManagerFeature.scheduleRequest("get", args, object: ResultConsumer { + override fun success(result: JSONObject) { + callback(Result.success(cookieFromJSON(result.getJSONObject("result")))) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) + } + + }) + } + + override fun getAllCookies( + domain: String?, + firstPartyDomain: String?, + name: String?, + partitionKey: CookiePartitionKey?, + storeId: String?, + url: String, + callback: (Result>) -> Unit + ) { + val args = JSONObject() + + if (domain == null) { + args.put("domain", JSONObject.NULL) + } else { + args.put("domain", domain) + } + + if (firstPartyDomain == null) { + args.put("firstPartyDomain", JSONObject.NULL) + } else { + args.put("firstPartyDomain", firstPartyDomain) + } + + args.put("name", name) + + if (partitionKey == null) { + args.put("partitionKey", JSONObject.NULL) + } else { + args.put("partitionKey", partitionKey.toJSON()) + } + + if (storeId == null) { + args.put("storeId", JSONObject.NULL) + } else { + args.put("storeId", storeId) + } + + args.put("url", url) + + CookieManagerFeature.scheduleRequest("getAll", args, object: ResultConsumer { + override fun success(result: JSONObject) { + val jsonArray = result.getJSONArray("result") + val cookies: MutableList = mutableListOf() + + repeat(jsonArray.length()) { + index -> + cookies.add(cookieFromJSON(jsonArray.getJSONObject(index))) + } + + callback(Result.success(cookies)) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) + } + + }) + } + + override fun setCookie( + domain: String?, + expirationDate: Long?, + firstPartyDomain: String?, + httpOnly: Boolean?, + name: String?, + partitionKey: CookiePartitionKey?, + path: String?, + sameSite: CookieSameSiteStatus?, + secure: Boolean?, + storeId: String?, + url: String, + value: String?, + callback: (Result) -> Unit + ) { + val args = JSONObject() + + if (domain == null) { + args.put("domain", JSONObject.NULL) + } else { + args.put("domain", domain) + } + + if (expirationDate == null) { + args.put("expirationDate", JSONObject.NULL) + } else { + args.put("expirationDate", domain) + } + + if (firstPartyDomain == null) { + args.put("firstPartyDomain", JSONObject.NULL) + } else { + args.put("firstPartyDomain", firstPartyDomain) + } + + if (httpOnly == null) { + args.put("httpOnly", JSONObject.NULL) + } else { + args.put("httpOnly", httpOnly) + } + + if (name == null) { + args.put("name", JSONObject.NULL) + } else { + args.put("name", name) + } + + if (partitionKey == null) { + args.put("partitionKey", JSONObject.NULL) + } else { + args.put("partitionKey", partitionKey.toJSON()) + } + + if (path == null) { + args.put("path", JSONObject.NULL) + } else { + args.put("path", path) + } + + if (sameSite == null) { + args.put("sameSite", JSONObject.NULL) + } else { + args.put("sameSite", when(sameSite) { + CookieSameSiteStatus.NO_RESTRICTION -> "no_restriction" + CookieSameSiteStatus.LAX -> "lax" + CookieSameSiteStatus.STRICT -> "strict" + CookieSameSiteStatus.UNSPECIFIED -> "" + }) + } + + if (secure == null) { + args.put("secure", JSONObject.NULL) + } else { + args.put("secure", secure) + } + + if (storeId == null) { + args.put("storeId", JSONObject.NULL) + } else { + args.put("storeId", storeId) + } + + args.put("url", url) + + if (value == null) { + args.put("value", JSONObject.NULL) + } else { + args.put("value", value) + } + + CookieManagerFeature.scheduleRequest("set", args, object: ResultConsumer { + override fun success(result: JSONObject) { + callback(Result.success(Unit)) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) + } + }) + } + + override fun removeCookie( + firstPartyDomain: String?, + name: String, + partitionKey: CookiePartitionKey?, + storeId: String?, + url: String, + callback: (Result) -> Unit + ) { + val args = JSONObject() + + if (firstPartyDomain == null) { + args.put("firstPartyDomain", JSONObject.NULL) + } else { + args.put("firstPartyDomain", firstPartyDomain) + } + + args.put("name", name) + + if (partitionKey == null) { + args.put("partitionKey", JSONObject.NULL) + } else { + args.put("partitionKey", partitionKey.toJSON()) + } + + if (storeId == null) { + args.put("storeId", JSONObject.NULL) + } else { + args.put("storeId", storeId) + } + + args.put("url", url) + + CookieManagerFeature.scheduleRequest("remove", args, object: ResultConsumer { + override fun success(result: JSONObject) { + callback(Result.success(Unit)) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) + } + }) + } + +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoIconsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoIconsApiImpl.kt new file mode 100644 index 00000000..929f4c44 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoIconsApiImpl.kt @@ -0,0 +1,96 @@ +package eu.lensai.flutter_mozilla_components.api + +import android.graphics.Bitmap +import android.os.Build +import eu.lensai.flutter_mozilla_components.GlobalComponents +import eu.lensai.flutter_mozilla_components.ext.toWebPBytes +import eu.lensai.flutter_mozilla_components.pigeons.* +import kotlinx.coroutines.* +import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.icons.Icon +import mozilla.components.concept.engine.manifest.Size as HtmlSize +import java.io.ByteArrayOutputStream + +typealias MozillaIconRequest = mozilla.components.browser.icons.IconRequest +typealias MozillaIconSize = mozilla.components.browser.icons.IconRequest.Size +typealias MozillaIconResource = mozilla.components.browser.icons.IconRequest.Resource +typealias MozillaIconResourceType = mozilla.components.browser.icons.IconRequest.Resource.Type + +class GeckoIconsApiImpl() : GeckoIconsApi { + private val icons: BrowserIcons by lazy { GlobalComponents.components!!.icons } + + override fun loadIcon(request: IconRequest, callback: (Result) -> Unit) { + CoroutineScope(Dispatchers.Default).launch { + runCatching { + loadIconAsync(request.toMozillaIconRequest()) + }.fold( + onSuccess = { result -> + withContext(Dispatchers.Main) { + callback(Result.success(result)) + } + }, + onFailure = { error -> + withContext(Dispatchers.Main) { + callback(Result.failure(error)) + } + } + ) + } + } + + private suspend fun loadIconAsync(request: MozillaIconRequest): IconResult { + val result = icons.loadIcon(request).await() + val imageBytes = result.bitmap.toWebPBytes() + return IconResult( + image = imageBytes, + maskable = result.maskable, + color = result.color?.toLong(), + source = result.source.toApiSource() + ) + } + + private fun IconRequest.toMozillaIconRequest(): MozillaIconRequest = + MozillaIconRequest( + url = url, + size = size.toMozillaSize(), + color = color?.toInt(), + waitOnNetworkLoad = waitOnNetworkLoad, + isPrivate = isPrivate, + resources = resources.filterNotNull().map { it.toMozillaResource() } + ) + + private fun IconSize.toMozillaSize(): MozillaIconSize = when (this) { + IconSize.DEFAULT_SIZE -> MozillaIconSize.DEFAULT + IconSize.LAUNCHER -> MozillaIconSize.LAUNCHER + IconSize.LAUNCHER_ADAPTIVE -> MozillaIconSize.LAUNCHER_ADAPTIVE + } + + private fun Resource.toMozillaResource(): MozillaIconResource = + MozillaIconResource( + url = url, + mimeType = mimeType, + maskable = maskable, + type = type.toMozillaType(), + sizes = sizes.filterNotNull().map { HtmlSize(it.height.toInt(), it.width.toInt()) } + ) + + private fun IconType.toMozillaType(): MozillaIconResourceType = when (this) { + IconType.FAVICON -> MozillaIconResourceType.FAVICON + IconType.APPLE_TOUCH_ICON -> MozillaIconResourceType.APPLE_TOUCH_ICON + IconType.FLUID_ICON -> MozillaIconResourceType.FLUID_ICON + IconType.IMAGE_SRC -> MozillaIconResourceType.IMAGE_SRC + IconType.OPEN_GRAPH -> MozillaIconResourceType.OPENGRAPH + IconType.TWITTER -> MozillaIconResourceType.TWITTER + IconType.MICROSOFT_TILE -> MozillaIconResourceType.MICROSOFT_TILE + IconType.TIPPY_TOP -> MozillaIconResourceType.TIPPY_TOP + IconType.MANIFEST_ICON -> MozillaIconResourceType.MANIFEST_ICON + } + + private fun Icon.Source.toApiSource(): Source = when (this) { + Icon.Source.GENERATOR -> Source.GENERATOR + Icon.Source.DOWNLOAD -> Source.DOWNLOAD + Icon.Source.INLINE -> Source.INLINE + Icon.Source.MEMORY -> Source.MEMORY + Icon.Source.DISK -> Source.DISK + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSessionApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSessionApiImpl.kt index 9f4a96d7..aee10ea6 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSessionApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoSessionApiImpl.kt @@ -1,16 +1,16 @@ package eu.lensai.flutter_mozilla_components.api -import GeckoSessionApi -import LoadUrlFlagsValue import android.util.Log import eu.lensai.flutter_mozilla_components.GlobalComponents +import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi +import eu.lensai.flutter_mozilla_components.pigeons.LoadUrlFlagsValue +import eu.lensai.flutter_mozilla_components.pigeons.TranslationOptions as PigeonTranslationOptions import mozilla.components.browser.state.state.BrowserState import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.translate.TranslationOptions -import TranslationOptions as PigeonTranslationOptions import mozilla.components.feature.session.SessionUseCases -class GeckoSessionApiImpl() : GeckoSessionApi { +class GeckoSessionApiImpl : GeckoSessionApi { private val sessionUseCases: SessionUseCases by lazy { GlobalComponents.components!!.sessionUseCases } private val state: BrowserState by lazy { GlobalComponents.components!!.store.state } @@ -33,21 +33,19 @@ class GeckoSessionApiImpl() : GeckoSessionApi { data = data, tabId = tabId ?: state.selectedTabId, mimeType = mimeType, - encoding = encoding, + encoding = encoding ) } override fun reload(tabId: String?, flags: LoadUrlFlagsValue) { sessionUseCases.reload( tabId = tabId ?: state.selectedTabId, - flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()), + flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()) ) } override fun stopLoading(tabId: String?) { - sessionUseCases.stopLoading( - tabId = tabId - ) + sessionUseCases.stopLoading(tabId = tabId ?: state.selectedTabId) } override fun goBack(tabId: String?, userInteraction: Boolean) { @@ -74,26 +72,20 @@ class GeckoSessionApiImpl() : GeckoSessionApi { override fun requestDesktopSite(tabId: String?, enable: Boolean) { sessionUseCases.requestDesktopSite( tabId = tabId ?: state.selectedTabId, - enable = enable, + enable = enable ) } override fun exitFullscreen(tabId: String?) { - sessionUseCases.exitFullscreen( - tabId = tabId ?: state.selectedTabId, - ) + sessionUseCases.exitFullscreen(tabId = tabId ?: state.selectedTabId) } override fun saveToPdf(tabId: String?) { - sessionUseCases.saveToPdf( - tabId = tabId ?: state.selectedTabId, - ) + sessionUseCases.saveToPdf(tabId = tabId ?: state.selectedTabId) } override fun printContent(tabId: String?) { - sessionUseCases.printContent( - tabId = tabId ?: state.selectedTabId, - ) + sessionUseCases.printContent(tabId = tabId ?: state.selectedTabId) } override fun translate( @@ -106,18 +98,16 @@ class GeckoSessionApiImpl() : GeckoSessionApi { tabId = tabId ?: state.selectedTabId, fromLanguage = fromLanguage, toLanguage = toLanguage, - options = if (options != null) TranslationOptions(downloadModel = options.downloadModel) else null, + options = options?.let { TranslationOptions(downloadModel = it.downloadModel) } ) } override fun translateRestore(tabId: String?) { - sessionUseCases.translateRestore( - tabId = tabId ?: state.selectedTabId, - ) + sessionUseCases.translateRestore(tabId = tabId ?: state.selectedTabId) } override fun crashRecovery(tabIds: List?) { - if(tabIds != null) { + if (tabIds != null) { sessionUseCases.crashRecovery.invoke(tabIds = tabIds) } else { sessionUseCases.crashRecovery.invoke() @@ -134,4 +124,4 @@ class GeckoSessionApiImpl() : GeckoSessionApi { lastAccess = lastAccess ?: System.currentTimeMillis() ) } -} \ 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 9b97cb02..e6c68991 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 @@ -1,13 +1,14 @@ package eu.lensai.flutter_mozilla_components.api -import GeckoTabsApi -import HistoryMetadataKey as PigeonHistoryMetadataKey -import LoadUrlFlagsValue -import RecoverableBrowserState as PigeonRecoverableBrowserState -import RestoreLocation as PigeonRestoreLocation -import RecoverableTab as PigeonRecoverableTab -import SourceValue +import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi +import eu.lensai.flutter_mozilla_components.pigeons.HistoryMetadataKey as PigeonHistoryMetadataKey +import eu.lensai.flutter_mozilla_components.pigeons.LoadUrlFlagsValue +import eu.lensai.flutter_mozilla_components.pigeons.RecoverableBrowserState as PigeonRecoverableBrowserState +import eu.lensai.flutter_mozilla_components.pigeons.RestoreLocation as PigeonRestoreLocation +import eu.lensai.flutter_mozilla_components.pigeons.RecoverableTab as PigeonRecoverableTab +import eu.lensai.flutter_mozilla_components.pigeons.SourceValue import eu.lensai.flutter_mozilla_components.GlobalComponents +import eu.lensai.flutter_mozilla_components.pigeons.RestoreLocation import mozilla.components.browser.session.storage.RecoverableBrowserState import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.selector.findTab @@ -80,7 +81,11 @@ class GeckoTabsApiImpl() : GeckoTabsApi { } private fun mapRestoreLocation(t: PigeonRestoreLocation) : TabListAction.RestoreAction.RestoreLocation { - return TabListAction.RestoreAction.RestoreLocation.entries.first { it.ordinal == t.ordinal } + return when(t) { + RestoreLocation.BEGINNING -> TabListAction.RestoreAction.RestoreLocation.BEGINNING + RestoreLocation.END -> TabListAction.RestoreAction.RestoreLocation.END + RestoreLocation.AT_INDEX -> TabListAction.RestoreAction.RestoreLocation.AT_INDEX + } } override fun selectTab(tabId: String) { 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 new file mode 100644 index 00000000..96ff31a0 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/ext/Bitmap.kt @@ -0,0 +1,17 @@ +package eu.lensai.flutter_mozilla_components.ext + +import android.graphics.Bitmap +import android.os.Build +import java.io.ByteArrayOutputStream + +fun Bitmap.toWebPBytes(): ByteArray { + val stream = ByteArrayOutputStream() + val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Bitmap.CompressFormat.WEBP_LOSSLESS + } else { + @Suppress("DEPRECATION") + Bitmap.CompressFormat.WEBP + } + compress(compressFormat, 100, stream) + return stream.toByteArray() +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/CookieManagerFeature.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/CookieManagerFeature.kt new file mode 100644 index 00000000..68ce55f0 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/feature/CookieManagerFeature.kt @@ -0,0 +1,109 @@ +package eu.lensai.flutter_mozilla_components.feature + +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import mozilla.components.concept.engine.webextension.MessageHandler +import mozilla.components.concept.engine.webextension.Port +import mozilla.components.concept.engine.webextension.WebExtensionRuntime +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.webextensions.WebExtensionController +import org.json.JSONObject + +interface ResultConsumer { + fun success(result: T) + fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) +} + +/** + * A feature that enables users to report site issues to Mozilla's Web Compatibility team for + * further diagnosis. + */ +object CookieManagerFeature { + private val logger = Logger("cookie-manager") + + private const val COOKIE_MANAGER_REPORTER_EXTENSION_ID = "cookie-manager@lensai.eu" + private const val COOKIE_MANAGER_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/cookie-manager/" + private const val COOKIE_MANAGER_REPORTER_MESSAGING_ID = "cookieManager" + + private var nextRequestId: Int = 0 + private val requestHandlers = HashMap>() + private val mutex = Mutex() + + @VisibleForTesting + // This is an internal var to make it mutable for unit testing purposes only + internal var extensionController = WebExtensionController( + COOKIE_MANAGER_REPORTER_EXTENSION_ID, + COOKIE_MANAGER_REPORTER_EXTENSION_URL, + COOKIE_MANAGER_REPORTER_MESSAGING_ID, + ) + + fun scheduleRequest(command: String, args: JSONObject, callback: ResultConsumer) { + val message = JSONObject() + message.put("action", command); + message.put("args", args) + + runBlocking { + withContext(Dispatchers.Default) { + mutex.withLock { + message.put("id", nextRequestId) + + requestHandlers[nextRequestId] = callback + + nextRequestId += 1 + + extensionController.sendBackgroundMessage(message) + } + } + } + } + + private class CookieManagerReporterBackgroundMessageHandler() : MessageHandler { + override fun onPortMessage(message: Any, port: Port) { + runBlocking { + withContext(Dispatchers.Default) { + mutex.withLock { + val messageJSON = message as JSONObject; + + val requestId = messageJSON.getInt("id") + val status = messageJSON.getString("status") + if (status == "success") { + requestHandlers[requestId]?.success(message) + } else { + requestHandlers[requestId]?.error( + "Cookie Manager", + "Failed to perform operation", + message.getString("error") + ) + } + } + } + } + } + } + + /** + * Installs the web extension in the runtime through the WebExtensionRuntime install method + * + * @param runtime a WebExtensionRuntime. + * @param productName a custom product name used to automatically label reports. Defaults to + * "android-components". + */ + fun install(runtime: WebExtensionRuntime) { + extensionController.registerBackgroundMessageHandler( + CookieManagerReporterBackgroundMessageHandler(), + ) + extensionController.install( + runtime, + onSuccess = { + logger.debug("Installed CookieManager webextension: ${it.id}") + }, + onError = { throwable -> + logger.error("Failed to install CookieManager webextension: ", throwable) + }, + ) + } +} 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 33e552e2..68519ed6 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 @@ -3,18 +3,24 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package eu.lensai.flutter_mozilla_components.middleware +import android.graphics.Bitmap import android.util.Log +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.state.BrowserState import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext +import java.io.ByteArrayOutputStream + + /** * [Middleware] implementation for handling [ContentAction.UpdateThumbnailAction] and storing * the thumbnail to the disk cache. */ -class FlutterEventMiddleware() : Middleware { +class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Middleware { @Suppress("ComplexMethod") override fun invoke( context: MiddlewareContext, @@ -23,10 +29,8 @@ class FlutterEventMiddleware() : Middleware { ) { when (action) { is ContentAction.UpdateThumbnailAction -> { - Log.d("UpdateThumbnailAction", action.sessionId) - } - is ContentAction.UpdateTitleAction -> { - Log.d("UpdateTitleAction", action.title) + val bytes = action.thumbnail.toWebPBytes() + flutterEvents.onThumbnailChange(action.sessionId, bytes) { _ -> } } else -> { // no-op 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 e28aa834..2fd48e4f 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 @@ -2,6 +2,7 @@ // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") +package eu.lensai.flutter_mozilla_components.pigeons import android.util.Log import io.flutter.plugin.common.BasicMessageChannel @@ -31,6 +32,9 @@ private fun wrapError(exception: Throwable): List { } } +private fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "")} + /** * Error class for passing custom error details to Flutter via a thrown PlatformException. * @property code The error code. @@ -59,6 +63,75 @@ enum class RestoreLocation(val raw: Int) { } } +/** An icon resource type. */ +enum class IconType(val raw: Int) { + FAVICON(0), + APPLE_TOUCH_ICON(1), + FLUID_ICON(2), + IMAGE_SRC(3), + OPEN_GRAPH(4), + TWITTER(5), + MICROSOFT_TILE(6), + TIPPY_TOP(7), + MANIFEST_ICON(8); + + companion object { + fun ofRaw(raw: Int): IconType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * Supported sizes. + * + * We are trying to limit the supported sizes in order to optimize our caching strategy. + */ +enum class IconSize(val raw: Int) { + DEFAULT_SIZE(0), + LAUNCHER(1), + LAUNCHER_ADAPTIVE(2); + + companion object { + fun ofRaw(raw: Int): IconSize? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** The source of an [Icon]. */ +enum class IconSource(val raw: Int) { + /** This icon was generated. */ + GENERATOR(0), + /** This icon was downloaded. */ + DOWNLOAD(1), + /** This icon was inlined in the document. */ + INLINE(2), + /** This icon was loaded from an in-memory cache. */ + MEMORY(3), + /** This icon was loaded from a disk cache. */ + DISK(4); + + companion object { + fun ofRaw(raw: Int): IconSource? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class CookieSameSiteStatus(val raw: Int) { + NO_RESTRICTION(0), + LAX(1), + STRICT(2), + UNSPECIFIED(3); + + companion object { + fun ofRaw(raw: Int): CookieSameSiteStatus? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** * Translation options that map to the Gecko Translations Options. * @@ -150,7 +223,7 @@ data class ReaderState ( */ data class LastMediaAccessState ( /** - * [ContentState.url] when media started playing. + * [TabContentState.url] when media started playing. * This is not the URL of the media but of the page when media started. * Defaults to "" (an empty String) if media hasn't started playing. * This value is only updated when media starts playing. @@ -354,7 +427,7 @@ data class TabState ( val private: Boolean, /** The last [HistoryMetadataKey] of the tab. */ val historyMetadata: HistoryMetadataKey? = null, - /** The last [Source] of the tab. */ + /** The last [IconSource] of the tab. */ val source: SourceValue, /** The index the tab should be restored at. */ val index: Long, @@ -456,6 +529,342 @@ data class RecoverableBrowserState ( ) } } + +/** + * A request to load an [Icon]. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class IconRequest ( + val url: String, + val size: IconSize, + val resources: List, + val color: Long? = null, + val isPrivate: Boolean, + val waitOnNetworkLoad: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): IconRequest { + val url = pigeonVar_list[0] as String + val size = pigeonVar_list[1] as IconSize + val resources = pigeonVar_list[2] as List + val color = pigeonVar_list[3] as Long? + val isPrivate = pigeonVar_list[4] as Boolean + val waitOnNetworkLoad = pigeonVar_list[5] as Boolean + return IconRequest(url, size, resources, color, isPrivate, waitOnNetworkLoad) + } + } + fun toList(): List { + return listOf( + url, + size, + resources, + color, + isPrivate, + waitOnNetworkLoad, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class ResourceSize ( + val height: Long, + val width: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): ResourceSize { + val height = pigeonVar_list[0] as Long + val width = pigeonVar_list[1] as Long + return ResourceSize(height, width) + } + } + fun toList(): List { + return listOf( + height, + width, + ) + } +} + +/** + * An icon resource that can be loaded. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class Resource ( + val url: String, + val type: IconType, + val sizes: List, + val mimeType: String? = null, + val maskable: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): Resource { + val url = pigeonVar_list[0] as String + val type = pigeonVar_list[1] as IconType + val sizes = pigeonVar_list[2] as List + val mimeType = pigeonVar_list[3] as String? + val maskable = pigeonVar_list[4] as Boolean + return Resource(url, type, sizes, mimeType, maskable) + } + } + fun toList(): List { + return listOf( + url, + type, + sizes, + mimeType, + maskable, + ) + } +} + +/** + * An [Icon] returned by [BrowserIcons] after processing an [IconRequest] + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class IconResult ( + /** The loaded icon as an [Uint8List]. */ + val image: ByteArray, + /** The dominant color of the icon. Will be null if no color could be extracted. */ + val color: Long? = null, + /** The source of the icon. */ + val source: IconSource, + /** True if the icon represents as full-bleed icon that can be cropped to other shapes. */ + val maskable: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): IconResult { + val image = pigeonVar_list[0] as ByteArray + val color = pigeonVar_list[1] as Long? + val source = pigeonVar_list[2] as IconSource + val maskable = pigeonVar_list[3] as Boolean + return IconResult(image, color, source, maskable) + } + } + fun toList(): List { + return listOf( + image, + color, + source, + maskable, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class CookiePartitionKey ( + val topLevelSite: String +) + { + companion object { + fun fromList(pigeonVar_list: List): CookiePartitionKey { + val topLevelSite = pigeonVar_list[0] as String + return CookiePartitionKey(topLevelSite) + } + } + fun toList(): List { + return listOf( + topLevelSite, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class Cookie ( + val domain: String, + val expirationDate: Long? = null, + val firstPartyDomain: String, + val hostOnly: Boolean, + val httpOnly: Boolean, + val name: String, + val partitionKey: CookiePartitionKey? = null, + val path: String, + val secure: Boolean, + val session: Boolean, + val sameSite: CookieSameSiteStatus, + val storeId: String, + val value: String +) + { + companion object { + fun fromList(pigeonVar_list: List): Cookie { + val domain = pigeonVar_list[0] as String + val expirationDate = pigeonVar_list[1] as Long? + val firstPartyDomain = pigeonVar_list[2] as String + val hostOnly = pigeonVar_list[3] as Boolean + val httpOnly = pigeonVar_list[4] as Boolean + val name = pigeonVar_list[5] as String + val partitionKey = pigeonVar_list[6] as CookiePartitionKey? + val path = pigeonVar_list[7] as String + val secure = pigeonVar_list[8] as Boolean + val session = pigeonVar_list[9] as Boolean + val sameSite = pigeonVar_list[10] as CookieSameSiteStatus + val storeId = pigeonVar_list[11] as String + val value = pigeonVar_list[12] as String + return Cookie(domain, expirationDate, firstPartyDomain, hostOnly, httpOnly, name, partitionKey, path, secure, session, sameSite, storeId, value) + } + } + fun toList(): List { + return listOf( + domain, + expirationDate, + firstPartyDomain, + hostOnly, + httpOnly, + name, + partitionKey, + path, + secure, + session, + sameSite, + storeId, + value, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class HistoryItem ( + val url: String, + val title: String +) + { + companion object { + fun fromList(pigeonVar_list: List): HistoryItem { + val url = pigeonVar_list[0] as String + val title = pigeonVar_list[1] as String + return HistoryItem(url, title) + } + } + fun toList(): List { + return listOf( + url, + title, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class HistoryState ( + val items: List, + val currentIndex: Long, + val canGoBack: Boolean, + val canGoForward: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): HistoryState { + val items = pigeonVar_list[0] as List + val currentIndex = pigeonVar_list[1] as Long + val canGoBack = pigeonVar_list[2] as Boolean + val canGoForward = pigeonVar_list[3] as Boolean + return HistoryState(items, currentIndex, canGoBack, canGoForward) + } + } + fun toList(): List { + return listOf( + items, + currentIndex, + canGoBack, + canGoForward, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class ReaderableState ( + /** + * Whether or not the current page can be transformed to + * be displayed in a reader view. + */ + val readerable: Boolean, + /** Whether or not reader view is active. */ + val active: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): ReaderableState { + val readerable = pigeonVar_list[0] as Boolean + val active = pigeonVar_list[1] as Boolean + return ReaderableState(readerable, active) + } + } + fun toList(): List { + return listOf( + readerable, + active, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SecurityInfoState ( + val secure: Boolean, + val host: String, + val issuer: String +) + { + companion object { + fun fromList(pigeonVar_list: List): SecurityInfoState { + val secure = pigeonVar_list[0] as Boolean + val host = pigeonVar_list[1] as String + val issuer = pigeonVar_list[2] as String + return SecurityInfoState(secure, host, issuer) + } + } + fun toList(): List { + return listOf( + secure, + host, + issuer, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class TabContentState ( + val id: String, + val contextId: String? = null, + val url: String, + val title: String, + val progress: Long, + val isPrivate: Boolean, + val isFullScreen: Boolean, + val isLoading: Boolean +) + { + companion object { + fun fromList(pigeonVar_list: List): TabContentState { + val id = pigeonVar_list[0] as String + val contextId = pigeonVar_list[1] as String? + val url = pigeonVar_list[2] as String + val title = pigeonVar_list[3] as String + val progress = pigeonVar_list[4] as Long + val isPrivate = pigeonVar_list[5] as Boolean + val isFullScreen = pigeonVar_list[6] as Boolean + val isLoading = pigeonVar_list[7] as Boolean + return TabContentState(id, contextId, url, title, progress, isPrivate, isFullScreen, isLoading) + } + } + fun toList(): List { + return listOf( + id, + contextId, + url, + title, + progress, + isPrivate, + isFullScreen, + isLoading, + ) + } +} private open class GeckoPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -465,60 +874,135 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { } } 130.toByte() -> { - return (readValue(buffer) as? List)?.let { - TranslationOptions.fromList(it) + return (readValue(buffer) as Long?)?.let { + IconType.ofRaw(it.toInt()) } } 131.toByte() -> { - return (readValue(buffer) as? List)?.let { - ReaderState.fromList(it) + return (readValue(buffer) as Long?)?.let { + IconSize.ofRaw(it.toInt()) } } 132.toByte() -> { - return (readValue(buffer) as? List)?.let { - LastMediaAccessState.fromList(it) + return (readValue(buffer) as Long?)?.let { + IconSource.ofRaw(it.toInt()) } } 133.toByte() -> { - return (readValue(buffer) as? List)?.let { - HistoryMetadataKey.fromList(it) + return (readValue(buffer) as Long?)?.let { + CookieSameSiteStatus.ofRaw(it.toInt()) } } 134.toByte() -> { return (readValue(buffer) as? List)?.let { - PackageCategoryValue.fromList(it) + TranslationOptions.fromList(it) } } 135.toByte() -> { return (readValue(buffer) as? List)?.let { - ExternalPackage.fromList(it) + ReaderState.fromList(it) } } 136.toByte() -> { return (readValue(buffer) as? List)?.let { - LoadUrlFlagsValue.fromList(it) + LastMediaAccessState.fromList(it) } } 137.toByte() -> { return (readValue(buffer) as? List)?.let { - SourceValue.fromList(it) + HistoryMetadataKey.fromList(it) } } 138.toByte() -> { return (readValue(buffer) as? List)?.let { - TabState.fromList(it) + PackageCategoryValue.fromList(it) } } 139.toByte() -> { return (readValue(buffer) as? List)?.let { - RecoverableTab.fromList(it) + ExternalPackage.fromList(it) } } 140.toByte() -> { + return (readValue(buffer) as? List)?.let { + LoadUrlFlagsValue.fromList(it) + } + } + 141.toByte() -> { + return (readValue(buffer) as? List)?.let { + SourceValue.fromList(it) + } + } + 142.toByte() -> { + return (readValue(buffer) as? List)?.let { + TabState.fromList(it) + } + } + 143.toByte() -> { + return (readValue(buffer) as? List)?.let { + RecoverableTab.fromList(it) + } + } + 144.toByte() -> { return (readValue(buffer) as? List)?.let { RecoverableBrowserState.fromList(it) } } + 145.toByte() -> { + return (readValue(buffer) as? List)?.let { + IconRequest.fromList(it) + } + } + 146.toByte() -> { + return (readValue(buffer) as? List)?.let { + ResourceSize.fromList(it) + } + } + 147.toByte() -> { + return (readValue(buffer) as? List)?.let { + Resource.fromList(it) + } + } + 148.toByte() -> { + return (readValue(buffer) as? List)?.let { + IconResult.fromList(it) + } + } + 149.toByte() -> { + return (readValue(buffer) as? List)?.let { + CookiePartitionKey.fromList(it) + } + } + 150.toByte() -> { + return (readValue(buffer) as? List)?.let { + Cookie.fromList(it) + } + } + 151.toByte() -> { + return (readValue(buffer) as? List)?.let { + HistoryItem.fromList(it) + } + } + 152.toByte() -> { + return (readValue(buffer) as? List)?.let { + HistoryState.fromList(it) + } + } + 153.toByte() -> { + return (readValue(buffer) as? List)?.let { + ReaderableState.fromList(it) + } + } + 154.toByte() -> { + return (readValue(buffer) as? List)?.let { + SecurityInfoState.fromList(it) + } + } + 155.toByte() -> { + return (readValue(buffer) as? List)?.let { + TabContentState.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -528,55 +1012,116 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { stream.write(129) writeValue(stream, value.raw) } - is TranslationOptions -> { + is IconType -> { stream.write(130) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is ReaderState -> { + is IconSize -> { stream.write(131) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is LastMediaAccessState -> { + is IconSource -> { stream.write(132) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is HistoryMetadataKey -> { + is CookieSameSiteStatus -> { stream.write(133) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is PackageCategoryValue -> { + is TranslationOptions -> { stream.write(134) writeValue(stream, value.toList()) } - is ExternalPackage -> { + is ReaderState -> { stream.write(135) writeValue(stream, value.toList()) } - is LoadUrlFlagsValue -> { + is LastMediaAccessState -> { stream.write(136) writeValue(stream, value.toList()) } - is SourceValue -> { + is HistoryMetadataKey -> { stream.write(137) writeValue(stream, value.toList()) } - is TabState -> { + is PackageCategoryValue -> { stream.write(138) writeValue(stream, value.toList()) } - is RecoverableTab -> { + is ExternalPackage -> { stream.write(139) writeValue(stream, value.toList()) } - is RecoverableBrowserState -> { + is LoadUrlFlagsValue -> { stream.write(140) writeValue(stream, value.toList()) } + is SourceValue -> { + stream.write(141) + writeValue(stream, value.toList()) + } + is TabState -> { + stream.write(142) + writeValue(stream, value.toList()) + } + is RecoverableTab -> { + stream.write(143) + writeValue(stream, value.toList()) + } + is RecoverableBrowserState -> { + stream.write(144) + writeValue(stream, value.toList()) + } + is IconRequest -> { + stream.write(145) + writeValue(stream, value.toList()) + } + is ResourceSize -> { + stream.write(146) + writeValue(stream, value.toList()) + } + is Resource -> { + stream.write(147) + writeValue(stream, value.toList()) + } + is IconResult -> { + stream.write(148) + writeValue(stream, value.toList()) + } + is CookiePartitionKey -> { + stream.write(149) + writeValue(stream, value.toList()) + } + is Cookie -> { + stream.write(150) + writeValue(stream, value.toList()) + } + is HistoryItem -> { + stream.write(151) + writeValue(stream, value.toList()) + } + is HistoryState -> { + stream.write(152) + writeValue(stream, value.toList()) + } + is ReaderableState -> { + stream.write(153) + writeValue(stream, value.toList()) + } + is SecurityInfoState -> { + stream.write(154) + writeValue(stream, value.toList()) + } + is TabContentState -> { + stream.write(155) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface GeckoBrowserApi { fun showNativeFragment() @@ -1256,3 +1801,305 @@ interface GeckoTabsApi { } } } +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface GeckoIconsApi { + fun loadIcon(request: IconRequest, callback: (Result) -> Unit) + + companion object { + /** The codec used by GeckoIconsApi. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoIconsApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoIconsApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoIconsApi.loadIcon$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestArg = args[0] as IconRequest + api.loadIcon(requestArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface GeckoCookieApi { + fun getCookie(firstPartyDomain: String?, name: String, partitionKey: CookiePartitionKey?, storeId: String?, url: String, callback: (Result) -> Unit) + fun getAllCookies(domain: String?, firstPartyDomain: String?, name: String?, partitionKey: CookiePartitionKey?, storeId: String?, url: String, callback: (Result>) -> Unit) + fun setCookie(domain: String?, expirationDate: Long?, firstPartyDomain: String?, httpOnly: Boolean?, name: String?, partitionKey: CookiePartitionKey?, path: String?, sameSite: CookieSameSiteStatus?, secure: Boolean?, storeId: String?, url: String, value: String?, callback: (Result) -> Unit) + fun removeCookie(firstPartyDomain: String?, name: String, partitionKey: CookiePartitionKey?, storeId: String?, url: String, callback: (Result) -> Unit) + + companion object { + /** The codec used by GeckoCookieApi. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoCookieApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoCookieApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.getCookie$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val firstPartyDomainArg = args[0] as String? + val nameArg = args[1] as String + val partitionKeyArg = args[2] as CookiePartitionKey? + val storeIdArg = args[3] as String? + val urlArg = args[4] as String + api.getCookie(firstPartyDomainArg, nameArg, partitionKeyArg, storeIdArg, urlArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.getAllCookies$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val domainArg = args[0] as String? + val firstPartyDomainArg = args[1] as String? + val nameArg = args[2] as String? + val partitionKeyArg = args[3] as CookiePartitionKey? + val storeIdArg = args[4] as String? + val urlArg = args[5] as String + api.getAllCookies(domainArg, firstPartyDomainArg, nameArg, partitionKeyArg, storeIdArg, urlArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.setCookie$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val domainArg = args[0] as String? + val expirationDateArg = args[1] as Long? + val firstPartyDomainArg = args[2] as String? + val httpOnlyArg = args[3] as Boolean? + val nameArg = args[4] as String? + val partitionKeyArg = args[5] as CookiePartitionKey? + val pathArg = args[6] as String? + val sameSiteArg = args[7] as CookieSameSiteStatus? + val secureArg = args[8] as Boolean? + val storeIdArg = args[9] as String? + val urlArg = args[10] as String + val valueArg = args[11] as String? + api.setCookie(domainArg, expirationDateArg, firstPartyDomainArg, httpOnlyArg, nameArg, partitionKeyArg, pathArg, sameSiteArg, secureArg, storeIdArg, urlArg, valueArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.removeCookie$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val firstPartyDomainArg = args[0] as String? + val nameArg = args[1] as String + val partitionKeyArg = args[2] as CookiePartitionKey? + val storeIdArg = args[3] as String? + val urlArg = args[4] as String + api.removeCookie(firstPartyDomainArg, nameArg, partitionKeyArg, storeIdArg, urlArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ +class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by GeckoStateEvents. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + } + fun onTabListChange(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)) { + 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 onSelectedTabChange(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)) { + 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 onTabContentStateChange(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)) { + 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 onHistoryStateChange(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)) { + 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 onReaderableStateChange(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)) { + 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 onSecurityInfoStateChange(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)) { + 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 onIconChange(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)) { + 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 onThumbnailChange(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)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } +} diff --git a/packages/flutter_mozilla_components/example/lib/main.dart b/packages/flutter_mozilla_components/example/lib/main.dart index d18dcbe2..915ce4cc 100644 --- a/packages/flutter_mozilla_components/example/lib/main.dart +++ b/packages/flutter_mozilla_components/example/lib/main.dart @@ -29,13 +29,13 @@ class MyAppState extends State { menuChildren: [ MenuItemButton( onPressed: () async { - await GeckoSessionService.forCurrentTab().reload(); + await GeckoSessionService.forActiveTab().reload(); }, child: const Text('Reload'), ), MenuItemButton( onPressed: () async { - await GeckoSessionService.forCurrentTab().goBack(); + await GeckoSessionService.forActiveTab().goBack(); }, child: const Text('Back'), ) diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart index 441dfae6..347d6b1b 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -1,4 +1,19 @@ export 'src/geckoview_widget.dart'; //export 'src/domain/services/gecko_browser.dart'; +export 'src/data/models/load_url_flags.dart'; +export 'src/data/models/source.dart'; export 'src/domain/services/gecko_session.dart'; -export 'src/domain/services/gecko_tabs.dart'; +export 'src/domain/services/gecko_tab.dart'; +export 'src/domain/services/gecko_icon.dart'; +export 'src/domain/services/gecko_cookie.dart'; +export 'src/domain/services/gecko_event.dart'; +export 'src/pigeons/gecko.g.dart' + show + Resource, + IconType, + ResourceSize, + IconSource, + CookieSameSiteStatus, + TabContentState, + SecurityInfoState, + HistoryMetadataKey; diff --git a/packages/flutter_mozilla_components/lib/src/data/models/source.dart b/packages/flutter_mozilla_components/lib/src/data/models/source.dart index bfff01c9..89ebe544 100644 --- a/packages/flutter_mozilla_components/lib/src/data/models/source.dart +++ b/packages/flutter_mozilla_components/lib/src/data/models/source.dart @@ -1,4 +1,3 @@ -import 'package:flutter_mozilla_components/src/helper/helper.dart'; import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; /// Describes a category of an external package. @@ -16,6 +15,13 @@ enum PackageCategory { final int id; const PackageCategory(this.id); + factory PackageCategory.fromInt(int? id) { + return PackageCategory.values.firstWhere( + (category) => category.id == id, + orElse: () => PackageCategory.unknown, + ); + } + PackageCategoryValue toValue() { return PackageCategoryValue(value: id); } @@ -36,7 +42,7 @@ abstract class Source { final caller = packageId != null ? ExternalPackage( packageId: packageId, - category: packageCategoryfromInt(packageCategory).toValue()) + category: PackageCategory.fromInt(packageCategory).toValue()) : null; switch (sourceId) { 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 c3a8dc34..53978b40 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 @@ -1,9 +1,11 @@ import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; +final _apiInstance = GeckoBrowserApi(); + class GeckoBrowserService { final GeckoBrowserApi _api; - GeckoBrowserService({GeckoBrowserApi? api}) : _api = api ?? GeckoBrowserApi(); + GeckoBrowserService({GeckoBrowserApi? api}) : _api = api ?? _apiInstance; Future showNativeFragment() { return _api.showNativeFragment(); diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_cookie.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_cookie.dart new file mode 100644 index 00000000..d054027b --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_cookie.dart @@ -0,0 +1,99 @@ +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; + +final _apiInstance = GeckoCookieApi(); + +class GeckoCookieService { + final GeckoCookieApi _api; + + GeckoCookieService({GeckoCookieApi? api}) : _api = api ?? _apiInstance; + + Future getCookie({ + Uri? firstPartyDomain, + required String name, + String? partitionKey, + String? storeId, + required Uri url, + }) { + return _api.getCookie( + firstPartyDomain?.host, + name, + (partitionKey != null) + ? CookiePartitionKey(topLevelSite: partitionKey) + : null, + storeId, + url.toString(), + ); + } + + Future> getAllCookies({ + Uri? domain, + Uri? firstPartyDomain, + String? name, + String? partitionKey, + String? storeId, + required Uri url, + }) { + return _api + .getAllCookies( + domain?.host, + firstPartyDomain?.host, + name, + (partitionKey != null) + ? CookiePartitionKey(topLevelSite: partitionKey) + : null, + storeId, + url.toString(), + ) + .then((value) => value.nonNulls.toList()); + } + + Future setCookie({ + Uri? domain, + int? expirationDate, + Uri? firstPartyDomain, + bool? httpOnly, + String? name, + String? partitionKey, + String? path, + CookieSameSiteStatus? sameSite, + bool? secure, + String? storeId, + required Uri url, + String? value, + }) { + return _api.setCookie( + domain?.host, + expirationDate, + firstPartyDomain?.host, + httpOnly, + name, + (partitionKey != null) + ? CookiePartitionKey(topLevelSite: partitionKey) + : null, + path, + sameSite, + secure, + storeId, + url.toString(), + value, + ); + } + + Future removeCookie({ + Uri? firstPartyDomain, + required String name, + String? partitionKey, + String? storeId, + required Uri url, + }) { + return _api.removeCookie( + firstPartyDomain?.host, + name, + (partitionKey != null) + ? CookiePartitionKey(topLevelSite: partitionKey) + : null, + storeId, + url.toString(), + ); + } +} 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 new file mode 100644 index 00000000..2871ed97 --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_event.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; + +// Typedefs for record types +typedef HistoryEvent = ({String tabId, HistoryState history}); +typedef ReaderableEvent = ({String tabId, ReaderableState readerable}); +typedef SecurityInfoEvent = ({String tabId, SecurityInfoState securityInfo}); +typedef IconEvent = ({String tabId, Uint8List? bytes}); +typedef ThumbnailEvent = ({String tabId, Uint8List? bytes}); + +class GeckoEventService extends GeckoStateEvents { + // Stream controllers + final _tabListController = StreamController>.broadcast(); + final _selectedTabController = StreamController.broadcast(); + final _tabContentController = StreamController.broadcast(); + final _historyController = StreamController.broadcast(); + final _readerableController = StreamController.broadcast(); + final _securityInfoController = + StreamController.broadcast(); + final _iconController = StreamController.broadcast(); + final _thumbnailController = StreamController.broadcast(); + + // Event streams + Stream> get tabListEvents => _tabListController.stream; + Stream get selectedTabEvents => _selectedTabController.stream; + Stream get tabContentEvents => _tabContentController.stream; + Stream get historyEvents => _historyController.stream; + Stream get readerableEvents => _readerableController.stream; + Stream get securityInfoEvents => + _securityInfoController.stream; + Stream get iconEvents => _iconController.stream; + Stream get thumbnailEvents => _thumbnailController.stream; + + // Overridden methods + @override + void onTabListChange(List tabIds) { + _tabListController.add(tabIds.nonNulls.toList()); + } + + @override + void onSelectedTabChange(String? id) { + _selectedTabController.add(id); + } + + @override + void onTabContentStateChange(TabContentState state) { + _tabContentController.add(state); + } + + @override + void onHistoryStateChange(String id, HistoryState state) { + _historyController.add((tabId: id, history: state)); + } + + @override + void onReaderableStateChange(String id, ReaderableState state) { + _readerableController.add((tabId: id, readerable: state)); + } + + @override + void onSecurityInfoStateChange(String id, SecurityInfoState state) { + _securityInfoController.add((tabId: id, securityInfo: state)); + } + + @override + void onIconChange(String id, Uint8List? bytes) { + _iconController.add((tabId: id, bytes: bytes)); + } + + @override + void onThumbnailChange(String id, Uint8List? bytes) { + _thumbnailController.add((tabId: id, bytes: bytes)); + } + + GeckoEventService.setUp({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + GeckoStateEvents.setUp(this); + } + + void dispose() { + _tabListController.close(); + _selectedTabController.close(); + _tabContentController.close(); + _historyController.close(); + _readerableController.close(); + _securityInfoController.close(); + _iconController.close(); + _thumbnailController.close(); + } +} diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_icon.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_icon.dart new file mode 100644 index 00000000..3d9475b3 --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_icon.dart @@ -0,0 +1,20 @@ +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; + +final _apiInstance = GeckoIconsApi(); + +class GeckoIconService { + final GeckoIconsApi _api; + + GeckoIconService({GeckoIconsApi? api}) : _api = api ?? _apiInstance; + + Future loadIcon( + {required Uri url, List resources = const []}) async { + return _api.loadIcon(IconRequest( + url: url.toString(), + size: IconSize.defaultSize, + resources: resources, + isPrivate: false, + waitOnNetworkLoad: true, + )); + } +} diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_session.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_session.dart index f3a64e3e..3a94b96e 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_session.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_session.dart @@ -1,16 +1,18 @@ import 'package:flutter_mozilla_components/src/data/models/load_url_flags.dart'; import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; +final _apiInstance = GeckoSessionApi(); + class GeckoSessionService { final String? tabId; final GeckoSessionApi _api; - GeckoSessionService.forCurrentTab({GeckoSessionApi? api}) - : _api = api ?? GeckoSessionApi(), + GeckoSessionService.forActiveTab({GeckoSessionApi? api}) + : _api = api ?? _apiInstance, tabId = null; GeckoSessionService({required String this.tabId, GeckoSessionApi? api}) - : _api = api ?? GeckoSessionApi(); + : _api = api ?? _apiInstance; Future loadUrl({ required Uri url, diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tabs.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart similarity index 96% rename from packages/flutter_mozilla_components/lib/src/domain/services/gecko_tabs.dart rename to packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart index 5451467c..3dfe9ff7 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tabs.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart @@ -1,11 +1,14 @@ import 'package:flutter_mozilla_components/src/data/models/load_url_flags.dart'; import 'package:flutter_mozilla_components/src/data/models/source.dart'; -import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart' + hide IconSource; -class GeckoTabsService { +final _apiInstance = GeckoTabsApi(); + +class GeckoTabService { final GeckoTabsApi _api; - GeckoTabsService({GeckoTabsApi? api}) : _api = api ?? GeckoTabsApi(); + GeckoTabService({GeckoTabsApi? api}) : _api = api ?? _apiInstance; Future selectTab({required String tabId}) { return _api.selectTab(tabId: tabId); diff --git a/packages/flutter_mozilla_components/lib/src/helper/helper.dart b/packages/flutter_mozilla_components/lib/src/helper/helper.dart deleted file mode 100644 index 10d6398e..00000000 --- a/packages/flutter_mozilla_components/lib/src/helper/helper.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter_mozilla_components/src/data/models/source.dart'; - -/// Maps an int category (as it can be obtained from a package manager) to our internal representation. -PackageCategory packageCategoryfromInt(int? id) { - return PackageCategory.values.firstWhere( - (category) => category.id == id, - orElse: () => PackageCategory.unknown, - ); -} 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 acd5a45e..11a9e66f 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -15,6 +15,16 @@ PlatformException _createConnectionError(String channelName) { ); } +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + /// Indicates what location the tabs should be restored at enum RestoreLocation { /// Restore tabs at the beginning of the tab list @@ -25,6 +35,49 @@ enum RestoreLocation { atIndex, } +/// An icon resource type. +enum IconType { + favicon, + appleTouchIcon, + fluidIcon, + imageSrc, + openGraph, + twitter, + microsoftTile, + tippyTop, + manifestIcon, +} + +/// Supported sizes. +/// +/// We are trying to limit the supported sizes in order to optimize our caching strategy. +enum IconSize { + defaultSize, + launcher, + launcherAdaptive, +} + +/// The source of an [Icon]. +enum IconSource { + /// This icon was generated. + generator, + /// This icon was downloaded. + download, + /// This icon was inlined in the document. + inline, + /// This icon was loaded from an in-memory cache. + memory, + /// This icon was loaded from a disk cache. + disk, +} + +enum CookieSameSiteStatus { + noRestriction, + lax, + strict, + unspecified, +} + /// Translation options that map to the Gecko Translations Options. /// /// @property downloadModel If the necessary models should be downloaded on request. If false, then @@ -121,7 +174,7 @@ class LastMediaAccessState { required this.mediaSessionActive, }); - /// [ContentState.url] when media started playing. + /// [TabContentState.url] when media started playing. /// This is not the URL of the media but of the page when media started. /// Defaults to "" (an empty String) if media hasn't started playing. /// This value is only updated when media starts playing. @@ -357,7 +410,7 @@ class TabState { /// The last [HistoryMetadataKey] of the tab. HistoryMetadataKey? historyMetadata; - /// The last [Source] of the tab. + /// The last [IconSource] of the tab. SourceValue source; /// The index the tab should be restored at. @@ -466,6 +519,442 @@ class RecoverableBrowserState { } } +/// A request to load an [Icon]. +class IconRequest { + IconRequest({ + required this.url, + required this.size, + required this.resources, + this.color, + required this.isPrivate, + required this.waitOnNetworkLoad, + }); + + String url; + + IconSize size; + + List resources; + + int? color; + + bool isPrivate; + + bool waitOnNetworkLoad; + + Object encode() { + return [ + url, + size, + resources, + color, + isPrivate, + waitOnNetworkLoad, + ]; + } + + static IconRequest decode(Object result) { + result as List; + return IconRequest( + url: result[0]! as String, + size: result[1]! as IconSize, + resources: (result[2] as List?)!.cast(), + color: result[3] as int?, + isPrivate: result[4]! as bool, + waitOnNetworkLoad: result[5]! as bool, + ); + } +} + +class ResourceSize { + ResourceSize({ + required this.height, + required this.width, + }); + + int height; + + int width; + + Object encode() { + return [ + height, + width, + ]; + } + + static ResourceSize decode(Object result) { + result as List; + return ResourceSize( + height: result[0]! as int, + width: result[1]! as int, + ); + } +} + +/// An icon resource that can be loaded. +class Resource { + Resource({ + required this.url, + required this.type, + required this.sizes, + this.mimeType, + required this.maskable, + }); + + String url; + + IconType type; + + List sizes; + + String? mimeType; + + bool maskable; + + Object encode() { + return [ + url, + type, + sizes, + mimeType, + maskable, + ]; + } + + static Resource decode(Object result) { + result as List; + return Resource( + url: result[0]! as String, + type: result[1]! as IconType, + sizes: (result[2] as List?)!.cast(), + mimeType: result[3] as String?, + maskable: result[4]! as bool, + ); + } +} + +/// An [Icon] returned by [BrowserIcons] after processing an [IconRequest] +class IconResult { + IconResult({ + required this.image, + this.color, + required this.source, + required this.maskable, + }); + + /// The loaded icon as an [Uint8List]. + Uint8List image; + + /// The dominant color of the icon. Will be null if no color could be extracted. + int? color; + + /// The source of the icon. + IconSource source; + + /// True if the icon represents as full-bleed icon that can be cropped to other shapes. + bool maskable; + + Object encode() { + return [ + image, + color, + source, + maskable, + ]; + } + + static IconResult decode(Object result) { + result as List; + return IconResult( + image: result[0]! as Uint8List, + color: result[1] as int?, + source: result[2]! as IconSource, + maskable: result[3]! as bool, + ); + } +} + +class CookiePartitionKey { + CookiePartitionKey({ + required this.topLevelSite, + }); + + String topLevelSite; + + Object encode() { + return [ + topLevelSite, + ]; + } + + static CookiePartitionKey decode(Object result) { + result as List; + return CookiePartitionKey( + topLevelSite: result[0]! as String, + ); + } +} + +class Cookie { + Cookie({ + required this.domain, + this.expirationDate, + required this.firstPartyDomain, + required this.hostOnly, + required this.httpOnly, + required this.name, + this.partitionKey, + required this.path, + required this.secure, + required this.session, + required this.sameSite, + required this.storeId, + required this.value, + }); + + String domain; + + int? expirationDate; + + String firstPartyDomain; + + bool hostOnly; + + bool httpOnly; + + String name; + + CookiePartitionKey? partitionKey; + + String path; + + bool secure; + + bool session; + + CookieSameSiteStatus sameSite; + + String storeId; + + String value; + + Object encode() { + return [ + domain, + expirationDate, + firstPartyDomain, + hostOnly, + httpOnly, + name, + partitionKey, + path, + secure, + session, + sameSite, + storeId, + value, + ]; + } + + static Cookie decode(Object result) { + result as List; + return Cookie( + domain: result[0]! as String, + expirationDate: result[1] as int?, + firstPartyDomain: result[2]! as String, + hostOnly: result[3]! as bool, + httpOnly: result[4]! as bool, + name: result[5]! as String, + partitionKey: result[6] as CookiePartitionKey?, + path: result[7]! as String, + secure: result[8]! as bool, + session: result[9]! as bool, + sameSite: result[10]! as CookieSameSiteStatus, + storeId: result[11]! as String, + value: result[12]! as String, + ); + } +} + +class HistoryItem { + HistoryItem({ + required this.url, + required this.title, + }); + + String url; + + String title; + + Object encode() { + return [ + url, + title, + ]; + } + + static HistoryItem decode(Object result) { + result as List; + return HistoryItem( + url: result[0]! as String, + title: result[1]! as String, + ); + } +} + +class HistoryState { + HistoryState({ + required this.items, + required this.currentIndex, + required this.canGoBack, + required this.canGoForward, + }); + + List items; + + int currentIndex; + + bool canGoBack; + + bool canGoForward; + + Object encode() { + return [ + items, + currentIndex, + canGoBack, + canGoForward, + ]; + } + + static HistoryState decode(Object result) { + result as List; + return HistoryState( + items: (result[0] as List?)!.cast(), + currentIndex: result[1]! as int, + canGoBack: result[2]! as bool, + canGoForward: result[3]! as bool, + ); + } +} + +class ReaderableState { + ReaderableState({ + required this.readerable, + required this.active, + }); + + /// Whether or not the current page can be transformed to + /// be displayed in a reader view. + bool readerable; + + /// Whether or not reader view is active. + bool active; + + Object encode() { + return [ + readerable, + active, + ]; + } + + static ReaderableState decode(Object result) { + result as List; + return ReaderableState( + readerable: result[0]! as bool, + active: result[1]! as bool, + ); + } +} + +class SecurityInfoState { + SecurityInfoState({ + required this.secure, + required this.host, + required this.issuer, + }); + + bool secure; + + String host; + + String issuer; + + Object encode() { + return [ + secure, + host, + issuer, + ]; + } + + static SecurityInfoState decode(Object result) { + result as List; + return SecurityInfoState( + secure: result[0]! as bool, + host: result[1]! as String, + issuer: result[2]! as String, + ); + } +} + +class TabContentState { + TabContentState({ + required this.id, + this.contextId, + required this.url, + required this.title, + required this.progress, + required this.isPrivate, + required this.isFullScreen, + required this.isLoading, + }); + + String id; + + String? contextId; + + String url; + + String title; + + int progress; + + bool isPrivate; + + bool isFullScreen; + + bool isLoading; + + Object encode() { + return [ + id, + contextId, + url, + title, + progress, + isPrivate, + isFullScreen, + isLoading, + ]; + } + + static TabContentState decode(Object result) { + result as List; + return TabContentState( + id: result[0]! as String, + contextId: result[1] as String?, + url: result[2]! as String, + title: result[3]! as String, + progress: result[4]! as int, + isPrivate: result[5]! as bool, + isFullScreen: result[6]! as bool, + isLoading: result[7]! as bool, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @@ -477,39 +966,84 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is RestoreLocation) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is TranslationOptions) { + } else if (value is IconType) { buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else if (value is ReaderState) { + writeValue(buffer, value.index); + } else if (value is IconSize) { buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else if (value is LastMediaAccessState) { + writeValue(buffer, value.index); + } else if (value is IconSource) { buffer.putUint8(132); - writeValue(buffer, value.encode()); - } else if (value is HistoryMetadataKey) { + writeValue(buffer, value.index); + } else if (value is CookieSameSiteStatus) { buffer.putUint8(133); - writeValue(buffer, value.encode()); - } else if (value is PackageCategoryValue) { + writeValue(buffer, value.index); + } else if (value is TranslationOptions) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is ExternalPackage) { + } else if (value is ReaderState) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is LoadUrlFlagsValue) { + } else if (value is LastMediaAccessState) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is SourceValue) { + } else if (value is HistoryMetadataKey) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is TabState) { + } else if (value is PackageCategoryValue) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is RecoverableTab) { + } else if (value is ExternalPackage) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is RecoverableBrowserState) { + } else if (value is LoadUrlFlagsValue) { buffer.putUint8(140); writeValue(buffer, value.encode()); + } else if (value is SourceValue) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else if (value is TabState) { + buffer.putUint8(142); + writeValue(buffer, value.encode()); + } else if (value is RecoverableTab) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is RecoverableBrowserState) { + buffer.putUint8(144); + writeValue(buffer, value.encode()); + } else if (value is IconRequest) { + buffer.putUint8(145); + writeValue(buffer, value.encode()); + } else if (value is ResourceSize) { + buffer.putUint8(146); + writeValue(buffer, value.encode()); + } else if (value is Resource) { + buffer.putUint8(147); + writeValue(buffer, value.encode()); + } else if (value is IconResult) { + buffer.putUint8(148); + writeValue(buffer, value.encode()); + } else if (value is CookiePartitionKey) { + buffer.putUint8(149); + writeValue(buffer, value.encode()); + } else if (value is Cookie) { + buffer.putUint8(150); + writeValue(buffer, value.encode()); + } else if (value is HistoryItem) { + buffer.putUint8(151); + writeValue(buffer, value.encode()); + } else if (value is HistoryState) { + buffer.putUint8(152); + writeValue(buffer, value.encode()); + } else if (value is ReaderableState) { + buffer.putUint8(153); + writeValue(buffer, value.encode()); + } else if (value is SecurityInfoState) { + buffer.putUint8(154); + writeValue(buffer, value.encode()); + } else if (value is TabContentState) { + buffer.putUint8(155); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -522,27 +1056,61 @@ class _PigeonCodec extends StandardMessageCodec { final int? value = readValue(buffer) as int?; return value == null ? null : RestoreLocation.values[value]; case 130: - return TranslationOptions.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : IconType.values[value]; case 131: - return ReaderState.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : IconSize.values[value]; case 132: - return LastMediaAccessState.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : IconSource.values[value]; case 133: - return HistoryMetadataKey.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : CookieSameSiteStatus.values[value]; case 134: - return PackageCategoryValue.decode(readValue(buffer)!); + return TranslationOptions.decode(readValue(buffer)!); case 135: - return ExternalPackage.decode(readValue(buffer)!); + return ReaderState.decode(readValue(buffer)!); case 136: - return LoadUrlFlagsValue.decode(readValue(buffer)!); + return LastMediaAccessState.decode(readValue(buffer)!); case 137: - return SourceValue.decode(readValue(buffer)!); + return HistoryMetadataKey.decode(readValue(buffer)!); case 138: - return TabState.decode(readValue(buffer)!); + return PackageCategoryValue.decode(readValue(buffer)!); case 139: - return RecoverableTab.decode(readValue(buffer)!); + return ExternalPackage.decode(readValue(buffer)!); case 140: + return LoadUrlFlagsValue.decode(readValue(buffer)!); + case 141: + return SourceValue.decode(readValue(buffer)!); + case 142: + return TabState.decode(readValue(buffer)!); + case 143: + return RecoverableTab.decode(readValue(buffer)!); + case 144: return RecoverableBrowserState.decode(readValue(buffer)!); + case 145: + return IconRequest.decode(readValue(buffer)!); + case 146: + return ResourceSize.decode(readValue(buffer)!); + case 147: + return Resource.decode(readValue(buffer)!); + case 148: + return IconResult.decode(readValue(buffer)!); + case 149: + return CookiePartitionKey.decode(readValue(buffer)!); + case 150: + return Cookie.decode(readValue(buffer)!); + case 151: + return HistoryItem.decode(readValue(buffer)!); + case 152: + return HistoryState.decode(readValue(buffer)!); + case 153: + return ReaderableState.decode(readValue(buffer)!); + case 154: + return SecurityInfoState.decode(readValue(buffer)!); + case 155: + return TabContentState.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -1322,3 +1890,389 @@ class GeckoTabsApi { } } } + +class GeckoIconsApi { + /// Constructor for [GeckoIconsApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeckoIconsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future loadIcon(IconRequest request) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoIconsApi.loadIcon$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([request]) 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 if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as IconResult?)!; + } + } +} + +class GeckoCookieApi { + /// Constructor for [GeckoCookieApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeckoCookieApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future getCookie(String? firstPartyDomain, String name, CookiePartitionKey? partitionKey, String? storeId, String url) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.getCookie$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([firstPartyDomain, name, partitionKey, storeId, url]) 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 if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as Cookie?)!; + } + } + + Future> getAllCookies(String? domain, String? firstPartyDomain, String? name, CookiePartitionKey? partitionKey, String? storeId, String url) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.getAllCookies$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([domain, firstPartyDomain, name, partitionKey, storeId, url]) 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 if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future setCookie(String? domain, int? expirationDate, String? firstPartyDomain, bool? httpOnly, String? name, CookiePartitionKey? partitionKey, String? path, CookieSameSiteStatus? sameSite, bool? secure, String? storeId, String url, String? value) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.setCookie$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([domain, expirationDate, firstPartyDomain, httpOnly, name, partitionKey, path, sameSite, secure, storeId, url, value]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future removeCookie(String? firstPartyDomain, String name, CookiePartitionKey? partitionKey, String? storeId, String url) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoCookieApi.removeCookie$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([firstPartyDomain, name, partitionKey, storeId, url]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } +} + +abstract class GeckoStateEvents { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + void onTabListChange(List tabIds); + + void onSelectedTabChange(String? id); + + 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); + + 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.onTabListChange$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.onTabListChange was null.'); + final List args = (message as List?)!; + final List? arg_tabIds = (args[0] as List?)?.cast(); + assert(arg_tabIds != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null, expected non-null List.'); + try { + api.onTabListChange(arg_tabIds!); + 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.onSelectedTabChange$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.onSelectedTabChange was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + try { + api.onSelectedTabChange(arg_id); + 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.onTabContentStateChange$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.onTabContentStateChange was null.'); + final List args = (message as List?)!; + final TabContentState? arg_state = (args[0] 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!); + 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.onHistoryStateChange$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.onHistoryStateChange was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + assert(arg_id != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null, expected non-null String.'); + final HistoryState? arg_state = (args[1] 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!); + 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.onReaderableStateChange$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.onReaderableStateChange was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + assert(arg_id != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null, expected non-null String.'); + final ReaderableState? arg_state = (args[1] 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!); + 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.onSecurityInfoStateChange$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.onSecurityInfoStateChange was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + assert(arg_id != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null, expected non-null String.'); + final SecurityInfoState? arg_state = (args[1] 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!); + 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.onIconChange$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.onIconChange was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + assert(arg_id != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange was null, expected non-null String.'); + final Uint8List? arg_bytes = (args[1] as Uint8List?); + try { + api.onIconChange(arg_id!, arg_bytes); + 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.onThumbnailChange$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.onThumbnailChange was null.'); + final List args = (message as List?)!; + final String? arg_id = (args[0] as String?); + assert(arg_id != null, + 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange was null, expected non-null String.'); + final Uint8List? arg_bytes = (args[1] as Uint8List?); + try { + api.onThumbnailChange(arg_id!, arg_bytes); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index bfc5df5e..1a6869fa 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -50,7 +50,7 @@ class ReaderState { /// Details about the last playing media in this tab. class LastMediaAccessState { - /// [ContentState.url] when media started playing. + /// [TabContentState.url] when media started playing. /// This is not the URL of the media but of the page when media started. /// Defaults to "" (an empty String) if media hasn't started playing. /// This value is only updated when media starts playing. @@ -173,7 +173,7 @@ class TabState { /// The last [HistoryMetadataKey] of the tab. final HistoryMetadataKey? historyMetadata; - /// The last [Source] of the tab. + /// The last [IconSource] of the tab. final SourceValue source; /// The index the tab should be restored at. @@ -241,12 +241,228 @@ enum RestoreLocation { atIndex, } +/// An icon resource type. +enum IconType { + favicon, + appleTouchIcon, + fluidIcon, + imageSrc, + openGraph, + twitter, + microsoftTile, + tippyTop, + manifestIcon, +} + +/// Supported sizes. +/// +/// We are trying to limit the supported sizes in order to optimize our caching strategy. +enum IconSize { + defaultSize, + launcher, + launcherAdaptive, +} + +/// A request to load an [Icon]. +class IconRequest { + final String url; + final IconSize size; + final List resources; + final int? color; + final bool isPrivate; + final bool waitOnNetworkLoad; + + IconRequest({ + required this.url, + this.size = IconSize.defaultSize, + this.resources = const [], + this.color, + this.isPrivate = false, + this.waitOnNetworkLoad = true, + }); +} + +class ResourceSize { + final int height; + final int width; + + const ResourceSize({required this.height, required this.width}); +} + +/// An icon resource that can be loaded. +class Resource { + final String url; + final IconType type; + final List sizes; + final String? mimeType; + final bool maskable; + + Resource({ + required this.url, + required this.type, + this.sizes = const [], + this.mimeType, + this.maskable = false, + }); +} + +/// An [Icon] returned by [BrowserIcons] after processing an [IconRequest] +class IconResult { + /// The loaded icon as an [Uint8List]. + final Uint8List image; + + /// The dominant color of the icon. Will be null if no color could be extracted. + final int? color; + + /// The source of the icon. + final IconSource source; + + /// True if the icon represents as full-bleed icon that can be cropped to other shapes. + final bool maskable; + + IconResult({ + required this.image, + this.color, + required this.source, + this.maskable = false, + }); +} + +/// The source of an [Icon]. +enum IconSource { + /// This icon was generated. + generator, + + /// This icon was downloaded. + download, + + /// This icon was inlined in the document. + inline, + + /// This icon was loaded from an in-memory cache. + memory, + + /// This icon was loaded from a disk cache. + disk, +} + +enum CookieSameSiteStatus { + noRestriction, + lax, + strict, + unspecified; +} + +class CookiePartitionKey { + final String topLevelSite; + + CookiePartitionKey(this.topLevelSite); +} + +class Cookie { + final String domain; + final int? expirationDate; + final String firstPartyDomain; + final bool hostOnly; + final bool httpOnly; + final String name; + final CookiePartitionKey? partitionKey; + final String path; + final bool secure; + final bool session; + final CookieSameSiteStatus sameSite; + final String storeId; + final String value; + + Cookie( + this.domain, + this.expirationDate, + this.firstPartyDomain, + this.hostOnly, + this.httpOnly, + this.name, + this.partitionKey, + this.path, + this.secure, + this.session, + this.sameSite, + this.storeId, + this.value); +} + +class HistoryItem { + final String url; + final String title; + + HistoryItem(this.url, this.title); +} + +class HistoryState { + final List items; + final int currentIndex; + + final bool canGoBack; + final bool canGoForward; + + HistoryState( + this.items, + this.currentIndex, + this.canGoBack, + this.canGoForward, + ); +} + +class ReaderableState { + /// Whether or not the current page can be transformed to + /// be displayed in a reader view. + final bool readerable; + + /// Whether or not reader view is active. + final bool active; + + ReaderableState(this.readerable, this.active); +} + +class SecurityInfoState { + final bool secure; + final String host; + final String issuer; + + SecurityInfoState(this.secure, this.host, this.issuer); +} + +class TabContentState { + final String id; + final String? contextId; + + final String url; + final String title; + + final int progress; + + final bool isPrivate; + final bool isFullScreen; + final bool isLoading; + + TabContentState( + this.id, + this.contextId, + this.url, + this.title, + this.progress, + this.isPrivate, + this.isFullScreen, + this.isLoading, + ); +} + @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/pigeons/gecko.g.dart', dartOptions: DartOptions(), kotlinOut: 'android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt', - kotlinOptions: KotlinOptions(), + kotlinOptions: + KotlinOptions(package: 'eu.lensai.flutter_mozilla_components.pigeons'), dartPackageName: 'flutter_mozilla_components', )) @HostApi() @@ -412,3 +628,69 @@ abstract class GeckoTabsApi { required String? alternativeUrl, }); } + +@HostApi() +abstract class GeckoIconsApi { + @async + IconResult loadIcon(IconRequest request); +} + +@HostApi() +abstract class GeckoCookieApi { + @async + Cookie getCookie( + String? firstPartyDomain, + String name, + CookiePartitionKey? partitionKey, + String? storeId, + String url, + ); + + @async + List getAllCookies( + String? domain, + String? firstPartyDomain, + String? name, + CookiePartitionKey? partitionKey, + String? storeId, + String url, + ); + + @async + void setCookie( + String? domain, + int? expirationDate, + String? firstPartyDomain, + bool? httpOnly, + String? name, + CookiePartitionKey? partitionKey, + String? path, + CookieSameSiteStatus? sameSite, + bool? secure, + String? storeId, + String url, + String? value, + ); + + @async + void removeCookie( + String? firstPartyDomain, + String name, + CookiePartitionKey? partitionKey, + String? storeId, + String url, + ); +} + +@FlutterApi() +abstract class GeckoStateEvents { + void onTabListChange(List tabIds); + void onSelectedTabChange(String? id); + + 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); +}