diff --git a/b0esche_cloud/lib/widgets/audio_player_bar.dart b/b0esche_cloud/lib/widgets/audio_player_bar.dart index b3e561f..348afe1 100644 --- a/b0esche_cloud/lib/widgets/audio_player_bar.dart +++ b/b0esche_cloud/lib/widgets/audio_player_bar.dart @@ -1,8 +1,13 @@ 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'; -import 'package:just_audio/just_audio.dart'; + +// Conditional imports for audio +import 'package:just_audio/just_audio.dart' + if (dart.library.html) 'web_audio_player.dart'; class AudioPlayerBar extends StatefulWidget { final String fileName; @@ -28,6 +33,10 @@ class _AudioPlayerBarState extends State Duration _position = Duration.zero; bool _isPlaying = false; bool _isLoading = true; + StreamSubscription? _positionSubscription; + StreamSubscription? _durationSubscription; + StreamSubscription? _playingSubscription; + StreamSubscription? _errorSubscription; @override void initState() { @@ -43,6 +52,10 @@ class _AudioPlayerBarState extends State @override void dispose() { _iconController.dispose(); + _positionSubscription?.cancel(); + _durationSubscription?.cancel(); + _playingSubscription?.cancel(); + _errorSubscription?.cancel(); _audioPlayer.dispose(); super.dispose(); } @@ -54,43 +67,95 @@ class _AudioPlayerBarState extends State print('[AudioPlayerBar] Loading audio URL: ${widget.fileUrl}'); try { await _audioPlayer.setUrl(widget.fileUrl); - // Wait until duration is available - _audioPlayer.durationStream.firstWhere((d) => d != null).then((d) async { - setState(() { - _duration = d ?? Duration.zero; - _isLoading = false; + + if (kIsWeb) { + // Web implementation + _durationSubscription = _audioPlayer.durationStream.listen((d) { + if (d != null) { + setState(() { + _duration = d; + _isLoading = false; + }); + } }); - try { - await _audioPlayer.play(); // Start playback automatically - if (_audioPlayer.playerState.playing) { + + _positionSubscription = _audioPlayer.positionStream.listen((pos) { + setState(() { + _position = pos; + }); + }); + + _playingSubscription = _audioPlayer.playingStream.listen((playing) { + setState(() { + _isPlaying = playing; + }); + if (playing) { if (mounted) _iconController.forward(); } else { - setState(() { - _errorMsg = 'Audio could not be played.'; - }); - // ignore: avoid_print - print('[AudioPlayerBar] ERROR: Audio could not be played.'); + if (mounted) _iconController.reverse(); } - } catch (e, st) { + }); + + _errorSubscription = _audioPlayer.errorStream.listen((error) { setState(() { - _errorMsg = - 'Audio playback error: ' + - (e is Exception ? e.toString() : 'Unknown error'); + _errorMsg = error.toString(); + _isLoading = false; }); // ignore: avoid_print - print('[AudioPlayerBar] Playback error: $e\n$st'); - } - }); - _audioPlayer.positionStream.listen((pos) { - setState(() { - _position = pos; + print('[AudioPlayerBar] Web audio error: $error'); }); - }); - _audioPlayer.playerStateStream.listen((state) { - setState(() { - _isPlaying = state.playing; + + // Auto-play for web + await _audioPlayer.play(); + } else { + // Mobile implementation (just_audio) + _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), () { + if (_audioPlayer.playerState.playing) { + if (mounted) _iconController.forward(); + } else { + setState(() { + _errorMsg = 'Audio could not be played.'; + }); + // ignore: avoid_print + print('[AudioPlayerBar] ERROR: Audio could not be played.'); + } + }); + } catch (e, st) { + setState(() { + _errorMsg = + 'Audio playback error: ' + + (e is Exception ? e.toString() : 'Unknown error'); + }); + // ignore: avoid_print + print('[AudioPlayerBar] Playback error: $e\n$st'); + } }); - }); + _audioPlayer.positionStream.listen((pos) { + setState(() { + _position = pos; + }); + }); + _audioPlayer.playerStateStream.listen((state) { + setState(() { + _isPlaying = state.playing; + }); + if (state.playing) { + if (mounted) _iconController.forward(); + } else { + if (mounted) _iconController.reverse(); + } + }); + } } catch (e, st) { setState(() { _isLoading = false; @@ -122,6 +187,9 @@ class _AudioPlayerBarState extends State return AnimatedContainer( duration: const Duration(milliseconds: 300), height: 48, + width: + MediaQuery.of(context).size.width * + 0.65, // Reduce width by 35% (100% - 35% = 65%) padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: AppTheme.glassDecoration.copyWith( borderRadius: BorderRadius.circular(16), diff --git a/b0esche_cloud/lib/widgets/web_audio_player.dart b/b0esche_cloud/lib/widgets/web_audio_player.dart new file mode 100644 index 0000000..c24d11e --- /dev/null +++ b/b0esche_cloud/lib/widgets/web_audio_player.dart @@ -0,0 +1,92 @@ +import 'package:web/web.dart' as web; +import 'dart:async'; + +class AudioPlayer { + web.HTMLAudioElement? _audioElement; + final StreamController _positionController = + StreamController.broadcast(); + final StreamController _durationController = + StreamController.broadcast(); + final StreamController _playingController = + StreamController.broadcast(); + final StreamController _errorController = + StreamController.broadcast(); + + Stream get positionStream => _positionController.stream; + Stream get durationStream => _durationController.stream; + Stream get playingStream => _playingController.stream; + Stream get errorStream => _errorController.stream; + + Future setUrl(String url) async { + try { + _audioElement = web.HTMLAudioElement(); + _audioElement!.src = url; + _audioElement!.crossOrigin = 'anonymous'; // Handle CORS + + // Set up event listeners + _audioElement!.onLoadedMetadata.listen((_) { + _durationController.add( + Duration(milliseconds: (_audioElement!.duration * 1000).toInt()), + ); + }); + + _audioElement!.onTimeUpdate.listen((_) { + _positionController.add( + Duration(milliseconds: (_audioElement!.currentTime * 1000).toInt()), + ); + }); + + _audioElement!.onPlay.listen((_) { + _playingController.add(true); + }); + + _audioElement!.onPause.listen((_) { + _playingController.add(false); + }); + + _audioElement!.onEnded.listen((_) { + _playingController.add(false); + }); + + _audioElement!.onError.listen((_) { + _errorController.add('Failed to load audio'); + }); + + // Load the audio + _audioElement!.load(); + } catch (e) { + _errorController.add('Error initializing audio: $e'); + } + } + + Future play() async { + try { + if (_audioElement != null) { + // The play() method returns a JSPromise, but we can call it without await + // since we're not depending on the promise resolution for our logic + _audioElement!.play(); + } + } catch (e) { + _errorController.add('Error playing audio: $e'); + } + } + + Future pause() async { + _audioElement?.pause(); + } + + Future seek(Duration position) async { + if (_audioElement != null) { + _audioElement!.currentTime = position.inMilliseconds / 1000; + } + } + + void dispose() { + _audioElement?.pause(); + _audioElement = null; + _positionController.close(); + _durationController.close(); + _playingController.close(); + _errorController.close(); + } +}