diff --git a/lib/constants.dart b/lib/constants.dart index a7634f859..0da7e06d3 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -3,6 +3,12 @@ import 'package:flutter/material.dart'; // This version must be updated in tandem with the pubspec version. const String kAppVersion = '0.1.2'; +const String kSharedPrefEmail = 'email'; +const String kSharedPrefPassword = 'password'; +const String kSharedPrefUrl = 'url'; +const String kSharedPrefSecret = 'secret'; +const String kSharedPrefEnableDarkMode = 'enable_dark_mode'; + const int kMinMajorAppVersion = 4; const int kMinMinorAppVersion = 5; diff --git a/lib/main.dart b/lib/main.dart index 7570e257e..f74bdc4e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,15 @@ -import 'package:invoiceninja_flutter/ui/settings/settings_screen.dart'; import 'package:redux/redux.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:redux_logging/redux_logging.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/redux/app/app_middleware.dart'; import 'package:invoiceninja_flutter/redux/client/client_actions.dart'; import 'package:invoiceninja_flutter/redux/client/client_middleware.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart'; +import 'package:invoiceninja_flutter/ui/settings/settings_screen.dart'; import 'package:invoiceninja_flutter/ui/auth/init_screen.dart'; import 'package:invoiceninja_flutter/ui/client/client_screen.dart'; import 'package:invoiceninja_flutter/ui/client/edit/client_edit_vm.dart'; @@ -14,7 +17,6 @@ import 'package:invoiceninja_flutter/ui/client/view/client_view_vm.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/invoice/view/invoice_view_vm.dart'; import 'package:invoiceninja_flutter/ui/product/edit/product_edit_vm.dart'; -import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; import 'package:invoiceninja_flutter/ui/dashboard/dashboard_screen.dart'; import 'package:invoiceninja_flutter/ui/product/product_screen.dart'; @@ -27,9 +29,9 @@ import 'package:invoiceninja_flutter/redux/product/product_actions.dart'; import 'package:invoiceninja_flutter/redux/product/product_middleware.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_middleware.dart'; import 'package:invoiceninja_flutter/ui/invoice/invoice_screen.dart'; -import 'package:redux_logging/redux_logging.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; -void main() { +void main() async { final store = Store(appReducer, initialState: AppState(), middleware: [] @@ -43,19 +45,23 @@ void main() { LoggingMiddleware.printer(), ])); - runApp(InvoiceNinjaApp(store: store)); + final prefs = await SharedPreferences.getInstance(); + final enableDarkMode = prefs.getBool(kSharedPrefEnableDarkMode); + + runApp(InvoiceNinjaApp(store: store, enableDarkMode: enableDarkMode)); } class InvoiceNinjaApp extends StatefulWidget { final Store store; - - const InvoiceNinjaApp({Key key, this.store}) : super(key: key); + final bool enableDarkMode; + const InvoiceNinjaApp({Key key, this.store, this.enableDarkMode}) + : super(key: key); @override - _InvoiceNinjaAppState createState() => _InvoiceNinjaAppState(); + InvoiceNinjaAppState createState() => InvoiceNinjaAppState(); } -class _InvoiceNinjaAppState extends State { +class InvoiceNinjaAppState extends State { @override Widget build(BuildContext context) { @@ -69,24 +75,21 @@ class _InvoiceNinjaAppState extends State { ], // light theme - theme: ThemeData().copyWith( - //accentColor: Colors.lightBlueAccent, - primaryColor: const Color(0xFF117cc1), - primaryColorLight: const Color(0xFF5dabf4), - primaryColorDark: const Color(0xFF0D5D91), - indicatorColor: Colors.white, - bottomAppBarColor: Colors.grey.shade300, - backgroundColor: Colors.grey.shade200, - buttonColor: const Color(0xFF0D5D91), - ), - - //dark theme - /* - theme: ThemeData( - brightness: Brightness.dark, - accentColor: Colors.lightBlueAccent, - ), - */ + theme: widget.enableDarkMode + ? ThemeData( + brightness: Brightness.dark, + accentColor: Colors.lightBlueAccent, + ) + : ThemeData().copyWith( + //accentColor: Colors.lightBlueAccent, + primaryColor: const Color(0xFF117cc1), + primaryColorLight: const Color(0xFF5dabf4), + primaryColorDark: const Color(0xFF0D5D91), + indicatorColor: Colors.white, + bottomAppBarColor: Colors.grey.shade300, + backgroundColor: Colors.grey.shade200, + buttonColor: const Color(0xFF0D5D91), + ), title: 'Invoice Ninja', routes: { diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index c66961d11..e02db2d62 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_actions.dart @@ -13,4 +13,10 @@ class LoadStaticSuccess { final StaticData data; LoadStaticSuccess(this.data); +} + +class UserSettingsChanged implements PersistUI { + final bool enableDarkMode; + + UserSettingsChanged({this.enableDarkMode}); } \ No newline at end of file diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index b26ea730c..211f8c779 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -105,6 +105,6 @@ abstract class AppState implements Built { String toString() { //return 'Is Loading: ${this.isLoading}, Invoice: ${this.invoiceUIState.selected}'; //return 'Date Formats: ${staticState.dateFormatMap}'; - return 'Route: ${uiState.currentRoute}'; + return 'Route: ${uiState.currentRoute}, Dark Mode: ${uiState.enableDarkMode}'; } } \ No newline at end of file diff --git a/lib/redux/auth/auth_middleware.dart b/lib/redux/auth/auth_middleware.dart index 81f6a2f00..fe4f375cf 100644 --- a/lib/redux/auth/auth_middleware.dart +++ b/lib/redux/auth/auth_middleware.dart @@ -24,26 +24,29 @@ List> createStoreAuthMiddleware([ void _saveAuthLocal(dynamic action) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setString('email', action.email); - prefs.setString('url', action.url); + prefs.setString(kSharedPrefEmail, action.email); + prefs.setString(kSharedPrefUrl, action.url); if (action.password == 'password') { - prefs.setString('password', action.password); + prefs.setString(kSharedPrefPassword, action.password); } if (action.secret == 'secret') { - prefs.setString('secret', action.secret); + prefs.setString(kSharedPrefSecret, action.secret); } } void _loadAuthLocal(Store store, dynamic action) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String email = prefs.getString('email') ?? Config.LOGIN_EMAIL; - final String password = prefs.getString('password') ?? Config.LOGIN_PASSWORD; - final String url = prefs.getString('url') ?? Config.LOGIN_URL; - final String secret = prefs.getString('secret') ?? Config.LOGIN_SECRET; - + final String email = prefs.getString(kSharedPrefEmail) ?? Config.LOGIN_EMAIL; + final String password = prefs.getString(kSharedPrefPassword) ?? Config.LOGIN_PASSWORD; + final String url = prefs.getString(kSharedPrefUrl) ?? Config.LOGIN_URL; + final String secret = prefs.getString(kSharedPrefSecret) ?? Config.LOGIN_SECRET; store.dispatch(UserLoginLoaded(email, password, url, secret)); + + final bool enableDarkMode = prefs.getBool(kSharedPrefEnableDarkMode) ?? false; + store.dispatch(UserSettingsChanged(enableDarkMode: enableDarkMode)); + Navigator.of(action.context).pushReplacementNamed(LoginScreen.route); } diff --git a/lib/redux/ui/ui_reducer.dart b/lib/redux/ui/ui_reducer.dart index 759b333b2..653f8e588 100644 --- a/lib/redux/ui/ui_reducer.dart +++ b/lib/redux/ui/ui_reducer.dart @@ -1,3 +1,4 @@ +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/client/client_reducer.dart'; import 'package:invoiceninja_flutter/redux/company/company_actions.dart'; import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; @@ -11,12 +12,21 @@ UIState uiReducer(UIState state, dynamic action) { return state.rebuild((b) => b ..selectedCompanyIndex = selectedCompanyIndexReducer(state.selectedCompanyIndex, action) ..currentRoute = currentRouteReducer(state.currentRoute, action) + ..enableDarkMode = darkModeReducer(state.enableDarkMode, action) ..productUIState.replace(productUIReducer(state.productUIState, action)) ..clientUIState.replace(clientUIReducer(state.clientUIState, action)) ..invoiceUIState.replace(invoiceUIReducer(state.invoiceUIState, action)) ); } +Reducer darkModeReducer = combineReducers([ + TypedReducer(updateDarkModeReducer), +]); + +bool updateDarkModeReducer(bool enableDarkMode, UserSettingsChanged action) { + return action.enableDarkMode; +} + Reducer currentRouteReducer = combineReducers([ TypedReducer(updateCurrentRouteReducer), ]); diff --git a/lib/redux/ui/ui_state.dart b/lib/redux/ui/ui_state.dart index d5c33fa2e..1bb836744 100644 --- a/lib/redux/ui/ui_state.dart +++ b/lib/redux/ui/ui_state.dart @@ -13,6 +13,7 @@ abstract class UIState implements Built { return _$UIState._( selectedCompanyIndex: 0, currentRoute: LoginScreen.route, + enableDarkMode: false, productUIState: ProductUIState(), clientUIState: ClientUIState(), invoiceUIState: InvoiceUIState(), @@ -22,6 +23,7 @@ abstract class UIState implements Built { int get selectedCompanyIndex; String get currentRoute; + bool get enableDarkMode; ProductUIState get productUIState; ClientUIState get clientUIState; InvoiceUIState get invoiceUIState; diff --git a/lib/redux/ui/ui_state.g.dart b/lib/redux/ui/ui_state.g.dart index b9a500a8c..e328a5c20 100644 --- a/lib/redux/ui/ui_state.g.dart +++ b/lib/redux/ui/ui_state.g.dart @@ -32,6 +32,9 @@ class _$UIStateSerializer implements StructuredSerializer { 'currentRoute', serializers.serialize(object.currentRoute, specifiedType: const FullType(String)), + 'enableDarkMode', + serializers.serialize(object.enableDarkMode, + specifiedType: const FullType(bool)), 'productUIState', serializers.serialize(object.productUIState, specifiedType: const FullType(ProductUIState)), @@ -65,6 +68,10 @@ class _$UIStateSerializer implements StructuredSerializer { result.currentRoute = serializers.deserialize(value, specifiedType: const FullType(String)) as String; break; + case 'enableDarkMode': + result.enableDarkMode = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool; + break; case 'productUIState': result.productUIState.replace(serializers.deserialize(value, specifiedType: const FullType(ProductUIState)) as ProductUIState); @@ -90,6 +97,8 @@ class _$UIState extends UIState { @override final String currentRoute; @override + final bool enableDarkMode; + @override final ProductUIState productUIState; @override final ClientUIState clientUIState; @@ -102,6 +111,7 @@ class _$UIState extends UIState { _$UIState._( {this.selectedCompanyIndex, this.currentRoute, + this.enableDarkMode, this.productUIState, this.clientUIState, this.invoiceUIState}) @@ -110,6 +120,8 @@ class _$UIState extends UIState { throw new BuiltValueNullFieldError('UIState', 'selectedCompanyIndex'); if (currentRoute == null) throw new BuiltValueNullFieldError('UIState', 'currentRoute'); + if (enableDarkMode == null) + throw new BuiltValueNullFieldError('UIState', 'enableDarkMode'); if (productUIState == null) throw new BuiltValueNullFieldError('UIState', 'productUIState'); if (clientUIState == null) @@ -131,6 +143,7 @@ class _$UIState extends UIState { if (other is! UIState) return false; return selectedCompanyIndex == other.selectedCompanyIndex && currentRoute == other.currentRoute && + enableDarkMode == other.enableDarkMode && productUIState == other.productUIState && clientUIState == other.clientUIState && invoiceUIState == other.invoiceUIState; @@ -141,8 +154,10 @@ class _$UIState extends UIState { return $jf($jc( $jc( $jc( - $jc($jc(0, selectedCompanyIndex.hashCode), - currentRoute.hashCode), + $jc( + $jc($jc(0, selectedCompanyIndex.hashCode), + currentRoute.hashCode), + enableDarkMode.hashCode), productUIState.hashCode), clientUIState.hashCode), invoiceUIState.hashCode)); @@ -153,6 +168,7 @@ class _$UIState extends UIState { return (newBuiltValueToStringHelper('UIState') ..add('selectedCompanyIndex', selectedCompanyIndex) ..add('currentRoute', currentRoute) + ..add('enableDarkMode', enableDarkMode) ..add('productUIState', productUIState) ..add('clientUIState', clientUIState) ..add('invoiceUIState', invoiceUIState)) @@ -172,6 +188,11 @@ class UIStateBuilder implements Builder { String get currentRoute => _$this._currentRoute; set currentRoute(String currentRoute) => _$this._currentRoute = currentRoute; + bool _enableDarkMode; + bool get enableDarkMode => _$this._enableDarkMode; + set enableDarkMode(bool enableDarkMode) => + _$this._enableDarkMode = enableDarkMode; + ProductUIStateBuilder _productUIState; ProductUIStateBuilder get productUIState => _$this._productUIState ??= new ProductUIStateBuilder(); @@ -196,6 +217,7 @@ class UIStateBuilder implements Builder { if (_$v != null) { _selectedCompanyIndex = _$v.selectedCompanyIndex; _currentRoute = _$v.currentRoute; + _enableDarkMode = _$v.enableDarkMode; _productUIState = _$v.productUIState?.toBuilder(); _clientUIState = _$v.clientUIState?.toBuilder(); _invoiceUIState = _$v.invoiceUIState?.toBuilder(); @@ -223,6 +245,7 @@ class UIStateBuilder implements Builder { new _$UIState._( selectedCompanyIndex: selectedCompanyIndex, currentRoute: currentRoute, + enableDarkMode: enableDarkMode, productUIState: productUIState.build(), clientUIState: clientUIState.build(), invoiceUIState: invoiceUIState.build()); diff --git a/lib/ui/app/app_drawer_vm.dart b/lib/ui/app/app_drawer_vm.dart index 5d8c305de..0e0253591 100644 --- a/lib/ui/app/app_drawer_vm.dart +++ b/lib/ui/app/app_drawer_vm.dart @@ -1,11 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; -import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; import 'package:redux/redux.dart'; import 'package:invoiceninja_flutter/ui/app/app_drawer.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; -import 'package:invoiceninja_flutter/redux/auth/auth_actions.dart'; import 'package:invoiceninja_flutter/redux/company/company_selectors.dart'; import 'package:invoiceninja_flutter/redux/company/company_actions.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index 65ca3e0d9..b49d9948b 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -6,13 +6,9 @@ import 'package:invoiceninja_flutter/utils/localization.dart'; class SettingsList extends StatelessWidget { final SettingsListVM viewModel; - final Function onThemeChange; - final bool isDark; const SettingsList({ Key key, - @required this.isDark, - @required this.onThemeChange, @required this.viewModel, }) : super(key: key); @@ -22,8 +18,8 @@ class SettingsList extends StatelessWidget { children: [ SwitchListTile( title: Text(AppLocalization.of(context).darkMode), - value: isDark ?? false, - onChanged: (value) => viewModel.onDarkModeChanged(value), + value: viewModel.enableDarkMode, + onChanged: (value) => viewModel.onDarkModeChanged(context, value), secondary: Icon(Icons.color_lens), ), ListTile( diff --git a/lib/ui/settings/settings_list_vm.dart b/lib/ui/settings/settings_list_vm.dart index 2250cb2a7..ab691d04f 100644 --- a/lib/ui/settings/settings_list_vm.dart +++ b/lib/ui/settings/settings_list_vm.dart @@ -1,5 +1,9 @@ +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:redux/redux.dart'; import 'package:invoiceninja_flutter/ui/auth/login_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/settings_list.dart'; @@ -8,10 +12,6 @@ import 'package:invoiceninja_flutter/redux/auth/auth_actions.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SettingsListBuilder extends StatelessWidget { - //final Function onThemeChange; - //final bool isDark; - //const SettingsListBuilder(this.onThemeChange, this.isDark, {Key key}) - // : super(key: key); const SettingsListBuilder({Key key}) : super(key: key); @@ -28,27 +28,40 @@ class SettingsListBuilder extends StatelessWidget { class SettingsListVM { final Function(BuildContext context) onLogoutTapped; - final Function(bool value) onDarkModeChanged; + final Function(BuildContext context, bool value) onDarkModeChanged; + final bool enableDarkMode; SettingsListVM({ @required this.onLogoutTapped, @required this.onDarkModeChanged, + @required this.enableDarkMode, }); static SettingsListVM fromStore(Store store) { + void _warnRestart(BuildContext context) { + final localization = AppLocalization.of(context); + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + semanticLabel: localization.restartAppToApplyChange, + title: Text(localization.restartAppToApplyChange), + actions: [ + new FlatButton( + child: Text(localization.ok.toUpperCase()), + onPressed: () => Navigator.pop(context)) + ], + ), + ); + } + return SettingsListVM(onLogoutTapped: (BuildContext context) { Navigator.popUntil(context, ModalRoute.withName(LoginScreen.route)); - /* - while (Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } - Navigator.of(context).pushReplacementNamed(LoginScreen.route); - */ store.dispatch(UserLogout()); - }, onDarkModeChanged: (bool value) async { - print('value: $value'); + }, onDarkModeChanged: (BuildContext context, bool value) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool('darkMode', value); - }); + prefs.setBool(kSharedPrefEnableDarkMode, value); + store.dispatch(UserSettingsChanged(enableDarkMode: value)); + _warnRestart(context); + }, enableDarkMode: store.state.uiState.enableDarkMode); } } diff --git a/lib/utils/localization.dart b/lib/utils/localization.dart index 03b650b34..ef7f0d0b5 100644 --- a/lib/utils/localization.dart +++ b/lib/utils/localization.dart @@ -172,6 +172,7 @@ class AppLocalization { 'done': 'Done', 'please_enter_a_client_or_contact_name': 'Please enter a client or contact name', 'dark_mode': 'Dark Mode', + 'restart_app_to_apply_change': 'Restart the app to apply the change', 'payment': 'Payment', 'payments': 'Payments', @@ -354,6 +355,7 @@ class AppLocalization { String get done => _localizedValues[locale.languageCode]['done']; String get pleaseEnterAClientOrContactName => _localizedValues[locale.languageCode]['please_enter_a_client_or_contact_name']; String get darkMode => _localizedValues[locale.languageCode]['dark_mode']; + String get restartAppToApplyChange => _localizedValues[locale.languageCode]['restart_app_to_apply_change']; String get payment => _localizedValues[locale.languageCode]['payment']; String get payments => _localizedValues[locale.languageCode]['payments'];