viewer: open panorama

This commit is contained in:
Thibault Deckers 2020-12-09 11:39:56 +09:00
parent 690d257375
commit d40f32b11b
11 changed files with 253 additions and 92 deletions

View file

@ -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',

View file

@ -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,
),
);

View file

@ -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),
);
}
}

View file

@ -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<FullscreenBody> with SingleTickerProvide
Widget bottomOverlay = ValueListenableBuilder<ImageEntry>(
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<FullscreenBody> with SingleTickerProvide
valueListenable: _overlayAnimationController,
builder: (context, animation, child) {
return Visibility(
visible: entry != null && _overlayAnimationController.status != AnimationStatus.dismissed,
visible: _overlayAnimationController.status != AnimationStatus.dismissed,
child: child,
);
},

View file

@ -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<MediaQueryData, Tuple3<double, EdgeInsets, EdgeInsets>>((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,
),
);
}
}

View file

@ -8,7 +8,12 @@ class OverlayButton extends StatelessWidget {
final Animation<double> 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<double> 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>(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<Color>(kOverlayBackgroundColor),
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
overlayColor: MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.12)),
minimumSize: _minSize,
side: MaterialStateProperty.all<BorderSide>(AvesCircleBorder.buildSide(context)),
shape: MaterialStateProperty.all<OutlinedBorder>(RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(_borderRadius)),
)),
// shape: MaterialStateProperty.all<OutlinedBorder>(CircleBorder()),
),
child: Text(text.toUpperCase()),
),
),
);
}
}

View file

@ -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<double> 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),
),
);
},
)
],
);
}
}

View file

@ -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<double> scale;
final IjkMediaController controller;
final EdgeInsets viewInsets, viewPadding;
final Animation<double> 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<VideoControlOverlay> with SingleTic
@override
Widget build(BuildContext context) {
final mq = context.select<MediaQueryData, Tuple3<double, EdgeInsets, EdgeInsets>>((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<IjkStatus>(
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<IjkStatus>(
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() {

View file

@ -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,
);
}
}

View file

@ -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:

View file

@ -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 <style> support: https://github.com/dnfield/flutter_svg/issues/105
# - inconsistent % unit support: https://github.com/dnfield/flutter_svg/issues/110
# video_player (as of v0.10.8+2, backed by ExoPlayer):
# - does not support content URIs (by default, but trivial by fork)
# - does not support AVI/XVID, AC3
@ -77,6 +70,7 @@ dependencies:
overlay_support:
package_info:
palette_generator:
panorama:
pdf:
pedantic:
percent_indicator: