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
@@ -33,6 +33,16 @@ class HomeScreen extends ConsumerWidget {
const Icon(Icons.error_outline, size: 48),
const SizedBox(height: 12),
Text('Lỗi tải dữ liệu', style: Theme.of(context).textTheme.bodyLarge),
Padding(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 0),
child: Text(
e.toString(),
textAlign: TextAlign.center,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
TextButton(
onPressed: () => ref.invalidate(homeProvider),
child: const Text('Thử lại'),
@@ -138,8 +148,9 @@ class _CarouselCard extends StatelessWidget {
CachedNetworkImage(
imageUrl: novel.coverUrl!,
fit: BoxFit.cover,
placeholder: (_, __) => Container(color: Colors.grey[200]),
errorWidget: (_, __, ___) => Container(color: Colors.grey[300]),
placeholder: (_, imageUrl) => Container(color: Colors.grey[200]),
errorWidget: (_, imageUrl, error) =>
Container(color: Colors.grey[300]),
)
else
Container(color: Theme.of(context).colorScheme.primaryContainer),
@@ -188,7 +199,7 @@ class _NovelHorizontalList extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
itemCount: novels.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
separatorBuilder: (_, separatorIndex) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final novel = novels[index];
return GestureDetector(
+35 -9
View File
@@ -1,4 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/foundation.dart';
import 'package:dio/dio.dart';
import '../../../core/models/novel_model.dart';
import '../../../core/network/providers.dart';
@@ -17,22 +19,46 @@ class HomeData {
final homeProvider = FutureProvider<HomeData>((ref) async {
final client = ref.read(apiClientProvider);
final results = await Future.wait([
final results = await Future.wait<Response<dynamic>>([
client.dio.get('/api/novels/browse', queryParameters: {'sort': 'popular', 'limit': '10', 'page': '1'}),
client.dio.get('/api/novels/browse', queryParameters: {'sort': 'latest', 'limit': '20', 'page': '1'}),
client.dio.get('/api/novels/browse', queryParameters: {'sort': 'rating', 'limit': '10', 'page': '1'}),
]);
List<NovelModel> parseItems(dynamic res) {
final data = res.data as Map<String, dynamic>;
return (data['items'] as List)
.map((e) => NovelModel.fromJson(e as Map<String, dynamic>))
.toList();
List<NovelModel> parseItems(Response<dynamic> res, String feedName) {
final raw = res.data;
if (raw is! Map<String, dynamic>) {
throw FormatException('Feed $feedName response is not a JSON object: ${raw.runtimeType}');
}
final rawItems = raw['items'];
if (rawItems is! List) {
throw FormatException('Feed $feedName missing items list');
}
final parsed = <NovelModel>[];
for (var i = 0; i < rawItems.length; i++) {
final item = rawItems[i];
if (item is! Map<String, dynamic>) {
debugPrint('[HOME][SKIP] $feedName item#$i has invalid type: ${item.runtimeType}');
continue;
}
try {
parsed.add(NovelModel.fromJson(item));
} catch (e) {
final id = item['id'];
debugPrint('[HOME][SKIP] $feedName item#$i id=$id parse failed: $e');
}
}
debugPrint('[HOME] $feedName parsed ${parsed.length}/${rawItems.length} items');
return parsed;
}
return HomeData(
hot: parseItems(results[0]),
latest: parseItems(results[1]),
topRated: parseItems(results[2]),
hot: parseItems(results[0], 'popular'),
latest: parseItems(results[1], 'latest'),
topRated: parseItems(results[2], 'rating'),
);
});