diff --git a/commet/lib/config/preferences.dart b/commet/lib/config/preferences.dart index c12603f1b..66d35b771 100644 --- a/commet/lib/config/preferences.dart +++ b/commet/lib/config/preferences.dart @@ -306,6 +306,12 @@ class Preferences { defaultGetter: () => Layout.mobile ? false : true, defaultValue: false); + BoolPreference autoRotateImages = + BoolPreference("lightbox_rotate_images", defaultValue: false); + + BoolPreference autoRotateVideos = + BoolPreference("lightbox_rotate_videos", defaultValue: false); + DoublePreference textScale = DoublePreference("text_scale", defaultValue: 1.0); diff --git a/commet/lib/ui/atoms/lightbox.dart b/commet/lib/ui/atoms/lightbox.dart index 19c1532ba..b2be0441b 100644 --- a/commet/lib/ui/atoms/lightbox.dart +++ b/commet/lib/ui/atoms/lightbox.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:commet/cache/file_provider.dart'; import 'package:commet/config/build_config.dart'; import 'package:commet/config/layout_config.dart'; +import 'package:commet/main.dart'; import 'package:commet/ui/atoms/scaled_safe_area.dart'; import 'package:commet/ui/molecules/video_player/video_player.dart'; import 'package:commet/ui/molecules/video_player/video_player_controller.dart'; @@ -72,17 +73,47 @@ class Lightbox extends StatefulWidget { } } -class _LightboxState extends State { +class _LightboxState extends State with TickerProviderStateMixin { double aspectRatio = 1; bool dismissing = false; final controller = TransformationController(); bool loadingHighQuality = false; + + StreamSubscription? onLodChanged; + bool rotate = false; + late final AnimationController _controller = AnimationController( + duration: const Duration(milliseconds: 850), + vsync: this, + ); + + late final Animation rotationAnimation = CurvedAnimation( + parent: _controller, + curve: Curves.easeInOutCubic, + ).drive(Tween(begin: -0.25, end: 0.0)); + + late final Animation scaleAnimation = CurvedAnimation( + parent: _controller, + curve: Curves.easeOut, + ).drive(Tween(begin: 0.6, end: 1.0)); + + late final Animation rotation = + ConstantTween(0.0).animate(_controller); + late final Animation scale = ConstantTween(1.0).animate(_controller); + + @override + void dispose() { + onLodChanged?.cancel(); + super.dispose(); + } + @override void initState() { super.initState(); + _controller.stop(canceled: true); + if (widget.aspectRatio != null) { aspectRatio = widget.aspectRatio!; } @@ -95,13 +126,20 @@ class _LightboxState extends State { getVideoInfo(); } - if (widget.image is LODImageProvider) { + if (widget.image case LODImageProvider lod) { + onLodChanged = lod.onLODChanged.listen((_) { + getImageInfo(); + }); + loadingHighQuality = true; - (widget.image as LODImageProvider).fetchFullRes().then((_) { - if (mounted) + lod.fetchFullRes().then((_) { + if (mounted) { + getImageInfo(); + setState(() { loadingHighQuality = false; }); + } }); } } @@ -117,6 +155,10 @@ class _LightboxState extends State { void getVideoInfo() async { WidgetsBinding.instance.addPostFrameCallback((_) async { + if (widget.aspectRatio != null) { + shouldRotate(); + } + var size = await widget.videoController?.getSize(); print(size); if (size != null) { @@ -129,18 +171,35 @@ class _LightboxState extends State { }); } + double counterRotation = 0.25; + void shouldRotate() { if (!Layout.mobile) { return; } + if (widget.image != null && preferences.autoRotateImages.value == false) { + return; + } + + if (widget.video != null && preferences.autoRotateVideos.value == false) { + return; + } + var size = MediaQuery.sizeOf(context); var screenRatio = size.width / size.height; - + bool prevValue = rotate; setState(() { rotate = (aspectRatio < 1 && screenRatio > 1) || (aspectRatio > 1 && screenRatio < 1); }); + + if (rotate != prevValue) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.value = 0; + _controller.animateTo(1); + }); + } } Future getImage() { @@ -176,79 +235,91 @@ class _LightboxState extends State { child: ScaledSafeArea( child: RotatedBox( quarterTurns: rotate ? 1 : 0, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: InteractiveViewer( - trackpadScrollCausesScale: true, - transformationController: controller, - maxScale: 3.5, - child: Container( - alignment: Alignment.center, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: GestureDetector( - onTap: () {}, - child: AspectRatio( - aspectRatio: aspectRatio, - child: widget.customWidget ?? - (widget.image != null - ? Stack( - fit: StackFit.expand, - children: [ - Image( - fit: BoxFit.cover, - image: widget.image!, - isAntiAlias: true, - filterQuality: FilterQuality.medium, - ), - if (loadingHighQuality) - Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: - const EdgeInsets.all(8.0), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surfaceContainer, - borderRadius: - BorderRadius - .circular(8)), - child: Padding( - padding: - const EdgeInsets.all( - 8.0), - child: SizedBox( - width: 12, - height: 12, - child: - CircularProgressIndicator()), - )), + child: ScaleTransition( + scale: rotate ? scaleAnimation : scale, + child: RotationTransition( + turns: rotate ? rotationAnimation : rotation, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: InteractiveViewer( + trackpadScrollCausesScale: true, + transformationController: controller, + maxScale: 3.5, + child: Container( + alignment: Alignment.center, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: GestureDetector( + onTap: () {}, + child: AspectRatio( + aspectRatio: aspectRatio, + child: widget.customWidget ?? + (widget.image != null + ? Stack( + fit: StackFit.expand, + children: [ + Image( + fit: BoxFit.cover, + image: widget.image!, + isAntiAlias: true, + filterQuality: + FilterQuality.medium, ), - ) - ], - ) - : widget.video != null - ? dismissing - ? widget.thumbnail != null - ? Image( - fit: BoxFit.cover, - image: widget.thumbnail!, - ) - : Container( - color: Colors.black, + if (loadingHighQuality) + Align( + alignment: + Alignment.bottomRight, + child: Padding( + padding: + const EdgeInsets.all( + 8.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of( + context) + .colorScheme + .surfaceContainer, + borderRadius: + BorderRadius + .circular( + 8)), + child: Padding( + padding: + const EdgeInsets + .all(8.0), + child: SizedBox( + width: 12, + height: 12, + child: + CircularProgressIndicator()), + )), + ), + ) + ], + ) + : widget.video != null + ? dismissing + ? widget.thumbnail != null + ? Image( + fit: BoxFit.cover, + image: + widget.thumbnail!, + ) + : Container( + color: Colors.black, + ) + : VideoPlayer( + widget.video!, + controller: + widget.videoController, + showProgressBar: true, + canGoFullscreen: false, + thumbnail: widget.thumbnail, + key: widget.contentKey, ) - : VideoPlayer( - widget.video!, - controller: - widget.videoController, - showProgressBar: true, - canGoFullscreen: false, - thumbnail: widget.thumbnail, - key: widget.contentKey, - ) - : const Placeholder())), + : const Placeholder())), + ), + ), ), ), ), diff --git a/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart b/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart index 537da54d0..969823041 100644 --- a/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart +++ b/commet/lib/ui/pages/settings/categories/app/general_settings_page.dart @@ -1,3 +1,4 @@ +import 'package:commet/config/layout_config.dart'; import 'package:commet/main.dart'; import 'package:commet/ui/pages/settings/categories/app/boolean_toggle.dart'; import 'package:commet/ui/pages/setup/menus/check_for_updates.dart'; @@ -72,6 +73,10 @@ class GeneralSettingsPageState extends State { desc: "Header for the settings tile for for media preview toggles", name: "labelMediaPreviewSettingsTitle"); + String get labelMediaSettings => Intl.message("Media", + desc: "Header for the settings tile for for media", + name: "labelMediaSettings"); + String get labelMediaPreviewPrivateRoomsToggle => Intl.message( "Private Rooms", desc: @@ -166,7 +171,7 @@ class GeneralSettingsPageState extends State { height: 10, ), Panel( - header: labelMediaPreviewSettingsTitle, + header: labelMediaSettings, mode: TileType.surfaceContainerLow, child: Column(children: [ BooleanPreferenceToggle( @@ -179,6 +184,21 @@ class GeneralSettingsPageState extends State { title: labelMediaPreviewPublicRoomsToggle, description: labelMediaPreviewPublicRoomsToggleDescription, ), + if (Layout.mobile) ...[ + Seperator(), + BooleanPreferenceToggle( + preference: preferences.autoRotateImages, + title: "Rotate Images", + description: + "When showing images in fullscreen, automatically rotate the image to best fill the screen", + ), + BooleanPreferenceToggle( + preference: preferences.autoRotateVideos, + title: "Rotate Videos", + description: + "When showing videos in fullscreen, automatically rotate the video to best fill the screen", + ), + ] ]), ), ], diff --git a/commet/lib/utils/image/lod_image.dart b/commet/lib/utils/image/lod_image.dart index ca0bd5cb1..9a2e66ca0 100644 --- a/commet/lib/utils/image/lod_image.dart +++ b/commet/lib/utils/image/lod_image.dart @@ -29,6 +29,10 @@ class LODImageProvider extends ImageProvider { bool autoLoadFullRes; Future Function()? loadThumbnail; Future Function()? loadFullRes; + + StreamController _lodChangedController = StreamController.broadcast(); + + Stream get onLODChanged => _lodChangedController.stream; LODImageCompleter? completer; int? thumbnailHeight; int? fullResHeight; @@ -61,6 +65,9 @@ class LODImageProvider extends ImageProvider { loadThumbnail: loadThumbnail, loadFullRes: loadFullRes, callback: decode, + onLODChanged: () { + _lodChangedController.add(null); + }, hasCachedFullres: hasCachedFullres, hasCachedThumbnail: hasCachedThumbnail, thumbnailHeight: thumbnailHeight, @@ -92,6 +99,7 @@ class LODImageCompleter extends ImageStreamCompleter { Future Function()? hasCachedFullres; Future Function()? loadThumbnail; Future Function()? loadFullRes; + Function()? onLODChanged; LODImageType? currentlyLoadedImage; final double _scale = 1; ImageInfo? currentImage; @@ -119,6 +127,7 @@ class LODImageCompleter extends ImageStreamCompleter { this.hasCachedFullres, this.hasCachedThumbnail, this.thumbnailHeight, + this.onLODChanged, this.fullResHeight, this.autoLoadFullres = true}) { loadImages(); @@ -153,6 +162,8 @@ class LODImageCompleter extends ImageStreamCompleter { currentlyLoadedImage = LODImageType.blurhash; setImage(ImageInfo(image: image)); } + + onLODChanged?.call(); } Future _loadThumbnail() async { @@ -178,6 +189,7 @@ class LODImageCompleter extends ImageStreamCompleter { await thumbnailLoading; thumbnailLoading = null; + onLODChanged?.call(); } Future fetchFullRes() async { @@ -218,6 +230,7 @@ class LODImageCompleter extends ImageStreamCompleter { await fullResLoading; fullResLoading = null; + onLODChanged?.call(); } Future _setCodec(LODImageType type, Codec codec) async {