Skip to content

Commit

Permalink
Image and video player improvements (#1369)
Browse files Browse the repository at this point in the history
* improved image dimension logic, added video logic when parsing post views

* added back button to video player, added snackbar when video fails to load

* added button to open in browser, removed play icon from media view, misc fixes

* fix media view UI inconsistencies

* fixed various issues with compact and card view rendering

* added mute functionality to youtube player, fixed video settings not applying immediately, fixed network connectivity checking

* fix status bar not showing after full screen
  • Loading branch information
hjiangsu committed May 23, 2024
1 parent ac6da24 commit 239379e
Show file tree
Hide file tree
Showing 17 changed files with 393 additions and 233 deletions.
1 change: 1 addition & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extensions:
62 changes: 62 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
PODS:
- background_fetch (1.3.2):
- Flutter
- Cache (6.0.0)
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
Expand All @@ -10,6 +14,13 @@ PODS:
- Flutter
- flutter_icmp_ping (0.0.1):
- Flutter
- flutter_inappwebview (0.0.1):
- Flutter
- flutter_inappwebview/Core (= 0.0.1)
- OrderedSet (~> 5.0)
- flutter_inappwebview/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
- flutter_keyboard_visibility (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
Expand All @@ -21,19 +32,40 @@ PODS:
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GCDWebServer (3.5.4):
- GCDWebServer/Core (= 3.5.4)
- GCDWebServer/Core (3.5.4)
- HLSCachingReverseProxyServer (0.1.0):
- GCDWebServer (~> 3.5)
- PINCache (>= 3.0.1-beta.3)
- image_picker_ios (0.0.1):
- Flutter
- OrderedSet (5.0.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- PINCache (3.0.4):
- PINCache/Arc-exception-safe (= 3.0.4)
- PINCache/Core (= 3.0.4)
- PINCache/Arc-exception-safe (3.0.4):
- PINCache/Core
- PINCache/Core (3.0.4):
- PINOperation (~> 1.2.3)
- PINOperation (1.2.3)
- pointer_interceptor_ios (0.0.1):
- Flutter
- push_ios (0.0.1):
- Flutter
- river_player (0.0.1):
- Cache (~> 6.0.0)
- Flutter
- GCDWebServer
- HLSCachingReverseProxyServer
- PINCache
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
Expand Down Expand Up @@ -61,16 +93,20 @@ PODS:
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter

DEPENDENCIES:
- background_fetch (from `.symlinks/plugins/background_fetch/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_custom_tabs_ios (from `.symlinks/plugins/flutter_custom_tabs_ios/ios`)
- flutter_file_dialog (from `.symlinks/plugins/flutter_file_dialog/ios`)
- flutter_icmp_ping (from `.symlinks/plugins/flutter_icmp_ping/ios`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
Expand All @@ -82,21 +118,31 @@ DEPENDENCIES:
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- push_ios (from `.symlinks/plugins/push_ios/ios`)
- river_player (from `.symlinks/plugins/river_player/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)

SPEC REPOS:
trunk:
- Cache
- GCDWebServer
- HLSCachingReverseProxyServer
- OrderedSet
- PINCache
- PINOperation
- sqlite3

EXTERNAL SOURCES:
background_fetch:
:path: ".symlinks/plugins/background_fetch/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
Expand All @@ -107,6 +153,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_file_dialog/ios"
flutter_icmp_ping:
:path: ".symlinks/plugins/flutter_icmp_ping/ios"
flutter_inappwebview:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_local_notifications:
Expand All @@ -129,6 +177,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
push_ios:
:path: ".symlinks/plugins/push_ios/ios"
river_player:
:path: ".symlinks/plugins/river_player/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
Expand All @@ -141,34 +191,46 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/uni_links/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"

SPEC CHECKSUMS:
background_fetch: 2319bf7e18237b4b269430b7f14d177c0df09c5a
Cache: 4ca7e00363fca5455f26534e5607634c820ffc2d
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_custom_tabs_ios: 62439c843b2691aae516fd50119a01eb9755fff7
flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299
flutter_icmp_ping: 2b159955eee0c487c766ad83fec224ae35e7c935
flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GCDWebServer: 2c156a56c8226e2d5c0c3f208a3621ccffbe3ce4
HLSCachingReverseProxyServer: 59935e1e0244ad7f3375d75b5ef46e8eb26ab181
image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
PINCache: d9a87a0ff397acffe9e2f0db972ac14680441158
PINOperation: fb563bcc9c32c26d6c78aaff967d405aa2ee74a7
pointer_interceptor_ios: 9280618c0b2eeb80081a343924aa8ad756c21375
push_ios: 2bd1b4d3f782209da1f571db1250af236957e807
river_player: ba880eae2d34deaff38fdf53a96b63edc654c9bf
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 73b7fc691fdc43277614250e04d183740cb15078
sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36

PODFILE CHECKSUM: 8d23d5c4d896af3a5f2a08e0206462ca9882e556
Expand Down
12 changes: 6 additions & 6 deletions lib/community/utils/post_card_action_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ final List<ExtendedPostCardActions> postCardActionItems = [
postCardAction: PostCardAction.shareMedia,
icon: Icons.image_rounded,
label: l10n.shareMedia,
getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.mediaUrl,
getSubtitleLabel: (context, postViewMedia) => postViewMedia.media.first.thumbnailUrl,
),
ExtendedPostCardActions(
postCardAction: PostCardAction.shareLink,
Expand Down Expand Up @@ -336,12 +336,12 @@ void showPostActionBottomModalSheet(
// Or if the media link is the same as the external link
if (postViewMedia.media.isEmpty ||
(postViewMedia.media.first.mediaType != MediaType.link && postViewMedia.media.first.mediaType != MediaType.image) ||
postViewMedia.media.first.originalUrl == postViewMedia.media.first.mediaUrl) {
postViewMedia.media.first.originalUrl == postViewMedia.media.first.thumbnailUrl) {
sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareLink);
}

// Remove the share media option if there is no media
if (postViewMedia.media.isEmpty || postViewMedia.media.first.mediaUrl == null) {
if (postViewMedia.media.isEmpty || postViewMedia.media.first.thumbnailUrl == null) {
sharePostCardActions.removeWhere((extendedAction) => extendedAction.postCardAction == PostCardAction.shareMedia);
}

Expand Down Expand Up @@ -582,18 +582,18 @@ class _PostCardActionPickerState extends State<PostCardActionPicker> {
break;
case PostCardAction.shareMedia:
action = () async {
if (widget.postViewMedia.media.first.mediaUrl != null) {
if (widget.postViewMedia.media.first.thumbnailUrl != null) {
try {
// Try to get the cached image first
var media = await DefaultCacheManager().getFileFromCache(widget.postViewMedia.media.first.mediaUrl!);
var media = await DefaultCacheManager().getFileFromCache(widget.postViewMedia.media.first.thumbnailUrl!);
File? mediaFile = media?.file;

if (media == null) {
// Tell user we're downloading the image
showSnackbar(AppLocalizations.of(widget.outerContext)!.downloadingMedia);

// Download
mediaFile = await DefaultCacheManager().getSingleFile(widget.postViewMedia.media.first.mediaUrl!);
mediaFile = await DefaultCacheManager().getSingleFile(widget.postViewMedia.media.first.thumbnailUrl!);
}

// Share
Expand Down
10 changes: 7 additions & 3 deletions lib/core/models/media.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ import 'package:thunder/core/enums/media_type.dart';
/// The Media class represents information for a given media source.
class Media {
Media({
this.thumbnailUrl,
this.mediaUrl,
this.originalUrl,
this.width,
this.height,
required this.mediaType,
});

/// The original URL of the media - this applies if the original URL of the media originates from a external link
/// The original external URL of the post
String? originalUrl;

/// The URL indicates the source of the media
/// The thumbnail URL of the media
String? thumbnailUrl;

/// The actual URL of the media source
String? mediaUrl;

/// The width of the media source
Expand All @@ -27,6 +31,6 @@ class Media {

@override
String toString() {
return '''Media { mediaUrl: $mediaUrl, originalUrl: $originalUrl, width: $width, height: $height, type: $mediaType }''';
return '''Media { thumbnailUrl: $thumbnailUrl, mediaUrl: $mediaUrl, originalUrl: $originalUrl, width: $width, height: $height, type: $mediaType }''';
}
}
4 changes: 4 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,10 @@
},
"failedToLoadBlocks": "Could not load blocks: {errorMessage}",
"@failedToLoadBlocks": {},
"failedToLoadVideo": "Failed to load video. Open link in browser?",
"@failedToLoadVideo": {
"description": "Error message that displays when we fail to load a video"
},
"failedToUnblock": "Could not unblock: {errorMessage}",
"@failedToUnblock": {},
"failedToUpdateNotificationSettings": "Failed to update notification settings",
Expand Down
2 changes: 1 addition & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ class _ThunderAppState extends State<ThunderApp> {
create: (context) => UserBloc(lemmyClient: LemmyClient.instance),
),
BlocProvider(
create: (context) => NetworkCheckerCubit(),
create: (context) => NetworkCheckerCubit()..getConnectionType(),
)
],
child: BlocBuilder<ThemeBloc, ThemeState>(
Expand Down
28 changes: 18 additions & 10 deletions lib/post/utils/post.dart
Original file line number Diff line number Diff line change
Expand Up @@ -276,11 +276,11 @@ Future<PostViewMedia> parsePostView(PostView postView, bool fetchImageDimensions
// 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
String? url = postView.post.url ?? '';
String? thumbnailUrl = postView.post.thumbnailUrl;
String? videoUrl = postView.post.embedVideoUrl; // @TODO: Add support for videos
String? videoUrl = postView.post.embedVideoUrl;

// First, check what type of link we're dealing with based on the url (MediaType.image, MediaType.video, MediaType.link, MediaType.text)
bool isImage = isImageUrl(url);
bool isVideo = isVideoUrl(url);
bool isVideo = isVideoUrl(videoUrl ?? url);

MediaType mediaType;

Expand All @@ -296,29 +296,37 @@ Future<PostViewMedia> parsePostView(PostView postView, bool fetchImageDimensions

Media media = Media(mediaType: mediaType, originalUrl: url);

// Determine the thumbnail url
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;
media.thumbnailUrl = thumbnailUrl;
} 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;
// If there is no thumbnail image, but the url is an image, we'll use that for the thumbnailUrl
media.thumbnailUrl = url;
} else if (scrapeMissingPreviews) {
// If there is no thumbnail image, we'll see if we should try to fetch the link metadata
LinkInfo linkInfo = await getLinkInfo(url);

if (linkInfo.imageURL != null && linkInfo.imageURL!.isNotEmpty) {
media.mediaUrl = linkInfo.imageURL!;
media.thumbnailUrl = linkInfo.imageURL!;
}
}

// Finally, check to see if we need to fetch the image dimensions
if (fetchImageDimensions && media.mediaUrl != null) {
// Determine the media url
if (isImage) {
media.mediaUrl = url;
} else if (isVideo) {
media.mediaUrl = videoUrl;
}

// Finally, check to see if we need to fetch the image dimensions for the thumbnail url
if (fetchImageDimensions && media.thumbnailUrl != null) {
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.thumbnailUrl ?? media.mediaUrl).timeout(const Duration(seconds: 2));
} catch (e) {
debugPrint('${media.mediaUrl ?? media.originalUrl} - $e: Falling back to default image size');
debugPrint('${media.thumbnailUrl ?? media.originalUrl} - $e: Falling back to default image size');
}

Size size = MediaExtension.getScaledMediaSize(width: result.width, height: result.height, offset: edgeToEdgeImages ? 0 : 24, tabletMode: tabletMode);
Expand Down
12 changes: 10 additions & 2 deletions lib/settings/pages/video_player_settings.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import 'dart:async';

import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

import 'package:thunder/core/enums/local_settings.dart';
import 'package:thunder/core/enums/video_auto_play.dart';
import 'package:thunder/core/enums/video_playback_speed.dart';
import 'package:thunder/core/singletons/preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:thunder/settings/widgets/list_option.dart';
import 'package:thunder/settings/widgets/toggle_option.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
import 'package:thunder/utils/bottom_sheet_list_picker.dart';

class VideoPlayerSettingsPage extends StatefulWidget {
Expand Down Expand Up @@ -97,12 +101,16 @@ class _VideoPlayerSettingsPageState extends State<VideoPlayerSettingsPage> {
break;
default:
}

if (context.mounted) {
context.read<ThunderBloc>().add(UserPreferencesChangeEvent());
}
}

void _initPreferences() async {
final prefs = (await UserPreferences.instance).sharedPreferences;
setState(() {
videoAutoMute = prefs.getBool(LocalSettings.videoAutoMute.name) ?? false;
videoAutoMute = prefs.getBool(LocalSettings.videoAutoMute.name) ?? true;
videoAutoFullscreen = prefs.getBool(LocalSettings.videoAutoFullscreen.name) ?? false;
videoAutoLoop = prefs.getBool(LocalSettings.videoAutoLoop.name) ?? false;
videoAutoPlay = VideoAutoPlay.values.byName(prefs.getString(LocalSettings.videoAutoPlay.name) ?? VideoAutoPlay.never.name);
Expand Down
Loading

0 comments on commit 239379e

Please sign in to comment.