From acb3895924bc4d8c78b4096ea6e7f8e3df23297a Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su <30667958+hjiangsu@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:45:29 -0700 Subject: [PATCH] added option to select thumbnail quality --- lib/core/enums/local_settings.dart | 2 ++ lib/core/enums/media_type.dart | 20 ++++++++++++++ lib/l10n/app_en.arb | 4 +++ lib/post/utils/post.dart | 16 +++++++++--- .../pages/post_appearance_settings_page.dart | 26 +++++++++++++++++++ lib/thunder/bloc/thunder_bloc.dart | 3 +++ lib/thunder/bloc/thunder_state.dart | 5 ++++ lib/utils/media/image.dart | 13 +++++++--- 8 files changed, 83 insertions(+), 6 deletions(-) diff --git a/lib/core/enums/local_settings.dart b/lib/core/enums/local_settings.dart index ae7513b7e..9f7a5234c 100644 --- a/lib/core/enums/local_settings.dart +++ b/lib/core/enums/local_settings.dart @@ -109,6 +109,7 @@ enum LocalSettings { useCompactView(name: 'setting_general_use_compact_view', key: 'compactView', category: LocalSettingsCategories.posts, subCategory: LocalSettingsSubCategories.posts), showPostTitleFirst(name: 'setting_general_show_title_first', key: 'showPostTitleFirst', category: LocalSettingsCategories.posts, subCategory: LocalSettingsSubCategories.posts), hideThumbnails(name: 'setting_general_hide_thumbnails', key: 'hideThumbnails', category: LocalSettingsCategories.posts, subCategory: LocalSettingsSubCategories.feed), + thumbnailQuality(name: 'setting_general_thumbnail_quality', key: 'thumbnailQuality', category: LocalSettingsCategories.posts, subCategory: LocalSettingsSubCategories.feed), showThumbnailPreviewOnRight( name: 'setting_compact_show_thumbnail_on_right', key: 'showThumbnailPreviewOnRight', category: LocalSettingsCategories.posts, subCategory: LocalSettingsSubCategories.posts), showTextPostIndicator(name: 'setting_compact_show_text_post_indicator', key: 'showTextPostIndicator', category: LocalSettingsCategories.posts, subCategory: LocalSettingsSubCategories.posts), @@ -334,6 +335,7 @@ extension LocalizationExt on AppLocalizations { 'compactView': compactView, 'showPostTitleFirst': showPostTitleFirst, 'hideThumbnails': hideThumbnails, + 'thumbnailQuality': thumbnailQuality, 'showThumbnailPreviewOnRight': showThumbnailPreviewOnRight, 'showTextPostIndicator': showTextPostIndicator, 'tappableAuthorCommunity': tappableAuthorCommunity, diff --git a/lib/core/enums/media_type.dart b/lib/core/enums/media_type.dart index b995513c4..68dbce3aa 100644 --- a/lib/core/enums/media_type.dart +++ b/lib/core/enums/media_type.dart @@ -1 +1,21 @@ enum MediaType { image, video, link, text } + +enum MediaQuality { + full, + high, + medium, + low; + + get size { + switch (this) { + case MediaQuality.full: + return null; + case MediaQuality.high: + return 1080; + case MediaQuality.medium: + return 720; + case MediaQuality.low: + return 480; + } + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b3eedc7ff..9fa3abf33 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1799,6 +1799,10 @@ "@thickness": { "description": "Describes a thickness (e.g., divider)" }, + "thumbnailQuality": "Thumbnail Quality", + "@thumbnailQuality": { + "description": "Setting for thumbnail quality" + }, "thunderHasBeenUpdated": "Thunder updated to {version}!", "@thunderHasBeenUpdated": { "description": "Heading for changelog when Thunder has been updated" diff --git a/lib/post/utils/post.dart b/lib/post/utils/post.dart index 04c9d9c46..5d63dfbfb 100644 --- a/lib/post/utils/post.dart +++ b/lib/post/utils/post.dart @@ -240,6 +240,7 @@ Future> parsePostViews(List postViews, {String? re bool tabletMode = prefs.getBool(LocalSettings.useTabletMode.name) ?? false; bool hideNsfwPosts = prefs.getBool(LocalSettings.hideNsfwPosts.name) ?? false; bool scrapeMissingPreviews = prefs.getBool(LocalSettings.scrapeMissingPreviews.name) ?? false; + MediaQuality thumbnailQuality = MediaQuality.values.byName(prefs.getString(LocalSettings.thumbnailQuality.name) ?? MediaQuality.medium.name); List postViewsFinal = []; @@ -261,7 +262,7 @@ Future> parsePostViews(List postViews, {String? re Iterable> postFutures = postViewsFinal .expand( (post) => [ - if (!hideNsfwPosts || (!post.post.nsfw && hideNsfwPosts)) parsePostView(post, fetchImageDimensions, edgeToEdgeImages, tabletMode, scrapeMissingPreviews), + if (!hideNsfwPosts || (!post.post.nsfw && hideNsfwPosts)) parsePostView(post, fetchImageDimensions, edgeToEdgeImages, tabletMode, scrapeMissingPreviews, thumbnailQuality), ], ) .toList(); @@ -270,7 +271,7 @@ Future> parsePostViews(List postViews, {String? re return posts; } -Future parsePostView(PostView postView, bool fetchImageDimensions, bool edgeToEdgeImages, bool tabletMode, bool scrapeMissingPreviews) async { +Future parsePostView(PostView postView, bool fetchImageDimensions, bool edgeToEdgeImages, bool tabletMode, bool scrapeMissingPreviews, MediaQuality thumbnailQuality) async { List mediaList = []; // There are three sources of URLs: the main url attached to the post, the thumbnail url attached to the post, and the video url attached to the post @@ -299,6 +300,15 @@ Future parsePostView(PostView postView, bool fetchImageDimensions if (thumbnailUrl != null && thumbnailUrl.isNotEmpty) { // Now check to see if there is a thumbnail image. If there is, we'll use that for the image media.mediaUrl = thumbnailUrl; + + // The thumbnail is typically from the /pictrs/ endpoint, so we can specify the height of the image. This will reduce resolution, but will speed up loading + if (isPictrsEndpoint(thumbnailUrl)) { + if (thumbnailQuality != MediaQuality.full) { + media.mediaUrl = '$thumbnailUrl?thumbnail=${thumbnailQuality.size}&format=png'; + } else { + media.mediaUrl = '$thumbnailUrl?format=png'; + } + } } else if (isImage) { // If there is no thumbnail image, but the url is an image, we'll use that for the mediaUrl media.mediaUrl = url; @@ -316,7 +326,7 @@ Future parsePostView(PostView postView, bool fetchImageDimensions Size result = Size(MediaQuery.of(GlobalContext.context).size.width, 200); try { - result = await retrieveImageDimensions(imageUrl: media.mediaUrl ?? media.originalUrl).timeout(const Duration(seconds: 2)); + result = await retrieveImageDimensions(imageUrl: media.mediaUrl ?? media.originalUrl).timeout(const Duration(seconds: 4)); } catch (e) { debugPrint('${media.mediaUrl ?? media.originalUrl} - $e: Falling back to default image size'); } diff --git a/lib/settings/pages/post_appearance_settings_page.dart b/lib/settings/pages/post_appearance_settings_page.dart index d49c01d1a..92066654c 100644 --- a/lib/settings/pages/post_appearance_settings_page.dart +++ b/lib/settings/pages/post_appearance_settings_page.dart @@ -16,6 +16,7 @@ import 'package:thunder/community/widgets/post_card_view_compact.dart'; import 'package:thunder/core/enums/custom_theme_type.dart'; import 'package:thunder/core/enums/feed_card_divider_thickness.dart'; import 'package:thunder/core/enums/local_settings.dart'; +import 'package:thunder/core/enums/media_type.dart'; import 'package:thunder/core/enums/post_body_view_type.dart'; import 'package:thunder/core/enums/view_mode.dart'; import 'package:thunder/core/models/post_view_media.dart'; @@ -54,6 +55,9 @@ class _PostAppearanceSettingsPageState extends State /// When enabled, the thumbnails in compact/card mode will be hidden bool hideThumbnails = false; + /// The quality of the thumbnails + MediaQuality thumbnailQuality = MediaQuality.medium; + /// When enabled, the thumbnail previews will be shown on the right. By default, they are shown on the left bool showThumbnailPreviewOnRight = false; @@ -143,6 +147,7 @@ class _PostAppearanceSettingsPageState extends State useCompactView = prefs.getBool(LocalSettings.useCompactView.name) ?? false; hideNsfwPreviews = prefs.getBool(LocalSettings.hideNsfwPreviews.name) ?? true; hideThumbnails = prefs.getBool(LocalSettings.hideThumbnails.name) ?? false; + thumbnailQuality = MediaQuality.values.byName(prefs.getString(LocalSettings.thumbnailQuality.name) ?? MediaQuality.medium.name); showPostAuthor = prefs.getBool(LocalSettings.showPostAuthor.name) ?? false; useDisplayNames = prefs.getBool(LocalSettings.useDisplayNamesForUsers.name) ?? true; postShowUserInstance = prefs.getBool(LocalSettings.postShowUserInstance.name) ?? false; @@ -194,6 +199,10 @@ class _PostAppearanceSettingsPageState extends State await prefs.setBool(LocalSettings.hideThumbnails.name, value); setState(() => hideThumbnails = value); break; + case LocalSettings.thumbnailQuality: + await prefs.setString(LocalSettings.thumbnailQuality.name, (value as MediaQuality).name); + setState(() => thumbnailQuality = value); + break; case LocalSettings.showPostAuthor: await prefs.setBool(LocalSettings.showPostAuthor.name, value); setState(() => showPostAuthor = value); @@ -304,6 +313,7 @@ class _PostAppearanceSettingsPageState extends State await prefs.remove(LocalSettings.useCompactView.name); await prefs.remove(LocalSettings.hideNsfwPreviews.name); await prefs.remove(LocalSettings.hideThumbnails.name); + await prefs.remove(LocalSettings.thumbnailQuality.name); await prefs.remove(LocalSettings.showPostAuthor.name); await prefs.remove(LocalSettings.useDisplayNamesForUsers.name); await prefs.remove(LocalSettings.postShowUserInstance.name); @@ -600,6 +610,22 @@ class _PostAppearanceSettingsPageState extends State highlightKey: settingToHighlight == LocalSettings.hideThumbnails ? settingToHighlightKey : null, ), ), + SliverToBoxAdapter( + child: ListOption( + description: l10n.thumbnailQuality, + subtitle: "Only supported for thumbnails hosted on instance", + value: ListPickerItem(label: thumbnailQuality.name, icon: Icons.high_quality_rounded, payload: thumbnailQuality), + options: [ + ListPickerItem(icon: Icons.star_rounded, label: MediaQuality.full.name, payload: MediaQuality.full), + ListPickerItem(icon: Icons.expand_less_rounded, label: MediaQuality.high.name, payload: MediaQuality.high), + ListPickerItem(icon: Icons.fit_screen, label: MediaQuality.medium.name, payload: MediaQuality.medium), + ListPickerItem(icon: Icons.expand_more_rounded, label: MediaQuality.low.name, payload: MediaQuality.low), + ], + icon: Icons.high_quality_rounded, + onChanged: (value) async => setPreferences(LocalSettings.thumbnailQuality, value.payload), + highlightKey: settingToHighlight == LocalSettings.thumbnailQuality ? settingToHighlightKey : null, + ), + ), SliverToBoxAdapter( child: ToggleOption( description: l10n.showPostAuthor, diff --git a/lib/thunder/bloc/thunder_bloc.dart b/lib/thunder/bloc/thunder_bloc.dart index 4b9bc6d77..1a8042a07 100644 --- a/lib/thunder/bloc/thunder_bloc.dart +++ b/lib/thunder/bloc/thunder_bloc.dart @@ -17,6 +17,7 @@ import 'package:thunder/core/enums/font_scale.dart'; import 'package:thunder/core/enums/full_name.dart'; import 'package:thunder/core/enums/image_caching_mode.dart'; import 'package:thunder/core/enums/local_settings.dart'; +import 'package:thunder/core/enums/media_type.dart'; import 'package:thunder/core/enums/nested_comment_indicator.dart'; import 'package:thunder/notification/enums/notification_type.dart'; import 'package:thunder/core/enums/post_body_view_type.dart'; @@ -142,6 +143,7 @@ class ThunderBloc extends Bloc { bool useCompactView = prefs.getBool(LocalSettings.useCompactView.name) ?? false; bool showTitleFirst = prefs.getBool(LocalSettings.showPostTitleFirst.name) ?? false; bool hideThumbnails = prefs.getBool(LocalSettings.hideThumbnails.name) ?? false; + MediaQuality thumbnailQuality = MediaQuality.values.byName(prefs.getString(LocalSettings.thumbnailQuality.name) ?? MediaQuality.medium.name); bool showThumbnailPreviewOnRight = prefs.getBool(LocalSettings.showThumbnailPreviewOnRight.name) ?? false; bool showTextPostIndicator = prefs.getBool(LocalSettings.showTextPostIndicator.name) ?? false; bool tappableAuthorCommunity = prefs.getBool(LocalSettings.tappableAuthorCommunity.name) ?? false; @@ -299,6 +301,7 @@ class ThunderBloc extends Bloc { useCompactView: useCompactView, showTitleFirst: showTitleFirst, hideThumbnails: hideThumbnails, + thumbnailQuality: thumbnailQuality, showThumbnailPreviewOnRight: showThumbnailPreviewOnRight, showTextPostIndicator: showTextPostIndicator, tappableAuthorCommunity: tappableAuthorCommunity, diff --git a/lib/thunder/bloc/thunder_state.dart b/lib/thunder/bloc/thunder_state.dart index 4ea7d4ee2..919083495 100644 --- a/lib/thunder/bloc/thunder_state.dart +++ b/lib/thunder/bloc/thunder_state.dart @@ -53,6 +53,7 @@ class ThunderState extends Equatable { this.useCompactView = false, this.showTitleFirst = false, this.hideThumbnails = false, + this.thumbnailQuality = MediaQuality.medium, this.showThumbnailPreviewOnRight = false, this.showTextPostIndicator = false, this.tappableAuthorCommunity = false, @@ -207,6 +208,7 @@ class ThunderState extends Equatable { final bool useCompactView; final bool showTitleFirst; final bool hideThumbnails; + final MediaQuality thumbnailQuality; final bool showThumbnailPreviewOnRight; final bool showTextPostIndicator; final bool tappableAuthorCommunity; @@ -368,6 +370,7 @@ class ThunderState extends Equatable { bool? useCompactView, bool? showTitleFirst, bool? hideThumbnails, + MediaQuality? thumbnailQuality, bool? showThumbnailPreviewOnRight, bool? showTextPostIndicator, bool? tappableAuthorCommunity, @@ -524,6 +527,7 @@ class ThunderState extends Equatable { useCompactView: useCompactView ?? this.useCompactView, showTitleFirst: showTitleFirst ?? this.showTitleFirst, hideThumbnails: hideThumbnails ?? this.hideThumbnails, + thumbnailQuality: thumbnailQuality ?? this.thumbnailQuality, showThumbnailPreviewOnRight: showThumbnailPreviewOnRight ?? this.showThumbnailPreviewOnRight, showTextPostIndicator: showTextPostIndicator ?? this.showTextPostIndicator, tappableAuthorCommunity: tappableAuthorCommunity ?? this.tappableAuthorCommunity, @@ -683,6 +687,7 @@ class ThunderState extends Equatable { useCompactView, showTitleFirst, hideThumbnails, + thumbnailQuality, showThumbnailPreviewOnRight, showTextPostIndicator, tappableAuthorCommunity, diff --git a/lib/utils/media/image.dart b/lib/utils/media/image.dart index 0fee8b7bd..2a8e4e519 100644 --- a/lib/utils/media/image.dart +++ b/lib/utils/media/image.dart @@ -12,9 +12,8 @@ import 'package:thunder/account/models/account.dart'; import 'package:thunder/shared/image_viewer.dart'; import 'package:thunder/shared/snackbar.dart'; -String generateRandomHeroString({int? len}) { - Random r = Random(); - return String.fromCharCodes(List.generate(len ?? 32, (index) => r.nextInt(33) + 89)); +bool isPictrsEndpoint(String url) { + return url.contains('/pictrs/image/'); } bool isImageUrl(String url) { @@ -26,6 +25,7 @@ bool isImageUrl(String url) { } catch (e) { return false; } + final path = uri.path.toLowerCase(); for (final extension in imageExtensions) { @@ -70,6 +70,7 @@ Future retrieveImageDimensions({String? imageUrl, Uint8List? imageBytes}) // are all PNGs. return getPNGImageDimensions(imageBytes); } + // We know imageUrl is not null here due to the assertion. else { bool isImage = isImageUrl(imageUrl!); @@ -77,6 +78,7 @@ Future retrieveImageDimensions({String? imageUrl, Uint8List? imageBytes}) final uri = Uri.parse(imageUrl); final path = uri.path.toLowerCase(); + final query = uri.queryParameters; // We'll just retrieve the first part of the image final rangeResponse = await http.get( @@ -87,6 +89,11 @@ Future retrieveImageDimensions({String? imageUrl, Uint8List? imageBytes}) // Read the response body as bytes final imageData = rangeResponse.bodyBytes; + // Override the image type if it's a Pictrs endpoint since we specify the format + if (isPictrsEndpoint(imageUrl) && query.containsKey("format") && query["format"] == "png") { + return getPNGImageDimensions(imageData); + } + // Get the image dimensions if (path.endsWith('jpg') || path.endsWith('jpeg')) { return getJPEGImageDimensions(imageData);