Cache favicons

This commit is contained in:
Juan Gilsanz Polo
2024-02-25 14:42:39 +01:00
parent cf13d76a68
commit c19ffed623
11 changed files with 297 additions and 10 deletions

View File

@@ -6,7 +6,7 @@
/// Locales: 2 /// Locales: 2
/// Strings: 234 (117 per locale) /// Strings: 234 (117 per locale)
/// ///
/// Built on 2024-02-25 at 03:56 UTC /// Built on 2024-02-25 at 12:46 UTC
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint

View File

@@ -7,6 +7,8 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:linkdy/constants/global_keys.dart'; import 'package:linkdy/constants/global_keys.dart';
import 'package:linkdy/providers/temporary_directory.provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
@@ -35,12 +37,15 @@ void main() async {
final appInfo = await PackageInfo.fromPlatform(); final appInfo = await PackageInfo.fromPlatform();
final baseDir = await getTemporaryDirectory();
void startApp() => runApp( void startApp() => runApp(
TranslationProvider( TranslationProvider(
child: ProviderScope( child: ProviderScope(
overrides: [ overrides: [
sharedPreferencesProvider.overrideWithValue(sharedPreferences), sharedPreferencesProvider.overrideWithValue(sharedPreferences),
appInfoProvider.overrideWithValue(appInfo), appInfoProvider.overrideWithValue(appInfo),
temporaryDirectoryProvider.overrideWithValue(baseDir),
], ],
child: const MyApp(), child: const MyApp(),
), ),

View File

@@ -0,0 +1,11 @@
class FaviconItem {
final String url;
final String favicon;
final bool isSvg;
const FaviconItem({
required this.url,
required this.favicon,
required this.isSvg,
});
}

View File

@@ -0,0 +1,10 @@
import 'dart:io';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'temporary_directory.provider.g.dart';
@Riverpod(keepAlive: true)
Directory temporaryDirectory(TemporaryDirectoryRef ref) {
throw UnimplementedError();
}

View File

@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'temporary_directory.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$temporaryDirectoryHash() =>
r'7d9abadfdaac781835728fd72f6672186d5410d3';
/// See also [temporaryDirectory].
@ProviderFor(temporaryDirectory)
final temporaryDirectoryProvider = Provider<Directory>.internal(
temporaryDirectory,
name: r'temporaryDirectoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$temporaryDirectoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef TemporaryDirectoryRef = ProviderRef<Directory>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -0,0 +1,9 @@
import 'package:linkdy/models/favicon_item.dart';
class FaviconLoaderModel {
List<FaviconItem> favicons;
FaviconLoaderModel({
required this.favicons,
});
}

View File

@@ -0,0 +1,47 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:favicon/favicon.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:linkdy/screens/bookmarks/model/favicon_loader.model.dart';
import 'package:linkdy/models/favicon_item.dart';
part 'favicon_loader.provider.g.dart';
@riverpod
FutureOr<FaviconItem?> fetchFavicon(FetchFaviconRef ref, String bookmarkUrl) async {
final exists = ref.watch(faviconStoreProvider).favicons.where((favicon) => favicon.url == bookmarkUrl).toList();
if (exists.isNotEmpty) {
return exists[0];
}
final faviconResult = await FaviconFinder.getBest(bookmarkUrl);
if (faviconResult == null) {
return null;
}
final result = await http.get(Uri.parse(faviconResult.url));
if (result.statusCode == 200) {
final faviconItem = FaviconItem(
url: bookmarkUrl,
favicon: faviconResult.url.contains("svg") ? result.body : base64.encode(result.bodyBytes),
isSvg: faviconResult.url.contains("svg"),
);
ref.read(faviconStoreProvider).favicons.add(faviconItem);
return faviconItem;
} else {
return null;
}
}
@Riverpod(keepAlive: true)
class FaviconStore extends _$FaviconStore {
@override
FaviconLoaderModel build() {
return FaviconLoaderModel(
favicons: [],
);
}
}

View File

@@ -0,0 +1,175 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'favicon_loader.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$fetchFaviconHash() => r'90d9f0060aa68162174a1d76a6faeee5ae820494';
/// 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 [fetchFavicon].
@ProviderFor(fetchFavicon)
const fetchFaviconProvider = FetchFaviconFamily();
/// See also [fetchFavicon].
class FetchFaviconFamily extends Family<AsyncValue<FaviconItem?>> {
/// See also [fetchFavicon].
const FetchFaviconFamily();
/// See also [fetchFavicon].
FetchFaviconProvider call(
String bookmarkUrl,
) {
return FetchFaviconProvider(
bookmarkUrl,
);
}
@override
FetchFaviconProvider getProviderOverride(
covariant FetchFaviconProvider provider,
) {
return call(
provider.bookmarkUrl,
);
}
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'fetchFaviconProvider';
}
/// See also [fetchFavicon].
class FetchFaviconProvider extends AutoDisposeFutureProvider<FaviconItem?> {
/// See also [fetchFavicon].
FetchFaviconProvider(
String bookmarkUrl,
) : this._internal(
(ref) => fetchFavicon(
ref as FetchFaviconRef,
bookmarkUrl,
),
from: fetchFaviconProvider,
name: r'fetchFaviconProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$fetchFaviconHash,
dependencies: FetchFaviconFamily._dependencies,
allTransitiveDependencies:
FetchFaviconFamily._allTransitiveDependencies,
bookmarkUrl: bookmarkUrl,
);
FetchFaviconProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.bookmarkUrl,
}) : super.internal();
final String bookmarkUrl;
@override
Override overrideWith(
FutureOr<FaviconItem?> Function(FetchFaviconRef provider) create,
) {
return ProviderOverride(
origin: this,
override: FetchFaviconProvider._internal(
(ref) => create(ref as FetchFaviconRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
bookmarkUrl: bookmarkUrl,
),
);
}
@override
AutoDisposeFutureProviderElement<FaviconItem?> createElement() {
return _FetchFaviconProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is FetchFaviconProvider && other.bookmarkUrl == bookmarkUrl;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, bookmarkUrl.hashCode);
return _SystemHash.finish(hash);
}
}
mixin FetchFaviconRef on AutoDisposeFutureProviderRef<FaviconItem?> {
/// The parameter `bookmarkUrl` of this provider.
String get bookmarkUrl;
}
class _FetchFaviconProviderElement
extends AutoDisposeFutureProviderElement<FaviconItem?>
with FetchFaviconRef {
_FetchFaviconProviderElement(super.provider);
@override
String get bookmarkUrl => (origin as FetchFaviconProvider).bookmarkUrl;
}
String _$faviconStoreHash() => r'a082b856f1564d04dfd9209b7c2efaff6f146e68';
/// See also [FaviconStore].
@ProviderFor(FaviconStore)
final faviconStoreProvider =
NotifierProvider<FaviconStore, FaviconLoaderModel>.internal(
FaviconStore.new,
name: r'faviconStoreProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$faviconStoreHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$FaviconStore = Notifier<FaviconLoaderModel>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -1,9 +1,12 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:favicon/favicon.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:linkdy/screens/bookmarks/provider/favicon_loader.provider.dart';
import 'package:linkdy/models/data/bookmarks.dart'; import 'package:linkdy/models/data/bookmarks.dart';
import 'package:linkdy/providers/app_status_provider.dart'; import 'package:linkdy/providers/app_status_provider.dart';
import 'package:linkdy/providers/router_provider.dart'; import 'package:linkdy/providers/router_provider.dart';
@@ -44,7 +47,7 @@ class BookmarkItem extends ConsumerWidget {
children: [ children: [
if (ref.watch(appStatusProvider).showFavicon == true) if (ref.watch(appStatusProvider).showFavicon == true)
FutureBuilder( FutureBuilder(
future: FaviconFinder.getBest(bookmark.url!), future: ref.read(fetchFaviconProvider(bookmark.url!).future),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData == false) { if (snapshot.hasData == false) {
return Padding( return Padding(
@@ -65,15 +68,15 @@ class BookmarkItem extends ConsumerWidget {
} }
return Row( return Row(
children: [ children: [
if (snapshot.data!.url.contains("svg")) if (snapshot.data!.isSvg == true)
SvgPicture.network( SvgPicture.string(
snapshot.data!.url, snapshot.data!.favicon,
width: 16, width: 16,
height: 16, height: 16,
), ),
if (!snapshot.data!.url.contains("svg")) if (snapshot.data!.isSvg == false)
Image.network( Image.memory(
snapshot.data!.url, base64Decode(snapshot.data!.favicon),
width: 16, width: 16,
height: 16, height: 16,
), ),

View File

@@ -558,7 +558,7 @@ packages:
source: hosted source: hosted
version: "0.15.4" version: "0.15.4"
http: http:
dependency: transitive dependency: "direct main"
description: description:
name: http name: http
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba

View File

@@ -55,6 +55,7 @@ dependencies:
flutter_dotenv: ^5.1.0 flutter_dotenv: ^5.1.0
favicon: ^1.1.2 favicon: ^1.1.2
skeletonizer: ^1.0.1 skeletonizer: ^1.0.1
http: ^1.2.0
dev_dependencies: dev_dependencies:
build_runner: ^2.4.8 build_runner: ^2.4.8