intermediate
This commit is contained in:
@@ -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,6 +0,0 @@
|
||||
class ReceivedParameter {
|
||||
final String? content;
|
||||
final String? tool;
|
||||
|
||||
ReceivedParameter(this.content, this.tool);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user