Refactor chapter list provider and improve TTS functionality
Build Android APK / build-apk (push) Successful in 12m10s
Build Android AAB / build-aab (push) Successful in 19m35s

- Removed the constant chapterPageSize and refactored ChapterListQuery to use a simpler approach for fetching chapters.
- Updated the chapter list provider to handle fetching all chapters in a single request with pagination.
- Enhanced error handling for fetching chapters by resolving canonical IDs when necessary.
- Modified TTS functionality to ensure proper handling of Android fallback reading and improved error management.
- Added a new setting to enable/disable TTS on sentence tap.
- Updated UI components in the reader screen for better user experience and added navigation buttons for chapters.
- Bumped version to 1.0.3+4 in pubspec.yaml.
This commit is contained in:
2026-04-24 03:03:32 +07:00
parent 2b8fa4ee57
commit 66613857e8
11 changed files with 1112 additions and 447 deletions
+90 -22
View File
@@ -155,6 +155,7 @@ class TtsNotifier extends StateNotifier<TtsState> {
int _pendingFallbackIndex = -1;
bool _didStartCurrentFallbackUtterance = false;
bool _hasPromptedNotificationSettings = false;
bool _androidFallbackReady = false;
bool get _useNativeAndroidMediaService => Platform.isAndroid;
@@ -315,6 +316,73 @@ class TtsNotifier extends StateNotifier<TtsState> {
);
}
Future<void> _ensureAndroidFallbackReady() async {
if (_androidFallbackReady) return;
await _tts.awaitSpeakCompletion(true);
await _tts.setSharedInstance(true);
await _configureVietnameseVoiceWithFlutterTts();
await _tts.setSpeechRate(state.speed);
await _tts.setVolume(1.0);
await _tts.setPitch(1.0);
_tts.setStartHandler(() {
_didStartCurrentFallbackUtterance = true;
final index = _pendingFallbackIndex;
if (index >= 0 && index < _segments.length) {
final segment = _segments[index];
state = state.copyWith(
status: TtsStatus.playing,
paragraphIndex: index,
activeParagraphIndex: segment.paragraphIndex,
progressStart: segment.start,
progressEnd: segment.end,
);
} else {
state = state.copyWith(status: TtsStatus.playing);
}
});
_tts.setCompletionHandler(() {
// Fallback playback progression is driven by _playFallbackFromGeneration.
});
_tts.setErrorHandler((_) {
if (_isInterruptingPlayback) return;
_pendingFallbackIndex = -1;
_didStartCurrentFallbackUtterance = false;
state = state.copyWith(
status: TtsStatus.idle,
activeParagraphIndex: -1,
progressStart: -1,
progressEnd: -1,
);
});
_androidFallbackReady = true;
}
Future<void> _startFallbackReading({
required int validIndex,
required _TtsSegment selectedSegment,
required String? contentKey,
}) async {
await _ensureAndroidFallbackReady();
final sessionId = await _interruptFallbackPlayback();
state = state.copyWith(
status: TtsStatus.playing,
paragraphIndex: validIndex,
totalParagraphs: _segments.length,
activeParagraphIndex: selectedSegment.paragraphIndex,
progressStart: selectedSegment.start,
progressEnd: selectedSegment.end,
contentKey: contentKey,
);
unawaited(_playFallbackFromGeneration(validIndex, sessionId));
}
void _handleAndroidMediaEvent(dynamic event) {
_applyAndroidSnapshot(event);
}
@@ -372,6 +440,7 @@ class TtsNotifier extends StateNotifier<TtsState> {
// Keep natural sentence flow while removing symbols that are usually read out noisily.
final cleaned = raw
.replaceAll(RegExp(r'["“”]'), ' ')
.replaceAll(RegExp(r'[_$#^*+=~`|<>\\\[\]{}]'), ' ')
.replaceAll(RegExp(r'\s+'), ' ')
.trim();
@@ -614,33 +683,32 @@ class TtsNotifier extends StateNotifier<TtsState> {
contentKey: contentKey,
);
await _mediaChannel.invokeMethod<void>('startReading', {
'contentKey': contentKey,
'title': title,
'startIndex': validIndex,
'speed': state.speed,
'language': state.language,
'voiceName': state.voiceName,
'backgroundModeEnabled': state.backgroundModeEnabled,
'segments': _segments.map((segment) => segment.toMap()).toList(),
});
try {
await _mediaChannel.invokeMethod<void>('startReading', {
'contentKey': contentKey,
'title': title,
'startIndex': validIndex,
'speed': state.speed,
'language': state.language,
'voiceName': state.voiceName,
'backgroundModeEnabled': state.backgroundModeEnabled,
'segments': _segments.map((segment) => segment.toMap()).toList(),
});
} on PlatformException {
await _startFallbackReading(
validIndex: validIndex,
selectedSegment: selectedSegment,
contentKey: contentKey,
);
}
return;
}
final sessionId = await _interruptFallbackPlayback();
state = state.copyWith(
status: TtsStatus.playing,
paragraphIndex: validIndex,
totalParagraphs: _segments.length,
activeParagraphIndex: -1,
progressStart: -1,
progressEnd: -1,
await _startFallbackReading(
validIndex: validIndex,
selectedSegment: selectedSegment,
contentKey: contentKey,
);
await _syncBackgroundMode();
unawaited(_playFallbackFromGeneration(validIndex, sessionId));
}
Future<int> _interruptFallbackPlayback() async {