video: duration in thumbnail / info, player in fullscreen

This commit is contained in:
Thibault Deckers 2019-08-04 13:40:33 +09:00
parent 49a28c6d09
commit cc0283d393
7 changed files with 296 additions and 145 deletions

View file

@ -67,6 +67,8 @@ class ImageEntry {
};
}
bool get isGif => mimeType == MimeTypes.MIME_GIF;
bool get isVideo => mimeType.startsWith(MimeTypes.MIME_VIDEO);
int getMegaPixels() {
@ -83,4 +85,19 @@ class ImageEntry {
final d = getBestDate();
return d == null ? null : DateTime(d.year, d.month);
}
String getDurationText() {
final d = Duration(milliseconds: durationMillis);
String twoDigits(int n) {
if (n >= 10) return '$n';
return '0$n';
}
String twoDigitSeconds = twoDigits(d.inSeconds.remainder(Duration.secondsPerMinute));
if (d.inHours == 0) return '${d.inMinutes}:$twoDigitSeconds';
String twoDigitMinutes = twoDigits(d.inMinutes.remainder(Duration.minutesPerHour));
return '${d.inHours}:$twoDigitMinutes:$twoDigitSeconds';
}
}

View file

@ -3,7 +3,6 @@ import 'dart:typed_data';
import 'package:aves/model/image_decode_service.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/model/mime_types.dart';
import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';
@ -26,7 +25,7 @@ class Thumbnail extends StatefulWidget {
class ThumbnailState extends State<Thumbnail> {
Future<Uint8List> _byteLoader;
String get mimeType => widget.entry.mimeType;
ImageEntry get entry => widget.entry;
String get uri => widget.entry.uri;
@ -56,10 +55,14 @@ class ThumbnailState extends State<Thumbnail> {
@override
Widget build(BuildContext context) {
final isVideo = mimeType.startsWith(MimeTypes.MIME_VIDEO);
final isGif = mimeType == MimeTypes.MIME_GIF;
final iconSize = widget.extent / 4;
return Container(
final fontSize = (widget.extent / 8).roundToDouble();
final iconSize = fontSize * 2;
return DefaultTextStyle(
style: TextStyle(
color: Colors.grey[200],
fontSize: fontSize,
),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey.shade700,
@ -92,12 +95,12 @@ class ThumbnailState extends State<Thumbnail> {
);
}),
),
if (isVideo)
Icon(
Icons.play_circle_outline,
size: iconSize,
),
if (isGif)
if (entry.isVideo)
VideoTag(
entry: entry,
iconSize: iconSize,
)
else if (entry.isGif)
Icon(
Icons.gif,
size: iconSize,
@ -105,6 +108,42 @@ class ThumbnailState extends State<Thumbnail> {
],
);
}),
),
);
}
}
class VideoTag extends StatelessWidget {
final ImageEntry entry;
final double iconSize;
const VideoTag({Key key, this.entry, this.iconSize}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(1),
padding: EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: Color(0xBB000000),
borderRadius: BorderRadius.all(
Radius.circular(iconSize),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.play_circle_outline,
size: iconSize,
),
SizedBox(width: 2),
Text(
entry.getDurationText(),
)
],
),
);
}
}

View file

