feat: Stay in audio mode after end of audio

This commit is contained in:
ggurdin 2025-12-19 09:40:05 -05:00
parent 6671abebd8
commit 12b320dcf5
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
3 changed files with 57 additions and 35 deletions

View file

@ -203,7 +203,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
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,

View file

@ -145,6 +145,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
static const double buttonSize = 40.0;
StreamSubscription? _playerStateSub;
final ValueNotifier<bool> _isPlayingNotifier = ValueNotifier(false);
StreamSubscription? _audioSub;
MatrixState? matrix;
@ -168,6 +169,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
matrix?.voiceMessageEventId.value = null;
_audioSub?.cancel();
_playerStateSub?.cancel();
_isPlayingNotifier.dispose();
controller.playTokenNotifier.removeListener(_playToken);
super.dispose();
}
@ -225,19 +227,22 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
Future<void> 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<void> _reloadAudio({Duration? seek}) async {
matrix?.audioPlayer?.dispose();
matrix?.audioPlayer = AudioPlayer();
matrix?.voiceMessageEventId.value = "${messageEvent.eventId}_button";
@ -250,21 +255,12 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
_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<SelectModeButtons> {
}
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<SelectModeButtons> {
}
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<SelectModeButtons> {
(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<SelectModeButtons> {
: 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,
),
),
),
),

View file

@ -93,8 +93,11 @@ class SelectModeController with LemmaEmojiSetter {
ValueNotifier<(ConstructIdentifier, String)?>(null);
final StreamController contentChangedStream = StreamController.broadcast();
ValueNotifier<PangeaTokenText?> playTokenNotifier =
ValueNotifier<PangeaTokenText?>(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<void> fetchAudio() => _audioLoader.load();
Future<void> fetchTranslation() => _translationLoader.load();