chore: calendar UI polish (#3484)

* chore: update calendar theming

* feat: add event popup editor

* chore: new event button redesign and add card shadows

* chore: unscheduled events button

* chore: event title text field

* fix: focus node double dispose

* chore: show popover when create new event

* test: integrate some tests for integration testing purposes

* fix: some fixes and more integration tests
This commit is contained in:
Richard Shiue 2023-09-23 09:47:48 +08:00 committed by GitHub
parent 593df96b10
commit 37ddce3a29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1020 additions and 355 deletions

View File

@ -79,33 +79,33 @@ void main() {
// Tap on create new event button
await tester.tapAddCalendarEventButton();
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event editor popup is shown
tester.assertEventEditorOpen();
tester.assertNumberOfEventsInCalendar(1);
// Dismiss the event editor popup
await tester.dismissEventEditor();
// Double click on today's calendar cell to create a new event
await tester.doubleClickCalendarCell(DateTime.now());
// Make sure that the row details page is opened
tester.assertRowDetailPageOpened();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Make sure that the event editor popup is shown
tester.assertEventEditorOpen();
// Make sure that the event is inserted in the cell
tester.assertNumberOfEventsInCalendar(2);
// Dismiss the event editor popup
await tester.dismissEventEditor();
// Click on the event
await tester.openCalendarEvent(index: 0);
tester.assertRowDetailPageOpened();
tester.assertEventEditorOpen();
// Change the title of the event
await tester.editTitleInRowDetailPage('hello world');
await tester.dismissRowDetailPage();
await tester.editEventTitle('hello world');
await tester.dismissEventEditor();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
@ -113,6 +113,10 @@ void main() {
// Click on the event
await tester.openCalendarEvent(index: 1);
tester.assertEventEditorOpen();
// Click on the open icon
await tester.openEventToRowDetailPage();
tester.assertRowDetailPageOpened();
// Duplicate the event
@ -126,12 +130,23 @@ void main() {
// Delete an event
await tester.openCalendarEvent(index: 1);
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
await tester.deleteEventFromEventEditor();
// Check that there is 1 event
tester.assertNumberOfEventsInCalendar(1, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
// Delete event from row detail page
await tester.openCalendarEvent(index: 1);
await tester.openEventToRowDetailPage();
tester.assertRowDetailPageOpened();
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
// Check that there is 0 event
tester.assertNumberOfEventsInCalendar(0, title: 'hello world');
tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now());
});
testWidgets('rescheduling events', (tester) async {
@ -145,7 +160,7 @@ void main() {
final today = DateTime.now();
final firstOfThisMonth = DateTime(today.year, today.month, 1);
await tester.doubleClickCalendarCell(firstOfThisMonth);
await tester.dismissRowDetailPage();
await tester.dismissEventEditor();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
@ -157,13 +172,12 @@ void main() {
// Delete the event
await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);
await tester.tapRowDetailPageRowActionButton();
await tester.tapRowDetailPageDeleteRowButton();
await tester.deleteEventFromEventEditor();
// Create a new event in today's calendar cell
await tester.scrollToToday();
await tester.doubleClickCalendarCell(today);
await tester.dismissRowDetailPage();
await tester.dismissEventEditor();
// Make sure that the event is today
tester.assertNumberOfEventsOnSpecificDay(1, today);
@ -182,12 +196,43 @@ void main() {
await tester.selectDay(content: newDate.day);
await tester.dismissCellEditor();
// Dismiss the row details page
await tester.dismissRowDetailPage();
// Dismiss the event editor
await tester.dismissEventEditor();
// Make sure that the event is edited
tester.assertNumberOfEventsInCalendar(1);
tester.assertNumberOfEventsOnSpecificDay(1, newDate);
// Click on the unscheduled events button
await tester.openUnscheduledEventsPopup();
// Assert that nothing shows up
tester.findUnscheduledPopup(findsNothing, 0);
// Click on the event in the calendar
await tester.openCalendarEvent(index: 0, date: newDate);
// Open the date editor of the event
await tester.tapDateCellInRowDetailPage();
await tester.findDateEditor(findsOneWidget);
// Clear the date of the event
await tester.clearDate();
// Dismiss the event editor
await tester.dismissEventEditor();
tester.assertNumberOfEventsInCalendar(0);
// Click on the unscheduled events button
await tester.openUnscheduledEventsPopup();
// Assert that a popup appears and 1 unscheduled event
tester.findUnscheduledPopup(findsOneWidget, 1);
// Click on the unscheduled event
await tester.clickUnscheduledEvent();
tester.assertRowDetailPageOpened();
});
});
}

View File

@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database_view/board/presentation/board_page.dar
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_editor.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
@ -1291,12 +1292,72 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(cards.at(index));
}
void assertEventEditorOpen() {
expect(find.byType(CalendarEventEditor), findsOneWidget);
}
Future<void> dismissEventEditor() async {
await simulateKeyEvent(LogicalKeyboardKey.escape);
}
Future<void> editEventTitle(String title) async {
final textField = find.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byType(FlowyTextField),
);
await enterText(textField, title);
await pumpAndSettle(const Duration(milliseconds: 300));
}
Future<void> openEventToRowDetailPage() async {
final button = find.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.full_view_s,
),
);
await tapButton(button);
}
Future<void> deleteEventFromEventEditor() async {
final button = find.descendant(
of: find.byType(CalendarEventEditor),
matching: find.byWidgetPredicate(
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
),
);
await tapButton(button);
}
Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
final findEventCard = find.byType(EventCard);
await drag(findEventCard.first, const Offset(0, 300));
await pumpAndSettle();
}
Future<void> openUnscheduledEventsPopup() async {
final button = find.byType(UnscheduledEventsButton);
await tapButton(button);
}
void findUnscheduledPopup(Matcher matcher, int numUnscheduledEvents) {
expect(find.byType(UnscheduleEventsList), matcher);
if (matcher != findsNothing) {
expect(
find.byType(UnscheduledEventCell),
findsNWidgets(numUnscheduledEvents),
);
}
}
Future<void> clickUnscheduledEvent() async {
final unscheduledEvent = find.byType(UnscheduledEventCell);
await tapButton(unscheduledEvent);
}
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
final findAddButton = find.byType(AddDatabaseViewButton);
await tapButton(findAddButton);

