feat: optimize calendar for mobile (#3979)

* feat: calendar mobile ui

- Resolves double border on Calendar Cells
- Adds Jump to year quick action for Mobile
- Reduces Cell height in Calendar
- Change out EventList with EventIndicator in Cell

* chore: push card details screen

* fix: navigation to card details

* feat: day events screen update on new event

* fix: changes after merging main

* fix: missing argument

* fix: amend test and remove stack
This commit is contained in:
Mathias Mogensen 2023-11-25 16:31:54 +02:00 committed by GitHub
parent b3dd5fb8bd
commit 7fb1b4f43f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 510 additions and 231 deletions

View File

@ -74,10 +74,10 @@ void main() {
await tester.scrollToToday();
// Hover over today's calendar cell
await tester.hoverOnTodayCalendarCell();
await tester.hoverOnTodayCalendarCell(
// Tap on create new event button
await tester.tapAddCalendarEventButton();
onHover: () async => await tester.tapAddCalendarEventButton(),
);
// Make sure that the event editor popup is shown
tester.assertEventEditorOpen();
@ -90,15 +90,9 @@ void main() {
// Double click on today's calendar cell to create a new event
await tester.doubleClickCalendarCell(DateTime.now());
// 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.assertEventEditorOpen();
@ -112,7 +106,7 @@ void main() {
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
// Click on the event
await tester.openCalendarEvent(index: 1);
await tester.openCalendarEvent(index: 0);
tester.assertEventEditorOpen();
// Click on the open icon
@ -137,7 +131,7 @@ void main() {
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
// Delete event from row detail page
await tester.openCalendarEvent(index: 1);
await tester.openCalendarEvent(index: 0);
await tester.openEventToRowDetailPage();
tester.assertRowDetailPageOpened();
@ -163,7 +157,7 @@ void main() {
await tester.dismissEventEditor();
// Drag and drop the event onto the next week, same day
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
await tester.dragDropRescheduleCalendarEvent();
// Make sure that the event has been rescheduled to the new date
final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7));

View File

@ -1249,12 +1249,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle(const Duration(milliseconds: 300));
}
Future<void> hoverOnTodayCalendarCell() async {
Future<void> hoverOnTodayCalendarCell({
Future<void> Function()? onHover,
}) async {
final todayCell = find.byWidgetPredicate(
(widget) => widget is CalendarDayCard && widget.isToday,
);
await hoverOnWidget(todayCell);
await hoverOnWidget(todayCell, onHover: onHover);
}
Future<void> tapAddCalendarEventButton() async {
@ -1362,7 +1364,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
Future<void> dragDropRescheduleCalendarEvent() async {
final findEventCard = find.byType(EventCard);
await drag(findEventCard.first, const Offset(0, 300));
await pumpAndSettle();

View File

@ -0,0 +1,97 @@
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileCalendarEventsScreen extends StatefulWidget {
static const routeName = "/calendar-events";
// GoRouter Arguments
static const calendarBlocKey = "calendar_bloc";
static const calendarDateKey = "date";
static const calendarEventsKey = "events";
static const calendarRowCacheKey = "row_cache";
static const calendarViewIdKey = "view_id";
const MobileCalendarEventsScreen({
super.key,
required this.calendarBloc,
required this.date,
required this.events,
required this.rowCache,
required this.viewId,
});
final CalendarBloc calendarBloc;
final DateTime date;
final List<CalendarDayEvent> events;
final RowCache rowCache;
final String viewId;
@override
State<MobileCalendarEventsScreen> createState() =>
_MobileCalendarEventsScreenState();
}
class _MobileCalendarEventsScreenState
extends State<MobileCalendarEventsScreen> {
late final List<CalendarDayEvent> _events = widget.events;
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: widget.calendarBloc,
child: BlocBuilder<CalendarBloc, CalendarState>(
buildWhen: (p, c) => p.newEvent != c.newEvent,
builder: (context, state) {
if (state.newEvent?.event != null) {
_events.add(state.newEvent!.event!);
}
return Scaffold(
floatingActionButton: FloatingActionButton(
key: const Key('add_event_fab'),
elevation: 6,
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
onPressed: () => widget.calendarBloc
.add(CalendarEvent.createEvent(widget.date)),
child: const Text('+'),
),
appBar: AppBar(
title: Text(
DateFormat.yMMMMd(context.locale.toLanguageTag())
.format(widget.date),
),
),
body: SingleChildScrollView(
child: Column(
children: [
const VSpace(10),
...widget.events.map((event) {
return ListTile(
dense: true,
title: EventCard(
fieldController: widget.calendarBloc.fieldController,
event: event,
viewId: widget.viewId,
rowCache: widget.rowCache,
constraints: const BoxConstraints.expand(),
autoEdit: false,
isDraggable: false,
),
);
}),
const VSpace(24),
],
),
),
);
},
),
);
}
}

View File

@ -146,11 +146,11 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
(settings) async {
final dateField = _getCalendarFieldInfo(settings.fieldId);
if (dateField != null) {
final newRow = await databaseController.createRow(
withCells: (builder) {
builder.insertDate(dateField, date);
},
).then(
final newRow = await databaseController
.createRow(
withCells: (builder) => builder.insertDate(dateField, date),
)
.then(
(result) => result.fold(
(newRow) => newRow,
(err) {
@ -207,10 +207,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
final payload = RowIdPB(viewId: viewId, rowId: rowId);
return DatabaseEventGetCalendarEvent(payload).send().then((result) {
return result.fold(
(eventPB) {
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
return calendarEvent;
},
(eventPB) => _calendarEventDataFromEventPB(eventPB),
(r) {
Log.error(r);
return null;

View File

@ -1,6 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_screen.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
@ -8,6 +11,7 @@ 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:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../../grid/presentation/layout/sizes.dart';
@ -15,6 +19,18 @@ import '../application/calendar_bloc.dart';
import 'calendar_event_card.dart';
class CalendarDayCard extends StatelessWidget {
const CalendarDayCard({
super.key,
required this.viewId,
required this.isToday,
required this.isInMonth,
required this.date,
required this.rowCache,
required this.events,
required this.onCreateEvent,
required this.position,
});
final String viewId;
final bool isToday;
final bool isInMonth;
@ -22,24 +38,10 @@ class CalendarDayCard extends StatelessWidget {
final RowCache rowCache;
final List<CalendarDayEvent> events;
final void Function(DateTime) onCreateEvent;
const CalendarDayCard({
required this.viewId,
required this.isToday,
required this.isInMonth,
required this.date,
required this.onCreateEvent,
required this.rowCache,
required this.events,
super.key,
});
final CellPosition position;
@override
Widget build(BuildContext context) {
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
@ -64,35 +66,51 @@ class CalendarDayCard extends StatelessWidget {
const VSpace(6.0),
// List of cards or empty space
if (events.isNotEmpty)
if (events.isNotEmpty && !PlatformExtension.isMobile) ...[
_EventList(
events: events,
viewId: viewId,
rowCache: rowCache,
constraints: constraints,
),
] else if (events.isNotEmpty && PlatformExtension.isMobile) ...[
const _EventIndicator(),
],
],
);
return Stack(
children: <Widget>[
GestureDetector(
return MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
opaque: false,
hitTestBehavior: HitTestBehavior.translucent,
child: GestureDetector(
onDoubleTap: () => onCreateEvent(date),
child: Container(color: backgroundColor),
),
DragTarget<CalendarDayEvent>(
onTap: PlatformExtension.isMobile
? () => _mobileOnTap(context)
: null,
behavior: HitTestBehavior.deferToChild,
child: Container(
color: date.isWeekend
? AFThemeExtension.of(context).calendarWeekendBGColor
: Colors.transparent,
child: DragTarget<CalendarDayEvent>(
builder: (context, candidate, __) {
return Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
color:
candidate.isEmpty ? null : hoverBackgroundColor,
decoration: BoxDecoration(
color: candidate.isEmpty
? null
: hoverBackgroundColor,
border: _borderFromPosition(context, position),
),
padding: const EdgeInsets.only(top: 5.0),
child: child,
),
if (candidate.isEmpty)
if (candidate.isEmpty && !PlatformExtension.isMobile)
NewEventButton(onCreate: () => onCreateEvent(date)),
],
);
@ -101,18 +119,14 @@ class CalendarDayCard extends StatelessWidget {
if (event.date == date) {
return;
}
context
.read<CalendarBloc>()
.add(CalendarEvent.moveEvent(event, date));
},
),
MouseRegion(
onEnter: (p) => notifyEnter(context, true),
onExit: (p) => notifyEnter(context, false),
opaque: false,
hitTestBehavior: HitTestBehavior.translucent,
),
],
),
);
},
);
@ -120,42 +134,94 @@ class CalendarDayCard extends StatelessWidget {
);
}
notifyEnter(BuildContext context, bool isEnter) {
Provider.of<_CardEnterNotifier>(
context,
listen: false,
).onEnter = isEnter;
void _mobileOnTap(BuildContext context) {
context.push(
MobileCalendarEventsScreen.routeName,
extra: {
MobileCalendarEventsScreen.calendarBlocKey:
context.read<CalendarBloc>(),
MobileCalendarEventsScreen.calendarDateKey: date,
MobileCalendarEventsScreen.calendarEventsKey: events,
MobileCalendarEventsScreen.calendarRowCacheKey: rowCache,
MobileCalendarEventsScreen.calendarViewIdKey: viewId,
},
);
}
notifyEnter(BuildContext context, bool isEnter) =>
Provider.of<_CardEnterNotifier>(context, listen: false).onEnter = isEnter;
Border _borderFromPosition(BuildContext context, CellPosition position) {
final BorderSide borderSide =
BorderSide(color: Theme.of(context).dividerColor);
return Border(
top: borderSide,
left: borderSide,
bottom: [
CellPosition.bottom,
CellPosition.bottomLeft,
CellPosition.bottomRight,
].contains(position)
? borderSide
: BorderSide.none,
right: [
CellPosition.topRight,
CellPosition.bottomRight,
CellPosition.right,
].contains(position)
? borderSide
: BorderSide.none,
);
}
}
class _EventIndicator extends StatelessWidget {
const _EventIndicator();
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).hintColor,
),
),
],
);
}
}
class _Header extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
const _Header({
required this.isToday,
required this.isInMonth,
required this.date,
Key? key,
}) : super(key: key);
});
final bool isToday;
final bool isInMonth;
final DateTime date;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: _DayBadge(
isToday: isToday,
isInMonth: isInMonth,
date: date,
),
child: _DayBadge(isToday: isToday, isInMonth: isInMonth, date: date),
);
}
}
@visibleForTesting
class NewEventButton extends StatelessWidget {
const NewEventButton({super.key, required this.onCreate});
final VoidCallback onCreate;
const NewEventButton({required this.onCreate, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -164,6 +230,7 @@ class NewEventButton extends StatelessWidget {
if (!notifier.onEnter) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(4.0),
child: FlowyIconButton(
@ -210,15 +277,15 @@ class NewEventButton extends StatelessWidget {
}
class _DayBadge extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
const _DayBadge({
required this.isToday,
required this.isInMonth,
required this.date,
Key? key,
}) : super(key: key);
});
final bool isToday;
final bool isInMonth;
final DateTime date;
@override
Widget build(BuildContext context) {
@ -239,10 +306,12 @@ class _DayBadge extends StatelessWidget {
return SizedBox(
height: 18,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment: PlatformExtension.isMobile
? MainAxisAlignment.center
: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (date.day == 1)
if (date.day == 1 && !PlatformExtension.isMobile)
FlowyText.medium(
monthString,
fontSize: 11,
@ -255,7 +324,6 @@ class _DayBadge extends StatelessWidget {
),
width: isToday ? 18 : null,
height: isToday ? 18 : null,
// padding: GridSize.typeOptionContentInsets,
child: Center(
child: FlowyText.medium(
dayString,
@ -271,27 +339,25 @@ 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);
});
final List<CalendarDayEvent> events;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
@override
Widget build(BuildContext context) {
final editingEvent = context.watch<CalendarBloc>().state.editingEvent;
return Flexible(
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: true,
),
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: true),
child: ListView.separated(
itemBuilder: (BuildContext context, int index) {
final autoEdit =
@ -307,7 +373,7 @@ class _EventList extends StatelessWidget {
},
itemCount: events.length,
padding: const EdgeInsets.fromLTRB(4.0, 0, 4.0, 4.0),
separatorBuilder: (BuildContext context, int index) =>
separatorBuilder: (_, __) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
),

View File

@ -1,6 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.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/widgets/card/card.dart';
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
@ -8,6 +10,7 @@ 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/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
import 'package:appflowy/util/platform_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';
@ -16,27 +19,30 @@ 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 'package:go_router/go_router.dart';
import '../application/calendar_bloc.dart';
import 'calendar_event_editor.dart';
class EventCard extends StatefulWidget {
const EventCard({
super.key,
required this.fieldController,
required this.event,
required this.viewId,
required this.rowCache,
required this.constraints,
required this.autoEdit,
this.isDraggable = true,
});
final FieldController fieldController;
final CalendarDayEvent event;
final String viewId;
final RowCache rowCache;
final BoxConstraints constraints;
final bool autoEdit;
const EventCard({
super.key,
required this.event,
required this.viewId,
required this.rowCache,
required this.constraints,
required this.autoEdit,
required this.fieldController,
});
final bool isDraggable;
@override
State<EventCard> createState() => _EventCardState();
@ -75,7 +81,7 @@ class _EventCardState extends State<EventCard> {
);
final renderHook = _calendarEventCardRenderHook(context);
final card = RowCard<CalendarDayEvent>(
Widget card = RowCard<CalendarDayEvent>(
// Add the key here to make sure the card is rebuilt when the cells
// in this row are updated.
key: ValueKey(widget.event.eventId),
@ -85,7 +91,25 @@ class _EventCardState extends State<EventCard> {
cardData: widget.event,
isEditing: false,
cellBuilder: cellBuilder,
openCard: (_) => _popoverController.show(),
openCard: (context) {
if (PlatformExtension.isMobile) {
final dataController = RowController(
rowMeta: rowInfo.rowMeta,
viewId: widget.viewId,
rowCache: widget.rowCache,
);
context.push(
MobileCardDetailScreen.routeName,
extra: {
MobileCardDetailScreen.argRowController: dataController,
MobileCardDetailScreen.argFieldController: widget.fieldController,
},
);
} else {
_popoverController.show();
}
},
styleConfiguration: RowCardStyleConfiguration(
showAccessory: false,
cellPadding: EdgeInsets.zero,
@ -132,21 +156,7 @@ class _EventCardState extends State<EventCard> {
],
);
return Draggable<CalendarDayEvent>(
data: widget.event,
feedback: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.constraints.maxWidth - 8.0,
),
child: Opacity(
opacity: 0.6,
child: DecoratedBox(
decoration: decoration,
child: card,
),
),
),
child: AppFlowyPopover(
card = AppFlowyPopover(
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.rightWithCenterAligned,
controller: _popoverController,
@ -163,19 +173,39 @@ class _EventCardState extends State<EventCard> {
return const SizedBox.shrink();
}
return CalendarEventEditor(
fieldController: widget.fieldController,
rowCache: widget.rowCache,
rowMeta: widget.event.event.rowMeta,
viewId: widget.viewId,
layoutSettings: settings,
fieldController: widget.fieldController,
);
},
child: DecoratedBox(
decoration: decoration,
child: card,
),
),
);
if (widget.isDraggable) {
return Draggable<CalendarDayEvent>(
data: widget.event,
feedback: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.constraints.maxWidth - 8.0,
),
child: Opacity(
opacity: 0.6,
child: DecoratedBox(
decoration: decoration,
child: card,
),
),
),
child: card,
);
}
return card;
}
RowCardRenderHook<CalendarDayEvent> _calendarEventCardRenderHook(

View File

@ -65,14 +65,15 @@ class CalendarEventEditor extends StatelessWidget {
}
class EventEditorControls extends StatelessWidget {
final RowController rowController;
final FieldController fieldController;
const EventEditorControls({
super.key,
required this.rowController,
required this.fieldController,
});
final RowController rowController;
final FieldController fieldController;
@override
Widget build(BuildContext context) {
return Padding(

View File

@ -1,11 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_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/tab_bar/tab_bar_view.dart';
import 'package:appflowy/util/platform_extension.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';
@ -18,6 +20,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../application/row/row_cache.dart';
import '../../application/row/row_controller.dart';
@ -65,24 +68,25 @@ class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
}
class CalendarPage extends StatefulWidget {
final ViewPB view;
final DatabaseController databaseController;
final bool shrinkWrap;
const CalendarPage({
super.key,
required this.view,
required this.databaseController,
this.shrinkWrap = false,
super.key,
});
final ViewPB view;
final DatabaseController databaseController;
final bool shrinkWrap;
@override
State<CalendarPage> createState() => _CalendarPageState();
}
class _CalendarPageState extends State<CalendarPage> {
final _eventController = EventController<CalendarDayEvent>();
late final CalendarBloc _calendarBloc;
GlobalKey<MonthViewState>? _calendarState;
late CalendarBloc _calendarBloc;
@override
void initState() {
@ -181,18 +185,21 @@ class _CalendarPageState extends State<CalendarPage> {
int firstDayOfWeek,
) {
return Padding(
padding: CalendarSize.contentInsets,
padding: PlatformExtension.isMobile
? CalendarSize.contentInsetsMobile
: CalendarSize.contentInsets,
child: LayoutBuilder(
// must specify MonthView width for useAvailableVerticalSpace to work properly
builder: (context, constraints) => ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: MonthView(
key: _calendarState,
// TODO(Xazin): Border Color on Mobile
controller: _eventController,
width: constraints.maxWidth,
cellAspectRatio: 0.6,
cellAspectRatio: PlatformExtension.isMobile ? 1 : 0.6,
startDay: _weekdayFromInt(firstDayOfWeek),
borderColor: Theme.of(context).dividerColor,
showBorder: false,
headerBuilder: _headerNavigatorBuilder,
weekDayBuilder: _headerWeekDayBuilder,
cellBuilder: _calendarDayBuilder,
@ -208,11 +215,41 @@ class _CalendarPageState extends State<CalendarPage> {
height: 24,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
onTap: PlatformExtension.isMobile
? () => showFlowyMobileBottomSheet(
context,
title: LocaleKeys.calendar_quickJumpYear.tr(),
builder: (_) => SizedBox(
height: 200,
child: YearPicker(
firstDate: CalendarConstants.epochDate.withoutTime,
lastDate: CalendarConstants.maxDate.withoutTime,
selectedDate: currentMonth,
initialDate: currentMonth,
currentDate: DateTime.now(),
onChanged: (newDate) {
_calendarState?.currentState?.jumpToMonth(newDate);
context.pop();
},
),
),
)
: null,
child: Row(
children: [
FlowyText.medium(
DateFormat('MMMM y', context.locale.toLanguageTag())
.format(currentMonth),
),
if (PlatformExtension.isMobile) ...[
const HSpace(6),
const FlowySvg(FlowySvgs.arrow_down_s),
],
],
),
),
const Spacer(),
FlowyIconButton(
width: CalendarSize.navigatorButtonWidth,
@ -253,7 +290,12 @@ class _CalendarPageState extends State<CalendarPage> {
Widget _headerWeekDayBuilder(day) {
// incoming day starts from Monday, the symbols start from Sunday
final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
final weekDayString = symbols.WEEKDAYS[(day + 1) % 7];
String weekDayString = symbols.WEEKDAYS[(day + 1) % 7];
if (PlatformExtension.isMobile) {
weekDayString = weekDayString.substring(0, 3);
}
return Center(
child: Padding(
padding: CalendarSize.daysOfWeekInsets,
@ -271,14 +313,14 @@ class _CalendarPageState extends State<CalendarPage> {
List<CalendarEventData<CalendarDayEvent>> calenderEvents,
isToday,
isInMonth,
position,
) {
final events = calenderEvents.map((value) => value.event!).toList();
// Sort the events by timestamp. Because the database view is not
// reserving the order of the events. Reserving the order of the rows/events
// is implemnted in the develop branch(WIP). Will be replaced with that.
events.sort(
(a, b) => a.event.timestamp.compareTo(b.event.timestamp),
);
final events = calenderEvents.map((value) => value.event!).toList()
..sort((a, b) => a.event.timestamp.compareTo(b.event.timestamp));
return CalendarDayCard(
viewId: widget.view.id,
isToday: isToday,
@ -286,11 +328,9 @@ class _CalendarPageState extends State<CalendarPage> {
events: events,
date: date,
rowCache: _calendarBloc.rowCache,
onCreateEvent: (date) {
_calendarBloc.add(
CalendarEvent.createEvent(date),
);
},
onCreateEvent: (date) =>
_calendarBloc.add(CalendarEvent.createEvent(date)),
position: position,
);
}

View File

@ -13,6 +13,13 @@ class CalendarSize {
CalendarSize.headerContainerPadding,
);
static EdgeInsets get contentInsetsMobile => EdgeInsets.fromLTRB(
GridSize.leadingHeaderPadding / 2,
CalendarSize.headerContainerPadding / 2,
GridSize.leadingHeaderPadding / 2,
CalendarSize.headerContainerPadding / 2,
);
static double get scrollBarSize => 8 * scale;
static double get navigatorButtonWidth => 20 * scale;
static double get navigatorButtonHeight => 24 * scale;

View File

@ -7,14 +7,15 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileGridSettingButton extends StatelessWidget {
final DatabaseController controller;
final ToggleExtensionNotifier toggleExtension;
const MobileGridSettingButton({
super.key,
required this.controller,
required this.toggleExtension,
super.key,
});
final DatabaseController controller;
final ToggleExtensionNotifier toggleExtension;
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@ -49,10 +50,18 @@ class MobileGridSettingButton extends StatelessWidget {
if (isLoading) {
return const SizedBox.shrink();
}
return IconButton(
return SizedBox(
height: 24,
width: 24,
child: IconButton(
padding: EdgeInsets.zero,
// TODO(Xazin): Database Settings
onPressed: () {},
icon: const FlowySvg(
FlowySvgs.m_setting_m,
size: Size.square(24),
),
),
);
},

View File

@ -24,7 +24,7 @@ class MobileTabBarHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
children: [
Expanded(

View File

@ -4,11 +4,12 @@ import 'package:appflowy/plugins/database_view/tab_bar/mobile/mobile_tab_bar_hea
import 'package:appflowy/plugins/database_view/widgets/share_button.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -85,6 +86,7 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
],
child: Column(
children: [
if (PlatformExtension.isMobile) const VSpace(12),
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
return ValueListenableBuilder<bool>(
@ -96,11 +98,14 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
if (value) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: PlatformExtension.isDesktop
? const TabBarHeader()
: const MobileTabBarHeader(),
padding: EdgeInsets.symmetric(
horizontal: PlatformExtension.isMobile ? 20 : 40,
),
child: PlatformExtension.isMobile
? const MobileTabBarHeader()
: const TabBarHeader(),
);
},
);

View File

@ -95,11 +95,11 @@ class _DatabaseSettingListPopoverState
return ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
controller: ScrollController(),
itemCount: cells.length,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return cells[index];

View File

@ -3,6 +3,7 @@ import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_cr
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart';
import 'package:appflowy/mobile/presentation/database/card/row/cells/cells.dart';
import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart';
@ -77,6 +78,10 @@ GoRouter generateRouter(Widget child) {
_mobileCodeLanguagePickerPageRoute(),
_mobileLanguagePickerPageRoute(),
_mobileFontPickerPageRoute(),
// calendar related
_mobileCalendarEventsPageRoute(),
_mobileBlockSettingsPageRoute(),
],
@ -331,6 +336,25 @@ GoRoute _mobileFontPickerPageRoute() {
);
}
GoRoute _mobileCalendarEventsPageRoute() {
return GoRoute(
path: MobileCalendarEventsScreen.routeName,
pageBuilder: (context, state) {
final args = state.extra as Map<String, dynamic>;
return MaterialPage(
child: MobileCalendarEventsScreen(
calendarBloc: args[MobileCalendarEventsScreen.calendarBlocKey],
date: args[MobileCalendarEventsScreen.calendarDateKey],
events: args[MobileCalendarEventsScreen.calendarEventsKey],
rowCache: args[MobileCalendarEventsScreen.calendarRowCacheKey],
viewId: args[MobileCalendarEventsScreen.calendarViewIdKey],
),
);
},
);
}
GoRoute _desktopHomeScreenRoute() {
return GoRoute(
path: DesktopHomeScreen.routeName,

View File

@ -8,6 +8,7 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@ -53,7 +54,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
const Spacer(),
FlowyLogoTitle(
title: LocaleKeys.welcomeText.tr(),
logoSize: const Size.square(40),
logoSize: Size.square(PlatformExtension.isMobile ? 80 : 40),
),
const VSpace(32),
GoButton(
@ -113,14 +114,14 @@ class SkipLoginPageFooter extends StatelessWidget {
Widget build(BuildContext context) {
// The placeholderWidth should be greater than the longest width of the LanguageSelectorOnWelcomePage
const double placeholderWidth = 180;
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
HSpace(placeholderWidth),
Expanded(child: SubscribeButtons()),
SizedBox(
if (!PlatformExtension.isMobile) const HSpace(placeholderWidth),
const Expanded(child: SubscribeButtons()),
const SizedBox(
width: placeholderWidth,
height: 28,
child: Row(

View File

@ -5,14 +5,15 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class FlowyLogoTitle extends StatelessWidget {
final String title;
final Size logoSize;
const FlowyLogoTitle({
super.key,
required this.title,
this.logoSize = const Size.square(40),
});
final String title;
final Size logoSize;
@override
Widget build(BuildContext context) {
return SizedBox(

View File

@ -205,11 +205,12 @@ packages:
calendar_view:
dependency: "direct main"
description:
name: calendar_view
sha256: "58a8b851ac0a2d62770fd06ad30f06683bd40848a5dd1a1eca332f5a6064bd82"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
path: "."
ref: "6fe0c98"
resolved-ref: "6fe0c989289b077569858d5472f3f7ec05b7746f"
url: "https://github.com/Xazin/flutter_calendar_view"
source: git
version: "1.0.5"
characters:
dependency: transitive
description:

View File

@ -47,7 +47,7 @@ dependencies:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: '31acaff'
ref: "31acaff"
appflowy_popover:
path: packages/appflowy_popover
@ -94,7 +94,10 @@ dependencies:
shared_preferences: ^2.1.1
google_fonts: ^4.0.5
percent_indicator: ^4.2.3
calendar_view: ^1.0.3
calendar_view:
git:
url: https://github.com/Xazin/flutter_calendar_view
ref: "6fe0c98"
window_manager: ^0.3.4
http: ^1.0.0
path: ^1.8.3

View File

@ -854,7 +854,8 @@
"clickToAdd": "Click to add to the calendar",
"name": "Calendar layout"
},
"referencedCalendarPrefix": "View of"
"referencedCalendarPrefix": "View of",
"quickJumpYear": "Jump to"
},
"errorDialog": {
"title": "AppFlowy Error",