diff --git a/lib/app.dart b/lib/app.dart index 138b64b..0087bbf 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,12 +4,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'bloc/language/language_cubit.dart'; -import 'bloc/post/post_cubit.dart'; import 'bloc/theme/theme_cubit.dart'; -import 'constants/app_themes.dart'; -import 'constants/supported_locales.dart'; -import 'data/repositories/post_repository.dart'; -import 'presentation/pages/posts/posts_page.dart'; +import 'presentation/pages/splash/splash_page.dart'; +import 'presentation/router/route_controller.dart'; +import 'utils/constants/app_themes.dart'; +import 'utils/constants/supported_locales.dart'; class App extends StatelessWidget { @override @@ -25,34 +24,14 @@ class App extends StatelessWidget { darkTheme: AppThemes.darkTheme, locale: locale, supportedLocales: supportedLocales, + home: SplashPage(), localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, AppLocalizations.delegate, ], - home: Navigator( - pages: [ - MaterialPage( - child: RepositoryProvider( - create: (_) => PostRepository(), - child: BlocProvider( - create: (ctx) => PostCubit( - ctx.read(), - )..fetch(), - child: PostsPage(), - ), - ), - ), - ], - onPopPage: (route, result) { - if (route.didPop(result)) { - return true; - } - - return false; - }, - ), + onGenerateRoute: RouteController.onGenerateRoute, ); } } diff --git a/lib/bloc/app_state/app_state.dart b/lib/bloc/app_state/app_state.dart new file mode 100644 index 0000000..4d3c9d7 --- /dev/null +++ b/lib/bloc/app_state/app_state.dart @@ -0,0 +1,3 @@ +import 'package:flutter/cupertino.dart'; + +class AppState extends ChangeNotifier {} diff --git a/lib/bloc/comment/comment_cubit.dart b/lib/bloc/comment/comment_cubit.dart new file mode 100644 index 0000000..57e7162 --- /dev/null +++ b/lib/bloc/comment/comment_cubit.dart @@ -0,0 +1,17 @@ +import 'dart:async'; + +import '../../data/contractors/base_comment_repository.dart'; +import '../../data/models/comment.dart'; +import '../data/data_cubit.dart'; + +export '../data/data_cubit.dart'; + +class CommentCubit extends DataCubit> { + CommentCubit(this.baseCommentRepository); + + final BaseCommentRepository baseCommentRepository; + + @override + FutureOr> loadData([int? postId]) => + baseCommentRepository.getComments(postId: postId!); +} diff --git a/lib/bloc/data/data_cubit.dart b/lib/bloc/data/data_cubit.dart index 68cf13f..0591c10 100644 --- a/lib/bloc/data/data_cubit.dart +++ b/lib/bloc/data/data_cubit.dart @@ -13,13 +13,13 @@ abstract class DataCubit extends Cubit> { bool get emitInProgress => true; - FutureOr loadData(); + FutureOr loadData([int? id]); - void fetch() async { + void fetch([int? id]) async { emit(state.inProgress()); try { - final data = await loadData(); + final data = await loadData(id); if (data != null) { _logger.fine('$data'); diff --git a/lib/bloc/language/language_cubit.dart b/lib/bloc/language/language_cubit.dart index 60df02f..b2eb58d 100644 --- a/lib/bloc/language/language_cubit.dart +++ b/lib/bloc/language/language_cubit.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../contractors/base_preferences_service.dart'; +import '../../data/contractors/base_preferences_service.dart'; class LanguageCubit extends Cubit { LanguageCubit(this.preferencesService) : super(Locale('en', 'US')) { diff --git a/lib/bloc/post/post_cubit.dart b/lib/bloc/post/post_cubit.dart index 000633f..b16e3b8 100644 --- a/lib/bloc/post/post_cubit.dart +++ b/lib/bloc/post/post_cubit.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import '../../contractors/base_post_repository.dart'; +import '../../data/contractors/base_post_repository.dart'; import '../../data/models/post.dart'; import '../data/data_cubit.dart'; -export '../data/data_cubit.dart'; export '../../data/models/post.dart'; +export '../data/data_cubit.dart'; class PostCubit extends DataCubit> { PostCubit(this.postRepository); @@ -13,5 +13,17 @@ class PostCubit extends DataCubit> { final BasePostRepository postRepository; @override - FutureOr> loadData() => postRepository.getPosts(); + FutureOr> loadData([int? id]) async { + final posts = await postRepository.getPosts(); + + return posts + .map( + (post) => post + ..body = post.body.replaceAll( + '\n', + ' ', + ), + ) + .toList(); + } } diff --git a/lib/bloc/theme/theme_cubit.dart b/lib/bloc/theme/theme_cubit.dart index f5bffc5..d5ec5b4 100644 --- a/lib/bloc/theme/theme_cubit.dart +++ b/lib/bloc/theme/theme_cubit.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../contractors/base_preferences_service.dart'; +import '../../data/contractors/base_preferences_service.dart'; class ThemeCubit extends Cubit { ThemeCubit(this.preferencesService) : super(ThemeMode.system) { @@ -11,6 +11,7 @@ class ThemeCubit extends Cubit { BasePreferencesService preferencesService; void changeTheme(ThemeMode themeMode) async { + print('themeMode: $themeMode'); if (state == themeMode) { return; } diff --git a/lib/config/init.dart b/lib/config/init.dart index bb56d80..11f8559 100644 --- a/lib/config/init.dart +++ b/lib/config/init.dart @@ -3,10 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../bloc/app_bloc_observer.dart'; +import '../data/data_providers/comment_data_provider.dart'; import '../data/data_providers/post_data_provider.dart'; +import '../data/services/logging_service.dart'; +import '../data/services/preferences_service.dart'; import '../locator.dart'; -import '../services/logging_service.dart'; -import '../services/preferences_service.dart'; import 'config.dart'; Future init() async { @@ -29,4 +30,5 @@ Future _initDataProviders() async { final dio = Dio(); locator.register(PostDataProvider(dio)); + locator.register(CommentDataProvider(dio)); } diff --git a/lib/constants/app_text_styles.dart b/lib/constants/app_text_styles.dart deleted file mode 100644 index b88558b..0000000 --- a/lib/constants/app_text_styles.dart +++ /dev/null @@ -1,3 +0,0 @@ -class AppTextStyles { - AppTextStyles._(); -} diff --git a/lib/constants/routes.dart b/lib/constants/routes.dart deleted file mode 100644 index 41fd715..0000000 --- a/lib/constants/routes.dart +++ /dev/null @@ -1,3 +0,0 @@ -class Routes { - Routes._(); -} diff --git a/lib/data/contractors/base_comment_repository.dart b/lib/data/contractors/base_comment_repository.dart new file mode 100644 index 0000000..7337b37 --- /dev/null +++ b/lib/data/contractors/base_comment_repository.dart @@ -0,0 +1,5 @@ +import '../../data/models/comment.dart'; + +abstract class BaseCommentRepository { + Future> getComments({required int postId}); +} diff --git a/lib/contractors/base_post_repository.dart b/lib/data/contractors/base_post_repository.dart similarity index 63% rename from lib/contractors/base_post_repository.dart rename to lib/data/contractors/base_post_repository.dart index 376e5b2..2a287f4 100644 --- a/lib/contractors/base_post_repository.dart +++ b/lib/data/contractors/base_post_repository.dart @@ -1,5 +1,5 @@ -import '../data/models/post.dart'; +import '../../data/models/post.dart'; abstract class BasePostRepository { Future> getPosts(); -} \ No newline at end of file +} diff --git a/lib/contractors/base_preferences_service.dart b/lib/data/contractors/base_preferences_service.dart similarity index 100% rename from lib/contractors/base_preferences_service.dart rename to lib/data/contractors/base_preferences_service.dart diff --git a/lib/data/data_providers/comment_data_provider.dart b/lib/data/data_providers/comment_data_provider.dart new file mode 100644 index 0000000..9dc136b --- /dev/null +++ b/lib/data/data_providers/comment_data_provider.dart @@ -0,0 +1,14 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +import '../models/comment.dart'; + +part 'comment_data_provider.g.dart'; + +@RestApi(baseUrl: 'https://jsonplaceholder.typicode.com/') +abstract class CommentDataProvider { + factory CommentDataProvider(Dio dio, {String baseUrl}) = _CommentDataProvider; + + @GET('/comments') + Future> getComments(@Query('postId') int postId); +} diff --git a/lib/data/data_providers/comment_data_provider.g.dart b/lib/data/data_providers/comment_data_provider.g.dart new file mode 100644 index 0000000..28831b5 --- /dev/null +++ b/lib/data/data_providers/comment_data_provider.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'comment_data_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +class _CommentDataProvider implements CommentDataProvider { + _CommentDataProvider(this._dio, {this.baseUrl}) { + baseUrl ??= 'https://jsonplaceholder.typicode.com/'; + } + + final Dio _dio; + + String? baseUrl; + + @override + Future> getComments(postId) async { + const _extra = {}; + final queryParameters = {r'postId': postId}; + final _data = {}; + final _result = await _dio.fetch>( + _setStreamType>( + Options(method: 'GET', headers: {}, extra: _extra) + .compose(_dio.options, '/comments', + queryParameters: queryParameters, data: _data) + .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl))); + var value = _result.data! + .map((dynamic i) => Comment.fromJson(i as Map)) + .toList(); + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } +} diff --git a/lib/data/exceptions/network_exceptions.dart b/lib/data/exceptions/network_exceptions.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/data/exceptions/network_exceptions.dart @@ -0,0 +1 @@ + diff --git a/lib/data/models/comment.dart b/lib/data/models/comment.dart new file mode 100644 index 0000000..c0918d1 --- /dev/null +++ b/lib/data/models/comment.dart @@ -0,0 +1,25 @@ +class Comment { + final int postId; + final int id; + final String name; + final String email; + String body; + + Comment({ + required this.postId, + required this.id, + required this.name, + required this.email, + required this.body, + }); + + factory Comment.fromJson(Map json) { + return Comment( + postId: json['postId'], + id: json['id'], + name: json['name'], + email: json['email'], + body: json['body'], + ); + } +} diff --git a/lib/data/models/post.dart b/lib/data/models/post.dart index e192354..f46d9e0 100644 --- a/lib/data/models/post.dart +++ b/lib/data/models/post.dart @@ -2,7 +2,7 @@ class Post { final int userId; final int id; final String title; - final String body; + String body; Post({ required this.userId, diff --git a/lib/data/repositories/comment_repository.dart b/lib/data/repositories/comment_repository.dart new file mode 100644 index 0000000..4a871ab --- /dev/null +++ b/lib/data/repositories/comment_repository.dart @@ -0,0 +1,13 @@ +import 'package:architecture_example/data/models/comment.dart'; + +import '../../locator.dart'; +import '../contractors/base_comment_repository.dart'; +import '../data_providers/comment_data_provider.dart'; + +class CommentRepository implements BaseCommentRepository { + final _commentDataProvider = Locator.instance.get(); + + @override + Future> getComments({required int postId}) => + _commentDataProvider.getComments(postId); +} diff --git a/lib/data/repositories/post_repository.dart b/lib/data/repositories/post_repository.dart index 4a296c4..62c0b54 100644 --- a/lib/data/repositories/post_repository.dart +++ b/lib/data/repositories/post_repository.dart @@ -1,7 +1,7 @@ import 'package:architecture_example/data/models/post.dart'; -import '../../contractors/base_post_repository.dart'; import '../../locator.dart'; +import '../contractors/base_post_repository.dart'; import '../data_providers/post_data_provider.dart'; class PostRepository implements BasePostRepository { diff --git a/lib/services/logging_service.dart b/lib/data/services/logging_service.dart similarity index 100% rename from lib/services/logging_service.dart rename to lib/data/services/logging_service.dart diff --git a/lib/services/preferences_service.dart b/lib/data/services/preferences_service.dart similarity index 100% rename from lib/services/preferences_service.dart rename to lib/data/services/preferences_service.dart diff --git a/lib/exceptions/network_exceptions.dart b/lib/exceptions/network_exceptions.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/l10n/app_az.arb b/lib/l10n/app_az.arb index d5bd6b3..c0e7f60 100644 --- a/lib/l10n/app_az.arb +++ b/lib/l10n/app_az.arb @@ -1,3 +1,5 @@ { - "hi": "Salam!" + "posts": "Postlar", + "profile": "Profil", + "setttings": "Tenzimlemeler" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 87a1e1f..8989f6a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,6 +1,6 @@ { - "hi": "Hi!", - "@hi": { - "description": "The conventional newborn programmer greeting" - } + "posts": "Posts", + "@posts": {}, + "profile": "Profile", + "@profile": {} } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 406534a..9cba18c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,7 @@ import 'app.dart'; import 'bloc/language/language_cubit.dart'; import 'bloc/theme/theme_cubit.dart'; import 'config/init.dart'; -import 'services/preferences_service.dart'; +import 'data/services/preferences_service.dart'; void main() async { await init(); diff --git a/lib/presentation/global/.gitkeep b/lib/presentation/global/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/presentation/pages/posts/widgets/post_item.dart b/lib/presentation/global/data_item.dart similarity index 56% rename from lib/presentation/pages/posts/widgets/post_item.dart rename to lib/presentation/global/data_item.dart index a9ab71a..752fcea 100644 --- a/lib/presentation/pages/posts/widgets/post_item.dart +++ b/lib/presentation/global/data_item.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../../../../data/models/post.dart'; -import 'action_button.dart'; + +import '../pages/posts/widgets/action_button.dart'; final avatarColors = { 1: Colors.red, @@ -16,13 +16,22 @@ final avatarColors = { 10: Colors.indigo, }; -class PostItem extends StatelessWidget { - const PostItem({ +class DataItem extends StatelessWidget { + const DataItem({ Key? key, - required this.post, + required this.id, + required this.body, + this.onLikeTap, + this.onCommentTap, + this.showActionBar = true, }) : super(key: key); - final Post post; + final T id; + final String body; + final VoidCallback? onLikeTap; + final VoidCallback? onCommentTap; + + final bool showActionBar; @override Widget build(BuildContext context) { @@ -35,7 +44,7 @@ class PostItem extends StatelessWidget { ), decoration: BoxDecoration( color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(8.0), + borderRadius: BorderRadius.circular(4.0), boxShadow: [ BoxShadow( color: Theme.of(context).primaryColor.withOpacity(0.2), @@ -51,32 +60,33 @@ class PostItem extends StatelessWidget { Row( children: [ CircleAvatar( - backgroundColor: avatarColors[post.userId], + backgroundColor: avatarColors[body.length % 10], ), const SizedBox(width: 10), - Text('User ${post.userId}') + Text('$id'), ], ), const SizedBox(height: 10), Text( - post.body, + body, textAlign: TextAlign.justify, ), const SizedBox(height: 10), - Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ActionButton( - icon: CupertinoIcons.hand_thumbsup, - onTap: () {}, - ), - ActionButton( - icon: CupertinoIcons.bubble_left, - onTap: () {}, - ), - ], - ), + if (showActionBar) Divider(), + if (showActionBar) + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ActionButton( + icon: CupertinoIcons.hand_thumbsup, + onTap: onLikeTap, + ), + ActionButton( + icon: CupertinoIcons.bubble_left, + onTap: onCommentTap, + ), + ], + ), ], ), ); diff --git a/lib/presentation/pages/comments/comments_page.dart b/lib/presentation/pages/comments/comments_page.dart new file mode 100644 index 0000000..519bb7e --- /dev/null +++ b/lib/presentation/pages/comments/comments_page.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../bloc/comment/comment_cubit.dart'; +import '../../global/data_item.dart'; + +class CommentsPage extends StatelessWidget { + const CommentsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final commentState = context.watch().state; + + late final child; + + if (commentState.isInProgress) { + child = Center(child: CircularProgressIndicator()); + } else if (commentState.isSuccess) { + child = ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8.0), + itemBuilder: (_, i) { + final comment = commentState.data?[i]; + + return DataItem( + id: comment!.postId, + body: comment.body, + showActionBar: false, + ); + }, + itemCount: commentState.data!.length, + ); + } else if (commentState.isFailure) { + child = Center(child: Text('ERROR!')); + } + + return Scaffold( + appBar: AppBar( + title: Text( + 'Comments', + ), + ), + body: SafeArea(child: child), + ); + } +} diff --git a/lib/presentation/pages/posts/posts_page.dart b/lib/presentation/pages/posts/posts_page.dart index 0d779a8..0ba58dc 100644 --- a/lib/presentation/pages/posts/posts_page.dart +++ b/lib/presentation/pages/posts/posts_page.dart @@ -1,37 +1,34 @@ -import 'package:architecture_example/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../../bloc/post/post_cubit.dart'; +import '../../../utils/constants/routes.dart'; +import '../../../utils/extensions/theme_ext.dart'; import '../../../utils/extensions/waitable_cubit_ext.dart'; +import '../../global/data_item.dart'; import 'widgets/home_bottom_bar.dart'; -import 'widgets/post_item.dart'; +import 'widgets/more_menu.dart'; class PostsPage extends StatelessWidget { @override Widget build(BuildContext context) { - final mainColor = Theme.of(context).brightness == Brightness.dark - ? Theme.of(context).cardColor - : Theme.of(context).primaryColorDark; - return AnnotatedRegion( value: SystemUiOverlayStyle( - statusBarColor: mainColor, + statusBarColor: Theme.of(context).statusBarColor, + statusBarIconBrightness: Brightness.light, ), child: Scaffold( body: SafeArea( child: CustomScrollView( slivers: [ SliverAppBar( - title: Text('Posts'), - backgroundColor: mainColor, - actions: [ - IconButton( - icon: Icon(Icons.more_vert), - onPressed: () {}, - ), - ], + title: Text( + AppLocalizations.of(context)!.posts, + ), + backgroundColor: Theme.of(context).statusBarColor, + actions: [MoreMenu()], ), SliverFillRemaining( child: RefreshIndicator( @@ -58,7 +55,16 @@ class PostsPage extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8.0), itemBuilder: (_, i) { final post = posts[i]; - return PostItem(post: post); + + return DataItem( + id: post.userId, + body: post.body, + onCommentTap: () => Navigator.pushNamed( + context, + Routes.comments, + arguments: post.id, + ), + ); }, itemCount: posts.length, ); @@ -68,7 +74,7 @@ class PostsPage extends StatelessWidget { return Text('Error occurred!'); } - return Expanded( + return Center( child: Container( color: Colors.white, ), diff --git a/lib/presentation/pages/posts/widgets/action_button.dart b/lib/presentation/pages/posts/widgets/action_button.dart index 4b50777..02ef979 100644 --- a/lib/presentation/pages/posts/widgets/action_button.dart +++ b/lib/presentation/pages/posts/widgets/action_button.dart @@ -14,7 +14,7 @@ class ActionButton extends StatelessWidget { Widget build(BuildContext context) { return Expanded( child: Material( - color: Theme.of(context).cardColor, + color: Theme.of(context).cardColor.withOpacity(0.0), child: InkWell( onTap: onTap, child: Padding( diff --git a/lib/presentation/pages/posts/widgets/home_bottom_bar.dart b/lib/presentation/pages/posts/widgets/home_bottom_bar.dart index dbb1435..e7e3928 100644 --- a/lib/presentation/pages/posts/widgets/home_bottom_bar.dart +++ b/lib/presentation/pages/posts/widgets/home_bottom_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class HomeBottomBar extends StatefulWidget { @override @@ -19,11 +20,11 @@ class _HomeBottomBarState extends State { }, items: [ BottomNavigationBarItem( - label: 'Posts', + label: AppLocalizations.of(context)!.posts, icon: Icon(Icons.list_alt), ), BottomNavigationBarItem( - label: 'Profile', + label: AppLocalizations.of(context)!.profile, icon: Icon(Icons.account_circle_rounded), ), ], diff --git a/lib/presentation/pages/posts/widgets/more_menu.dart b/lib/presentation/pages/posts/widgets/more_menu.dart new file mode 100644 index 0000000..64db7ad --- /dev/null +++ b/lib/presentation/pages/posts/widgets/more_menu.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../../../../utils/constants/routes.dart'; + +class MoreMenu extends StatelessWidget { + const MoreMenu({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + color: Theme.of(context).cardColor, + onSelected: (String value) { + print('onSelected: $value'); + if (value == 'Settings') { + Navigator.of(context).pushNamed(Routes.settings); + } else if (value == 'About') {} + }, + itemBuilder: (_) { + return >[ + PopupMenuItem( + value: 'Settings', + child: Text('Settings'), + ), + PopupMenuDivider(), + PopupMenuItem( + value: 'About', + child: Text('About'), + ), + ]; + }, + ); + } +} diff --git a/lib/presentation/pages/settings/settings_page.dart b/lib/presentation/pages/settings/settings_page.dart new file mode 100644 index 0000000..12a02c2 --- /dev/null +++ b/lib/presentation/pages/settings/settings_page.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../utils/extensions/theme_ext.dart'; +import 'widgets/language_selection.dart'; +import 'widgets/theme_selection.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({Key? key}) : super(key: key); + + @override + _SettingsPageState createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + bool isLangExpanded = false; + bool isThemeExpanded = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Settings'), + backwardsCompatibility: false, + backgroundColor: Theme.of(context).statusBarColor, + brightness: Brightness.light, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Theme.of(context).statusBarColor, + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(8.0), + child: Container( + child: ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + setState(() { + if (index == 0) { + isLangExpanded = !isExpanded; + } else if (index == 1) { + isThemeExpanded = !isExpanded; + } + }); + }, + children: [ + ExpansionPanel( + isExpanded: isLangExpanded, + headerBuilder: (_, isExpanded) { + return ListTile( + title: Text('Language'), + ); + }, + body: LanguageSelection(), + ), + ExpansionPanel( + isExpanded: isThemeExpanded, + headerBuilder: (_, isExpanded) { + return ListTile( + title: Text('Theme'), + ); + }, + body: ThemeSelection(), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/pages/settings/widgets/language_selection.dart b/lib/presentation/pages/settings/widgets/language_selection.dart new file mode 100644 index 0000000..ae29085 --- /dev/null +++ b/lib/presentation/pages/settings/widgets/language_selection.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../bloc/language/language_cubit.dart'; + +class LanguageSelection extends StatelessWidget { + const LanguageSelection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final languageCubit = context.watch(); + final locale = languageCubit.state; + int index = 0; + + if (locale.languageCode == 'az') { + index = 1; + } + + return Column( + children: [ + RadioListTile( + value: 0, + title: Text('English'), + groupValue: index, + onChanged: (index) { + languageCubit.changeLocale(Locale('en', 'US')); + }, + ), + RadioListTile( + value: 1, + title: Text('Azerbaijan'), + groupValue: index, + onChanged: (index) { + languageCubit.changeLocale(Locale('az', 'AZ')); + }, + ), + ], + ); + } +} diff --git a/lib/presentation/pages/settings/widgets/theme_selection.dart b/lib/presentation/pages/settings/widgets/theme_selection.dart new file mode 100644 index 0000000..7d04f2b --- /dev/null +++ b/lib/presentation/pages/settings/widgets/theme_selection.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../bloc/theme/theme_cubit.dart'; + +class ThemeSelection extends StatelessWidget { + const ThemeSelection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final themeCubit = context.watch(); + final themeMode = themeCubit.state; + + return Column( + children: [ + RadioListTile( + value: 0, + title: Text('System default'), + groupValue: themeMode.index, + onChanged: (index) { + themeCubit.changeTheme(ThemeMode.system); + }, + ), + RadioListTile( + value: 1, + title: Text('Light'), + groupValue: themeMode.index, + onChanged: (index) { + themeCubit.changeTheme(ThemeMode.light); + }, + ), + RadioListTile( + value: 2, + title: Text('Dark'), + groupValue: themeMode.index, + onChanged: (index) { + themeCubit.changeTheme(ThemeMode.dark); + }, + ), + ], + ); + } +} diff --git a/lib/presentation/pages/splash/splash_page.dart b/lib/presentation/pages/splash/splash_page.dart new file mode 100644 index 0000000..3fd8618 --- /dev/null +++ b/lib/presentation/pages/splash/splash_page.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../../../utils/constants/routes.dart'; + +class SplashPage extends StatefulWidget { + @override + _SplashPageState createState() => _SplashPageState(); +} + +class _SplashPageState extends State { + late Timer? timer; + + @override + void initState() { + super.initState(); + + timer = Timer.periodic(Duration(seconds: 2), (_) { + timer?.cancel(); + Navigator.popAndPushNamed(context, Routes.posts); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).primaryColor, + body: Center( + child: Text( + 'Splash Page', + style: TextStyle( + color: Colors.white, + fontSize: 20, + ), + ), + ), + ); + } + + @override + void dispose() { + timer?.cancel(); + timer = null; + super.dispose(); + } +} diff --git a/lib/presentation/router/.gitkeep b/lib/presentation/router/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/lib/presentation/router/route_controller.dart b/lib/presentation/router/route_controller.dart new file mode 100644 index 0000000..0eadb23 --- /dev/null +++ b/lib/presentation/router/route_controller.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../bloc/comment/comment_cubit.dart'; +import '../../bloc/post/post_cubit.dart'; +import '../../data/repositories/comment_repository.dart'; +import '../../data/repositories/post_repository.dart'; +import '../../utils/constants/routes.dart'; +import '../pages/comments/comments_page.dart'; +import '../pages/posts/posts_page.dart'; +import '../pages/settings/settings_page.dart'; + +class RouteController { + RouteController._(); + + static Route? onGenerateRoute(RouteSettings settings) { + switch (settings.name) { + case Routes.posts: + return MaterialPageRoute( + builder: (_) => RepositoryProvider( + create: (_) => PostRepository(), + child: BlocProvider( + create: (context) => PostCubit( + context.read(), + )..fetch(), + child: PostsPage(), + ), + ), + ); + case Routes.comments: + final postId = settings.arguments as int; + + return MaterialPageRoute( + builder: (_) => RepositoryProvider( + create: (_) => CommentRepository(), + child: BlocProvider( + create: (context) => CommentCubit( + context.read(), + )..fetch(postId), + child: CommentsPage(), + ), + ), + ); + case Routes.settings: + return MaterialPageRoute( + builder: (_) => SettingsPage(), + ); + default: + throw UnimplementedError('Routes is not defined for ${settings.name}!'); + } + } +} diff --git a/lib/constants/app_colors.dart b/lib/utils/constants/app_colors.dart similarity index 100% rename from lib/constants/app_colors.dart rename to lib/utils/constants/app_colors.dart diff --git a/lib/utils/constants/app_text_styles.dart b/lib/utils/constants/app_text_styles.dart new file mode 100644 index 0000000..02b8e3e --- /dev/null +++ b/lib/utils/constants/app_text_styles.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class AppTextStyles { + AppTextStyles._(); + + // Poppins - 12 w500 + // Poppins - 12 w500 + // Poppins - 16 w600 + + static final poppinsw500 = TextStyle(fontWeight: FontWeight.w500); + static final poppinsw600 = TextStyle(fontWeight: FontWeight.w600); +} diff --git a/lib/constants/app_themes.dart b/lib/utils/constants/app_themes.dart similarity index 84% rename from lib/constants/app_themes.dart rename to lib/utils/constants/app_themes.dart index aa24767..5b8bb60 100644 --- a/lib/constants/app_themes.dart +++ b/lib/utils/constants/app_themes.dart @@ -1,3 +1,4 @@ +import 'package:architecture_example/utils/constants/app_text_styles.dart'; import 'package:flutter/material.dart'; import 'app_colors.dart'; @@ -14,6 +15,9 @@ abstract class AppThemes { bottomNavigationBarTheme: BottomNavigationBarThemeData( selectedItemColor: AppColors.primaryColor, ), + textTheme: TextTheme( + bodyText1: AppTextStyles.poppinsw500.copyWith(fontSize: 16), + ), ); static final darkTheme = ThemeData( diff --git a/lib/utils/constants/routes.dart b/lib/utils/constants/routes.dart new file mode 100644 index 0000000..d32652e --- /dev/null +++ b/lib/utils/constants/routes.dart @@ -0,0 +1,10 @@ +class Routes { + Routes._(); + + static const String splash = '/splash'; + static const String signIn = '/signIn'; + static const String posts = '/posts'; + static const String comments = '/comments'; + static const String settings = '/settings'; + static const String about = '/about'; +} diff --git a/lib/constants/supported_locales.dart b/lib/utils/constants/supported_locales.dart similarity index 100% rename from lib/constants/supported_locales.dart rename to lib/utils/constants/supported_locales.dart diff --git a/lib/utils/extensions/theme_ext.dart b/lib/utils/extensions/theme_ext.dart new file mode 100644 index 0000000..a9fc13c --- /dev/null +++ b/lib/utils/extensions/theme_ext.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +extension ThemeExt on ThemeData { + Color get statusBarColor => + brightness == Brightness.dark ? cardColor : primaryColorDark; +} diff --git a/lib/utils/mixins/walk_mixin.dart b/lib/utils/mixins/walk_mixin.dart new file mode 100644 index 0000000..b3361cc --- /dev/null +++ b/lib/utils/mixins/walk_mixin.dart @@ -0,0 +1,15 @@ +mixin WalkMixin { + void walk() {} +} + +mixin SwimMixin { + void swim() {} +} + +class APerson with WalkMixin, SwimMixin {} + +void test() { + final a = APerson(); + a.walk(); + a.swim(); +} diff --git a/test/mocks/mock_preferences_service.dart b/test/mocks/mock_preferences_service.dart index 5bbe74b..0c52cb6 100644 --- a/test/mocks/mock_preferences_service.dart +++ b/test/mocks/mock_preferences_service.dart @@ -1,4 +1,4 @@ -import 'package:architecture_example/contractors/base_preferences_service.dart'; +import 'package:architecture_example/data/contractors/base_preferences_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/src/material/app.dart'; import 'package:shared_preferences/shared_preferences.dart';