View File

@ -63,6 +63,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
createEvent: (DateTime date) async {
await _createEvent(date);
},
newEventPopupDisplayed: () {
emit(state.copyWith(editingEvent: null));
},
moveEvent: (CalendarDayEvent event, DateTime date) async {
await _moveEvent(event, date);
},
@ -378,6 +381,10 @@ class CalendarEvent with _$CalendarEvent {
CalendarEventData<CalendarDayEvent> event,
) = _DidReceiveNewEvent;
// Called after creating a new event
const factory CalendarEvent.newEventPopupDisplayed() =
_NewEventPopupDisplayed;
// Called when receive a new event
const factory CalendarEvent.didReceiveEvent(
CalendarEventData<CalendarDayEvent> event,

View File

@ -0,0 +1,82 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'calendar_event_editor_bloc.freezed.dart';
class CalendarEventEditorBloc
extends Bloc<CalendarEventEditorEvent, CalendarEventEditorState> {
final RowController rowController;
final CalendarLayoutSettingPB layoutSettings;
final RowBackendService _rowService;
CalendarEventEditorBloc({
required this.rowController,
required this.layoutSettings,
}) : _rowService = RowBackendService(viewId: rowController.viewId),
super(CalendarEventEditorState.initial()) {
on<CalendarEventEditorEvent>((event, emit) async {
await event.when(
initial: () {
_startListening();
final cells = rowController.loadData();
if (!isClosed) {
add(
CalendarEventEditorEvent.didReceiveCellDatas(
cells.values.toList(),
),
);
}
},
didReceiveCellDatas: (cells) {
emit(state.copyWith(cells: cells));
},
delete: () async {
final result = await _rowService.deleteRow(rowController.rowId);
result.fold((l) => null, (err) => Log.error(err));
},
);
});
}
void _startListening() {
rowController.addListener(
onRowChanged: (cells, reason) {
if (!isClosed) {
add(
CalendarEventEditorEvent.didReceiveCellDatas(cells.values.toList()),
);
}
},
);
}
@override
Future<void> close() async {
rowController.dispose();
return super.close();
}
}
@freezed
class CalendarEventEditorEvent with _$CalendarEventEditorEvent {
const factory CalendarEventEditorEvent.initial() = _Initial;
const factory CalendarEventEditorEvent.didReceiveCellDatas(
List<DatabaseCellContext> cells,
) = _DidReceiveCellDatas;
const factory CalendarEventEditorEvent.delete() = _Delete;
}
@freezed
class CalendarEventEditorState with _$CalendarEventEditorState {
const factory CalendarEventEditorState({
required List<DatabaseCellContext> cells,
}) = _CalendarEventEditorState;
factory CalendarEventEditorState.initial() =>
CalendarEventEditorState(cells: List.empty());
}

View File

@ -58,9 +58,12 @@ class UnscheduleEventsBloc
);
},
didReceiveEvent: (CalendarEventPB event) {
final events = [...state.allEvents, event];
emit(
state.copyWith(
allEvents: [...state.allEvents, event],
allEvents: events,
unscheduleEvents:
events.where((element) => !element.isScheduled).toList(),
),
);
},

View File

