prepare for multiple apps

This commit is contained in:
Fabian Freund
2026-04-06 12:23:11 +02:00
parent bd1600e8dc
commit 5afc323f04
904 changed files with 29 additions and 29 deletions
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:ui';
class EquatableImage {
Image? _value;
final int _imageHash;
bool _isDisposed = false;
EquatableImage(Image value, {required int hash})
: _value = value,
_imageHash = hash;
/// The underlying ui.Image. Returns null if disposed.
Image? get value => _isDisposed ? null : _value;
/// Whether this image has been disposed.
bool get isDisposed => _isDisposed;
/// Disposes the underlying ui.Image to free GPU memory.
/// This is safe to call multiple times.
void dispose() {
if (_isDisposed) return;
_isDisposed = true;
// Delay disposal to allow widgets to finish rendering
Future.delayed(const Duration(seconds: 3), () {
_value?.dispose();
_value = null;
});
}
@override
int get hashCode => _imageHash.hashCode;
@override
bool operator ==(Object other) {
return other is EquatableImage && other._imageHash == _imageHash;
}
}
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:uuid/uuid_value.dart';
import 'package:weblibre/core/uuid.dart';
import 'package:weblibre/features/user/data/models/auth_settings.dart';
part 'profile.g.dart';
@JsonSerializable()
@CopyWith()
class Profile with FastEquatable {
@CopyWithField(immutable: true)
final String id;
final String name;
final AuthSettings authSettings;
late final uuidValue = UuidValue.fromString(id);
static String getNewProfileId() => uuid.v7();
Profile({required this.id, required this.name, AuthSettings? authSettings})
: authSettings = authSettings ?? AuthSettings.withDefaults();
factory Profile.create({required String name, AuthSettings? authSettings}) {
return Profile(
id: getNewProfileId(),
name: name,
authSettings: authSettings,
);
}
@override
List<Object?> get hashParameters => [id, name, authSettings];
factory Profile.fromJson(Map<String, dynamic> json) =>
_$ProfileFromJson(json);
Map<String, dynamic> toJson() => _$ProfileToJson(this);
}
@@ -0,0 +1,87 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'profile.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$ProfileCWProxy {
Profile name(String name);
Profile authSettings(AuthSettings? authSettings);
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Profile(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```dart
/// Profile(...).copyWith(id: 12, name: "My name")
/// ```
Profile call({String name, AuthSettings? authSettings});
}
/// Callable proxy for `copyWith` functionality.
/// Use as `instanceOfProfile.copyWith(...)` or call `instanceOfProfile.copyWith.fieldName(value)` for a single field.
class _$ProfileCWProxyImpl implements _$ProfileCWProxy {
const _$ProfileCWProxyImpl(this._value);
final Profile _value;
@override
Profile name(String name) => call(name: name);
@override
Profile authSettings(AuthSettings? authSettings) =>
call(authSettings: authSettings);
@override
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Profile(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```dart
/// Profile(...).copyWith(id: 12, name: "My name")
/// ```
Profile call({
Object? name = const $CopyWithPlaceholder(),
Object? authSettings = const $CopyWithPlaceholder(),
}) {
return Profile(
id: _value.id,
name: name == const $CopyWithPlaceholder() || name == null
? _value.name
// ignore: cast_nullable_to_non_nullable
: name as String,
authSettings: authSettings == const $CopyWithPlaceholder()
? _value.authSettings
// ignore: cast_nullable_to_non_nullable
: authSettings as AuthSettings?,
);
}
}
extension $ProfileCopyWith on Profile {
/// Returns a callable class used to build a new instance with modified fields.
/// Example: `instanceOfProfile.copyWith(...)` or `instanceOfProfile.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$ProfileCWProxy get copyWith => _$ProfileCWProxyImpl(this);
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Profile _$ProfileFromJson(Map<String, dynamic> json) => Profile(
id: json['id'] as String,
name: json['name'] as String,
authSettings: json['authSettings'] == null
? null
: AuthSettings.fromJson(json['authSettings'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'authSettings': instance.authSettings.toJson(),
};
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/widgets.dart' hide Locale;
import 'package:intl/locale.dart' as intl;
import 'package:locale_resolver/locale_resolver.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/extensions/locale.dart';
part 'locale_resolver.g.dart';
@Riverpod(keepAlive: true)
class LocaleResolverRepository extends _$LocaleResolverRepository {
final _service = LocaleResolver();
final _cache = <intl.Locale, LocalizedResult>{};
Future<LocalizedResult> resolve(intl.Locale locale) async {
final cached = _cache[locale];
if (cached != null) {
return cached;
}
return _cache[locale] = await _service.resolve(
locale.toLanguageTag(),
targetLocale.toLanguageTag(),
);
}
@override
void build(intl.Locale targetLocale) {}
}
@Riverpod()
Future<LocalizedResult> resolveLocale(Ref ref, intl.Locale locale) {
return ref
.read(
localeResolverRepositoryProvider(
WidgetsBinding.instance.platformDispatcher.locale.toIntlLocale(),
).notifier,
)
.resolve(locale);
}
@@ -0,0 +1,184 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'locale_resolver.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(LocaleResolverRepository)
final localeResolverRepositoryProvider = LocaleResolverRepositoryFamily._();
final class LocaleResolverRepositoryProvider
extends $NotifierProvider<LocaleResolverRepository, void> {
LocaleResolverRepositoryProvider._({
required LocaleResolverRepositoryFamily super.from,
required intl.Locale super.argument,
}) : super(
retry: null,
name: r'localeResolverRepositoryProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$localeResolverRepositoryHash();
@override
String toString() {
return r'localeResolverRepositoryProvider'
''
'($argument)';
}
@$internal
@override
LocaleResolverRepository create() => LocaleResolverRepository();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
@override
bool operator ==(Object other) {
return other is LocaleResolverRepositoryProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$localeResolverRepositoryHash() =>
r'dc74908d6ac1f5cc851e2a10dcf33b0dc68b5b15';
final class LocaleResolverRepositoryFamily extends $Family
with
$ClassFamilyOverride<
LocaleResolverRepository,
void,
void,
void,
intl.Locale
> {
LocaleResolverRepositoryFamily._()
: super(
retry: null,
name: r'localeResolverRepositoryProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: false,
);
LocaleResolverRepositoryProvider call(intl.Locale targetLocale) =>
LocaleResolverRepositoryProvider._(argument: targetLocale, from: this);
@override
String toString() => r'localeResolverRepositoryProvider';
}
abstract class _$LocaleResolverRepository extends $Notifier<void> {
late final _$args = ref.$arg as intl.Locale;
intl.Locale get targetLocale => _$args;
void build(intl.Locale targetLocale);
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@ProviderFor(resolveLocale)
final resolveLocaleProvider = ResolveLocaleFamily._();
final class ResolveLocaleProvider
extends
$FunctionalProvider<
AsyncValue<LocalizedResult>,
LocalizedResult,
FutureOr<LocalizedResult>
>
with $FutureModifier<LocalizedResult>, $FutureProvider<LocalizedResult> {
ResolveLocaleProvider._({
required ResolveLocaleFamily super.from,
required intl.Locale super.argument,
}) : super(
retry: null,
name: r'resolveLocaleProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$resolveLocaleHash();
@override
String toString() {
return r'resolveLocaleProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<LocalizedResult> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<LocalizedResult> create(Ref ref) {
final argument = this.argument as intl.Locale;
return resolveLocale(ref, argument);
}
@override
bool operator ==(Object other) {
return other is ResolveLocaleProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$resolveLocaleHash() => r'94d7a9b307a81de372b8b40b06f046acc76e01c8';
final class ResolveLocaleFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<LocalizedResult>, intl.Locale> {
ResolveLocaleFamily._()
: super(
retry: null,
name: r'resolveLocaleProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ResolveLocaleProvider call(intl.Locale locale) =>
ResolveLocaleProvider._(argument: locale, from: this);
@override
String toString() => r'resolveLocaleProvider';
}
@@ -0,0 +1,94 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'package:exceptions/exceptions.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/providers/format.dart';
import 'package:weblibre/features/about/domain/providers.dart';
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
import 'package:weblibre/features/bangs/domain/repositories/sync.dart';
part 'app_initialization.g.dart';
@Riverpod(keepAlive: true)
class AppInitializationService extends _$AppInitializationService {
/// Will de facto restart the app
Future<void> reinitialize() {
ref.invalidateSelf();
return initialize();
}
Future<void> _initPackageInfo() {
//Ensure Package info is loaded
state = Result.success((
initialized: false,
stage: 'Loading Package Info...',
errors: List.empty(),
));
return ref.read(packageInfoProvider.future);
}
Future<Map<BangGroup, Result<void>>> _initBangs() {
state = Result.success((
initialized: false,
stage: 'Synchronizing Bangs...',
errors: List.empty(),
));
return ref
.read(bangSyncRepositoryProvider.notifier)
.syncBundledBangGroups();
}
Future<void> initialize() async {
state = await Result.fromAsync(() async {
final errors = <ErrorMessage>[];
await ref.read(formatProvider.future);
if (!ref.mounted) {
return (initialized: false, stage: null, errors: errors);
}
await _initPackageInfo();
if (!ref.mounted) {
return (initialized: false, stage: null, errors: errors);
}
final bangSyncResults = await _initBangs();
for (final MapEntry(value: result) in bangSyncResults.entries) {
result.onFailure(errors.add);
}
return (initialized: true, stage: null, errors: errors);
});
}
@override
Result<({bool initialized, String? stage, List<ErrorMessage> errors})>
build() {
return Result.success((
initialized: false,
stage: null,
errors: List.empty(),
));
}
}
@@ -0,0 +1,98 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_initialization.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(AppInitializationService)
final appInitializationServiceProvider = AppInitializationServiceProvider._();
final class AppInitializationServiceProvider
extends
$NotifierProvider<
AppInitializationService,
Result<({List<ErrorMessage> errors, bool initialized, String? stage})>
> {
AppInitializationServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'appInitializationServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$appInitializationServiceHash();
@$internal
@override
AppInitializationService create() => AppInitializationService();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(
Result<({List<ErrorMessage> errors, bool initialized, String? stage})>
value,
) {
return $ProviderOverride(
origin: this,
providerOverride:
$SyncValueProvider<
Result<
({List<ErrorMessage> errors, bool initialized, String? stage})
>
>(value),
);
}
}
String _$appInitializationServiceHash() =>
r'c26f968d53b4ea18f7be890c9610c9bf9d300322';
abstract class _$AppInitializationService
extends
$Notifier<
Result<({List<ErrorMessage> errors, bool initialized, String? stage})>
> {
Result<({List<ErrorMessage> errors, bool initialized, String? stage})>
build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<
Result<
({List<ErrorMessage> errors, bool initialized, String? stage})
>,
Result<
({List<ErrorMessage> errors, bool initialized, String? stage})
>
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
Result<
({List<ErrorMessage> errors, bool initialized, String? stage})
>,
Result<
({List<ErrorMessage> errors, bool initialized, String? stage})
>
>,
Result<
({List<ErrorMessage> errors, bool initialized, String? stage})
>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,407 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:io';
import 'dart:ui';
import 'package:exceptions/exceptions.dart';
import 'package:flutter/foundation.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/io_client.dart';
import 'package:nullability/nullability.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:socks5_proxy/socks_client.dart';
import 'package:weblibre/core/http_error_handler.dart';
import 'package:weblibre/data/models/web_page_info.dart';
import 'package:weblibre/extensions/http_encoding.dart';
import 'package:weblibre/extensions/uri.dart';
import 'package:weblibre/features/geckoview/domain/entities/browser_icon.dart';
import 'package:weblibre/features/user/domain/repositories/cache.dart';
import 'package:weblibre/features/web_feed/utils/feed_finder.dart';
import 'package:weblibre/utils/lru_cache.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,
};
final class _InFlightFetch {
final Future<Result<WebPageInfo>> future;
const _InFlightFetch(this.future);
}
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 CacheRepository _cacheRepository;
late final LRUCache<String, BrowserIcon> _browserIconCache;
final _inFlightFetches = <Uri, _InFlightFetch>{};
GenericWebsiteService()
: _iconsService = GeckoIconService(),
_browserIconCache = LRUCache(50, onEvict: (icon) => icon.image.dispose());
@override
void build() {
_cacheRepository = ref.watch(cacheRepositoryProvider.notifier);
}
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,
);
}
static Uri _resolveRelativeUri(Uri baseUri, Uri uri) {
if (!uri.isAbsolute) {
return baseUri.resolveUri(uri);
}
return uri;
}
static bool _isHttpUrl(Uri url) => url.isHttpOrHttps;
static List<Resource> _extractIcons(Uri baseUrl, 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) {
if (Uri.tryParse(href) case final Uri url) {
icons.add(
Resource(
url: _resolveRelativeUri(baseUrl, url).toString(),
type: type,
sizes: sizesToList(link.attributes['sizes']).toList(),
mimeType: mimeType.isNotEmpty ? mimeType : null,
maskable: false,
),
);
}
}
}
}
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) {
if (Uri.tryParse(content) case final Uri url) {
icons.add(
Resource(
type: type,
url: _resolveRelativeUri(baseUrl, url).toString(),
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) {
if (Uri.tryParse(content) case final Uri url) {
icons.add(
Resource(
type: type,
url: _resolveRelativeUri(baseUrl, url).toString(),
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>> fetchPageInfo({
required Uri url,
required bool isImageRequest,
required int? proxyPort,
}) {
return Result.fromAsync(() async {
final result = await compute((args) async {
final [String urlString, bool isImageRequest, int? proxyPort] = args;
final httpClient = HttpClient();
if (proxyPort != null) {
SocksTCPClient.assignToHttpClient(httpClient, [
ProxySettings(InternetAddress.loopbackIPv4, proxyPort),
]);
}
final client = IOClient(httpClient);
try {
final baseUri = Uri.parse(urlString);
final response = await client
.get(baseUri)
.timeout(const Duration(seconds: 15));
//When this is a request for an icon and we hit an image, directly return it
if (isImageRequest) {
final contentType = response.headers['content-type'];
if (contentType?.contains('image/') == true) {
return {
'imageBytes': [response.bodyBytes],
};
}
}
final document = html_parser.parse(response.bodyUnicodeFallback);
final title = document.querySelector('title')?.text;
final resources = _extractIcons(baseUri, document);
final feeds = await FeedFinder(
url: baseUri,
document: document,
).parse();
return {
'title': title,
'resources': resources.map(_serializeResource).toList(),
'feeds': feeds.map((uri) => uri.toString()).toList(),
};
} finally {
client.close();
}
}, <dynamic>[url.toString(), isImageRequest, proxyPort]);
if (result['imageBytes'] case final Uint8List imageBytes) {
return WebPageInfo(
url: url,
favicon: await BrowserIcon.fromBytes(
imageBytes,
dominantColor: null,
source: IconSource.download,
),
);
}
final resources = (result['resources']! as List<Map<String, dynamic>>)
.map(_deserializeResource)
.toList();
final favicon =
await getCachedIcon(url) ??
await loadIcon(url: url, resources: resources);
return WebPageInfo(
url: url,
title: (result['title'] as String?)?.trim(),
favicon: favicon,
feeds: Set.from(
(result['feeds']! as List<String>).map((url) => Uri.tryParse(url)),
),
);
}, exceptionHandler: handleHttpError);
}
Future<BrowserIcon?> getCachedIcon(Uri url) async {
if (_isHttpUrl(url)) {
final cachedBrowserIcon = _browserIconCache.get(url.origin);
if (cachedBrowserIcon?.image.value != null) {
return cachedBrowserIcon;
} else if (cachedBrowserIcon != null) {
_browserIconCache.remove(url.origin);
}
final cachedIcon = await _cacheRepository.getCachedIcon(url.origin);
if (cachedIcon != null) {
return _browserIconCache.set(
url.origin,
await BrowserIcon.fromBytes(
cachedIcon,
dominantColor: null,
source: IconSource.disk,
),
);
}
}
return null;
}
Future<BrowserIcon> loadIcon({
required Uri url,
required List<Resource> resources,
bool isPrivate = false,
bool waitOnNetworkLoad = true,
}) async {
final result = await _iconsService.loadIcon(
url: url,
resources: resources,
isPrivate: isPrivate,
waitOnNetworkLoad: waitOnNetworkLoad,
);
if (result.source != IconSource.generator &&
result.source != IconSource.memory) {
await _cacheRepository.cacheIcon(url, result.image);
}
return _browserIconCache.set(
url.origin,
await BrowserIcon.fromBytes(
result.image,
dominantColor: result.color.mapNotNull((color) => Color(color)),
source: result.source,
),
);
}
Future<Result<WebPageInfo>> _deduplicatedFetchPageInfo(Uri url) {
final existing = _inFlightFetches[url];
if (existing != null) {
return existing.future;
}
late final Future<Result<WebPageInfo>> inFlightFetch;
inFlightFetch = fetchPageInfo(url: url, isImageRequest: true, proxyPort: null)
.timeout(
const Duration(seconds: 20),
onTimeout: () => Result.failure(
const ErrorMessage(source: 'icon', message: 'Icon fetch timeout'),
),
)
.whenComplete(() {
// Only clear this entry if it is still the active in-flight request.
if (identical(_inFlightFetches[url]?.future, inFlightFetch)) {
_inFlightFetches.remove(url);
}
});
_inFlightFetches[url] = _InFlightFetch(inFlightFetch);
return inFlightFetch;
}
Future<BrowserIcon?> getUrlIcon(List<Uri> urlList) async {
for (final url in urlList) {
if (!_isHttpUrl(url)) {
continue;
}
final cachedIcon = await getCachedIcon(url);
if (cachedIcon != null) {
return cachedIcon;
}
if (ref.mounted) {
final result = await _deduplicatedFetchPageInfo(url);
if (result.isSuccess) {
if (result.value.favicon case final BrowserIcon favicon) {
if (!_browserIconCache.contains(url.origin)) {
_browserIconCache.set(url.origin, favicon);
}
return favicon;
}
}
}
}
return null;
}
}
@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'generic_website.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(GenericWebsiteService)
final genericWebsiteServiceProvider = GenericWebsiteServiceProvider._();
final class GenericWebsiteServiceProvider
extends $NotifierProvider<GenericWebsiteService, void> {
GenericWebsiteServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'genericWebsiteServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$genericWebsiteServiceHash();
@$internal
@override
GenericWebsiteService create() => GenericWebsiteService();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$genericWebsiteServiceHash() =>
r'a2bd892f0ca07467eaa906648e57f232a7e50155';
abstract class _$GenericWebsiteService extends $Notifier<void> {
void build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}