Tablet layout

This commit is contained in:
Hillel Coren 2019-08-13 11:11:35 +03:00
parent 7179b51260
commit 83a7e840ec
16 changed files with 239 additions and 32 deletions

3
.gitignore vendored
View File

@ -17,4 +17,5 @@ key.properties
google-services.json
GoogleService-Info.plist
ios/Runner/Info.plist
android/app/build.gradle
android/app/build.gradle
android/app/release/

View File

@ -20,6 +20,9 @@ const String kSharedPrefAppVersion = 'app_version';
const String kSharedPrefRequireAuthentication = 'require_authentication';
const String kSharedPrefAddDocumentsToInvoice = 'add_documents_to_invoice';
const int kMobileLayoutWidth = 600;
const int kTabletLayoutWidth = 1000;
const String kPlanFree = '';
const String kPlanPro = 'pro';
const String kPlanEnterprise = 'enterprise';

View File

@ -8,6 +8,7 @@ part of 'serializers.dart';
Serializers _$serializers = (new Serializers().toBuilder()
..add(ActivityEntity.serializer)
..add(AppLayout.serializer)
..add(AppState.serializer)
..add(AuthState.serializer)
..add(ClientEntity.serializer)

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:invoiceninja_flutter/.env.dart';
import 'package:invoiceninja_flutter/ui/app/main_screen.dart';
import 'package:invoiceninja_flutter/ui/product/view/product_view_vm.dart';
import 'package:sentry/sentry.dart';
import 'package:invoiceninja_flutter/redux/company/company_selectors.dart';
@ -95,8 +96,9 @@ void main() async {
final store = Store<AppState>(appReducer,
initialState: AppState(
enableDarkMode: enableDarkMode,
requireAuthentication: requireAuthentication),
enableDarkMode: enableDarkMode,
requireAuthentication: requireAuthentication,
),
middleware: []
..addAll(createStoreAuthMiddleware())
..addAll(createStoreDocumentsMiddleware())
@ -221,7 +223,6 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
final state = widget.store.state;
Intl.defaultLocale = localeSelector(state);
final localization = AppLocalization(Locale(Intl.defaultLocale));
return MaterialApp(
supportedLocales: kLanguages
.map((String locale) => AppLocalization.createLocale(locale))
@ -286,6 +287,9 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
LoginScreen.route: (context) {
return LoginScreen();
},
MainScreen.route: (context) {
return MainScreen();
},
DashboardScreen.route: (context) {
if (widget.store.state.dashboardState.isStale) {
widget.store.dispatch(LoadDashboard());
@ -348,7 +352,6 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
},
ProjectViewScreen.route: (context) => ProjectViewScreen(),
ProjectEditScreen.route: (context) => ProjectEditScreen(),
PaymentScreen.route: (context) {
if (widget.store.state.paymentState.isStale) {
widget.store.dispatch(LoadPayments());

View File

@ -1,5 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_state.dart';
class PersistUI {}
@ -13,6 +15,18 @@ class RefreshClient {
final int clientId;
}
class UpdateLayout {
UpdateLayout(this.layout);
final AppLayout layout;
}
class ViewMainScreen {
ViewMainScreen(this.context);
BuildContext context;
}
class StartLoading {}
class StopLoading {}

View File

@ -15,6 +15,7 @@ import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart';
import 'package:invoiceninja_flutter/redux/static/static_state.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_state.dart';
import 'package:invoiceninja_flutter/ui/app/app_builder.dart';
import 'package:invoiceninja_flutter/ui/app/main_screen.dart';
import 'package:invoiceninja_flutter/ui/auth/login_vm.dart';
import 'package:invoiceninja_flutter/utils/platforms.dart';
import 'package:redux/redux.dart';
@ -110,6 +111,8 @@ List<Middleware<AppState>> createStorePersistenceMiddleware([
company4Repository,
company5Repository);
final viewMainScreen = _createViewMainScreen();
return [
TypedMiddleware<AppState, UserLogout>(deleteState),
TypedMiddleware<AppState, LoadStateRequest>(loadState),
@ -118,6 +121,7 @@ List<Middleware<AppState>> createStorePersistenceMiddleware([
TypedMiddleware<AppState, PersistData>(persistData),
TypedMiddleware<AppState, PersistStatic>(persistStatic),
TypedMiddleware<AppState, PersistUI>(persistUI),
TypedMiddleware<AppState, ViewMainScreen>(viewMainScreen),
];
}
@ -182,15 +186,21 @@ Middleware<AppState> _createLoadState(
if (uiState.currentRoute != LoginScreen.route &&
authState.url.isNotEmpty) {
final NavigatorState navigator = Navigator.of(action.context);
bool isFirst = true;
_getRoutes(appState).forEach((route) {
if (isFirst) {
navigator.pushReplacementNamed(route);
} else {
navigator.pushNamed(route);
}
isFirst = false;
});
if (uiState.layout == AppLayout.mobile) {
bool isFirst = true;
_getRoutes(appState).forEach((route) {
if (isFirst) {
navigator.pushReplacementNamed(route);
} else {
navigator.pushNamed(route);
}
isFirst = false;
});
} else {
store.dispatch(ViewMainScreen(action.context));
}
} else {
throw 'Unknown page';
}
} catch (error) {
print(error);
@ -238,7 +248,8 @@ List<String> _getRoutes(AppState state) {
route += '/view';
}
} else {
if (!['dashboard', 'settings'].contains(part) && entityType == null) {
if (!['main', 'dashboard', 'settings'].contains(part) &&
entityType == null) {
entityType = EntityType.valueOf(part);
}
@ -384,6 +395,15 @@ Middleware<AppState> _createDeleteState(
};
}
Middleware<AppState> _createViewMainScreen() {
return (Store<AppState> store, dynamic action, NextDispatcher next) {
Navigator.of(action.context).pushNamedAndRemoveUntil(
MainScreen.route, (Route<dynamic> route) => false);
next(action);
};
}
/*
Future<bool> _checkLastLoadWasSuccesfull() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();

View File

@ -25,8 +25,12 @@ import 'package:invoiceninja_flutter/redux/quote/quote_state.dart';
part 'app_state.g.dart';
abstract class AppState implements Built<AppState, AppStateBuilder> {
factory AppState(
{String appVersion, bool enableDarkMode, bool requireAuthentication}) {
factory AppState({
String appVersion,
bool enableDarkMode,
bool requireAuthentication,
AppLayout layout,
}) {
return _$AppState._(
isLoading: false,
isSaving: false,
@ -38,9 +42,12 @@ abstract class AppState implements Built<AppState, AppStateBuilder> {
companyState3: CompanyState(),
companyState4: CompanyState(),
companyState5: CompanyState(),
uiState: UIState(CompanyEntity(),
enableDarkMode: enableDarkMode,
requireAuthentication: requireAuthentication),
uiState: UIState(
CompanyEntity(),
enableDarkMode: enableDarkMode,
requireAuthentication: requireAuthentication,
layout: layout ?? AppLayout.mobile,
),
);
}
@ -156,7 +163,9 @@ abstract class AppState implements Built<AppState, AppStateBuilder> {
// STARTER: state getters - do not remove comment
DocumentState get documentState => selectedCompanyState.documentState;
ListUIState get documentListState => uiState.documentUIState.listUIState;
DocumentUIState get documentUIState => uiState.documentUIState;
ExpenseState get expenseState => selectedCompanyState.expenseState;
@ -219,6 +228,6 @@ abstract class AppState implements Built<AppState, AppStateBuilder> {
String toString() {
//return 'Is Loading: ${this.isLoading}, Invoice: ${this.invoiceUIState.selected}';
//return 'Expense Categories: ${selectedCompany.expenseCategories}';
return 'Route: ${uiState.currentRoute}: Server Version: $serverVersion';
return 'Layout: ${uiState.layout}, Route: ${uiState.currentRoute}';
}
}

View File

@ -6,6 +6,7 @@ import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/client/client_screen.dart';
import 'package:invoiceninja_flutter/ui/client/edit/client_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/client/view/client_view_vm.dart';
import 'package:invoiceninja_flutter/utils/platforms.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/client/client_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
@ -45,11 +46,13 @@ Middleware<AppState> _editClient() {
store.dispatch(UpdateCurrentRoute(ClientEditScreen.route));
}
final client =
await Navigator.of(action.context).pushNamed(ClientEditScreen.route);
if (action.context != null && isMobile(action.context)) {
final client =
await Navigator.of(action.context).pushNamed(ClientEditScreen.route);
if (action.completer != null && client != null) {
action.completer.complete(client);
if (action.completer != null && client != null) {
action.completer.complete(client);
}
}
};
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart';
import 'package:invoiceninja_flutter/redux/client/client_actions.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart';
import 'package:invoiceninja_flutter/ui/dashboard/dashboard_screen.dart';
import 'package:invoiceninja_flutter/utils/platforms.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
@ -24,7 +25,7 @@ Middleware<AppState> _createViewDashboard() {
store.dispatch(LoadDashboard());
store.dispatch(UpdateCurrentRoute(DashboardScreen.route));
if (action.context != null) {
if (action.context != null && isMobile(action.context)) {
Navigator.of(action.context).pushNamedAndRemoveUntil(
DashboardScreen.route, (Route<dynamic> route) => false);
}

View File

@ -5,6 +5,7 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart';
import 'package:invoiceninja_flutter/ui/product/edit/product_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/product/product_screen.dart';
import 'package:invoiceninja_flutter/ui/product/view/product_view_vm.dart';
import 'package:invoiceninja_flutter/utils/platforms.dart';
import 'package:redux/redux.dart';
import 'package:invoiceninja_flutter/redux/product/product_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
@ -48,7 +49,10 @@ Middleware<AppState> _viewProduct() {
next(action);
store.dispatch(UpdateCurrentRoute(ProductViewScreen.route));
Navigator.of(action.context).pushNamed(ProductViewScreen.route);
if (action.context != null && isMobile(action.context)) {
Navigator.of(action.context).pushNamed(ProductViewScreen.route);
}
};
}

View File

@ -28,6 +28,7 @@ UIState uiReducer(UIState state, dynamic action) {
..filter = filterReducer(state.filter, action)
..selectedCompanyIndex =
selectedCompanyIndexReducer(state.selectedCompanyIndex, action)
..layout = layoutReducer(state.layout, action)
..currentRoute = currentRouteReducer(state.currentRoute, action)
..enableDarkMode = darkModeReducer(state.enableDarkMode, action)
..autoStartTasks = autoStartTasksReducer(state.autoStartTasks, action)
@ -59,6 +60,14 @@ String updateFilter(String filter, FilterCompany action) {
return action.filter;
}
Reducer<AppLayout> layoutReducer = combineReducers([
TypedReducer<AppLayout, UpdateLayout>(updateLayout),
]);
AppLayout updateLayout(AppLayout layout, UpdateLayout action) {
return action.layout;
}
Reducer<bool> emailPaymentReducer = combineReducers([
TypedReducer<bool, UserSettingsChanged>(updateEmailPaymentReducer),
]);

View File

@ -1,3 +1,4 @@
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:invoiceninja_flutter/data/models/company_model.dart';
@ -26,9 +27,10 @@ part 'ui_state.g.dart';
abstract class UIState implements Built<UIState, UIStateBuilder> {
factory UIState(CompanyEntity company,
{bool enableDarkMode, bool requireAuthentication}) {
{bool enableDarkMode, bool requireAuthentication, AppLayout layout}) {
return _$UIState._(
selectedCompanyIndex: 0,
layout: layout ?? AppLayout.mobile,
currentRoute: LoginScreen.route,
enableDarkMode: enableDarkMode ?? false,
requireAuthentication: requireAuthentication ?? false,
@ -41,7 +43,6 @@ abstract class UIState implements Built<UIState, UIStateBuilder> {
invoiceUIState: InvoiceUIState(),
// STARTER: constructor - do not remove comment
documentUIState: DocumentUIState(),
expenseUIState: ExpenseUIState(),
vendorUIState: VendorUIState(),
taskUIState: TaskUIState(),
@ -53,6 +54,8 @@ abstract class UIState implements Built<UIState, UIStateBuilder> {
UIState._();
AppLayout get layout;
int get selectedCompanyIndex;
String get currentRoute;
@ -97,3 +100,17 @@ abstract class UIState implements Built<UIState, UIStateBuilder> {
bool containsRoute(String route) => currentRoute.contains(route);
}
class AppLayout extends EnumClass {
const AppLayout._(String name) : super(name);
static Serializer<AppLayout> get serializer => _$appLayoutSerializer;
static const AppLayout mobile = _$mobile;
static const AppLayout tablet = _$tablet;
static const AppLayout desktop = _$desktop;
static BuiltSet<AppLayout> get values => _$values;
static AppLayout valueOf(String name) => _$valueOf(name);
}

View File

@ -6,7 +6,31 @@ part of 'ui_state.dart';
// BuiltValueGenerator
// **************************************************************************
const AppLayout _$mobile = const AppLayout._('mobile');
const AppLayout _$tablet = const AppLayout._('tablet');
const AppLayout _$desktop = const AppLayout._('desktop');
AppLayout _$valueOf(String name) {
switch (name) {
case 'mobile':
return _$mobile;
case 'tablet':
return _$tablet;
case 'desktop':
return _$desktop;
default:
throw new ArgumentError(name);
}
}
final BuiltSet<AppLayout> _$values = new BuiltSet<AppLayout>(const <AppLayout>[
_$mobile,
_$tablet,
_$desktop,
]);
Serializer<UIState> _$uIStateSerializer = new _$UIStateSerializer();
Serializer<AppLayout> _$appLayoutSerializer = new _$AppLayoutSerializer();
class _$UIStateSerializer implements StructuredSerializer<UIState> {
@override
@ -18,6 +42,9 @@ class _$UIStateSerializer implements StructuredSerializer<UIState> {
Iterable<Object> serialize(Serializers serializers, UIState object,
{FullType specifiedType = FullType.unspecified}) {
final result = <Object>[
'layout',
serializers.serialize(object.layout,
specifiedType: const FullType(AppLayout)),
'selectedCompanyIndex',
serializers.serialize(object.selectedCompanyIndex,
specifiedType: const FullType(int)),
@ -93,6 +120,10 @@ class _$UIStateSerializer implements StructuredSerializer<UIState> {
iterator.moveNext();
final dynamic value = iterator.current;
switch (key) {
case 'layout':
result.layout = serializers.deserialize(value,
specifiedType: const FullType(AppLayout)) as AppLayout;
break;
case 'selectedCompanyIndex':
result.selectedCompanyIndex = serializers.deserialize(value,
specifiedType: const FullType(int)) as int;
@ -178,7 +209,26 @@ class _$UIStateSerializer implements StructuredSerializer<UIState> {
}
}
class _$AppLayoutSerializer implements PrimitiveSerializer<AppLayout> {
@override
final Iterable<Type> types = const <Type>[AppLayout];
@override
final String wireName = 'AppLayout';
@override
Object serialize(Serializers serializers, AppLayout object,
{FullType specifiedType = FullType.unspecified}) =>
object.name;
@override
AppLayout deserialize(Serializers serializers, Object serialized,
{FullType specifiedType = FullType.unspecified}) =>
AppLayout.valueOf(serialized as String);
}
class _$UIState extends UIState {
@override
final AppLayout layout;
@override
final int selectedCompanyIndex;
@override
@ -222,7 +272,8 @@ class _$UIState extends UIState {
(new UIStateBuilder()..update(updates)).build();
_$UIState._(
{this.selectedCompanyIndex,
{this.layout,
this.selectedCompanyIndex,
this.currentRoute,
this.enableDarkMode,
this.requireAuthentication,
@ -242,6 +293,9 @@ class _$UIState extends UIState {
this.paymentUIState,
this.quoteUIState})
: super._() {
if (layout == null) {
throw new BuiltValueNullFieldError('UIState', 'layout');
}
if (selectedCompanyIndex == null) {
throw new BuiltValueNullFieldError('UIState', 'selectedCompanyIndex');
}
@ -309,6 +363,7 @@ class _$UIState extends UIState {
bool operator ==(Object other) {
if (identical(other, this)) return true;
return other is UIState &&
layout == other.layout &&
selectedCompanyIndex == other.selectedCompanyIndex &&
currentRoute == other.currentRoute &&
enableDarkMode == other.enableDarkMode &&
@ -351,7 +406,10 @@ class _$UIState extends UIState {
$jc(
$jc(
$jc(
0,
$jc(
0,
layout
.hashCode),
selectedCompanyIndex
.hashCode),
currentRoute
@ -383,6 +441,7 @@ class _$UIState extends UIState {
@override
String toString() {
return (newBuiltValueToStringHelper('UIState')
..add('layout', layout)
..add('selectedCompanyIndex', selectedCompanyIndex)
..add('currentRoute', currentRoute)
..add('enableDarkMode', enableDarkMode)
@ -409,6 +468,10 @@ class _$UIState extends UIState {
class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
_$UIState _$v;
AppLayout _layout;
AppLayout get layout => _$this._layout;
set layout(AppLayout layout) => _$this._layout = layout;
int _selectedCompanyIndex;
int get selectedCompanyIndex => _$this._selectedCompanyIndex;
set selectedCompanyIndex(int selectedCompanyIndex) =>
@ -516,6 +579,7 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
UIStateBuilder get _$this {
if (_$v != null) {
_layout = _$v.layout;
_selectedCompanyIndex = _$v.selectedCompanyIndex;
_currentRoute = _$v.currentRoute;
_enableDarkMode = _$v.enableDarkMode;
@ -559,6 +623,7 @@ class UIStateBuilder implements Builder<UIState, UIStateBuilder> {
try {
_$result = _$v ??
new _$UIState._(
layout: layout,
selectedCompanyIndex: selectedCompanyIndex,
currentRoute: currentRoute,
enableDarkMode: enableDarkMode,

View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/ui/app/app_drawer_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/invoice_screen.dart';
class MainScreen extends StatelessWidget {
static const String route = '/main';
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
AppDrawerBuilder(),
//Expanded(child: DashboardScreen()),
Expanded(
flex: 3,
child: InvoiceScreen(),
),
Expanded(
flex: 5,
child: InvoiceEditScreen(),
),
],
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/redux/app/app_actions.dart';
import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart';
import 'package:invoiceninja_flutter/ui/app/app_builder.dart';
import 'package:invoiceninja_flutter/utils/platforms.dart';
@ -63,8 +64,14 @@ class LoginVM {
);
void _handleLogin(BuildContext context) {
store.dispatch(UpdateLayout(calculateLayout(context)));
AppBuilder.of(context).rebuild();
store.dispatch(ViewDashboard(context));
if (isMobile(context)) {
store.dispatch(ViewDashboard(context));
} else {
store.dispatch(ViewMainScreen(context));
}
}
return LoginVM(

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/ui/ui_state.dart';
bool isAndroid(BuildContext context) =>
Theme.of(context).platform == TargetPlatform.android;
@ -17,3 +20,24 @@ String getPlatform(BuildContext context) =>
String getAppURL(BuildContext context) =>
isAndroid(context) ? kGoogleStoreUrl : kAppleStoreUrl;
AppLayout calculateLayout(BuildContext context) {
final size = MediaQuery.of(context).size.shortestSide;
if (size < kMobileLayoutWidth) {
return AppLayout.mobile;
} else if (size > kTabletLayoutWidth) {
return AppLayout.desktop;
} else {
return AppLayout.tablet;
}
}
AppLayout getLayout(BuildContext context) =>
StoreProvider.of<AppState>(context).state.uiState.layout ??
AppLayout.mobile;
bool isMobile(BuildContext context) => getLayout(context) == AppLayout.mobile;
bool isTablet(BuildContext context) => getLayout(context) == AppLayout.tablet;
bool isDesktop(BuildContext context) => getLayout(context) == AppLayout.desktop;