mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
405 lines
12 KiB
Dart
405 lines
12 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:kiwi/kiwi.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:nc_photos/account.dart';
|
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
|
import 'package:nc_photos/app_localizations.dart';
|
|
import 'package:nc_photos/di_container.dart';
|
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
|
import 'package:nc_photos/entity/pref.dart';
|
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
|
import 'package:nc_photos/k.dart' as k;
|
|
import 'package:nc_photos/np_api_util.dart';
|
|
import 'package:nc_photos/snack_bar_manager.dart';
|
|
import 'package:nc_photos/use_case/request_public_link.dart';
|
|
import 'package:nc_photos/widget/disposable.dart';
|
|
import 'package:nc_photos/widget/wakelock_util.dart';
|
|
import 'package:np_codegen/np_codegen.dart';
|
|
import 'package:np_platform_util/np_platform_util.dart';
|
|
import 'package:np_ui/np_ui.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
part 'video_viewer.g.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,
|
|
this.canLoop = true,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
createState() => _VideoViewerState();
|
|
|
|
final Account account;
|
|
final FileDescriptor file;
|
|
final VoidCallback? onLoaded;
|
|
final VoidCallback? onLoadFailure;
|
|
final ValueChanged<double>? onHeightChanged;
|
|
final VoidCallback? onPlay;
|
|
final VoidCallback? onPause;
|
|
final bool isControlVisible;
|
|
final bool canPlay;
|
|
|
|
/// If false, disable the loop control and always stop after playing once
|
|
final bool canLoop;
|
|
}
|
|
|
|
@npLog
|
|
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();
|
|
}
|
|
|
|
Future<void> _initController(String url) async {
|
|
try {
|
|
_controller = VideoPlayerController.network(
|
|
url,
|
|
httpHeaders: {
|
|
"Authorization": AuthUtil.fromAccount(widget.account).toHeaderValue(),
|
|
},
|
|
);
|
|
await _controller.initialize();
|
|
final c = KiwiContainer().resolve<DiContainer>();
|
|
unawaited(_controller.setVolume(c.pref.isVideoPlayerMuteOr() ? 0 : 1));
|
|
if (widget.canLoop) {
|
|
unawaited(_controller.setLooping(c.pref.isVideoPlayerLoopOr()));
|
|
}
|
|
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: Theme.of(context).textTheme.labelLarge!.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: VideoProgressIndicator(
|
|
_controller,
|
|
allowScrubbing: true,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
colors: VideoProgressColors(
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.surface,
|
|
bufferedColor:
|
|
Theme.of(context).colorScheme.surfaceVariant,
|
|
playedColor: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
if (_controller.value.duration != Duration.zero)
|
|
Text(
|
|
_durationToString(_controller.value.duration),
|
|
style: Theme.of(context).textTheme.labelLarge!.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
if (widget.canLoop) _LoopToggle(controller: _controller),
|
|
_MuteToggle(controller: _controller),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
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 _play() {
|
|
if (widget.canPlay) {
|
|
_isFinished = false;
|
|
_controller.play();
|
|
widget.onPlay?.call();
|
|
}
|
|
}
|
|
|
|
void _pause() {
|
|
_controller.pause();
|
|
widget.onPause?.call();
|
|
}
|
|
|
|
Future<String> _getVideoUrl() async {
|
|
if (getRawPlatform() == NpPlatform.web) {
|
|
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;
|
|
}
|
|
|
|
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: () {
|
|
final willLoop = !widget.controller.value.isLooping;
|
|
setState(() {
|
|
widget.controller.setLooping(willLoop);
|
|
});
|
|
final c = KiwiContainer().resolve<DiContainer>();
|
|
c.pref.setVideoPlayerLoop(willLoop);
|
|
},
|
|
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"),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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"),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|