diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 00000000..d077b08b --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,69 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def parse_KV_file(file, separator='=') + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return []; + end + pods_ary = [] + skip_line_start_symbols = ["#", "/"] + File.foreach(file_abs_path) { |line| + next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } + plugin = line.split(pattern=separator) + if plugin.length == 2 + podname = plugin[0].strip() + path = plugin[1].strip() + podpath = File.expand_path("#{path}", file_abs_path) + pods_ary.push({:name => podname, :path => podpath}); + else + puts "Invalid plugin specification: #{line}" + end + } + return pods_ary +end + +target 'Runner' do + # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock + # referring to absolute paths on developers' machines. + system('rm -rf .symlinks') + system('mkdir -p .symlinks/plugins') + + # Flutter Pods + generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') + if generated_xcode_build_settings.empty? + puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." + end + generated_xcode_build_settings.map { |p| + if p[:name] == 'FLUTTER_FRAMEWORK_DIR' + symlink = File.join('.symlinks', 'flutter') + File.symlink(File.dirname(p[:path]), symlink) + pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) + end + } + + # Plugin Pods + plugin_pods = parse_KV_file('../.flutter-plugins') + plugin_pods.map { |p| + symlink = File.join('.symlinks', 'plugins', p[:name]) + File.symlink(p[:path], symlink) + pod p[:name], :path => File.join(symlink, 'ios') + } +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['ENABLE_BITCODE'] = 'NO' + end + end +end diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..949b6789 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + BuildSystemType + Original + + diff --git a/lib/constants/app_theme.dart b/lib/constants/app_theme.dart index 531d7dfc..fb2884be 100644 --- a/lib/constants/app_theme.dart +++ b/lib/constants/app_theme.dart @@ -23,16 +23,23 @@ import 'package:flutter/material.dart'; -final ThemeData themeData = new ThemeData( +final ThemeData themeData = ThemeData( fontFamily: 'ProductSans', brightness: Brightness.light, - primarySwatch: MaterialColor( - AppColors.green[500].value, AppColors.green), + primarySwatch: MaterialColor(AppColors.green[500].value, AppColors.green), primaryColor: AppColors.green[500], primaryColorBrightness: Brightness.light, accentColor: AppColors.green[500], - accentColorBrightness: Brightness.light -); + accentColorBrightness: Brightness.light); + +final ThemeData themeDataDark = ThemeData( + fontFamily: 'ProductSans', + brightness: Brightness.dark, + primarySwatch: MaterialColor(AppColors.green[900].value, AppColors.green), + primaryColor: AppColors.green[500], + primaryColorBrightness: Brightness.dark, + accentColor: AppColors.green[500], + accentColorBrightness: Brightness.dark); class AppColors { AppColors._(); // this basically makes it so you can instantiate this class @@ -49,4 +56,4 @@ class AppColors { 800: const Color(0xFF76af60), 900: const Color(0xFF64a24d) }; -} \ No newline at end of file +} diff --git a/lib/constants/dimens.dart b/lib/constants/dimens.dart index 20b774d9..d509316e 100644 --- a/lib/constants/dimens.dart +++ b/lib/constants/dimens.dart @@ -4,4 +4,7 @@ class Dimens { //for all screens static const double horizontal_padding = 12.0; static const double vertical_padding = 12.0; -} \ No newline at end of file + + static const double tablet_breakpoint = 720.0; + static const double tablet_list_width = 400.0; +} diff --git a/lib/constants/index.dart b/lib/constants/index.dart new file mode 100644 index 00000000..961e1f8c --- /dev/null +++ b/lib/constants/index.dart @@ -0,0 +1,2 @@ +export 'app_theme.dart'; +export 'dimens.dart'; diff --git a/lib/constants/strings.dart b/lib/constants/strings.dart deleted file mode 100644 index 2c049d91..00000000 --- a/lib/constants/strings.dart +++ /dev/null @@ -1,12 +0,0 @@ -class Strings { - Strings._(); - - //General - static const String appName = "Boilerplate Project"; - - //Login - static const String login_et_user_email = "Enter user email"; - static const String login_et_user_password = "Enter password"; - static const String login_btn_forgot_password = "Forgot Password?"; - static const String login_btn_sign_in = "Sign In"; -} diff --git a/lib/data/index.dart b/lib/data/index.dart new file mode 100644 index 00000000..e06c9336 --- /dev/null +++ b/lib/data/index.dart @@ -0,0 +1 @@ +export 'repository.dart'; diff --git a/lib/data/local/constants/index.dart b/lib/data/local/constants/index.dart new file mode 100644 index 00000000..a8f34230 --- /dev/null +++ b/lib/data/local/constants/index.dart @@ -0,0 +1 @@ +export 'db_constants.dart'; diff --git a/lib/data/local/datasources/post/index.dart b/lib/data/local/datasources/post/index.dart new file mode 100644 index 00000000..340edc22 --- /dev/null +++ b/lib/data/local/datasources/post/index.dart @@ -0,0 +1 @@ +export 'post_datasource.dart'; diff --git a/lib/data/local/datasources/post/post_datasource.dart b/lib/data/local/datasources/post/post_datasource.dart index 543ea155..3e2e0d5e 100644 --- a/lib/data/local/datasources/post/post_datasource.dart +++ b/lib/data/local/datasources/post/post_datasource.dart @@ -85,4 +85,4 @@ class PostDataSource { return post; }).toList(); } -} \ No newline at end of file +} diff --git a/lib/data/local/index.dart b/lib/data/local/index.dart new file mode 100644 index 00000000..b930557d --- /dev/null +++ b/lib/data/local/index.dart @@ -0,0 +1 @@ +export 'app_database.dart'; diff --git a/lib/data/network/apis/posts/index.dart b/lib/data/network/apis/posts/index.dart new file mode 100644 index 00000000..9d952e97 --- /dev/null +++ b/lib/data/network/apis/posts/index.dart @@ -0,0 +1 @@ +export 'post_api.dart'; diff --git a/lib/data/network/apis/posts/post_api.dart b/lib/data/network/apis/posts/post_api.dart index 43d63c90..093974b3 100644 --- a/lib/data/network/apis/posts/post_api.dart +++ b/lib/data/network/apis/posts/post_api.dart @@ -25,14 +25,13 @@ class PostApi { /// Returns list of post in response Future getPosts() { - return _dioClient .get(Endpoints.getPosts) .then((dynamic res) => PostsList.fromJson(res)) .catchError((error) => throw error); } -/// sample api call with default rest client + /// sample api call with default rest client // Future getPosts() { // // return _restClient diff --git a/lib/data/network/constants/endpoints.dart b/lib/data/network/constants/endpoints.dart index 25fe2584..e260482b 100644 --- a/lib/data/network/constants/endpoints.dart +++ b/lib/data/network/constants/endpoints.dart @@ -12,4 +12,4 @@ class Endpoints { // booking endpoints static const String getPosts = baseUrl + "/posts"; -} \ No newline at end of file +} diff --git a/lib/data/network/constants/index.dart b/lib/data/network/constants/index.dart new file mode 100644 index 00000000..bf657c52 --- /dev/null +++ b/lib/data/network/constants/index.dart @@ -0,0 +1 @@ +export 'endpoints.dart'; diff --git a/lib/data/network/dio_client.dart b/lib/data/network/dio_client.dart index 7b2c3cfd..92cf2ad9 100644 --- a/lib/data/network/dio_client.dart +++ b/lib/data/network/dio_client.dart @@ -24,7 +24,6 @@ final Dio dio = new Dio() })); class DioClient { - // singleton object static final DioClient _singleton = DioClient._(); @@ -37,7 +36,7 @@ class DioClient { // Singleton accessor static DioClient get instance => DioClient(); - + // Get:----------------------------------------------------------------------- Future get(String uri) async { try { diff --git a/lib/data/network/exceptions/index.dart b/lib/data/network/exceptions/index.dart new file mode 100644 index 00000000..608e3305 --- /dev/null +++ b/lib/data/network/exceptions/index.dart @@ -0,0 +1 @@ +export 'network_exceptions.dart'; diff --git a/lib/data/network/exceptions/network_exceptions.dart b/lib/data/network/exceptions/network_exceptions.dart index f0ba1f8d..5400292e 100644 --- a/lib/data/network/exceptions/network_exceptions.dart +++ b/lib/data/network/exceptions/network_exceptions.dart @@ -5,5 +5,6 @@ class NetworkException implements Exception { } class AuthException extends NetworkException { - AuthException({message, statusCode}) : super(message: message, statusCode: statusCode); -} \ No newline at end of file + AuthException({message, statusCode}) + : super(message: message, statusCode: statusCode); +} diff --git a/lib/data/network/index.dart b/lib/data/network/index.dart new file mode 100644 index 00000000..16ebca69 --- /dev/null +++ b/lib/data/network/index.dart @@ -0,0 +1,2 @@ +export 'dio_client.dart'; +export 'rest_client.dart'; diff --git a/lib/data/network/rest_client.dart b/lib/data/network/rest_client.dart index ebfcc559..01549bb5 100644 --- a/lib/data/network/rest_client.dart +++ b/lib/data/network/rest_client.dart @@ -14,10 +14,10 @@ class RestClient { // factory method to return the same object each time its needed factory RestClient() => _singleton; - + // Singleton accessor static RestClient get instance => RestClient(); - + // instantiate json decoder for json serialization final JsonDecoder _decoder = JsonDecoder(); @@ -28,7 +28,8 @@ class RestClient { final int statusCode = response.statusCode; if (statusCode < 200 || statusCode > 400 || json == null) { - throw NetworkException(message:"Error fetching data from server", statusCode: statusCode); + throw NetworkException( + message: "Error fetching data from server", statusCode: statusCode); } print(res); @@ -45,7 +46,8 @@ class RestClient { final int statusCode = response.statusCode; if (statusCode < 200 || statusCode > 400 || json == null) { - throw NetworkException(message:"Error fetching data from server", statusCode: statusCode); + throw NetworkException( + message: "Error fetching data from server", statusCode: statusCode); } return _decoder.convert(res); }); diff --git a/lib/data/repository.dart b/lib/data/repository.dart index 9409c66d..7906a223 100644 --- a/lib/data/repository.dart +++ b/lib/data/repository.dart @@ -39,14 +39,12 @@ class Repository { .catchError((error) => throw error); Future> findPostById(int id) { - //creating filter List filters = List(); //check to see if dataLogsType is not null if (id != null) { - Filter dataLogTypeFilter = - Filter.equal(DBConstants.FIELD_ID, id); + Filter dataLogTypeFilter = Filter.equal(DBConstants.FIELD_ID, id); filters.add(dataLogTypeFilter); } diff --git a/lib/data/sharedpref/constants/index.dart b/lib/data/sharedpref/constants/index.dart new file mode 100644 index 00000000..38d4eaf1 --- /dev/null +++ b/lib/data/sharedpref/constants/index.dart @@ -0,0 +1 @@ +export 'preferences.dart'; diff --git a/lib/data/sharedpref/constants/preferences.dart b/lib/data/sharedpref/constants/preferences.dart index 7eaaf94b..8d16edcf 100644 --- a/lib/data/sharedpref/constants/preferences.dart +++ b/lib/data/sharedpref/constants/preferences.dart @@ -3,4 +3,4 @@ class Preferences { static const String is_logged_in = "isLoggedIn"; static const String auth_token = "authToken"; -} \ No newline at end of file +} diff --git a/lib/locale/index.dart b/lib/locale/index.dart new file mode 100644 index 00000000..e51b0a22 --- /dev/null +++ b/lib/locale/index.dart @@ -0,0 +1 @@ +export 'localizations.dart'; diff --git a/lib/locale/localizations.dart b/lib/locale/localizations.dart new file mode 100644 index 00000000..4d3e90f9 --- /dev/null +++ b/lib/locale/localizations.dart @@ -0,0 +1,106 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// A simple "rough and ready" example of localizing a Flutter app. +// Spanish and English (locale language codes 'en' and 'es') are +// supported. + +// The pubspec.yaml file must include flutter_localizations in its +// dependencies section. For example: +// +// dependencies: +// flutter: +// sdk: flutter +// flutter_localizations: +// sdk: flutter + +// If you run this app with the device's locale set to anything but +// English or Spanish, the app's locale will be English. If you +// set the device's locale to Spanish, the app's locale will be +// Spanish. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show SynchronousFuture; + +class AppLocalizations { + AppLocalizations(this.locale); + + final Locale locale; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static Map> _localizedValues = { + 'en': { + 'title': 'Boilerplate Project', + 'login_et_user_email': 'Enter user email', + 'login_et_user_password': 'Enter password', + 'login_btn_forgot_password': 'Forgot Password?', + 'login_btn_sign_in': 'Sign In', + 'login_validation_error': 'Please fill in all fields', + 'posts_title': 'Posts', + 'posts_not_found': 'No Posts Found', + 'settings_title': 'Settings', + }, + 'es': { + 'title': 'Proyecto repetitivo', + 'login_et_user_email': 'Ingrese el email del usuario', + 'login_et_user_password': 'introducir la contraseña', + 'login_btn_forgot_password': 'Se te olvidó tu contraseña', + 'login_btn_sign_in': 'Registrarse', + 'login_validation_error': 'Por favor rellena todos los campos', + 'posts_title': 'Mensajes', + 'posts_not_found': 'No se han encontrado publicacionesd', + 'settings_title': 'Ajustes', + }, + }; + + String get title => _localizedValues[locale.languageCode]['title']; + String get login_et_user_email => + _localizedValues[locale.languageCode]['login_et_user_email']; + String get login_et_user_password => + _localizedValues[locale.languageCode]['login_et_user_password']; + String get login_btn_forgot_password => + _localizedValues[locale.languageCode]['login_btn_forgot_password']; + String get login_btn_sign_in => + _localizedValues[locale.languageCode]['login_btn_sign_in']; + String get login_validation_error => + _localizedValues[locale.languageCode]['login_validation_error']; + String get posts_title => + _localizedValues[locale.languageCode]['posts_title']; + String get posts_not_found => + _localizedValues[locale.languageCode]['posts_not_found']; + String get settings_title => + _localizedValues[locale.languageCode]['settings_title']; +} + +class AppLocalizationsDelegate extends LocalizationsDelegate { + const AppLocalizationsDelegate(); + + static List get supportedLocales => [ + Locale('en', ''), + Locale('es', ''), + ]; + + @override + bool isSupported(Locale locale) { + return supportedLocales + .map((l) => l.languageCode) + .toList() + .contains(locale.languageCode); + } + + @override + Future load(Locale locale) { + // Returning a SynchronousFuture here because an async "load" operation + // isn't needed to produce an instance of AppLocalizations. + return SynchronousFuture(AppLocalizations(locale)); + } + + @override + bool shouldReload(AppLocalizationsDelegate old) => false; +} diff --git a/lib/main.dart b/lib/main.dart index 5e2ab6ad..77f12972 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,10 @@ -import 'package:boilerplate/routes.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'constants/app_theme.dart'; -import 'constants/strings.dart'; +import 'locale/index.dart'; +import 'routes.dart'; import 'ui/splash/splash.dart'; void main() { @@ -22,9 +23,19 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + localizationsDelegates: [ + // ... app-specific localization delegate[s] here + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + const AppLocalizationsDelegate(), + ], + supportedLocales: AppLocalizationsDelegate.supportedLocales, debugShowCheckedModeBanner: false, - title: Strings.appName, + onGenerateTitle: (BuildContext context) { + return AppLocalizations.of(context).title; + }, theme: themeData, + darkTheme: themeDataDark, routes: Routes.routes, home: SplashScreen(), ); diff --git a/lib/models/post/index.dart b/lib/models/post/index.dart new file mode 100644 index 00000000..d6c0a6ab --- /dev/null +++ b/lib/models/post/index.dart @@ -0,0 +1,2 @@ +export 'post.dart'; +export 'post_list.dart'; diff --git a/lib/models/post/post.dart b/lib/models/post/post.dart index aa9acd2b..317c9e5b 100644 --- a/lib/models/post/post.dart +++ b/lib/models/post/post.dart @@ -1,4 +1,3 @@ - class Post { int userId; int id; @@ -25,5 +24,4 @@ class Post { "title": title, "body": body, }; - } diff --git a/lib/routes.dart b/lib/routes.dart index b4fbe8bb..1694def1 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'ui/home/home.dart'; import 'ui/login/login.dart'; +import 'ui/navigation.dart'; +import 'ui/settings/settings.dart'; import 'ui/splash/splash.dart'; class Routes { @@ -15,9 +17,6 @@ class Routes { static final routes = { splash: (BuildContext context) => SplashScreen(), login: (BuildContext context) => LoginScreen(), - home: (BuildContext context) => HomeScreen(), + home: (BuildContext context) => AppNavigation(), }; } - - - diff --git a/lib/stores/error/error_store.dart b/lib/stores/error/error_store.dart index de18af79..b7c81d38 100644 --- a/lib/stores/error/error_store.dart +++ b/lib/stores/error/error_store.dart @@ -5,11 +5,10 @@ part 'error_store.g.dart'; class ErrorStore = _ErrorStore with _$ErrorStore; abstract class _ErrorStore implements Store { - // store variables:----------------------------------------------------------- @observable String errorMessage; @observable bool showError = false; -} \ No newline at end of file +} diff --git a/lib/stores/form/form_store.dart b/lib/stores/form/form_store.dart index 1a73c1bb..5498a0cb 100644 --- a/lib/stores/form/form_store.dart +++ b/lib/stores/form/form_store.dart @@ -50,7 +50,9 @@ abstract class _FormStore implements Store { @computed bool get canLogin => - !formErrorStore.hasErrorsInLogin && userEmail.isNotEmpty && password.isNotEmpty; + !formErrorStore.hasErrorsInLogin && + userEmail.isNotEmpty && + password.isNotEmpty; @computed bool get canRegister => diff --git a/lib/stores/post/post_store.dart b/lib/stores/post/post_store.dart index cd01acf8..d59d4aa3 100644 --- a/lib/stores/post/post_store.dart +++ b/lib/stores/post/post_store.dart @@ -9,7 +9,6 @@ part 'post_store.g.dart'; class PostStore = _PostStore with _$PostStore; abstract class _PostStore implements Store { - // store for handling errors final ErrorStore errorStore = ErrorStore(); @@ -41,4 +40,4 @@ abstract class _PostStore implements Store { print(e); }); } -} \ No newline at end of file +} diff --git a/lib/ui/home/home.dart b/lib/ui/home/home.dart index 232ec470..cfbd2131 100644 --- a/lib/ui/home/home.dart +++ b/lib/ui/home/home.dart @@ -1,12 +1,16 @@ -import 'package:boilerplate/data/sharedpref/constants/preferences.dart'; -import 'package:boilerplate/routes.dart'; -import 'package:boilerplate/stores/post/post_store.dart'; -import 'package:boilerplate/widgets/progress_indicator_widget.dart'; +import 'package:boilerplate/constants/index.dart'; +import 'package:boilerplate/models/post/index.dart'; import 'package:flushbar/flushbar_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../data/sharedpref/constants/preferences.dart'; +import '../../locale/index.dart'; +import '../../routes.dart'; +import '../../stores/post/post_store.dart'; +import '../../widgets/progress_indicator_widget.dart'; + class HomeScreen extends StatefulWidget { @override _HomeScreenState createState() => _HomeScreenState(); @@ -24,56 +28,86 @@ class _HomeScreenState extends State { _store.getPosts(); } + int _selectedIndex = 0; @override Widget build(BuildContext context) { - return Scaffold( - appBar: _buildAppBar(context), - body: _buildBody(), - ); - } - - Widget _buildAppBar(BuildContext context) { - return AppBar( - title: Text('Posts'), - actions: [ - IconButton( - onPressed: () { - SharedPreferences.getInstance().then((preference) { - preference.setBool(Preferences.is_logged_in, false); - Navigator.of(context).pushReplacementNamed(Routes.login); - }); - }, - icon: Icon( - Icons.power_settings_new, - ), - ) - ], - ); - } - - Widget _buildBody() { - return Stack( - children: [ - Observer( - builder: (context) { - return _store.loading - ? CustomProgressIndicatorWidget() - : Material(child: _buildListView()); - }, - ), - Observer( - name: 'error', - builder: (context) { - return _store.success - ? Container() - : showErrorMessage(context, _store.errorStore.errorMessage); - }, - ) - ], + return LayoutBuilder( + builder: (context, dimens) { + if (MediaQuery.of(context).orientation == Orientation.landscape && + dimens.maxWidth >= Dimens.tablet_breakpoint) { + return Row( + children: [ + Container( + width: Dimens.tablet_list_width, + child: Stack( + children: [ + Observer( + builder: (context) { + return _store.loading + ? CustomProgressIndicatorWidget() + : Material( + child: _buildListView((val) { + if (mounted) + setState(() { + _selectedIndex = val; + }); + }, true)); + }, + ), + Observer( + name: 'error', + builder: (context) { + return _store.success + ? Container() + : showErrorMessage( + context, _store.errorStore.errorMessage); + }, + ) + ], + ), + ), + Expanded( + child: PostDetailsScreen( + post: _store.postsList.posts[_selectedIndex], + showAppBar: false, + ), + ), + ], + ); + } + return Stack( + children: [ + Observer( + builder: (context) { + return _store.loading + ? CustomProgressIndicatorWidget() + : Material( + child: _buildListView((val) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => PostDetailsScreen( + post: _store.postsList.posts[_selectedIndex], + ), + ), + ); + }, false)); + }, + ), + Observer( + name: 'error', + builder: (context) { + return _store.success + ? Container() + : showErrorMessage(context, _store.errorStore.errorMessage); + }, + ) + ], + ); + }, ); } - Widget _buildListView() { + Widget _buildListView(ValueChanged selected, bool tablet) { return _store.postsList != null ? ListView.separated( itemCount: _store.postsList.posts.length, @@ -82,6 +116,7 @@ class _HomeScreenState extends State { }, itemBuilder: (context, position) { return ListTile( + selected: tablet ? _selectedIndex == position : false, leading: Icon(Icons.cloud_circle), title: Text( '${_store.postsList.posts[position].title}', @@ -96,10 +131,11 @@ class _HomeScreenState extends State { overflow: TextOverflow.ellipsis, softWrap: false, ), + onTap: () => selected(position), ); }, ) - : Center(child: Text('No posts found')); + : Center(child: Text(AppLocalizations.of(context).posts_not_found)); } // General Methods:----------------------------------------------------------- @@ -117,3 +153,39 @@ class _HomeScreenState extends State { return Container(); } } + +class PostDetailsScreen extends StatelessWidget { + const PostDetailsScreen({ + @required this.post, + this.showAppBar = true, + }); + final Post post; + final bool showAppBar; + @override + Widget build(BuildContext context) { + final _textTheme = Theme.of(context).textTheme; + return Scaffold( + appBar: showAppBar + ? AppBar( + title: Text('Details'), + ) + : null, + body: ListView( + children: [ + ListTile( + title: Text( + post.title, + style: _textTheme.title, + ), + ), + ListTile( + title: Text( + post.body, + style: _textTheme.subtitle, + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/login/login.dart b/lib/ui/login/login.dart index a5b1e6b2..cd24cae1 100644 --- a/lib/ui/login/login.dart +++ b/lib/ui/login/login.dart @@ -1,16 +1,18 @@ -import 'package:boilerplate/constants/strings.dart'; -import 'package:boilerplate/data/sharedpref/constants/preferences.dart'; -import 'package:boilerplate/routes.dart'; -import 'package:boilerplate/stores/form/form_store.dart'; -import 'package:boilerplate/widgets/app_icon_widget.dart'; -import 'package:boilerplate/widgets/empty_app_bar_widget.dart'; -import 'package:boilerplate/widgets/progress_indicator_widget.dart'; -import 'package:boilerplate/widgets/rounded_button_widget.dart'; -import 'package:boilerplate/widgets/textfield_widget.dart'; +import 'package:boilerplate/constants/index.dart'; +import 'package:flushbar/flushbar_helper.dart'; import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flushbar/flushbar_helper.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../data/sharedpref/constants/preferences.dart'; +import '../../locale/index.dart'; +import '../../routes.dart'; +import '../../stores/form/form_store.dart'; +import '../../widgets/app_icon_widget.dart'; +import '../../widgets/empty_app_bar_widget.dart'; +import '../../widgets/progress_indicator_widget.dart'; +import '../../widgets/rounded_button_widget.dart'; +import '../../widgets/textfield_widget.dart'; class LoginScreen extends StatefulWidget { @override @@ -69,29 +71,27 @@ class _LoginScreenState extends State { return Material( child: Stack( children: [ - OrientationBuilder( - builder: (context, orientation) { - //variable to hold widget - var child; - + LayoutBuilder( + builder: (context, dimens) { //check to see whether device is in landscape or portrait - //load widgets based on device orientation - orientation == Orientation.landscape - ? child = Row( - children: [ - Expanded( - flex: 1, - child: _buildLeftSide(), - ), - Expanded( - flex: 1, - child: _buildRightSide(), - ), - ], - ) - : child = Center(child: _buildRightSide()); - - return child; + //load widgets based on device orientation and max width + if (MediaQuery.of(context).orientation == Orientation.landscape || + dimens.maxWidth >= Dimens.tablet_breakpoint) { + return Row( + children: [ + Expanded( + flex: 1, + child: _buildLeftSide(), + ), + Expanded( + flex: 1, + child: _buildRightSide(), + ), + ], + ); + } + + return Center(child: _buildRightSide()); }, ), Observer( @@ -153,7 +153,7 @@ class _LoginScreenState extends State { return Observer( builder: (context) { return TextFieldWidget( - hint: Strings.login_et_user_email, + hint: AppLocalizations.of(context).login_et_user_email, inputType: TextInputType.emailAddress, icon: Icons.person, iconColor: Colors.black54, @@ -172,7 +172,7 @@ class _LoginScreenState extends State { return Observer( builder: (context) { return TextFieldWidget( - hint: Strings.login_et_user_password, + hint: AppLocalizations.of(context).login_et_user_password, isObscure: true, padding: EdgeInsets.only(top: 16.0), icon: Icons.lock, @@ -191,7 +191,7 @@ class _LoginScreenState extends State { child: FlatButton( padding: EdgeInsets.all(0.0), child: Text( - Strings.login_btn_forgot_password, + AppLocalizations.of(context).login_btn_forgot_password, style: Theme.of(context) .textTheme .caption @@ -204,14 +204,15 @@ class _LoginScreenState extends State { Widget _buildSignInButton() { return RoundedButtonWidget( - buttonText: Strings.login_btn_sign_in, + buttonText: AppLocalizations.of(context).login_btn_sign_in, buttonColor: Colors.orangeAccent, textColor: Colors.white, onPressed: () async { if (_store.canLogin) { _store.login(); } else { - showErrorMessage(context, 'Please fill in all fields'); + showErrorMessage( + context, AppLocalizations.of(context).login_validation_error); } }, ); @@ -219,13 +220,12 @@ class _LoginScreenState extends State { // General Methods:----------------------------------------------------------- showErrorMessage(BuildContext context, String message) { - if(message != null) { + if (message != null) { FlushbarHelper.createError( message: message, title: 'Error', duration: Duration(seconds: 3), - ) - ..show(context); + )..show(context); } return Container(); diff --git a/lib/ui/navigation.dart b/lib/ui/navigation.dart new file mode 100644 index 00000000..626ffd36 --- /dev/null +++ b/lib/ui/navigation.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../data/sharedpref/constants/index.dart'; +import '../locale/index.dart'; +import '../routes.dart'; +import '../widgets/index.dart'; +import 'home/home.dart'; +import 'settings/settings.dart'; + +class AppNavigation extends StatelessWidget { + @override + Widget build(BuildContext context) { + return DynamicNavigation( + type: NavigationType.bottomTabs, + children: [ + Screen( + iconData: Icons.home, + title: AppLocalizations.of(context).posts_title, + child: HomeScreen(), + actions: [ + IconButton( + onPressed: () { + SharedPreferences.getInstance().then((preference) { + preference.setBool(Preferences.is_logged_in, false); + Navigator.of(context).pushReplacementNamed(Routes.login); + }); + }, + icon: Icon( + Icons.power_settings_new, + ), + ), + ]), + Screen( + iconData: Icons.settings, + title: AppLocalizations.of(context).settings_title, + child: SettingsScreen(), + ), + ], + ); + } +} diff --git a/lib/ui/settings/settings.dart b/lib/ui/settings/settings.dart new file mode 100644 index 00000000..a537c7b0 --- /dev/null +++ b/lib/ui/settings/settings.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class SettingsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/ui/splash/splash.dart b/lib/ui/splash/splash.dart index e5b616ec..f9f035b1 100644 --- a/lib/ui/splash/splash.dart +++ b/lib/ui/splash/splash.dart @@ -1,16 +1,19 @@ import 'dart:async'; -import 'package:boilerplate/data/sharedpref/constants/preferences.dart'; -import 'package:boilerplate/routes.dart'; -import 'package:boilerplate/widgets/app_icon_widget.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../data/sharedpref/constants/preferences.dart'; +import '../../routes.dart'; +import '../../widgets/app_icon_widget.dart'; + class SplashScreen extends StatefulWidget { @override State createState() => _SplashScreenState(); } +const bool autoLogin = true; + class _SplashScreenState extends State { @override void initState() { @@ -33,8 +36,8 @@ class _SplashScreenState extends State { navigate() async { SharedPreferences preferences = await SharedPreferences.getInstance(); - if (preferences.getBool(Preferences.is_logged_in) ?? false) { - Navigator.of(context).pushReplacementNamed(Routes.login); + if (autoLogin && (preferences?.getBool(Preferences.is_logged_in) ?? false)) { + Navigator.of(context).pushReplacementNamed(Routes.home); } else { Navigator.of(context).pushReplacementNamed(Routes.login); } diff --git a/lib/utils/dio/dio_error_util.dart b/lib/utils/dio/dio_error_util.dart index dbbebc8f..b5e9d816 100644 --- a/lib/utils/dio/dio_error_util.dart +++ b/lib/utils/dio/dio_error_util.dart @@ -14,14 +14,14 @@ class DioErrorUtil { break; case DioErrorType.DEFAULT: errorDescription = - "Connection to API server failed due to internet connection"; + "Connection to API server failed due to internet connection"; break; case DioErrorType.RECEIVE_TIMEOUT: errorDescription = "Receive timeout in connection with API server"; break; case DioErrorType.RESPONSE: errorDescription = - "Received invalid status code: ${error.response.statusCode}"; + "Received invalid status code: ${error.response.statusCode}"; break; case DioErrorType.SEND_TIMEOUT: errorDescription = "Send timeout in connection with API server"; @@ -32,4 +32,4 @@ class DioErrorUtil { } return errorDescription; } -} \ No newline at end of file +} diff --git a/lib/utils/dio/index.dart b/lib/utils/dio/index.dart new file mode 100644 index 00000000..361976fa --- /dev/null +++ b/lib/utils/dio/index.dart @@ -0,0 +1 @@ +export 'dio_error_util.dart'; diff --git a/lib/utils/encryption/index.dart b/lib/utils/encryption/index.dart new file mode 100644 index 00000000..cfae3b8d --- /dev/null +++ b/lib/utils/encryption/index.dart @@ -0,0 +1 @@ +export 'xxtea.dart'; diff --git a/lib/widgets/index.dart b/lib/widgets/index.dart new file mode 100644 index 00000000..d0ac5d01 --- /dev/null +++ b/lib/widgets/index.dart @@ -0,0 +1,6 @@ +export 'app_icon_widget.dart'; +export 'empty_app_bar_widget.dart'; +export 'navigation.dart'; +export 'progress_indicator_widget.dart'; +export 'rounded_button_widget.dart'; +export 'textfield_widget.dart'; diff --git a/lib/widgets/navigation.dart b/lib/widgets/navigation.dart new file mode 100644 index 00000000..9d9d6bdc --- /dev/null +++ b/lib/widgets/navigation.dart @@ -0,0 +1,162 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +class DynamicNavigation extends StatefulWidget { + DynamicNavigation({ + @required this.children, + this.type = NavigationType.bottomTabs, + }); + + final List children; + final NavigationType type; + + @override + _DynamicNavigationState createState() => _DynamicNavigationState(); +} + +class _DynamicNavigationState extends State { + int _currentIndex = 0; + + void tabSelected(val) { + if (mounted) + setState(() { + _currentIndex = val; + }); + } + + @override + Widget build(BuildContext context) { + if (widget.type == NavigationType.sideDrawer) { + return Scaffold( + appBar: AppBar( + title: Text(widget.children[_currentIndex].title), + actions: widget.children[_currentIndex]?.actions, + ), + drawer: Container( + child: Drawer( + child: SingleChildScrollView( + child: SafeArea( + child: Column(children: [ + for (var tab in widget.children) ...[ + ListTile( + selected: _currentIndex == widget.children.indexOf(tab), + leading: Icon(tab.icon), + title: Text(tab.title), + subtitle: + tab?.description != null ? Text(tab.description) : null, + onTap: () => tabSelected(widget.children.indexOf(tab)), + ), + ], + ]), + ), + )), + ), + body: Stack( + children: [ + for (int i = 0; i < widget.children.length; i++) ...[ + Offstage( + offstage: _currentIndex != i, + child: widget.children[i].child, + ), + ], + ], + ), + ); + } + if (widget.type == NavigationType.bottomTabs) { + return Scaffold( + appBar: AppBar( + title: Text(widget.children[_currentIndex].title), + actions: widget.children[_currentIndex]?.actions, + ), + body: Stack( + children: [ + for (int i = 0; i < widget.children.length; i++) ...[ + Offstage( + offstage: _currentIndex != i, + child: widget.children[i].child, + ), + ], + ], + ), + bottomNavigationBar: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: tabSelected, + items: [ + for (var tab in widget.children) ...[ + BottomNavigationBarItem( + icon: Icon(tab.icon), + title: Text(tab.title), + ), + ], + ], + ), + ); + } + + if (widget.type == NavigationType.pages) { + return DefaultTabController( + initialIndex: _currentIndex, + length: widget.children.length, + child: Scaffold( + appBar: AppBar( + title: Text(widget.children[_currentIndex].title), + actions: widget.children[_currentIndex]?.actions, + bottom: TabBar( + onTap: tabSelected, + tabs: [ + for (var tab in widget.children) ...[tab.tab], + ], + ), + ), + body: Stack( + children: [ + for (int i = 0; i < widget.children.length; i++) ...[ + Offstage( + offstage: _currentIndex != i, + child: widget.children[i].child, + ), + ], + ], + ), + ), + ); + } + + return Container(); + } +} + +enum NavigationType { + bottomTabs, + sideDrawer, + pages, +} + +class Screen { + const Screen({ + @required this.title, + @required this.iconData, + @required this.child, + this.description, + this.iosIconData, + this.pageTab, + this.actions, + }); + + final List actions; + final Widget child; + final String title, description; + final IconData iconData, iosIconData; + final Tab pageTab; + + IconData get icon { + if (Platform.isIOS || Platform.isMacOS) { + return iosIconData ?? iconData; + } + return iconData; + } + + Tab get tab => pageTab ?? Tab(text: title); +} diff --git a/lib/widgets/progress_indicator_widget.dart b/lib/widgets/progress_indicator_widget.dart index 0bc91600..420d4ecc 100644 --- a/lib/widgets/progress_indicator_widget.dart +++ b/lib/widgets/progress_indicator_widget.dart @@ -27,8 +27,7 @@ class CustomProgressIndicatorWidget extends StatelessWidget { ), ), ), - decoration: BoxDecoration( - color: Color.fromARGB(100, 105, 105, 105)), + decoration: BoxDecoration(color: Color.fromARGB(100, 105, 105, 105)), ), ); } diff --git a/lib/widgets/textfield_widget.dart b/lib/widgets/textfield_widget.dart index 4962d3e5..8a8923d1 100644 --- a/lib/widgets/textfield_widget.dart +++ b/lib/widgets/textfield_widget.dart @@ -58,5 +58,4 @@ class TextFieldWidget extends StatelessWidget { ), ); } - } diff --git a/pubspec.yaml b/pubspec.yaml index 41ab3b53..c525d377 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,19 +10,22 @@ description: A flutter boilerplate project created using MobX and Provider. version: 1.0.0+1 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.2.2 <3.0.0" dependencies: flutter: sdk: flutter - + flutter_localizations: + sdk: flutter + # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 # The following adds the shared pref as a dependency in your application shared_preferences: ^0.4.3 - + localstorage: ^2.0.0 + # A composable, Future-based library for making HTTP requests. http: ^0.12.0+1 @@ -60,12 +63,14 @@ dependencies: # A flexible widget for user notification. flushbar: 1.5.3 +dependency_overrides: + f_logs: + git: "https://github.com/AppleEducate/Flogs" dev_dependencies: flutter_test: sdk: flutter - flutter_launcher_icons: "^0.7.0" build_runner: ^1.3.0 flutter_icons: diff --git a/test/widget_test.dart b/test/widget_test.dart index a48deac4..40aa4800 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -9,7 +9,6 @@ import 'package:boilerplate/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame.