From d40f32b11b3ff3a50231a59a0397fa035b296b2f Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 9 Dec 2020 11:39:56 +0900 Subject: [PATCH] viewer: open panorama --- lib/utils/constants.dart | 6 ++ lib/widgets/common/fx/blurred.dart | 8 +- lib/widgets/common/fx/borders.dart | 17 ++- lib/widgets/fullscreen/fullscreen_body.dart | 30 ++++-- lib/widgets/fullscreen/overlay/bottom.dart | 32 ++++++ lib/widgets/fullscreen/overlay/common.dart | 49 ++++++++- lib/widgets/fullscreen/overlay/panorama.dart | 37 +++++++ lib/widgets/fullscreen/overlay/video.dart | 104 ++++++++----------- lib/widgets/fullscreen/panorama_page.dart | 30 ++++++ pubspec.lock | 14 +++ pubspec.yaml | 18 ++-- 11 files changed, 253 insertions(+), 92 deletions(-) create mode 100644 lib/widgets/fullscreen/overlay/panorama.dart create mode 100644 lib/widgets/fullscreen/panorama_page.dart diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index cf0c59894..6304c3ec1 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -209,6 +209,12 @@ class Constants { licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/palette_generator/LICENSE', sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/palette_generator', ), + Dependency( + name: 'Panorama', + license: 'Apache 2.0', + licenseUrl: 'https://github.com/zesage/panorama/blob/master/LICENSE', + sourceUrl: 'https://github.com/zesage/panorama', + ), Dependency( name: 'PDF for Dart and Flutter', license: 'Apache 2.0', diff --git a/lib/widgets/common/fx/blurred.dart b/lib/widgets/common/fx/blurred.dart index e21af0e29..a3f606ede 100644 --- a/lib/widgets/common/fx/blurred.dart +++ b/lib/widgets/common/fx/blurred.dart @@ -2,6 +2,8 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +final _filter = ImageFilter.blur(sigmaX: 4, sigmaY: 4); + class BlurredRect extends StatelessWidget { final Widget child; @@ -11,7 +13,7 @@ class BlurredRect extends StatelessWidget { Widget build(BuildContext context) { return ClipRect( child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), + filter: _filter, child: child, ), ); @@ -29,7 +31,7 @@ class BlurredRRect extends StatelessWidget { return ClipRRect( borderRadius: BorderRadius.circular(borderRadius), child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), + filter: _filter, child: child, ), ); @@ -45,7 +47,7 @@ class BlurredOval extends StatelessWidget { Widget build(BuildContext context) { return ClipOval( child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), + filter: _filter, child: child, ), ); diff --git a/lib/widgets/common/fx/borders.dart b/lib/widgets/common/fx/borders.dart index 92c38bf78..3c02d47f2 100644 --- a/lib/widgets/common/fx/borders.dart +++ b/lib/widgets/common/fx/borders.dart @@ -1,11 +1,18 @@ import 'package:flutter/material.dart'; class AvesCircleBorder { - static BoxBorder build(BuildContext context) { - final subPixel = MediaQuery.of(context).devicePixelRatio > 2; - return Border.all( - color: Colors.white30, - width: subPixel ? 0.5 : 1.0, + static const borderColor = Colors.white30; + + static double _borderWidth(BuildContext context) => MediaQuery.of(context).devicePixelRatio > 2 ? 0.5 : 1.0; + + static Border build(BuildContext context) { + return Border.fromBorderSide(buildSide(context)); + } + + static BorderSide buildSide(BuildContext context) { + return BorderSide( + color: borderColor, + width: _borderWidth(context), ); } } diff --git a/lib/widgets/fullscreen/fullscreen_body.dart b/lib/widgets/fullscreen/fullscreen_body.dart index 3fce014bf..1363a2ec2 100644 --- a/lib/widgets/fullscreen/fullscreen_body.dart +++ b/lib/widgets/fullscreen/fullscreen_body.dart @@ -5,8 +5,8 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/utils/change_notifier.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/fullscreen/entry_action_delegate.dart'; import 'package:aves/widgets/fullscreen/image_page.dart'; @@ -14,6 +14,7 @@ import 'package:aves/widgets/fullscreen/image_view.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/notifications.dart'; import 'package:aves/widgets/fullscreen/overlay/bottom.dart'; +import 'package:aves/widgets/fullscreen/overlay/panorama.dart'; import 'package:aves/widgets/fullscreen/overlay/top.dart'; import 'package:aves/widgets/fullscreen/overlay/video.dart'; import 'package:flutter/foundation.dart'; @@ -223,22 +224,33 @@ class FullscreenBodyState extends State with SingleTickerProvide Widget bottomOverlay = ValueListenableBuilder( valueListenable: _entryNotifier, builder: (context, entry, child) { - Widget videoOverlay; - if (entry != null) { - final videoController = entry.isVideo ? _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2 : null; + if (entry == null) return SizedBox.shrink(); + + Widget extraBottomOverlay; + if (entry.isVideo) { + final videoController = _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; if (videoController != null) { - videoOverlay = VideoControlOverlay( + extraBottomOverlay = VideoControlOverlay( entry: entry, controller: videoController, scale: _bottomOverlayScale, - viewInsets: _frozenViewInsets, - viewPadding: _frozenViewPadding, ); } + } else if (entry.is360) { + extraBottomOverlay = PanoramaOverlay( + entry: entry, + scale: _bottomOverlayScale, + ); } + final child = Column( children: [ - if (videoOverlay != null) videoOverlay, + if (extraBottomOverlay != null) + ExtraBottomOverlay( + viewInsets: _frozenViewInsets, + viewPadding: _frozenViewPadding, + child: extraBottomOverlay, + ), SlideTransition( position: _bottomOverlayOffset, child: FullscreenBottomOverlay( @@ -255,7 +267,7 @@ class FullscreenBodyState extends State with SingleTickerProvide valueListenable: _overlayAnimationController, builder: (context, animation, child) { return Visibility( - visible: entry != null && _overlayAnimationController.status != AnimationStatus.dismissed, + visible: _overlayAnimationController.status != AnimationStatus.dismissed, child: child, ); }, diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index b2d08980f..577c2ad43 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -303,3 +303,35 @@ class _ShootingRow extends StatelessWidget { ); } } + +class ExtraBottomOverlay extends StatelessWidget { + final EdgeInsets viewInsets, viewPadding; + final Widget child; + + const ExtraBottomOverlay({ + Key key, + this.viewInsets, + this.viewPadding, + @required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final mq = context.select>((mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding)); + final mqWidth = mq.item1; + final mqViewInsets = mq.item2; + final mqViewPadding = mq.item3; + + final viewInsets = this.viewInsets ?? mqViewInsets; + final viewPadding = this.viewPadding ?? mqViewPadding; + final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0); + + return Padding( + padding: safePadding, + child: SizedBox( + width: mqWidth - safePadding.horizontal, + child: child, + ), + ); + } +} diff --git a/lib/widgets/fullscreen/overlay/common.dart b/lib/widgets/fullscreen/overlay/common.dart index 9c7183bee..23ad79666 100644 --- a/lib/widgets/fullscreen/overlay/common.dart +++ b/lib/widgets/fullscreen/overlay/common.dart @@ -8,7 +8,12 @@ class OverlayButton extends StatelessWidget { final Animation scale; final Widget child; - const OverlayButton({Key key, this.scale, this.child}) : super(key: key); + const OverlayButton({ + Key key, + @required this.scale, + @required this.child, + }) : assert(scale != null), + super(key: key); @override Widget build(BuildContext context) { @@ -30,3 +35,45 @@ class OverlayButton extends StatelessWidget { ); } } + +class OverlayTextButton extends StatelessWidget { + final Animation scale; + final String text; + final VoidCallback onPressed; + + const OverlayTextButton({ + Key key, + @required this.scale, + @required this.text, + this.onPressed, + }) : assert(scale != null), + super(key: key); + + static const _borderRadius = 123.0; + static final _minSize = MaterialStateProperty.all(Size(kMinInteractiveDimension, kMinInteractiveDimension)); + + @override + Widget build(BuildContext context) { + return SizeTransition( + sizeFactor: scale, + child: BlurredRRect( + borderRadius: _borderRadius, + child: OutlinedButton( + onPressed: onPressed, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(kOverlayBackgroundColor), + foregroundColor: MaterialStateProperty.all(Colors.white), + overlayColor: MaterialStateProperty.all(Colors.white.withOpacity(0.12)), + minimumSize: _minSize, + side: MaterialStateProperty.all(AvesCircleBorder.buildSide(context)), + shape: MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(_borderRadius)), + )), + // shape: MaterialStateProperty.all(CircleBorder()), + ), + child: Text(text.toUpperCase()), + ), + ), + ); + } +} diff --git a/lib/widgets/fullscreen/overlay/panorama.dart b/lib/widgets/fullscreen/overlay/panorama.dart new file mode 100644 index 000000000..386752e5f --- /dev/null +++ b/lib/widgets/fullscreen/overlay/panorama.dart @@ -0,0 +1,37 @@ +import 'package:aves/model/image_entry.dart'; +import 'package:aves/widgets/fullscreen/overlay/common.dart'; +import 'package:aves/widgets/fullscreen/panorama_page.dart'; +import 'package:flutter/material.dart'; + +class PanoramaOverlay extends StatelessWidget { + final ImageEntry entry; + final Animation scale; + + const PanoramaOverlay({ + Key key, + @required this.entry, + @required this.scale, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Spacer(), + OverlayTextButton( + scale: scale, + text: 'Open Panorama', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: PanoramaPage.routeName), + builder: (context) => PanoramaPage(entry: entry), + ), + ); + }, + ) + ], + ); + } +} diff --git a/lib/widgets/fullscreen/overlay/video.dart b/lib/widgets/fullscreen/overlay/video.dart index 47e7a69b8..1e0b20dbf 100644 --- a/lib/widgets/fullscreen/overlay/video.dart +++ b/lib/widgets/fullscreen/overlay/video.dart @@ -10,22 +10,17 @@ import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; -import 'package:provider/provider.dart'; -import 'package:tuple/tuple.dart'; class VideoControlOverlay extends StatefulWidget { final ImageEntry entry; - final Animation scale; final IjkMediaController controller; - final EdgeInsets viewInsets, viewPadding; + final Animation scale; const VideoControlOverlay({ Key key, @required this.entry, @required this.controller, @required this.scale, - this.viewInsets, - this.viewPadding, }) : super(key: key); @override @@ -99,63 +94,48 @@ class VideoControlOverlayState extends State with SingleTic @override Widget build(BuildContext context) { - final mq = context.select>((mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding)); - final mqWidth = mq.item1; - final mqViewInsets = mq.item2; - final mqViewPadding = mq.item3; - - final viewInsets = widget.viewInsets ?? mqViewInsets; - final viewPadding = widget.viewPadding ?? mqViewPadding; - final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0); - - return Padding( - padding: safePadding, - child: SizedBox( - width: mqWidth - safePadding.horizontal, - child: StreamBuilder( - stream: controller.ijkStatusStream, - builder: (context, snapshot) { - // do not use stream snapshot because it is obsolete when switching between videos - final status = controller.ijkStatus; - return TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: status == IjkStatus.error - ? [ - OverlayButton( - scale: scale, - child: IconButton( - icon: Icon(AIcons.openInNew), - onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype), - tooltip: 'Open', - ), + return StreamBuilder( + stream: controller.ijkStatusStream, + builder: (context, snapshot) { + // do not use stream snapshot because it is obsolete when switching between videos + final status = controller.ijkStatus; + return TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: status == IjkStatus.error + ? [ + OverlayButton( + scale: scale, + child: IconButton( + icon: Icon(AIcons.openInNew), + onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype), + tooltip: 'Open', + ), + ), + ] + : [ + Expanded( + child: _buildProgressBar(), + ), + SizedBox(width: 8), + OverlayButton( + scale: scale, + child: IconButton( + icon: AnimatedIcon( + icon: AnimatedIcons.play_pause, + progress: _playPauseAnimation, ), - ] - : [ - Expanded( - child: _buildProgressBar(), - ), - SizedBox(width: 8), - OverlayButton( - scale: scale, - child: IconButton( - icon: AnimatedIcon( - icon: AnimatedIcons.play_pause, - progress: _playPauseAnimation, - ), - onPressed: _playPause, - tooltip: isPlaying ? 'Pause' : 'Play', - ), - ), - ], - ), - ); - }), - ), - ); + onPressed: _playPause, + tooltip: isPlaying ? 'Pause' : 'Play', + ), + ), + ], + ), + ); + }); } Widget _buildProgressBar() { diff --git a/lib/widgets/fullscreen/panorama_page.dart b/lib/widgets/fullscreen/panorama_page.dart new file mode 100644 index 000000000..17fa4fac5 --- /dev/null +++ b/lib/widgets/fullscreen/panorama_page.dart @@ -0,0 +1,30 @@ +import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:flutter/material.dart'; +import 'package:panorama/panorama.dart'; + +class PanoramaPage extends StatelessWidget { + static const routeName = '/fullscreen/panorama'; + + final ImageEntry entry; + + const PanoramaPage({@required this.entry}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Panorama( + child: Image( + image: UriImage( + uri: entry.uri, + mimeType: entry.mimeType, + rotationDegrees: entry.rotationDegrees, + isFlipped: entry.isFlipped, + expectedContentLength: entry.sizeBytes, + ), + ), + ), + resizeToAvoidBottomInset: false, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 32888931d..3857dc12b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -283,6 +283,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + flutter_cube: + dependency: transitive + description: + name: flutter_cube + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.6" flutter_driver: dependency: "direct dev" description: flutter @@ -585,6 +592,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.3" + panorama: + dependency: "direct main" + description: + name: panorama + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" path: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4dffbafbc..088af199f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,26 +1,19 @@ name: aves description: Aves is a gallery and metadata explorer app, built for Android. -# The following line prevents the package from being accidentally published to -# pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.2.8+34 # brendan-duncan/image (as of v2.1.19): # - does not support TIFF with JPEG compression (issue #184) # - TIFF tile decoding is not public (issue #258) +# dnfield/flutter_svg (as of v0.19.1): +# - `Could not parse "currentColor" as a color`: https://github.com/dnfield/flutter_svg/issues/31 +# - no