diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart index d4f49cb9a9..e61f27b6b2 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart @@ -64,7 +64,7 @@ class _MobileBottomSheetRenameWidgetState padding: const EdgeInsets.symmetric( horizontal: 16.0, ), - fontColor: Colors.white, + textColor: Colors.white, fillColor: Theme.of(context).primaryColor, onPressed: () { widget.onRename(controller.text); diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart index f69d116df5..172c3b2bc9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart @@ -18,7 +18,8 @@ import 'package:appflowy_editor/appflowy_editor.dart' Node, Path, Delta, - composeAttributes; + composeAttributes, + blockComponentDelta; import 'package:collection/collection.dart'; import 'package:nanoid/nanoid.dart'; @@ -81,6 +82,15 @@ class TransactionAdapter { } final blockActions = actions.map((e) => e.blockActionPB).toList(growable: false); + + for (final action in blockActions) { + if (enableDocumentInternalLog) { + Log.debug( + '[editor_transaction_adapter] action => ${action.toProto3Json()}', + ); + } + } + await documentService.applyAction( documentId: documentId, actions: blockActions, @@ -164,6 +174,7 @@ extension on InsertOperation { childrenId: nanoid(6), externalId: textId, externalType: textId != null ? _kExternalTextType : null, + attributes: {...node.attributes}..remove(blockComponentDelta), ) ..parentId = parentId ..prevId = prevId; @@ -234,10 +245,13 @@ extension on UpdateOperation { ) : null; + final composedAttributes = composeAttributes(oldAttributes, attributes); + composedAttributes?.remove(blockComponentDelta); + final payload = BlockActionPayloadPB() ..block = node.toBlock( parentId: parentId, - attributes: composeAttributes(oldAttributes, attributes), + attributes: composedAttributes, ) ..parentId = parentId; final blockActionPB = BlockActionPB() diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart index 62d16a1714..c66f553bc2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart @@ -73,7 +73,11 @@ class MentionBlock extends StatelessWidget { switch (type) { case MentionType.page: - final String pageId = mention[MentionBlockKeys.pageId]; + final String? pageId = mention[MentionBlockKeys.pageId] as String?; + if (pageId == null) { + return const SizedBox.shrink(); + } + return MentionPageBlock( key: ValueKey(pageId), editorState: editorState, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index a4a4f670f2..9f2a16b0cf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -26,6 +26,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nanoid/non_secure.dart'; @@ -182,68 +183,13 @@ class _MentionDateBlockState extends State { return GestureDetector( onTapDown: (details) { - if (widget.editorState.editable) { - if (PlatformExtension.isMobile) { - showMobileBottomSheet( - context, - builder: (_) => DraggableScrollableSheet( - expand: false, - snap: true, - initialChildSize: 0.7, - minChildSize: 0.4, - snapSizes: const [0.4, 0.7, 1.0], - builder: (_, controller) => Material( - color: - Theme.of(context).colorScheme.secondaryContainer, - child: ListView( - controller: controller, - children: [ - ColoredBox( - color: Theme.of(context).colorScheme.surface, - child: const Center(child: DragHandle()), - ), - const MobileDateHeader(), - MobileAppFlowyDatePicker( - selectedDay: parsedDate, - timeStr: timeStr, - dateStr: parsedDate != null - ? options.dateFormat - .formatDate(parsedDate!, _includeTime) - : null, - includeTime: options.includeTime, - use24hFormat: options.timeFormat == - UserTimeFormatPB.TwentyFourHour, - rebuildOnDaySelected: true, - rebuildOnTimeChanged: true, - timeFormat: options.timeFormat.simplified, - selectedReminderOption: widget.reminderOption, - onDaySelected: options.onDaySelected, - onStartTimeChanged: (time) => options - .onStartTimeChanged - ?.call(time ?? ""), - onIncludeTimeChanged: - options.onIncludeTimeChanged, - liveDateFormatter: (selected) => - appearance.dateFormat.formatDate( - selected, - false, - appearance.timeFormat, - ), - onReminderSelected: (option) => - _updateReminder(option, reminder), - ), - ], - ), - ), - ), - ); - } else { - DatePickerMenu( - context: context, - editorState: widget.editorState, - ).show(details.globalPosition, options: options); - } - } + _showDatePicker( + context: context, + offset: details.globalPosition, + reminder: reminder, + timeStr: timeStr, + options: options, + ); }, child: MouseRegion( cursor: SystemMouseCursors.click, @@ -251,15 +197,10 @@ class _MentionDateBlockState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - widget.reminderId != null - ? '@$formattedDate' - : formattedDate, - style: widget.textStyle?.copyWith( - color: color, - leadingDistribution: TextLeadingDistribution.even, - ), - strutStyle: widget.textStyle != null - ? StrutStyle.fromTextStyle(widget.textStyle!) + '@$formattedDate', + style: textStyle, + strutStyle: textStyle != null + ? StrutStyle.fromTextStyle(textStyle) : null, ), const HSpace(4), @@ -402,4 +343,109 @@ class _MentionDateBlockState extends State { ), ); } + + void _showDatePicker({ + required BuildContext context, + required DatePickerOptions options, + required Offset offset, + String? timeStr, + ReminderPB? reminder, + }) { + if (!widget.editorState.editable) { + return; + } + if (PlatformExtension.isMobile) { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + + showMobileBottomSheet( + context, + builder: (_) => DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 0.7, + minChildSize: 0.4, + snapSizes: const [0.4, 0.7, 1.0], + builder: (_, controller) => _DatePickerBottomSheet( + controller: controller, + parsedDate: parsedDate, + timeStr: timeStr, + options: options, + includeTime: _includeTime, + reminderOption: widget.reminderOption, + onReminderSelected: (option) => _updateReminder( + option, + reminder, + ), + ), + ), + ); + } else { + DatePickerMenu( + context: context, + editorState: widget.editorState, + ).show(offset, options: options); + } + } +} + +class _DatePickerBottomSheet extends StatelessWidget { + const _DatePickerBottomSheet({ + required this.controller, + required this.parsedDate, + required this.timeStr, + required this.options, + required this.includeTime, + this.reminderOption, + required this.onReminderSelected, + }); + + final ScrollController controller; + final DateTime? parsedDate; + final String? timeStr; + final DatePickerOptions options; + final bool includeTime; + final ReminderOption? reminderOption; + final void Function(ReminderOption) onReminderSelected; + + @override + Widget build(BuildContext context) { + final appearance = context.read().state; + + return Material( + color: Theme.of(context).colorScheme.secondaryContainer, + child: ListView( + controller: controller, + children: [ + ColoredBox( + color: Theme.of(context).colorScheme.surface, + child: const Center(child: DragHandle()), + ), + const MobileDateHeader(), + MobileAppFlowyDatePicker( + selectedDay: parsedDate, + timeStr: timeStr, + dateStr: parsedDate != null + ? options.dateFormat.formatDate(parsedDate!, includeTime) + : null, + includeTime: options.includeTime, + use24hFormat: options.timeFormat == UserTimeFormatPB.TwentyFourHour, + rebuildOnDaySelected: true, + rebuildOnTimeChanged: true, + timeFormat: options.timeFormat.simplified, + selectedReminderOption: reminderOption, + onDaySelected: options.onDaySelected, + onStartTimeChanged: (time) => + options.onStartTimeChanged?.call(time ?? ""), + onIncludeTimeChanged: options.onIncludeTimeChanged, + liveDateFormatter: (selected) => appearance.dateFormat.formatDate( + selected, + false, + appearance.timeFormat, + ), + onReminderSelected: onReminderSelected, + ), + ], + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart index 12714e04df..cdeec95e88 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart @@ -1,6 +1,3 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; @@ -13,8 +10,9 @@ import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobi import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class MobileAppFlowyDatePicker extends StatefulWidget { @@ -389,57 +387,107 @@ class _IncludeTimePickerState extends State<_IncludeTimePicker> { children.addAll([ Expanded(child: FlowyText(dateStr, textAlign: TextAlign.center)), Container(width: 1, height: 16, color: Colors.grey), - Expanded(child: FlowyText(timeStr ?? '', textAlign: TextAlign.center)), + Expanded( + child: GestureDetector( + onTap: () => _showTimePicker( + context, + use24hFormat: use24hFormat, + isStartDay: isStartDay, + ), + child: FlowyText(timeStr ?? '', textAlign: TextAlign.center), + ), + ), ]); } - return GestureDetector( - onTap: !isIncludeTime - ? null - : () async { - await showMobileBottomSheet( - context, - builder: (context) => ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: CupertinoDatePicker( - mode: CupertinoDatePickerMode.time, - use24hFormat: use24hFormat, - onDateTimeChanged: (dateTime) { - final selectedTime = use24hFormat - ? DateFormat('HH:mm').format(dateTime) - : DateFormat('hh:mm a').format(dateTime); - - if (isStartDay) { - widget.onStartTimeChanged(selectedTime); - - if (widget.rebuildOnTimeChanged && mounted) { - setState(() => _timeStr = selectedTime); - } - } else { - widget.onEndTimeChanged?.call(selectedTime); - - if (widget.rebuildOnTimeChanged && mounted) { - setState(() => _endTimeStr = selectedTime); - } - } - }, - ), - ), - ); - }, - child: Container( - constraints: const BoxConstraints(minHeight: 36), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Theme.of(context).colorScheme.secondaryContainer, - border: Border.all( - color: Theme.of(context).colorScheme.outline, - ), + return Container( + constraints: const BoxConstraints(minHeight: 36), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Theme.of(context).colorScheme.secondaryContainer, + border: Border.all( + color: Theme.of(context).colorScheme.outline, ), - child: Row(children: children), + ), + child: Row(children: children), + ); + } + + Future _showTimePicker( + BuildContext context, { + required bool use24hFormat, + required bool isStartDay, + }) async { + String? selectedTime = isStartDay ? _timeStr : _endTimeStr; + final initialDateTime = selectedTime != null + ? _convertTimeStringToDateTime(selectedTime) + : null; + + return showMobileBottomSheet( + context, + builder: (context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + initialDateTime: initialDateTime, + use24hFormat: use24hFormat, + onDateTimeChanged: (dateTime) { + selectedTime = use24hFormat + ? DateFormat('HH:mm').format(dateTime) + : DateFormat('hh:mm a').format(dateTime); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 36), + child: FlowyTextButton( + LocaleKeys.button_confirm.tr(), + constraints: const BoxConstraints.tightFor(height: 42), + mainAxisAlignment: MainAxisAlignment.center, + textColor: Theme.of(context).colorScheme.onPrimary, + fillColor: Theme.of(context).primaryColor, + onPressed: () { + if (isStartDay) { + widget.onStartTimeChanged(selectedTime); + + if (widget.rebuildOnTimeChanged && mounted) { + setState(() => _timeStr = selectedTime); + } + } else { + widget.onEndTimeChanged?.call(selectedTime); + + if (widget.rebuildOnTimeChanged && mounted) { + setState(() => _endTimeStr = selectedTime); + } + } + + Navigator.of(context).pop(); + }, + ), + ), + const VSpace(18.0), + ], ), ); } + + DateTime _convertTimeStringToDateTime(String timeString) { + final DateTime now = DateTime.now(); + + final List timeParts = timeString.split(':'); + + if (timeParts.length != 2) { + return now; + } + + final int hour = int.parse(timeParts[0]); + final int minute = int.parse(timeParts[1]); + + return DateTime(now.year, now.month, now.day, hour, minute); + } } class _EndDateSwitch extends StatelessWidget {