nc-photos/app/lib/widget/video_viewer.dart

410 lines
12 KiB
Dart
Raw Normal View History

2022-12-26 11:03:33 +01:00
import 'dart:async';
2021-05-06 13:36:20 +02:00
import 'package:flutter/material.dart';
2022-12-26 11:03:33 +01:00
import 'package:kiwi/kiwi.dart';
2021-05-09 12:32:03 +02:00
import 'package:logging/logging.dart';
2021-05-06 13:36:20 +02:00
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
2022-12-26 11:03:33 +01:00
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
2023-07-17 09:35:45 +02:00
import 'package:nc_photos/entity/pref.dart';
2021-08-05 09:04:12 +02:00
import 'package:nc_photos/exception_util.dart' as exception_util;
2021-05-06 13:36:20 +02:00
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/np_api_util.dart';
2021-08-05 09:04:12 +02:00
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/use_case/request_public_link.dart';
2021-09-14 22:39:34 +02:00
import 'package:nc_photos/widget/disposable.dart';
import 'package:nc_photos/widget/wakelock_util.dart';
2022-12-16 16:01:04 +01:00
import 'package:np_codegen/np_codegen.dart';
2023-08-27 12:58:05 +02:00
import 'package:np_platform_util/np_platform_util.dart';
import 'package:np_ui/np_ui.dart';
2021-05-06 13:36:20 +02:00
import 'package:video_player/video_player.dart';
2022-12-16 16:01:04 +01:00
part 'video_viewer.g.dart';
2021-05-06 13:36:20 +02:00
class VideoViewer extends StatefulWidget {
2021-09-15 08:58:06 +02:00
const VideoViewer({
2024-05-28 17:10:33 +02:00
super.key,
2021-07-23 22:05:57 +02:00
required this.account,
required this.file,
2021-05-06 13:36:20 +02:00
this.onLoaded,
2021-09-14 23:00:24 +02:00
this.onLoadFailure,
2021-05-06 13:36:20 +02:00
this.onHeightChanged,
this.onPlay,
this.onPause,
this.isControlVisible = false,
this.canPlay = true,
2022-12-29 17:18:22 +01:00
this.canLoop = true,
2024-05-28 17:10:33 +02:00
});
2021-05-06 13:36:20 +02:00
@override
createState() => _VideoViewerState();
final Account account;
final FileDescriptor file;
2021-07-23 22:05:57 +02:00
final VoidCallback? onLoaded;
2021-09-14 23:00:24 +02:00
final VoidCallback? onLoadFailure;
2021-07-23 22:05:57 +02:00
final ValueChanged<double>? onHeightChanged;
final VoidCallback? onPlay;
final VoidCallback? onPause;
2021-05-06 13:36:20 +02:00
final bool isControlVisible;
final bool canPlay;
2022-12-29 17:18:22 +01:00
/// If false, disable the loop control and always stop after playing once
final bool canLoop;
2021-05-06 13:36:20 +02:00
}
2022-12-16 16:01:04 +01:00
@npLog
2021-09-14 22:39:34 +02:00
class _VideoViewerState extends State<VideoViewer>
with DisposableManagerMixin<VideoViewer> {
2021-05-06 13:36:20 +02:00
@override
2024-05-11 15:26:04 +02:00
void initState() {
2021-05-06 13:36:20 +02:00
super.initState();
2021-08-05 09:04:12 +02:00
_getVideoUrl().then((url) {
2024-05-11 15:26:04 +02:00
if (mounted) {
setState(() {
_initController(url);
});
}
2021-08-05 09:04:12 +02:00
}).onError((e, stacktrace) {
_log.shout("[initState] Failed while _getVideoUrl", e, stacktrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
2021-08-05 09:04:12 +02:00
duration: k.snackBarDurationNormal,
));
2021-09-14 23:00:24 +02:00
widget.onLoadFailure?.call();
2021-08-05 09:04:12 +02:00
});
2021-09-14 22:39:34 +02:00
}
2024-05-11 15:26:04 +02:00
@override
void dispose() {
_controllerValue?.dispose();
super.dispose();
}
2021-09-14 22:39:34 +02:00
@override
initDisposables() {
return [
...super.initDisposables(),
WakelockControllerDisposable(),
];
2021-05-06 13:36:20 +02:00
}
@override
build(BuildContext context) {
Widget content;
2021-08-05 09:04:12 +02:00
if (_isControllerInitialized && _controller.value.isInitialized) {
2021-05-06 13:36:20 +02:00
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,
);
}
2022-07-28 20:45:43 +02:00
Future<void> _initController(String url) async {
2021-08-29 17:22:10 +02:00
try {
_controllerValue = VideoPlayerController.networkUrl(
Uri.parse(url),
2021-08-29 17:22:10 +02:00
httpHeaders: {
"Authorization": AuthUtil.fromAccount(widget.account).toHeaderValue(),
2021-08-29 17:22:10 +02:00
},
);
await _controller.initialize();
2022-12-26 11:03:33 +01:00
final c = KiwiContainer().resolve<DiContainer>();
unawaited(_controller.setVolume(c.pref.isVideoPlayerMuteOr() ? 0 : 1));
2022-12-29 17:18:22 +01:00
if (widget.canLoop) {
unawaited(_controller.setLooping(c.pref.isVideoPlayerLoopOr()));
}
2021-08-29 17:22:10 +02:00
widget.onLoaded?.call();
2022-06-20 13:49:58 +02:00
WidgetsBinding.instance.addPostFrameCallback((_) {
2021-08-29 17:22:10 +02:00
if (_key.currentContext != null) {
widget.onHeightChanged?.call(_key.currentContext!.size!.height);
}
2021-08-05 09:04:12 +02:00
});
2021-08-29 17:22:10 +02:00
_controller.addListener(_onControllerChanged);
_isControllerInitialized = true;
2021-08-29 18:28:32 +02:00
setState(() {
_play();
});
2021-08-29 17:22:10 +02:00
} catch (e, stackTrace) {
_log.shout("[_initController] Failed while initialize", e, stackTrace);
SnackBarManager().showSnackBar(SnackBar(
content: Text(exception_util.toUserString(e)),
duration: k.snackBarDurationNormal,
));
2021-09-14 23:00:24 +02:00
widget.onLoadFailure?.call();
2021-08-29 17:22:10 +02:00
}
2021-08-05 09:04:12 +02:00
}
2021-05-06 13:36:20 +02:00
Widget _buildPlayer(BuildContext context) {
if (_controller.value.isPlaying && !widget.canPlay) {
2022-06-20 13:49:58 +02:00
WidgetsBinding.instance.addPostFrameCallback((_) {
2021-05-06 13:36:20 +02:00
_pause();
});
}
return Stack(
children: [
Align(
alignment: Alignment.center,
child: AspectRatio(
key: _key,
aspectRatio: _controller.value.aspectRatio,
2021-08-05 09:04:12 +02:00
child: IgnorePointer(
child: VideoPlayer(_controller),
),
2021-05-06 13:36:20 +02:00
),
),
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(),
),
),
),
),
),
2021-09-15 12:50:51 +02:00
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),
2022-11-12 10:55:33 +01:00
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
2021-08-29 17:16:33 +02:00
),
2021-09-15 12:50:51 +02:00
),
const SizedBox(width: 8),
Expanded(
child: VideoProgressIndicator(
_controller,
allowScrubbing: true,
padding: const EdgeInsets.symmetric(vertical: 8),
2022-11-12 10:55:33 +01:00
colors: VideoProgressColors(
backgroundColor:
Theme.of(context).colorScheme.surface,
bufferedColor:
Theme.of(context).colorScheme.surfaceVariant,
playedColor: Theme.of(context).colorScheme.primary,
2021-08-29 17:14:36 +02:00
),
),
2021-09-15 12:50:51 +02:00
),
const SizedBox(width: 8),
if (_controller.value.duration != Duration.zero)
Text(
_durationToString(_controller.value.duration),
2022-11-12 10:55:33 +01:00
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
2021-09-15 12:50:51 +02:00
),
const SizedBox(width: 4),
2022-12-29 17:18:22 +01:00
if (widget.canLoop) _LoopToggle(controller: _controller),
2022-12-26 11:14:59 +01:00
_MuteToggle(controller: _controller),
2021-09-15 12:50:51 +02:00
],
2021-05-06 13:36:20 +02:00
),
),
),
),
),
],
);
}
void _onPlayPressed() {
if (_controller.value.position == _controller.value.duration) {
2021-05-06 13:36:20 +02:00
_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();
});
2021-05-06 13:36:20 +02:00
}
}
void _play() {
if (widget.canPlay) {
_isFinished = false;
2021-05-06 13:36:20 +02:00
_controller.play();
widget.onPlay?.call();
}
}
void _pause() {
_controller.pause();
widget.onPause?.call();
}
2021-08-05 09:04:12 +02:00
Future<String> _getVideoUrl() async {
2023-08-27 12:58:05 +02:00
if (getRawPlatform() == NpPlatform.web) {
2021-08-05 09:04:12 +02:00
return RequestPublicLink()(widget.account, widget.file);
} else {
return api_util.getFileUrl(widget.account, widget.file);
}
}
2024-05-11 15:26:04 +02:00
VideoPlayerController get _controller => _controllerValue!;
2021-05-06 13:36:20 +02:00
final _key = GlobalKey();
2021-08-05 09:04:12 +02:00
bool _isControllerInitialized = false;
2024-05-11 15:26:04 +02:00
VideoPlayerController? _controllerValue;
2021-05-06 13:36:20 +02:00
var _isFinished = false;
}
2021-08-29 17:16:33 +02:00
2022-12-26 04:32:29 +01:00
class _LoopToggle extends StatefulWidget {
const _LoopToggle({
required this.controller,
});
@override
State<StatefulWidget> createState() => _LoopToggleState();
final VideoPlayerController controller;
}
class _LoopToggleState extends State<_LoopToggle> {
@override
Widget build(BuildContext context) {
return Tooltip(
message: L10n.global().loopTooltip,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(32)),
onTap: () {
2022-12-26 11:03:33 +01:00
final willLoop = !widget.controller.value.isLooping;
2022-12-26 04:32:29 +01:00
setState(() {
2022-12-26 11:03:33 +01:00
widget.controller.setLooping(willLoop);
2022-12-26 04:32:29 +01:00
});
2022-12-26 11:03:33 +01:00
final c = KiwiContainer().resolve<DiContainer>();
c.pref.setVideoPlayerLoop(willLoop);
2022-12-26 04:32:29 +01:00
},
child: Padding(
padding: const EdgeInsets.all(4),
child: AnimatedSwitcher(
duration: k.animationDurationNormal,
child: widget.controller.value.isLooping
? const Icon(
Icons.loop,
key: Key("loop_on"),
)
: const Icon(
Icons.sync_disabled,
key: Key("loop_off"),
),
),
),
),
);
}
}
2022-12-26 11:14:59 +01:00
class _MuteToggle extends StatefulWidget {
const _MuteToggle({
required this.controller,
});
@override
State<StatefulWidget> createState() => _MuteToggleState();
final VideoPlayerController controller;
}
class _MuteToggleState extends State<_MuteToggle> {
@override
Widget build(BuildContext context) {
return Tooltip(
message: widget.controller.value.volume == 0
? L10n.global().unmuteTooltip
: L10n.global().muteTooltip,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(32)),
onTap: () {
final willMute = widget.controller.value.volume != 0;
setState(() {
widget.controller.setVolume(willMute ? 0 : 1);
});
final c = KiwiContainer().resolve<DiContainer>();
c.pref.setVideoPlayerMute(willMute);
},
child: Padding(
padding: const EdgeInsets.all(4),
child: AnimatedSwitcher(
duration: k.animationDurationNormal,
child: widget.controller.value.volume == 0
? const Icon(
Icons.volume_off_outlined,
key: Key("mute_on"),
)
: const Icon(
Icons.volume_up_outlined,
key: Key("mute_off"),
),
),
),
),
);
}
}
2021-08-29 17:16:33 +02:00
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;
}