@ -1,13 +1,14 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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 '../application/calendar_bloc.dart';
@ -18,7 +19,7 @@ class CalendarDayCard extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
final RowCache _rowCache;
final RowCache rowCache;
final List<CalendarDayEvent> events;
final void Function(DateTime) onCreateEvent;
@ -28,18 +29,21 @@ class CalendarDayCard extends StatelessWidget {
required this.isInMonth,
required this.date,
required this.onCreateEvent,
required RowCache rowCache,
required this.rowCache,
required this.events,
Key? key,
}) : _rowCache = rowCache,
super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
Color backgroundColor = Theme.of(context).colorScheme.surface;
if (!isInMonth) {
backgroundColor = AFThemeExtension.of(context).lightGreyHover;
Color backgroundColor = Colors.transparent;
if (date.isWeekend) {
backgroundColor = AFThemeExtension.of(context).calendarWeekendBGColor;
}
final hoverBackgroundColor =
Theme.of(context).brightness == Brightness.light
? Theme.of(context).colorScheme.secondaryContainer
: Colors.transparent;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
@ -57,14 +61,14 @@ class CalendarDayCard extends StatelessWidget {
),
// Add a separator between the header and the content.
VSpace(GridSize.typeOptionSeparatorHeight),
const VSpace(6.0),
// List of cards or empty space
if (events.isNotEmpty)
_EventList(
events: events,
viewId: viewId,
rowCache: _rowCache,
rowCache: rowCache,
constraints: constraints,
),
],
@ -79,34 +83,29 @@ class CalendarDayCard extends StatelessWidget {
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),
Container(
width: double.infinity,
height: double.infinity,
color:
candidate.isEmpty ? null : hoverBackgroundColor,
padding: const EdgeInsets.only(top: 5.0),
child: child,
)
),
if (candidate.isEmpty)
NewEventButton(onCreate: () => onCreateEvent(date)),
],
);
},
onWillAccept: (CalendarDayEvent? event) {
if (event == null) {
return false;
}
return !isSameDay(event.date, date);
},
onAccept: (CalendarDayEvent event) {
if (event.date == date) {
return;
}
context
.read<CalendarBloc>()
.add(CalendarEvent.moveEvent(event, date));
},
),
NewEventButton(onCreate: () => onCreateEvent(date)),
MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
@ -143,7 +142,7 @@ class _Header extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: _DayBadge(
isToday: isToday,
isInMonth: isInMonth,
@ -166,7 +165,7 @@ class NewEventButton extends StatelessWidget {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(4.0),
child: FlowyIconButton(
onPressed: onCreate,
iconPadding: EdgeInsets.zero,
@ -174,6 +173,35 @@ class NewEventButton extends StatelessWidget {
fillColor: Theme.of(context).colorScheme.background,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 22,
tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(),
decoration: BoxDecoration(
border: Border.fromBorderSide(
BorderSide(
color: Theme.of(context).brightness == Brightness.light
? const Color(0xffd0d3d6)
: const Color(0xff59647a),
width: 0.5,
),
),
borderRadius: Corners.s5Border,
boxShadow: [
BoxShadow(
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 2,
),
BoxShadow(
spreadRadius: 0,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 4,
),
BoxShadow(
spreadRadius: 2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 8,
),
],
),
),
);
},
@ -195,37 +223,49 @@ class _DayBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color dayTextColor = Theme.of(context).colorScheme.onBackground;
Color monthTextColor = Theme.of(context).colorScheme.onBackground;
final String monthString =
DateFormat("MMM ", context.locale.toLanguageTag()).format(date);
final String dayString = date.day.toString();
if (!isInMonth) {
dayTextColor = Theme.of(context).disabledColor;
monthTextColor = Theme.of(context).disabledColor;
}
if (isToday) {
dayTextColor = Theme.of(context).colorScheme.onPrimary;
}
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (date.day == 1) FlowyText.medium(monthString),
Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: Corners.s6Border,
),
width: isToday ? 26 : null,
height: isToday ? 26 : null,
padding: GridSize.typeOptionContentInsets,
child: Center(
child: FlowyText.medium(
dayString,
color: dayTextColor,
return SizedBox(
height: 18,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (date.day == 1)
FlowyText.medium(
monthString,
fontSize: 11,
color: monthTextColor,
),
Container(
decoration: BoxDecoration(
color: isToday ? Theme.of(context).colorScheme.primary : null,
borderRadius: BorderRadius.circular(10),
),
width: isToday ? 18 : null,
height: isToday ? 18 : null,
// padding: GridSize.typeOptionContentInsets,
child: Center(
child: FlowyText.medium(
dayString,
fontSize: 11,
color: dayTextColor,
),
),
),
),
],
],
),
);
}
}
@ -246,20 +286,26 @@ class _EventList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final editingEvent = context.watch<CalendarBloc>().state.editingEvent;
return Flexible(
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: true,
),
child: ListView.separated(
itemBuilder: (BuildContext context, int index) => EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
),
itemBuilder: (BuildContext context, int index) {
final autoEdit =
editingEvent?.event?.eventId == events[index].eventId;
return EventCard(
event: events[index],
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
autoEdit: autoEdit,
);
},
itemCount: events.length,
padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 8.0),
padding: const EdgeInsets.fromLTRB(4.0, 0, 4.0, 4.0),
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,

View File

