Polish navigation, Quran flows, and sharing UX
This commit is contained in:
@@ -1,16 +1,80 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' show ViewFocusEvent, ViewFocusState;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../core/providers/theme_provider.dart';
|
||||
import '../features/dashboard/data/prayer_times_provider.dart';
|
||||
import 'router.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
|
||||
/// Root MaterialApp.router wired to GoRouter + ThemeMode from Riverpod.
|
||||
class App extends ConsumerWidget {
|
||||
class App extends ConsumerStatefulWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends ConsumerState<App> with WidgetsBindingObserver {
|
||||
Timer? _midnightResyncTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
HardwareKeyboard.instance.syncKeyboardState();
|
||||
});
|
||||
_scheduleMidnightResync();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_midnightResyncTimer?.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed ||
|
||||
state == AppLifecycleState.inactive) {
|
||||
// Resync stale pressed-key state to avoid repeated KeyDown assertions.
|
||||
HardwareKeyboard.instance.syncKeyboardState();
|
||||
}
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
ref.invalidate(prayerTimesProvider);
|
||||
unawaited(ref.read(prayerTimesProvider.future));
|
||||
_scheduleMidnightResync();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeViewFocus(ViewFocusEvent event) {
|
||||
if (event.state == ViewFocusState.focused) {
|
||||
HardwareKeyboard.instance.syncKeyboardState();
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleMidnightResync() {
|
||||
_midnightResyncTimer?.cancel();
|
||||
final now = DateTime.now();
|
||||
final nextRun = DateTime(now.year, now.month, now.day, 0, 5).isAfter(now)
|
||||
? DateTime(now.year, now.month, now.day, 0, 5)
|
||||
: DateTime(now.year, now.month, now.day + 1, 0, 5);
|
||||
final delay = nextRun.difference(now);
|
||||
_midnightResyncTimer = Timer(delay, () {
|
||||
ref.invalidate(prayerTimesProvider);
|
||||
unawaited(ref.read(prayerTimesProvider.future));
|
||||
_scheduleMidnightResync();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeMode = ref.watch(themeProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
|
||||
119
lib/app/icons/app_icons.dart
Normal file
119
lib/app/icons/app_icons.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
|
||||
@immutable
|
||||
class AppIconGlyph {
|
||||
const AppIconGlyph.material(this.material) : huge = null;
|
||||
const AppIconGlyph.huge(this.huge) : material = null;
|
||||
|
||||
final IconData? material;
|
||||
final List<List<dynamic>>? huge;
|
||||
}
|
||||
|
||||
class AppIcon extends StatelessWidget {
|
||||
const AppIcon({
|
||||
super.key,
|
||||
required this.glyph,
|
||||
this.color,
|
||||
this.size,
|
||||
this.strokeWidth,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final AppIconGlyph glyph;
|
||||
final Color? color;
|
||||
final double? size;
|
||||
final double? strokeWidth;
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final huge = glyph.huge;
|
||||
if (huge != null) {
|
||||
return HugeIcon(
|
||||
icon: huge,
|
||||
color: color,
|
||||
size: size,
|
||||
strokeWidth: strokeWidth,
|
||||
);
|
||||
}
|
||||
|
||||
return Icon(
|
||||
glyph.material,
|
||||
color: color,
|
||||
size: size,
|
||||
semanticLabel: semanticLabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppIcons {
|
||||
const AppIcons._();
|
||||
|
||||
static const AppIconGlyph home =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedHome01);
|
||||
static const AppIconGlyph calendar =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedCalendar01);
|
||||
static const AppIconGlyph quran =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedQuran02);
|
||||
static const AppIconGlyph dzikir =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedTasbih);
|
||||
static const AppIconGlyph lainnya =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedGridView);
|
||||
static const AppIconGlyph ibadah =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedCheckList);
|
||||
static const AppIconGlyph laporan =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedChart01);
|
||||
|
||||
static const AppIconGlyph murattal =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedHeadset);
|
||||
static const AppIconGlyph qibla =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedCompass);
|
||||
static const AppIconGlyph doa = AppIconGlyph.huge(HugeIcons.strokeRoundedDua);
|
||||
static const AppIconGlyph hadits =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedBooks01);
|
||||
static const AppIconGlyph quranEnrichment =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedQuran01);
|
||||
|
||||
static const AppIconGlyph notification =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedNotification03);
|
||||
static const AppIconGlyph settings =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedSettings02);
|
||||
static const AppIconGlyph share =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedShare01);
|
||||
static const AppIconGlyph themeMoon =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedMoon02);
|
||||
static const AppIconGlyph themeSun =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedSun02);
|
||||
static const AppIconGlyph checkCircle =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedCheckmarkCircle02);
|
||||
static const AppIconGlyph circle =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedCircle);
|
||||
static const AppIconGlyph musicNote =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedMusicNote01);
|
||||
static const AppIconGlyph shuffle =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedShuffle);
|
||||
static const AppIconGlyph previousTrack =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedPrevious);
|
||||
static const AppIconGlyph nextTrack =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedNext);
|
||||
static const AppIconGlyph play =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedPlay);
|
||||
static const AppIconGlyph pause =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedPause);
|
||||
static const AppIconGlyph playlist =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedPlaylist01);
|
||||
static const AppIconGlyph user =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedUser03);
|
||||
static const AppIconGlyph arrowDown =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedArrowDown01);
|
||||
|
||||
static const AppIconGlyph backArrow =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedArrowLeft01);
|
||||
static const AppIconGlyph location =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedMosqueLocation);
|
||||
static const AppIconGlyph locationActive =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedLocation01);
|
||||
static const AppIconGlyph locationOffline =
|
||||
AppIconGlyph.huge(HugeIcons.strokeRoundedLocationOffline01);
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -4,29 +4,55 @@ import 'package:flutter/material.dart';
|
||||
class AppColors {
|
||||
AppColors._();
|
||||
|
||||
// ── Primary ──
|
||||
static const Color primary = Color(0xFF70DF20);
|
||||
static const Color onPrimary = Color(0xFF0A1A00);
|
||||
// ── Brand tokens: logo palette (teal + gold) ──
|
||||
static const Color brandTeal500 = Color(0xFF118A8D);
|
||||
static const Color brandTeal700 = Color(0xFF0C676A);
|
||||
static const Color brandTeal900 = Color(0xFF0A4447);
|
||||
|
||||
// ── Background ──
|
||||
static const Color backgroundLight = Color(0xFFF7F8F6);
|
||||
static const Color backgroundDark = Color(0xFF182111);
|
||||
static const Color brandGold200 = Color(0xFFF6DE96);
|
||||
static const Color brandGold300 = Color(0xFFE9C75B);
|
||||
static const Color brandGold400 = Color(0xFFD6A21D);
|
||||
static const Color brandGold700 = Color(0xFF8B6415);
|
||||
|
||||
// ── Theme base tokens ──
|
||||
static const Color backgroundLight = Color(0xFFF3F4F6);
|
||||
static const Color backgroundDark = Color(0xFF0F1217);
|
||||
|
||||
// ── Surface ──
|
||||
static const Color surfaceLight = Color(0xFFFFFFFF);
|
||||
static const Color surfaceDark = Color(0xFF1E2A14);
|
||||
static const Color surfaceLightElevated = Color(0xFFF9FAFB);
|
||||
|
||||
// ── Sage (secondary text / section labels) ──
|
||||
static const Color sage = Color(0xFF728764);
|
||||
static const Color surfaceDark = Color(0xFF171B22);
|
||||
static const Color surfaceDarkElevated = Color(0xFF1D222B);
|
||||
|
||||
// ── Cream (dividers, borders — light mode only) ──
|
||||
static const Color cream = Color(0xFFF2F4F0);
|
||||
static const Color textPrimaryLight = Color(0xFF1F2937);
|
||||
static const Color textPrimaryDark = Color(0xFFE8ECF2);
|
||||
static const Color textSecondaryLight = Color(0xFF6B7280);
|
||||
static const Color textSecondaryDark = Color(0xFF9AA4B2);
|
||||
|
||||
// ── Text ──
|
||||
static const Color textPrimaryLight = Color(0xFF1A2A0A);
|
||||
static const Color textPrimaryDark = Color(0xFFF2F4F0);
|
||||
static const Color textSecondaryLight = Color(0xFF64748B);
|
||||
static const Color textSecondaryDark = Color(0xFF94A3B8);
|
||||
// ── Compatibility aliases (existing UI references) ──
|
||||
static const Color primary = brandTeal500;
|
||||
static const Color onPrimary = Color(0xFFFFFFFF);
|
||||
static const Color sage = brandTeal700;
|
||||
static const Color cream = Color(0xFFE5E7EB);
|
||||
|
||||
// ── Luxury active-state tokens ──
|
||||
static const Color navActiveGold = brandGold400;
|
||||
static const Color navActiveGoldBright = brandGold300;
|
||||
static const Color navActiveGoldPale = brandGold200;
|
||||
static const Color navActiveGoldDeep = brandGold700;
|
||||
|
||||
static const Color navActiveSurfaceDark = surfaceDarkElevated;
|
||||
static const Color navActiveSurfaceLight = surfaceLight;
|
||||
|
||||
static const Color navGlowDark = Color(0x5CD6A21D);
|
||||
static const Color navGlowLight = Color(0x36D6A21D);
|
||||
static const Color navShadowLight = Color(0x1F0F172A);
|
||||
static const Color navStrokeNeutralDark = Color(0x33FFFFFF);
|
||||
static const Color navStrokeNeutralLight = Color(0x220F172A);
|
||||
|
||||
static const Color navEmbossHighlight = Color(0xE6FFFFFF);
|
||||
static const Color navEmbossShadow = Color(0x2B0F172A);
|
||||
static const Color navEmbossGoldShadow = Color(0x42B88912);
|
||||
|
||||
// ── Semantic ──
|
||||
static const Color errorLight = Color(0xFFEF4444);
|
||||
@@ -36,25 +62,29 @@ class AppColors {
|
||||
|
||||
// ── Convenience helpers for theme building ──
|
||||
|
||||
static ColorScheme get lightColorScheme => ColorScheme.light(
|
||||
static ColorScheme get lightColorScheme => const ColorScheme.light(
|
||||
primary: primary,
|
||||
onPrimary: onPrimary,
|
||||
primaryContainer: brandTeal700,
|
||||
onPrimaryContainer: Colors.white,
|
||||
surface: surfaceLight,
|
||||
onSurface: textPrimaryLight,
|
||||
error: errorLight,
|
||||
onError: Colors.white,
|
||||
secondary: sage,
|
||||
onSecondary: Colors.white,
|
||||
secondary: navActiveGold,
|
||||
onSecondary: brandGold700,
|
||||
);
|
||||
|
||||
static ColorScheme get darkColorScheme => ColorScheme.dark(
|
||||
static ColorScheme get darkColorScheme => const ColorScheme.dark(
|
||||
primary: primary,
|
||||
onPrimary: onPrimary,
|
||||
primaryContainer: brandTeal900,
|
||||
onPrimaryContainer: textPrimaryDark,
|
||||
surface: surfaceDark,
|
||||
onSurface: textPrimaryDark,
|
||||
error: errorDark,
|
||||
onError: Colors.black,
|
||||
secondary: sage,
|
||||
onSecondary: Colors.white,
|
||||
secondary: navActiveGold,
|
||||
onSecondary: brandGold200,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Typography definitions from PRD §3.2.
|
||||
/// Plus Jakarta Sans (bundled) for UI text, Amiri (bundled) for Arabic content.
|
||||
/// Plus Jakarta Sans (bundled) for UI text.
|
||||
/// Scheherazade New (bundled) for Arabic/Quran text, with Uthman/KFGQPC fallback.
|
||||
class AppTextStyles {
|
||||
AppTextStyles._();
|
||||
|
||||
static const String _fontFamily = 'PlusJakartaSans';
|
||||
static const String _arabicFontFamily = 'ScheherazadeNew';
|
||||
static const List<String> _arabicFallbackFamilies = <String>[
|
||||
'UthmanTahaNaskh',
|
||||
'KFGQPCUthmanicHafs',
|
||||
'Amiri',
|
||||
'Noto Naskh Arabic',
|
||||
'Noto Sans Arabic',
|
||||
'Droid Arabic Naskh',
|
||||
'sans-serif',
|
||||
];
|
||||
|
||||
/// Builds the full TextTheme for the app using bundled Plus Jakarta Sans.
|
||||
static const TextTheme textTheme = TextTheme(
|
||||
@@ -52,19 +63,21 @@ class AppTextStyles {
|
||||
),
|
||||
);
|
||||
|
||||
// ── Arabic text styles (Amiri — bundled font) ──
|
||||
// ── Arabic text styles (Scheherazade New — bundled font) ──
|
||||
|
||||
static const TextStyle arabicBody = TextStyle(
|
||||
fontFamily: 'Amiri',
|
||||
fontFamily: _arabicFontFamily,
|
||||
fontFamilyFallback: _arabicFallbackFamilies,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 2.0,
|
||||
height: 1.8,
|
||||
);
|
||||
|
||||
static const TextStyle arabicLarge = TextStyle(
|
||||
fontFamily: 'Amiri',
|
||||
fontFamily: _arabicFontFamily,
|
||||
fontFamilyFallback: _arabicFallbackFamilies,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 2.2,
|
||||
height: 2.0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,17 +29,17 @@ class AppTheme {
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: AppColors.surfaceLight,
|
||||
selectedItemColor: AppColors.primary,
|
||||
selectedItemColor: AppColors.navActiveGoldDeep,
|
||||
unselectedItemColor: AppColors.textSecondaryLight,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 0,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: AppColors.surfaceLight,
|
||||
color: AppColors.surfaceLightElevated,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(
|
||||
side: const BorderSide(
|
||||
color: AppColors.cream,
|
||||
),
|
||||
),
|
||||
@@ -70,21 +70,21 @@ class AppTheme {
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: AppColors.surfaceDark,
|
||||
selectedItemColor: AppColors.primary,
|
||||
selectedItemColor: AppColors.navActiveGold,
|
||||
unselectedItemColor: AppColors.textSecondaryDark,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 0,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: AppColors.surfaceDark,
|
||||
color: AppColors.surfaceDarkElevated,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(
|
||||
color: AppColors.primary.withValues(alpha: 0.1),
|
||||
color: AppColors.brandTeal500.withValues(alpha: 0.22),
|
||||
),
|
||||
),
|
||||
),
|
||||
dividerColor: AppColors.surfaceDark,
|
||||
dividerColor: AppColors.surfaceDarkElevated,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user