Polish navigation, Quran flows, and sharing UX
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import '../data/local/hive_boxes.dart';
|
||||
@@ -19,6 +21,7 @@ import '../features/quran/presentation/quran_reading_screen.dart';
|
||||
import '../features/quran/presentation/quran_murattal_screen.dart';
|
||||
import '../features/quran/presentation/quran_bookmarks_screen.dart';
|
||||
import '../features/quran/presentation/quran_enrichment_screen.dart';
|
||||
import '../features/notifications/presentation/notification_center_screen.dart';
|
||||
import '../features/settings/presentation/settings_screen.dart';
|
||||
|
||||
/// Navigation key for the shell navigator (bottom-nav screens).
|
||||
@@ -33,12 +36,16 @@ final GoRouter appRouter = GoRouter(
|
||||
// ── Shell route (bottom nav persists) ──
|
||||
ShellRoute(
|
||||
navigatorKey: _shellNavigatorKey,
|
||||
builder: (context, state, child) => _ScaffoldWithNav(child: child),
|
||||
pageBuilder: (context, state, child) => NoTransitionPage(
|
||||
key: ValueKey<String>('shell-${state.pageKey.value}'),
|
||||
child: _ScaffoldWithNav(child: child),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: DashboardScreen(),
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
key: state.pageKey,
|
||||
child: const DashboardScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
@@ -50,26 +57,30 @@ final GoRouter appRouter = GoRouter(
|
||||
),
|
||||
GoRoute(
|
||||
path: '/imsakiyah',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: ImsakiyahScreen(),
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
key: state.pageKey,
|
||||
child: const ImsakiyahScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/checklist',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: ChecklistScreen(),
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
key: state.pageKey,
|
||||
child: const ChecklistScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/laporan',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: LaporanScreen(),
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
key: state.pageKey,
|
||||
child: const LaporanScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/tools',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: ToolsScreen(),
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
key: state.pageKey,
|
||||
child: const ToolsScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
@@ -97,8 +108,10 @@ final GoRouter appRouter = GoRouter(
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) {
|
||||
final surahId = state.pathParameters['surahId']!;
|
||||
final startVerse = int.tryParse(state.uri.queryParameters['startVerse'] ?? '');
|
||||
return QuranReadingScreen(surahId: surahId, initialVerse: startVerse);
|
||||
final startVerse = int.tryParse(
|
||||
state.uri.queryParameters['startVerse'] ?? '');
|
||||
return QuranReadingScreen(
|
||||
surahId: surahId, initialVerse: startVerse);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
@@ -107,9 +120,10 @@ final GoRouter appRouter = GoRouter(
|
||||
builder: (context, state) {
|
||||
final surahId = state.pathParameters['surahId']!;
|
||||
final qariId = state.uri.queryParameters['qariId'];
|
||||
final autoplay = state.uri.queryParameters['autoplay'] == 'true';
|
||||
final autoplay =
|
||||
state.uri.queryParameters['autoplay'] == 'true';
|
||||
return QuranMurattalScreen(
|
||||
surahId: surahId,
|
||||
surahId: surahId,
|
||||
initialQariId: qariId,
|
||||
autoPlay: autoplay,
|
||||
);
|
||||
@@ -139,7 +153,8 @@ final GoRouter appRouter = GoRouter(
|
||||
// Simple Mode Tab: Zikir
|
||||
GoRoute(
|
||||
path: '/dzikir',
|
||||
builder: (context, state) => const DzikirScreen(isSimpleModeTab: true),
|
||||
builder: (context, state) =>
|
||||
const DzikirScreen(isSimpleModeTab: true),
|
||||
),
|
||||
// Simple Mode Tab: Tilawah
|
||||
GoRoute(
|
||||
@@ -148,18 +163,24 @@ final GoRouter appRouter = GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'enrichment',
|
||||
builder: (context, state) => const QuranEnrichmentScreen(),
|
||||
builder: (context, state) =>
|
||||
const QuranEnrichmentScreen(isSimpleModeTab: true),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'bookmarks',
|
||||
builder: (context, state) => const QuranBookmarksScreen(),
|
||||
builder: (context, state) =>
|
||||
const QuranBookmarksScreen(isSimpleModeTab: true),
|
||||
),
|
||||
GoRoute(
|
||||
path: ':surahId',
|
||||
builder: (context, state) {
|
||||
final surahId = state.pathParameters['surahId']!;
|
||||
final startVerse = int.tryParse(state.uri.queryParameters['startVerse'] ?? '');
|
||||
return QuranReadingScreen(surahId: surahId, initialVerse: startVerse, isSimpleModeTab: true);
|
||||
final startVerse =
|
||||
int.tryParse(state.uri.queryParameters['startVerse'] ?? '');
|
||||
return QuranReadingScreen(
|
||||
surahId: surahId,
|
||||
initialVerse: startVerse,
|
||||
isSimpleModeTab: true);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
@@ -168,9 +189,10 @@ final GoRouter appRouter = GoRouter(
|
||||
builder: (context, state) {
|
||||
final surahId = state.pathParameters['surahId']!;
|
||||
final qariId = state.uri.queryParameters['qariId'];
|
||||
final autoplay = state.uri.queryParameters['autoplay'] == 'true';
|
||||
final autoplay =
|
||||
state.uri.queryParameters['autoplay'] == 'true';
|
||||
return QuranMurattalScreen(
|
||||
surahId: surahId,
|
||||
surahId: surahId,
|
||||
initialQariId: qariId,
|
||||
autoPlay: autoplay,
|
||||
isSimpleModeTab: true,
|
||||
@@ -187,11 +209,17 @@ final GoRouter appRouter = GoRouter(
|
||||
),
|
||||
GoRoute(
|
||||
path: '/hadits',
|
||||
builder: (context, state) => const HaditsScreen(isSimpleModeTab: true),
|
||||
builder: (context, state) =>
|
||||
const HaditsScreen(isSimpleModeTab: true),
|
||||
),
|
||||
],
|
||||
),
|
||||
// ── Settings (pushed, no bottom nav) ──
|
||||
GoRoute(
|
||||
path: '/notifications',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
builder: (context, state) => const NotificationCenterScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
parentNavigatorKey: _rootNavigatorKey,
|
||||
@@ -201,11 +229,31 @@ final GoRouter appRouter = GoRouter(
|
||||
);
|
||||
|
||||
/// Scaffold wrapper that provides the persistent bottom nav bar.
|
||||
class _ScaffoldWithNav extends StatelessWidget {
|
||||
class _ScaffoldWithNav extends StatefulWidget {
|
||||
const _ScaffoldWithNav({required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<_ScaffoldWithNav> createState() => _ScaffoldWithNavState();
|
||||
}
|
||||
|
||||
class _ScaffoldWithNavState extends State<_ScaffoldWithNav> {
|
||||
DateTime? _lastBackPressedAt;
|
||||
|
||||
bool _shouldHideBottomNav({
|
||||
required bool isSimpleMode,
|
||||
required String path,
|
||||
}) {
|
||||
if (!isSimpleMode) return false;
|
||||
if (path == '/dzikir') return true;
|
||||
if (!path.startsWith('/quran/')) return false;
|
||||
|
||||
final tail = path.substring('/quran/'.length);
|
||||
if (tail == 'bookmarks' || tail == 'enrichment') return false;
|
||||
return !tail.contains('/');
|
||||
}
|
||||
|
||||
/// Maps route locations to bottom nav indices.
|
||||
int _currentIndex(BuildContext context) {
|
||||
final location = GoRouterState.of(context).uri.toString();
|
||||
@@ -214,9 +262,13 @@ class _ScaffoldWithNav extends StatelessWidget {
|
||||
|
||||
if (isSimpleMode) {
|
||||
if (location.startsWith('/imsakiyah')) return 1;
|
||||
if (location.startsWith('/quran') && !location.contains('/murattal')) return 2;
|
||||
if (location.contains('/murattal')) return 3;
|
||||
if (location.startsWith('/dzikir')) return 4;
|
||||
if (location.startsWith('/quran')) return 2;
|
||||
if (location.startsWith('/dzikir')) return 3;
|
||||
if (location.startsWith('/tools') ||
|
||||
location.startsWith('/doa') ||
|
||||
location.startsWith('/hadits')) {
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
if (location.startsWith('/imsakiyah')) return 1;
|
||||
@@ -243,10 +295,10 @@ class _ScaffoldWithNav extends StatelessWidget {
|
||||
context.go('/quran');
|
||||
break;
|
||||
case 3:
|
||||
context.push('/quran/1/murattal');
|
||||
context.go('/dzikir');
|
||||
break;
|
||||
case 4:
|
||||
context.go('/dzikir');
|
||||
context.go('/tools');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -270,16 +322,97 @@ class _ScaffoldWithNav extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isMainShellRoute({
|
||||
required bool isSimpleMode,
|
||||
required String path,
|
||||
}) {
|
||||
if (isSimpleMode) {
|
||||
return path == '/' ||
|
||||
path == '/imsakiyah' ||
|
||||
path == '/quran' ||
|
||||
path == '/dzikir' ||
|
||||
path == '/tools';
|
||||
}
|
||||
|
||||
return path == '/' ||
|
||||
path == '/imsakiyah' ||
|
||||
path == '/checklist' ||
|
||||
path == '/laporan' ||
|
||||
path == '/tools';
|
||||
}
|
||||
|
||||
Future<void> _handleMainRouteBack(
|
||||
BuildContext context, {
|
||||
required String path,
|
||||
}) async {
|
||||
if (path != '/') {
|
||||
context.go('/');
|
||||
return;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final pressedRecently = _lastBackPressedAt != null &&
|
||||
now.difference(_lastBackPressedAt!) <= const Duration(seconds: 2);
|
||||
|
||||
if (pressedRecently) {
|
||||
await SystemNavigator.pop();
|
||||
return;
|
||||
}
|
||||
|
||||
_lastBackPressedAt = now;
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
messenger
|
||||
?..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Tekan sekali lagi untuk keluar'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<Box<AppSettings>>(
|
||||
valueListenable: Hive.box<AppSettings>(HiveBoxes.settings).listenable(),
|
||||
builder: (context, box, _) {
|
||||
return Scaffold(
|
||||
body: child,
|
||||
bottomNavigationBar: AppBottomNavBar(
|
||||
currentIndex: _currentIndex(context),
|
||||
onTap: (i) => _onTap(context, i),
|
||||
final isSimpleMode = box.get('default')?.simpleMode ?? false;
|
||||
final path = GoRouterState.of(context).uri.path;
|
||||
final hideBottomNav = _shouldHideBottomNav(
|
||||
isSimpleMode: isSimpleMode,
|
||||
path: path,
|
||||
);
|
||||
final modeScopedChild = KeyedSubtree(
|
||||
key: ValueKey(
|
||||
'shell:$path:${isSimpleMode ? 'simple' : 'full'}',
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
final pageBody = hideBottomNav
|
||||
? SafeArea(
|
||||
top: false,
|
||||
child: modeScopedChild,
|
||||
)
|
||||
: modeScopedChild;
|
||||
|
||||
final handleBackInShell =
|
||||
defaultTargetPlatform == TargetPlatform.android &&
|
||||
_isMainShellRoute(isSimpleMode: isSimpleMode, path: path);
|
||||
|
||||
return PopScope(
|
||||
canPop: !handleBackInShell,
|
||||
onPopInvokedWithResult: (didPop, _) async {
|
||||
if (didPop || !handleBackInShell) return;
|
||||
await _handleMainRouteBack(context, path: path);
|
||||
},
|
||||
child: Scaffold(
|
||||
body: pageBody,
|
||||
bottomNavigationBar: hideBottomNav
|
||||
? null
|
||||
: AppBottomNavBar(
|
||||
currentIndex: _currentIndex(context),
|
||||
onTap: (i) => _onTap(context, i),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user