334 lines
10 KiB
Dart
334 lines
10 KiB
Dart
import "dart:async";
|
|
import "dart:io";
|
|
|
|
import "package:flutter/cupertino.dart";
|
|
import "package:flutter/material.dart";
|
|
import "package:media_kit/media_kit.dart";
|
|
import "package:media_kit_video/media_kit_video.dart";
|
|
import "package:photos/core/constants.dart";
|
|
import "package:photos/generated/l10n.dart";
|
|
import "package:photos/models/file/extensions/file_props.dart";
|
|
import "package:photos/models/file/file.dart";
|
|
import "package:photos/services/files_service.dart";
|
|
import "package:photos/theme/colors.dart";
|
|
import "package:photos/theme/ente_theme.dart";
|
|
import "package:photos/ui/actions/file/file_actions.dart";
|
|
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
|
|
import "package:photos/utils/dialog_util.dart";
|
|
import "package:photos/utils/file_util.dart";
|
|
import "package:photos/utils/toast_util.dart";
|
|
|
|
class VideoWidgetNew extends StatefulWidget {
|
|
final EnteFile file;
|
|
final String? tagPrefix;
|
|
final Function(bool)? playbackCallback;
|
|
const VideoWidgetNew(
|
|
this.file, {
|
|
this.tagPrefix,
|
|
this.playbackCallback,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
State<VideoWidgetNew> createState() => _VideoWidgetNewState();
|
|
}
|
|
|
|
class _VideoWidgetNewState extends State<VideoWidgetNew>
|
|
with WidgetsBindingObserver {
|
|
static const verticalMargin = 72.0;
|
|
late final player = Player();
|
|
VideoController? controller;
|
|
final _progressNotifier = ValueNotifier<double?>(null);
|
|
late StreamSubscription<bool> playingStreamSubscription;
|
|
bool _isAppInFG = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
if (widget.file.isRemoteFile) {
|
|
_loadNetworkVideo();
|
|
_setFileSizeIfNull();
|
|
} else if (widget.file.isSharedMediaToAppSandbox) {
|
|
final localFile = File(getSharedMediaFilePath(widget.file));
|
|
if (localFile.existsSync()) {
|
|
_setVideoController(localFile.path);
|
|
} else if (widget.file.uploadedFileID != null) {
|
|
_loadNetworkVideo();
|
|
}
|
|
} else {
|
|
widget.file.getAsset.then((asset) async {
|
|
if (asset == null || !(await asset.exists)) {
|
|
if (widget.file.uploadedFileID != null) {
|
|
_loadNetworkVideo();
|
|
}
|
|
} else {
|
|
asset.getMediaUrl().then((url) {
|
|
_setVideoController(
|
|
url ??
|
|
'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
|
|
);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
playingStreamSubscription = player.stream.playing.listen((event) {
|
|
if (widget.playbackCallback != null && mounted) {
|
|
widget.playbackCallback!(event);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed) {
|
|
_isAppInFG = true;
|
|
} else {
|
|
_isAppInFG = false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
playingStreamSubscription.cancel();
|
|
player.dispose();
|
|
_progressNotifier.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = getEnteColorScheme(context);
|
|
return Hero(
|
|
tag: widget.tagPrefix! + widget.file.tag,
|
|
child: MaterialVideoControlsTheme(
|
|
normal: MaterialVideoControlsThemeData(
|
|
backdropColor: null,
|
|
automaticallyImplySkipNextButton: false,
|
|
automaticallyImplySkipPreviousButton: false,
|
|
seekOnDoubleTap: false,
|
|
displaySeekBar: true,
|
|
seekBarMargin: const EdgeInsets.only(bottom: verticalMargin),
|
|
bottomButtonBarMargin: const EdgeInsets.only(bottom: 112),
|
|
controlsHoverDuration: const Duration(seconds: 3),
|
|
seekBarHeight: 2,
|
|
seekBarThumbSize: 16,
|
|
seekBarBufferColor: Colors.transparent,
|
|
seekBarThumbColor: backgroundElevatedLight,
|
|
seekBarColor: fillMutedDark,
|
|
seekBarPositionColor: colorScheme.primary300,
|
|
seekBarContainerHeight: 56,
|
|
seekBarAlignment: Alignment.center,
|
|
|
|
///topButtonBarMargin is needed for keeping the buffering loading
|
|
///indicator to be center aligned
|
|
topButtonBarMargin: const EdgeInsets.only(top: verticalMargin),
|
|
bottomButtonBar: [
|
|
const Spacer(),
|
|
PausePlayAndDuration(controller?.player),
|
|
const Spacer(),
|
|
],
|
|
primaryButtonBar: [],
|
|
),
|
|
fullscreen: const MaterialVideoControlsThemeData(),
|
|
child: GestureDetector(
|
|
onVerticalDragUpdate: (d) => {
|
|
if (d.delta.dy > dragSensitivity)
|
|
{
|
|
Navigator.of(context).pop(),
|
|
}
|
|
else if (d.delta.dy < (dragSensitivity * -1))
|
|
{
|
|
showDetailsSheet(context, widget.file),
|
|
},
|
|
},
|
|
child: Center(
|
|
child: controller != null
|
|
? Video(
|
|
controller: controller!,
|
|
)
|
|
: _getLoadingWidget(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _loadNetworkVideo() {
|
|
getFileFromServer(
|
|
widget.file,
|
|
progressCallback: (count, total) {
|
|
_progressNotifier.value = count / (widget.file.fileSize ?? total);
|
|
if (_progressNotifier.value == 1) {
|
|
if (mounted) {
|
|
showShortToast(context, S.of(context).decryptingVideo);
|
|
}
|
|
}
|
|
},
|
|
).then((file) {
|
|
if (file != null) {
|
|
_setVideoController(file.path);
|
|
}
|
|
}).onError((error, stackTrace) {
|
|
showErrorDialog(context, "Error", S.of(context).failedToDownloadVideo);
|
|
});
|
|
}
|
|
|
|
void _setFileSizeIfNull() {
|
|
if (widget.file.fileSize == null && widget.file.canEditMetaInfo) {
|
|
FilesService.instance
|
|
.getFileSize(widget.file.uploadedFileID!)
|
|
.then((value) {
|
|
widget.file.fileSize = value;
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Widget _getLoadingWidget() {
|
|
return Stack(
|
|
children: [
|
|
_getThumbnail(),
|
|
Container(
|
|
color: Colors.black12,
|
|
constraints: const BoxConstraints.expand(),
|
|
),
|
|
Center(
|
|
child: SizedBox.fromSize(
|
|
size: const Size.square(20),
|
|
child: ValueListenableBuilder(
|
|
valueListenable: _progressNotifier,
|
|
builder: (BuildContext context, double? progress, _) {
|
|
return progress == null || progress == 1
|
|
? const CupertinoActivityIndicator(
|
|
color: Colors.white,
|
|
)
|
|
: CircularProgressIndicator(
|
|
backgroundColor: Colors.black,
|
|
value: progress,
|
|
valueColor: const AlwaysStoppedAnimation<Color>(
|
|
Color.fromRGBO(45, 194, 98, 1.0),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _getThumbnail() {
|
|
return Container(
|
|
color: Colors.black,
|
|
constraints: const BoxConstraints.expand(),
|
|
child: ThumbnailWidget(
|
|
widget.file,
|
|
fit: BoxFit.contain,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _setVideoController(String url) {
|
|
if (mounted) {
|
|
setState(() {
|
|
controller = VideoController(player);
|
|
player.open(Media(url), play: _isAppInFG);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
class PausePlayAndDuration extends StatefulWidget {
|
|
final Player? player;
|
|
const PausePlayAndDuration(this.player, {super.key});
|
|
|
|
@override
|
|
State<PausePlayAndDuration> createState() => _PausePlayAndDurationState();
|
|
}
|
|
|
|
class _PausePlayAndDurationState extends State<PausePlayAndDuration> {
|
|
Color backgroundColor = fillStrongLight;
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTapDown: (details) {
|
|
setState(() {
|
|
backgroundColor = fillMutedDark;
|
|
});
|
|
},
|
|
onTapUp: (details) {
|
|
Future.delayed(const Duration(milliseconds: 175), () {
|
|
if (mounted) {
|
|
setState(() {
|
|
backgroundColor = fillStrongLight;
|
|
});
|
|
}
|
|
});
|
|
},
|
|
onTapCancel: () {
|
|
Future.delayed(const Duration(milliseconds: 175), () {
|
|
if (mounted) {
|
|
setState(() {
|
|
backgroundColor = fillStrongLight;
|
|
});
|
|
}
|
|
});
|
|
},
|
|
onTap: () => widget.player!.playOrPause(),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 150),
|
|
curve: Curves.easeInBack,
|
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
color: backgroundColor,
|
|
border: Border.all(
|
|
color: strokeFaintDark,
|
|
width: 1,
|
|
),
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
child: AnimatedSize(
|
|
duration: const Duration(seconds: 2),
|
|
curve: Curves.easeInOutExpo,
|
|
child: Row(
|
|
children: [
|
|
StreamBuilder(
|
|
builder: (context, snapshot) {
|
|
final bool isPlaying = snapshot.data ?? false;
|
|
return AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 350),
|
|
switchInCurve: Curves.easeInOutCirc,
|
|
switchOutCurve: Curves.easeInOutCirc,
|
|
child: Icon(
|
|
key: ValueKey(
|
|
isPlaying ? "pause_button" : "play_button",
|
|
),
|
|
isPlaying
|
|
? Icons.pause_rounded
|
|
: Icons.play_arrow_rounded,
|
|
color: backdropBaseLight,
|
|
size: 24,
|
|
),
|
|
);
|
|
},
|
|
initialData: widget.player?.state.playing,
|
|
stream: widget.player?.stream.playing,
|
|
),
|
|
const SizedBox(width: 8),
|
|
MaterialPositionIndicator(
|
|
style: getEnteTextTheme(context).tiny.copyWith(
|
|
color: textBaseDark,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|