feat: drag and drop events to reschedule (#2558)

* chore: send DateCellDataPB instead of timestamp

* chore: separate event card into own widget

* chore: add hover effect to event card itself

* feat: draggable event cards

* feat: drag target WIP

* chore: revert "chore: send DateCellDataPB instead of timestamp"

This reverts commit 1faaf21c6a50ac67da70ddf3bcfa8278ca5963d4.

* chore: remove timezone from date cell data

* fix: #2498 open calendar event faster

* chore: remove unused timezone

* feat: implement logic for rescheduling events

* fix: reschedule doesn't show up on UI

* fix: reorganize gesture detection layering

* fix: layout fix

* test: fix date single sort test

* chore: remove unused chrono-tz

* chore: add hint to unscheduled event popover

* chore: apply suggestions to 3 files

* fix: #2569 fix overflow

* chore: add timezone data to DateTypeOption

* test: make date tests run on Etc/UTC timezone

* chore: fix clippy warnings

* fix: use the right get db function

* chore: code cleanup

* test: run tests in utc

* test: fix tests

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Richard Shiue
2023-05-31 16:52:37 +08:00
committed by GitHub
parent 188b36cae6
commit 80f08d4bec
26 changed files with 617 additions and 650 deletions

View File

@ -8,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:dartz/dartz.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -62,10 +63,11 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
createEvent: (DateTime date, String title) async {
await _createEvent(date, title);
},
moveEvent: (CalendarDayEvent event, DateTime date) async {
await _moveEvent(event, date);
},
didCreateEvent: (CalendarEventData<CalendarDayEvent> event) {
emit(
state.copyWith(editEvent: event),
);
emit(state.copyWith(editingEvent: event));
},
updateCalendarLayoutSetting:
(CalendarLayoutSettingPB layoutSetting) async {
@ -79,11 +81,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
if (index != -1) {
allEvents[index] = eventData;
}
emit(
state.copyWith(
allEvents: allEvents,
),
);
emit(state.copyWith(allEvents: allEvents, updateEvent: eventData));
},
didDeleteEvents: (List<RowId> deletedRowIds) {
var events = [...state.allEvents];
@ -185,6 +183,30 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
);
}
Future<void> _moveEvent(CalendarDayEvent event, DateTime date) async {
final timestamp = _eventTimestamp(event, date);
final payload = MoveCalendarEventPB(
cellPath: CellIdPB(
viewId: viewId,
rowId: event.eventId,
fieldId: event.dateFieldId,
),
timestamp: timestamp,
);
return DatabaseEventMoveCalendarEvent(payload).send().then((result) {
return result.fold(
(_) async {
final modifiedEvent = await _loadEvent(event.eventId);
add(CalendarEvent.didUpdateEvent(modifiedEvent!));
},
(err) {
Log.error(err);
return null;
},
);
});
}
Future<void> _updateCalendarLayoutSetting(
CalendarLayoutSettingPB layoutSetting,
) async {
@ -238,26 +260,27 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
CalendarEventPB eventPB,
) {
final fieldInfo = fieldInfoByFieldId[eventPB.dateFieldId];
if (fieldInfo != null) {
final eventData = CalendarDayEvent(
event: eventPB,
eventId: eventPB.rowId,
dateFieldId: eventPB.dateFieldId,
);
// The timestamp is using UTC in the backend, so we need to convert it
// to local time.
final date = DateTime.fromMillisecondsSinceEpoch(
eventPB.timestamp.toInt() * 1000,
);
return CalendarEventData(
title: eventPB.title,
date: date,
event: eventData,
);
} else {
if (fieldInfo == null) {
return null;
}
// timestamp is stored as seconds, but constructor requires milliseconds
final date = DateTime.fromMillisecondsSinceEpoch(
eventPB.timestamp.toInt() * 1000,
);
final eventData = CalendarDayEvent(
event: eventPB,
eventId: eventPB.rowId,
dateFieldId: eventPB.dateFieldId,
date: date,
);
return CalendarEventData(
title: eventPB.title,
date: date,
event: eventData,
);
}
void _startListening() {
@ -266,28 +289,37 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
if (isClosed) return;
},
onFieldsChanged: (fieldInfos) {
if (isClosed) return;
if (isClosed) {
return;
}
fieldInfoByFieldId = {
for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
};
},
onRowsCreated: ((rowIds) async {
onRowsCreated: (rowIds) async {
if (isClosed) {
return;
}
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null && !isClosed) {
add(CalendarEvent.didReceiveEvent(event));
}
}
}),
},
onRowsDeleted: (rowIds) {
if (isClosed) return;
if (isClosed) {
return;
}
add(CalendarEvent.didDeleteEvents(rowIds));
},
onRowsUpdated: (rowIds) async {
if (isClosed) return;
if (isClosed) {
return;
}
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null && isEventDayChanged(event)) {
if (event != null) {
if (isEventDayChanged(event)) {
add(CalendarEvent.didDeleteEvents([id]));
add(CalendarEvent.didReceiveEvent(event));
@ -317,7 +349,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
void _didReceiveLayoutSetting(LayoutSettingPB layoutSetting) {
if (layoutSetting.hasCalendar()) {
if (isClosed) return;
if (isClosed) {
return;
}
add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar));
}
}
@ -329,17 +363,20 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
}
}
bool isEventDayChanged(
CalendarEventData<CalendarDayEvent> event,
) {
bool isEventDayChanged(CalendarEventData<CalendarDayEvent> event) {
final index = state.allEvents.indexWhere(
(element) => element.event!.eventId == event.event!.eventId,
);
if (index != -1) {
return state.allEvents[index].date.day != event.date.day;
} else {
if (index == -1) {
return false;
}
return state.allEvents[index].date.day != event.date.day;
}
Int64 _eventTimestamp(CalendarDayEvent event, DateTime date) {
final time =
event.date.hour * 3600 + event.date.minute * 60 + event.date.second;
return Int64(date.millisecondsSinceEpoch ~/ 1000 + time);
}
}
@ -381,6 +418,10 @@ class CalendarEvent with _$CalendarEvent {
const factory CalendarEvent.createEvent(DateTime date, String title) =
_CreateEvent;
// Called when moving an event
const factory CalendarEvent.moveEvent(CalendarDayEvent event, DateTime date) =
_MoveEvent;
// Called when updating the calendar's layout settings
const factory CalendarEvent.updateCalendarLayoutSetting(
CalendarLayoutSettingPB layoutSetting,
@ -401,7 +442,7 @@ class CalendarState with _$CalendarState {
// events by row id
required Events allEvents,
required Events initialEvents,
CalendarEventData<CalendarDayEvent>? editEvent,
CalendarEventData<CalendarDayEvent>? editingEvent,
CalendarEventData<CalendarDayEvent>? newEvent,
CalendarEventData<CalendarDayEvent>? updateEvent,
required List<String> deleteEventIds,
@ -439,14 +480,12 @@ class CalendarEditingRow {
});
}
class CalendarDayEvent {
final CalendarEventPB event;
final String dateFieldId;
final String eventId;
CalendarDayEvent({
required this.dateFieldId,
required this.eventId,
required this.event,
});
@freezed
class CalendarDayEvent with _$CalendarDayEvent {
const factory CalendarDayEvent({
required CalendarEventPB event,
required String dateFieldId,
required String eventId,
required DateTime date,
}) = _CalendarDayEvent;
}

View File

@ -13,6 +13,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:table_calendar/table_calendar.dart';
import '../../grid/presentation/layout/sizes.dart';
import '../../widgets/row/cells/select_option_cell/extension.dart';
@ -47,190 +48,86 @@ class CalendarDayCard extends StatelessWidget {
backgroundColor = AFThemeExtension.of(context).lightGreyHover;
}
return ChangeNotifierProvider(
create: (_) => _CardEnterNotifier(),
builder: (context, child) {
Widget? multipleCards;
if (events.isNotEmpty) {
multipleCards = Flexible(
child: ListView.separated(
itemBuilder: (BuildContext context, int index) =>
_buildCard(context, events[index]),
itemCount: events.length,
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
),
);
}
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ChangeNotifierProvider(
create: (_) => _CardEnterNotifier(),
builder: (context, child) {
final child = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_Header(
date: date,
isInMonth: isInMonth,
isToday: isToday,
),
final child = Column(
mainAxisSize: MainAxisSize.min,
children: [
_Header(
date: date,
isInMonth: isInMonth,
isToday: isToday,
onCreate: () => onCreateEvent(date),
),
// Add a separator between the header and the content.
VSpace(GridSize.typeOptionSeparatorHeight),
// Add a separator between the header and the content.
VSpace(GridSize.typeOptionSeparatorHeight),
// List of cards or empty space
if (events.isNotEmpty)
_EventList(
events: events,
viewId: viewId,
rowCache: _rowCache,
constraints: constraints,
),
],
);
// Use SizedBox instead of ListView if there are no cards.
multipleCards ?? const SizedBox(),
],
);
return Container(
color: backgroundColor,
child: GestureDetector(
onDoubleTap: () => onCreateEvent(date),
child: MouseRegion(
cursor: SystemMouseCursors.basic,
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: child,
),
),
),
return Stack(
children: <Widget>[
GestureDetector(
onDoubleTap: () => onCreateEvent(date),
child: Container(color: backgroundColor),
),
DragTarget<CalendarDayEvent>(
builder: (context, candidate, __) {
return Stack(
fit: StackFit.expand,
children: [
if (candidate.isNotEmpty)
Container(
color: Theme.of(context)
.colorScheme
.secondaryContainer,
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: child,
)
],
);
},
onWillAccept: (CalendarDayEvent? event) {
if (event == null) {
return false;
}
return !isSameDay(event.date, date);
},
onAccept: (CalendarDayEvent event) {
context
.read<CalendarBloc>()
.add(CalendarEvent.moveEvent(event, date));
},
),
_NewEventButton(onCreate: () => onCreateEvent(date)),
MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
opaque: false,
hitTestBehavior: HitTestBehavior.translucent,
),
],
);
},
);
},
);
}
Widget _buildCard(BuildContext context, CalendarDayEvent event) {
final styles = <FieldType, CardCellStyle>{
FieldType.Number: NumberCardCellStyle(10),
FieldType.URL: URLCardCellStyle(10),
};
final cellBuilder = CardCellBuilder<String>(
_rowCache.cellCache,
styles: styles,
);
final rowInfo = _rowCache.getRow(event.eventId);
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, primaryFieldId, _) {
if (cellData.isEmpty) {
return const SizedBox();
}
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
cellData,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: null, // Enable multiple lines
),
);
});
renderHook.addDateCellHook((cellData, cardData, _) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 3,
child: FlowyText.regular(
cellData.date,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: FlowyText.regular(
cellData.time,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
)
],
),
),
);
});
renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
if (selectedOptions.isEmpty) {
return const SizedBox.shrink();
}
final children = selectedOptions.map(
(option) {
return SelectOptionTag.fromOption(
context: context,
option: option,
);
},
).toList();
return IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: SizedBox.expand(
child: Wrap(spacing: 4, runSpacing: 4, children: children),
),
),
);
});
// renderHook.addDateFieldHook((cellData, cardData) {
final card = RowCard<String>(
// Add the key here to make sure the card is rebuilt when the cells
// in this row are updated.
key: ValueKey(event.eventId),
row: rowInfo!.rowPB,
viewId: viewId,
rowCache: _rowCache,
cardData: event.dateFieldId,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) => showEventDetails(
context: context,
event: event,
viewId: viewId,
rowCache: _rowCache,
),
styleConfiguration: const RowCardStyleConfiguration(
showAccessory: false,
cellPadding: EdgeInsets.zero,
),
renderHook: renderHook,
onStartEditing: () {},
onEndEditing: () {},
);
return FlowyHover(
style: HoverStyle(
hoverColor: Theme.of(context).colorScheme.tertiaryContainer,
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).dividerColor,
width: 1.5,
),
),
borderRadius: Corners.s6Border,
),
child: card,
),
);
}
notifyEnter(BuildContext context, bool isEnter) {
Provider.of<_CardEnterNotifier>(
context,
@ -243,55 +140,49 @@ class _Header extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
final VoidCallback onCreate;
const _Header({
required this.isToday,
required this.isInMonth,
required this.date,
required this.onCreate,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<_CardEnterNotifier>(
builder: (context, notifier, _) {
final badge = _DayBadge(
isToday: isToday,
isInMonth: isInMonth,
date: date,
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
if (notifier.onEnter) _NewEventButton(onClick: onCreate),
const Spacer(),
badge,
],
),
);
},
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: _DayBadge(
isToday: isToday,
isInMonth: isInMonth,
date: date,
),
);
}
}
class _NewEventButton extends StatelessWidget {
final VoidCallback onClick;
const _NewEventButton({
required this.onClick,
Key? key,
}) : super(key: key);
final VoidCallback onCreate;
const _NewEventButton({required this.onCreate, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlowyIconButton(
onPressed: onClick,
iconPadding: EdgeInsets.zero,
icon: const FlowySvg(name: "home/add"),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 22,
return Consumer<_CardEnterNotifier>(
builder: (context, notifier, _) {
if (!notifier.onEnter) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: FlowyIconButton(
onPressed: onCreate,
iconPadding: EdgeInsets.zero,
icon: const FlowySvg(name: "home/add"),
fillColor: Theme.of(context).colorScheme.background,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 22,
),
);
},
);
}
}
@ -322,6 +213,7 @@ class _DayBadge extends StatelessWidget {
}
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (date.day == 1) FlowyText.medium(monthString),
Container(
@ -344,6 +236,199 @@ class _DayBadge extends StatelessWidget {
}
}
class _EventList extends StatelessWidget {
final List<CalendarDayEvent> events;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
const _EventList({
required this.events,
required this.viewId,
required this.rowCache,
required this.constraints,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Flexible(
child: ListView.separated(
itemBuilder: (BuildContext context, int index) => _EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
),
itemCount: events.length,
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
),
);
}
}
class _EventCard extends StatelessWidget {
final CalendarDayEvent event;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
const _EventCard({
required this.event,
required this.viewId,
required this.rowCache,
required this.constraints,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final rowInfo = rowCache.getRow(event.eventId);
final styles = <FieldType, CardCellStyle>{
FieldType.Number: NumberCardCellStyle(10),
FieldType.URL: URLCardCellStyle(10),
};
final cellBuilder = CardCellBuilder<String>(
rowCache.cellCache,
styles: styles,
);
final renderHook = _calendarEventCardRenderHook(context);
final card = RowCard<String>(
// Add the key here to make sure the card is rebuilt when the cells
// in this row are updated.
key: ValueKey(event.eventId),
row: rowInfo!.rowPB,
viewId: viewId,
rowCache: rowCache,
cardData: event.dateFieldId,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) => showEventDetails(
context: context,
event: event,
viewId: viewId,
rowCache: rowCache,
),
styleConfiguration: RowCardStyleConfiguration(
showAccessory: false,
cellPadding: EdgeInsets.zero,
hoverStyle: HoverStyle(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
),
),
renderHook: renderHook,
onStartEditing: () {},
onEndEditing: () {},
);
final decoration = BoxDecoration(
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
borderRadius: Corners.s6Border,
);
return Draggable<CalendarDayEvent>(
data: event,
feedback: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth - 16.0,
),
child: Container(
decoration: decoration.copyWith(
color: AFThemeExtension.of(context).lightGreyHover,
),
child: card,
),
),
child: Container(
decoration: decoration,
child: card,
),
);
}
RowCardRenderHook<String> _calendarEventCardRenderHook(BuildContext context) {
final renderHook = RowCardRenderHook<String>();
renderHook.addTextCellHook((cellData, primaryFieldId, _) {
if (cellData.isEmpty) {
return const SizedBox.shrink();
}
return Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
cellData,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: null, // Enable multiple lines
),
);
});
renderHook.addDateCellHook((cellData, cardData, _) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 3,
child: FlowyText.regular(
cellData.date,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
if (cellData.includeTime)
Flexible(
child: FlowyText.regular(
cellData.time,
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
)
],
),
),
);
});
renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
if (selectedOptions.isEmpty) {
return const SizedBox.shrink();
}
final children = selectedOptions.map(
(option) {
return SelectOptionTag.fromOption(
context: context,
option: option,
);
},
).toList();
return IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: SizedBox.expand(
child: Wrap(spacing: 4, runSpacing: 4, children: children),
),
),
);
});
return renderHook;
}
}
class _CardEnterNotifier extends ChangeNotifier {
bool _onEnter = false;

View File

@ -74,12 +74,12 @@ class _CalendarPageState extends State<CalendarPage> {
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.editEvent != c.editEvent,
listenWhen: (p, c) => p.editingEvent != c.editingEvent,
listener: (context, state) {
if (state.editEvent != null) {
if (state.editingEvent != null) {
showEventDetails(
context: context,
event: state.editEvent!.event!,
event: state.editingEvent!.event!,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
@ -96,6 +96,20 @@ class _CalendarPageState extends State<CalendarPage> {
}
},
),
BlocListener<CalendarBloc, CalendarState>(
// When an event is rescheduled
listenWhen: (p, c) => p.updateEvent != c.updateEvent,
listener: (context, state) {
if (state.updateEvent != null) {
_eventController.removeWhere(
(element) =>
element.event!.eventId ==
state.updateEvent!.event!.eventId,
);
_eventController.add(state.updateEvent!);
}
},
),
],
child: BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {

View File

@ -38,11 +38,6 @@ class _SettingButton extends StatefulWidget {
}
class _SettingButtonState extends State<_SettingButton> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
@ -111,6 +106,7 @@ class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
direction: PopoverDirection.bottomWithCenterAligned,
controller: _controller,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
child: FlowyTextButton(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${unscheduledEvents.length})",
fillColor: Colors.transparent,
@ -118,31 +114,31 @@ class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
padding: GridSize.typeOptionContentInsets,
),
popupBuilder: (context) {
if (unscheduledEvents.isEmpty) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: Center(
child: FlowyText.medium(
LocaleKeys.calendar_settings_emptyNoDate.tr(),
color: Theme.of(context).hintColor,
),
),
);
}
return ListView.separated(
itemBuilder: (context, index) => _UnscheduledEventItem(
event: unscheduledEvents[index],
onPressed: () {
showEventDetails(
context: context,
event: unscheduledEvents[index].event!,
viewId: viewId,
rowCache: rowCache,
);
_controller.close();
},
final cells = <Widget>[
FlowyText.medium(
LocaleKeys.calendar_settings_noDateHint.tr(),
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
itemCount: unscheduledEvents.length,
const VSpace(10),
...unscheduledEvents.map(
(e) => _UnscheduledEventItem(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e.event!,
viewId: viewId,
rowCache: rowCache,
);
_controller.close();
},
),
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
@ -167,12 +163,9 @@ class _UnscheduledEventItem extends StatelessWidget {
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyTextButton(
event.title,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
onPressed: onPressed,
child: FlowyButton(
text: FlowyText.medium(event.title),
onTap: onPressed,
),
);
}

View File

@ -5,6 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -68,9 +69,9 @@ class RowCard<CustomCardData> extends StatefulWidget {
}
class _RowCardState<T> extends State<RowCard<T>> {
late CardBloc _cardBloc;
late EditableRowNotifier rowNotifier;
late PopoverController popoverController;
late final CardBloc _cardBloc;
late final EditableRowNotifier rowNotifier;
late final PopoverController popoverController;
AccessoryType? accessoryType;
@override
@ -209,9 +210,24 @@ class _CardContent<CustomCardData> extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: _makeCells(context, cells),
if (styleConfiguration.hoverStyle != null) {
return FlowyHover(
style: styleConfiguration.hoverStyle,
child: Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _makeCells(context, cells),
),
),
);
}
return Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: _makeCells(context, cells),
),
);
}
@ -298,9 +314,13 @@ class _CardEditOption extends StatelessWidget with CardAccessory {
class RowCardStyleConfiguration {
final bool showAccessory;
final EdgeInsets cellPadding;
final EdgeInsets cardPadding;
final HoverStyle? hoverStyle;
const RowCardStyleConfiguration({
this.showAccessory = true,
this.cellPadding = const EdgeInsets.only(left: 4, right: 4),
this.cardPadding = const EdgeInsets.all(8),
this.hoverStyle,
});
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'accessory.dart';
@ -45,12 +44,9 @@ class RowCardContainer extends StatelessWidget {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => openCard(context),
child: Padding(
padding: const EdgeInsets.all(8),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
child: container,
),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 30),
child: container,
),
);
},
@ -78,10 +74,14 @@ class _CardEnterRegion extends StatelessWidget {
List<Widget> children = [child];
if (onEnter) {
children.add(
CardAccessoryContainer(
accessories: accessories,
onTapAccessory: onTapAccessory,
).positioned(right: 0),
Positioned(
top: 8.0,
right: 8.0,
child: CardAccessoryContainer(
accessories: accessories,
onTapAccessory: onTapAccessory,
),
),
);
}