Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d505806f6e | |||
| 41309ff6ee | |||
| fd370f7833 | |||
| d4c6cdb013 |
+18
-6
@@ -24,15 +24,27 @@ class _ReaderAppState extends ConsumerState<ReaderApp> {
|
|||||||
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||||
ProviderSubscription<int>? _sessionExpirySub;
|
ProviderSubscription<int>? _sessionExpirySub;
|
||||||
late final GoRouter _router;
|
late final GoRouter _router;
|
||||||
|
String? _previousPath;
|
||||||
|
|
||||||
void _persistRouteForRestore() {
|
void _persistRouteForRestore() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
unawaited(() async {
|
final uri = _router.state.uri;
|
||||||
final uri = _router.state.uri;
|
final fullPath = uri.hasQuery ? '${uri.path}?${uri.query}' : uri.path;
|
||||||
final fullPath = uri.hasQuery ? '${uri.path}?${uri.query}' : uri.path;
|
if (fullPath == RouteNames.splash) return;
|
||||||
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
|
@override
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class ReaderScreen extends ConsumerStatefulWidget {
|
|||||||
ConsumerState<ReaderScreen> createState() => _ReaderScreenState();
|
ConsumerState<ReaderScreen> createState() => _ReaderScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
class _ReaderScreenState extends ConsumerState<ReaderScreen>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
static const List<Color> _backgroundColorChoices = [
|
static const List<Color> _backgroundColorChoices = [
|
||||||
Color(0xFFFFFEF8),
|
Color(0xFFFFFEF8),
|
||||||
Color(0xFFF6EAD7),
|
Color(0xFFF6EAD7),
|
||||||
@@ -260,11 +261,39 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||||||
return partiallyVisibleIndex ?? 0;
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
_scrollCtrl.addListener(_onScroll);
|
_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.
|
/// Handle TTS state transitions that require navigation or restarts.
|
||||||
@@ -336,6 +365,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_uiAutoHideTimer?.cancel();
|
_uiAutoHideTimer?.cancel();
|
||||||
_scrollCtrl.removeListener(_onScroll);
|
_scrollCtrl.removeListener(_onScroll);
|
||||||
_scrollCtrl.dispose();
|
_scrollCtrl.dispose();
|
||||||
|
|||||||
@@ -261,6 +261,22 @@ class TtsNotifier extends StateNotifier<TtsState> {
|
|||||||
await _mediaChannel.invokeMethod<void>('openNotificationSettings');
|
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 {
|
Future<void> _configureVietnameseVoiceWithFlutterTts() async {
|
||||||
final dynamic voicesRaw = await _tts.getVoices;
|
final dynamic voicesRaw = await _tts.getVoices;
|
||||||
|
|
||||||
|
|||||||
@@ -19,17 +19,19 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
|
|||||||
|
|
||||||
bool _isRestorableRoute(String path) {
|
bool _isRestorableRoute(String path) {
|
||||||
if (path.isEmpty || path == RouteNames.splash) return false;
|
if (path.isEmpty || path == RouteNames.splash) return false;
|
||||||
return path == RouteNames.home ||
|
// Composite "parentPath|deepPath" — validate the deep path portion
|
||||||
path == RouteNames.login ||
|
final checkPath = path.contains('|') ? path.substring(path.indexOf('|') + 1) : path;
|
||||||
path == RouteNames.search ||
|
return checkPath == RouteNames.home ||
|
||||||
path.startsWith('${RouteNames.search}?') ||
|
checkPath == RouteNames.login ||
|
||||||
path == RouteNames.genres ||
|
checkPath == RouteNames.search ||
|
||||||
path == RouteNames.bookshelf ||
|
checkPath.startsWith('${RouteNames.search}?') ||
|
||||||
path == RouteNames.profile ||
|
checkPath == RouteNames.genres ||
|
||||||
path == RouteNames.settings ||
|
checkPath == RouteNames.bookshelf ||
|
||||||
path.startsWith('/novel/') ||
|
checkPath == RouteNames.profile ||
|
||||||
path.startsWith('/reader/') ||
|
checkPath == RouteNames.settings ||
|
||||||
path.startsWith('/comments/');
|
checkPath.startsWith('/novel/') ||
|
||||||
|
checkPath.startsWith('/reader/') ||
|
||||||
|
checkPath.startsWith('/comments/');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -40,7 +42,34 @@ class _SplashScreenState extends ConsumerState<SplashScreen> {
|
|||||||
final lastPath = await ref.read(localStoreProvider).loadLastRoutePath();
|
final lastPath = await ref.read(localStoreProvider).loadLastRoutePath();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (lastPath != null && _isRestorableRoute(lastPath)) {
|
if (lastPath != null && _isRestorableRoute(lastPath)) {
|
||||||
context.go(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;
|
return;
|
||||||
}
|
}
|
||||||
context.go(RouteNames.home);
|
context.go(RouteNames.home);
|
||||||
|
|||||||
+1
-1
@@ -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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.11.3
|
sdk: ^3.11.3
|
||||||
|
|||||||
Reference in New Issue
Block a user