mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: add borders on mobile grid (#4146)
* feat: mobile grid ui improvements * chore: restore Podfile.lock
This commit is contained in:
parent
d26606d3d3
commit
d862455342
@ -20,7 +20,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/disclosure_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/desktop_field_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
|
||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/mobile/presentation/database/board/board.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
@ -81,31 +82,47 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
final showCreateGroupButton =
|
||||
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
|
||||
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
|
||||
return AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints: BoxConstraints.tightFor(width: screenWidth * 0.7),
|
||||
config: config,
|
||||
leading: showHiddenGroups
|
||||
? MobileHiddenGroupsColumn(padding: config.groupHeaderPadding)
|
||||
: const HSpace(16),
|
||||
trailing: showCreateGroupButton
|
||||
? const MobileBoardTrailing()
|
||||
: const HSpace(16),
|
||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: GroupCardHeader(
|
||||
groupData: groupData,
|
||||
return Column(
|
||||
children: [
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
indent: GridSize.leadingHeaderPadding / 2,
|
||||
endIndent: GridSize.leadingHeaderPadding / 2,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context: context,
|
||||
afGroupData: column,
|
||||
afGroupItem: columnItem,
|
||||
cardMargin: config.cardMargin,
|
||||
),
|
||||
Expanded(
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints:
|
||||
BoxConstraints.tightFor(width: screenWidth * 0.7),
|
||||
config: config,
|
||||
leading: showHiddenGroups
|
||||
? MobileHiddenGroupsColumn(
|
||||
padding: config.groupHeaderPadding,
|
||||
)
|
||||
: const HSpace(16),
|
||||
trailing: showCreateGroupButton
|
||||
? const MobileBoardTrailing()
|
||||
: const HSpace(16),
|
||||
headerBuilder: (_, groupData) =>
|
||||
BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: GroupCardHeader(
|
||||
groupData: groupData,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context: context,
|
||||
afGroupData: column,
|
||||
afGroupItem: columnItem,
|
||||
cardMargin: config.cardMargin,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -186,28 +186,45 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
EventController eventController,
|
||||
int firstDayOfWeek,
|
||||
) {
|
||||
return Padding(
|
||||
padding: PlatformExtension.isMobile
|
||||
? CalendarSize.contentInsetsMobile
|
||||
: CalendarSize.contentInsets,
|
||||
child: LayoutBuilder(
|
||||
// must specify MonthView width for useAvailableVerticalSpace to work properly
|
||||
builder: (context, constraints) => ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: MonthView(
|
||||
key: _calendarState,
|
||||
controller: _eventController,
|
||||
width: constraints.maxWidth,
|
||||
cellAspectRatio: PlatformExtension.isMobile ? 0.9 : 0.6,
|
||||
startDay: _weekdayFromInt(firstDayOfWeek),
|
||||
showBorder: false,
|
||||
headerBuilder: _headerNavigatorBuilder,
|
||||
weekDayBuilder: _headerWeekDayBuilder,
|
||||
cellBuilder: _calendarDayBuilder,
|
||||
useAvailableVerticalSpace: widget.shrinkWrap,
|
||||
return LayoutBuilder(
|
||||
// must specify MonthView width for useAvailableVerticalSpace to work properly
|
||||
builder: (context, constraints) {
|
||||
Widget calendar = Padding(
|
||||
padding: PlatformExtension.isMobile
|
||||
? CalendarSize.contentInsetsMobile
|
||||
: CalendarSize.contentInsets,
|
||||
child: ScrollConfiguration(
|
||||
behavior:
|
||||
ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: MonthView(
|
||||
key: _calendarState,
|
||||
controller: _eventController,
|
||||
width: constraints.maxWidth,
|
||||
cellAspectRatio: PlatformExtension.isMobile ? 0.9 : 0.6,
|
||||
startDay: _weekdayFromInt(firstDayOfWeek),
|
||||
showBorder: false,
|
||||
headerBuilder: _headerNavigatorBuilder,
|
||||
weekDayBuilder: _headerWeekDayBuilder,
|
||||
cellBuilder: _calendarDayBuilder,
|
||||
useAvailableVerticalSpace: widget.shrinkWrap,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (PlatformExtension.isMobile) {
|
||||
calendar = Column(
|
||||
children: [
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
indent: GridSize.leadingHeaderPadding / 2,
|
||||
endIndent: GridSize.leadingHeaderPadding / 2,
|
||||
),
|
||||
Expanded(child: calendar),
|
||||
],
|
||||
);
|
||||
}
|
||||
return calendar;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/shortcuts.dart';
|
||||
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/mobile_database_settings_button.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@ -21,13 +22,10 @@ import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
||||
|
||||
import 'grid_page.dart';
|
||||
import 'grid_scroll.dart';
|
||||
import 'layout/layout.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'widgets/footer/grid_footer.dart';
|
||||
import 'widgets/header/grid_header.dart';
|
||||
import 'widgets/header/mobile_grid_header.dart';
|
||||
import 'widgets/mobile_fab.dart';
|
||||
import 'widgets/row/mobile_row.dart';
|
||||
import 'widgets/shortcuts.dart';
|
||||
|
||||
class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
final _toggleExtension = ToggleExtensionNotifier();
|
||||
@ -102,9 +100,7 @@ class _MobileGridPageState extends State<MobileGridPage> {
|
||||
loading: (_) =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => GridShortcuts(
|
||||
child: GridPageContent(view: widget.view),
|
||||
),
|
||||
(_) => GridShortcuts(child: GridPageContent(view: widget.view)),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
@ -170,11 +166,8 @@ class _GridPageContentState extends State<GridPageContent> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: GridSize.leadingHeaderPadding),
|
||||
child: _GridHeader(
|
||||
headerScrollController: headerScrollController,
|
||||
),
|
||||
_GridHeader(
|
||||
headerScrollController: headerScrollController,
|
||||
),
|
||||
_GridRows(
|
||||
viewId: widget.view.id,
|
||||
@ -201,7 +194,7 @@ class _GridHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridBloc, GridState>(
|
||||
builder: (context, state) {
|
||||
return GridHeaderSliverAdaptor(
|
||||
return MobileGridHeader(
|
||||
viewId: state.viewId,
|
||||
anchorScrollController: headerScrollController,
|
||||
);
|
||||
@ -224,7 +217,8 @@ class _GridRows extends StatelessWidget {
|
||||
return BlocBuilder<GridBloc, GridState>(
|
||||
buildWhen: (previous, current) => previous.fields != current.fields,
|
||||
builder: (context, state) {
|
||||
final contentWidth = GridLayout.headerWidth(state.fields);
|
||||
final double contentWidth =
|
||||
(state.fields.length + 1) * 200 + GridSize.leadingHeaderPadding;
|
||||
return Expanded(
|
||||
child: _WrapScrollView(
|
||||
scrollController: scrollController,
|
||||
@ -238,14 +232,13 @@ class _GridRows extends StatelessWidget {
|
||||
orElse: () => false,
|
||||
),
|
||||
builder: (context, state) {
|
||||
final rowInfos = state.rowInfos;
|
||||
final behavior = ScrollConfiguration.of(context).copyWith(
|
||||
scrollbars: false,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
);
|
||||
return ScrollConfiguration(
|
||||
behavior: behavior,
|
||||
child: _renderList(context, contentWidth, state, rowInfos),
|
||||
child: _renderList(context, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -257,11 +250,9 @@ class _GridRows extends StatelessWidget {
|
||||
|
||||
Widget _renderList(
|
||||
BuildContext context,
|
||||
double contentWidth,
|
||||
GridState state,
|
||||
List<RowInfo> rowInfos,
|
||||
) {
|
||||
final children = rowInfos.mapIndexed((index, rowInfo) {
|
||||
final children = state.rowInfos.mapIndexed((index, rowInfo) {
|
||||
return ReorderableDelayedDragStartListener(
|
||||
key: ValueKey(rowInfo.rowMeta.id),
|
||||
index: index,
|
||||
@ -288,27 +279,14 @@ class _GridRows extends StatelessWidget {
|
||||
}
|
||||
context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));
|
||||
},
|
||||
itemCount: rowInfos.length,
|
||||
itemCount: state.rowInfos.length,
|
||||
itemBuilder: (context, index) => children[index],
|
||||
header: Padding(
|
||||
padding: EdgeInsets.only(left: GridSize.leadingHeaderPadding),
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: contentWidth,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
footer: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: GridSize.footerContentInsets,
|
||||
child: const SizedBox(
|
||||
height: 42,
|
||||
child: GridAddRowButton(
|
||||
key: Key('gridFooter'),
|
||||
),
|
||||
),
|
||||
child: _AddRowButton(),
|
||||
),
|
||||
Container(
|
||||
height: 30,
|
||||
@ -431,3 +409,46 @@ class _GridFooter extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddRowButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderSide = BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1.0,
|
||||
);
|
||||
const radius = BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
);
|
||||
final decoration = BoxDecoration(
|
||||
borderRadius: radius,
|
||||
border: BorderDirectional(
|
||||
start: borderSide,
|
||||
end: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
);
|
||||
return Container(
|
||||
height: 54,
|
||||
decoration: decoration,
|
||||
child: FlowyButton(
|
||||
text: FlowyText(
|
||||
LocaleKeys.grid_row_newRow.tr(),
|
||||
fontSize: 15,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
radius: radius,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()),
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: const Size.square(18),
|
||||
),
|
||||
leftIconSize: const Size.square(18),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,9 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/mobile_field_cell.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -17,7 +14,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:reorderables/reorderables.dart';
|
||||
|
||||
import '../../layout/sizes.dart';
|
||||
import 'field_cell.dart';
|
||||
import 'desktop_field_cell.dart';
|
||||
|
||||
class GridHeaderSliverAdaptor extends StatefulWidget {
|
||||
final String viewId;
|
||||
@ -81,38 +78,25 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridHeaderBloc, GridHeaderState>(
|
||||
builder: (context, state) {
|
||||
final fields = [...state.fields];
|
||||
FieldInfo? firstField;
|
||||
if (PlatformExtension.isMobile && fields.isNotEmpty) {
|
||||
firstField = fields.removeAt(0);
|
||||
}
|
||||
|
||||
final cells = fields
|
||||
final cells = state.fields
|
||||
.map(
|
||||
(fieldInfo) => PlatformExtension.isDesktop
|
||||
? GridFieldCell(
|
||||
key: _getKeyById(fieldInfo.id),
|
||||
viewId: widget.viewId,
|
||||
fieldInfo: fieldInfo,
|
||||
fieldController: widget.fieldController,
|
||||
onTap: () => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.startEditingField(fieldInfo.id)),
|
||||
onFieldInsertedOnEitherSide: (fieldId) => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.startEditingNewField(fieldId)),
|
||||
onEditorOpened: () => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(const GridHeaderEvent.endEditingField()),
|
||||
isEditing: state.editingFieldId == fieldInfo.id,
|
||||
isNew: state.newFieldId == fieldInfo.id,
|
||||
)
|
||||
: MobileFieldButton(
|
||||
key: _getKeyById(fieldInfo.id),
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
fieldInfo: fieldInfo,
|
||||
),
|
||||
(fieldInfo) => GridFieldCell(
|
||||
key: _getKeyById(fieldInfo.id),
|
||||
viewId: widget.viewId,
|
||||
fieldInfo: fieldInfo,
|
||||
fieldController: widget.fieldController,
|
||||
onTap: () => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.startEditingField(fieldInfo.id)),
|
||||
onFieldInsertedOnEitherSide: (fieldId) => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.startEditingNewField(fieldId)),
|
||||
onEditorOpened: () => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(const GridHeaderEvent.endEditingField()),
|
||||
isEditing: state.editingFieldId == fieldInfo.id,
|
||||
isNew: state.newFieldId == fieldInfo.id,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
@ -124,15 +108,10 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
child: child,
|
||||
),
|
||||
draggingWidgetOpacity: 0,
|
||||
header: _cellLeading(firstField),
|
||||
header: _cellLeading(),
|
||||
needsLongPressDraggable: PlatformExtension.isMobile,
|
||||
footer: _CellTrailing(viewId: widget.viewId),
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
// to offset removing the first field from `state.fields`
|
||||
if (PlatformExtension.isMobile) {
|
||||
oldIndex++;
|
||||
newIndex++;
|
||||
}
|
||||
context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.moveField(oldIndex, newIndex));
|
||||
@ -157,24 +136,8 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
return newKey;
|
||||
}
|
||||
|
||||
Widget _cellLeading(FieldInfo? fieldInfo) {
|
||||
if (PlatformExtension.isDesktop) {
|
||||
return SizedBox(width: GridSize.leadingHeaderPadding);
|
||||
} else {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: GridSize.leadingHeaderPadding),
|
||||
if (fieldInfo != null)
|
||||
MobileFieldButton(
|
||||
key: _getKeyById(fieldInfo.id),
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
fieldInfo: fieldInfo,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
Widget _cellLeading() {
|
||||
return SizedBox(width: GridSize.leadingHeaderPadding);
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,15 +148,13 @@ class _CellTrailing extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderSide =
|
||||
BorderSide(color: Theme.of(context).dividerColor, width: 1.0);
|
||||
return Container(
|
||||
width: GridSize.trailHeaderPadding,
|
||||
decoration: PlatformExtension.isDesktop
|
||||
? BoxDecoration(
|
||||
border: Border(bottom: borderSide),
|
||||
)
|
||||
: null,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1.0),
|
||||
),
|
||||
),
|
||||
padding: GridSize.headerContentInsets,
|
||||
child: CreateFieldButton(
|
||||
viewId: viewId,
|
||||
@ -223,34 +184,25 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
margin: PlatformExtension.isDesktop
|
||||
? GridSize.cellContentInsets
|
||||
: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
|
||||
margin: GridSize.cellContentInsets,
|
||||
radius: BorderRadius.zero,
|
||||
text: FlowyText(
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
fontSize: PlatformExtension.isDesktop ? null : 15,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
||||
onTap: () async {
|
||||
if (PlatformExtension.isMobile) {
|
||||
showCreateFieldBottomSheet(context, widget.viewId);
|
||||
} else {
|
||||
final result = await FieldBackendService.createField(
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
result.fold(
|
||||
(typeOptionPB) => widget.onFieldCreated(typeOptionPB.field_2.id),
|
||||
(err) => Log.error("Failed to create field type option: $err"),
|
||||
);
|
||||
}
|
||||
final result = await FieldBackendService.createField(
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
result.fold(
|
||||
(typeOptionPB) => widget.onFieldCreated(typeOptionPB.field_2.id),
|
||||
(err) => Log.error("Failed to create field type option: $err"),
|
||||
);
|
||||
},
|
||||
leftIcon: FlowySvg(
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
size: const Size.square(18),
|
||||
color: PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
|
||||
size: Size.square(18),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -9,30 +9,39 @@ import 'field_type_extension.dart';
|
||||
|
||||
class MobileFieldButton extends StatelessWidget {
|
||||
final String viewId;
|
||||
final int? index;
|
||||
final FieldController fieldController;
|
||||
final FieldInfo fieldInfo;
|
||||
final int? maxLines;
|
||||
final BorderRadius? radius;
|
||||
final EdgeInsets? margin;
|
||||
|
||||
const MobileFieldButton({
|
||||
super.key,
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
required this.fieldInfo,
|
||||
this.maxLines = 1,
|
||||
this.radius = BorderRadius.zero,
|
||||
this.margin,
|
||||
required this.index,
|
||||
}) : radius = BorderRadius.zero,
|
||||
margin = const EdgeInsets.symmetric(vertical: 14, horizontal: 12);
|
||||
|
||||
const MobileFieldButton.first({
|
||||
super.key,
|
||||
});
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
required this.fieldInfo,
|
||||
}) : radius = const BorderRadius.only(topLeft: Radius.circular(24)),
|
||||
margin = const EdgeInsets.symmetric(vertical: 14, horizontal: 18),
|
||||
index = null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: fieldInfo.fieldSettings!.width.toDouble(),
|
||||
Widget child = Container(
|
||||
width: 200,
|
||||
decoration: _getDecoration(context),
|
||||
child: FlowyButton(
|
||||
onTap: () => showQuickEditField(context, viewId, fieldInfo),
|
||||
radius: BorderRadius.zero,
|
||||
margin: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
|
||||
radius: radius,
|
||||
margin: margin,
|
||||
leftIconSize: const Size.square(18),
|
||||
leftIcon: FlowySvg(
|
||||
fieldInfo.fieldType.icon(),
|
||||
@ -41,10 +50,36 @@ class MobileFieldButton extends StatelessWidget {
|
||||
text: FlowyText(
|
||||
fieldInfo.name,
|
||||
fontSize: 15,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (index != null) {
|
||||
child = ReorderableDelayedDragStartListener(index: index!, child: child);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
BoxDecoration? _getDecoration(BuildContext context) {
|
||||
final borderSide = BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1.0,
|
||||
);
|
||||
|
||||
if (index == null) {
|
||||
return BoxDecoration(
|
||||
borderRadius: const BorderRadiusDirectional.only(
|
||||
topStart: Radius.circular(24),
|
||||
),
|
||||
border: BorderDirectional(
|
||||
top: borderSide,
|
||||
start: borderSide,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/field/bottom_sheet_create_field.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../layout/sizes.dart';
|
||||
import 'mobile_field_button.dart';
|
||||
|
||||
class MobileGridHeader extends StatefulWidget {
|
||||
final String viewId;
|
||||
final ScrollController anchorScrollController;
|
||||
|
||||
const MobileGridHeader({
|
||||
required this.viewId,
|
||||
required this.anchorScrollController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MobileGridHeader> createState() => _MobileGridHeaderState();
|
||||
}
|
||||
|
||||
class _MobileGridHeaderState extends State<MobileGridHeader> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fieldController =
|
||||
context.read<GridBloc>().databaseController.fieldController;
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return GridHeaderBloc(
|
||||
viewId: widget.viewId,
|
||||
fieldController: fieldController,
|
||||
)..add(const GridHeaderEvent.initial());
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: widget.anchorScrollController,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
HSpace(GridSize.leadingHeaderPadding),
|
||||
Stack(
|
||||
children: [
|
||||
Positioned(top: 0, left: 24, right: 24, child: _divider()),
|
||||
Positioned(bottom: 0, left: 0, right: 0, child: _divider()),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: _GridHeader(
|
||||
viewId: widget.viewId,
|
||||
fieldController: fieldController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const HSpace(20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _divider() {
|
||||
return Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridHeader extends StatefulWidget {
|
||||
final String viewId;
|
||||
final FieldController fieldController;
|
||||
|
||||
const _GridHeader({
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_GridHeader> createState() => _GridHeaderState();
|
||||
}
|
||||
|
||||
class _GridHeaderState extends State<_GridHeader> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridHeaderBloc, GridHeaderState>(
|
||||
builder: (context, state) {
|
||||
final fields = [...state.fields];
|
||||
FieldInfo? firstField;
|
||||
if (fields.isNotEmpty) {
|
||||
firstField = fields.removeAt(0);
|
||||
}
|
||||
|
||||
final cells = fields
|
||||
.mapIndexed(
|
||||
(index, fieldInfo) => MobileFieldButton(
|
||||
key: ValueKey(fieldInfo.id),
|
||||
index: index,
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
fieldInfo: fieldInfo,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return ReorderableListView.builder(
|
||||
scrollController: ScrollController(),
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
proxyDecorator: (child, index, anim) => Material(
|
||||
color: Colors.transparent,
|
||||
child: child,
|
||||
),
|
||||
header: firstField != null
|
||||
? MobileFieldButton.first(
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
fieldInfo: firstField,
|
||||
)
|
||||
: null,
|
||||
footer: CreateFieldButton(
|
||||
viewId: widget.viewId,
|
||||
onFieldCreated: (fieldId) => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.startEditingNewField(fieldId)),
|
||||
),
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
oldIndex++;
|
||||
newIndex++;
|
||||
context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.moveField(oldIndex, newIndex));
|
||||
},
|
||||
itemCount: cells.length,
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateFieldButton extends StatefulWidget {
|
||||
const CreateFieldButton({
|
||||
super.key,
|
||||
required this.viewId,
|
||||
required this.onFieldCreated,
|
||||
});
|
||||
|
||||
final String viewId;
|
||||
final void Function(String fieldId) onFieldCreated;
|
||||
|
||||
@override
|
||||
State<CreateFieldButton> createState() => _CreateFieldButtonState();
|
||||
}
|
||||
|
||||
class _CreateFieldButtonState extends State<CreateFieldButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 200,
|
||||
decoration: _getDecoration(context),
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),
|
||||
radius: const BorderRadius.only(topRight: Radius.circular(24)),
|
||||
text: FlowyText(
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
fontSize: 15,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
||||
onTap: () => showCreateFieldBottomSheet(context, widget.viewId),
|
||||
leftIconSize: const Size.square(18),
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
size: const Size.square(18),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration? _getDecoration(BuildContext context) {
|
||||
final borderSide = BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1.0,
|
||||
);
|
||||
|
||||
return BoxDecoration(
|
||||
borderRadius: const BorderRadiusDirectional.only(
|
||||
topEnd: Radius.circular(24),
|
||||
),
|
||||
border: BorderDirectional(
|
||||
top: borderSide,
|
||||
end: borderSide,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -27,9 +27,9 @@ Widget getGridFabs(BuildContext context) {
|
||||
}
|
||||
},
|
||||
boxShadow: const BoxShadow(
|
||||
offset: Offset(0, 6),
|
||||
offset: Offset(0, 8),
|
||||
color: Color(0x145D7D8B),
|
||||
blurRadius: 18,
|
||||
blurRadius: 20,
|
||||
),
|
||||
icon: FlowySvgs.properties_s,
|
||||
iconSize: const Size.square(24),
|
||||
@ -81,12 +81,13 @@ class MobileGridFab extends StatelessWidget {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: const Border.fromBorderSide(BorderSide( width: 0.5, color: Color(0xFFE4EDF0))),
|
||||
borderRadius: radius,
|
||||
boxShadow: [boxShadow],
|
||||
),
|
||||
child: Material(
|
||||
borderOnForeground: false,
|
||||
color: backgroundColor,
|
||||
color: Colors.transparent,
|
||||
borderRadius: radius,
|
||||
child: InkWell(
|
||||
borderRadius: radius,
|
||||
|
@ -126,7 +126,8 @@ class RowContent extends StatelessWidget {
|
||||
buildWhen: (previous, current) =>
|
||||
!listEquals(previous.cells, current.cells),
|
||||
builder: (context, state) {
|
||||
return IntrinsicHeight(
|
||||
return SizedBox(
|
||||
height: 52,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
@ -148,9 +149,7 @@ class RowContent extends StatelessWidget {
|
||||
final GridCellWidget child = builder.build(cellId);
|
||||
|
||||
return MobileCellContainer(
|
||||
width: cellId.fieldInfo.fieldSettings!.width.toDouble(),
|
||||
isPrimary: cellId.fieldInfo.field.isPrimary,
|
||||
accessoryBuilder: (_) => [],
|
||||
onPrimaryFieldCellTap: onExpand,
|
||||
child: child,
|
||||
);
|
||||
@ -159,16 +158,14 @@ class RowContent extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _finalCellDecoration(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: Container(
|
||||
width: GridSize.trailHeaderPadding,
|
||||
padding: GridSize.headerContentInsets,
|
||||
constraints: const BoxConstraints(minHeight: 46),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
return Container(
|
||||
width: 200,
|
||||
padding: GridSize.headerContentInsets,
|
||||
constraints: const BoxConstraints(minHeight: 46),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Theme.of(context).dividerColor),
|
||||
right: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -27,7 +27,6 @@ class _MobileTabBarHeaderState extends State<MobileTabBarHeader> {
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -45,55 +44,45 @@ class _MobileTabBarHeaderState extends State<MobileTabBarHeader> {
|
||||
|
||||
controller.text = currentView.view.name;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildViewIconButton(currentView.view),
|
||||
const HSpace(8.0),
|
||||
Expanded(
|
||||
child: FlowyTextField(
|
||||
autoFocus: false,
|
||||
maxLines: null,
|
||||
controller: controller,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
onSubmitted: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
context.read<ViewBloc>().add(
|
||||
ViewEvent.rename(value),
|
||||
);
|
||||
}
|
||||
},
|
||||
onCanceled: () {
|
||||
controller.text = currentView.view.name;
|
||||
},
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildViewIconButton(currentView.view),
|
||||
const HSpace(8.0),
|
||||
Expanded(
|
||||
child: FlowyTextField(
|
||||
autoFocus: false,
|
||||
maxLines: 1,
|
||||
controller: controller,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
MobileDatabaseSettingsButton(
|
||||
controller: state
|
||||
.tabBarControllerByViewId[currentView.viewId]!
|
||||
.controller,
|
||||
toggleExtension: ToggleExtensionNotifier(),
|
||||
),
|
||||
],
|
||||
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||
onSubmitted: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
context.read<ViewBloc>().add(
|
||||
ViewEvent.rename(value),
|
||||
);
|
||||
}
|
||||
},
|
||||
onCanceled: () {
|
||||
controller.text = currentView.view.name;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
],
|
||||
MobileDatabaseSettingsButton(
|
||||
controller: state
|
||||
.tabBarControllerByViewId[currentView.viewId]!.controller,
|
||||
toggleExtension: ToggleExtensionNotifier(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -1,26 +1,19 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import '../accessory/cell_accessory.dart';
|
||||
import '../accessory/cell_shortcuts.dart';
|
||||
import '../cell_builder.dart';
|
||||
import 'cell_container.dart';
|
||||
|
||||
class MobileCellContainer extends StatelessWidget {
|
||||
final GridCellWidget child;
|
||||
final AccessoryBuilder? accessoryBuilder;
|
||||
final double width;
|
||||
final bool isPrimary;
|
||||
final VoidCallback? onPrimaryFieldCellTap;
|
||||
|
||||
const MobileCellContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.width,
|
||||
required this.isPrimary,
|
||||
this.accessoryBuilder,
|
||||
this.onPrimaryFieldCellTap,
|
||||
});
|
||||
|
||||
@ -33,23 +26,6 @@ class MobileCellContainer extends StatelessWidget {
|
||||
builder: (providerContext, isFocus, _) {
|
||||
Widget container = Center(child: GridCellShortcuts(child: child));
|
||||
|
||||
if (accessoryBuilder != null) {
|
||||
final accessories = accessoryBuilder!.call(
|
||||
GridCellAccessoryBuildContext(
|
||||
anchorContext: context,
|
||||
isCellEditing: isFocus,
|
||||
),
|
||||
);
|
||||
|
||||
if (accessories.isNotEmpty) {
|
||||
container = _GridCellEnterRegion(
|
||||
accessories: accessories,
|
||||
isPrimary: isPrimary,
|
||||
child: container,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isPrimary) {
|
||||
container = IgnorePointer(child: container);
|
||||
}
|
||||
@ -66,8 +42,8 @@ class MobileCellContainer extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
|
||||
decoration: _makeBoxDecoration(context, isFocus),
|
||||
constraints: const BoxConstraints(maxWidth: 200, minHeight: 46),
|
||||
decoration: _makeBoxDecoration(context, isPrimary, isFocus),
|
||||
child: container,
|
||||
),
|
||||
);
|
||||
@ -76,7 +52,11 @@ class MobileCellContainer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
|
||||
BoxDecoration _makeBoxDecoration(
|
||||
BuildContext context,
|
||||
bool isPrimary,
|
||||
bool isFocus,
|
||||
) {
|
||||
if (isFocus) {
|
||||
return BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
@ -89,43 +69,11 @@ class MobileCellContainer extends StatelessWidget {
|
||||
|
||||
final borderSide = BorderSide(color: Theme.of(context).dividerColor);
|
||||
return BoxDecoration(
|
||||
border: Border(right: borderSide, bottom: borderSide),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GridCellEnterRegion extends StatelessWidget {
|
||||
const _GridCellEnterRegion({
|
||||
required this.child,
|
||||
required this.accessories,
|
||||
required this.isPrimary,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final List<GridCellAccessoryBuilder> accessories;
|
||||
final bool isPrimary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<CellContainerNotifier, bool>(
|
||||
selector: (context, cellNotifier) => !cellNotifier.isFocus && isPrimary,
|
||||
builder: (context, showAccessory, _) {
|
||||
final List<Widget> children = [child];
|
||||
|
||||
if (showAccessory) {
|
||||
children.add(
|
||||
CellAccessoryContainer(accessories: accessories).positioned(
|
||||
right: GridSize.cellContentInsets.right,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: StackFit.expand,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
border: Border(
|
||||
left: isPrimary ? borderSide : BorderSide.none,
|
||||
right: borderSide,
|
||||
bottom: borderSide,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_service.dar
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/desktop_field_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
|
||||
|
Loading…
Reference in New Issue
Block a user