From f0a17af8458fcfc74e9a2b4d63d72507fd38ae1f Mon Sep 17 00:00:00 2001 From: olshab Date: Tue, 17 Dec 2024 14:31:39 +0400 Subject: [PATCH] final improvements added internet permission in manifest added unique app bar titles for each section fixed card layout fixed card details page layout added appbar button for toggling dark mode --- android/app/src/main/AndroidManifest.xml | 1 + l10n/app_en.arb | 1 + l10n/app_ru.arb | 7 +- .../locale/l10n/app_localizations.dart | 6 ++ .../locale/l10n/app_localizations_en.dart | 3 + .../locale/l10n/app_localizations_ru.dart | 3 + lib/main.dart | 28 +++++- .../details_page/details_page.dart | 32 ++++--- .../favourites_page/favourites_page.dart | 9 +- lib/presentation/home_page/bloc/bloc.dart | 18 ++++ lib/presentation/home_page/bloc/state.dart | 5 + lib/presentation/home_page/card_crypto.dart | 81 +++++++++------- lib/presentation/home_page/cards_list.dart | 5 +- lib/presentation/home_page/home_page.dart | 96 ++++++++++--------- .../settings_page/settings_page.dart | 50 +++++----- 15 files changed, 213 insertions(+), 132 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a0542aa..cf4ef58 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + 'Cryptocurrency info'; + @override + String get favouritesPageAppBarTitle => 'Favourites'; + @override String get settingsPageAppBarTitle => 'Settings'; diff --git a/lib/components/locale/l10n/app_localizations_ru.dart b/lib/components/locale/l10n/app_localizations_ru.dart index d25fc41..bf2878d 100644 --- a/lib/components/locale/l10n/app_localizations_ru.dart +++ b/lib/components/locale/l10n/app_localizations_ru.dart @@ -12,6 +12,9 @@ class AppLocaleRu extends AppLocale { @override String get detailsPageAppBarTitle => 'Сведения о валюте'; + @override + String get favouritesPageAppBarTitle => 'Избранное'; + @override String get settingsPageAppBarTitle => 'Настройки'; diff --git a/lib/main.dart b/lib/main.dart index 1ecff7e..91acad7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,9 +15,16 @@ void main() { runApp(const MyApp()); } -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { const MyApp({super.key}); - + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + bool isDarkMode = false; + @override Widget build(BuildContext context) { return BlocProvider( @@ -33,8 +40,12 @@ class MyApp extends StatelessWidget { localizationsDelegates: AppLocale.localizationsDelegates, supportedLocales: AppLocale.supportedLocales, theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigoAccent), + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.indigoAccent, + brightness: isDarkMode ? Brightness.dark : Brightness.light, + ), useMaterial3: true, + brightness: isDarkMode ? Brightness.dark : Brightness.light, ), debugShowCheckedModeBanner: false, home: RepositoryProvider( @@ -43,7 +54,10 @@ class MyApp extends StatelessWidget { child: BlocProvider( lazy: false, create: (context) => HomeBloc(context.read()), - child: const MainScaffold(), + child: MainScaffold( + toggleDarkMode: _toggleDarkMode, + isDarkModeSelected: isDarkMode, + ), ), ), ), @@ -52,6 +66,12 @@ class MyApp extends StatelessWidget { ); } + void _toggleDarkMode() { + setState(() { + isDarkMode = !isDarkMode; + }); + } + String _getLangCode(String fullLocaleName) { int index = fullLocaleName.indexOf('_'); return index != -1 ? fullLocaleName.substring(0, index) : fullLocaleName; diff --git a/lib/presentation/details_page/details_page.dart b/lib/presentation/details_page/details_page.dart index 0d326be..5015e9e 100644 --- a/lib/presentation/details_page/details_page.dart +++ b/lib/presentation/details_page/details_page.dart @@ -14,19 +14,25 @@ class DetailsPage extends StatelessWidget { title: Text(context.locale.detailsPageAppBarTitle), backgroundColor: Theme.of(context).colorScheme.inversePrimary, ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Image.network(data.imageUrl ?? ''), - ), - Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text(data.title, style: Theme.of(context).textTheme.headlineLarge), - ), - Text(data.currentPrice, style: Theme.of(context).textTheme.bodyLarge), - ], + body: Padding( + padding: const EdgeInsets.only(left: 20, right: 20, top: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 40), + child: Image.network(data.imageUrl ?? ''), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text(data.title, style: Theme.of(context).textTheme.headlineLarge), + ), + Text(data.currentPrice, style: Theme.of(context).textTheme.bodyLarge), + Text(data.priceChange, style: Theme.of(context).textTheme.labelLarge), + ], + ), ), ); } diff --git a/lib/presentation/favourites_page/favourites_page.dart b/lib/presentation/favourites_page/favourites_page.dart index 1be1c3d..1972d93 100644 --- a/lib/presentation/favourites_page/favourites_page.dart +++ b/lib/presentation/favourites_page/favourites_page.dart @@ -54,7 +54,7 @@ class _FavouritesPageState extends State { } }, child: Padding( - padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + padding: EdgeInsets.only(top: 12), child: Column( children: [ CardsList( @@ -65,7 +65,10 @@ class _FavouritesPageState extends State { ), BlocBuilder( builder: (context, state) => state.isPaginationLoading - ? const CircularProgressIndicator() + ? const Padding( + padding: EdgeInsets.all(12), + child: CircularProgressIndicator(), + ) : const SizedBox.shrink(), ), ], @@ -112,7 +115,7 @@ class _FavouritesPageState extends State { void _onNextPage() { final bloc = context.read(); - if (!bloc.state.isPaginationLoading) { + if (!bloc.state.isPaginationLoading && !bloc.state.isAllPagesLoaded) { bloc.add(HomeLoadFavouritesDataEvent( ids: favouritesIds, nextPage: bloc.state.data?.nextPage, diff --git a/lib/presentation/home_page/bloc/bloc.dart b/lib/presentation/home_page/bloc/bloc.dart index be81a6b..9590fef 100644 --- a/lib/presentation/home_page/bloc/bloc.dart +++ b/lib/presentation/home_page/bloc/bloc.dart @@ -12,6 +12,8 @@ class HomeBloc extends Bloc { } Future _onLoadData(HomeLoadDataEvent event, Emitter emit) async { + const int pageSize = 20; + if (event.nextPage == null) { emit(state.copyWith(isLoading: true)); } else { @@ -23,10 +25,16 @@ class HomeBloc extends Bloc { final data = await repo.loadData( search: event.search, page: event.nextPage ?? 1, + pageSize: pageSize, onError: (e) => error = e, locale: event.locale, ); + bool isLastPage = false; + if (data?.data != null && data!.data!.length < pageSize) { + isLastPage = true; + } + if (event.nextPage != null) { data?.data?.insertAll(0, state.data?.data ?? []); } @@ -36,10 +44,13 @@ class HomeBloc extends Bloc { isPaginationLoading: false, data: data, error: error, + isAllPagesLoaded: isLastPage, )); } Future _onLoadFavouritesData(HomeLoadFavouritesDataEvent event, Emitter emit) async { + const int pageSize = 10; + if (event.nextPage == null) { emit(state.copyWith(isLoading: true)); } else { @@ -51,10 +62,16 @@ class HomeBloc extends Bloc { final data = await repo.loadDataWithIds( ids: event.ids ?? [], page: event.nextPage ?? 1, + pageSize: pageSize, onError: (e) => error = e, locale: event.locale, ); + bool isLastPage = false; + if (data?.data != null && data!.data!.length < pageSize) { + isLastPage = true; + } + if (event.nextPage != null) { data?.data?.insertAll(0, state.data?.data ?? []); } @@ -64,6 +81,7 @@ class HomeBloc extends Bloc { isPaginationLoading: false, data: data, error: error, + isAllPagesLoaded: isLastPage, )); } } diff --git a/lib/presentation/home_page/bloc/state.dart b/lib/presentation/home_page/bloc/state.dart index 093cb70..236257c 100644 --- a/lib/presentation/home_page/bloc/state.dart +++ b/lib/presentation/home_page/bloc/state.dart @@ -6,12 +6,14 @@ class HomeState extends Equatable { final bool isLoading; final bool isPaginationLoading; final String? error; + final bool isAllPagesLoaded; const HomeState({ this.data, this.isLoading = false, this.isPaginationLoading = false, this.error, + this.isAllPagesLoaded = false, }); HomeState copyWith({ @@ -19,11 +21,13 @@ class HomeState extends Equatable { bool? isLoading, bool? isPaginationLoading, String? error, + bool? isAllPagesLoaded, }) => HomeState( data: data ?? this.data, isLoading: isLoading ?? this.isLoading, isPaginationLoading: isPaginationLoading ?? this.isPaginationLoading, error: error ?? this.error, + isAllPagesLoaded: isAllPagesLoaded ?? this.isAllPagesLoaded, ); @override @@ -32,5 +36,6 @@ class HomeState extends Equatable { isLoading, isPaginationLoading, error, + isAllPagesLoaded, ]; } diff --git a/lib/presentation/home_page/card_crypto.dart b/lib/presentation/home_page/card_crypto.dart index 6916d28..7b0dd98 100644 --- a/lib/presentation/home_page/card_crypto.dart +++ b/lib/presentation/home_page/card_crypto.dart @@ -54,7 +54,7 @@ class CardCrypto extends StatelessWidget { borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( - color: Colors.grey.withOpacity(0.5), + color: Colors.black38.withOpacity(0.2), spreadRadius: 4, offset: const Offset(0, 5), blurRadius: 6, @@ -81,43 +81,52 @@ class CardCrypto extends StatelessWidget { ), ), ), - Flexible( - child: Padding( - padding: const EdgeInsets.only(left: 12.0, top: 12, bottom: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context).textTheme.headlineLarge, - ), - Text( - currentPrice, - style: Theme.of(context).textTheme.bodyLarge), - ], - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 16, 16), - child: GestureDetector( - onTap: () => onLike?.call(id, title, isLiked), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 100), - child: isLiked - ? const Icon( - Icons.star, - color: Colors.orangeAccent, - key: ValueKey(0), - ) - : const Icon( - Icons.star_border, - key: ValueKey(1), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16, top: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.headlineLarge, + ), + Text( + currentPrice, + style: Theme.of(context).textTheme.bodyLarge), + ], ), + ), ), - ), + Row( + children: [ + const Spacer(), + Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 16, 16), + child: GestureDetector( + onTap: () => onLike?.call(id, title, isLiked), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 100), + child: isLiked + ? const Icon( + Icons.star, + color: Colors.orangeAccent, + key: ValueKey(0), + ) + : const Icon( + Icons.star_border, + key: ValueKey(1), + ), + ), + ), + ), + ], + ) + ], ), ), ], diff --git a/lib/presentation/home_page/cards_list.dart b/lib/presentation/home_page/cards_list.dart index b18348e..be54e1f 100644 --- a/lib/presentation/home_page/cards_list.dart +++ b/lib/presentation/home_page/cards_list.dart @@ -52,7 +52,10 @@ class _CardsListState extends State { style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.red), ) : state.isLoading - ? const CircularProgressIndicator() + ? const Center(child: Padding( + padding: EdgeInsets.only(top: 20), + child: CircularProgressIndicator(), + )) : BlocBuilder( builder: (context, likeState) => Expanded( child: RefreshIndicator( diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart index a8f7144..fecdcff 100644 --- a/lib/presentation/home_page/home_page.dart +++ b/lib/presentation/home_page/home_page.dart @@ -16,7 +16,14 @@ import '../favourites_bloc/favourites_events.dart'; import '../settings_page/settings_page.dart'; class MainScaffold extends StatefulWidget { - const MainScaffold({super.key}); + const MainScaffold({ + super.key, + this.toggleDarkMode, + required this.isDarkModeSelected, + }); + + final void Function()? toggleDarkMode; + final bool isDarkModeSelected; @override State createState() => _MainScaffoldState(); @@ -30,15 +37,19 @@ class _MainScaffoldState extends State { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(context.locale.mainAppBarTitle), + title: [ + Text(context.locale.mainAppBarTitle), + Text(context.locale.favouritesPageAppBarTitle), + Text(context.locale.settingsPageAppBarTitle), + ][currentPageIndex], actions: [ Padding( padding: const EdgeInsets.only(right: 12.0), child: IconButton( - icon: const Icon(Icons.settings), - onPressed: () => Navigator.push(context, MaterialPageRoute( - builder: (BuildContext context) => const SettingsPage(), - )), + isSelected: widget.isDarkModeSelected, + onPressed: () => widget.toggleDarkMode?.call(), + icon: const Icon(Icons.wb_sunny_outlined), + selectedIcon: const Icon(Icons.brightness_2_outlined), ), ), ], @@ -73,7 +84,6 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { final TextEditingController searchController = TextEditingController(); - @override void initState() { SvgObjects.init(); @@ -86,41 +96,43 @@ class _HomePageState extends State { super.initState(); } + @override + void dispose() { + searchController.dispose(); + + super.dispose(); + } + @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), child: Column( children: [ - Row( - children: [ - Expanded( - flex: 4, - child: Padding( - padding: const EdgeInsets.all(12), - child: SearchBar( - controller: searchController, - onChanged: (search) { - Debounce.run(() => context.read().add(HomeLoadDataEvent(search: search, locale: context.locale))); - }, - leading: const Icon(Icons.search), - trailing: [ - IconButton( - icon: const Icon(Icons.close), - onPressed: () { - searchController.clear(); - context.read().add(HomeLoadDataEvent(locale: context.locale)); - }, - ), - ], - hintText: context.locale.searchHint, - elevation: const WidgetStatePropertyAll(0.0), - padding: const WidgetStatePropertyAll(EdgeInsets.only(left: 18, right: 10)), - backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.secondaryContainer), - ), + Padding( + padding: const EdgeInsets.all(12), + child: SearchBar( + controller: searchController, + onChanged: (search) { + Debounce.run(() => context.read().add(HomeLoadDataEvent(search: search, locale: context.locale))); + }, + leading: const Icon(Icons.search), + trailing: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + if (searchController.text.isNotEmpty) { + searchController.clear(); + context.read().add(HomeLoadDataEvent(locale: context.locale)); + } + }, ), - ), - ], + ], + hintText: context.locale.searchHint, + elevation: const WidgetStatePropertyAll(0.0), + padding: const WidgetStatePropertyAll(EdgeInsets.only(left: 18, right: 10)), + backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.secondaryContainer), + ), ), CardsList( onListRefresh: _onRefresh, @@ -130,7 +142,10 @@ class _HomePageState extends State { ), BlocBuilder( builder: (context, state) => state.isPaginationLoading - ? const CircularProgressIndicator() + ? const Padding( + padding: EdgeInsets.all(12), + child: CircularProgressIndicator(), + ) : const SizedBox.shrink(), ), ], @@ -138,13 +153,6 @@ class _HomePageState extends State { ); } - @override - void dispose() { - searchController.dispose(); - - super.dispose(); - } - void _showSnackBar(BuildContext context, String title, bool isLiked) { WidgetsBinding.instance.addPostFrameCallback((_) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( @@ -178,7 +186,7 @@ class _HomePageState extends State { void _onNextPage() { final bloc = context.read(); - if (!bloc.state.isPaginationLoading) { + if (!bloc.state.isPaginationLoading && !bloc.state.isAllPagesLoaded) { bloc.add(HomeLoadDataEvent( search: searchController.text, nextPage: bloc.state.data?.nextPage, diff --git a/lib/presentation/settings_page/settings_page.dart b/lib/presentation/settings_page/settings_page.dart index 04fd467..ad68d6c 100644 --- a/lib/presentation/settings_page/settings_page.dart +++ b/lib/presentation/settings_page/settings_page.dart @@ -12,41 +12,35 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.locale.settingsPageAppBarTitle), - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - ), - body: Padding( - padding: const EdgeInsets.only(left: 20, right: 16, top: 8), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${context.locale.settingsLanguage}:', - style: Theme.of(context).textTheme.titleMedium, - ), - GestureDetector( - onTap: () => context.read().add(const ChangeLocaleEvent()), - child: SizedBox.square( - dimension: 50, - child: Padding( - padding: const EdgeInsets.only(right: 0), - child: BlocBuilder( + return Padding( + padding: const EdgeInsets.only(left: 20, right: 16, top: 8), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${context.locale.settingsLanguage}:', + style: Theme.of(context).textTheme.titleMedium, + ), + GestureDetector( + onTap: () => context.read().add(const ChangeLocaleEvent()), + child: SizedBox.square( + dimension: 50, + child: Padding( + padding: const EdgeInsets.only(right: 0), + child: BlocBuilder( builder: (context, state) { return state.currentLocale.languageCode == 'ru' ? const SvgRu() : const SvgUs(); }), - ), ), ), - ], - ), - ], - ), + ), + ], + ), + ], ), ); }