@ -8,40 +8,66 @@ import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_ce
import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
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:flutter_bloc/flutter_bloc.dart';
import '../application/calendar_bloc.dart';
import 'calendar_page.dart';
import 'calendar_event_editor.dart';
class EventCard extends StatelessWidget {
class EventCard extends StatefulWidget {
final CalendarDayEvent event;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
final bool autoEdit;
const EventCard({
required this.event,
required this.viewId,
required this.rowCache,
required this.constraints,
required this.autoEdit,
super.key,
});
@override
State<EventCard> createState() => _EventCardState();
}
class _EventCardState extends State<EventCard> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
if (widget.autoEdit) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_popoverController.show();
context
.read<CalendarBloc>()
.add(const CalendarEvent.newEventPopupDisplayed());
});
}
}
@override
Widget build(BuildContext context) {
final rowInfo = rowCache.getRow(event.eventId);
final rowInfo = widget.rowCache.getRow(widget.event.eventId);
if (rowInfo == null) {
return const SizedBox.shrink();
}
final styles = <FieldType, CardCellStyle>{
FieldType.Number: NumberCardCellStyle(10),
FieldType.URL: URLCardCellStyle(10),
};
final cellBuilder = CardCellBuilder<CalendarDayEvent>(
rowCache.cellCache,
widget.rowCache.cellCache,
styles: styles,
);
final renderHook = _calendarEventCardRenderHook(context);
@ -49,24 +75,22 @@ class EventCard extends StatelessWidget {
final card = RowCard<CalendarDayEvent>(
// Add the key here to make sure the card is rebuilt when the cells
// in this row are updated.
key: ValueKey(event.eventId),
rowMeta: rowInfo!.rowMeta,
viewId: viewId,
rowCache: rowCache,
cardData: event,
key: ValueKey(widget.event.eventId),
rowMeta: rowInfo.rowMeta,
viewId: widget.viewId,
rowCache: widget.rowCache,
cardData: widget.event,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (context) => showEventDetails(
context: context,
event: event.event,
viewId: viewId,
rowCache: rowCache,
),
openCard: (_) => _popoverController.show(),
styleConfiguration: RowCardStyleConfiguration(
showAccessory: false,
cellPadding: EdgeInsets.zero,
cardPadding: const EdgeInsets.all(6),
hoverStyle: HoverStyle(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: Theme.of(context).brightness == Brightness.light
? const Color(0x0F1F2329)
: const Color(0x0FEFF4FB),
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
),
),
@ -76,28 +100,76 @@ class EventCard extends StatelessWidget {
);
final decoration = BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
BorderSide(
color: Theme.of(context).brightness == Brightness.light
? const Color(0xffd0d3d6)
: const Color(0xff59647a),
width: 0.5,
),
),
borderRadius: Corners.s6Border,
boxShadow: [
BoxShadow(
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 2,
),
BoxShadow(
spreadRadius: 0,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 4,
),
BoxShadow(
spreadRadius: 2,
color: const Color(0xFF1F2329).withOpacity(0.02),
blurRadius: 8,
),
],
);
return Draggable<CalendarDayEvent>(
data: event,
data: widget.event,
feedback: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.maxWidth - 16.0,
maxWidth: widget.constraints.maxWidth - 8.0,
),
child: DecoratedBox(
decoration: decoration.copyWith(
color: AFThemeExtension.of(context).lightGreyHover,
child: Opacity(
opacity: 0.6,
child: DecoratedBox(
decoration: decoration,
child: card,
),
child: card,
),
),
child: DecoratedBox(
decoration: decoration,
child: card,
child: AppFlowyPopover(
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.rightWithCenterAligned,
controller: _popoverController,
constraints: const BoxConstraints(maxWidth: 360, maxHeight: 348),
asBarrier: true,
margin: EdgeInsets.zero,
offset: const Offset(10.0, 0),
popupBuilder: (BuildContext popoverContext) {
final settings = context.watch<CalendarBloc>().state.settings.fold(
() => null,
(layoutSettings) => layoutSettings,
);
if (settings == null) {
return const SizedBox.shrink();
}
return CalendarEventEditor(
rowCache: widget.rowCache,
rowMeta: widget.event.event.rowMeta,
viewId: widget.viewId,
layoutSettings: settings,
);
},
child: DecoratedBox(
decoration: decoration,
child: card,
),
),
);
}
@ -127,8 +199,9 @@ class EventCard extends StatelessWidget {
child: FlowyText.medium(
text,
textAlign: TextAlign.left,
fontSize: 11,
maxLines: null, // Enable multiple lines
fontSize: isTitle ? 11 : 10,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
},

View File

@ -0,0 +1,280 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/application/row/row_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_event_editor_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_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.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';
class CalendarEventEditor extends StatelessWidget {
final RowController rowController;
final CalendarLayoutSettingPB layoutSettings;
final GridCellBuilder cellBuilder;
CalendarEventEditor({
super.key,
required RowCache rowCache,
required RowMetaPB rowMeta,
required String viewId,
required this.layoutSettings,
}) : rowController = RowController(
rowMeta: rowMeta,
viewId: viewId,
rowCache: rowCache,
),
cellBuilder = GridCellBuilder(cellCache: rowCache.cellCache);
@override
Widget build(BuildContext context) {
return BlocProvider<CalendarEventEditorBloc>(
create: (context) => CalendarEventEditorBloc(
rowController: rowController,
layoutSettings: layoutSettings,
)..add(const CalendarEventEditorEvent.initial()),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
EventEditorControls(rowController: rowController),
Flexible(
child: EventPropertyList(
dateFieldId: layoutSettings.fieldId,
cellBuilder: cellBuilder,
),
),
],
),
);
}
}
class EventEditorControls extends StatelessWidget {
final RowController rowController;
const EventEditorControls({
super.key,
required this.rowController,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.delete_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () => context
.read<CalendarEventEditorBloc>()
.add(const CalendarEventEditorEvent.delete()),
),
const HSpace(8.0),
FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.full_view_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () {
PopoverContainer.of(context).close();
FlowyOverlay.show(
context: context,
builder: (BuildContext context) {
return RowDetailPage(
cellBuilder: GridCellBuilder(
cellCache: rowController.cellCache,
),
rowController: rowController,
);
},
);
},
),
],
),
);
}
}
class EventPropertyList extends StatelessWidget {
final String dateFieldId;
final GridCellBuilder cellBuilder;
const EventPropertyList({
super.key,
required this.dateFieldId,
required this.cellBuilder,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<CalendarEventEditorBloc, CalendarEventEditorState>(
builder: (context, state) {
final reorderedList = List<DatabaseCellContext>.from(state.cells)
..retainWhere((cell) => !cell.fieldInfo.isPrimary);
final primaryCellContext =
state.cells.firstWhereOrNull((cell) => cell.fieldInfo.isPrimary);
final dateFieldIndex =
reorderedList.indexWhere((cell) => cell.fieldId == dateFieldId);
if (primaryCellContext == null || dateFieldIndex == -1) {
return const SizedBox.shrink();
}
reorderedList.insert(0, reorderedList.removeAt(dateFieldIndex));
final children = <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: cellBuilder.build(
primaryCellContext,
style: GridTextCellStyle(
cellPadding: EdgeInsets.zero,
placeholder: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),
textStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontSize: 11),
autofocus: true,
useRoundedBorder: true,
),
),
),
...reorderedList.map(
(cell) => PropertyCell(cellContext: cell, cellBuilder: cellBuilder),
),
];
return ListView(
shrinkWrap: true,
padding: const EdgeInsets.only(bottom: 16.0),
children: children,
);
},
);
}
}
class PropertyCell extends StatefulWidget {
final DatabaseCellContext cellContext;
final GridCellBuilder cellBuilder;
const PropertyCell({
required this.cellContext,
required this.cellBuilder,
Key? key,
}) : super(key: key);
@override
State<StatefulWidget> 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);
final gesture = GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(child: cell),
);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
constraints: const BoxConstraints(minHeight: 28),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: 88,
height: 28,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 6),
child: Row(
children: [
FlowySvg(
widget.cellContext.fieldType.icon(),
color: Theme.of(context).hintColor,
size: const Size.square(14),
),
const HSpace(4.0),
FlowyText.regular(
widget.cellContext.fieldInfo.name,
color: Theme.of(context).hintColor,
fontSize: 11,
),
],
),
),
),
const HSpace(8),
Expanded(child: gesture),
],
),
);
}
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 SelectOptionCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
);
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;
}
}

