split file_viewer into focused sub-widgets
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../app_progress_indicator.dart';
|
||||
|
||||
/// Plays a local video (via Chewie) or audio file (via [_AudioControls]).
|
||||
/// Reports an inline "format not supported" message when the platform can't
|
||||
/// initialize the file.
|
||||
class MediaPlayer extends StatefulWidget {
|
||||
final String path;
|
||||
final bool isAudio;
|
||||
final String? filename;
|
||||
const MediaPlayer({
|
||||
super.key,
|
||||
required this.path,
|
||||
required this.isAudio,
|
||||
this.filename,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MediaPlayer> createState() => _MediaPlayerState();
|
||||
}
|
||||
|
||||
class _MediaPlayerState extends State<MediaPlayer> {
|
||||
VideoPlayerController? _video;
|
||||
ChewieController? _chewie;
|
||||
Object? _initError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
final controller = VideoPlayerController.file(File(widget.path));
|
||||
try {
|
||||
await controller.initialize();
|
||||
} on Object catch (e) {
|
||||
await controller.dispose();
|
||||
if (!mounted) return;
|
||||
setState(() => _initError = e);
|
||||
return;
|
||||
}
|
||||
if (!mounted) {
|
||||
await controller.dispose();
|
||||
return;
|
||||
}
|
||||
if (widget.isAudio) {
|
||||
controller.addListener(_onAudioTick);
|
||||
setState(() => _video = controller);
|
||||
} else {
|
||||
setState(() {
|
||||
_video = controller;
|
||||
_chewie = ChewieController(
|
||||
videoPlayerController: controller,
|
||||
autoPlay: false,
|
||||
looping: false,
|
||||
allowFullScreen: true,
|
||||
allowPlaybackSpeedChanging: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onAudioTick() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_video?.removeListener(_onAudioTick);
|
||||
_chewie?.dispose();
|
||||
_video?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_initError != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
widget.isAudio
|
||||
? 'Audio kann nicht abgespielt werden'
|
||||
: 'Video kann nicht abgespielt werden',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Format wird auf diesem Gerät nicht unterstützt. Über das Menü kannst du die Datei in einer anderen App öffnen.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_video == null) {
|
||||
return const Center(child: AppProgressIndicator.large());
|
||||
}
|
||||
if (widget.isAudio) {
|
||||
return _AudioControls(
|
||||
controller: _video!,
|
||||
filename: widget.filename ?? '',
|
||||
);
|
||||
}
|
||||
return Chewie(controller: _chewie!);
|
||||
}
|
||||
}
|
||||
|
||||
class _AudioControls extends StatelessWidget {
|
||||
final VideoPlayerController controller;
|
||||
final String filename;
|
||||
const _AudioControls({required this.controller, required this.filename});
|
||||
|
||||
String _format(Duration d) {
|
||||
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
if (d.inHours > 0) return '${d.inHours}:$m:$s';
|
||||
return '$m:$s';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = controller.value;
|
||||
final duration = value.duration;
|
||||
final position = value.position;
|
||||
final maxMs = duration.inMilliseconds == 0 ? 1 : duration.inMilliseconds;
|
||||
final posMs = position.inMilliseconds.clamp(0, maxMs).toDouble();
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.audiotrack,
|
||||
size: 96,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
filename,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Slider(
|
||||
min: 0,
|
||||
max: maxMs.toDouble(),
|
||||
value: posMs,
|
||||
onChanged: (v) =>
|
||||
controller.seekTo(Duration(milliseconds: v.toInt())),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_format(position),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
_format(duration),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FloatingActionButton(
|
||||
heroTag: 'audioPlayPause',
|
||||
onPressed: () {
|
||||
if (value.isPlaying) {
|
||||
controller.pause();
|
||||
} else {
|
||||
controller.play();
|
||||
}
|
||||
},
|
||||
child: Icon(value.isPlaying ? Icons.pause : Icons.play_arrow),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user