4 Commits

Author SHA1 Message Date
virtus d505806f6e chore: Bump version to 1.0.3+6 in pubspec.yaml
Build Android APK / build-apk (push) Successful in 17m27s
Build Android AAB / build-aab (push) Successful in 18m23s
2026-04-27 21:36:59 +07:00
virtus 41309ff6ee feat: Enhance route persistence and restoration logic for improved navigation 2026-04-27 21:36:14 +07:00
virtus fd370f7833 chore: Bump version to 1.0.3+5 in pubspec.yaml
Build Android APK / build-apk (push) Successful in 12m9s
Build Android AAB / build-aab (push) Successful in 14m14s
2026-04-27 01:18:14 +07:00
virtus d4c6cdb013 feat: Add native TTS snapshot reconciliation and lifecycle management
Build Android AAB / build-aab (push) Successful in 12m11s
Build Android APK / build-apk (push) Successful in 14m12s
2026-04-27 00:58:51 +07:00
5 changed files with 107 additions and 20 deletions
+15 -3
View File
@@ -24,15 +24,27 @@ class _ReaderAppState extends ConsumerState<ReaderApp> {
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
ProviderSubscription<int>? _sessionExpirySub;
late final GoRouter _router;
String? _previousPath;
void _persistRouteForRestore() {
if (!mounted) return;
unawaited(() async {
final uri = _router.state.uri;
final fullPath = uri.hasQuery ? '${uri.path}?${uri.query}' : uri.path;
if (fullPath == RouteNames.splash) return;
await ref.read(localStoreProvider).saveLastRoutePath(fullPath);
}());
// When navigating into reader from a novel page, save "novelPath|readerPath"
// so the splash screen can reconstruct the full back stack on restore.
final String pathToSave;
if (fullPath.startsWith('/reader/') &&
_previousPath != null &&
_previousPath!.startsWith('/novel/')) {
pathToSave = '$_previousPath|$fullPath';
} else {
pathToSave = fullPath;
}
_previousPath = fullPath;
unawaited(ref.read(localStoreProvider).saveLastRoutePath(pathToSave));
}
@override
@@ -26,7 +26,8 @@ class ReaderScreen extends ConsumerStatefulWidget {
ConsumerState<ReaderScreen> createState() => _ReaderScreenState();
}
class _ReaderScreenState extends ConsumerState<ReaderScreen> {
class _ReaderScreenState extends ConsumerState<ReaderScreen>
with WidgetsBindingObserver {
static const List<Color> _backgroundColorChoices = [
Color(0xFFFFFEF8),
Color(0xFFF6EAD7),
@@ -260,11 +261,39 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
return partiallyVisibleIndex ?? 0;
}
Future<void> _reconcileChapterWithNativeTts() async {
if (defaultTargetPlatform != TargetPlatform.android) return;
final notifier = ref.read(ttsProvider.notifier);
await notifier.refreshNativeSnapshot();
if (!mounted) return;
final tts = ref.read(ttsProvider);
final targetChapterId = tts.contentKey;
if (targetChapterId == null || targetChapterId.isEmpty) return;
if (targetChapterId == widget.chapterId) return;
if (tts.status != TtsStatus.playing && tts.status != TtsStatus.paused) return;
context.pushReplacement(RouteNames.readerChapter(targetChapterId));
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
_scrollCtrl.addListener(_onScroll);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
unawaited(_reconcileChapterWithNativeTts());
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
unawaited(_reconcileChapterWithNativeTts());
}
}
/// Handle TTS state transitions that require navigation or restarts.
@@ -336,6 +365,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_uiAutoHideTimer?.cancel();
_scrollCtrl.removeListener(_onScroll);
_scrollCtrl.dispose();
+16
View File
@@ -261,6 +261,22 @@ class TtsNotifier extends StateNotifier<TtsState> {
await _mediaChannel.invokeMethod<void>('openNotificationSettings');
}
Future<void> refreshNativeSnapshot() async {
if (!_useNativeAndroidMediaService) return;
if (!_initialized) {
await (_initFuture ?? _init());
return;
}
try {
final snapshot = await _mediaChannel.invokeMethod<dynamic>('getSnapshot');
_applyAndroidSnapshot(snapshot);
} catch (_) {
// Ignore snapshot pull errors; event stream updates will continue.
}
}
Future<void> _configureVietnameseVoiceWithFlutterTts() async {
final dynamic voicesRaw = await _tts.getVoices;
@@ -19,17 +19,19 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
bool _isRestorableRoute(String path) {
if (path.isEmpty || path == RouteNames.splash) return false;
return path == RouteNames.home ||
path == RouteNames.login ||
path == RouteNames.search ||
path.startsWith('${RouteNames.search}?') ||
path == RouteNames.genres ||
path == RouteNames.bookshelf ||
path == RouteNames.profile ||
path == RouteNames.settings ||
path.startsWith('/novel/') ||
path.startsWith('/reader/') ||
path.startsWith('/comments/');
// Composite "parentPath|deepPath" — validate the deep path portion
final checkPath = path.contains('|') ? path.substring(path.indexOf('|') + 1) : path;
return checkPath == RouteNames.home ||
checkPath == RouteNames.login ||
checkPath == RouteNames.search ||
checkPath.startsWith('${RouteNames.search}?') ||
checkPath == RouteNames.genres ||
checkPath == RouteNames.bookshelf ||
checkPath == RouteNames.profile ||
checkPath == RouteNames.settings ||
checkPath.startsWith('/novel/') ||
checkPath.startsWith('/reader/') ||
checkPath.startsWith('/comments/');
}
@override
@@ -40,7 +42,34 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
final lastPath = await ref.read(localStoreProvider).loadLastRoutePath();
if (!mounted) return;
if (lastPath != null && _isRestorableRoute(lastPath)) {
if (lastPath.contains('|')) {
// Composite "parentPath|deepPath" e.g. "/novel/123|/reader/abc"
// Restore full stack: Home → Novel Detail → Reader
final sep = lastPath.indexOf('|');
final parentPath = lastPath.substring(0, sep);
final deepPath = lastPath.substring(sep + 1);
context.go(RouteNames.home);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.push(parentPath);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) context.push(deepPath);
});
});
} else {
// Single deep route (novel, comments) outside ShellRoute: push on Home
final isDeepRoute = lastPath.startsWith('/reader/') ||
lastPath.startsWith('/novel/') ||
lastPath.startsWith('/comments/');
if (isDeepRoute) {
context.go(RouteNames.home);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) context.push(lastPath);
});
} else {
context.go(lastPath);
}
}
return;
}
context.go(RouteNames.home);
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.3+4
version: 1.0.3+6
environment:
sdk: ^3.11.3