View File

@ -2,11 +2,15 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -119,19 +123,6 @@ class _CalendarPageState extends State<CalendarPage> {
);
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.editingEvent != c.editingEvent,
listener: (context, state) {
if (state.editingEvent != null) {
showEventDetails(
context: context,
event: state.editingEvent!.event!.event,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
}
},
),
BlocListener<CalendarBloc, CalendarState>(
// Event create by click the + button or double click on the
// calendar
@ -211,40 +202,49 @@ class _CalendarPageState extends State<CalendarPage> {
}
Widget _headerNavigatorBuilder(DateTime currentMonth) {
return Row(
children: [
FlowyText.medium(
DateFormat('MMMM y', context.locale.toLanguageTag())
.format(currentMonth),
),
const Spacer(),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_left_s),
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.previousPage(),
),
FlowyTextButton(
LocaleKeys.calendar_navigation_today.tr(),
fillColor: Colors.transparent,
fontWeight: FontWeight.w500,
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () =>
_calendarState?.currentState?.animateToMonth(DateTime.now()),
),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_right_s),
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.nextPage(),
),
],
return SizedBox(
height: 24,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlowyText.medium(
DateFormat('MMMM y', context.locale.toLanguageTag())
.format(currentMonth),
),
const Spacer(),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_left_s),
tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.previousPage(),
),
FlowyTextButton(
LocaleKeys.calendar_navigation_today.tr(),
fillColor: Colors.transparent,
fontWeight: FontWeight.w400,
fontSize: 10,
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () =>
_calendarState?.currentState?.animateToMonth(DateTime.now()),
),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
height: CalendarSize.navigatorButtonHeight,
icon: const FlowySvg(FlowySvgs.arrow_right_s),
tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
onPressed: () => _calendarState?.currentState?.nextPage(),
),
const HSpace(6.0),
UnscheduledEventsButton(
databaseController: widget.databaseController,
),
],
),
);
}
@ -255,8 +255,9 @@ class _CalendarPageState extends State<CalendarPage> {
return Center(
child: Padding(
padding: CalendarSize.daysOfWeekInsets,
child: FlowyText.medium(
child: FlowyText.regular(
weekDayString,
fontSize: 9,
color: Theme.of(context).hintColor,
),
),
@ -321,3 +322,150 @@ void showEventDetails({
},
);
}
class UnscheduledEventsButton extends StatefulWidget {
final DatabaseController databaseController;
const UnscheduledEventsButton({super.key, required this.databaseController});
@override
State<UnscheduledEventsButton> createState() =>
_UnscheduledEventsButtonState();
}
class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
}
@override
Widget build(BuildContext context) {
return BlocProvider<UnscheduleEventsBloc>(
create: (_) =>
UnscheduleEventsBloc(databaseController: widget.databaseController)
..add(const UnscheduleEventsEvent.initial()),
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
builder: (context, state) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
triggerActions: PopoverTriggerFlags.none,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 282, maxHeight: 600),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: Corners.s6Border,
),
side:
BorderSide(color: Theme.of(context).dividerColor, width: 1),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
visualDensity: VisualDensity.compact,
),
onPressed: () {
if (state.unscheduleEvents.isNotEmpty) {
_popoverController.show();
}
},
child: FlowyText.regular(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
fontSize: 10,
),
),
popupBuilder: (context) {
return UnscheduleEventsList(
viewId: widget.databaseController.viewId,
rowCache: widget.databaseController.rowCache,
unscheduleEvents: state.unscheduleEvents,
);
},
);
},
),
);
}
}
class UnscheduleEventsList extends StatelessWidget {
final String viewId;
final RowCache rowCache;
final List<CalendarEventPB> unscheduleEvents;
const UnscheduleEventsList({
required this.viewId,
required this.unscheduleEvents,
required this.rowCache,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
LocaleKeys.calendar_settings_clickToAdd.tr(),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
...unscheduleEvents.map(
(e) => UnscheduledEventCell(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e,
viewId: viewId,
rowCache: rowCache,
);
PopoverContainer.of(context).close();
},
),
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
}
}
class UnscheduledEventCell extends StatelessWidget {
final CalendarEventPB event;
final VoidCallback onPressed;
const UnscheduledEventCell({
required this.event,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 26,
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
text: FlowyText.medium(
event.title.isEmpty
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
: event.title,
fontSize: 11,
),
onTap: onPressed,
),
);
}
}

