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
+20 -4
View File
@@ -1,13 +1,29 @@
import 'package:flutter/foundation.dart';
class AppConfig {
AppConfig._();
static const String baseUrl = String.fromEnvironment(
'BASE_URL',
defaultValue: 'https://localhost:3000',
);
static const String _baseUrlFromEnv = String.fromEnvironment('BASE_URL');
static String get baseUrl {
if (_baseUrlFromEnv.isNotEmpty) {
return _baseUrlFromEnv;
}
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
return 'http://10.0.2.2:8000';
}
return 'http://localhost:8000';
}
static const String googleClientId = String.fromEnvironment(
'GOOGLE_CLIENT_ID',
defaultValue: '',
);
static const String googleServerClientId = String.fromEnvironment(
'GOOGLE_SERVER_CLIENT_ID',
defaultValue: '',
);
}
@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AppProviderObserver extends ProviderObserver {
const AppProviderObserver();
@override
void providerDidFail(
ProviderBase<Object?> provider,
Object error,
StackTrace stackTrace,
ProviderContainer container,
) {
debugPrint('[APP][PROVIDER_ERROR] ${provider.name ?? provider.runtimeType}: $error');
debugPrintStack(stackTrace: stackTrace);
}
}
+17 -6
View File
@@ -41,13 +41,24 @@ class NovelModel extends Equatable {
final SeriesModel? series;
final LatestChapterInfo? latestChapter;
static String _stringValue(dynamic value, {String fallback = ''}) {
if (value is String) return value;
if (value == null) return fallback;
return value.toString();
}
static int _intValue(dynamic value, {int fallback = 0}) {
if (value is num) return value.toInt();
return fallback;
}
factory NovelModel.fromJson(Map<String, dynamic> json) => NovelModel(
id: json['id'] as String,
title: json['title'] as String,
slug: json['slug'] as String,
authorName: json['authorName'] as String,
status: json['status'] as String,
totalChapters: (json['totalChapters'] as num).toInt(),
id: _stringValue(json['id']),
title: _stringValue(json['title'], fallback: 'Không rõ tiêu đề'),
slug: _stringValue(json['slug']),
authorName: _stringValue(json['authorName'], fallback: 'Chưa rõ tác giả'),
status: _stringValue(json['status'], fallback: 'Đang ra'),
totalChapters: _intValue(json['totalChapters']),
originalTitle: json['originalTitle'] as String?,
description: json['description'] as String?,
coverUrl: json['coverUrl'] as String?,
+24
View File
@@ -4,24 +4,40 @@ class ReadingSettings {
this.lineHeight = 1.8,
this.letterSpacing = 0,
this.fontFamily = 'serif',
this.themePreset = 'paper',
this.horizontalPadding = 20,
this.paragraphSpacing = 24,
this.textAlign = 'justify',
});
final double fontSize;
final double lineHeight;
final double letterSpacing;
final String fontFamily;
final String themePreset;
final double horizontalPadding;
final double paragraphSpacing;
final String textAlign;
ReadingSettings copyWith({
double? fontSize,
double? lineHeight,
double? letterSpacing,
String? fontFamily,
String? themePreset,
double? horizontalPadding,
double? paragraphSpacing,
String? textAlign,
}) =>
ReadingSettings(
fontSize: fontSize ?? this.fontSize,
lineHeight: lineHeight ?? this.lineHeight,
letterSpacing: letterSpacing ?? this.letterSpacing,
fontFamily: fontFamily ?? this.fontFamily,
themePreset: themePreset ?? this.themePreset,
horizontalPadding: horizontalPadding ?? this.horizontalPadding,
paragraphSpacing: paragraphSpacing ?? this.paragraphSpacing,
textAlign: textAlign ?? this.textAlign,
);
factory ReadingSettings.fromJson(Map<String, dynamic> json) => ReadingSettings(
@@ -29,6 +45,10 @@ class ReadingSettings {
lineHeight: (json['lineHeight'] as num?)?.toDouble() ?? 1.8,
letterSpacing: (json['letterSpacing'] as num?)?.toDouble() ?? 0,
fontFamily: json['fontFamily'] as String? ?? 'serif',
themePreset: json['themePreset'] as String? ?? 'paper',
horizontalPadding: (json['horizontalPadding'] as num?)?.toDouble() ?? 20,
paragraphSpacing: (json['paragraphSpacing'] as num?)?.toDouble() ?? 24,
textAlign: json['textAlign'] as String? ?? 'justify',
);
Map<String, dynamic> toJson() => {
@@ -36,5 +56,9 @@ class ReadingSettings {
'lineHeight': lineHeight,
'letterSpacing': letterSpacing,
'fontFamily': fontFamily,
'themePreset': themePreset,
'horizontalPadding': horizontalPadding,
'paragraphSpacing': paragraphSpacing,
'textAlign': textAlign,
};
}
+17
View File
@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../storage/secure_store.dart';
@@ -18,12 +19,28 @@ class ApiClient {
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
debugPrint('[API] ${options.method} ${options.baseUrl}${options.path}');
final token = await _secureStore.getAccessToken();
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onResponse: (response, handler) {
debugPrint(
'[API][OK] ${response.requestOptions.method} '
'${response.requestOptions.baseUrl}${response.requestOptions.path} '
'-> ${response.statusCode}',
);
handler.next(response);
},
onError: (error, handler) {
debugPrint(
'[API][ERROR] ${error.requestOptions.method} ${error.requestOptions.baseUrl}${error.requestOptions.path} '
'-> ${error.type}: ${error.message}',
);
handler.next(error);
},
),
);
}
+12
View File
@@ -7,6 +7,10 @@ class LocalStore {
static const _kLineHeight = 'reader_line_height';
static const _kLetterSpacing = 'reader_letter_spacing';
static const _kFontFamily = 'reader_font_family';
static const _kThemePreset = 'reader_theme_preset';
static const _kHorizontalPadding = 'reader_horizontal_padding';
static const _kParagraphSpacing = 'reader_paragraph_spacing';
static const _kTextAlign = 'reader_text_align';
static const _kProgressChapterId = 'progress_chapter_id_';
static const _kProgressChapterNum = 'progress_chapter_num_';
static const _kProgressOffset = 'progress_offset_';
@@ -19,6 +23,10 @@ class LocalStore {
await prefs.setDouble(_kLineHeight, settings.lineHeight);
await prefs.setDouble(_kLetterSpacing, settings.letterSpacing);
await prefs.setString(_kFontFamily, settings.fontFamily);
await prefs.setString(_kThemePreset, settings.themePreset);
await prefs.setDouble(_kHorizontalPadding, settings.horizontalPadding);
await prefs.setDouble(_kParagraphSpacing, settings.paragraphSpacing);
await prefs.setString(_kTextAlign, settings.textAlign);
}
Future<ReadingSettings?> loadReadingSettings() async {
@@ -29,6 +37,10 @@ class LocalStore {
lineHeight: prefs.getDouble(_kLineHeight) ?? 1.8,
letterSpacing: prefs.getDouble(_kLetterSpacing) ?? 0,
fontFamily: prefs.getString(_kFontFamily) ?? 'serif',
themePreset: prefs.getString(_kThemePreset) ?? 'paper',
horizontalPadding: prefs.getDouble(_kHorizontalPadding) ?? 20,
paragraphSpacing: prefs.getDouble(_kParagraphSpacing) ?? 24,
textAlign: prefs.getString(_kTextAlign) ?? 'justify',
);
}
+14
View File
@@ -16,4 +16,18 @@ class AppTheme {
foregroundColor: Color(0xFF121826),
),
);
static final ThemeData darkTheme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF155DFC),
brightness: Brightness.dark,
),
scaffoldBackgroundColor: const Color(0xFF0E1420),
appBarTheme: const AppBarTheme(
centerTitle: false,
backgroundColor: Color(0xFF0E1420),
foregroundColor: Color(0xFFE5EAF3),
),
);
}