intermediate

This commit is contained in:
Fabian Freund
2024-09-13 09:11:06 +02:00
parent 3b047b5d8a
commit 37d2c84d7f
155 changed files with 5666 additions and 1816 deletions
+2 -2
View File
@@ -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
@@ -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"
-15
View File
@@ -1,15 +0,0 @@
import 'package:exceptions/exceptions.dart';
import 'package:riverpod/riverpod.dart';
extension AsyncExtension<T> on Result<T> {
AsyncValue<T> 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),
};
}
}
+1 -1
View File
@@ -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';
+17
View File
@@ -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,
});
}
+78
View File
@@ -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);
}
@@ -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<EquatableImage> 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;
}
}
@@ -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<String, dynamic> json) {
return WebPageInfo(
url: Uri.parse(json['url'] as String),
title: json['title'] as String?,
favicon: switch (json['favicon']) {
final Map<String, dynamic> favicon => Favicon.fromMap(favicon),
_ => null
},
);
}
Map<String, dynamic> toJson() {
return {
'url': url.toString(),
'title': title,
'favicon': favicon?.toJson(),
};
}
}
@@ -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 = <ErrorMessage>[];
unawaited(ref.read(readerabilityScriptProvider.future));
await _initPackageInfo();
final bangSyncResults = await _initBangs();
@@ -7,7 +7,7 @@ part of 'app_initialization.dart';
// **************************************************************************
String _$appInitializationServiceHash() =>
r'dadcf205d32efa61eb2a0ff1c9b3d49658b76a56';
r'eee9c8eadc793fb435af1032ad2099dfd381a034';
/// See also [AppInitializationService].
@ProviderFor(AppInitializationService)
+170 -61
View File
@@ -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<ResourceSize> 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<String, bool> _httpsCache;
GenericWebsiteService() : _httpsCache = {};
GenericWebsiteService()
: _httpsCache = {},
_iconsService = GeckoIconService();
@override
void build() {
_client = http.Client();
}
static Iterable<Favicon> _extractFavicons(Uri url, Document document) sync* {
final links = document.querySelectorAll(
'link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]',
static Map<String, dynamic> _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<String, dynamic> resource) {
return Resource(
url: resource['url'] as String,
type: resource['type'] as IconType,
mimeType: resource['mimeType'] as String?,
sizes: (resource['sizes'] as List<List<int>>)
.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<Resource> _extractIcons(Document document) {
final List<Resource> 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<Result<WebPageInfo>> 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<String, dynamic>>)
.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<Result<Uint8List?>> 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<BrowserIcon> getUrlFavicon({
required Uri url,
List<Resource> 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,
);
}
@@ -7,7 +7,7 @@ part of 'generic_website.dart';
// **************************************************************************
String _$genericWebsiteServiceHash() =>
r'517304ca95e1ef8377d4c1a3bd06fef9c68c92cd';
r'0b1d953fe1b30b9dd0396df34e6176424cdaf941';
/// See also [GenericWebsiteService].
@ProviderFor(GenericWebsiteService)
+20
View File
@@ -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<String?> calculateHash() async {
final bytes = await toByteData();
if (bytes != null) {
final digest = sha1.convert(bytes.buffer.asUint8List());
return digest.toString();
}
return null;
}
Future<EquatableImage> toEquatable() {
return EquatableImage.calculate(this);
}
}
-4
View File
@@ -1,4 +0,0 @@
extension UriFavicon on Uri {
Uri genericFavicon() =>
Uri.parse('https://icons.duckduckgo.com/ip3/$host.ico');
}
@@ -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';
@@ -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<BangDatabase> with _$BangDaoMixin {
return db.bangQuery(query: db.buildQuery(searchString));
}
Future<int> upsertBangIcon(String trigger, Uint8List iconData) {
return db.bangIcon.insertOne(
BangIconCompanion.insert(
trigger: trigger,
iconData: iconData,
fetchDate: DateTime.now(),
),
mode: InsertMode.insertOrReplace,
);
}
// Future<int> 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,
// );
// }
}
@@ -1021,8 +1021,6 @@ class BangDataView extends ViewInfo<BangDataView, BangData>
.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<String>('format')),
frequency: row.readNullable<int>('frequency'),
lastUsed: row.readNullable<DateTime>('last_used'),
iconData: row.readNullable<Uint8List>('icon_data'),
));
}
@@ -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,
];
}
@@ -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<BangFormat>? 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?,
);
}
}
@@ -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<Result<BangData>> 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<double> watchIconCacheSize() {
@@ -7,7 +7,7 @@ part of 'data.dart';
// **************************************************************************
String _$bangDataRepositoryHash() =>
r'ef06babcaa07361e37f5cc62a268eb10aed0fc82';
r'abacc6db3a61958cae67a91cbfd312ae87c641b3';
/// See also [BangDataRepository].
@ProviderFor(BangDataRepository)
@@ -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,
),
);
@@ -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),
);
}
@@ -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 {
@@ -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,
@@ -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 {
@@ -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!),
),
),
@@ -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';
@@ -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';
@@ -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<BrowserIcon> 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<Object?> get hashParameters => [
image,
dominantColor,
source,
];
}
@@ -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<Object?> get hashParameters => [url, title];
}
class HistoryState with FastEquatable {
final List<HistoryItem> 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<Object?> get hashParameters =>
[items, currentIndex, canGoBack, canGoForward];
}
@@ -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<Object?> get hashParameters => [readerable, active];
}
@@ -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<Object?> get hashParameters => [secure, host, issuer];
}
@@ -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<Object?> get hashParameters => [
id,
contextId,
url,
title,
icon,
thumbnail,
progress,
isPrivate,
isFullScreen,
isLoading,
securityInfoState,
historyState,
readerableState,
];
}
@@ -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);
}
@@ -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);
}
@@ -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<GeckoEventService>.internal(
eventService,
name: r'eventServiceProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$eventServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef EventServiceRef = ProviderRef<GeckoEventService>;
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<TabState?> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'tabStateProvider';
}
/// See also [tabState].
class TabStateProvider extends AutoDisposeProvider<TabState?> {
/// 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<TabState?> 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<TabState?> {
/// The parameter `tabId` of this provider.
String? get tabId;
}
class _TabStateProviderElement extends AutoDisposeProviderElement<TabState?>
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<TabState?>.internal(
selectedTabState,
name: r'selectedTabStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedTabStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef SelectedTabStateRef = AutoDisposeProviderRef<TabState?>;
String _$selectedTabSessionNotifierHash() =>
r'1b0fe1afa87c0b09fc8407ceb91946fffaef5214';
/// See also [selectedTabSessionNotifier].
@ProviderFor(selectedTabSessionNotifier)
final selectedTabSessionNotifierProvider =
AutoDisposeProvider<TabSession>.internal(
selectedTabSessionNotifier,
name: r'selectedTabSessionNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedTabSessionNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef<TabSession>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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<void> 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;
}
}
@@ -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<SelectedTabController, String?>.internal(
SelectedTabController.new,
name: r'selectedTabControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedTabControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SelectedTabController = Notifier<String?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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<String> 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<void> _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<void> _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<String> 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<String, String>? 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<void> closeTabs(List<String> tabIds) {
return _tabsService.removeTabs(ids: tabIds);
}
@override
Map<String, TabState> 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 {};
}
}
@@ -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<TabRepository, Map<String, TabState>>.internal(
TabRepository.new,
name: r'tabRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$tabRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$TabRepository = Notifier<Map<String, TabState>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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<void> loadUrl({
required Uri url,
LoadUrlFlags flags = LoadUrlFlags.NONE,
Map<String, String>? additionalHeaders,
}) {
return _sessionService.loadUrl(
url: url,
flags: flags,
additionalHeaders: additionalHeaders,
);
}
@override
void build(String? tabId) {
_sessionService = (tabId != null)
? GeckoSessionService(tabId: tabId)
: GeckoSessionService.forActiveTab();
}
}
@@ -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<void> {
late final String? tabId;
void build(
String? tabId,
);
}
/// See also [TabSession].
@ProviderFor(TabSession)
const tabSessionProvider = TabSessionFamily();
/// See also [TabSession].
class TabSessionFamily extends Family<void> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'tabSessionProvider';
}
/// See also [TabSession].
class TabSessionProvider
extends AutoDisposeNotifierProviderImpl<TabSession, void> {
/// 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<TabSession, void> 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<void> {
/// The parameter `tabId` of this provider.
String? get tabId;
}
class _TabSessionProviderElement
extends AutoDisposeNotifierProviderElement<TabSession, void>
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
@@ -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<TabDatabase> with _$TabLinkDaoMixin {
TabLinkDao(super.db);
Selectable<String> 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<void> upsertTabLink(
String tabId, {
required DateTime timestamp,
Value<String?> topicId = const Value.absent(),
}) {
return db.tabLink.insertOne(
TabLinkCompanion.insert(
id: tabId,
timestamp: timestamp,
topicId: topicId,
),
mode: InsertMode.insertOrReplace,
);
}
Future<void> 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<void> syncTabLinks({required List<String> retainTabIds}) {
return (db.tabLink.delete()..where((t) => t.id.isNotIn(retainTabIds))).go();
}
}
@@ -3,4 +3,4 @@
part of 'tab.dart';
// ignore_for_file: type=lint
mixin _$TabDaoMixin on DatabaseAccessor<TabDatabase> {}
mixin _$TabLinkDaoMixin on DatabaseAccessor<TabDatabase> {}
@@ -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';
@@ -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);
}
@@ -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;
@@ -131,11 +131,11 @@ class TopicCompanion extends UpdateCompanion<TopicData> {
}
}
class Tab extends Table with TableInfo<Tab, TabData> {
class TabLink extends Table with TableInfo<TabLink, TabLinkData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
Tab(this.attachedDatabase, [this._alias]);
TabLink(this.attachedDatabase, [this._alias]);
late final GeneratedColumn<String> id = GeneratedColumn<String>(
'id', aliasedName, false,
type: DriftSqlType.string,
@@ -151,75 +151,42 @@ class Tab extends Table with TableInfo<Tab, TabData> {
type: DriftSqlType.dateTime,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
late final GeneratedColumnWithTypeConverter<Uri, String> url =
GeneratedColumn<String>('url', aliasedName, false,
type: DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL')
.withConverter<Uri>(Tab.$converterurl);
late final GeneratedColumn<String> title = GeneratedColumn<String>(
'title', aliasedName, true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '');
late final GeneratedColumn<Uint8List> screenshot = GeneratedColumn<Uint8List>(
'screenshot', aliasedName, true,
type: DriftSqlType.blob,
requiredDuringInsert: false,
$customConstraints: '');
@override
List<GeneratedColumn> get $columns =>
[id, topicId, timestamp, url, title, screenshot];
List<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => {id};
@override
TabData map(Map<String, dynamic> data, {String? tablePrefix}) {
TabLinkData map(Map<String, dynamic> 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<Uri, String> $converterurl = const UriConverter();
@override
bool get dontWriteConstraints => true;
}
class TabData extends DataClass implements Insertable<TabData> {
class TabLinkData extends DataClass implements Insertable<TabLinkData> {
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<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
@@ -228,28 +195,16 @@ class TabData extends DataClass implements Insertable<TabData> {
map['topic_id'] = Variable<String>(topicId);
}
map['timestamp'] = Variable<DateTime>(timestamp);
{
map['url'] = Variable<String>(Tab.$converterurl.toSql(url));
}
if (!nullToAbsent || title != null) {
map['title'] = Variable<String>(title);
}
if (!nullToAbsent || screenshot != null) {
map['screenshot'] = Variable<Uint8List>(screenshot);
}
return map;
}
factory TabData.fromJson(Map<String, dynamic> json,
factory TabLinkData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return TabData(
return TabLinkData(
id: serializer.fromJson<String>(json['id']),
topicId: serializer.fromJson<String?>(json['topic_id']),
timestamp: serializer.fromJson<DateTime>(json['timestamp']),
url: serializer.fromJson<Uri>(json['url']),
title: serializer.fromJson<String?>(json['title']),
screenshot: serializer.fromJson<Uint8List?>(json['screenshot']),
);
}
@override
@@ -259,130 +214,88 @@ class TabData extends DataClass implements Insertable<TabData> {
'id': serializer.toJson<String>(id),
'topic_id': serializer.toJson<String?>(topicId),
'timestamp': serializer.toJson<DateTime>(timestamp),
'url': serializer.toJson<Uri>(url),
'title': serializer.toJson<String?>(title),
'screenshot': serializer.toJson<Uint8List?>(screenshot),
};
}
TabData copyWith(
TabLinkData copyWith(
{String? id,
Value<String?> topicId = const Value.absent(),
DateTime? timestamp,
Uri? url,
Value<String?> title = const Value.absent(),
Value<Uint8List?> 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<TabData> {
class TabLinkCompanion extends UpdateCompanion<TabLinkData> {
final Value<String> id;
final Value<String?> topicId;
final Value<DateTime> timestamp;
final Value<Uri> url;
final Value<String?> title;
final Value<Uint8List?> screenshot;
final Value<int> 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<TabData> custom({
timestamp = Value(timestamp);
static Insertable<TabLinkData> custom({
Expression<String>? id,
Expression<String>? topicId,
Expression<DateTime>? timestamp,
Expression<String>? url,
Expression<String>? title,
Expression<Uint8List>? screenshot,
Expression<int>? 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<String>? id,
Value<String?>? topicId,
Value<DateTime>? timestamp,
Value<Uri>? url,
Value<String?>? title,
Value<Uint8List?>? screenshot,
Value<int>? 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<TabData> {
if (timestamp.present) {
map['timestamp'] = Variable<DateTime>(timestamp.value);
}
if (url.present) {
map['url'] = Variable<String>(Tab.$converterurl.toSql(url.value));
}
if (title.present) {
map['title'] = Variable<String>(title.value);
}
if (screenshot.present) {
map['screenshot'] = Variable<Uint8List>(screenshot.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
@@ -416,13 +320,10 @@ class TabCompanion extends UpdateCompanion<TabData> {
@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<TopicDataWithCount> 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<String>('id'),
name: row.readNullable<String>('name'),
@@ -455,7 +356,7 @@ abstract class _$TabDatabase extends GeneratedDatabase {
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [topic, tab];
List<DatabaseSchemaEntity> 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<Tab, List<TabData>> _tabRefsTable(
static MultiTypedResultKey<TabLink, List<TabLinkData>> _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<String?> topicId,
required DateTime timestamp,
required Uri url,
Value<String?> title,
Value<Uint8List?> screenshot,
Value<int> rowid,
});
typedef $TabUpdateCompanionBuilder = TabCompanion Function({
typedef $TabLinkUpdateCompanionBuilder = TabLinkCompanion Function({
Value<String> id,
Value<String?> topicId,
Value<DateTime> timestamp,
Value<Uri> url,
Value<String?> title,
Value<Uint8List?> screenshot,
Value<int> 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<String> 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<Uri, Uri, String> get url =>
$state.composableBuilder(
column: $state.table.url,
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column,
joinBuilders: joinBuilders));
ColumnFilters<String> get title => $state.composableBuilder(
column: $state.table.title,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnFilters<Uint8List> 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<String> 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<String> get url => $state.composableBuilder(
column: $state.table.url,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get title => $state.composableBuilder(
column: $state.table.title,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<Uint8List> 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<String> id = const Value.absent(),
Value<String?> topicId = const Value.absent(),
Value<DateTime> timestamp = const Value.absent(),
Value<Uri> url = const Value.absent(),
Value<String?> title = const Value.absent(),
Value<Uint8List?> screenshot = const Value.absent(),
Value<int> 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<String?> topicId = const Value.absent(),
required DateTime timestamp,
required Uri url,
Value<String?> title = const Value.absent(),
Value<Uint8List?> screenshot = const Value.absent(),
Value<int> 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);
}
@@ -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<bool> isTabExisting(IsTabExistingRef ref, String tabId) {
final db = ref.watch(tabDatabaseProvider);
return db.tabDao.watchTabExisiting(tabId);
}
@Riverpod()
Stream<TabData?> tabData(TabDataRef ref, String tabId) {
final db = ref.watch(tabDatabaseProvider);
return db.tabDao.watchTab(tabId);
}
@@ -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<TabDatabase>.internal(
tabDatabase,
name: r'tabDatabaseProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$tabDatabaseHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef TabDatabaseRef = ProviderRef<TabDatabase>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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<Color> unusedRandomTopicColor(UnusedRandomTopicColorRef ref) async {
return randomColor;
}
@Riverpod()
Stream<List<TopicDataWithCount>> topicsWithCount(TopicsWithCountRef ref) {
final db = ref.watch(tabDatabaseProvider);
return db.topicsWithCount().watch();
}
@Riverpod()
Stream<List<String>> topicTabIds(TopicTabIdsRef ref, String topicId) {
final db = ref.watch(tabDatabaseProvider);
return db.tabLinkDao.topicTabIds(topicId).watch();
}
@@ -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<TopicData?>.internal(
selectedTopicData,
name: r'selectedTopicDataProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedTopicDataHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef SelectedTopicDataRef = AutoDisposeStreamProviderRef<TopicData?>;
String _$unusedRandomTopicColorHash() =>
r'099730b0d987cc37bcae4ef5c999cbebfa67d7da';
/// See also [unusedRandomTopicColor].
@ProviderFor(unusedRandomTopicColor)
final unusedRandomTopicColorProvider =
AutoDisposeFutureProvider<Color>.internal(
unusedRandomTopicColor,
name: r'unusedRandomTopicColorProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$unusedRandomTopicColorHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef UnusedRandomTopicColorRef = AutoDisposeFutureProviderRef<Color>;
String _$topicsWithCountHash() => r'cd9792b0e0af12cd8796a3f0b76b42171a4200e6';
/// See also [topicsWithCount].
@ProviderFor(topicsWithCount)
final topicsWithCountProvider =
AutoDisposeStreamProvider<List<TopicDataWithCount>>.internal(
topicsWithCount,
name: r'topicsWithCountProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$topicsWithCountHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef TopicsWithCountRef
= AutoDisposeStreamProviderRef<List<TopicDataWithCount>>;
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<AsyncValue<List<String>>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'topicTabIdsProvider';
}
/// See also [topicTabIds].
class TopicTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
/// 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<List<String>> 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<List<String>> 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<List<String>> {
/// The parameter `topicId` of this provider.
String get topicId;
}
class _TopicTabIdsProviderElement
extends AutoDisposeStreamProviderElement<List<String>> 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<SelectedTopic, String?>.internal(
SelectedTopic.new,
name: r'selectedTopicProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedTopicHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SelectedTopic = Notifier<String?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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;
}
}
@@ -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<SelectedTopicRepository, String?>.internal(
SelectedTopicRepository.new,
name: r'selectedTopicRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedTopicRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SelectedTopicRepository = Notifier<String?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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<void> assignTab(String tabId, String? topicId) {
return _db.tabLinkDao.upsertTabLink(
tabId,
timestamp: DateTime.now(),
topicId: Value(topicId),
);
}
Future<void> closeAllTabs(String? topicId) async {
final tabIds = await _db.tabLinkDao.topicTabIds(topicId).get();
if (tabIds.isNotEmpty) {
await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds);
}
}
}
@@ -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<TabLinkRepository, void>.internal(
TabLinkRepository.new,
name: r'tabLinkRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$tabLinkRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$TabLinkRepository = Notifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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<List<TopicDataWithCount>> build() {
void build() {
_db = ref.watch(tabDatabaseProvider);
return _db.topicsWithCount().watch();
}
Future<void> addTopic({required String? name, required Color color}) {
@@ -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<TopicRepository, List<TopicDataWithCount>>.internal(
NotifierProvider<TopicRepository, void>.internal(
TopicRepository.new,
name: r'topicRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -21,6 +21,6 @@ final topicRepositoryProvider =
allTransitiveDependencies: null,
);
typedef _$TopicRepository = StreamNotifier<List<TopicDataWithCount>>;
typedef _$TopicRepository = Notifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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 {
@@ -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({
@@ -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 {
@@ -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});
@@ -0,0 +1,29 @@
import 'dart:typed_data';
import 'dart:ui';
Future<Image?> 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;
}
@@ -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<void> 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() {}
}
@@ -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<SessionService, void>.internal(
SessionService.new,
name: r'sessionServiceProvider',
/// See also [KagiSessionService].
@ProviderFor(KagiSessionService)
final kagiSessionServiceProvider =
AutoDisposeNotifierProvider<KagiSessionService, void>.internal(
KagiSessionService.new,
name: r'kagiSessionServiceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$sessionServiceHash,
: _$kagiSessionServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SessionService = AutoDisposeNotifier<void>;
typedef _$KagiSessionService = AutoDisposeNotifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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<Object?> get hashParameters => [content, preferredTool];
}
class ViewTabs extends Sheet {
class ViewTabsSheet extends Sheet {
@override
bool get cacheHash => true;
@@ -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<double> _extentStreamController;
void add(double extent) {
_extentStreamController.add(extent);
}
@override
Stream<double> 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 = <String>{};
List<TabData> _availableTabs = [];
@override
List<WebView> 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;
}
}
@@ -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<OverlayDialog, Widget?>.internal(
OverlayDialog.new,
name: r'overlayDialogProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$overlayDialogHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$OverlayDialog = AutoDisposeNotifier<Widget?>;
String _$bottomSheetHash() => r'4040ec50a488b4b12123bedceab6d8620e6abe3d';
/// See also [BottomSheet].
@ProviderFor(BottomSheet)
final bottomSheetProvider =
AutoDisposeNotifierProvider<BottomSheet, Sheet?>.internal(
BottomSheet.new,
name: r'bottomSheetProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$bottomSheetHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$BottomSheet = AutoDisposeNotifier<Sheet?>;
String _$bottomSheetExtendHash() => r'81ae254499d1d7f57309274b0f18747b94d6dda3';
/// See also [BottomSheetExtend].
@ProviderFor(BottomSheetExtend)
final bottomSheetExtendProvider =
StreamNotifierProvider<BottomSheetExtend, double>.internal(
BottomSheetExtend.new,
name: r'bottomSheetExtendProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$bottomSheetExtendHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$BottomSheetExtend = StreamNotifier<double>;
String _$selectedBangTriggerHash() =>
r'b0e12bc95e93d50d04f1c658230cea9f15ff0385';
@@ -411,22 +364,5 @@ final showFindInPageProvider = NotifierProvider<ShowFindInPage, bool>.internal(
);
typedef _$ShowFindInPage = Notifier<bool>;
String _$webViewWidgetCacheHash() =>
r'7d6f769e8c314d9dc579d700d36d6d4c944e600f';
/// See also [WebViewWidgetCache].
@ProviderFor(WebViewWidgetCache)
final webViewWidgetCacheProvider =
AutoDisposeNotifierProvider<WebViewWidgetCache, List<WebView>>.internal(
WebViewWidgetCache.new,
name: r'webViewWidgetCacheProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$webViewWidgetCacheHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$WebViewWidgetCache = AutoDisposeNotifier<List<WebView>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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<ReceivedParameter, CreateTab>.fromHandlers(
final _parameterToCreateTabSheetTransformer =
StreamTransformer<ReceivedParameter, CreateTabSheet>.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<CreateTab> _streamController;
late StreamController<CreateTabSheet> _streamController;
@override
Stream<CreateTab> build() {
Stream<CreateTabSheet> 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);
}
}
@@ -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<CreateTabStream, CreateTab>.internal(
AutoDisposeStreamNotifierProvider<CreateTabStream, CreateTabSheet>.internal(
CreateTabStream.new,
name: r'createTabStreamProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -21,6 +21,6 @@ final createTabStreamProvider =
allTransitiveDependencies: null,
);
typedef _$CreateTabStream = AutoDisposeStreamNotifier<CreateTab>;
typedef _$CreateTabStream = AutoDisposeStreamNotifier<CreateTabSheet>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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<void> 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<void> clearAllData() async {
await _cookieManager.deleteAllCookies();
final webViewLoaded = Completer<void>();
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() {}
}
@@ -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<double> _extentStreamController;
void add(double extent) {
_extentStreamController.add(extent);
}
@override
Stream<double> build() {
_extentStreamController = StreamController();
ref.onDispose(() async {
await _extentStreamController.close();
});
return _extentStreamController.stream
.sampleTime(const Duration(milliseconds: 50));
}
}
@@ -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<BottomSheetController, Sheet?>.internal(
BottomSheetController.new,
name: r'bottomSheetControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$bottomSheetControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$BottomSheetController = AutoDisposeNotifier<Sheet?>;
String _$bottomSheetExtendHash() => r'81ae254499d1d7f57309274b0f18747b94d6dda3';
/// See also [BottomSheetExtend].
@ProviderFor(BottomSheetExtend)
final bottomSheetExtendProvider =
StreamNotifierProvider<BottomSheetExtend, double>.internal(
BottomSheetExtend.new,
name: r'bottomSheetExtendProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$bottomSheetExtendHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$BottomSheetExtend = StreamNotifier<double>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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;
}
}
@@ -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<OverlayDialogController, Widget?>.internal(
OverlayDialogController.new,
name: r'overlayDialogControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$overlayDialogControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$OverlayDialogController = AutoDisposeNotifier<Widget?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -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: [
@@ -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<KagiTool?>(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,
@@ -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),
),
@@ -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 {
@@ -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;
@@ -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;
@@ -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';
@@ -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';
@@ -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;
@@ -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,
@@ -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');
@@ -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';
@@ -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';
@@ -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<HostSource>? parseHostSources(List<String>? input) => input
?.map(
@@ -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';
@@ -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<TabDatabase> with _$TabDaoMixin {
TabDao(super.db);
Stream<List<TabData>> watchTabs() {
return (db.tab.select()..orderBy([(u) => OrderingTerm.asc(u.id)])).watch();
}
Stream<TabData?> watchTab(String tabId) {
return (db.tab.select()..where((t) => t.id.equals(tabId)))
.watchSingleOrNull();
}
Stream<bool> 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<List<String>> 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<void> 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<void> updateTab(
String id, {
required DateTime timestamp,
Value<Uri> url = const Value.absent(),
Value<String?> title = const Value.absent(),
Value<String?> topicId = const Value.absent(),
Value<Uint8List?> 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<void> deleteTab(String id) {
return db.tab.deleteOne(TabCompanion.custom(id: Variable(id)));
}
Future<void> deleteTopicTabs(String? topicId) {
return (db.tab.delete()
..where(
(t) => (topicId == null)
? t.topicId.isNull()
: t.topicId.equals(topicId),
))
.go();
}
}

Some files were not shown because too many files have changed in this diff Show More