View File

@ -15,7 +15,7 @@ class CalendarSize {
static double get scrollBarSize => 8 * scale;
static double get navigatorButtonWidth => 20 * scale;
static double get navigatorButtonHeight => 25 * scale;
static double get navigatorButtonHeight => 24 * scale;
static EdgeInsets get daysOfWeekInsets =>
EdgeInsets.symmetric(vertical: 10.0 * scale);
EdgeInsets.only(top: 12.0 * scale, bottom: 5.0 * scale);
}

View File

@ -1,18 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CalendarSettingBar extends StatelessWidget {
final DatabaseController databaseController;
@ -28,8 +16,6 @@ class CalendarSettingBar extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
UnscheduleEventsButton(databaseController: databaseController),
const HSpace(2),
SettingButton(
databaseController: databaseController,
),
@ -38,148 +24,3 @@ class CalendarSettingBar extends StatelessWidget {
);
}
}
class UnscheduleEventsButton extends StatefulWidget {
final DatabaseController databaseController;
const UnscheduleEventsButton({
required this.databaseController,
Key? key,
}) : super(key: key);
@override
State<UnscheduleEventsButton> createState() => _UnscheduleEventsButtonState();
}
class _UnscheduleEventsButtonState extends State<UnscheduleEventsButton> {
late final PopoverController _popoverController;
late final UnscheduleEventsBloc _bloc;
@override
void initState() {
super.initState();
_bloc = UnscheduleEventsBloc(databaseController: widget.databaseController)
..add(const UnscheduleEventsEvent.initial());
_popoverController = PopoverController();
}
@override
dispose() {
_bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
child: BlocProvider.value(
value: _bloc,
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
buildWhen: (previous, current) =>
previous.unscheduleEvents.length !=
current.unscheduleEvents.length,
builder: (context, state) {
return FlowyTextButton(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
fontSize: FontSizes.s11,
fontColor: AFThemeExtension.of(context).textColor,
fontWeight: FontWeight.w400,
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border,
onPressed: () => _popoverController.show(),
);
},
),
),
popupBuilder: (context) {
return UnscheduleEventsList(
viewId: _bloc.viewId,
rowCache: _bloc.rowCache,
controller: _popoverController,
unscheduleEvents: _bloc.state.unscheduleEvents,
);
},
);
}
}
class UnscheduleEventsList extends StatelessWidget {
final String viewId;
final RowCache rowCache;
final PopoverController controller;
final List<CalendarEventPB> unscheduleEvents;
const UnscheduleEventsList({
required this.viewId,
required this.controller,
required this.unscheduleEvents,
required this.rowCache,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
LocaleKeys.calendar_settings_clickToAdd.tr(),
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
const VSpace(6),
...unscheduleEvents.map(
(e) => UnscheduledEventCell(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e,
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,
);
}
}
class UnscheduledEventCell extends StatelessWidget {
final CalendarEventPB event;
final VoidCallback onPressed;
const UnscheduledEventCell({
required this.event,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
event.title.isEmpty
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
: event.title,
),
onTap: onPressed,
),
);
}
}

View File

@ -205,6 +205,17 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
},
);
}
@override
void dispose() {
focusNode.removeListener(() {
if (focusNode.hasFocus) {
widget.popoverMutex.close();
}
});
focusNode.dispose();
super.dispose();
}
}
class _DeleteFieldButton extends StatelessWidget {

View File

@ -325,4 +325,15 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
},
);
}
@override
void dispose() {
_focusNode.removeListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex?.close();
}
});
_focusNode.dispose();
super.dispose();
}
}

View File

@ -247,7 +247,9 @@ class RequestFocusListener extends ChangeNotifier {
}
}
abstract class GridCellStyle {}
abstract class GridCellStyle {
const GridCellStyle();
}
class SingleListenerFocusNode extends FocusNode {
VoidCallback? _listener;

View File

@ -438,6 +438,18 @@ class _TimeTextFieldState extends State<_TimeTextField> {
},
);
}
@override
void dispose() {
_textController.dispose();
_focusNode.removeListener(() {
if (_focusNode.hasFocus) {
widget.popoverMutex.close();
}
});
_focusNode.dispose();
super.dispose();
}
}
@visibleForTesting

