custom video control overlay
This commit is contained in:
parent
6d12f3ddaa
commit
c4d44b49ea
8 changed files with 341 additions and 64 deletions
|
@ -4,6 +4,7 @@ import 'package:aves/model/image_file_service.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/metadata_service.dart';
|
import 'package:aves/model/metadata_service.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
|
import 'package:aves/utils/date_utils.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:geocoder/geocoder.dart';
|
import 'package:geocoder/geocoder.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
@ -124,20 +125,7 @@ class ImageEntry {
|
||||||
return d == null ? null : DateTime(d.year, d.month);
|
return d == null ? null : DateTime(d.year, d.month);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get durationText {
|
String get durationText => formatDuration(Duration(milliseconds: durationMillis));
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasGps => isCatalogued && catalogMetadata.latitude != null;
|
bool get hasGps => isCatalogued && catalogMetadata.latitude != null;
|
||||||
|
|
||||||
|
|
|
@ -9,3 +9,16 @@ bool isToday(DateTime d) => isAtSameDayAs(d, DateTime.now());
|
||||||
bool isThisMonth(DateTime d) => isAtSameMonthAs(d, DateTime.now());
|
bool isThisMonth(DateTime d) => isAtSameMonthAs(d, DateTime.now());
|
||||||
|
|
||||||
bool isThisYear(DateTime d) => isAtSameYearAs(d, DateTime.now());
|
bool isThisYear(DateTime d) => isAtSameYearAs(d, DateTime.now());
|
||||||
|
|
||||||
|
String formatDuration(Duration d) {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,24 @@ class BlurredRect extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BlurredRRect extends StatelessWidget {
|
||||||
|
final double borderRadius;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const BlurredRRect({Key key, this.borderRadius, this.child}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class BlurredOval extends StatelessWidget {
|
class BlurredOval extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/utils/android_app_service.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay_bottom.dart';
|
import 'package:aves/widgets/fullscreen/overlay_bottom.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay_top.dart';
|
import 'package:aves/widgets/fullscreen/overlay_top.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/overlay_video.dart';
|
||||||
import 'package:aves/widgets/fullscreen/video.dart';
|
import 'package:aves/widgets/fullscreen/video.dart';
|
||||||
import 'package:flushbar/flushbar.dart';
|
import 'package:flushbar/flushbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -17,6 +18,8 @@ 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:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
import 'package:screen/screen.dart';
|
import 'package:screen/screen.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
class FullscreenPage extends AnimatedWidget {
|
class FullscreenPage extends AnimatedWidget {
|
||||||
final ImageCollection collection;
|
final ImageCollection collection;
|
||||||
|
@ -92,6 +95,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
Animation<double> _topOverlayScale;
|
Animation<double> _topOverlayScale;
|
||||||
Animation<Offset> _bottomOverlayOffset;
|
Animation<Offset> _bottomOverlayOffset;
|
||||||
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
||||||
|
List<Tuple2<String, VideoPlayerController>> _videoControllers = List();
|
||||||
|
|
||||||
ImageCollection get collection => widget.collection;
|
ImageCollection get collection => widget.collection;
|
||||||
|
|
||||||
|
@ -117,6 +121,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
curve: Curves.easeOutQuart,
|
curve: Curves.easeOutQuart,
|
||||||
));
|
));
|
||||||
_overlayVisible.addListener(onOverlayVisibleChange);
|
_overlayVisible.addListener(onOverlayVisibleChange);
|
||||||
|
initVideoController();
|
||||||
|
|
||||||
Screen.keepOn(true);
|
Screen.keepOn(true);
|
||||||
initOverlay();
|
initOverlay();
|
||||||
|
@ -132,12 +137,13 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_overlayVisible.removeListener(onOverlayVisibleChange);
|
_overlayVisible.removeListener(onOverlayVisibleChange);
|
||||||
|
_videoControllers.forEach((kv) => kv.item2.dispose());
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final entry = _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null;
|
final entry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null;
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () {
|
onWillPop: () {
|
||||||
if (_currentVerticalPage == 1) {
|
if (_currentVerticalPage == 1) {
|
||||||
|
@ -160,8 +166,9 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
collection: collection,
|
collection: collection,
|
||||||
pageController: _horizontalPager,
|
pageController: _horizontalPager,
|
||||||
onTap: () => _overlayVisible.value = !_overlayVisible.value,
|
onTap: () => _overlayVisible.value = !_overlayVisible.value,
|
||||||
onPageChanged: (page) => setState(() => _currentHorizontalPage = page),
|
onPageChanged: onHorizontalPageChanged,
|
||||||
onScaleChanged: (state) => setState(() => _isInitialScale = state == PhotoViewScaleState.initial),
|
onScaleChanged: (state) => setState(() => _isInitialScale = state == PhotoViewScaleState.initial),
|
||||||
|
videoControllers: _videoControllers,
|
||||||
),
|
),
|
||||||
NotificationListener(
|
NotificationListener(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
|
@ -172,33 +179,51 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (_currentHorizontalPage != null && _currentVerticalPage == 0) ...[
|
..._buildOverlay(entry)
|
||||||
FullscreenTopOverlay(
|
|
||||||
entries: entries,
|
|
||||||
index: _currentHorizontalPage,
|
|
||||||
scale: _topOverlayScale,
|
|
||||||
viewInsets: _frozenViewInsets,
|
|
||||||
viewPadding: _frozenViewPadding,
|
|
||||||
onActionSelected: (action) => onActionSelected(entry, action),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
child: SlideTransition(
|
|
||||||
position: _bottomOverlayOffset,
|
|
||||||
child: FullscreenBottomOverlay(
|
|
||||||
entries: entries,
|
|
||||||
index: _currentHorizontalPage,
|
|
||||||
viewInsets: _frozenViewInsets,
|
|
||||||
viewPadding: _frozenViewPadding,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildOverlay(ImageEntry entry) {
|
||||||
|
if (entry == null || _currentVerticalPage != 0) return [];
|
||||||
|
final videoController = entry.isVideo ? _videoControllers.firstWhere((kv) => kv.item1 == entry.path, orElse: () => null)?.item2 : null;
|
||||||
|
return [
|
||||||
|
FullscreenTopOverlay(
|
||||||
|
entries: entries,
|
||||||
|
index: _currentHorizontalPage,
|
||||||
|
scale: _topOverlayScale,
|
||||||
|
viewInsets: _frozenViewInsets,
|
||||||
|
viewPadding: _frozenViewPadding,
|
||||||
|
onActionSelected: (action) => onActionSelected(entry, action),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (videoController != null)
|
||||||
|
VideoControlOverlay(
|
||||||
|
entry: entry,
|
||||||
|
controller: videoController,
|
||||||
|
scale: _topOverlayScale,
|
||||||
|
viewInsets: _frozenViewInsets,
|
||||||
|
viewPadding: _frozenViewPadding,
|
||||||
|
),
|
||||||
|
SlideTransition(
|
||||||
|
position: _bottomOverlayOffset,
|
||||||
|
child: FullscreenBottomOverlay(
|
||||||
|
entries: entries,
|
||||||
|
index: _currentHorizontalPage,
|
||||||
|
viewInsets: _frozenViewInsets,
|
||||||
|
viewPadding: _frozenViewPadding,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
goToVerticalPage(int page) {
|
goToVerticalPage(int page) {
|
||||||
return _verticalPager.animateToPage(
|
return _verticalPager.animateToPage(
|
||||||
page,
|
page,
|
||||||
|
@ -344,6 +369,30 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
if (newName == null || newName.isEmpty) return;
|
if (newName == null || newName.isEmpty) return;
|
||||||
showFeedback(await entry.rename(newName) ? 'Done!' : 'Failed');
|
showFeedback(await entry.rename(newName) ? 'Done!' : 'Failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onHorizontalPageChanged(int page) {
|
||||||
|
_currentHorizontalPage = page;
|
||||||
|
initVideoController();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
initVideoController() {
|
||||||
|
final entry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null;
|
||||||
|
if (entry == null || !entry.isVideo) return;
|
||||||
|
|
||||||
|
final path = entry.path;
|
||||||
|
var controllerEntry = _videoControllers.firstWhere((kv) => kv.item1 == entry.path, orElse: () => null);
|
||||||
|
if (controllerEntry != null) {
|
||||||
|
_videoControllers.remove(controllerEntry);
|
||||||
|
} else {
|
||||||
|
final controller = VideoPlayerController.file(File(path))..initialize();
|
||||||
|
controllerEntry = Tuple2(path, controller);
|
||||||
|
}
|
||||||
|
_videoControllers.insert(0, controllerEntry);
|
||||||
|
while (_videoControllers.length > 3) {
|
||||||
|
_videoControllers.removeLast().item2.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FullscreenAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share }
|
enum FullscreenAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share }
|
||||||
|
@ -354,6 +403,7 @@ class ImagePage extends StatefulWidget {
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final ValueChanged<int> onPageChanged;
|
final ValueChanged<int> onPageChanged;
|
||||||
final ValueChanged<PhotoViewScaleState> onScaleChanged;
|
final ValueChanged<PhotoViewScaleState> onScaleChanged;
|
||||||
|
final List<Tuple2<String, VideoPlayerController>> videoControllers;
|
||||||
|
|
||||||
const ImagePage({
|
const ImagePage({
|
||||||
this.collection,
|
this.collection,
|
||||||
|
@ -361,6 +411,7 @@ class ImagePage extends StatefulWidget {
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onPageChanged,
|
this.onPageChanged,
|
||||||
this.onScaleChanged,
|
this.onScaleChanged,
|
||||||
|
this.videoControllers,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -378,10 +429,19 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
|
||||||
builder: (galleryContext, index) {
|
builder: (galleryContext, index) {
|
||||||
final entry = entries[index];
|
final entry = entries[index];
|
||||||
if (entry.isVideo) {
|
if (entry.isVideo) {
|
||||||
|
final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.path, orElse: () => null)?.item2;
|
||||||
return PhotoViewGalleryPageOptions.customChild(
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
child: AvesVideo(entry: entry),
|
child: videoController != null
|
||||||
|
? AvesVideo(
|
||||||
|
entry: entry,
|
||||||
|
controller: videoController,
|
||||||
|
)
|
||||||
|
: SizedBox(),
|
||||||
childSize: MediaQuery.of(galleryContext).size,
|
childSize: MediaQuery.of(galleryContext).size,
|
||||||
// no heroTag because `Chewie` already internally builds one with the videoController
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
|
tag: entry.uri,
|
||||||
|
transitionOnUserGestures: true,
|
||||||
|
),
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
initialScale: PhotoViewComputedScale.contained,
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
onTapUp: (tapContext, details, value) => widget.onTap?.call(),
|
onTapUp: (tapContext, details, value) => widget.onTap?.call(),
|
||||||
|
|
|
@ -16,8 +16,8 @@ class FullscreenBottomOverlay extends StatefulWidget {
|
||||||
|
|
||||||
const FullscreenBottomOverlay({
|
const FullscreenBottomOverlay({
|
||||||
Key key,
|
Key key,
|
||||||
this.entries,
|
@required this.entries,
|
||||||
this.index,
|
@required this.index,
|
||||||
this.viewInsets,
|
this.viewInsets,
|
||||||
this.viewPadding,
|
this.viewPadding,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
|
@ -15,9 +15,9 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
|
|
||||||
const FullscreenTopOverlay({
|
const FullscreenTopOverlay({
|
||||||
Key key,
|
Key key,
|
||||||
this.entries,
|
@required this.entries,
|
||||||
this.index,
|
@required this.index,
|
||||||
this.scale,
|
@required this.scale,
|
||||||
this.viewInsets,
|
this.viewInsets,
|
||||||
this.viewPadding,
|
this.viewPadding,
|
||||||
this.onActionSelected,
|
this.onActionSelected,
|
||||||
|
|
165
lib/widgets/fullscreen/overlay_video.dart
Normal file
165
lib/widgets/fullscreen/overlay_video.dart
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/utils/android_app_service.dart';
|
||||||
|
import 'package:aves/utils/date_utils.dart';
|
||||||
|
import 'package:aves/widgets/common/blurred.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/overlay_top.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
class VideoControlOverlay extends StatefulWidget {
|
||||||
|
final ImageEntry entry;
|
||||||
|
final Animation<double> scale;
|
||||||
|
final VideoPlayerController controller;
|
||||||
|
final EdgeInsets viewInsets, viewPadding;
|
||||||
|
|
||||||
|
const VideoControlOverlay({
|
||||||
|
Key key,
|
||||||
|
@required this.entry,
|
||||||
|
@required this.controller,
|
||||||
|
@required this.scale,
|
||||||
|
this.viewInsets,
|
||||||
|
this.viewPadding,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => VideoControlOverlayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoControlOverlayState extends State<VideoControlOverlay> {
|
||||||
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
Animation<double> get scale => widget.scale;
|
||||||
|
|
||||||
|
VideoPlayerController get controller => widget.controller;
|
||||||
|
|
||||||
|
VideoPlayerValue get value => widget.controller.value;
|
||||||
|
|
||||||
|
double get progress => value.position != null && value.duration != null ? value.position.inMilliseconds / value.duration.inMilliseconds : 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
registerWidget(widget);
|
||||||
|
_onValueChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(VideoControlOverlay oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
unregisterWidget(oldWidget);
|
||||||
|
registerWidget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
unregisterWidget(widget);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerWidget(VideoControlOverlay widget) {
|
||||||
|
widget.controller.addListener(_onValueChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterWidget(VideoControlOverlay widget) {
|
||||||
|
widget.controller.removeListener(_onValueChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final progressBarBorderRadius = 123.0;
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final viewInsets = widget.viewInsets ?? mediaQuery.viewInsets;
|
||||||
|
final viewPadding = widget.viewPadding ?? mediaQuery.viewPadding;
|
||||||
|
final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0);
|
||||||
|
return Padding(
|
||||||
|
padding: safePadding,
|
||||||
|
child: value.hasError
|
||||||
|
? OverlayButton(
|
||||||
|
scale: scale,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(Icons.open_in_new),
|
||||||
|
onPressed: () => AndroidAppService.open(entry.uri, entry.mimeType),
|
||||||
|
tooltip: 'Open',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SizedBox(
|
||||||
|
width: mediaQuery.size.width - safePadding.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
OverlayButton(
|
||||||
|
scale: scale,
|
||||||
|
child: value.isPlaying
|
||||||
|
? IconButton(
|
||||||
|
icon: Icon(Icons.pause),
|
||||||
|
onPressed: () => _playPause(),
|
||||||
|
tooltip: 'Pause',
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
icon: Icon(Icons.play_arrow),
|
||||||
|
onPressed: () => _playPause(),
|
||||||
|
tooltip: 'Play',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: scale,
|
||||||
|
child: BlurredRRect(
|
||||||
|
borderRadius: progressBarBorderRadius,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16) + EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black26,
|
||||||
|
border: Border.all(color: Colors.white30, width: 0.5),
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(progressBarBorderRadius),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(formatDuration(value.position ?? Duration.zero)),
|
||||||
|
Spacer(),
|
||||||
|
Text(formatDuration(value.duration ?? Duration.zero)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
LinearProgressIndicator(value: progress),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
OverlayButton(
|
||||||
|
scale: scale,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(Icons.fullscreen),
|
||||||
|
onPressed: () => _goFullscreen(),
|
||||||
|
tooltip: 'Fullscreen',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_playPause() async {
|
||||||
|
if (value.isPlaying) {
|
||||||
|
controller.pause();
|
||||||
|
} else {
|
||||||
|
if (!value.initialized) {
|
||||||
|
await controller.initialize();
|
||||||
|
}
|
||||||
|
controller.play();
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
_goFullscreen() {}
|
||||||
|
|
||||||
|
_onValueChange() => setState(() {});
|
||||||
|
}
|
|
@ -1,49 +1,82 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:chewie/chewie.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
class AvesVideo extends StatefulWidget {
|
class AvesVideo extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
final VideoPlayerController controller;
|
||||||
|
|
||||||
const AvesVideo({Key key, this.entry}) : super(key: key);
|
const AvesVideo({
|
||||||
|
Key key,
|
||||||
|
@required this.entry,
|
||||||
|
@required this.controller,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => AvesVideoState();
|
State<StatefulWidget> createState() => AvesVideoState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvesVideoState extends State<AvesVideo> {
|
class AvesVideoState extends State<AvesVideo> {
|
||||||
VideoPlayerController videoPlayerController;
|
|
||||||
ChewieController chewieController;
|
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
VideoPlayerValue get value => widget.controller.value;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
videoPlayerController = VideoPlayerController.file(
|
registerWidget(widget);
|
||||||
File(entry.path),
|
_onValueChange();
|
||||||
);
|
}
|
||||||
chewieController = ChewieController(
|
|
||||||
videoPlayerController: videoPlayerController,
|
@override
|
||||||
aspectRatio: entry.aspectRatio,
|
void didUpdateWidget(AvesVideo oldWidget) {
|
||||||
autoInitialize: true,
|
super.didUpdateWidget(oldWidget);
|
||||||
);
|
unregisterWidget(oldWidget);
|
||||||
|
registerWidget(widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
videoPlayerController.dispose();
|
unregisterWidget(widget);
|
||||||
chewieController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerWidget(AvesVideo widget) {
|
||||||
|
widget.controller.addListener(_onValueChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterWidget(AvesVideo widget) {
|
||||||
|
widget.controller.removeListener(_onValueChange);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Chewie(
|
if (value == null) return SizedBox();
|
||||||
controller: chewieController,
|
if (value.hasError)
|
||||||
|
return Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return Center(
|
||||||
|
child: Container(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: entry.aspectRatio,
|
||||||
|
child: VideoPlayer(widget.controller),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onValueChange() {
|
||||||
|
if (!value.isPlaying && value.position == value.duration) goToBeginning();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
goToBeginning() async {
|
||||||
|
await widget.controller.seekTo(Duration.zero);
|
||||||
|
await widget.controller.pause();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue