Implement desktop client layout

This commit is contained in:
Hillel Coren 2022-05-30 15:04:59 +03:00
parent 9680cf6833
commit d38307d902
8 changed files with 244 additions and 171 deletions

View File

@ -565,6 +565,10 @@ Serializers _$serializers = (new Serializers().toBuilder()
const FullType(BuiltMap,
const [const FullType(EntityType), const FullType(bool)]),
() => new MapBuilder<EntityType, bool>())
..addBuilderFactory(
const FullType(BuiltMap,
const [const FullType(EntityType), const FullType(bool)]),
() => new MapBuilder<EntityType, bool>())
..addBuilderFactory(
const FullType(
BuiltMap, const [const FullType(String), const FullType(String)]),

View File

@ -1420,6 +1420,9 @@ void selectEntity({
} else if (isDesktop(context) && !state.prefState.isPreviewVisible) {
if (uiState.isEditing && entityUIState.editingId == entity.id) {
viewEntitiesByType(entityType: entity.entityType);
} else if (entity.entityType == EntityType.client &&
state.prefState.isViewerFullScreen(EntityType.client)) {
viewEntitiesByType(entityType: EntityType.invoice, filterEntity: entity);
} else {
if (!state.prefState.isPreviewVisible) {
store.dispatch(TogglePreviewSidebar());

View File

@ -20,6 +20,7 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
moduleLayout: ModuleLayout.table,
isPreviewVisible: false,
useSidebarEditor: BuiltMap<EntityType, bool>(),
useSidebarViewer: BuiltMap<EntityType, bool>(),
menuSidebarMode: AppSidebarMode.collapse,
historySidebarMode: AppSidebarMode.float,
rowsPerPage: 10,
@ -102,6 +103,8 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
BuiltMap<EntityType, bool> get useSidebarEditor;
BuiltMap<EntityType, bool> get useSidebarViewer;
BuiltMap<String, String> get customColors;
bool get isPreviewVisible;
@ -169,6 +172,18 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
return !(useSidebarEditor[entityType.baseType] ?? false);
}
bool isViewerFullScreen(EntityType entityType) {
if (!isDesktop) {
return false;
}
if (![EntityType.client].contains(entityType)) {
return false;
}
return !(useSidebarViewer[entityType.baseType] ?? false);
}
bool get isNotDesktop => !isDesktop;
bool get isMobile => appLayout == AppLayout.mobile;
@ -201,6 +216,7 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
// ignore: unused_element
static void _initializeBuilder(PrefStateBuilder builder) => builder
..useSidebarEditor.replace(BuiltMap<EntityType, bool>())
..useSidebarViewer.replace(BuiltMap<EntityType, bool>())
..sortFields.replace(BuiltMap<EntityType, PrefStateSortField>())
..customColors.replace(builder.enableDarkMode == true
? BuiltMap<String, String>()

View File

@ -129,6 +129,10 @@ class _$PrefStateSerializer implements StructuredSerializer<PrefState> {
serializers.serialize(object.useSidebarEditor,
specifiedType: const FullType(BuiltMap,
const [const FullType(EntityType), const FullType(bool)])),
'useSidebarViewer',
serializers.serialize(object.useSidebarViewer,
specifiedType: const FullType(BuiltMap,
const [const FullType(EntityType), const FullType(bool)])),
'customColors',
serializers.serialize(object.customColors,
specifiedType: const FullType(BuiltMap,
@ -245,6 +249,11 @@ class _$PrefStateSerializer implements StructuredSerializer<PrefState> {
specifiedType: const FullType(BuiltMap,
const [const FullType(EntityType), const FullType(bool)])));
break;
case 'useSidebarViewer':
result.useSidebarViewer.replace(serializers.deserialize(value,
specifiedType: const FullType(BuiltMap,
const [const FullType(EntityType), const FullType(bool)])));
break;
case 'customColors':
result.customColors.replace(serializers.deserialize(value,
specifiedType: const FullType(BuiltMap,
@ -589,6 +598,8 @@ class _$PrefState extends PrefState {
@override
final BuiltMap<EntityType, bool> useSidebarEditor;
@override
final BuiltMap<EntityType, bool> useSidebarViewer;
@override
final BuiltMap<String, String> customColors;
@override
final bool isPreviewVisible;
@ -646,6 +657,7 @@ class _$PrefState extends PrefState {
this.menuSidebarMode,
this.historySidebarMode,
this.useSidebarEditor,
this.useSidebarViewer,
this.customColors,
this.isPreviewVisible,
this.isMenuVisible,
@ -680,6 +692,8 @@ class _$PrefState extends PrefState {
historySidebarMode, 'PrefState', 'historySidebarMode');
BuiltValueNullFieldError.checkNotNull(
useSidebarEditor, 'PrefState', 'useSidebarEditor');
BuiltValueNullFieldError.checkNotNull(
useSidebarViewer, 'PrefState', 'useSidebarViewer');
BuiltValueNullFieldError.checkNotNull(
customColors, 'PrefState', 'customColors');
BuiltValueNullFieldError.checkNotNull(
@ -745,6 +759,7 @@ class _$PrefState extends PrefState {
menuSidebarMode == other.menuSidebarMode &&
historySidebarMode == other.historySidebarMode &&
useSidebarEditor == other.useSidebarEditor &&
useSidebarViewer == other.useSidebarViewer &&
customColors == other.customColors &&
isPreviewVisible == other.isPreviewVisible &&
isMenuVisible == other.isMenuVisible &&
@ -792,7 +807,7 @@ class _$PrefState extends PrefState {
$jc(
$jc(
$jc(
$jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, appLayout.hashCode), moduleLayout.hashCode), menuSidebarMode.hashCode), historySidebarMode.hashCode), useSidebarEditor.hashCode), customColors.hashCode), isPreviewVisible.hashCode), isMenuVisible.hashCode), showKanban.hashCode), showPdfPreview.hashCode),
$jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc($jc(0, appLayout.hashCode), moduleLayout.hashCode), menuSidebarMode.hashCode), historySidebarMode.hashCode), useSidebarEditor.hashCode), useSidebarViewer.hashCode), customColors.hashCode), isPreviewVisible.hashCode), isMenuVisible.hashCode), showKanban.hashCode), showPdfPreview.hashCode),
enableTouchEvents.hashCode),
isHistoryVisible.hashCode),
enableDarkMode.hashCode),
@ -822,6 +837,7 @@ class _$PrefState extends PrefState {
..add('menuSidebarMode', menuSidebarMode)
..add('historySidebarMode', historySidebarMode)
..add('useSidebarEditor', useSidebarEditor)
..add('useSidebarViewer', useSidebarViewer)
..add('customColors', customColors)
..add('isPreviewVisible', isPreviewVisible)
..add('isMenuVisible', isMenuVisible)
@ -878,6 +894,12 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
set useSidebarEditor(MapBuilder<EntityType, bool> useSidebarEditor) =>
_$this._useSidebarEditor = useSidebarEditor;
MapBuilder<EntityType, bool> _useSidebarViewer;
MapBuilder<EntityType, bool> get useSidebarViewer =>
_$this._useSidebarViewer ??= new MapBuilder<EntityType, bool>();
set useSidebarViewer(MapBuilder<EntityType, bool> useSidebarViewer) =>
_$this._useSidebarViewer = useSidebarViewer;
MapBuilder<String, String> _customColors;
MapBuilder<String, String> get customColors =>
_$this._customColors ??= new MapBuilder<String, String>();
@ -1007,6 +1029,7 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
_menuSidebarMode = $v.menuSidebarMode;
_historySidebarMode = $v.historySidebarMode;
_useSidebarEditor = $v.useSidebarEditor.toBuilder();
_useSidebarViewer = $v.useSidebarViewer.toBuilder();
_customColors = $v.customColors.toBuilder();
_isPreviewVisible = $v.isPreviewVisible;
_isMenuVisible = $v.isMenuVisible;
@ -1062,6 +1085,7 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
historySidebarMode: BuiltValueNullFieldError.checkNotNull(
historySidebarMode, 'PrefState', 'historySidebarMode'),
useSidebarEditor: useSidebarEditor.build(),
useSidebarViewer: useSidebarViewer.build(),
customColors: customColors.build(),
isPreviewVisible: BuiltValueNullFieldError.checkNotNull(
isPreviewVisible, 'PrefState', 'isPreviewVisible'),
@ -1095,6 +1119,8 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
try {
_$failedField = 'useSidebarEditor';
useSidebarEditor.build();
_$failedField = 'useSidebarViewer';
useSidebarViewer.build();
_$failedField = 'customColors';
customColors.build();

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
// Package imports:
import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/ui/app/screen_imports.dart';
import 'package:overflow_view/overflow_view.dart';
// Project imports:
@ -44,172 +45,186 @@ class EntityTopFilter extends StatelessWidget {
return Material(
color: backgroundColor,
child: AnimatedContainer(
height: show ? 46 : 0,
duration: Duration(milliseconds: kDefaultAnimationDuration),
curve: Curves.easeInOutCubic,
child: AnimatedOpacity(
opacity: show ? 1 : 0,
duration: Duration(milliseconds: kDefaultAnimationDuration),
curve: Curves.easeInOutCubic,
child: filterEntity == null
? Container(
color: backgroundColor,
)
: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!prefState.isFilterVisible)
InkWell(
onTap: () {
store.dispatch(UpdateUserPreferences(
isFilterVisible: !prefState.isFilterVisible));
},
onLongPress: () {
editEntity(context: context, entity: filterEntity);
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 12),
Icon(
Icons.chrome_reader_mode,
color: state.headerTextColor,
),
SizedBox(width: 12),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 220),
child: Text(
EntityPresenter()
.initialize(filterEntity, context)
.title(),
style: TextStyle(
fontSize: 17, color: state.headerTextColor),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
SizedBox(width: 12),
],
),
),
SizedBox(width: 12),
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: OverflowView.flexible(
spacing: 4,
children: <Widget>[
for (int i = 0; i < relatedTypes.length; i++)
DecoratedBox(
child: TextButton(
child: Column(
children: [
if (prefState.isViewerFullScreen(state.uiState.filterEntityType))
Expanded(
child: ClientViewScreen(
isTopFilter: true,
)),
AnimatedContainer(
height: show ? 46 : 0,
duration: Duration(milliseconds: kDefaultAnimationDuration),
curve: Curves.easeInOutCubic,
child: AnimatedOpacity(
opacity: show ? 1 : 0,
duration: Duration(milliseconds: kDefaultAnimationDuration),
curve: Curves.easeInOutCubic,
child: filterEntity == null
? Container(
color: backgroundColor,
)
: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!prefState.isFilterVisible)
InkWell(
onTap: () {
store.dispatch(UpdateUserPreferences(
isFilterVisible: !prefState.isFilterVisible));
},
onLongPress: () {
editEntity(
context: context, entity: filterEntity);
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: 12),
Icon(
Icons.chrome_reader_mode,
color: state.headerTextColor,
),
SizedBox(width: 12),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 220),
child: Text(
localization
.lookup('${relatedTypes[i].plural}'),
EntityPresenter()
.initialize(filterEntity, context)
.title(),
style: TextStyle(
color: state.headerTextColor,
),
),
style: TextButton.styleFrom(
padding:
EdgeInsets.symmetric(horizontal: 12),
minimumSize: Size(0, 36),
),
onPressed: () {
viewEntitiesByType(
entityType: relatedTypes[i],
filterEntity: filterEntity,
);
},
onLongPress: () {
handleEntityAction(
filterEntity,
EntityAction.newEntityType(
relatedTypes[i]));
},
),
decoration: BoxDecoration(
border: relatedTypes[i] == routeEntityType
? Border(
bottom: BorderSide(
color: prefState.enableDarkMode ||
!state.hasAccentColor
? state.accentColor
: Colors.white,
width: 2,
),
)
: null,
),
)
],
builder: (context, remaining) {
return PopupMenuButton<EntityType>(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 4),
child: Row(
children: [
Text(
localization.more,
style: TextStyle(
color: state.headerTextColor),
),
SizedBox(width: 4),
Icon(Icons.arrow_drop_down,
fontSize: 17,
color: state.headerTextColor),
],
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
),
initialValue: routeEntityType,
onSelected: (EntityType value) {
if (value == filterEntityType) {
viewEntity(
entity: filterEntity,
);
} else {
viewEntitiesByType(
entityType: value,
filterEntity: filterEntity,
);
}
},
itemBuilder: (BuildContext context) =>
filterEntityType.relatedTypes
.sublist(relatedTypes.length - remaining)
.where((element) => state.company
.isModuleEnabled(element))
.map((type) => PopupMenuItem<EntityType>(
value: type,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 75,
SizedBox(width: 12),
],
),
),
SizedBox(width: 12),
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: OverflowView.flexible(
spacing: 4,
children: <Widget>[
for (int i = 0; i < relatedTypes.length; i++)
DecoratedBox(
child: TextButton(
child: Text(
localization.lookup(
'${relatedTypes[i].plural}'),
style: TextStyle(
color: state.headerTextColor,
),
),
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: 12),
minimumSize: Size(0, 36),
),
onPressed: () {
viewEntitiesByType(
entityType: relatedTypes[i],
filterEntity: filterEntity,
);
},
onLongPress: () {
handleEntityAction(
filterEntity,
EntityAction.newEntityType(
relatedTypes[i]));
},
),
decoration: BoxDecoration(
border: relatedTypes[i] == routeEntityType
? Border(
bottom: BorderSide(
color: prefState
.enableDarkMode ||
!state.hasAccentColor
? state.accentColor
: Colors.white,
width: 2,
),
child: Text(type ==
filterEntityType
? localization.overview
: '${localization.lookup(type.plural)}'),
),
))
.toList(),
);
},
)
: null,
),
)
],
builder: (context, remaining) {
return PopupMenuButton<EntityType>(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4),
child: Row(
children: [
Text(
localization.more,
style: TextStyle(
color: state.headerTextColor),
),
SizedBox(width: 4),
Icon(Icons.arrow_drop_down,
color: state.headerTextColor),
],
),
),
initialValue: routeEntityType,
onSelected: (EntityType value) {
if (value == filterEntityType) {
viewEntity(
entity: filterEntity,
);
} else {
viewEntitiesByType(
entityType: value,
filterEntity: filterEntity,
);
}
},
itemBuilder: (BuildContext context) =>
filterEntityType.relatedTypes
.sublist(
relatedTypes.length - remaining)
.where((element) => state.company
.isModuleEnabled(element))
.map((type) =>
PopupMenuItem<EntityType>(
value: type,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 75,
),
child: Text(type ==
filterEntityType
? localization.overview
: '${localization.lookup(type.plural)}'),
),
))
.toList(),
);
},
),
),
),
),
SizedBox(width: 4),
IconButton(
icon: Icon(
Icons.clear,
color: state.headerTextColor,
),
onPressed: () => store.dispatch(
FilterByEntity(entity: uiState.filterEntity),
),
),
],
),
SizedBox(width: 4),
IconButton(
icon: Icon(
Icons.clear,
color: state.headerTextColor,
),
onPressed: () => store.dispatch(
FilterByEntity(entity: uiState.filterEntity),
),
),
],
),
),
),
),
],
),
);
}