View File

@ -8,22 +8,24 @@ import '../../../../grid/presentation/layout/sizes.dart';
import '../../cell_builder.dart';
class GridTextCellStyle extends GridCellStyle {
String? placeholder;
TextStyle? textStyle;
bool? autofocus;
double emojiFontSize;
double emojiHPadding;
bool showEmoji;
EdgeInsets? cellPadding;
final String? placeholder;
final TextStyle? textStyle;
final EdgeInsets? cellPadding;
final bool autofocus;
final double emojiFontSize;
final double emojiHPadding;
final bool showEmoji;
final bool useRoundedBorder;
GridTextCellStyle({
const GridTextCellStyle({
this.placeholder,
this.textStyle,
this.autofocus,
this.cellPadding,
this.autofocus = false,
this.showEmoji = true,
this.emojiFontSize = 16,
this.emojiHPadding = 0,
this.cellPadding,
this.useRoundedBorder = false,
});
}
@ -38,7 +40,7 @@ class GridTextCell extends GridCellWidget {
if (style != null) {
cellStyle = (style as GridTextCellStyle);
} else {
cellStyle = GridTextCellStyle();
cellStyle = const GridTextCellStyle();
}
}
@ -55,12 +57,12 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
@override
void initState() {
super.initState();
final cellController =
widget.cellControllerBuilder.build() as TextCellController;
_cellBloc = TextCellBloc(cellController: cellController);
_cellBloc.add(const TextCellEvent.initial());
_cellBloc = TextCellBloc(cellController: cellController)
..add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content);
super.initState();
}
@override
@ -94,23 +96,36 @@ class _GridTextCellState extends GridEditableTextCell<GridTextCell> {
),
HSpace(widget.cellStyle.emojiHPadding),
Expanded(
child: TextField(
controller: _controller,
focusNode: focusNode,
maxLines: null,
style: widget.cellStyle.textStyle ??
Theme.of(context).textTheme.bodyMedium,
autofocus: widget.cellStyle.autofocus ?? false,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
top: GridSize.cellContentInsets.top,
bottom: GridSize.cellContentInsets.bottom,
),
border: InputBorder.none,
hintText: widget.cellStyle.placeholder,
isDense: true,
),
),
child: widget.cellStyle.useRoundedBorder
? FlowyTextField(
controller: _controller,
textStyle: widget.cellStyle.textStyle ??
Theme.of(context).textTheme.bodyMedium,
focusNode: focusNode,
autoFocus: widget.cellStyle.autofocus,
hintText: widget.cellStyle.placeholder,
onChanged: (text) => _cellBloc.add(
TextCellEvent.updateText(text),
),
debounceDuration: const Duration(milliseconds: 300),
)
: TextField(
controller: _controller,
focusNode: focusNode,
maxLines: null,
style: widget.cellStyle.textStyle ??
Theme.of(context).textTheme.bodyMedium,
autofocus: widget.cellStyle.autofocus,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
top: GridSize.cellContentInsets.top,
bottom: GridSize.cellContentInsets.bottom,
),
border: InputBorder.none,
hintText: widget.cellStyle.placeholder,
isDense: true,
),
),
)
],
),

View File

@ -127,6 +127,7 @@ class _AutoCompletionBlockComponentState
_onExit();
_unsubscribeSelectionGesture();
controller.dispose();
textFieldFocusNode.dispose();
super.dispose();
}

View File

@ -433,6 +433,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
toggleOffFill: theme.shader5,
progressBarBGColor: theme.progressBarBGColor,
toggleButtonBGColor: theme.toggleButtonBGColor,
calendarWeekendBGColor: theme.calendarWeekendBGColor,
code: _getFontStyle(
fontFamily: monospaceFontFamily,
fontColor: theme.shader3,

View File

@ -85,6 +85,7 @@ class FlowyColorScheme {
//editor toolbar BG color
final Color toolbarColor;
final Color toggleButtonBGColor;
final Color calendarWeekendBGColor;
const FlowyColorScheme({
required this.surface,
@ -133,6 +134,7 @@ class FlowyColorScheme {
required this.progressBarBGColor,
required this.toolbarColor,
required this.toggleButtonBGColor,
required this.calendarWeekendBGColor,
});
factory FlowyColorScheme.fromJson(Map<String, dynamic> json) =>

View File

@ -72,6 +72,7 @@ class DandelionColorScheme extends FlowyColorScheme {
progressBarBGColor: _lightTint9,
toolbarColor: _lightShader1,
toggleButtonBGColor: _lightShader5,
calendarWeekendBGColor: const Color(0xFFFBFBFC),
);
const DandelionColorScheme.dark()
@ -122,5 +123,6 @@ class DandelionColorScheme extends FlowyColorScheme {
progressBarBGColor: _darkShader3,
toolbarColor: _darkInput,
toggleButtonBGColor: _darkShader1,
calendarWeekendBGColor: const Color(0xff121212),
);
}

View File

@ -70,6 +70,7 @@ class DefaultColorScheme extends FlowyColorScheme {
progressBarBGColor: _lightTint9,
toolbarColor: _lightShader1,
toggleButtonBGColor: _lightShader5,
calendarWeekendBGColor: const Color(0xFFFBFBFC),
);
const DefaultColorScheme.dark()
@ -120,5 +121,6 @@ class DefaultColorScheme extends FlowyColorScheme {
progressBarBGColor: _darkShader3,
toolbarColor: _darkInput,
toggleButtonBGColor: _darkShader1,
calendarWeekendBGColor: const Color(0xff121212),
);
}

View File

@ -73,6 +73,7 @@ class LavenderColorScheme extends FlowyColorScheme {
progressBarBGColor: _lightTint9,
toolbarColor: _lightShader1,
toggleButtonBGColor: _lightSelector,
calendarWeekendBGColor: const Color(0xFFFBFBFC),
);
const LavenderColorScheme.dark()
@ -123,5 +124,6 @@ class LavenderColorScheme extends FlowyColorScheme {
progressBarBGColor: _darkShader3,
toolbarColor: _darkInput,
toggleButtonBGColor: _darkShader1,
calendarWeekendBGColor: const Color(0xff121212),
);
}

