feat: optimize date picker & mention block (#5954)

* chore: optimize rename button on mobile

* fix: mention block id empty error

* chore: optimize mention block style

* feat: add confirm button in date picker
This commit is contained in:
Lucas.Xu 2024-08-14 09:31:30 +08:00 committed by GitHub
parent 463c8c7ee4
commit b3a0119c18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 236 additions and 124 deletions

View File

@ -64,7 +64,7 @@ class _MobileBottomSheetRenameWidgetState
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,
), ),
fontColor: Colors.white, textColor: Colors.white,
fillColor: Theme.of(context).primaryColor, fillColor: Theme.of(context).primaryColor,
onPressed: () { onPressed: () {
widget.onRename(controller.text); widget.onRename(controller.text);

View File

@ -18,7 +18,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
Node, Node,
Path, Path,
Delta, Delta,
composeAttributes; composeAttributes,
blockComponentDelta;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:nanoid/nanoid.dart'; import 'package:nanoid/nanoid.dart';
@ -81,6 +82,15 @@ class TransactionAdapter {
} }
final blockActions = final blockActions =
actions.map((e) => e.blockActionPB).toList(growable: false); 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( await documentService.applyAction(
documentId: documentId, documentId: documentId,
actions: blockActions, actions: blockActions,
@ -164,6 +174,7 @@ extension on InsertOperation {
childrenId: nanoid(6), childrenId: nanoid(6),
externalId: textId, externalId: textId,
externalType: textId != null ? _kExternalTextType : null, externalType: textId != null ? _kExternalTextType : null,
attributes: {...node.attributes}..remove(blockComponentDelta),
) )
..parentId = parentId ..parentId = parentId
..prevId = prevId; ..prevId = prevId;
@ -234,10 +245,13 @@ extension on UpdateOperation {
) )
: null; : null;
final composedAttributes = composeAttributes(oldAttributes, attributes);
composedAttributes?.remove(blockComponentDelta);
final payload = BlockActionPayloadPB() final payload = BlockActionPayloadPB()
..block = node.toBlock( ..block = node.toBlock(
parentId: parentId, parentId: parentId,
attributes: composeAttributes(oldAttributes, attributes), attributes: composedAttributes,
) )
..parentId = parentId; ..parentId = parentId;
final blockActionPB = BlockActionPB() final blockActionPB = BlockActionPB()

View File

@ -73,7 +73,11 @@ class MentionBlock extends StatelessWidget {
switch (type) { switch (type) {
case MentionType.page: 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( return MentionPageBlock(
key: ValueKey(pageId), key: ValueKey(pageId),
editorState: editorState, editorState: editorState,

View File

@ -26,6 +26,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nanoid/non_secure.dart'; import 'package:nanoid/non_secure.dart';
@ -182,68 +183,13 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
return GestureDetector( return GestureDetector(
onTapDown: (details) { onTapDown: (details) {
if (widget.editorState.editable) { _showDatePicker(
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, context: context,
editorState: widget.editorState, offset: details.globalPosition,
).show(details.globalPosition, options: options); reminder: reminder,
} timeStr: timeStr,
} options: options,
);
}, },
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
@ -251,15 +197,10 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
widget.reminderId != null '@$formattedDate',
? '@$formattedDate' style: textStyle,
: formattedDate, strutStyle: textStyle != null
style: widget.textStyle?.copyWith( ? StrutStyle.fromTextStyle(textStyle)
color: color,
leadingDistribution: TextLeadingDistribution.even,
),
strutStyle: widget.textStyle != null
? StrutStyle.fromTextStyle(widget.textStyle!)
: null, : null,
), ),
const HSpace(4), const HSpace(4),
@ -402,4 +343,109 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
), ),
); );
} }
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<AppearanceSettingsCubit>().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,
),
],
),
);
}
} }

View File

@ -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/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.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/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class MobileAppFlowyDatePicker extends StatefulWidget { class MobileAppFlowyDatePicker extends StatefulWidget {
@ -389,26 +387,69 @@ class _IncludeTimePickerState extends State<_IncludeTimePicker> {
children.addAll([ children.addAll([
Expanded(child: FlowyText(dateStr, textAlign: TextAlign.center)), Expanded(child: FlowyText(dateStr, textAlign: TextAlign.center)),
Container(width: 1, height: 16, color: Colors.grey), 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( return Container(
onTap: !isIncludeTime constraints: const BoxConstraints(minHeight: 36),
? null decoration: BoxDecoration(
: () async { borderRadius: BorderRadius.circular(6),
await showMobileBottomSheet( color: Theme.of(context).colorScheme.secondaryContainer,
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
),
child: Row(children: children),
);
}
Future<void> _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, context,
builder: (context) => ConstrainedBox( builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300), constraints: const BoxConstraints(maxHeight: 300),
child: CupertinoDatePicker( child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.time, mode: CupertinoDatePickerMode.time,
initialDateTime: initialDateTime,
use24hFormat: use24hFormat, use24hFormat: use24hFormat,
onDateTimeChanged: (dateTime) { onDateTimeChanged: (dateTime) {
final selectedTime = use24hFormat selectedTime = use24hFormat
? DateFormat('HH:mm').format(dateTime) ? DateFormat('HH:mm').format(dateTime)
: DateFormat('hh:mm a').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) { if (isStartDay) {
widget.onStartTimeChanged(selectedTime); widget.onStartTimeChanged(selectedTime);
@ -422,24 +463,31 @@ class _IncludeTimePickerState extends State<_IncludeTimePicker> {
setState(() => _endTimeStr = selectedTime); setState(() => _endTimeStr = selectedTime);
} }
} }
Navigator.of(context).pop();
}, },
), ),
), ),
); const VSpace(18.0),
}, ],
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,
),
),
child: Row(children: children),
), ),
); );
} }
DateTime _convertTimeStringToDateTime(String timeString) {
final DateTime now = DateTime.now();
final List<String> 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 { class _EndDateSwitch extends StatelessWidget {