View File

@ -687,7 +687,11 @@ class EntityScreens extends StatelessWidget {
? listWidget
: Column(
children: [
topFilterChild,
if (prefState.isViewerFullScreen(
state.uiState.filterEntityType))
Expanded(child: topFilterChild)
else
topFilterChild,
Expanded(
child: AppBorder(
isTop: uiState.filterEntityType != null &&
@ -705,16 +709,9 @@ class EntityScreens extends StatelessWidget {
Expanded(
flex: isFullScreen ? (listFlex + previewFlex) : previewFlex,
child: AppBorder(
child: Column(
children: [
if (isFullScreen) topFilterChild,
Expanded(
child: AppBorder(
child: child,
isTop: isFullScreen && uiState.filterEntityType != null,
),
),
],
child: AppBorder(
child: child,
isTop: isFullScreen && uiState.filterEntityType != null,
),
isLeft: true,
),

View File

@ -25,11 +25,13 @@ class ClientView extends StatefulWidget {
Key key,
@required this.viewModel,
@required this.isFilter,
@required this.isTopFilter,
@required this.tabIndex,
}) : super(key: key);
final ClientViewVM viewModel;
final bool isFilter;
final bool isTopFilter;
final int tabIndex;
@override
@ -86,6 +88,13 @@ class _ClientViewState extends State<ClientView>
final documents = client.documents;
final userCompany = viewModel.state.userCompany;
if (widget.isTopFilter) {
return ViewScaffold(
body: Placeholder(),
entity: client,
);
}
return ViewScaffold(
isFilter: widget.isFilter,
entity: client,

View File

@ -25,8 +25,10 @@ class ClientViewScreen extends StatelessWidget {
const ClientViewScreen({
Key key,
this.isFilter = false,
this.isTopFilter = false,
}) : super(key: key);
final bool isFilter;
final bool isTopFilter;
static const String route = '/client/view';
@ -41,6 +43,7 @@ class ClientViewScreen extends StatelessWidget {
return ClientView(
viewModel: vm,
isFilter: isFilter,
isTopFilter: isTopFilter,
tabIndex: vm.state.clientUIState.tabIndex,
);
},