viewer: open panorama
This commit is contained in:
parent
690d257375
commit
d40f32b11b
11 changed files with 253 additions and 92 deletions
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
37
lib/widgets/fullscreen/overlay/panorama.dart
Normal file
37
lib/widgets/fullscreen/overlay/panorama.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,20 +94,7 @@ 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>(
|
||||
return StreamBuilder<IjkStatus>(
|
||||
stream: controller.ijkStatusStream,
|
||||
builder: (context, snapshot) {
|
||||
// do not use stream snapshot because it is obsolete when switching between videos
|
||||
|
@ -153,9 +135,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
|||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildProgressBar() {
|
||||
|
|
30
lib/widgets/fullscreen/panorama_page.dart
Normal file
30
lib/widgets/fullscreen/panorama_page.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
14
pubspec.lock
14
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:
|
||||
|
|
18
pubspec.yaml
18
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 <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:
|
||||
|
|
Loading…
Reference in a new issue