video: duration in thumbnail / info, player in fullscreen
This commit is contained in:
parent
49a28c6d09
commit
cc0283d393
7 changed files with 296 additions and 145 deletions
|
@ -67,6 +67,8 @@ class ImageEntry {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isGif => mimeType == MimeTypes.MIME_GIF;
|
||||||
|
|
||||||
bool get isVideo => mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
bool get isVideo => mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
||||||
|
|
||||||
int getMegaPixels() {
|
int getMegaPixels() {
|
||||||
|
@ -83,4 +85,19 @@ class ImageEntry {
|
||||||
final d = getBestDate();
|
final d = getBestDate();
|
||||||
return d == null ? null : DateTime(d.year, d.month);
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:aves/model/image_decode_service.dart';
|
import 'package:aves/model/image_decode_service.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/mime_types.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:transparent_image/transparent_image.dart';
|
import 'package:transparent_image/transparent_image.dart';
|
||||||
|
|
||||||
|
@ -26,7 +25,7 @@ class Thumbnail extends StatefulWidget {
|
||||||
class ThumbnailState extends State<Thumbnail> {
|
class ThumbnailState extends State<Thumbnail> {
|
||||||
Future<Uint8List> _byteLoader;
|
Future<Uint8List> _byteLoader;
|
||||||
|
|
||||||
String get mimeType => widget.entry.mimeType;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
String get uri => widget.entry.uri;
|
String get uri => widget.entry.uri;
|
||||||
|
|
||||||
|
@ -56,55 +55,95 @@ class ThumbnailState extends State<Thumbnail> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isVideo = mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
final fontSize = (widget.extent / 8).roundToDouble();
|
||||||
final isGif = mimeType == MimeTypes.MIME_GIF;
|
final iconSize = fontSize * 2;
|
||||||
final iconSize = widget.extent / 4;
|
return DefaultTextStyle(
|
||||||
return Container(
|
style: TextStyle(
|
||||||
decoration: BoxDecoration(
|
color: Colors.grey[200],
|
||||||
border: Border.all(
|
fontSize: fontSize,
|
||||||
color: Colors.grey.shade700,
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: FutureBuilder(
|
child: Container(
|
||||||
future: _byteLoader,
|
decoration: BoxDecoration(
|
||||||
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
border: Border.all(
|
||||||
final bytes = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : kTransparentImage;
|
color: Colors.grey.shade700,
|
||||||
return Stack(
|
width: 0.5,
|
||||||
alignment: AlignmentDirectional.bottomStart,
|
),
|
||||||
children: [
|
),
|
||||||
Hero(
|
child: FutureBuilder(
|
||||||
tag: uri,
|
future: _byteLoader,
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
builder: (futureContext, AsyncSnapshot<Uint8List> snapshot) {
|
||||||
// during hero animation back from a fullscreen image,
|
final bytes = (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) ? snapshot.data : kTransparentImage;
|
||||||
// the image covers the whole screen (because of the 'fit' prop and the full screen hero constraints)
|
return Stack(
|
||||||
// so we wrap the image to apply better constraints
|
alignment: AlignmentDirectional.bottomStart,
|
||||||
final dim = min(constraints.maxWidth, constraints.maxHeight);
|
children: [
|
||||||
return Container(
|
Hero(
|
||||||
alignment: Alignment.center,
|
tag: uri,
|
||||||
constraints: BoxConstraints.tight(Size(dim, dim)),
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
child: Image.memory(
|
// during hero animation back from a fullscreen image,
|
||||||
bytes,
|
// the image covers the whole screen (because of the 'fit' prop and the full screen hero constraints)
|
||||||
width: dim,
|
// so we wrap the image to apply better constraints
|
||||||
height: dim,
|
final dim = min(constraints.maxWidth, constraints.maxHeight);
|
||||||
fit: BoxFit.cover,
|
return Container(
|
||||||
),
|
alignment: Alignment.center,
|
||||||
);
|
constraints: BoxConstraints.tight(Size(dim, dim)),
|
||||||
}),
|
child: Image.memory(
|
||||||
),
|
bytes,
|
||||||
if (isVideo)
|
width: dim,
|
||||||
Icon(
|
height: dim,
|
||||||
Icons.play_circle_outline,
|
fit: BoxFit.cover,
|
||||||
size: iconSize,
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
if (isGif)
|
if (entry.isVideo)
|
||||||
Icon(
|
VideoTag(
|
||||||
Icons.gif,
|
entry: entry,
|
||||||
size: iconSize,
|
iconSize: iconSize,
|
||||||
),
|
)
|
||||||
],
|
else if (entry.isGif)
|
||||||
);
|
Icon(
|
||||||
}),
|
Icons.gif,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,14 @@ import 'dart:math';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info_page.dart';
|
import 'package:aves/widgets/fullscreen/info_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay.dart';
|
import 'package:aves/widgets/fullscreen/overlay.dart';
|
||||||
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:photo_view/photo_view_gallery.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 {
|
class FullscreenPage extends StatefulWidget {
|
||||||
final List<ImageEntry> entries;
|
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);
|
_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));
|
_bottomOverlayOffset = Tween(begin: Offset(0, 1), end: Offset(0, 0)).animate(CurvedAnimation(parent: _overlayAnimationController, curve: Curves.easeOutQuart, reverseCurve: Curves.easeInQuart));
|
||||||
_overlayVisible.addListener(onOverlayVisibleChange);
|
_overlayVisible.addListener(onOverlayVisibleChange);
|
||||||
|
|
||||||
|
Screen.keepOn(true);
|
||||||
initOverlay();
|
initOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,64 +73,70 @@ class FullscreenPageState extends State<FullscreenPage> with SingleTickerProvide
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return WillPopScope(
|
||||||
backgroundColor: Colors.black,
|
onWillPop: () {
|
||||||
body: Stack(
|
Screen.keepOn(false);
|
||||||
children: [
|
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||||
PageView(
|
return Future.value(true);
|
||||||
scrollDirection: Axis.vertical,
|
},
|
||||||
controller: _verticalPager,
|
child: Scaffold(
|
||||||
physics: _isInitialScale ? PageScrollPhysics() : NeverScrollableScrollPhysics(),
|
backgroundColor: Colors.black,
|
||||||
onPageChanged: (page) => setState(() => _currentVerticalPage = page),
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
ImagePage(
|
PageView(
|
||||||
entries: entries,
|
scrollDirection: Axis.vertical,
|
||||||
pageController: _horizontalPager,
|
controller: _verticalPager,
|
||||||
onTap: () => _overlayVisible.value = !_overlayVisible.value,
|
physics: _isInitialScale ? PageScrollPhysics() : NeverScrollableScrollPhysics(),
|
||||||
onPageChanged: (page) => setState(() => _currentHorizontalPage = page),
|
onPageChanged: (page) => setState(() => _currentVerticalPage = page),
|
||||||
onScaleChanged: (state) => setState(() => _isInitialScale = state == PhotoViewScaleState.initial),
|
children: [
|
||||||
),
|
ImagePage(
|
||||||
NotificationListener(
|
|
||||||
onNotification: (notification) {
|
|
||||||
if (notification is BackUpNotification) {
|
|
||||||
_verticalPager.animateToPage(
|
|
||||||
0,
|
|
||||||
duration: const Duration(milliseconds: 400),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
child: InfoPage(
|
|
||||||
entry: entries[_currentHorizontalPage],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (_currentHorizontalPage != null && _currentVerticalPage == 0) ...[
|
|
||||||
FullscreenTopOverlay(
|
|
||||||
entries: entries,
|
|
||||||
index: _currentHorizontalPage,
|
|
||||||
scale: _topOverlayScale,
|
|
||||||
viewInsets: _frozenViewInsets,
|
|
||||||
viewPadding: _frozenViewPadding,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
child: SlideTransition(
|
|
||||||
position: _bottomOverlayOffset,
|
|
||||||
child: FullscreenBottomOverlay(
|
|
||||||
entries: entries,
|
entries: entries,
|
||||||
index: _currentHorizontalPage,
|
pageController: _horizontalPager,
|
||||||
viewInsets: _frozenViewInsets,
|
onTap: () => _overlayVisible.value = !_overlayVisible.value,
|
||||||
viewPadding: _frozenViewPadding,
|
onPageChanged: (page) => setState(() => _currentHorizontalPage = page),
|
||||||
|
onScaleChanged: (state) => setState(() => _isInitialScale = state == PhotoViewScaleState.initial),
|
||||||
),
|
),
|
||||||
|
NotificationListener(
|
||||||
|
onNotification: (notification) {
|
||||||
|
if (notification is BackUpNotification) {
|
||||||
|
_verticalPager.animateToPage(
|
||||||
|
0,
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: InfoPage(
|
||||||
|
entry: entries[_currentHorizontalPage],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_currentHorizontalPage != null && _currentVerticalPage == 0) ...[
|
||||||
|
FullscreenTopOverlay(
|
||||||
|
entries: entries,
|
||||||
|
index: _currentHorizontalPage,
|
||||||
|
scale: _topOverlayScale,
|
||||||
|
viewInsets: _frozenViewInsets,
|
||||||
|
viewPadding: _frozenViewPadding,
|
||||||
),
|
),
|
||||||
)
|
Positioned(
|
||||||
]
|
bottom: 0,
|
||||||
],
|
child: SlideTransition(
|
||||||
),
|
position: _bottomOverlayOffset,
|
||||||
resizeToAvoidBottomInset: false,
|
child: FullscreenBottomOverlay(
|
||||||
|
entries: entries,
|
||||||
|
index: _currentHorizontalPage,
|
||||||
|
viewInsets: _frozenViewInsets,
|
||||||
|
viewPadding: _frozenViewPadding,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
// Hero(
|
// Hero(
|
||||||
// tag: uri,
|
// tag: uri,
|
||||||
// child: Stack(
|
// child: Stack(
|
||||||
|
@ -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);
|
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||||
_overlayAnimationController.forward();
|
_overlayAnimationController.forward();
|
||||||
} else {
|
} else {
|
||||||
final mq = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
_frozenViewInsets = mq.viewInsets;
|
_frozenViewInsets = mediaQuery.viewInsets;
|
||||||
_frozenViewPadding = mq.viewPadding;
|
_frozenViewPadding = mediaQuery.viewPadding;
|
||||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||||
await _overlayAnimationController.reverse();
|
await _overlayAnimationController.reverse();
|
||||||
_frozenViewInsets = null;
|
_frozenViewInsets = null;
|
||||||
|
@ -198,8 +210,24 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return PhotoViewGallery.builder(
|
return PhotoViewGallery.builder(
|
||||||
itemCount: widget.entries.length,
|
itemCount: widget.entries.length,
|
||||||
builder: (context, index) {
|
builder: (galleryContext, index) {
|
||||||
final entry = widget.entries[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(
|
return PhotoViewGalleryPageOptions(
|
||||||
imageProvider: FileImage(File(entry.path)),
|
imageProvider: FileImage(File(entry.path)),
|
||||||
heroTag: entry.uri,
|
heroTag: entry.uri,
|
||||||
|
@ -222,3 +250,43 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ class InfoPageState extends State<InfoPage> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final date = entry.getBestDate();
|
final date = entry.getBestDate();
|
||||||
final dateText = '${DateFormat.yMMMd().format(date)} – ${DateFormat.Hm().format(date)}';
|
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(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
|
@ -75,6 +75,7 @@ class InfoPageState extends State<InfoPage> {
|
||||||
SectionRow('File'),
|
SectionRow('File'),
|
||||||
InfoRow('Title', entry.title),
|
InfoRow('Title', entry.title),
|
||||||
InfoRow('Date', dateText),
|
InfoRow('Date', dateText),
|
||||||
|
if (entry.isVideo) InfoRow('Duration', entry.getDurationText()),
|
||||||
InfoRow('Resolution', resolutionText),
|
InfoRow('Resolution', resolutionText),
|
||||||
InfoRow('Size', formatFilesize(entry.sizeBytes)),
|
InfoRow('Size', formatFilesize(entry.sizeBytes)),
|
||||||
InfoRow('Path', entry.path),
|
InfoRow('Path', entry.path),
|
||||||
|
@ -86,7 +87,7 @@ class InfoPageState extends State<InfoPage> {
|
||||||
return Text(snapshot.error);
|
return Text(snapshot.error);
|
||||||
}
|
}
|
||||||
if (snapshot.connectionState != ConnectionState.done) {
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
return CircularProgressIndicator();
|
return SizedBox.shrink();
|
||||||
}
|
}
|
||||||
final metadataMap = snapshot.data.cast<String, Map>();
|
final metadataMap = snapshot.data.cast<String, Map>();
|
||||||
final directoryNames = metadataMap.keys.toList()..sort();
|
final directoryNames = metadataMap.keys.toList()..sort();
|
||||||
|
@ -122,17 +123,15 @@ class SectionRow extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Row(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
children: [
|
||||||
child: Row(
|
Expanded(child: Divider(color: Colors.white70)),
|
||||||
children: [
|
Padding(
|
||||||
Expanded(child: Divider(color: Colors.white70)),
|
padding: EdgeInsets.all(16.0),
|
||||||
SizedBox(width: 8),
|
child: Text(title, style: TextStyle(fontSize: 20)),
|
||||||
Text(title, style: TextStyle(fontSize: 18)),
|
),
|
||||||
SizedBox(width: 8),
|
Expanded(child: Divider(color: Colors.white70)),
|
||||||
Expanded(child: Divider(color: Colors.white70)),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,31 +108,29 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
||||||
final viewInsets = widget.viewInsets ?? mediaQuery.viewInsets;
|
final viewInsets = widget.viewInsets ?? mediaQuery.viewInsets;
|
||||||
final viewPadding = widget.viewPadding ?? mediaQuery.viewPadding;
|
final viewPadding = widget.viewPadding ?? mediaQuery.viewPadding;
|
||||||
final overlayContentMaxWidth = mediaQuery.size.width - viewPadding.horizontal - innerPadding.horizontal;
|
final overlayContentMaxWidth = mediaQuery.size.width - viewPadding.horizontal - innerPadding.horizontal;
|
||||||
return BlurredRect(
|
return IgnorePointer(
|
||||||
child: Container(
|
child: BlurredRect(
|
||||||
color: kOverlayBackground,
|
child: Container(
|
||||||
child: IgnorePointer(
|
color: kOverlayBackground,
|
||||||
|
padding: viewInsets + viewPadding.copyWith(top: 0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: viewInsets + viewPadding.copyWith(top: 0),
|
padding: innerPadding,
|
||||||
child: Container(
|
child: FutureBuilder(
|
||||||
padding: innerPadding,
|
future: _detailLoader,
|
||||||
child: FutureBuilder(
|
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
|
||||||
future: _detailLoader,
|
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
||||||
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
|
_lastDetails = snapshot.data;
|
||||||
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
|
_lastEntry = entry;
|
||||||
_lastDetails = snapshot.data;
|
}
|
||||||
_lastEntry = entry;
|
return _lastEntry == null
|
||||||
}
|
? SizedBox.shrink()
|
||||||
return _lastEntry == null
|
: _FullscreenBottomOverlayContent(
|
||||||
? SizedBox.shrink()
|
entry: _lastEntry,
|
||||||
: _FullscreenBottomOverlayContent(
|
details: _lastDetails,
|
||||||
entry: _lastEntry,
|
position: '${widget.index + 1}/${widget.entries.length}',
|
||||||
details: _lastDetails,
|
maxWidth: overlayContentMaxWidth,
|
||||||
position: '${widget.index + 1}/${widget.entries.length}',
|
);
|
||||||
maxWidth: overlayContentMaxWidth,
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
28
pubspec.lock
28
pubspec.lock
|
@ -29,6 +29,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
chewie:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: chewie
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.7"
|
||||||
collection:
|
collection:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -74,6 +81,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.6"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -102,6 +116,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.3"
|
||||||
|
screen:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: screen
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.5"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -170,6 +191,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
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:
|
sdks:
|
||||||
dart: ">=2.2.2 <3.0.0"
|
dart: ">=2.2.2 <3.0.0"
|
||||||
flutter: ">=1.5.9-pre.94 <2.0.0"
|
flutter: ">=1.5.9-pre.94 <2.0.0"
|
||||||
|
|
|
@ -19,10 +19,12 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
chewie:
|
||||||
collection:
|
collection:
|
||||||
flutter_sticky_header:
|
flutter_sticky_header:
|
||||||
intl:
|
intl:
|
||||||
photo_view:
|
photo_view:
|
||||||
|
screen:
|
||||||
transparent_image:
|
transparent_image:
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
Loading…
Reference in a new issue