feat: While audio is playing, allow clicking of word to move audio to that spot

This commit is contained in:
ggurdin 2025-12-18 14:48:51 -05:00
parent b8bb4ea4a0
commit e9e4604aad
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
4 changed files with 38 additions and 18 deletions

View file

@ -15,7 +15,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart';
import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/file_description.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
@ -34,7 +33,6 @@ class AudioPlayerWidget extends StatefulWidget {
final String roomId;
final String senderId;
final PangeaAudioFile? matrixFile;
final MessageOverlayController? overlayController;
final bool autoplay;
// Pangea#
@ -50,7 +48,6 @@ class AudioPlayerWidget extends StatefulWidget {
required this.roomId,
required this.senderId,
this.matrixFile,
this.overlayController,
this.autoplay = false,
// Pangea#
super.key,
@ -72,7 +69,6 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
String? _durationString;
// #Pangea
StreamSubscription? _onAudioPositionChanged;
StreamSubscription? _onAudioStateChanged;
double playbackSpeed = 1.0;
@ -154,7 +150,6 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
audioPlayer.dispose();
matrix.voiceMessageEventId.value = matrix.audioPlayer = null;
// #Pangea
_onAudioPositionChanged?.cancel();
_onAudioStateChanged?.cancel();
// Pangea#
}
@ -262,18 +257,6 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
// #Pangea
audioPlayer.setSpeed(playbackSpeed);
_onAudioPositionChanged?.cancel();
_onAudioPositionChanged =
matrix.audioPlayer!.positionStream.listen((state) {
// Pass current timestamp to overlay, so it can highlight as necessary
if (widget.matrixFile?.tokens != null) {
widget.overlayController?.highlightCurrentText(
state.inMilliseconds,
widget.matrixFile!.tokens!,
);
}
});
_onAudioStateChanged?.cancel();
_onAudioStateChanged =
matrix.audioPlayer!.playerStateStream.listen((state) {

View file

@ -193,9 +193,15 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
return;
}
if (selectedSpan == _selectedSpan) return;
if (selectedSpan == _selectedSpan) {
selectModeController.setPlayingToken(selectedToken?.text);
return;
}
_selectedSpan = selectedSpan;
selectedTokenNotifier.value = selectedToken;
selectModeController.setPlayingToken(selectedToken?.text);
if (mounted) {
setState(() {});
if (selectedToken != null && isNewToken(selectedToken!)) {

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:just_audio/just_audio.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
@ -156,6 +157,8 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
if (messageEvent.isAudioMessage == true) {
controller.fetchTranscription();
}
controller.playTokenNotifier.addListener(_playToken);
}
@override
@ -165,6 +168,7 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
matrix?.voiceMessageEventId.value = null;
_audioSub?.cancel();
_playerStateSub?.cancel();
controller.playTokenNotifier.removeListener(_playToken);
super.dispose();
}
@ -304,6 +308,26 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
}
}
void _playToken() {
final token = controller.playTokenNotifier.value;
if (token == null ||
controller.audioFile?.$1.tokens == null ||
controller.selectedMode.value != SelectMode.audio) {
return;
}
final ttsToken = controller.audioFile!.$1.tokens!.firstWhereOrNull(
(t) => t.text == token,
);
if (ttsToken != null && matrix?.audioPlayer != null) {
final start = Duration(milliseconds: ttsToken.startMS);
matrix!.audioPlayer!.seek(start);
matrix!.audioPlayer!.play();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);

View file

@ -10,6 +10,7 @@ import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
import 'package:fluffychat/pangea/common/utils/async_state.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/speech_to_text/speech_to_text_response_model.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance/select_mode_buttons.dart';
@ -92,10 +93,13 @@ class SelectModeController with LemmaEmojiSetter {
ValueNotifier<(ConstructIdentifier, String)?>(null);
final StreamController contentChangedStream = StreamController.broadcast();
ValueNotifier<PangeaTokenText?> playTokenNotifier =
ValueNotifier<PangeaTokenText?>(null);
void dispose() {
selectedMode.dispose();
constructEmojiNotifier.dispose();
playTokenNotifier.dispose();
_transcriptLoader.dispose();
_translationLoader.dispose();
_sttTranslationLoader.dispose();
@ -197,6 +201,9 @@ class SelectModeController with LemmaEmojiSetter {
) =>
constructEmojiNotifier.value = (constructId, emoji);
void setPlayingToken(PangeaTokenText? token) =>
playTokenNotifier.value = token;
Future<void> fetchAudio() => _audioLoader.load();
Future<void> fetchTranslation() => _translationLoader.load();
Future<void> fetchTranscription() => _transcriptLoader.load();