mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-23 09:16:19 +01:00
322 lines
9.5 KiB
Dart
322 lines
9.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:nc_photos/account.dart';
|
|
import 'package:nc_photos/api/api.dart';
|
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
|
import 'package:nc_photos/app_localizations.dart';
|
|
import 'package:nc_photos/entity/file.dart';
|
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
|
import 'package:nc_photos/k.dart' as k;
|
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
|
import 'package:nc_photos/snack_bar_manager.dart';
|
|
import 'package:nc_photos/use_case/request_public_link.dart';
|
|
import 'package:nc_photos/widget/animated_visibility.dart';
|
|
import 'package:nc_photos/widget/disposable.dart';
|
|
import 'package:nc_photos/widget/wakelock_util.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
class VideoViewer extends StatefulWidget {
|
|
const VideoViewer({
|
|
Key? key,
|
|
required this.account,
|
|
required this.file,
|
|
this.onLoaded,
|
|
this.onLoadFailure,
|
|
this.onHeightChanged,
|
|
this.onPlay,
|
|
this.onPause,
|
|
this.isControlVisible = false,
|
|
this.canPlay = true,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
createState() => _VideoViewerState();
|
|
|
|
final Account account;
|
|
final File file;
|
|
final VoidCallback? onLoaded;
|
|
final VoidCallback? onLoadFailure;
|
|
final ValueChanged<double>? onHeightChanged;
|
|
final VoidCallback? onPlay;
|
|
final VoidCallback? onPause;
|
|
final bool isControlVisible;
|
|
final bool canPlay;
|
|
}
|
|
|
|
class _VideoViewerState extends State<VideoViewer>
|
|
with DisposableManagerMixin<VideoViewer> {
|
|
@override
|
|
initState() {
|
|
super.initState();
|
|
_getVideoUrl().then((url) {
|
|
setState(() {
|
|
_initController(url);
|
|
});
|
|
}).onError((e, stacktrace) {
|
|
_log.shout("[initState] Failed while _getVideoUrl", e, stacktrace);
|
|
SnackBarManager().showSnackBar(SnackBar(
|
|
content: Text(exception_util.toUserString(e)),
|
|
duration: k.snackBarDurationNormal,
|
|
));
|
|
widget.onLoadFailure?.call();
|
|
});
|
|
}
|
|
|
|
@override
|
|
initDisposables() {
|
|
return [
|
|
...super.initDisposables(),
|
|
WakelockControllerDisposable(),
|
|
];
|
|
}
|
|
|
|
@override
|
|
build(BuildContext context) {
|
|
Widget content;
|
|
if (_isControllerInitialized && _controller.value.isInitialized) {
|
|
content = _buildPlayer(context);
|
|
} else {
|
|
content = Container();
|
|
}
|
|
|
|
return Container(
|
|
width: MediaQuery.of(context).size.width,
|
|
height: MediaQuery.of(context).size.height,
|
|
alignment: Alignment.center,
|
|
child: content,
|
|
);
|
|
}
|
|
|
|
@override
|
|
dispose() {
|
|
super.dispose();
|
|
_controller.dispose();
|
|
}
|
|
|
|
void _initController(String url) async {
|
|
try {
|
|
_controller = VideoPlayerController.network(
|
|
url,
|
|
httpHeaders: {
|
|
"Authorization": Api.getAuthorizationHeaderValue(widget.account),
|
|
},
|
|
);
|
|
await _controller.initialize();
|
|
widget.onLoaded?.call();
|
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
|
if (_key.currentContext != null) {
|
|
widget.onHeightChanged?.call(_key.currentContext!.size!.height);
|
|
}
|
|
});
|
|
_controller.addListener(_onControllerChanged);
|
|
_isControllerInitialized = true;
|
|
setState(() {
|
|
_play();
|
|
});
|
|
} catch (e, stackTrace) {
|
|
_log.shout("[_initController] Failed while initialize", e, stackTrace);
|
|
SnackBarManager().showSnackBar(SnackBar(
|
|
content: Text(exception_util.toUserString(e)),
|
|
duration: k.snackBarDurationNormal,
|
|
));
|
|
widget.onLoadFailure?.call();
|
|
}
|
|
}
|
|
|
|
Widget _buildPlayer(BuildContext context) {
|
|
if (_controller.value.isPlaying && !widget.canPlay) {
|
|
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
|
_pause();
|
|
});
|
|
}
|
|
|
|
return Stack(
|
|
children: [
|
|
Align(
|
|
alignment: Alignment.center,
|
|
child: AspectRatio(
|
|
key: _key,
|
|
aspectRatio: _controller.value.aspectRatio,
|
|
child: IgnorePointer(
|
|
child: VideoPlayer(_controller),
|
|
),
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
child: AnimatedVisibility(
|
|
opacity: widget.isControlVisible ? 1.0 : 0.0,
|
|
duration: k.animationDurationNormal,
|
|
child: Container(
|
|
color: Colors.black45,
|
|
child: Center(
|
|
child: IconButton(
|
|
icon: Icon(_controller.value.isPlaying
|
|
? Icons.pause_circle_filled
|
|
: Icons.play_circle_filled),
|
|
iconSize: 48,
|
|
padding: const EdgeInsets.all(16),
|
|
color: Colors.white,
|
|
onPressed: () => _controller.value.isPlaying
|
|
? _onPausePressed()
|
|
: _onPlayPressed(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
bottom: kToolbarHeight + 8, left: 8, right: 8),
|
|
child: AnimatedVisibility(
|
|
opacity: widget.isControlVisible ? 1.0 : 0.0,
|
|
duration: k.animationDurationNormal,
|
|
child: Material(
|
|
type: MaterialType.transparency,
|
|
child: Row(
|
|
children: [
|
|
ValueListenableBuilder(
|
|
valueListenable: _controller,
|
|
builder: (context, VideoPlayerValue value, child) => Text(
|
|
_durationToString(value.position),
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.white.withOpacity(.87),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: VideoProgressIndicator(
|
|
_controller,
|
|
allowScrubbing: true,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
colors: const VideoProgressColors(
|
|
backgroundColor: Colors.white24,
|
|
bufferedColor: Colors.white38,
|
|
playedColor: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
if (_controller.value.duration != Duration.zero)
|
|
Text(
|
|
_durationToString(_controller.value.duration),
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.white.withOpacity(.87),
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Tooltip(
|
|
message: _controller.value.volume == 0
|
|
? L10n.global().unmuteTooltip
|
|
: L10n.global().muteTooltip,
|
|
child: InkWell(
|
|
borderRadius:
|
|
const BorderRadius.all(Radius.circular(32)),
|
|
onTap: _onVolumnPressed,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(4),
|
|
child: Icon(
|
|
_controller.value.volume == 0
|
|
? Icons.volume_mute_outlined
|
|
: Icons.volume_up_outlined,
|
|
color: Colors.white.withOpacity(.87),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _onPlayPressed() {
|
|
if (_controller.value.position == _controller.value.duration) {
|
|
_controller.seekTo(const Duration()).then((_) {
|
|
setState(() {
|
|
_play();
|
|
});
|
|
});
|
|
} else {
|
|
setState(() {
|
|
_play();
|
|
});
|
|
}
|
|
}
|
|
|
|
void _onPausePressed() {
|
|
setState(() {
|
|
_pause();
|
|
});
|
|
}
|
|
|
|
void _onControllerChanged() {
|
|
if (!_controller.value.isInitialized) {
|
|
return;
|
|
}
|
|
if (!_isFinished &&
|
|
_controller.value.position == _controller.value.duration) {
|
|
_isFinished = true;
|
|
setState(() {
|
|
_pause();
|
|
});
|
|
}
|
|
}
|
|
|
|
void _onVolumnPressed() {
|
|
setState(() {
|
|
if (_controller.value.volume == 0) {
|
|
_controller.setVolume(1);
|
|
} else {
|
|
_controller.setVolume(0);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _play() {
|
|
if (widget.canPlay) {
|
|
_isFinished = false;
|
|
_controller.play();
|
|
widget.onPlay?.call();
|
|
}
|
|
}
|
|
|
|
void _pause() {
|
|
_controller.pause();
|
|
widget.onPause?.call();
|
|
}
|
|
|
|
Future<String> _getVideoUrl() async {
|
|
if (platform_k.isWeb) {
|
|
return RequestPublicLink()(widget.account, widget.file);
|
|
} else {
|
|
return api_util.getFileUrl(widget.account, widget.file);
|
|
}
|
|
}
|
|
|
|
final _key = GlobalKey();
|
|
bool _isControllerInitialized = false;
|
|
late VideoPlayerController _controller;
|
|
var _isFinished = false;
|
|
|
|
static final _log = Logger("widget.video_viewer._VideoViewerState");
|
|
}
|
|
|
|
String _durationToString(Duration duration) {
|
|
String product = "";
|
|
if (duration.inHours > 0) {
|
|
product += "${duration.inHours}:";
|
|
}
|
|
final minStr = (duration.inMinutes % 60).toString().padLeft(2, "0");
|
|
final secStr = (duration.inSeconds % 60).toString().padLeft(2, "0");
|
|
product += "$minStr:$secStr";
|
|
return product;
|
|
}
|