Files
b0esche_cloud/b0esche_cloud/lib/widgets/audio_player_bar.dart

321 lines
9.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:async';
import '../theme/app_theme.dart';
import '../theme/modern_glass_button.dart';
// Conditional imports for audio
import 'package:just_audio/just_audio.dart' as just_audio;
import 'web_audio_player.dart' as web_audio;
class AudioPlayerBar extends StatefulWidget {
final String fileName;
final String fileUrl;
final String? mimeType;
final VoidCallback? onClose;
const AudioPlayerBar({
super.key,
required this.fileName,
required this.fileUrl,
this.mimeType,
this.onClose,
});
@override
State<AudioPlayerBar> createState() => _AudioPlayerBarState();
}
class _AudioPlayerBarState extends State<AudioPlayerBar>
with SingleTickerProviderStateMixin {
dynamic _audioPlayer;
late AnimationController _iconController;
Duration _duration = Duration.zero;
Duration _position = Duration.zero;
bool _isPlaying = false;
bool _isLoading = true;
StreamSubscription? _positionSubscription;
StreamSubscription? _durationSubscription;
StreamSubscription? _playingSubscription;
StreamSubscription? _errorSubscription;
@override
void initState() {
super.initState();
_audioPlayer = kIsWeb ? web_audio.AudioPlayer() : just_audio.AudioPlayer();
_iconController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 250),
);
_initAudio();
}
@override
void dispose() {
_iconController.dispose();
_positionSubscription?.cancel();
_durationSubscription?.cancel();
_playingSubscription?.cancel();
_errorSubscription?.cancel();
_audioPlayer.dispose();
super.dispose();
}
String? _errorMsg;
Future<void> _initAudio() async {
try {
if (kIsWeb) {
// Web implementation
await _audioPlayer.setUrl(
widget.fileUrl,
mimeType: widget.mimeType,
);
_durationSubscription = _audioPlayer.durationStream.listen((d) {
if (d != null) {
setState(() {
_duration = d;
_isLoading = false;
});
}
});
_positionSubscription = _audioPlayer.positionStream.listen((pos) {
setState(() {
_position = pos;
});
});
_playingSubscription = _audioPlayer.playingStream.listen((playing) {
setState(() {
_isPlaying = playing;
});
if (playing) {
if (mounted) _iconController.forward();
} else {
if (mounted) _iconController.reverse();
}
});
_errorSubscription = _audioPlayer.errorStream.listen((error) {
setState(() {
_errorMsg = error.toString();
_isLoading = false;
});
});
// Auto-play for web
await _audioPlayer.play();
} else {
// Mobile implementation (just_audio)
await _audioPlayer.setAudioSource(
just_audio.AudioSource.uri(Uri.parse(widget.fileUrl)),
);
_audioPlayer.durationStream.firstWhere((d) => d != null).then((
d,
) async {
setState(() {
_duration = d ?? Duration.zero;
_isLoading = false;
});
try {
await _audioPlayer.play(); // Start playback automatically
// For mobile, check playing state after a short delay
Future.delayed(const Duration(milliseconds: 100), () {
// Use dynamic access to avoid compilation issues with conditional imports
final player = _audioPlayer as dynamic;
if (player.playerState?.playing == true) {
if (mounted) _iconController.forward();
} else {
setState(() {
_errorMsg = 'Audio could not be played.';
});
}
});
} catch (e) {
setState(() {
_errorMsg =
'Audio playback error: ${e is Exception ? e.toString() : 'Unknown error'}';
});
}
});
_audioPlayer.positionStream.listen((pos) {
setState(() {
_position = pos;
});
});
// Use dynamic access to avoid compilation issues with conditional imports
final player = _audioPlayer as dynamic;
player.playerStateStream.listen((state) {
setState(() {
_isPlaying = state.playing;
});
if (state.playing) {
if (mounted) _iconController.forward();
} else {
if (mounted) _iconController.reverse();
}
});
}
} catch (e) {
setState(() {
_isLoading = false;
_errorMsg =
'Audio load error: ${e is Exception ? e.toString() : 'Unknown error'}';
});
}
}
void _handlePlayPause() {
if (_isPlaying) {
_audioPlayer.pause();
_iconController.reverse();
} else {
// If at end, seek to start
if (_position >= _duration && _duration > Duration.zero) {
_audioPlayer.seek(Duration.zero);
}
_audioPlayer.play();
_iconController.forward();
}
}
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: AppTheme.glassDecoration.copyWith(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.accentColor.withValues(alpha: 0.15),
blurRadius: 12,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Play/Pause button (AnimatedIcon)
ModernGlassButton(
onPressed: _isLoading ? () {} : _handlePlayPause,
child: Transform.translate(
offset: const Offset(0, -4), // Move icon up by 4px
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _iconController,
color: AppTheme.primaryText,
size: 22,
),
),
),
const SizedBox(width: 10),
// File name and slider
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.fileName,
style: const TextStyle(
color: AppTheme.primaryText,
fontWeight: FontWeight.w600,
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(
height: 18,
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 2.2,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6,
),
overlayShape: SliderComponentShape.noOverlay,
activeTrackColor: AppTheme.accentColor,
inactiveTrackColor: AppTheme.accentColor.withValues(
alpha: 0.18,
),
),
child: Slider(
min: 0,
max: _duration.inMilliseconds.toDouble(),
value: _position.inMilliseconds
.clamp(0, _duration.inMilliseconds)
.toDouble(),
onChanged: _isLoading
? null
: (value) {
_audioPlayer.seek(
Duration(milliseconds: value.toInt()),
);
},
activeColor: AppTheme.accentColor,
inactiveColor: AppTheme.accentColor.withValues(
alpha: 0.18,
),
),
),
),
if (_errorMsg != null)
Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text(
_errorMsg!,
style: const TextStyle(
color: Colors.red,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const SizedBox(width: 10),
// Time
Text(
'${_formatDuration(_position)} / ${_formatDuration(_duration)}',
style: const TextStyle(
color: AppTheme.secondaryText,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
// Close button (simple small x)
if (widget.onClose != null)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: GestureDetector(
onTap: widget.onClose!,
child: const Text(
'×',
style: TextStyle(
color: AppTheme.primaryText,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
// Utility to format duration
String _formatDuration(Duration d) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(d.inMinutes.remainder(60));
final seconds = twoDigits(d.inSeconds.remainder(60));
return '$minutes:$seconds';
}
}