From 12b320dcf5b3dd8cf1025758aea6b8302e8d274e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 19 Dec 2025 09:40:05 -0500 Subject: [PATCH] feat: Stay in audio mode after end of audio --- .../toolbar/message_selection_overlay.dart | 3 +- .../select_mode_buttons.dart | 80 ++++++++++++------- .../select_mode_controller.dart | 9 ++- 3 files changed, 57 insertions(+), 35 deletions(-) diff --git a/lib/pangea/toolbar/message_selection_overlay.dart b/lib/pangea/toolbar/message_selection_overlay.dart index af3d5494d..376db6695 100644 --- a/lib/pangea/toolbar/message_selection_overlay.dart +++ b/lib/pangea/toolbar/message_selection_overlay.dart @@ -203,7 +203,8 @@ class MessageOverlayController extends State selectedTokenNotifier.value = selectedToken; selectModeController.setPlayingToken(selectedToken?.text); - if (selectedToken != null) { + if (selectedToken != null && + selectModeController.selectedMode.value != SelectMode.audio) { TtsController.tryToSpeak( selectedToken!.text.content, langCode: pangeaMessageEvent.messageDisplayLangCode, diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart index 52cf02b7a..116fd451d 100644 --- a/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart +++ b/lib/pangea/toolbar/reading_assistance/select_mode_buttons.dart @@ -145,6 +145,7 @@ class SelectModeButtonsState extends State { static const double buttonSize = 40.0; StreamSubscription? _playerStateSub; + final ValueNotifier _isPlayingNotifier = ValueNotifier(false); StreamSubscription? _audioSub; MatrixState? matrix; @@ -168,6 +169,7 @@ class SelectModeButtonsState extends State { matrix?.voiceMessageEventId.value = null; _audioSub?.cancel(); _playerStateSub?.cancel(); + _isPlayingNotifier.dispose(); controller.playTokenNotifier.removeListener(_playToken); super.dispose(); } @@ -225,19 +227,22 @@ class SelectModeButtonsState extends State { Future playAudio() async { final playerID = "${messageEvent.eventId}_button"; + final isPlaying = matrix?.audioPlayer != null && + matrix?.voiceMessageEventId.value == playerID && + matrix!.audioPlayer!.playerState.processingState != + ProcessingState.completed; - if (matrix?.audioPlayer != null && - matrix?.voiceMessageEventId.value == playerID) { - // If the audio player is already initialized and playing the same message, pause it - if (matrix!.audioPlayer!.playerState.playing) { - await matrix!.audioPlayer!.pause(); - return; - } - // If the audio player is paused, resume it - await matrix!.audioPlayer!.play(); + if (isPlaying) { + matrix!.audioPlayer!.playerState.playing + ? await matrix!.audioPlayer!.pause() + : await matrix!.audioPlayer!.play(); return; } + _reloadAudio(); + } + + Future _reloadAudio({Duration? seek}) async { matrix?.audioPlayer?.dispose(); matrix?.audioPlayer = AudioPlayer(); matrix?.voiceMessageEventId.value = "${messageEvent.eventId}_button"; @@ -250,21 +255,12 @@ class SelectModeButtonsState extends State { _audioSub = matrix?.audioPlayer?.positionStream.listen(_onPlayAudio); try { - if (matrix?.audioPlayer != null && - matrix!.audioPlayer!.playerState.playing) { - await matrix!.audioPlayer!.pause(); - return; - } else if (matrix?.audioPlayer?.position != Duration.zero) { - TtsController.stop(); - await matrix?.audioPlayer?.play(); - return; - } - if (controller.audioFile == null) { await controller.fetchAudio(); } if (controller.audioFile == null) return; + final (PangeaAudioFile pangeaAudioFile, File? audioFile) = controller.audioFile!; @@ -280,6 +276,11 @@ class SelectModeButtonsState extends State { } TtsController.stop(); + + if (seek != null) { + matrix!.audioPlayer!.seek(seek); + } + await matrix?.audioPlayer?.play(); } catch (e, s) { ErrorHandler.logError( @@ -303,13 +304,20 @@ class SelectModeButtonsState extends State { } void _onUpdatePlayerState(PlayerState state) { - if (state.processingState == ProcessingState.completed) { - updateMode(null); + final current = _isPlayingNotifier.value; + if (!current && + state.processingState == ProcessingState.ready && + state.playing) { + _isPlayingNotifier.value = true; + } else if (current && + (!state.playing || + state.processingState == ProcessingState.completed)) { + _isPlayingNotifier.value = false; } } void _playToken() { - final token = controller.playTokenNotifier.value; + final token = controller.playTokenNotifier.value.$1; if (token == null || controller.audioFile?.$1.tokens == null || @@ -321,10 +329,18 @@ class SelectModeButtonsState extends State { (t) => t.text == token, ); - if (ttsToken != null && matrix?.audioPlayer != null) { - final start = Duration(milliseconds: ttsToken.startMS); + if (ttsToken == null) return; + + final isPlaying = matrix?.audioPlayer != null && + matrix!.audioPlayer!.playerState.processingState != + ProcessingState.completed; + + final start = Duration(milliseconds: ttsToken.startMS); + if (isPlaying) { matrix!.audioPlayer!.seek(start); matrix!.audioPlayer!.play(); + } else { + _reloadAudio(seek: start); } } @@ -381,13 +397,15 @@ class SelectModeButtonsState extends State { : theme.colorScheme.primaryContainer, shape: BoxShape.circle, ), - child: _SelectModeButtonIcon( - mode: mode, - loading: - controller.isLoading && mode == selectedMode, - playing: mode == SelectMode.audio && - matrix?.audioPlayer?.playerState.playing == - true, + child: ValueListenableBuilder( + valueListenable: _isPlayingNotifier, + builder: (context, playing, __) => + _SelectModeButtonIcon( + mode: mode, + loading: controller.isLoading && + mode == selectedMode, + playing: mode == SelectMode.audio && playing, + ), ), ), ), diff --git a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart index 0d3a40a0e..7f84d8769 100644 --- a/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart +++ b/lib/pangea/toolbar/reading_assistance/select_mode_controller.dart @@ -93,8 +93,11 @@ class SelectModeController with LemmaEmojiSetter { ValueNotifier<(ConstructIdentifier, String)?>(null); final StreamController contentChangedStream = StreamController.broadcast(); - ValueNotifier playTokenNotifier = - ValueNotifier(null); + + // Sometimes the same token is clicked twice. Setting it to the same value + // won't trigger the notifier, so use the bool for force it to trigger. + ValueNotifier<(PangeaTokenText?, bool)> playTokenNotifier = + ValueNotifier<(PangeaTokenText?, bool)>((null, false)); void dispose() { selectedMode.dispose(); @@ -202,7 +205,7 @@ class SelectModeController with LemmaEmojiSetter { constructEmojiNotifier.value = (constructId, emoji); void setPlayingToken(PangeaTokenText? token) => - playTokenNotifier.value = token; + playTokenNotifier.value = (token, !playTokenNotifier.value.$2); Future fetchAudio() => _audioLoader.load(); Future fetchTranslation() => _translationLoader.load();