mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user