diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart new file mode 100644 index 0000000000..47370a8d95 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart @@ -0,0 +1 @@ +export 'card_detail/mobile_card_detail_screen.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart new file mode 100644 index 0000000000..d0ca5a5135 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart @@ -0,0 +1,186 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart'; +import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:go_router/go_router.dart'; + +class MobileCardDetailScreen extends StatefulWidget { + const MobileCardDetailScreen({ + super.key, + required this.rowController, + }); + + static const routeName = '/MobileCardDetailScreen'; + static const argRowController = 'rowController'; + static const argCellBuilder = 'cellBuilder'; + final RowController rowController; + + @override + State createState() => _MobileCardDetailScreenState(); +} + +class _MobileCardDetailScreenState extends State { + late final ScrollController _scrollController; + late final GridCellBuilder _cellBuilder; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _cellBuilder = GridCellBuilder( + cellCache: widget.rowController.cellCache, + ); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // TODO(yijing): fix context issue when navigating in bottom navigation bar + return BlocProvider( + create: (context) => RowDetailBloc(rowController: widget.rowController) + ..add(const RowDetailEvent.initial()), + child: Scaffold( + // appbar with duplicate and delete card features + appBar: AppBar( + title: Text(LocaleKeys.board_cardDetail.tr()), + actions: [ + BlocProvider( + create: (context) => RowActionSheetBloc( + viewId: widget.rowController.viewId, + rowId: widget.rowController.rowId, + groupId: widget.rowController.groupId, + ), + child: Builder( + builder: (context) { + return IconButton( + onPressed: () { + showFlowyMobileBottomSheet( + context, + title: LocaleKeys.board_cardActions.tr(), + builder: (_) => Row( + children: [ + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.copy_s, + text: LocaleKeys.button_duplicate.tr(), + onTap: () { + context.read().add( + const RowActionSheetEvent + .duplicateRow(), + ); + context + ..pop() + ..pop(); + Fluttertoast.showToast( + msg: LocaleKeys.board_cardDuplicated.tr(), + gravity: ToastGravity.CENTER, + ); + }, + ), + ), + const HSpace(8), + Expanded( + child: BottomSheetActionWidget( + svg: FlowySvgs.m_delete_m, + text: LocaleKeys.button_delete.tr(), + onTap: () { + context.read().add( + const RowActionSheetEvent.deleteRow(), + ); + context + ..pop() + ..pop(); + Fluttertoast.showToast( + msg: LocaleKeys.board_cardDeleted.tr(), + gravity: ToastGravity.CENTER, + ); + }, + ), + ), + ], + ), + ); + }, + icon: const Icon(Icons.more_horiz), + ); + }, + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: ListView( + controller: _scrollController, + children: [ + BlocProvider( + create: (context) => RowBannerBloc( + viewId: widget.rowController.viewId, + rowMeta: widget.rowController.rowMeta, + )..add(const RowBannerEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + // primaryField is the property cannot be deleted like card title + if (state.primaryField != null) { + final mobileStyle = GridTextCellStyle( + placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(), + textStyle: Theme.of(context).textTheme.titleLarge, + ); + + // get the cell context for the card title + final cellContext = DatabaseCellContext( + viewId: widget.rowController.viewId, + rowMeta: widget.rowController.rowMeta, + fieldInfo: FieldInfo.initial(state.primaryField!), + ); + + return _cellBuilder.build( + cellContext, + style: mobileStyle, + ); + } + return const SizedBox.shrink(); + }, + ), + ), + const VSpace(8), + // Card Properties + MobileRowPropertyList( + cellBuilder: _cellBuilder, + viewId: widget.rowController.viewId, + ), + const Divider(), + const VSpace(16), + RowDocument( + viewId: widget.rowController.viewId, + rowId: widget.rowController.rowId, + scrollController: _scrollController, + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart new file mode 100644 index 0000000000..53ad24255d --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart @@ -0,0 +1,59 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class MobileCreateRowFieldScreen extends StatefulWidget { + static const routeName = '/MobileCreateRowFieldScreen'; + static const argViewId = 'viewId'; + static const argTypeOption = 'typeOption'; + + const MobileCreateRowFieldScreen({ + required this.viewId, + required this.typeOption, + super.key, + }); + + final String viewId; + final TypeOptionPB typeOption; + + @override + State createState() => + _MobileCreateRowFieldScreenState(); +} + +class _MobileCreateRowFieldScreenState + extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(LocaleKeys.grid_field_newProperty.tr()), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: TextButton( + onPressed: () => context.pop(), + child: Text( + LocaleKeys.button_done.tr(), + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ), + ), + ], + ), + body: MobileFieldEditor( + viewId: widget.viewId, + typeOptionLoader: FieldTypeOptionLoader( + viewId: widget.viewId, + field: widget.typeOption.field_2, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_row_field_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_row_field_button.dart new file mode 100644 index 0000000000..634d695283 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_row_field_button.dart @@ -0,0 +1,47 @@ +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_create_row_field_screen.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class MobileCreateRowFieldButton extends StatelessWidget { + const MobileCreateRowFieldButton({super.key, required this.viewId}); + + final String viewId; + + @override + Widget build(BuildContext context) { + return TextButton.icon( + label: Text( + LocaleKeys.grid_field_newProperty.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).hintColor, + ), + ), + onPressed: () async { + final result = await TypeOptionBackendService.createFieldTypeOption( + viewId: viewId, + ); + result.fold( + (typeOption) { + context.push( + MobileCreateRowFieldScreen.routeName, + extra: { + MobileCreateRowFieldScreen.argViewId: viewId, + MobileCreateRowFieldScreen.argTypeOption: typeOption, + }, + ); + }, + (r) => Log.error("Failed to create field type option: $r"), + ); + }, + icon: FlowySvg( + FlowySvgs.add_m, + color: Theme.of(context).hintColor, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_field_name_text_field.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_field_name_text_field.dart new file mode 100644 index 0000000000..be10ffedf7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_field_name_text_field.dart @@ -0,0 +1,45 @@ +import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileFieldNameTextField extends StatefulWidget { + const MobileFieldNameTextField({ + super.key, + this.text, + }); + + final String? text; + + @override + State createState() => + _MobileFieldNameTextFieldState(); +} + +class _MobileFieldNameTextFieldState extends State { + final controller = TextEditingController(); + @override + void initState() { + super.initState(); + if (widget.text != null) { + controller.text = widget.text!; + } + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + onChanged: (newName) { + context + .read() + .add(FieldEditorEvent.updateName(newName)); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart new file mode 100644 index 0000000000..fb5eac6ce4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart @@ -0,0 +1,221 @@ +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/widgets/mobile_create_row_field_button.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_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_type_extension.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart'; + +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +/// Display the row properties in a list. Only use this widget in the +/// [MobileCardDetailScreen]. +class MobileRowPropertyList extends StatelessWidget { + const MobileRowPropertyList({ + super.key, + required this.viewId, + required this.cellBuilder, + }); + + final String viewId; + final GridCellBuilder cellBuilder; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final List visibleCells = state.visibleCells + .where((element) => !element.fieldInfo.field.isPrimary) + .toList(); + + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: visibleCells.length, + itemBuilder: (context, index) => _PropertyCell( + key: ValueKey('row_detail_${visibleCells[index].fieldId}'), + cellContext: visibleCells[index], + cellBuilder: cellBuilder, + index: index, + ), + onReorder: (oldIndex, newIndex) { + // when reorderiing downwards, need to update index + if (oldIndex < newIndex) { + newIndex--; + } + final reorderedFieldId = visibleCells[oldIndex].fieldId; + final targetFieldId = visibleCells[newIndex].fieldId; + + context.read().add( + RowDetailEvent.reorderField( + reorderedFieldId, + targetFieldId, + oldIndex, + newIndex, + ), + ); + }, + footer: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (context.read().state.numHiddenFields != 0) + const ToggleHiddenFieldsVisibilityButton(), + const VSpace(8), + // add new field + MobileCreateRowFieldButton( + viewId: viewId, + ), + ], + ), + ), + ); + }, + ); + } +} + +// TODO(yijing): temperary locate here +// It may need to be share with other widgets +const cellHeight = 32.0; + +class _PropertyCell extends StatefulWidget { + const _PropertyCell({ + super.key, + required this.cellContext, + required this.cellBuilder, + required this.index, + }); + + final DatabaseCellContext cellContext; + final GridCellBuilder cellBuilder; + final int index; + + @override + State createState() => _PropertyCellState(); +} + +class _PropertyCellState extends State<_PropertyCell> { + @override + Widget build(BuildContext context) { + final style = _customCellStyle(widget.cellContext.fieldType); + final cell = widget.cellBuilder.build(widget.cellContext, style: style); + + return ListTile( + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + horizontalTitleGap: 4, + // FieldCellButton in Desktop + // TODO(yijing): adjust width with sreen size + leading: SizedBox( + width: 150, + height: cellHeight, + child: TextButton.icon( + icon: FlowySvg( + widget.cellContext.fieldInfo.field.fieldType.icon(), + color: Theme.of(context).colorScheme.onSurface, + ), + label: Text( + widget.cellContext.fieldInfo.field.name, + // TODO(yijing): update text style + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + overflow: TextOverflow.ellipsis, + ), + style: TextButton.styleFrom( + alignment: Alignment.centerLeft, + padding: EdgeInsets.zero, + ), + // naivgator to field editor + onPressed: () => context.push( + CardPropertyEditScreen.routeName, + extra: { + CardPropertyEditScreen.argCellContext: widget.cellContext, + CardPropertyEditScreen.argRowDetailBloc: + context.read(), + }, + ), + ), + ), + title: SizedBox( + width: double.infinity, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => cell.requestFocus.notify(), + //property values + child: AccessoryHover( + fieldType: widget.cellContext.fieldType, + child: cell, + ), + ), + ), + ); + } +} + +GridCellStyle? _customCellStyle(FieldType fieldType) { + switch (fieldType) { + case FieldType.Checkbox: + return GridCheckboxCellStyle( + cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + ); + case FieldType.DateTime: + return DateCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + alignment: Alignment.centerLeft, + cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + ); + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return TimestampCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + alignment: Alignment.centerLeft, + cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + ); + case FieldType.MultiSelect: + return SelectOptionCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + ); + case FieldType.Checklist: + return ChecklistCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + cellPadding: EdgeInsets.zero, + showTasksInline: true, + ); + case FieldType.Number: + return GridNumberCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + case FieldType.RichText: + return GridTextCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + case FieldType.SingleSelect: + return SelectOptionCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + ); + + case FieldType.URL: + return GridURLCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + accessoryTypes: [ + GridURLCellAccessoryType.copyURL, + GridURLCellAccessoryType.visitURL, + ], + ); + } + throw UnimplementedError; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/widgets.dart new file mode 100644 index 0000000000..7b037a90e5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'mobile_field_name_text_field.dart'; +export 'mobile_create_row_field_button.dart'; +export 'mobile_row_property_list.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart new file mode 100644 index 0000000000..1f1573a301 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart @@ -0,0 +1,64 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart'; +import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class CardPropertyEditScreen extends StatelessWidget { + const CardPropertyEditScreen({ + super.key, + required this.cellContext, + }); + + static const routeName = '/CardPropertyEditScreen'; + static const argCellContext = 'cellContext'; + static const argRowDetailBloc = 'rowDetailBloc'; + + final DatabaseCellContext cellContext; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(LocaleKeys.grid_field_editProperty.tr()), + actions: [ + // show delete button when this field is not used to group cards + if (!cellContext.fieldInfo.isGroupField) + IconButton( + onPressed: () { + showFlowyMobileConfirmDialog( + context, + title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), + actionButtonTitle: LocaleKeys.button_delete.tr(), + actionButtonColor: Theme.of(context).colorScheme.error, + onActionButtonPressed: () { + context.read().add( + RowDetailEvent.deleteField( + cellContext.fieldInfo.field.id, + ), + ); + context.pop(); + }, + ); + }, + icon: const FlowySvg(FlowySvgs.m_delete_m), + ), + ], + ), + body: MobileFieldEditor( + viewId: cellContext.viewId, + typeOptionLoader: FieldTypeOptionLoader( + viewId: cellContext.viewId, + field: cellContext.fieldInfo.field, + ), + fieldInfo: cellContext.fieldInfo, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart new file mode 100644 index 0000000000..0ebe4b2fa5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart @@ -0,0 +1,130 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_type_option_editor.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/widgets/property_title.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Used in [CardPropertyEditScreen] and [MobileCreateRowFieldScreen] +class MobileFieldEditor extends StatelessWidget { + const MobileFieldEditor({ + super.key, + required this.viewId, + required this.typeOptionLoader, + this.isGroupingField = false, + this.fieldInfo, + }); + + final String viewId; + final bool isGroupingField; + final FieldTypeOptionLoader typeOptionLoader; + final FieldInfo? fieldInfo; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + return FieldEditorBloc( + // group field is the field to be used to group cards in database view, it can not be deleted + isGroupField: isGroupingField, + loader: typeOptionLoader, + field: typeOptionLoader.field, + )..add(const FieldEditorEvent.initial()); + }, + child: BlocBuilder( + builder: (context, state) { + // for field type edit option + final dataController = context.read().dataController; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // property name + // TODO(yijing): improve hint text + PropertyTitle(LocaleKeys.settings_user_name.tr()), + BlocSelector( + selector: (state) { + return state.name; + }, + builder: (context, propertyName) { + return MobileFieldNameTextField( + text: propertyName, + ); + }, + ), + Row( + children: [ + PropertyTitle(LocaleKeys.grid_field_visibility.tr()), + const Spacer(), + VisibilitySwitch( + isFieldHidden: + fieldInfo?.visibility == FieldVisibility.AlwaysHidden, + onChanged: () { + state.field.fold( + () => Log.error('Can not hidden the field'), + (field) => context.read().add( + RowDetailEvent.toggleFieldVisibility( + field.id, + ), + ), + ); + }, + ), + ], + ), + const VSpace(8), + // edit property type and settings + if (!typeOptionLoader.field.isPrimary) + MobileFieldTypeOptionEditor( + dataController: dataController, + ), + ], + ), + ); + }, + ), + ); + } +} + +class VisibilitySwitch extends StatefulWidget { + const VisibilitySwitch({ + super.key, + required this.isFieldHidden, + this.onChanged, + }); + + final bool isFieldHidden; + final Function? onChanged; + + @override + State createState() => _VisibilitySwitchState(); +} + +class _VisibilitySwitchState extends State { + late bool _isFieldHidden = widget.isFieldHidden; + + @override + Widget build(BuildContext context) { + return Switch.adaptive( + activeColor: Theme.of(context).colorScheme.primary, + value: !_isFieldHidden, + onChanged: (bool value) { + setState(() { + _isFieldHidden = !_isFieldHidden; + widget.onChanged?.call(); + }); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_type_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_type_list.dart new file mode 100644 index 0000000000..2f8f7b58e5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_type_list.dart @@ -0,0 +1,82 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_type_option_edit_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; + +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +typedef SelectFieldCallback = void Function(FieldType); + +class MobileFieldTypeList extends StatelessWidget { + const MobileFieldTypeList({ + super.key, + required this.onSelectField, + required this.bloc, + }); + + final FieldTypeOptionEditBloc bloc; + final SelectFieldCallback onSelectField; + + @override + Widget build(BuildContext context) { + const allFieldTypes = FieldType.values; + return BlocProvider.value( + value: bloc, + child: ListView.builder( + shrinkWrap: true, + itemCount: allFieldTypes.length, + itemBuilder: (_, index) { + return MobileFieldTypeCell( + fieldType: allFieldTypes[index], + onSelectField: onSelectField, + ); + }, + ), + ); + } +} + +class MobileFieldTypeCell extends StatelessWidget { + const MobileFieldTypeCell({ + super.key, + required this.fieldType, + required this.onSelectField, + }); + + final FieldType fieldType; + final SelectFieldCallback onSelectField; + + @override + Widget build(BuildContext context) { + return RadioListTile( + dense: true, + controlAffinity: ListTileControlAffinity.trailing, + contentPadding: EdgeInsets.zero, + value: fieldType, + groupValue: context.select( + (FieldTypeOptionEditBloc bloc) => bloc.state.field.fieldType, + ), + onChanged: (value) { + if (value != null) { + onSelectField(value); + } + }, + title: Row( + children: [ + FlowySvg( + fieldType.icon(), + ), + const HSpace(8), + Text( + fieldType.title(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_type_option_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_type_option_editor.dart new file mode 100644 index 0000000000..26ab104b61 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/mobile_field_type_option_editor.dart @@ -0,0 +1,191 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/type_option_widget_builder.dart'; +import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_type_option_edit_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_controller.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/type_option/builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import 'mobile_field_type_list.dart'; + +class MobileFieldTypeOptionEditor extends StatelessWidget { + const MobileFieldTypeOptionEditor({ + super.key, + required TypeOptionController dataController, + }) : _dataController = dataController; + + final TypeOptionController _dataController; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => FieldTypeOptionEditBloc(_dataController) + ..add(const FieldTypeOptionEditEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final typeOptionWidget = _makeTypeOptionMobileWidget( + context: context, + dataController: _dataController, + ); + + return Column( + children: [ + const _MobileSwitchFieldButton(), + if (typeOptionWidget != null) ...[ + const VSpace(8), + typeOptionWidget, + ], + ], + ); + }, + ), + ); + } +} + +class _MobileSwitchFieldButton extends StatelessWidget { + const _MobileSwitchFieldButton(); + + @override + Widget build(BuildContext context) { + final fieldType = context.select( + (FieldTypeOptionEditBloc bloc) => bloc.state.field.fieldType, + ); + return GestureDetector( + child: Row( + children: [ + Text( + LocaleKeys.grid_field_propertyType.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + FlowySvg(fieldType.icon()), + const HSpace(4), + Text( + fieldType.title(), + style: Theme.of(context).textTheme.titleMedium, + ), + Icon( + Icons.arrow_forward_ios_sharp, + color: Theme.of(context).hintColor, + ), + ], + ), + onTap: () => showFlowyMobileBottomSheet( + context, + isScrollControlled: true, + title: LocaleKeys.grid_field_propertyType.tr(), + builder: (_) => MobileFieldTypeList( + bloc: context.read(), + onSelectField: (newFieldType) { + context.read().add( + FieldTypeOptionEditEvent.switchToField(newFieldType), + ); + context.pop(); + }, + ), + ), + ); + } +} + +Widget? _makeTypeOptionMobileWidget({ + required BuildContext context, + required TypeOptionController dataController, +}) => + _makeTypeOptionMobileWidgetBuilder( + dataController: dataController, + ).build(context); + +TypeOptionWidgetBuilder _makeTypeOptionMobileWidgetBuilder({ + required TypeOptionController dataController, +}) { + final viewId = dataController.loader.viewId; + final fieldType = dataController.field.fieldType; + + switch (dataController.field.fieldType) { + case FieldType.Checkbox: + return CheckboxTypeOptionMobileWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + case FieldType.DateTime: + return DateTypeOptionMobileWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return TimestampTypeOptionMobileWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + case FieldType.SingleSelect: + return SingleSelectTypeOptionMobileWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + case FieldType.MultiSelect: + return MultiSelectTypeOptionMobileWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + case FieldType.Number: + return NumberTypeOptionMobileWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + case FieldType.RichText: + return RichTextTypeOptionMobileWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + + case FieldType.URL: + return URLTypeOptionMobileWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + + case FieldType.Checklist: + return ChecklistTypeOptionMobileWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + ); + } + throw UnimplementedError; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/checkbox.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/checkbox.dart new file mode 100644 index 0000000000..94d7f71cff --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/checkbox.dart @@ -0,0 +1,12 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:flutter/material.dart'; + +class CheckboxTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder { + CheckboxTypeOptionMobileWidgetBuilder( + CheckboxTypeOptionContext typeOptionContext, + ); + + @override + Widget? build(BuildContext context) => null; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/checklist.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/checklist.dart new file mode 100644 index 0000000000..b5b5299775 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/checklist.dart @@ -0,0 +1,12 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:flutter/material.dart'; + +class ChecklistTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder { + ChecklistTypeOptionMobileWidgetBuilder( + ChecklistTypeOptionContext typeOptionContext, + ); + + @override + Widget? build(BuildContext context) => const Text('Under Construction'); +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/date.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/date.dart new file mode 100644 index 0000000000..809495fc0c --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/date.dart @@ -0,0 +1,88 @@ +import 'package:appflowy/mobile/presentation/database/card/row/cells/date_cell/widgets/widgets.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/date_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DateTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder { + final DateTypeOptionMobileWidget _widget; + DateTypeOptionMobileWidgetBuilder( + DateTypeOptionContext typeOptionContext, + ) : _widget = DateTypeOptionMobileWidget( + typeOptionContext: typeOptionContext, + ); + + @override + Widget? build(BuildContext context) => _widget; +} + +class DateTypeOptionMobileWidget extends TypeOptionWidget { + const DateTypeOptionMobileWidget({ + super.key, + required this.typeOptionContext, + }); + + final DateTypeOptionContext typeOptionContext; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + DateTypeOptionBloc(typeOptionContext: typeOptionContext), + child: BlocConsumer( + listener: (context, state) => + typeOptionContext.typeOption = state.typeOption, + builder: (context, state) { + final List children = [ + DateFormatListTile( + currentFormatStr: state.typeOption.dateFormat.title(), + groupValue: context + .watch() + .state + .typeOption + .dateFormat, + onChanged: (newFormat) { + if (newFormat != null) { + context.read().add( + DateTypeOptionEvent.didSelectDateFormat( + newFormat, + ), + ); + } + }, + ), + TimeFormatListTile( + currentFormatStr: state.typeOption.timeFormat.title(), + groupValue: context + .watch() + .state + .typeOption + .timeFormat, + onChanged: (newFormat) { + if (newFormat != null) { + context.read().add( + DateTypeOptionEvent.didSelectTimeFormat( + newFormat, + ), + ); + } + }, + ), + ]; + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (_, index) => const VSpace(8), + itemCount: children.length, + itemBuilder: (_, index) => children[index], + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/multi_select.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/multi_select.dart new file mode 100644 index 0000000000..828313fe77 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/multi_select.dart @@ -0,0 +1,12 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:flutter/material.dart'; + +class MultiSelectTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder { + MultiSelectTypeOptionMobileWidgetBuilder( + MultiSelectTypeOptionContext typeOptionContext, + ); + + @override + Widget? build(BuildContext context) => const Text('Under Construction'); +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/number.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/number.dart new file mode 100644 index 0000000000..42cc59a6bd --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/number.dart @@ -0,0 +1,154 @@ +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/widgets/property_title.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/number_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/number_format_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:go_router/go_router.dart'; + +class NumberTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder { + final NumberTypeOptionMobileWidget _widget; + + NumberTypeOptionMobileWidgetBuilder( + NumberTypeOptionContext typeOptionContext, + ) : _widget = NumberTypeOptionMobileWidget( + typeOptionContext: typeOptionContext, + ); + + @override + Widget? build(BuildContext context) { + return _widget; + } +} + +class NumberTypeOptionMobileWidget extends TypeOptionWidget { + final NumberTypeOptionContext typeOptionContext; + + const NumberTypeOptionMobileWidget({ + super.key, + required this.typeOptionContext, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + NumberTypeOptionBloc(typeOptionContext: typeOptionContext), + child: BlocConsumer( + listener: (context, state) => + typeOptionContext.typeOption = state.typeOption, + builder: (context, state) { + return GestureDetector( + child: Row( + children: [ + PropertyTitle(LocaleKeys.grid_field_numberFormat.tr()), + const Spacer(), + const HSpace(4), + Text( + state.typeOption.format.title(), + style: Theme.of(context).textTheme.titleMedium, + ), + Icon( + Icons.arrow_forward_ios, + color: Theme.of(context).hintColor, + ), + ], + ), + onTap: () => showFlowyMobileBottomSheet( + context, + isScrollControlled: true, + title: LocaleKeys.grid_field_numberFormat.tr(), + builder: (bottomsheetContext) => GestureDetector( + child: NumberFormatList( + onSelected: (format) { + context + .read() + .add(NumberTypeOptionEvent.didSelectFormat(format)); + bottomsheetContext.pop(); + }, + selectedFormat: state.typeOption.format, + ), + ), + ), + ); + }, + ), + ); + } +} + +typedef SelectNumberFormatCallback = Function(NumberFormatPB format); + +class NumberFormatList extends StatelessWidget { + const NumberFormatList({ + super.key, + required this.selectedFormat, + required this.onSelected, + }); + + final SelectNumberFormatCallback onSelected; + final NumberFormatPB selectedFormat; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => NumberFormatBloc(), + child: Column( + children: [ + const _FilterTextField(), + const VSpace(16), + SizedBox( + height: 300, + child: BlocBuilder( + builder: (context, state) { + final List formatList = state.formats; + return ListView.builder( + itemCount: formatList.length, + itemBuilder: (BuildContext context, int index) { + final format = formatList[index]; + return RadioListTile( + controlAffinity: ListTileControlAffinity.trailing, + visualDensity: VisualDensity.compact, + value: format, + groupValue: selectedFormat, + onChanged: (format) => onSelected(format!), + title: Text(format.title()), + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} + +class _FilterTextField extends StatelessWidget { + const _FilterTextField(); + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: TextField( + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (text) => context + .read() + .add(NumberFormatEvent.setFilter(text)), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/rich_text.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/rich_text.dart new file mode 100644 index 0000000000..1db4606a7e --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/rich_text.dart @@ -0,0 +1,12 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:flutter/material.dart'; + +class RichTextTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder { + RichTextTypeOptionMobileWidgetBuilder( + RichTextTypeOptionContext typeOptionContext, + ); + + @override + Widget? build(BuildContext context) => null; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/single_select.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/single_select.dart new file mode 100644 index 0000000000..70926bc465 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/single_select.dart @@ -0,0 +1,13 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:flutter/material.dart'; + +class SingleSelectTypeOptionMobileWidgetBuilder + extends TypeOptionWidgetBuilder { + SingleSelectTypeOptionMobileWidgetBuilder( + SingleSelectTypeOptionContext typeOptionContext, + ); + + @override + Widget? build(BuildContext context) => const Text('Under Construction'); +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/timestamp.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/timestamp.dart new file mode 100644 index 0000000000..95d864934c --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/timestamp.dart @@ -0,0 +1,98 @@ +import 'package:appflowy/mobile/presentation/database/card/row/cells/date_cell/widgets/widgets.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/timestamp_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class TimestampTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder { + final TimestampTypeOptionMobileWidget _widget; + + TimestampTypeOptionMobileWidgetBuilder( + TimestampTypeOptionContext typeOptionContext, + ) : _widget = TimestampTypeOptionMobileWidget( + typeOptionContext: typeOptionContext, + ); + + @override + Widget? build(BuildContext context) => _widget; +} + +class TimestampTypeOptionMobileWidget extends TypeOptionWidget { + const TimestampTypeOptionMobileWidget({ + super.key, + required this.typeOptionContext, + }); + + final TimestampTypeOptionContext typeOptionContext; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + TimestampTypeOptionBloc(typeOptionContext: typeOptionContext), + child: BlocConsumer( + listener: (context, state) => + typeOptionContext.typeOption = state.typeOption, + builder: (context, state) { + final List children = [ + // used to add a separator padding at the top + const SizedBox.shrink(), + DateFormatListTile( + currentFormatStr: state.typeOption.dateFormat.title(), + groupValue: context + .watch() + .state + .typeOption + .dateFormat, + onChanged: (newFormat) { + if (newFormat != null) { + context.read().add( + TimestampTypeOptionEvent.didSelectDateFormat( + newFormat, + ), + ); + } + }, + ), + IncludeTimeSwitch( + switchValue: state.typeOption.includeTime, + onChanged: (value) => context + .read() + .add(TimestampTypeOptionEvent.includeTime(value)), + ), + if (state.typeOption.includeTime) + TimeFormatListTile( + currentFormatStr: state.typeOption.timeFormat.title(), + groupValue: context + .watch() + .state + .typeOption + .timeFormat, + onChanged: (newFormat) { + if (newFormat != null) { + context.read().add( + TimestampTypeOptionEvent.didSelectTimeFormat( + newFormat, + ), + ); + } + }, + ), + ]; + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => const VSpace(8), + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/type_option_widget_builder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/type_option_widget_builder.dart new file mode 100644 index 0000000000..22661c5522 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/type_option_widget_builder.dart @@ -0,0 +1,9 @@ +export 'checklist.dart'; +export 'checkbox.dart'; +export 'date.dart'; +export 'multi_select.dart'; +export 'number.dart'; +export 'rich_text.dart'; +export 'single_select.dart'; +export 'timestamp.dart'; +export 'url.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/url.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/url.dart new file mode 100644 index 0000000000..00c6deedca --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/url.dart @@ -0,0 +1,12 @@ +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; +import 'package:flutter/material.dart'; + +class URLTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder { + URLTypeOptionMobileWidgetBuilder( + URLTypeOptionContext typeOptionContext, + ); + + @override + Widget? build(BuildContext context) => null; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/widgets/property_title.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/widgets/property_title.dart new file mode 100644 index 0000000000..8328768820 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_property_edit/widgets/property_title.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class PropertyTitle extends StatelessWidget { + const PropertyTitle(this.name, {super.key}); + + final String name; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + name, + style: Theme.of(context).textTheme.titleMedium, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/cells.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/cells.dart new file mode 100644 index 0000000000..352fe7ac19 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/cells.dart @@ -0,0 +1,7 @@ +export 'mobile_text_cell.dart'; +export 'mobile_number_cell.dart'; +export 'mobile_timestamp_cell.dart'; +export 'mobile_checkbox_cell.dart'; +export 'mobile_url_cell.dart'; +export 'date_cell/mobile_date_cell.dart'; +export 'date_cell/mobile_date_cell_edit_screen.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/mobile_date_cell.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/mobile_date_cell.dart new file mode 100644 index 0000000000..552cd53770 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/mobile_date_cell.dart @@ -0,0 +1,103 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import 'mobile_date_cell_edit_screen.dart'; + +class MobileDateCell extends GridCellWidget { + MobileDateCell({ + super.key, + required this.cellControllerBuilder, + required this.hintText, + }); + + final CellControllerBuilder cellControllerBuilder; + final String? hintText; + + @override + GridCellState createState() => _DateCellState(); +} + +class _DateCellState extends GridCellState { + late final DateCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as DateCellController; + _cellBloc = DateCellBloc(cellController: cellController) + ..add(const DateCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + // full screen show the date edit screen + return GestureDetector( + onTap: () => context.push( + MobileDateCellEditScreen.routeName, + extra: { + MobileDateCellEditScreen.argCellController: + widget.cellControllerBuilder.build() as DateCellController, + }, + ), + child: SizedBox( + width: double.infinity, + child: MobileDateCellText( + dateStr: state.dateStr, + placeholder: widget.hintText ?? "", + ), + ), + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + void requestBeginFocus() {} + + @override + String? onCopy() => _cellBloc.state.dateStr; +} + +class MobileDateCellText extends StatelessWidget { + const MobileDateCellText({ + super.key, + required this.dateStr, + required this.placeholder, + }); + + final String dateStr; + final String placeholder; + + @override + Widget build(BuildContext context) { + final isPlaceholder = dateStr.isEmpty; + final text = isPlaceholder ? placeholder : dateStr; + return Align( + alignment: Alignment.centerLeft, + child: Text( + text, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: isPlaceholder + ? Theme.of(context).hintColor + : Theme.of(context).colorScheme.onBackground, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/mobile_date_cell_edit_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/mobile_date_cell_edit_screen.dart new file mode 100644 index 0000000000..bc09a67737 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/mobile_date_cell_edit_screen.dart @@ -0,0 +1,372 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:dartz/dartz.dart' hide State; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'widgets/widgets.dart'; + +class MobileDateCellEditScreen extends StatefulWidget { + static const routeName = '/MobileDateCellEditScreen'; + static const argCellController = 'cellController'; + + const MobileDateCellEditScreen( + this.cellController, { + super.key, + }); + + final DateCellController cellController; + + @override + State createState() => + _MobileDateCellEditScreenState(); +} + +class _MobileDateCellEditScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(LocaleKeys.button_edit.tr()), + ), + body: FutureBuilder>( + future: widget.cellController.getTypeOption( + DateTypeOptionDataParser(), + ), + builder: (BuildContext context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!.fold( + (dateTypeOptionPB) { + return _DateCellEditBody( + dateCellController: widget.cellController, + dateTypeOptionPB: dateTypeOptionPB, + ); + }, + (err) { + Log.error(err); + return FlowyMobileStateContainer.error( + title: LocaleKeys.grid_field_failedToLoadDate.tr(), + errorMsg: err.toString(), + ); + }, + ); + } + return const Center(child: CircularProgressIndicator.adaptive()); + }, + ), + ); + } +} + +class _DateCellEditBody extends StatefulWidget { + const _DateCellEditBody({ + required this.dateCellController, + required this.dateTypeOptionPB, + }); + + final DateCellController dateCellController; + final DateTypeOptionPB dateTypeOptionPB; + + @override + State<_DateCellEditBody> createState() => _DateCellEditBodyState(); +} + +class _DateCellEditBodyState extends State<_DateCellEditBody> { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DateCellCalendarBloc( + dateTypeOptionPB: widget.dateTypeOptionPB, + cellData: widget.dateCellController.getCellData(), + cellController: widget.dateCellController, + )..add(const DateCellCalendarEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final widgetsList = [ + DateAndTimeDisplay(state), + const DatePicker(), + const _EndDateSwitch(), + const _IncludeTimeSwitch(), + const _StartDayTime(), + const _EndDayTime(), + const Divider(), + const _DateFormatOption(), + const _TimeFormatOption(), + const Divider(), + const _ClearDateButton(), + ]; + return Padding( + padding: const EdgeInsets.all(16), + child: ListView.separated( + itemBuilder: (context, index) => widgetsList[index], + separatorBuilder: (_, __) => const VSpace(8), + itemCount: widgetsList.length, + ), + ); + }, + ), + ); + } +} + +class _EndDateSwitch extends StatelessWidget { + const _EndDateSwitch(); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.isRange, + builder: (context, isRange) { + return Row( + children: [ + Text( + LocaleKeys.grid_field_isRange.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + Switch.adaptive( + value: isRange, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (value) { + context + .read() + .add(DateCellCalendarEvent.setIsRange(value)); + }, + ), + ], + ); + }, + ); + } +} + +class _IncludeTimeSwitch extends StatelessWidget { + const _IncludeTimeSwitch(); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.includeTime, + builder: (context, includeTime) { + return IncludeTimeSwitch( + switchValue: includeTime, + onChanged: (value) { + context + .read() + .add(DateCellCalendarEvent.setIncludeTime(value)); + }, + ); + }, + ); + } +} + +class _StartDayTime extends StatelessWidget { + const _StartDayTime(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: state.includeTime + ? Row( + children: [ + Text( + state.isRange + ? LocaleKeys.grid_field_startDateTime.tr() + : LocaleKeys.grid_field_dateTime.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + // TODO(yijing): improve width + SizedBox( + width: 180, + child: _TimeTextField( + timeStr: state.timeStr, + isEndTime: false, + ), + ), + ], + ) + : const SizedBox.shrink(), + ); + }, + ); + } +} + +class _EndDayTime extends StatelessWidget { + const _EndDayTime(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: state.includeTime && state.endTimeStr != null + ? Row( + children: [ + Text( + LocaleKeys.grid_field_endDateTime.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + // TODO(yijing): improve width + SizedBox( + width: 180, + child: _TimeTextField( + timeStr: state.timeStr, + isEndTime: true, + ), + ), + ], + ) + : const SizedBox.shrink(), + ); + }, + ); + } +} + +class _TimeTextField extends StatefulWidget { + const _TimeTextField({ + required this.timeStr, + required this.isEndTime, + }); + + final String? timeStr; + final bool isEndTime; + + @override + State<_TimeTextField> createState() => _TimeTextFieldState(); +} + +class _TimeTextFieldState extends State<_TimeTextField> { + late final TextEditingController _textController = + TextEditingController(text: widget.timeStr); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + _textController.text = + widget.isEndTime ? state.endTimeStr ?? "" : state.timeStr ?? ""; + }, + builder: (context, state) { + return TextFormField( + controller: _textController, + textAlign: TextAlign.end, + decoration: InputDecoration( + hintText: state.timeHintText, + errorText: widget.isEndTime + ? state.parseEndTimeError + : state.parseTimeError, + ), + keyboardType: TextInputType.datetime, + onFieldSubmitted: (timeStr) { + context.read().add( + widget.isEndTime + ? DateCellCalendarEvent.setEndTime(timeStr) + : DateCellCalendarEvent.setTime(timeStr), + ); + }, + ); + }, + ); + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } +} + +class _ClearDateButton extends StatelessWidget { + const _ClearDateButton(); + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: Align( + alignment: Alignment.centerLeft, + child: Text( + LocaleKeys.grid_field_clearDate.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + ), + onTap: () => context + .read() + .add(const DateCellCalendarEvent.clearDate()), + ); + } +} + +class _TimeFormatOption extends StatelessWidget { + const _TimeFormatOption(); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.dateTypeOptionPB.timeFormat, + builder: (context, state) { + return TimeFormatListTile( + currentFormatStr: state.title(), + groupValue: context + .watch() + .state + .dateTypeOptionPB + .timeFormat, + onChanged: (newFormat) { + if (newFormat == null) return; + context + .read() + .add(DateCellCalendarEvent.setTimeFormat(newFormat)); + }, + ); + }, + ); + } +} + +class _DateFormatOption extends StatelessWidget { + const _DateFormatOption(); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.dateTypeOptionPB.dateFormat, + builder: (context, state) { + return DateFormatListTile( + currentFormatStr: state.title(), + groupValue: context + .watch() + .state + .dateTypeOptionPB + .dateFormat, + onChanged: (newFormat) { + if (newFormat == null) return; + context + .read() + .add(DateCellCalendarEvent.setDateFormat(newFormat)); + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/date_and_time_display.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/date_and_time_display.dart new file mode 100644 index 0000000000..ae1e5d40a5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/date_and_time_display.dart @@ -0,0 +1,142 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DateAndTimeDisplay extends StatelessWidget { + const DateAndTimeDisplay(this.state, {super.key}); + final DateCellCalendarState state; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // date/start date and time + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _DateEditButton( + dateStr: state.isRange + ? _formatDateByDateFormatPB( + state.startDay, + state.dateTypeOptionPB.dateFormat, + ) + : state.dateStr, + initialDate: state.isRange ? state.startDay : state.dateTime, + onDaySelected: (newDate) { + context.read().add( + state.isRange + ? DateCellCalendarEvent.setStartDay(newDate) + : DateCellCalendarEvent.selectDay(newDate), + ); + }, + ), + const HSpace(8), + if (state.includeTime) + Expanded(child: _TimeEditButton(state.timeStr)), + ], + ), + const VSpace(8), + // end date and time + if (state.isRange) ...[ + _DateEditButton( + dateStr: state.endDay != null ? state.endDateStr : null, + initialDate: state.endDay, + onDaySelected: (newDate) { + context.read().add( + DateCellCalendarEvent.setEndDay(newDate), + ); + }, + ), + const HSpace(8), + if (state.includeTime) + Expanded(child: _TimeEditButton(state.endTimeStr)), + ], + ], + ); + } +} + +class _DateEditButton extends StatelessWidget { + const _DateEditButton({ + required this.dateStr, + required this.initialDate, + required this.onDaySelected, + }); + + final String? dateStr; + + /// initial date for date picker, if null, use DateTime.now() + final DateTime? initialDate; + final void Function(DateTime)? onDaySelected; + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return SizedBox( + // share space with TimeEditButton + width: (size.width - 8) / 2, + child: OutlinedButton( + onPressed: () async { + final DateTime? newDate = await showDatePicker( + context: context, + initialDate: initialDate ?? DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime(2100), + ); + if (newDate != null) { + onDaySelected?.call(newDate); + } + }, + child: Text( + dateStr ?? LocaleKeys.grid_field_selectDate.tr(), + style: Theme.of(context).textTheme.labelMedium, + ), + ), + ); + } +} + +class _TimeEditButton extends StatelessWidget { + const _TimeEditButton( + this.timeStr, + ); + + final String? timeStr; + + @override + Widget build(BuildContext context) { + return OutlinedButton( + // TODO(yijing): implement time picker + onPressed: null, + child: Text( + timeStr ?? LocaleKeys.grid_field_selectTime.tr(), + style: Theme.of(context).textTheme.labelMedium, + ), + ); + } +} + +String? _formatDateByDateFormatPB(DateTime? date, DateFormatPB dateFormatPB) { + if (date == null) { + return null; + } + switch (dateFormatPB) { + case DateFormatPB.Local: + return DateFormat('MM/dd/yyyy').format(date); + case DateFormatPB.US: + return DateFormat('yyyy/MM/dd').format(date); + case DateFormatPB.ISO: + return DateFormat('yyyy-MM-dd').format(date); + case DateFormatPB.Friendly: + return DateFormat('MMM dd, yyyy').format(date); + case DateFormatPB.DayMonthYear: + return DateFormat('dd/MM/yyyy').format(date); + default: + return 'Unavailable date format'; + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/date_format_list_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/date_format_list_tile.dart new file mode 100644 index 0000000000..a2fce67b1f --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/date_format_list_tile.dart @@ -0,0 +1,141 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class DateFormatListTile extends StatelessWidget { + const DateFormatListTile({ + super.key, + required this.currentFormatStr, + this.groupValue, + this.onChanged, + }); + + final String currentFormatStr; + + /// The group value for the radio list tile. + final DateFormatPB? groupValue; + + /// The Function for the radio list tile. + final void Function(DateFormatPB?)? onChanged; + + @override + Widget build(BuildContext context) { + final style = Theme.of(context); + return Row( + children: [ + Text( + LocaleKeys.grid_field_dateFormat.tr(), + style: style.textTheme.titleMedium, + ), + const Spacer(), + GestureDetector( + child: Row( + children: [ + Text( + currentFormatStr, + style: style.textTheme.titleMedium, + ), + const HSpace(4), + Icon( + Icons.arrow_forward_ios_sharp, + color: style.hintColor, + ), + ], + ), + onTap: () => showFlowyMobileBottomSheet( + context, + title: LocaleKeys.grid_field_dateFormat.tr(), + builder: (_) { + return Column( + children: [ + _DateFormatRadioListTile( + title: LocaleKeys.grid_field_dateFormatLocal.tr(), + dateFormatPB: DateFormatPB.Local, + groupValue: groupValue, + onChanged: (newFormat) { + onChanged?.call(newFormat); + context.pop(); + }, + ), + _DateFormatRadioListTile( + title: LocaleKeys.grid_field_dateFormatUS.tr(), + dateFormatPB: DateFormatPB.US, + groupValue: groupValue, + onChanged: (newFormat) { + onChanged?.call(newFormat); + context.pop(); + }, + ), + _DateFormatRadioListTile( + title: LocaleKeys.grid_field_dateFormatISO.tr(), + dateFormatPB: DateFormatPB.ISO, + groupValue: groupValue, + onChanged: (newFormat) { + onChanged?.call(newFormat); + context.pop(); + }, + ), + _DateFormatRadioListTile( + title: LocaleKeys.grid_field_dateFormatFriendly.tr(), + dateFormatPB: DateFormatPB.Friendly, + groupValue: groupValue, + onChanged: (newFormat) { + onChanged?.call(newFormat); + context.pop(); + }, + ), + _DateFormatRadioListTile( + title: LocaleKeys.grid_field_dateFormatDayMonthYear.tr(), + dateFormatPB: DateFormatPB.DayMonthYear, + groupValue: groupValue, + onChanged: (newFormat) { + onChanged?.call(newFormat); + context.pop(); + }, + ), + ], + ); + }, + ), + ), + ], + ); + } +} + +class _DateFormatRadioListTile extends StatelessWidget { + const _DateFormatRadioListTile({ + required this.title, + required this.dateFormatPB, + required this.groupValue, + required this.onChanged, + }); + + final String title; + final DateFormatPB dateFormatPB; + final DateFormatPB? groupValue; + final void Function(DateFormatPB?)? onChanged; + + @override + Widget build(BuildContext context) { + final style = Theme.of(context); + return RadioListTile( + dense: true, + contentPadding: const EdgeInsets.fromLTRB(0, 0, 4, 0), + controlAffinity: ListTileControlAffinity.trailing, + title: Text( + title, + style: style.textTheme.bodyMedium?.copyWith( + color: style.colorScheme.onSurface, + ), + ), + groupValue: groupValue, + value: dateFormatPB, + onChanged: onChanged, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/include_time_switch.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/include_time_switch.dart new file mode 100644 index 0000000000..5ccb0ec378 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/include_time_switch.dart @@ -0,0 +1,32 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class IncludeTimeSwitch extends StatelessWidget { + const IncludeTimeSwitch({ + super.key, + required this.switchValue, + required this.onChanged, + }); + + final bool switchValue; + final void Function(bool)? onChanged; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + LocaleKeys.grid_field_includeTime.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + Switch.adaptive( + value: switchValue, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: onChanged, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/time_format_list_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/time_format_list_tile.dart new file mode 100644 index 0000000000..296679471d --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/time_format_list_tile.dart @@ -0,0 +1,115 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class TimeFormatListTile extends StatelessWidget { + const TimeFormatListTile({ + super.key, + required this.currentFormatStr, + required this.groupValue, + required this.onChanged, + }); + + final String currentFormatStr; + + /// The group value for the radio list tile. + final TimeFormatPB? groupValue; + + /// The Function for the radio list tile. + final void Function(TimeFormatPB?)? onChanged; + + @override + Widget build(BuildContext context) { + final style = Theme.of(context); + return Row( + children: [ + Text( + LocaleKeys.grid_field_timeFormat.tr(), + style: style.textTheme.titleMedium, + ), + const Spacer(), + GestureDetector( + child: Row( + children: [ + Text( + currentFormatStr, + style: style.textTheme.titleMedium, + ), + const HSpace(4), + Icon( + Icons.arrow_forward_ios_sharp, + color: style.hintColor, + ), + ], + ), + onTap: () => showFlowyMobileBottomSheet( + context, + title: LocaleKeys.grid_field_timeFormat.tr(), + builder: (context) { + return Column( + children: [ + _TimeFormatRadioListTile( + title: LocaleKeys.grid_field_timeFormatTwelveHour.tr(), + timeFormatPB: TimeFormatPB.TwelveHour, + groupValue: groupValue, + onChanged: (newFormat) { + onChanged?.call(newFormat); + context.pop(); + }, + ), + _TimeFormatRadioListTile( + title: LocaleKeys.grid_field_timeFormatTwentyFourHour.tr(), + timeFormatPB: TimeFormatPB.TwentyFourHour, + groupValue: groupValue, + onChanged: (newFormat) { + onChanged?.call(newFormat); + context.pop(); + }, + ), + ], + ); + }, + ), + ), + ], + ); + } +} + +class _TimeFormatRadioListTile extends StatelessWidget { + const _TimeFormatRadioListTile({ + required this.title, + required this.timeFormatPB, + required this.groupValue, + required this.onChanged, + }); + + final String title; + final TimeFormatPB timeFormatPB; + final TimeFormatPB? groupValue; + final void Function(TimeFormatPB?)? onChanged; + + @override + Widget build(BuildContext context) { + final style = Theme.of(context); + return RadioListTile( + dense: true, + contentPadding: const EdgeInsets.fromLTRB(0, 0, 4, 0), + controlAffinity: ListTileControlAffinity.trailing, + title: Text( + title, + style: style.textTheme.bodyMedium?.copyWith( + color: style.colorScheme.onSurface, + ), + ), + groupValue: groupValue, + value: timeFormatPB, + onChanged: onChanged, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/widgets.dart new file mode 100644 index 0000000000..630e84c303 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/date_cell/widgets/widgets.dart @@ -0,0 +1,4 @@ +export 'date_and_time_display.dart'; +export 'date_format_list_tile.dart'; +export 'time_format_list_tile.dart'; +export 'include_time_switch.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_checkbox_cell.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_checkbox_cell.dart new file mode 100644 index 0000000000..bfb28d23d7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_checkbox_cell.dart @@ -0,0 +1,66 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileCheckboxCell extends GridCellWidget { + MobileCheckboxCell({ + super.key, + required this.cellControllerBuilder, + GridCellStyle? style, + }); + + final CellControllerBuilder cellControllerBuilder; + + @override + GridCellState createState() => _CheckboxCellState(); +} + +class _CheckboxCellState extends GridCellState { + late final CheckboxCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as CheckboxCellController; + _cellBloc = CheckboxCellBloc(cellController: cellController) + ..add(const CheckboxCellEvent.initial()); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + return Align( + alignment: Alignment.centerLeft, + // TODO(yijing): improve icon here + child: FlowySvg( + state.isSelected ? FlowySvgs.checkbox_s : FlowySvgs.uncheck_s, + color: Theme.of(context).colorScheme.onBackground, + size: const Size.square(24), + ), + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + void requestBeginFocus() { + _cellBloc.add(const CheckboxCellEvent.select()); + } + + @override + String? onCopy() => _cellBloc.state.isSelected ? "Yes" : "No"; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_number_cell.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_number_cell.dart new file mode 100644 index 0000000000..16fb1b2641 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_number_cell.dart @@ -0,0 +1,85 @@ +import 'dart:async'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileNumberCell extends GridCellWidget { + MobileNumberCell({ + super.key, + required this.cellControllerBuilder, + this.hintText, + }); + + final CellControllerBuilder cellControllerBuilder; + final String? hintText; + + @override + GridEditableTextCell createState() => _NumberCellState(); +} + +class _NumberCellState extends GridEditableTextCell { + late final NumberCellBloc _cellBloc; + late final TextEditingController _controller; + + @override + SingleListenerFocusNode focusNode = SingleListenerFocusNode(); + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as NumberCellController; + _cellBloc = NumberCellBloc(cellController: cellController) + ..add(const NumberCellEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.cellContent); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.cellContent != c.cellContent, + listener: (context, state) => _controller.text = state.cellContent, + ), + ], + child: TextField( + controller: _controller, + focusNode: focusNode, + decoration: InputDecoration( + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintText: widget.hintText, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + ), + // close keyboard when tapping outside of the text field + onTapOutside: (event) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Future focusChanged() async { + if (mounted && + _cellBloc.isClosed == false && + _controller.text != _cellBloc.state.cellContent) { + _cellBloc.add(NumberCellEvent.updateCell(_controller.text)); + } + } + + @override + String? onCopy() => _cellBloc.state.cellContent; +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_text_cell.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_text_cell.dart new file mode 100644 index 0000000000..3a9e224730 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_text_cell.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileTextCell extends GridCellWidget { + MobileTextCell({ + super.key, + required this.cellControllerBuilder, + this.hintText, + }); + + final CellControllerBuilder cellControllerBuilder; + final String? hintText; + + @override + GridEditableTextCell createState() => _MobileTextCellState(); +} + +class _MobileTextCellState extends GridEditableTextCell { + late final TextCellBloc _cellBloc; + late final TextEditingController _controller; + + @override + SingleListenerFocusNode focusNode = SingleListenerFocusNode(); + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as TextCellController; + _cellBloc = TextCellBloc(cellController: cellController) + ..add(const TextCellEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.content); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocListener( + listener: (context, state) { + if (_controller.text != state.content) { + _controller.text = state.content; + } + }, + child: TextField( + controller: _controller, + focusNode: focusNode, + // TODO(yijing): update text style + decoration: InputDecoration( + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintText: widget.hintText, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + ), + // close keyboard when tapping outside of the text field + onTapOutside: (event) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + String? onCopy() => _cellBloc.state.content; + + @override + Future focusChanged() { + _cellBloc.add( + TextCellEvent.updateText(_controller.text), + ); + return super.focusChanged(); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_timestamp_cell.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_timestamp_cell.dart new file mode 100644 index 0000000000..02d169b186 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_timestamp_cell.dart @@ -0,0 +1,57 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MobileTimestampCell extends GridCellWidget { + MobileTimestampCell({ + super.key, + required this.cellControllerBuilder, + }); + + final CellControllerBuilder cellControllerBuilder; + + @override + GridCellState createState() => _TimestampCellState(); +} + +class _TimestampCellState extends GridCellState { + late final TimestampCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as TimestampCellController; + _cellBloc = TimestampCellBloc(cellController: cellController) + ..add(const TimestampCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + return Align( + alignment: Alignment.centerLeft, + child: Text(state.dateStr), + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + String? onCopy() => _cellBloc.state.dateStr; + + @override + void requestBeginFocus() {} +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_url_cell.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_url_cell.dart new file mode 100644 index 0000000000..ab1cd11989 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/row/cells/mobile_url_cell.dart @@ -0,0 +1,119 @@ +import 'dart:async'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class MobileURLCell extends GridCellWidget { + MobileURLCell({ + super.key, + required this.cellControllerBuilder, + this.hintText, + }); + + final CellControllerBuilder cellControllerBuilder; + final String? hintText; + + @override + GridCellState createState() => _GridURLCellState(); +} + +class _GridURLCellState extends GridCellState { + late final URLCellBloc _cellBloc; + + @override + void initState() { + super.initState(); + final cellController = + widget.cellControllerBuilder.build() as URLCellController; + _cellBloc = URLCellBloc(cellController: cellController) + ..add(const URLCellEvent.initial()); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocSelector( + selector: (state) => state.content, + builder: (context, content) { + if (content.isEmpty) { + return TextField( + keyboardType: TextInputType.url, + decoration: InputDecoration( + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + hintText: widget.hintText, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + ), + // close keyboard when tapping outside of the text field + onTapOutside: (event) => + FocusManager.instance.primaryFocus?.unfocus(), + onSubmitted: (value) { + _cellBloc.add( + URLCellEvent.updateURL(value), + ); + }, + ); + } + + return Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () { + if (content.isEmpty) { + return; + } + final shouldAddScheme = !['http', 'https'] + .any((pattern) => content.startsWith(pattern)); + final url = shouldAddScheme ? 'http://$content' : content; + canLaunchUrlString(url).then((value) => launchUrlString(url)); + }, + onLongPress: () => showFlowyMobileBottomSheet( + context, + title: LocaleKeys.board_mobile_editURL.tr(), + builder: (_) { + final controller = TextEditingController(text: content); + return TextField( + controller: controller, + autofocus: true, + keyboardType: TextInputType.url, + onEditingComplete: () { + _cellBloc.add( + URLCellEvent.updateURL(controller.text), + ); + context.pop(); + }, + ); + }, + ), + child: Text( + content, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + decoration: TextDecoration.underline, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + }, + ), + ); + } + + @override + void requestBeginFocus() {} +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart index 3839069552..8e4ddc29cd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart @@ -39,6 +39,7 @@ class FlowyMobileStateContainer extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + return SizedBox.expand( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 32), @@ -47,7 +48,10 @@ class FlowyMobileStateContainer extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - emoji ?? '', + emoji ?? + (_stateType == _FlowyMobileStateContainerType.error + ? '🛸' + : ''), style: const TextStyle(fontSize: 40), ), const SizedBox(height: 8), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart index 78086f413f..742f12109b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart @@ -5,10 +5,11 @@ Future showFlowyMobileBottomSheet( BuildContext context, { required String title, required Widget Function(BuildContext) builder, + bool isScrollControlled = false, }) async { return showModalBottomSheet( context: context, - isScrollControlled: true, + isScrollControlled: isScrollControlled, builder: (context) => Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index fd9b497135..81cec7815c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -1,7 +1,7 @@ import 'dart:collection'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; @@ -10,6 +10,7 @@ import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; +import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; @@ -21,6 +22,7 @@ import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart' hide Card; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import '../../widgets/card/cells/card_cell.dart'; import '../../widgets/card/card_cell_builder.dart'; @@ -319,13 +321,23 @@ class _BoardContentState extends State { groupId: groupId, ); - FlowyOverlay.show( - context: context, - builder: (_) => RowDetailPage( - cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), - rowController: dataController, - ), - ); + // navigate to card detail screen when it is in mobile + if (PlatformExtension.isMobile) { + context.push( + MobileCardDetailScreen.routeName, + extra: { + 'rowController': dataController, + }, + ); + } else { + FlowyOverlay.show( + context: context, + builder: (_) => RowDetailPage( + cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), + rowController: dataController, + ), + ); + } } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart index 6ea45ce5bb..a8d79dd92c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart @@ -123,5 +123,5 @@ class SwitchFieldButton extends StatelessWidget { } abstract class TypeOptionWidget extends StatelessWidget { - const TypeOptionWidget({Key? key}) : super(key: key); + const TypeOptionWidget({super.key}); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart index 96bf4baf64..363ac2fc3d 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart @@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database_view/application/row/row_controller.da import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -273,7 +274,8 @@ class RowContent extends StatelessWidget { ) { return cellByFieldId.values.map( (cellId) { - final GridCellWidget child = builder.build(cellId); + final cellStyle = customCellStyle(cellId.fieldType); + final GridCellWidget child = builder.build(cellId, style: cellStyle); return CellContainer( width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart index b8d4d09f90..260d9b89c5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart'; +import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -122,6 +123,27 @@ class _RowCardState extends State> { return !listEquals(previous.cells, current.cells); }, builder: (context, state) { + // mobile + if (PlatformExtension.isMobile) { + // TODO(yijing): refactor it in mobile to display card in database view + return RowCardContainer( + buildAccessoryWhen: () => state.isEditing == false, + accessoryBuilder: (context) { + return []; + }, + openAccessory: (p0) {}, + openCard: (context) => widget.openCard(context), + child: _CardContent( + rowNotifier: rowNotifier, + cellBuilder: widget.cellBuilder, + styleConfiguration: widget.styleConfiguration, + cells: state.cells, + renderHook: widget.renderHook, + cardData: widget.cardData, + ), + ); + } + // desktop return AppFlowyPopover( controller: popoverController, triggerActions: PopoverTriggerFlags.none, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart index 31ddb023fb..af2a1fdc5d 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart @@ -1,4 +1,6 @@ +import 'package:appflowy/mobile/presentation/database/card/row/cells/cells.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; @@ -23,7 +25,7 @@ class GridCellBuilder { GridCellWidget build( DatabaseCellContext cellContext, { - GridCellStyle? style, + required GridCellStyle? style, }) { final cellControllerBuilder = CellControllerBuilder( cellContext: cellContext, @@ -31,6 +33,30 @@ class GridCellBuilder { ); final key = cellContext.key(); + + if (PlatformExtension.isMobile) { + return _getMobileCardCellWidget( + key, + cellContext, + cellControllerBuilder, + style, + ); + } + + return _getDesktopGridCellWidget( + key, + cellContext, + cellControllerBuilder, + style, + ); + } + + GridCellWidget _getDesktopGridCellWidget( + ValueKey key, + DatabaseCellContext cellContext, + CellControllerBuilder cellControllerBuilder, + GridCellStyle? style, + ) { switch (cellContext.fieldType) { case FieldType.Checkbox: return GridCheckboxCell( @@ -94,6 +120,74 @@ class GridCellBuilder { } } +// editable cell/(card's propery value) widget +GridCellWidget _getMobileCardCellWidget( + ValueKey key, + DatabaseCellContext cellContext, + CellControllerBuilder cellControllerBuilder, + GridCellStyle? style, +) { + switch (cellContext.fieldType) { + case FieldType.RichText: + style as GridTextCellStyle; + return MobileTextCell( + cellControllerBuilder: cellControllerBuilder, + hintText: style.placeholder, + ); + case FieldType.Number: + style as GridNumberCellStyle; + return MobileNumberCell( + cellControllerBuilder: cellControllerBuilder, + hintText: style.placeholder, + ); + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return MobileTimestampCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.Checkbox: + return MobileCheckboxCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.DateTime: + style as DateCellStyle; + return MobileDateCell( + cellControllerBuilder: cellControllerBuilder, + hintText: style.placeholder, + key: key, + ); + case FieldType.URL: + style as GridURLCellStyle; + return MobileURLCell( + cellControllerBuilder: cellControllerBuilder, + hintText: style.placeholder, + key: key, + ); + // TODO(yijing): implement the following mobile select option cell + case FieldType.SingleSelect: + return GridSingleSelectCell( + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); + case FieldType.MultiSelect: + return GridMultiSelectCell( + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); + case FieldType.Checklist: + return GridChecklistCell( + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); + } + throw UnimplementedError; +} + class BlankCell extends StatelessWidget { const BlankCell({Key? key}) : super(key: key); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart index 0524a899c1..5d50cf5e6c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart @@ -38,20 +38,23 @@ class DateCellCalendarBloc await event.when( initial: () async => _startListening(), didReceiveCellUpdate: (DateCellDataPB? cellData) { - final (dateTime, endDateTime, time, endTime, includeTime, isRange) = - _dateDataFromCellData(cellData); + final dateCellData = _dateDataFromCellData(cellData); final endDay = - isRange == state.isRange && isRange ? endDateTime : null; + dateCellData.isRange == state.isRange && dateCellData.isRange + ? dateCellData.endDateTime + : null; emit( state.copyWith( - dateTime: dateTime, - time: time, - endDateTime: endDateTime, - endTime: endTime, - includeTime: includeTime, - isRange: isRange, - startDay: isRange ? dateTime : null, + dateTime: dateCellData.dateTime, + timeStr: dateCellData.timeStr, + endDateTime: dateCellData.endDateTime, + endTimeStr: dateCellData.endTimeStr, + includeTime: dateCellData.includeTime, + isRange: dateCellData.isRange, + startDay: dateCellData.isRange ? dateCellData.dateTime : null, endDay: endDay, + dateStr: dateCellData.dateStr, + endDateStr: dateCellData.endDateStr, ), ); }, @@ -76,21 +79,31 @@ class DateCellCalendarBloc setIsRange: (isRange) async { await _updateDateData(isRange: isRange); }, - setTime: (time) async { - await _updateDateData(time: time); + setTime: (timeStr) async { + await _updateDateData(timeStr: timeStr); }, selectDateRange: (DateTime? start, DateTime? end) async { if (end == null && state.startDay != null && state.endDay == null) { final (newStart, newEnd) = state.startDay!.isBefore(start!) ? (state.startDay!, start) : (start, state.startDay!); - emit(state.copyWith(startDay: null, endDay: null)); + emit( + state.copyWith( + startDay: null, + endDay: null, + ), + ); await _updateDateData( date: newStart.date, endDate: newEnd.date, ); } else if (end == null) { - emit(state.copyWith(startDay: start, endDay: null)); + emit( + state.copyWith( + startDay: start, + endDay: null, + ), + ); } else { await _updateDateData( date: start!.date, @@ -98,8 +111,54 @@ class DateCellCalendarBloc ); } }, + setStartDay: (DateTime startDay) async { + if (state.endDay == null) { + emit( + state.copyWith( + startDay: startDay, + ), + ); + } else if (startDay.isAfter(state.endDay!)) { + emit( + state.copyWith( + startDay: startDay, + endDay: null, + ), + ); + } else { + emit( + state.copyWith( + startDay: startDay, + ), + ); + _updateDateData(date: startDay.date, endDate: state.endDay!.date); + } + }, + setEndDay: (DateTime endDay) async { + if (state.startDay == null) { + emit( + state.copyWith( + endDay: endDay, + ), + ); + } else if (endDay.isBefore(state.startDay!)) { + emit( + state.copyWith( + startDay: null, + endDay: endDay, + ), + ); + } else { + emit( + state.copyWith( + endDay: endDay, + ), + ); + _updateDateData(date: state.startDay!.date, endDate: endDay.date); + } + }, setEndTime: (String endTime) async { - await _updateDateData(endTime: endTime); + await _updateDateData(endTimeStr: endTime); }, setDateFormat: (dateFormat) async { await _updateTypeOption(emit, dateFormat: dateFormat); @@ -117,30 +176,31 @@ class DateCellCalendarBloc Future _updateDateData({ DateTime? date, - String? time, + String? timeStr, DateTime? endDate, - String? endTime, + String? endTimeStr, bool? includeTime, bool? isRange, }) async { // make sure that not both date and time are updated at the same time assert( - !(date != null && time != null) || !(endDate != null && endTime != null), + !(date != null && timeStr != null) || + !(endDate != null && endTimeStr != null), ); // if not updating the time, use the old time in the state - final String? newTime = time ?? state.time; + final String? newTime = timeStr ?? state.timeStr; DateTime? newDate; - if (time != null && time.isNotEmpty) { + if (timeStr != null && timeStr.isNotEmpty) { newDate = state.dateTime ?? DateTime.now(); } else { newDate = _utcToLocalAndAddCurrentTime(date); } // if not updating the time, use the old time in the state - final String? newEndTime = endTime ?? state.endTime; + final String? newEndTime = endTimeStr ?? state.endTimeStr; DateTime? newEndDate; - if (endTime != null && endTime.isNotEmpty) { + if (endTimeStr != null && endTimeStr.isNotEmpty) { newEndDate = state.endDateTime ?? DateTime.now(); } else { newEndDate = _utcToLocalAndAddCurrentTime(endDate); @@ -306,6 +366,12 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent { DateTime? start, DateTime? end, ) = _SelectDateRange; + const factory DateCellCalendarEvent.setStartDay( + DateTime startDay, + ) = _SetStartDay; + const factory DateCellCalendarEvent.setEndDay( + DateTime endDay, + ) = _SetEndDay; const factory DateCellCalendarEvent.setTime(String time) = _Time; const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime; const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) = @@ -334,10 +400,12 @@ class DateCellCalendarState with _$DateCellCalendarState { // cell data from the backend required DateTime? dateTime, required DateTime? endDateTime, - required String? time, - required String? endTime, + required String? timeStr, + required String? endTimeStr, required bool includeTime, required bool isRange, + required String? dateStr, + required String? endDateStr, // error and hint text required String? parseTimeError, @@ -349,18 +417,19 @@ class DateCellCalendarState with _$DateCellCalendarState { DateTypeOptionPB dateTypeOptionPB, DateCellDataPB? cellData, ) { - final (dateTime, endDateTime, time, endTime, includeTime, isRange) = - _dateDataFromCellData(cellData); + final dateCellData = _dateDataFromCellData(cellData); return DateCellCalendarState( dateTypeOptionPB: dateTypeOptionPB, - startDay: isRange ? dateTime : null, - endDay: isRange ? endDateTime : null, - dateTime: dateTime, - endDateTime: endDateTime, - time: time, - endTime: endTime, - includeTime: includeTime, - isRange: isRange, + startDay: dateCellData.isRange ? dateCellData.dateTime : null, + endDay: dateCellData.isRange ? dateCellData.endDateTime : null, + dateTime: dateCellData.dateTime, + endDateTime: dateCellData.endDateTime, + timeStr: dateCellData.timeStr, + endTimeStr: dateCellData.endTimeStr, + dateStr: dateCellData.dateStr, + endDateStr: dateCellData.endDateStr, + includeTime: dateCellData.includeTime, + isRange: dateCellData.isRange, parseTimeError: null, parseEndTimeError: null, timeHintText: _timeHintText(dateTypeOptionPB), @@ -379,31 +448,78 @@ String _timeHintText(DateTypeOptionPB typeOption) { } } -(DateTime?, DateTime?, String?, String?, bool, bool) _dateDataFromCellData( +DateCellData _dateDataFromCellData( DateCellDataPB? cellData, ) { // a null DateCellDataPB may be returned, indicating that all the fields are // their default values: empty strings and false booleans if (cellData == null) { - return (null, null, null, null, false, false); + return DateCellData( + dateTime: null, + endDateTime: null, + timeStr: null, + endTimeStr: null, + includeTime: false, + isRange: false, + dateStr: null, + endDateStr: null, + ); } DateTime? dateTime; - String? time; + String? timeStr; DateTime? endDateTime; - String? endTime; + String? endTimeStr; + + String? endDateStr; if (cellData.hasTimestamp()) { final timestamp = cellData.timestamp * 1000; dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt()); - time = cellData.time; + timeStr = cellData.time; if (cellData.hasEndTimestamp()) { final endTimestamp = cellData.endTimestamp * 1000; endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt()); - endTime = cellData.endTime; + endTimeStr = cellData.endTime; } } final bool includeTime = cellData.includeTime; final bool isRange = cellData.isRange; - return (dateTime, endDateTime, time, endTime, includeTime, isRange); + if (cellData.isRange) { + endDateStr = cellData.endDate; + } + final String dateStr = cellData.date; + + return DateCellData( + dateTime: dateTime, + endDateTime: endDateTime, + timeStr: timeStr, + endTimeStr: endTimeStr, + includeTime: includeTime, + isRange: isRange, + dateStr: dateStr, + endDateStr: endDateStr, + ); +} + +class DateCellData { + final DateTime? dateTime; + final DateTime? endDateTime; + final String? timeStr; + final String? endTimeStr; + final bool includeTime; + final bool isRange; + final String? dateStr; + final String? endDateStr; + + DateCellData({ + required this.dateTime, + required this.endDateTime, + required this.timeStr, + required this.endTimeStr, + required this.includeTime, + required this.isRange, + required this.dateStr, + required this.endDateStr, + }); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart index 5a293fd2c8..7a143f4a70 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart @@ -136,7 +136,7 @@ class StartTextField extends StatelessWidget { child: state.includeTime ? _TimeTextField( isEndTime: false, - timeStr: state.time, + timeStr: state.timeStr, popoverMutex: popoverMutex, ) : const SizedBox.shrink(), @@ -161,7 +161,7 @@ class EndTextField extends StatelessWidget { padding: const EdgeInsets.only(top: 8.0), child: _TimeTextField( isEndTime: true, - timeStr: state.endTime, + timeStr: state.endTimeStr, popoverMutex: popoverMutex, ), ) @@ -413,17 +413,17 @@ class _TimeTextFieldState extends State<_TimeTextField> { return BlocConsumer( listener: (context, state) { if (widget.isEndTime) { - _textController.text = state.endTime ?? ""; + _textController.text = state.endTimeStr ?? ""; } else { - _textController.text = state.time ?? ""; + _textController.text = state.timeStr ?? ""; } }, builder: (context, state) { String text = ""; - if (!widget.isEndTime && state.time != null) { - text = state.time!; - } else if (state.endTime != null) { - text = state.endTime!; + if (!widget.isEndTime && state.timeStr != null) { + text = state.timeStr!; + } else if (state.endTimeStr != null) { + text = state.endTimeStr!; } return Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart index c451d8b34f..3f2b47b05f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart @@ -134,7 +134,7 @@ class _PropertyCellState extends State<_PropertyCell> { @override Widget build(BuildContext context) { - final style = _customCellStyle(widget.cellContext.fieldType); + final style = customCellStyle(widget.cellContext.fieldType); final cell = widget.cellBuilder.build(widget.cellContext, style: style); final dragThumb = MouseRegion( @@ -247,7 +247,7 @@ class _PropertyCellState extends State<_PropertyCell> { } } -GridCellStyle? _customCellStyle(FieldType fieldType) { +GridCellStyle? customCellStyle(FieldType fieldType) { switch (fieldType) { case FieldType.Checkbox: return GridCheckboxCellStyle( diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 2c02f4a32c..157cf9bce4 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -1,3 +1,7 @@ +import 'package:appflowy/mobile/presentation/database/card/card.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart'; +import 'package:appflowy/mobile/presentation/database/card/row/cells/cells.dart'; import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart'; import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; @@ -7,6 +11,7 @@ import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dar import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; @@ -16,6 +21,7 @@ import 'package:appflowy/util/platform_extension.dart'; import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; GoRouter generateRouter(Widget child) { @@ -45,6 +51,11 @@ GoRouter generateRouter(Widget child) { _mobileGridScreenRoute(), _mobileBoardScreenRoute(), _mobileCalendarScreenRoute(), + // card detail page + _mobileCardDetailScreenRoute(), + _mobileCardPropertyEditScreenRoute(), + _mobileDateCellEditScreenRoute(), + _mobileCreateRowFieldScreenRoute(), // home // MobileHomeSettingPage is outside the bottom navigation bar, thus it is not in the StatefulShellRoute. @@ -415,6 +426,77 @@ GoRoute _mobileCalendarScreenRoute() { ); } +GoRoute _mobileCardDetailScreenRoute() { + return GoRoute( + path: MobileCardDetailScreen.routeName, + pageBuilder: (context, state) { + final args = state.extra as Map; + final rowController = args[MobileCardDetailScreen.argRowController]; + + return MaterialPage( + child: MobileCardDetailScreen( + rowController: rowController, + ), + ); + }, + ); +} + +GoRoute _mobileCardPropertyEditScreenRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: CardPropertyEditScreen.routeName, + pageBuilder: (context, state) { + final args = state.extra as Map; + final cellContext = args[CardPropertyEditScreen.argCellContext]; + final rowDetailBloc = args[CardPropertyEditScreen.argRowDetailBloc]; + + return MaterialPage( + child: BlocProvider.value( + value: rowDetailBloc as RowDetailBloc, + child: CardPropertyEditScreen( + cellContext: cellContext, + ), + ), + ); + }, + ); +} + +GoRoute _mobileDateCellEditScreenRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: MobileDateCellEditScreen.routeName, + pageBuilder: (context, state) { + final args = state.extra as Map; + final cellController = args[MobileDateCellEditScreen.argCellController]; + + return MaterialPage( + child: MobileDateCellEditScreen(cellController), + fullscreenDialog: true, + ); + }, + ); +} + +GoRoute _mobileCreateRowFieldScreenRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: MobileCreateRowFieldScreen.routeName, + pageBuilder: (context, state) { + final args = state.extra as Map; + final viewId = args[MobileCreateRowFieldScreen.argViewId]; + final typeOption = args[MobileCreateRowFieldScreen.argTypeOption]; + + return MaterialPage( + child: + MobileCreateRowFieldScreen(viewId: viewId, typeOption: typeOption), + fullscreenDialog: true, + ); + }, + ); +} + GoRoute _rootRoute(Widget child) { return GoRoute( path: '/', diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 8446512456..722db49ea7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -239,6 +239,9 @@ class MobileAppearance extends BaseAppearance { ), colorScheme: colorTheme, indicatorColor: Colors.blue, + textSelectionTheme: TextSelectionThemeData( + cursorColor: colorTheme.onBackground, + ), extensions: [ AFThemeExtension( warning: theme.yellow, diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index a353b632a6..c2e8f14462 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -108,7 +108,7 @@ dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 super_clipboard: ^0.6.3 - go_router: ^10.1.2 + go_router: ^12.1.1 string_validator: ^1.0.0 unsplash_client: ^2.1.1 flutter_emoji_mart: diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index c41b1d562b..7a457c65fa 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -497,6 +497,14 @@ "timeFormatTwelveHour": "12 hour", "timeFormatTwentyFourHour": "24 hour", "clearDate": "Clear date", + "dateTime": "Date time", + "startDateTime": "Start date time", + "endDateTime": "End date time", + "failedToLoadDate": "Failed to load date value", + "selectTime": "Select time", + "selectDate": "Select date", + "visibility": "Visibility", + "propertyType": "Property type", "addSelectOption": "Add an option", "optionTitle": "Options", "addOption": "Add option", @@ -786,13 +794,20 @@ "collapseTooltip": "Hide the hidden groups", "expandTooltip": "View the hidden groups" }, + "cardDetail": "Card Detail", + "cardActions": "Card Actions", + "cardDuplicated": "Card has been duplicated", + "cardDeleted": "Card has been deleted", "menuName": "Board", "showUngrouped": "Show ungrouped items", "ungroupedButtonText": "Ungrouped", "ungroupedButtonTooltip": "Contains cards that don't belong in any group", "ungroupedItemsTitle": "Click to add to the board", "groupBy": "Group by", - "referencedBoardPrefix": "View of" + "referencedBoardPrefix": "View of", + "mobile": { + "editURL": "Edit URL" + } }, "calendar": { "menuName": "Calendar",