diff --git a/lib/data/models/serializers.g.dart b/lib/data/models/serializers.g.dart index 768d1192f..0929671b4 100644 --- a/lib/data/models/serializers.g.dart +++ b/lib/data/models/serializers.g.dart @@ -91,6 +91,7 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(LanguageListResponse.serializer) ..add(ListUIState.serializer) ..add(LoginResponse.serializer) + ..add(ModuleLayout.serializer) ..add(PaymentEntity.serializer) ..add(PaymentItemResponse.serializer) ..add(PaymentListResponse.serializer) diff --git a/lib/main.dart b/lib/main.dart index 1dd2546f0..2bff3ef3c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -59,7 +59,7 @@ import 'package:invoiceninja_flutter/ui/company_gateway/view/company_gateway_vie import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_middleware.dart'; void main({bool isTesting = false}) async { - //WidgetsFlutterBinding.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); final SentryClient _sentry = Config.SENTRY_DNS.isEmpty ? null diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index ff1252b3c..0e4777034 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_actions.dart @@ -36,6 +36,8 @@ class RefreshClient { final String clientId; } +class SwitchListTableLayout implements PersistUI, PersistPrefs {} + class ViewMainScreen { ViewMainScreen(this.context); diff --git a/lib/redux/app/app_middleware.dart b/lib/redux/app/app_middleware.dart index 85be2f72d..9cc41e4c0 100644 --- a/lib/redux/app/app_middleware.dart +++ b/lib/redux/app/app_middleware.dart @@ -217,7 +217,7 @@ Middleware _createLoadState( uiState.currentRoute.isNotEmpty) { final NavigatorState navigator = Navigator.of(action.context); final routes = _getRoutes(appState); - if (appState.prefState.layout == AppLayout.mobile) { + if (appState.prefState.appLayout == AppLayout.mobile) { bool isFirst = true; routes.forEach((route) { if (isFirst) { @@ -250,7 +250,7 @@ Middleware _createLoadState( final Completer completer = Completer(); completer.future.then((_) { final layout = calculateLayout(action.context); - if (store.state.prefState.layout == AppLayout.tablet && + if (store.state.prefState.appLayout == AppLayout.tablet && layout == AppLayout.mobile) { store.dispatch(UserSettingsChanged(layout: layout)); store.dispatch( diff --git a/lib/redux/ui/pref_reducer.dart b/lib/redux/ui/pref_reducer.dart index ca6d3db33..f3bb5abc8 100644 --- a/lib/redux/ui/pref_reducer.dart +++ b/lib/redux/ui/pref_reducer.dart @@ -27,7 +27,8 @@ PrefState prefReducer( return state.rebuild((b) => b ..companyPrefs[selectedCompanyIndex] = companyPrefReducer(state.companyPrefs[selectedCompanyIndex], action) - ..layout = layoutReducer(state.layout, action) + ..appLayout = layoutReducer(state.appLayout, action) + ..moduleLayout = moduleLayoutReducer(state.moduleLayout, action) ..menuSidebarMode = manuSidebarReducer(state.menuSidebarMode, action) ..historySidebarMode = historySidebarReducer(state.historySidebarMode, action) @@ -92,6 +93,16 @@ Reducer layoutReducer = combineReducers([ }), ]); +Reducer moduleLayoutReducer = combineReducers([ + TypedReducer((moduleLayout, action) { + if (moduleLayout == ModuleLayout.list) { + return ModuleLayout.table; + } else { + return ModuleLayout.list; + } + }), +]); + Reducer manuSidebarReducer = combineReducers([ TypedReducer((mode, action) { return action.menuMode ?? mode; diff --git a/lib/redux/ui/pref_state.dart b/lib/redux/ui/pref_state.dart index 0e44357ab..81fe3669b 100644 --- a/lib/redux/ui/pref_state.dart +++ b/lib/redux/ui/pref_state.dart @@ -10,7 +10,8 @@ part 'pref_state.g.dart'; abstract class PrefState implements Built { factory PrefState() { return _$PrefState._( - layout: AppLayout.tablet, + appLayout: AppLayout.tablet, + moduleLayout: ModuleLayout.list, menuSidebarMode: AppSidebarMode.collapse, historySidebarMode: AppSidebarMode.visible, isMenuVisible: false, @@ -30,7 +31,9 @@ abstract class PrefState implements Built { PrefState._(); - AppLayout get layout; + AppLayout get appLayout; + + ModuleLayout get moduleLayout; AppSidebarMode get menuSidebarMode; @@ -59,10 +62,10 @@ abstract class PrefState implements Built { bool get isMobile => layout == AppLayout.mobile; bool get isMenuFloated => - layout == AppLayout.mobile || menuSidebarMode == AppSidebarMode.float; + appLayout == AppLayout.mobile || menuSidebarMode == AppSidebarMode.float; bool get isHistoryFloated => - layout == AppLayout.mobile || historySidebarMode == AppSidebarMode.float; + appLayout == AppLayout.mobile || historySidebarMode == AppSidebarMode.float; bool get showMenu => (isMenuVisible && menuSidebarMode == AppSidebarMode.visible) || @@ -112,6 +115,19 @@ class AppLayout extends EnumClass { static AppLayout valueOf(String name) => _$valueOf(name); } +class ModuleLayout extends EnumClass { + const ModuleLayout._(String name) : super(name); + + static Serializer get serializer => _$moduleLayoutSerializer; + + static const ModuleLayout list = _$list; + static const ModuleLayout table = _$table; + + static BuiltSet get values => _$moduleLayoutValues; + + static ModuleLayout valueOf(String name) => _$moduleLayoutValueOf(name); +} + class AppSidebar extends EnumClass { const AppSidebar._(String name) : super(name); diff --git a/lib/redux/ui/pref_state.g.dart b/lib/redux/ui/pref_state.g.dart index 8c76b7be7..2235e12e3 100644 --- a/lib/redux/ui/pref_state.g.dart +++ b/lib/redux/ui/pref_state.g.dart @@ -29,6 +29,26 @@ final BuiltSet _$values = new BuiltSet(const [ _$desktop, ]); +const ModuleLayout _$list = const ModuleLayout._('list'); +const ModuleLayout _$table = const ModuleLayout._('table'); + +ModuleLayout _$moduleLayoutValueOf(String name) { + switch (name) { + case 'list': + return _$list; + case 'table': + return _$table; + default: + throw new ArgumentError(name); + } +} + +final BuiltSet _$moduleLayoutValues = + new BuiltSet(const [ + _$list, + _$table, +]); + const AppSidebar _$menu = const AppSidebar._('menu'); const AppSidebar _$history = const AppSidebar._('history'); @@ -77,6 +97,8 @@ Serializer _$prefStateSerializer = new _$PrefStateSerializer(); Serializer _$companyPrefStateSerializer = new _$CompanyPrefStateSerializer(); Serializer _$appLayoutSerializer = new _$AppLayoutSerializer(); +Serializer _$moduleLayoutSerializer = + new _$ModuleLayoutSerializer(); Serializer _$appSidebarSerializer = new _$AppSidebarSerializer(); Serializer _$appSidebarModeSerializer = new _$AppSidebarModeSerializer(); @@ -93,9 +115,12 @@ class _$PrefStateSerializer implements StructuredSerializer { Iterable serialize(Serializers serializers, PrefState object, {FullType specifiedType = FullType.unspecified}) { final result = [ - 'layout', - serializers.serialize(object.layout, + 'appLayout', + serializers.serialize(object.appLayout, specifiedType: const FullType(AppLayout)), + 'moduleLayout', + serializers.serialize(object.moduleLayout, + specifiedType: const FullType(ModuleLayout)), 'menuSidebarMode', serializers.serialize(object.menuSidebarMode, specifiedType: const FullType(AppSidebarMode)), @@ -146,10 +171,14 @@ class _$PrefStateSerializer implements StructuredSerializer { iterator.moveNext(); final dynamic value = iterator.current; switch (key) { - case 'layout': - result.layout = serializers.deserialize(value, + case 'appLayout': + result.appLayout = serializers.deserialize(value, specifiedType: const FullType(AppLayout)) as AppLayout; break; + case 'moduleLayout': + result.moduleLayout = serializers.deserialize(value, + specifiedType: const FullType(ModuleLayout)) as ModuleLayout; + break; case 'menuSidebarMode': result.menuSidebarMode = serializers.deserialize(value, specifiedType: const FullType(AppSidebarMode)) as AppSidebarMode; @@ -272,6 +301,23 @@ class _$AppLayoutSerializer implements PrimitiveSerializer { AppLayout.valueOf(serialized as String); } +class _$ModuleLayoutSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [ModuleLayout]; + @override + final String wireName = 'ModuleLayout'; + + @override + Object serialize(Serializers serializers, ModuleLayout object, + {FullType specifiedType = FullType.unspecified}) => + object.name; + + @override + ModuleLayout deserialize(Serializers serializers, Object serialized, + {FullType specifiedType = FullType.unspecified}) => + ModuleLayout.valueOf(serialized as String); +} + class _$AppSidebarSerializer implements PrimitiveSerializer { @override final Iterable types = const [AppSidebar]; @@ -363,7 +409,9 @@ class _$HistoryRecordSerializer implements StructuredSerializer { class _$PrefState extends PrefState { @override - final AppLayout layout; + final AppLayout appLayout; + @override + final ModuleLayout moduleLayout; @override final AppSidebarMode menuSidebarMode; @override @@ -391,7 +439,8 @@ class _$PrefState extends PrefState { (new PrefStateBuilder()..update(updates)).build(); _$PrefState._( - {this.layout, + {this.appLayout, + this.moduleLayout, this.menuSidebarMode, this.historySidebarMode, this.isMenuVisible, @@ -404,8 +453,11 @@ class _$PrefState extends PrefState { this.addDocumentsToInvoice, this.companyPrefs}) : super._() { - if (layout == null) { - throw new BuiltValueNullFieldError('PrefState', 'layout'); + if (appLayout == null) { + throw new BuiltValueNullFieldError('PrefState', 'appLayout'); + } + if (moduleLayout == null) { + throw new BuiltValueNullFieldError('PrefState', 'moduleLayout'); } if (menuSidebarMode == null) { throw new BuiltValueNullFieldError('PrefState', 'menuSidebarMode'); @@ -454,7 +506,8 @@ class _$PrefState extends PrefState { bool operator ==(Object other) { if (identical(other, this)) return true; return other is PrefState && - layout == other.layout && + appLayout == other.appLayout && + moduleLayout == other.moduleLayout && menuSidebarMode == other.menuSidebarMode && historySidebarMode == other.historySidebarMode && isMenuVisible == other.isMenuVisible && @@ -480,7 +533,9 @@ class _$PrefState extends PrefState { $jc( $jc( $jc( - $jc($jc(0, layout.hashCode), + $jc( + $jc($jc(0, appLayout.hashCode), + moduleLayout.hashCode), menuSidebarMode.hashCode), historySidebarMode.hashCode), isMenuVisible.hashCode), @@ -497,7 +552,8 @@ class _$PrefState extends PrefState { @override String toString() { return (newBuiltValueToStringHelper('PrefState') - ..add('layout', layout) + ..add('appLayout', appLayout) + ..add('moduleLayout', moduleLayout) ..add('menuSidebarMode', menuSidebarMode) ..add('historySidebarMode', historySidebarMode) ..add('isMenuVisible', isMenuVisible) @@ -516,9 +572,14 @@ class _$PrefState extends PrefState { class PrefStateBuilder implements Builder { _$PrefState _$v; - AppLayout _layout; - AppLayout get layout => _$this._layout; - set layout(AppLayout layout) => _$this._layout = layout; + AppLayout _appLayout; + AppLayout get appLayout => _$this._appLayout; + set appLayout(AppLayout appLayout) => _$this._appLayout = appLayout; + + ModuleLayout _moduleLayout; + ModuleLayout get moduleLayout => _$this._moduleLayout; + set moduleLayout(ModuleLayout moduleLayout) => + _$this._moduleLayout = moduleLayout; AppSidebarMode _menuSidebarMode; AppSidebarMode get menuSidebarMode => _$this._menuSidebarMode; @@ -579,7 +640,8 @@ class PrefStateBuilder implements Builder { PrefStateBuilder get _$this { if (_$v != null) { - _layout = _$v.layout; + _appLayout = _$v.appLayout; + _moduleLayout = _$v.moduleLayout; _menuSidebarMode = _$v.menuSidebarMode; _historySidebarMode = _$v.historySidebarMode; _isMenuVisible = _$v.isMenuVisible; @@ -615,7 +677,8 @@ class PrefStateBuilder implements Builder { try { _$result = _$v ?? new _$PrefState._( - layout: layout, + appLayout: appLayout, + moduleLayout: moduleLayout, menuSidebarMode: menuSidebarMode, historySidebarMode: historySidebarMode, isMenuVisible: isMenuVisible, diff --git a/lib/ui/app/app_bottom_bar.dart b/lib/ui/app/app_bottom_bar.dart index 7a6ce6380..0a29c3b82 100644 --- a/lib/ui/app/app_bottom_bar.dart +++ b/lib/ui/app/app_bottom_bar.dart @@ -1,8 +1,11 @@ import 'package:built_collection/built_collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/ui/list_ui_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/pref_state.dart'; +import 'package:invoiceninja_flutter/redux/ui/ui_state.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:redux/redux.dart'; @@ -260,11 +263,20 @@ class _AppBottomBarState extends State { return StoreBuilder(builder: (BuildContext context, Store store) { final localization = AppLocalization.of(context); + final prefState = store.state.prefState; + final isList = prefState.moduleLayout == ModuleLayout.list; return BottomAppBar( shape: CircularNotchedRectangle(), child: Row( children: [ + IconButton( + tooltip: AppLocalization.of(context).switchListTable, + icon: Icon(isList ? Icons.table_chart : Icons.view_list), + onPressed: () { + store.dispatch(SwitchListTableLayout()); + }, + ), if (widget.sortFields.isNotEmpty) IconButton( tooltip: AppLocalization.of(context).sort, diff --git a/lib/ui/settings/device_settings_list.dart b/lib/ui/settings/device_settings_list.dart index 8d03d90a0..c4af424f2 100644 --- a/lib/ui/settings/device_settings_list.dart +++ b/lib/ui/settings/device_settings_list.dart @@ -53,7 +53,7 @@ class _DeviceSettingsState extends State { children: [ AppDropdownButton( labelText: localization.layout, - value: viewModel.state.prefState.layout, + value: viewModel.state.prefState.appLayout, onChanged: (dynamic value) => viewModel.onLayoutChanged(context, value), items: [ @@ -67,7 +67,7 @@ class _DeviceSettingsState extends State { ), ], ), - if (state.prefState.layout == AppLayout.tablet) ...[ + if (state.prefState.appLayout == AppLayout.tablet) ...[ AppDropdownButton( labelText: localization.menuSidebar, value: state.prefState.menuSidebarMode, diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 43541c739..81d93c568 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -814,7 +814,8 @@ mixin LocalizationsProvider on LocaleCodeAware { 'default_tax_rate_3': 'Default Tax Rate 3', 'email_subject_invoice': 'Email Invoice Subject', 'email_subject_quote': 'Email Quote Subject', - 'email_subject_payment': 'Email Payment Subject' + 'email_subject_payment': 'Email Payment Subject', + 'switch_list_table': 'Switch List Table' }, 'sq': { 'thank_you_for_your_purchase': 'Thank you for your purchase!', @@ -15766,6 +15767,8 @@ mixin LocalizationsProvider on LocaleCodeAware { String get newPayment => _localizedValues[localeCode]['new_payment']; + String get switchListTable => _localizedValues[localeCode]['switch_list_table']; + String lookup(String key) { final lookupKey = toSnakeCase(key); return _localizedValues[localeCode][lookupKey] ?? diff --git a/lib/utils/platforms.dart b/lib/utils/platforms.dart index f185c716f..9a267826b 100644 --- a/lib/utils/platforms.dart +++ b/lib/utils/platforms.dart @@ -33,7 +33,7 @@ AppLayout calculateLayout(BuildContext context) { } AppLayout getLayout(BuildContext context) => - StoreProvider.of(context).state.prefState.layout ?? + StoreProvider.of(context).state.prefState.appLayout ?? AppLayout.mobile; bool isMobile(BuildContext context) => getLayout(context) == AppLayout.mobile;