@ -4,11 +4,14 @@ import 'dart:math';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/fullscreen/info_page.dart';
import 'package:aves/widgets/fullscreen/overlay.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:screen/screen.dart';
import 'package:video_player/video_player.dart';
class FullscreenPage extends StatefulWidget {
final List<ImageEntry> entries;
@ -50,6 +53,8 @@ class FullscreenPageState extends State<FullscreenPage> with SingleTickerProvide
_topOverlayScale = CurvedAnimation(parent: _overlayAnimationController, curve: Curves.easeOutQuart, reverseCurve: Curves.easeInQuart);
_bottomOverlayOffset = Tween(begin: Offset(0, 1), end: Offset(0, 0)).animate(CurvedAnimation(parent: _overlayAnimationController, curve: Curves.easeOutQuart, reverseCurve: Curves.easeInQuart));
_overlayVisible.addListener(onOverlayVisibleChange);
Screen.keepOn(true);
initOverlay();
}
@ -68,7 +73,13 @@ class FullscreenPageState extends State<FullscreenPage> with SingleTickerProvide
@override
Widget build(BuildContext context) {
return Scaffold(
return WillPopScope(
onWillPop: () {
Screen.keepOn(false);
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
return Future.value(true);
},
child: Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
@ -154,6 +165,7 @@ class FullscreenPageState extends State<FullscreenPage> with SingleTickerProvide
// ],
// ),
// ),
),
);
}
@ -162,9 +174,9 @@ class FullscreenPageState extends State<FullscreenPage> with SingleTickerProvide
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
_overlayAnimationController.forward();
} else {
final mq = MediaQuery.of(context);
_frozenViewInsets = mq.viewInsets;
_frozenViewPadding = mq.viewPadding;
final mediaQuery = MediaQuery.of(context);
_frozenViewInsets = mediaQuery.viewInsets;
_frozenViewPadding = mediaQuery.viewPadding;
SystemChrome.setEnabledSystemUIOverlays([]);
await _overlayAnimationController.reverse();
_frozenViewInsets = null;
@ -198,8 +210,24 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
super.build(context);
return PhotoViewGallery.builder(
itemCount: widget.entries.length,
builder: (context, index) {
builder: (galleryContext, index) {
final entry = widget.entries[index];
if (entry.isVideo) {
final screenSize = MediaQuery.of(galleryContext).size;
final videoAspectRatio = entry.width / entry.height;
final childWidth = min(screenSize.width, entry.width);
final childHeight = childWidth / videoAspectRatio;
debugPrint('ImagePageState video path=${entry.path} childWidth=$childWidth childHeight=$childHeight var=$videoAspectRatio car=${childWidth / childHeight}');
return PhotoViewGalleryPageOptions.customChild(
child: AvesVideo(entry: entry),
childSize: Size(childWidth, childHeight),
// no heroTag because `Chewie` already internally builds one with the videoController
minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => widget.onTap?.call(),
);
}
return PhotoViewGalleryPageOptions(
imageProvider: FileImage(File(entry.path)),
heroTag: entry.uri,
@ -222,3 +250,43 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
@override
bool get wantKeepAlive => true;
}
class AvesVideo extends StatefulWidget {
final ImageEntry entry;
const AvesVideo({Key key, this.entry}) : super(key: key);
@override
State<StatefulWidget> createState() => AvesVideoState();
}
class AvesVideoState extends State<AvesVideo> {
VideoPlayerController videoPlayerController;
ChewieController chewieController;
@override
void initState() {
super.initState();
videoPlayerController = VideoPlayerController.file(
File(widget.entry.path),
// ensure the first frame is shown after the video is initialized
)..initialize().then((_) => setState(() {}));
chewieController = ChewieController(
videoPlayerController: videoPlayerController,
);
}
@override
void dispose() {
videoPlayerController.dispose();
chewieController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Chewie(
controller: chewieController,
);
}
}

View file

@ -39,7 +39,7 @@ class InfoPageState extends State<InfoPage> {
Widget build(BuildContext context) {
final date = entry.getBestDate();
final dateText = '${DateFormat.yMMMd().format(date)} ${DateFormat.Hm().format(date)}';
final resolutionText = '${entry.width} × ${entry.height}${entry.isVideo ? '': ' (${entry.getMegaPixels()} MP)'}';
final resolutionText = '${entry.width} × ${entry.height}${entry.isVideo ? '' : ' (${entry.getMegaPixels()} MP)'}';
return Scaffold(
appBar: AppBar(
leading: IconButton(
@ -75,6 +75,7 @@ class InfoPageState extends State<InfoPage> {
SectionRow('File'),
InfoRow('Title', entry.title),
InfoRow('Date', dateText),
if (entry.isVideo) InfoRow('Duration', entry.getDurationText()),
InfoRow('Resolution', resolutionText),
InfoRow('Size', formatFilesize(entry.sizeBytes)),
InfoRow('Path', entry.path),
@ -86,7 +87,7 @@ class InfoPageState extends State<InfoPage> {
return Text(snapshot.error);
}
if (snapshot.connectionState != ConnectionState.done) {
return CircularProgressIndicator();
return SizedBox.shrink();
}
final metadataMap = snapshot.data.cast<String, Map>();
final directoryNames = metadataMap.keys.toList()..sort();
@ -122,17 +123,15 @@ class SectionRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
return Row(
children: [
Expanded(child: Divider(color: Colors.white70)),
SizedBox(width: 8),
Text(title, style: TextStyle(fontSize: 18)),
SizedBox(width: 8),
Padding(
padding: EdgeInsets.all(16.0),
child: Text(title, style: TextStyle(fontSize: 20)),
),
Expanded(child: Divider(color: Colors.white70)),
],
),
);
}
}

View file

@ -108,13 +108,12 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
final viewInsets = widget.viewInsets ?? mediaQuery.viewInsets;
final viewPadding = widget.viewPadding ?? mediaQuery.viewPadding;
final overlayContentMaxWidth = mediaQuery.size.width - viewPadding.horizontal - innerPadding.horizontal;
return BlurredRect(
return IgnorePointer(
child: BlurredRect(
child: Container(
color: kOverlayBackground,
child: IgnorePointer(
child: Padding(
padding: viewInsets + viewPadding.copyWith(top: 0),
child: Container(
child: Padding(
padding: innerPadding,
child: FutureBuilder(
future: _detailLoader,
@ -136,7 +135,6 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
),
),
),
),
);
}
}

View file

@ -29,6 +29,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
chewie:
dependency: "direct main"
description:
name: chewie
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7"
collection:
dependency: "direct main"
description:
@ -74,6 +81,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
open_iconic_flutter:
dependency: transitive
description:
name: open_iconic_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
path:
dependency: transitive
description:
@ -102,6 +116,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
screen:
dependency: "direct main"
description:
name: screen
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.5"
sky_engine:
dependency: transitive
description: flutter
@ -170,6 +191,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
video_player:
dependency: transitive
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.1+6"
sdks:
dart: ">=2.2.2 <3.0.0"
flutter: ">=1.5.9-pre.94 <2.0.0"

View file

@ -19,10 +19,12 @@ environment:
dependencies:
flutter:
sdk: flutter
chewie:
collection:
flutter_sticky_header:
intl:
photo_view:
screen:
transparent_image:
dev_dependencies: