diff --git a/lib/repo/database_repository.dart b/lib/repo/database_repository.dart index a8dab82..b7fdef8 100644 --- a/lib/repo/database_repository.dart +++ b/lib/repo/database_repository.dart @@ -107,6 +107,15 @@ class DatabaseRepository { ); } + // get to total number of items in the database + Future getCount() async { + final db = await dbHelper.database; + final count = Sqflite.firstIntValue( + await db.rawQuery('SELECT COUNT(*) FROM $tableName'), + ); + return count ?? 0; + } + Future> getSortedPaginatedOrganization( int page, int pageSize, String category) async { final db = await dbHelper.database; diff --git a/lib/states/deties_state.dart b/lib/states/deties_state.dart index 5ac08c1..f98a745 100644 --- a/lib/states/deties_state.dart +++ b/lib/states/deties_state.dart @@ -11,6 +11,7 @@ class DeityListState { final int page; final int pageSize; final String? error; + final int total; DeityListState({ required this.deities, @@ -19,6 +20,7 @@ class DeityListState { required this.page, required this.pageSize, this.error, + required this.total, }); factory DeityListState.initial() { @@ -28,6 +30,7 @@ class DeityListState { hasReachedMax: false, page: 0, pageSize: 20, + total: 0, ); } @@ -38,6 +41,7 @@ class DeityListState { int? page, int? pageSize, String? error, + int? total, }) { return DeityListState( deities: deities ?? this.deities, @@ -46,6 +50,7 @@ class DeityListState { page: page ?? this.page, pageSize: pageSize ?? this.pageSize, error: error ?? this.error, + total: total ?? this.total, ); } } @@ -120,6 +125,11 @@ class DeityNotifier extends StateNotifier { } } + // get the total number of deties in the database + Future getDeitiesCount() async { + return await repository.getCount(); + } + void clearSearchResults() { state = DeityListState.initial(); } diff --git a/lib/states/festival_state.dart b/lib/states/festival_state.dart index 8045355..2856e18 100644 --- a/lib/states/festival_state.dart +++ b/lib/states/festival_state.dart @@ -74,6 +74,12 @@ class FestivalNotifier extends StateNotifier { } } + // to get the total number of festivals + Future getFestivalCount() async { + final totalFestivals = await repository.getCount(); + return totalFestivals; + } + void clearSearchResults() { state = FestivalListState.initial(); } diff --git a/lib/states/organization_state.dart b/lib/states/organization_state.dart index 4f96ccc..081c1b3 100644 --- a/lib/states/organization_state.dart +++ b/lib/states/organization_state.dart @@ -130,6 +130,11 @@ class OrganizationNotifier extends StateNotifier { } } + // get total number of organizations + Future getOrganizationCount() async { + return await repository.getCount(); + } + void clearSearchResults() { state = OrganizationListState.initial(); } diff --git a/lib/ui/screen/home_screen.dart b/lib/ui/screen/home_screen.dart index 2b4f78d..4337305 100644 --- a/lib/ui/screen/home_screen.dart +++ b/lib/ui/screen/home_screen.dart @@ -2,47 +2,201 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:gompa_tour/states/bottom_nav_state.dart'; +import 'package:gompa_tour/states/deties_state.dart'; +import 'package:gompa_tour/states/festival_state.dart'; +import 'package:gompa_tour/states/organization_state.dart'; +import 'package:gompa_tour/states/recent_search.dart'; +import 'package:gompa_tour/states/search_state.dart'; import 'package:gompa_tour/ui/screen/deities_list_screen.dart'; import 'package:gompa_tour/ui/screen/festival_list_screen.dart'; -import 'package:gompa_tour/ui/screen/organization_list_screen.dart'; import 'package:gompa_tour/ui/screen/orginatzations_screen.dart'; +import 'package:gompa_tour/ui/widget/search_card_item.dart'; import 'package:gompa_tour/util/enum.dart'; +import 'package:gompa_tour/util/search_debouncer.dart'; -class HomeScreen extends ConsumerWidget { +class HomeScreen extends ConsumerStatefulWidget { const HomeScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - return SingleChildScrollView( - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildCard( - MenuType.deities, - 'assets/images/buddha.png', - context, - ), - const SizedBox(height: 16), - _buildCard( - MenuType.organization, - 'assets/images/potala2.png', - context, + ConsumerState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends ConsumerState { + final TextEditingController _searchController = TextEditingController(); + final FocusNode _searchFocusNode = FocusNode(); + final _searchDebouncer = SearchDebouncer(); + + int totalDeity = 0; + int totalOrganization = 0; + int totalFestival = 0; + + @override + void initState() { + super.initState(); + _loadCounts(); + } + + void _loadCounts() async { + totalDeity = + await ref.read(detiesNotifierProvider.notifier).getDeitiesCount(); + totalOrganization = await ref + .read(organizationNotifierProvider.notifier) + .getOrganizationCount(); + totalFestival = + await ref.read(festivalNotifierProvider.notifier).getFestivalCount(); + } + + void _performSearch(String query) async { + if (query.isEmpty || query.length < 3) { + _clearSearchResults(); + return; + } + + _searchDebouncer.run( + query, + onSearch: (q) => + ref.read(searchNotifierProvider.notifier).searchAcrossTables(q), + onSaveSearch: (q) => + ref.read(recentSearchesProvider.notifier).addSearch(q), + onClearResults: _clearSearchResults, + ); + } + + @override + Widget build(BuildContext context) { + final searchState = ref.watch(searchNotifierProvider); + + return Column( + children: [ + _buildHeader(context), + _buildSearchBar(context), + const Divider(), + _buildCategoryCards(context), + _buildSearchResults(context, searchState), + searchState.isLoading + ? const Center( + child: CircularProgressIndicator(), + ) + : const SizedBox(), + ], + ); + } + + Widget _buildHeader(BuildContext context) { + return Column( + children: [ + Text( + 'Department of Religion and Culture', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 16), - _buildCard( - MenuType.festival, - 'assets/images/duchen.png', - context, + ), + Text( + 'Central Tibetan Administration', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, ), - const SizedBox(height: 32), + ), + ], + ); + } + + Widget _buildSearchBar(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: 28, + vertical: 16, + ), + child: SearchBar( + backgroundColor: WidgetStateProperty.resolveWith( + (states) => Colors.white, + ), + controller: _searchController, + focusNode: _searchFocusNode, + leading: Icon(Icons.search), + trailing: [ + _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + _clearSearchResults(); + }, + ) + : const SizedBox(), + IconButton( + icon: Icon(Icons.qr_code), + onPressed: () { + ref.read(bottomNavProvider.notifier).setAndPersistValue(2); + }, + ) ], + hintText: 'Search here....', + onChanged: (value) { + _performSearch(value); + }, ), ); } - Widget _buildCard(MenuType type, String imagePath, BuildContext context) { + Widget _buildCategoryCards(BuildContext context) { + return _searchController.text.isEmpty + ? Expanded( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 600), // Optional: limits max width + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 600), // Optional: limits max width + child: GridView.count( + crossAxisCount: 2, + padding: EdgeInsets.all(8), + mainAxisSpacing: 16, + crossAxisSpacing: 16, + shrinkWrap: true, + childAspectRatio: 1, + children: [ + _buildCard( + MenuType.deities, + 'assets/images/buddha.png', + context, + totalDeity, + ), + _buildCard( + MenuType.organization, + 'assets/images/potala2.png', + context, + totalOrganization, + ), + _buildCard( + MenuType.pilgrimage, + 'assets/images/duchen.png', + context, + totalFestival, + ), + _buildCard( + MenuType.festival, + 'assets/images/duchen.png', + context, + totalFestival, + ), + ], + ), + ), + ), + ), + ), + ) + : const SizedBox(); + } + + Widget _buildCard( + MenuType type, String imagePath, BuildContext context, int count) { return GestureDetector( onTap: () { switch (type) { @@ -59,32 +213,51 @@ class HomeScreen extends ConsumerWidget { } }, child: Card( - color: Colors.blue, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Image.asset( + color: Colors.white, + child: Column( + children: [ + Flexible( + child: Image.asset( imagePath, - height: 140, + height: 120, fit: BoxFit.contain, ), - const SizedBox(height: 8), - Text( - _getTitle(type, context), - style: const TextStyle( - color: Colors.white, - fontSize: 22, - fontWeight: FontWeight.bold, - ), + ), + Text( + _getTitle(type, context), + style: const TextStyle( + color: Colors.black, + fontSize: 22, + fontWeight: FontWeight.bold, ), - ], - ), + ), + Text(count.toString()), + const SizedBox(height: 8), + ], ), ), ); } + Widget _buildSearchResults(BuildContext context, SearchState searchState) { + if (searchState.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return searchState.results.isNotEmpty + ? Expanded( + child: ListView.builder( + itemCount: searchState.results.length, + itemBuilder: (context, index) { + final searchableItem = searchState.results[index]; + return SearchCardItem(searchableItem: searchableItem); + }, + ), + ) + : const SizedBox(); + } + String _getTitle(MenuType type, BuildContext context) { switch (type) { case MenuType.deities: @@ -93,6 +266,20 @@ class HomeScreen extends ConsumerWidget { return AppLocalizations.of(context)!.organizations; case MenuType.festival: return AppLocalizations.of(context)!.festival; + case MenuType.pilgrimage: + return "Pilgrimage"; } } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + _searchDebouncer.dispose(); + super.dispose(); + } + + void _clearSearchResults() { + ref.read(searchNotifierProvider.notifier).clearSearchResults(); + } } diff --git a/lib/ui/screen/skeleton_screen.dart b/lib/ui/screen/skeleton_screen.dart index 456b053..84d6c71 100644 --- a/lib/ui/screen/skeleton_screen.dart +++ b/lib/ui/screen/skeleton_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_switch/flutter_switch.dart'; +import 'package:gompa_tour/states/language_state.dart'; +import 'package:gompa_tour/states/theme_mode_state.dart'; import 'package:gompa_tour/ui/screen/qr_screen.dart'; import 'package:gompa_tour/ui/screen/search_screen.dart'; import 'package:gompa_tour/ui/screen/settings_screen.dart'; @@ -16,6 +19,7 @@ class SkeletonScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final int? navIndex = ref.watch(bottomNavProvider) as int?; + final currentLanguage = ref.watch(languageProvider).currentLanguage; // Tab configuration List> tabConfigurations = _tabConfiguration(context); @@ -24,22 +28,133 @@ class SkeletonScreen extends ConsumerWidget { HomeScreen(), MapScreen(), QrScreen(), - SearchScreen(), SettingsScreen(), ]; + final themeMode = ref.watch(themeProvider).themeMode; + return Scaffold( //extendBodyBehindAppBar: true, appBar: currentTab['title'] != null ? AppBar( - title: currentTab['title'] == "home" - ? Image.asset( - 'assets/images/logo.png', - height: 40, - ) - : Text(currentTab['title']), - centerTitle: true, + backgroundColor: Colors.white, + leading: Padding( + padding: const EdgeInsets.only(left: 12), + child: Image.asset( + 'assets/images/logo.png', + height: 40, + ), + ), + title: Text( + "Neykor", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + centerTitle: false, elevation: 1, + actions: [ + currentTab["title"] == "home" + ? FlutterSwitch( + width: 60, + height: 30, + toggleSize: 20, + valueFontSize: 12.0, + value: currentLanguage == LanguageState.TIBETAN, + activeText: "བོད།", + inactiveText: "EN", + showOnOff: true, + onToggle: (val) { + ref.read(languageProvider.notifier).setLanguage(val + ? LanguageState.TIBETAN + : LanguageState.ENGLISH); + }, + ) + : const SizedBox(), + MenuAnchor( + builder: (BuildContext context, MenuController controller, + Widget? child) { + return IconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + icon: const Icon(Icons.more_vert), + tooltip: 'Show menu', + ); + }, + style: MenuStyle( + padding: WidgetStateProperty.all( + EdgeInsets.all(12.0), + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + minimumSize: WidgetStateProperty.all(Size(190, 48))), + menuChildren: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(AppLocalizations.of(context)!.theme, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + )), + const SizedBox(width: 20), + FlutterSwitch( + width: 55, + height: 30, + toggleSize: 20, + value: themeMode == ThemeMode.dark, + activeIcon: Icon(Icons.dark_mode, size: 15), + inactiveIcon: Icon(Icons.light_mode, size: 15), + onToggle: (val) { + ref.read(themeProvider).themeMode = + val ? ThemeMode.dark : ThemeMode.light; + }, + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(AppLocalizations.of(context)!.language, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + )), + const SizedBox(width: 20), + FlutterSwitch( + width: 55, + height: 30, + toggleSize: 20, + valueFontSize: 12.0, + value: currentLanguage == LanguageState.TIBETAN, + activeText: "བོད།", + inactiveText: "EN", + showOnOff: true, + onToggle: (val) { + ref.read(languageProvider.notifier).setLanguage( + val + ? LanguageState.TIBETAN + : LanguageState.ENGLISH); + }, + ), + ], + ), + ]), + // IconButton( + // icon: const Icon(Icons.more_vert), + // onPressed: () {}, + // ), + ], ) : null, body: AnimatedSwitcher( diff --git a/lib/ui/widget/bottom_nav_bar.dart b/lib/ui/widget/bottom_nav_bar.dart index 9335408..cbbfa30 100644 --- a/lib/ui/widget/bottom_nav_bar.dart +++ b/lib/ui/widget/bottom_nav_bar.dart @@ -53,11 +53,7 @@ class BottomNavBar extends ConsumerWidget { label: AppLocalizations.of(context)!.qr, ), BottomNavigationBarItem( - icon: const Icon(Icons.search), - label: AppLocalizations.of(context)!.search, - ), - BottomNavigationBarItem( - icon: const Icon(Icons.settings), + icon: const Icon(Icons.person), label: AppLocalizations.of(context)!.settings, ), ], diff --git a/lib/util/enum.dart b/lib/util/enum.dart index 68a8436..6ab5eed 100644 --- a/lib/util/enum.dart +++ b/lib/util/enum.dart @@ -1,2 +1,2 @@ // create enum -enum MenuType { festival, organization, deities } +enum MenuType { festival, organization, deities, pilgrimage }