feat: Enhance TTS player functionality and UI

- Added resume functionality to TTS player when paused.
- Display voice name or language in TTS player UI.
- Improved error handling in reader provider with debug messages.
- Updated TTS service to configure Vietnamese voice and handle platform-specific audio settings.
- Removed wakelock dependency and related code.
- Fixed search screen error handling.
- Updated settings screen to navigate to home after sign out.
- Improved splash screen with timer management.
- Enhanced main app error handling with logging.
- Removed unused package_info_plus and wakelock_plus dependencies.
- Added environment variable support for mobile runtime.
- Integrated Google Sign-In configuration for Android.
- Created logging observer for Riverpod providers.
- Added scripts for environment setup and Google Sign-In validation.
This commit is contained in:
2026-03-30 11:38:04 +07:00
parent 8da9c4152c
commit 1afff18f4d
40 changed files with 1735 additions and 312 deletions
+78 -11
View File
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
enum TtsStatus { idle, playing, paused }
@@ -9,12 +10,16 @@ class TtsState {
final int paragraphIndex;
final int totalParagraphs;
final double speed;
final String language;
final String? voiceName;
const TtsState({
this.status = TtsStatus.idle,
this.paragraphIndex = 0,
this.totalParagraphs = 0,
this.speed = 1.0,
this.language = 'vi-VN',
this.voiceName,
});
TtsState copyWith({
@@ -22,12 +27,17 @@ class TtsState {
int? paragraphIndex,
int? totalParagraphs,
double? speed,
String? language,
String? voiceName,
bool clearVoiceName = false,
}) =>
TtsState(
status: status ?? this.status,
paragraphIndex: paragraphIndex ?? this.paragraphIndex,
totalParagraphs: totalParagraphs ?? this.totalParagraphs,
speed: speed ?? this.speed,
language: language ?? this.language,
voiceName: clearVoiceName ? null : (voiceName ?? this.voiceName),
);
bool get isPlaying => status == TtsStatus.playing;
@@ -36,17 +46,42 @@ class TtsState {
class TtsNotifier extends StateNotifier<TtsState> {
final FlutterTts _tts = FlutterTts();
List<String> _paragraphs = [];
bool _initialized = false;
Future<void>? _initFuture;
TtsNotifier() : super(const TtsState()) {
_init();
_initFuture = _init();
}
Future<void> _init() async {
await _tts.setLanguage('vi-VN');
await _tts.awaitSpeakCompletion(true);
await _tts.setSharedInstance(true);
if (Platform.isIOS) {
await _tts.setIosAudioCategory(
IosTextToSpeechAudioCategory.playback,
[
IosTextToSpeechAudioCategoryOptions.allowBluetooth,
IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP,
IosTextToSpeechAudioCategoryOptions.mixWithOthers,
],
IosTextToSpeechAudioMode.defaultMode,
);
}
if (Platform.isAndroid) {
await _tts.setAudioAttributesForNavigation();
}
await _configureVietnameseVoice();
await _tts.setSpeechRate(1.0);
await _tts.setVolume(1.0);
await _tts.setPitch(1.0);
_tts.setStartHandler(() {
state = state.copyWith(status: TtsStatus.playing);
});
_tts.setCompletionHandler(() {
if (state.status == TtsStatus.playing) {
_next();
@@ -55,12 +90,49 @@ class TtsNotifier extends StateNotifier<TtsState> {
_tts.setErrorHandler((msg) {
state = state.copyWith(status: TtsStatus.idle);
WakelockPlus.disable();
});
_initialized = true;
}
Future<void> _configureVietnameseVoice() async {
final dynamic voicesRaw = await _tts.getVoices;
String? selectedName;
String selectedLanguage = 'vi-VN';
if (voicesRaw is List) {
final vietnamese = voicesRaw.whereType<Map>().where((voice) {
final locale = (voice['locale'] ?? voice['language'] ?? '').toString().toLowerCase();
return locale.startsWith('vi');
}).toList();
if (vietnamese.isNotEmpty) {
final preferred = vietnamese.firstWhere(
(voice) =>
(voice['name']?.toString().toLowerCase().contains('female') ?? false) ||
(voice['name']?.toString().toLowerCase().contains('natural') ?? false),
orElse: () => vietnamese.first,
);
selectedName = preferred['name']?.toString();
selectedLanguage =
(preferred['locale'] ?? preferred['language'] ?? 'vi-VN').toString();
}
}
await _tts.setLanguage(selectedLanguage);
if (selectedName != null) {
await _tts.setVoice({'name': selectedName, 'locale': selectedLanguage});
}
state = state.copyWith(language: selectedLanguage, voiceName: selectedName);
}
/// Start reading from [content] starting at optional [paragraphIndex].
Future<void> startReading(String content, {int paragraphIndex = 0}) async {
if (!_initialized) {
await (_initFuture ?? _init());
}
_paragraphs = content
.split(RegExp(r'\n+'))
.map((p) => p.trim())
@@ -75,14 +147,12 @@ class TtsNotifier extends StateNotifier<TtsState> {
paragraphIndex: validIndex,
totalParagraphs: _paragraphs.length,
);
await WakelockPlus.enable();
await _speak(validIndex);
}
Future<void> _speak(int index) async {
if (index >= _paragraphs.length) {
state = state.copyWith(status: TtsStatus.idle);
await WakelockPlus.disable();
return;
}
await _tts.setSpeechRate(state.speed);
@@ -93,7 +163,6 @@ class TtsNotifier extends StateNotifier<TtsState> {
final next = state.paragraphIndex + 1;
if (next >= state.totalParagraphs) {
state = state.copyWith(status: TtsStatus.idle, paragraphIndex: 0);
await WakelockPlus.disable();
return;
}
state = state.copyWith(paragraphIndex: next);
@@ -103,20 +172,18 @@ class TtsNotifier extends StateNotifier<TtsState> {
Future<void> pause() async {
await _tts.pause();
state = state.copyWith(status: TtsStatus.paused);
await WakelockPlus.disable();
}
Future<void> resume() async {
if (state.status != TtsStatus.paused) return;
state = state.copyWith(status: TtsStatus.playing);
await WakelockPlus.enable();
// Use paragraph-level resume for consistent behavior across engines.
await _speak(state.paragraphIndex);
}
Future<void> stop() async {
await _tts.stop();
state = state.copyWith(status: TtsStatus.idle, paragraphIndex: 0);
await WakelockPlus.disable();
}
Future<void> skipForward() async {
@@ -126,6 +193,7 @@ class TtsNotifier extends StateNotifier<TtsState> {
Future<void> skipBack() async {
await _tts.stop();
if (state.totalParagraphs <= 0) return;
final prev = (state.paragraphIndex - 1).clamp(0, state.totalParagraphs - 1);
state = state.copyWith(paragraphIndex: prev);
if (state.status == TtsStatus.playing) await _speak(prev);
@@ -139,7 +207,6 @@ class TtsNotifier extends StateNotifier<TtsState> {
@override
void dispose() {
_tts.stop();
WakelockPlus.disable();
super.dispose();
}
}