View File

@ -24,6 +24,7 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
final Color toggleButtonBGColor;
final Color calloutBGColor;
final Color tableCellBGColor;
final Color calendarWeekendBGColor;
final TextStyle code;
final TextStyle callout;
@ -48,6 +49,7 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
required this.textColor,
required this.calloutBGColor,
required this.tableCellBGColor,
required this.calendarWeekendBGColor,
required this.code,
required this.callout,
required this.caption,
@ -81,6 +83,7 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
Color? toggleOffFill,
Color? progressBarBGColor,
Color? toggleButtonBGColor,
Color? calendarWeekendBGColor,
TextStyle? code,
TextStyle? callout,
TextStyle? caption,
@ -106,6 +109,8 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
toggleOffFill: toggleOffFill ?? this.toggleOffFill,
progressBarBGColor: progressBarBGColor ?? this.progressBarBGColor,
toggleButtonBGColor: toggleButtonBGColor ?? this.toggleButtonBGColor,
calendarWeekendBGColor:
calendarWeekendBGColor ?? this.calendarWeekendBGColor,
code: code ?? this.code,
callout: callout ?? this.callout,
caption: caption ?? this.caption,
@ -142,6 +147,8 @@ class AFThemeExtension extends ThemeExtension<AFThemeExtension> {
Color.lerp(progressBarBGColor, other.progressBarBGColor, t)!,
toggleButtonBGColor:
Color.lerp(toggleButtonBGColor, other.toggleButtonBGColor, t)!,
calendarWeekendBGColor:
Color.lerp(calendarWeekendBGColor, other.calendarWeekendBGColor, t)!,
code: other.code,
callout: other.callout,
caption: other.caption,

View File

@ -16,6 +16,7 @@ class FlowyIconButton extends StatelessWidget {
final String? tooltipText;
final InlineSpan? richTooltipText;
final bool preferBelow;
final BoxDecoration? decoration;
const FlowyIconButton({
Key? key,
@ -27,6 +28,7 @@ class FlowyIconButton extends StatelessWidget {
this.iconColorOnHover,
this.iconPadding = EdgeInsets.zero,
this.radius,
this.decoration,
this.tooltipText,
this.richTooltipText,
this.preferBelow = true,
@ -47,11 +49,12 @@ class FlowyIconButton extends StatelessWidget {
assert(size.width > iconPadding.horizontal);
assert(size.height > iconPadding.vertical);
return ConstrainedBox(
return Container(
constraints: BoxConstraints.tightFor(
width: size.width,
height: size.height,
),
decoration: decoration,
child: Tooltip(
preferBelow: preferBelow,
message: tooltipMessage,

View File

@ -5,8 +5,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class FlowyTextField extends StatefulWidget {
final String hintText;
final String text;
final String? hintText;
final String? text;
final TextStyle? textStyle;
final void Function(String)? onChanged;
final void Function()? onEditingComplete;
final void Function(String)? onSubmitted;
@ -23,7 +24,8 @@ class FlowyTextField extends StatefulWidget {
const FlowyTextField({
this.hintText = "",
this.text = "",
this.text,
this.textStyle,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
@ -55,7 +57,10 @@ class FlowyTextFieldState extends State<FlowyTextField> {
focusNode.addListener(notifyDidEndEditing);
controller = widget.controller ?? TextEditingController();
controller.text = widget.text;
if (widget.text != null) {
controller.text = widget.text!;
}
if (widget.autoFocus) {
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
@ -105,7 +110,7 @@ class FlowyTextFieldState extends State<FlowyTextField> {
maxLines: widget.maxLines,
maxLength: widget.maxLength,
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
style: Theme.of(context).textTheme.bodySmall,
style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall,
decoration: InputDecoration(
constraints: BoxConstraints(
maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58),
@ -158,7 +163,9 @@ class FlowyTextFieldState extends State<FlowyTextField> {
@override
void dispose() {
focusNode.removeListener(notifyDidEndEditing);
focusNode.dispose();
if (widget.focusNode == null) {
focusNode.dispose();
}
super.dispose();
}

View File

@ -659,6 +659,7 @@
"calendar": {
"menuName": "Calendar",
"defaultNewCalendarTitle": "Untitled",
"newEventButtonTooltip": "Add a new event",
"navigation": {
"today": "Today",
"jumpToday": "Jump to Today",