import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:html/parser.dart'; import 'package:result_monad/result_monad.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../models/exec_error.dart'; import '../../models/link_preview_data.dart'; import '../rp_provider_extension.dart'; import 'network_services.dart'; part 'link_preview_services.g.dart'; const ogTitleKey = 'og:title'; const ogDescriptionKey = 'og:description'; const ogSiteNameKey = 'og:site_name'; const ogImageKey = 'og:image'; @riverpod Future> linkPreview( Ref ref, String url) async { return await ref.read(_openGraphDataProvider(url).future).transform((ogData) { final title = ogData.getValue(ogTitleKey); final description = ogData.getValue(ogDescriptionKey); final siteName = ogData.getValue(ogSiteNameKey); final availableImageUrls = ogData.getValues(ogImageKey); final selectedImageUrl = availableImageUrls.isEmpty ? '' : availableImageUrls.first; return LinkPreviewData( link: url, title: title, description: description, siteName: siteName, availableImageUrls: availableImageUrls, selectedImageUrl: selectedImageUrl, ); }).execErrorCastAsync(); } @riverpod Future>, ExecError>> _openGraphData( Ref ref, String url) async { ref.cacheFor(const Duration(minutes: 5)); final headers = ref.watch(userAgentHeaderProvider); final request = NetworkRequest(Uri.parse(url), headers: headers); return await ref .read(httpGetRawBytesProvider(request).future) .transform((bodyBytes) { final rawHtml = utf8.decode(bodyBytes); final htmlDoc = parse(rawHtml); final openGraphTags = htmlDoc.head ?.querySelectorAll("[property*='og:']") .map((p) { final key = p.attributes['property'] ?? ''; final value = p.attributes['content'] ?? ''; return MapEntry(key, value); }) .where((e) => e.key.isNotEmpty) .toList(); return openGraphTags ?? []; }).mapError((error) { final type = error is ExecError ? error.type : ErrorType.serverError; final message = error is ExecError ? 'Error getting link preview: ${error.message}' : 'Error getting link preview'; return ExecError(type: type, message: message); }); } extension OpenGraphFinders on List> { String getValue(String key) { return firstWhere( (e) => e.key == key, orElse: () => const MapEntry('', ''), ).value; } List getValues(String key) { return where((e) => e.key == key).map((e) => e.value).toList(); } }