mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: reminder launch review (#3716)
This commit is contained in:
parent
3647af44c9
commit
966547faa0
@ -366,6 +366,9 @@ class EndTimeButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _maxLengthTwelveHour = 8;
|
||||||
|
const _maxLengthTwentyFourHour = 5;
|
||||||
|
|
||||||
class _TimeTextField extends StatefulWidget {
|
class _TimeTextField extends StatefulWidget {
|
||||||
final bool isEndTime;
|
final bool isEndTime;
|
||||||
final String? timeStr;
|
final String? timeStr;
|
||||||
@ -433,6 +436,11 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
|||||||
errorText: widget.isEndTime
|
errorText: widget.isEndTime
|
||||||
? state.parseEndTimeError
|
? state.parseEndTimeError
|
||||||
: state.parseTimeError,
|
: state.parseTimeError,
|
||||||
|
maxLength:
|
||||||
|
state.dateTypeOptionPB.timeFormat == TimeFormatPB.TwelveHour
|
||||||
|
? _maxLengthTwelveHour
|
||||||
|
: _maxLengthTwentyFourHour,
|
||||||
|
showCounter: false,
|
||||||
onSubmitted: (timeStr) {
|
onSubmitted: (timeStr) {
|
||||||
if (widget.isEndTime) {
|
if (widget.isEndTime) {
|
||||||
context
|
context
|
||||||
|
@ -83,10 +83,12 @@ class MentionDateBlock extends StatelessWidget {
|
|||||||
|
|
||||||
// We can remove time from the date/reminder
|
// We can remove time from the date/reminder
|
||||||
// block when toggled off.
|
// block when toggled off.
|
||||||
if (!includeTime && isReminder) {
|
if (isReminder) {
|
||||||
_updateScheduledAt(
|
_updateScheduledAt(
|
||||||
reminderId: reminderId!,
|
reminderId: reminderId!,
|
||||||
selectedDay: parsedDate!.withoutTime,
|
selectedDay:
|
||||||
|
includeTime ? parsedDate! : parsedDate!.withoutTime,
|
||||||
|
includeTime: includeTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -99,6 +101,7 @@ class MentionDateBlock extends StatelessWidget {
|
|||||||
_updateScheduledAt(
|
_updateScheduledAt(
|
||||||
reminderId: reminderId!,
|
reminderId: reminderId!,
|
||||||
selectedDay: selectedDay,
|
selectedDay: selectedDay,
|
||||||
|
includeTime: includeTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -171,10 +174,15 @@ class MentionDateBlock extends StatelessWidget {
|
|||||||
void _updateScheduledAt({
|
void _updateScheduledAt({
|
||||||
required String reminderId,
|
required String reminderId,
|
||||||
required DateTime selectedDay,
|
required DateTime selectedDay,
|
||||||
|
bool? includeTime,
|
||||||
}) {
|
}) {
|
||||||
editorContext.read<ReminderBloc>().add(
|
editorContext.read<ReminderBloc>().add(
|
||||||
ReminderEvent.update(
|
ReminderEvent.update(
|
||||||
ReminderUpdate(id: reminderId, scheduledAt: selectedDay),
|
ReminderUpdate(
|
||||||
|
id: reminderId,
|
||||||
|
scheduledAt: selectedDay,
|
||||||
|
includeTime: includeTime,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart';
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
|
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
|
||||||
|
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
@ -209,7 +210,9 @@ class ReminderReferenceService {
|
|||||||
objectId: viewId,
|
objectId: viewId,
|
||||||
title: LocaleKeys.reminderNotification_title.tr(),
|
title: LocaleKeys.reminderNotification_title.tr(),
|
||||||
message: LocaleKeys.reminderNotification_message.tr(),
|
message: LocaleKeys.reminderNotification_message.tr(),
|
||||||
meta: {"document_id": viewId},
|
meta: {
|
||||||
|
ReminderMetaKeys.includeTime.name: false.toString(),
|
||||||
|
},
|
||||||
scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
|
scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
|
||||||
isAck: date.isBefore(DateTime.now()),
|
isAck: date.isBefore(DateTime.now()),
|
||||||
);
|
);
|
||||||
|
@ -23,7 +23,7 @@ import 'package:appflowy/user/application/user_service.dart';
|
|||||||
import 'package:appflowy/user/presentation/router.dart';
|
import 'package:appflowy/user/presentation/router.dart';
|
||||||
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
|
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
|
@ -14,7 +14,7 @@ import 'package:flowy_infra/theme.dart';
|
|||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/workspace/application/local_notifications/notification_service.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_service.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||||
import 'package:appflowy/user/application/user_settings_service.dart';
|
import 'package:appflowy/user/application/user_settings_service.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'notification_filter_bloc.freezed.dart';
|
part 'notification_filter_bloc.freezed.dart';
|
||||||
@ -10,12 +9,6 @@ class NotificationFilterBloc
|
|||||||
on<NotificationFilterEvent>((event, emit) async {
|
on<NotificationFilterEvent>((event, emit) async {
|
||||||
event.when(
|
event.when(
|
||||||
reset: () => emit(const NotificationFilterState()),
|
reset: () => emit(const NotificationFilterState()),
|
||||||
changeSortBy: (NotificationSortOption sortBy) => emit(
|
|
||||||
state.copyWith(sortBy: sortBy),
|
|
||||||
),
|
|
||||||
toggleGroupByDate: () => emit(
|
|
||||||
state.copyWith(groupByDate: !state.groupByDate),
|
|
||||||
),
|
|
||||||
toggleShowUnreadsOnly: () => emit(
|
toggleShowUnreadsOnly: () => emit(
|
||||||
state.copyWith(showUnreadsOnly: !state.showUnreadsOnly),
|
state.copyWith(showUnreadsOnly: !state.showUnreadsOnly),
|
||||||
),
|
),
|
||||||
@ -24,42 +17,22 @@ class NotificationFilterBloc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NotificationSortOption {
|
|
||||||
descending,
|
|
||||||
ascending,
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class NotificationFilterEvent with _$NotificationFilterEvent {
|
class NotificationFilterEvent with _$NotificationFilterEvent {
|
||||||
const factory NotificationFilterEvent.toggleShowUnreadsOnly() =
|
const factory NotificationFilterEvent.toggleShowUnreadsOnly() =
|
||||||
_ToggleShowUnreadsOnly;
|
_ToggleShowUnreadsOnly;
|
||||||
|
|
||||||
const factory NotificationFilterEvent.toggleGroupByDate() =
|
|
||||||
_ToggleGroupByDate;
|
|
||||||
|
|
||||||
const factory NotificationFilterEvent.changeSortBy(
|
|
||||||
NotificationSortOption sortBy,
|
|
||||||
) = _ChangeSortBy;
|
|
||||||
|
|
||||||
const factory NotificationFilterEvent.reset() = _Reset;
|
const factory NotificationFilterEvent.reset() = _Reset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class NotificationFilterState extends Equatable with _$NotificationFilterState {
|
class NotificationFilterState with _$NotificationFilterState {
|
||||||
const NotificationFilterState._();
|
const NotificationFilterState._();
|
||||||
|
|
||||||
const factory NotificationFilterState({
|
const factory NotificationFilterState({
|
||||||
@Default(false) bool showUnreadsOnly,
|
@Default(false) bool showUnreadsOnly,
|
||||||
@Default(false) bool groupByDate,
|
|
||||||
@Default(NotificationSortOption.descending) NotificationSortOption sortBy,
|
|
||||||
}) = _NotificationFilterState;
|
}) = _NotificationFilterState;
|
||||||
|
|
||||||
// If state is not default values, then there are custom changes
|
// If state is not default values, then there are custom changes
|
||||||
bool get hasFilters =>
|
bool get hasFilters => showUnreadsOnly != false;
|
||||||
showUnreadsOnly != false ||
|
|
||||||
groupByDate != false ||
|
|
||||||
sortBy != NotificationSortOption.descending;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [showUnreadsOnly, groupByDate, sortBy];
|
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_service.dart';
|
import 'package:appflowy/user/application/reminder/reminder_service.dart';
|
||||||
import 'package:appflowy/workspace/application/local_notifications/notification_action.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
|
||||||
import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/local_notifications/notification_service.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_service.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
@ -35,6 +36,24 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
|
|||||||
|
|
||||||
on<ReminderEvent>((event, emit) async {
|
on<ReminderEvent>((event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
|
markAllRead: () async {
|
||||||
|
final unreadReminders =
|
||||||
|
state.pastReminders.where((reminder) => !reminder.isRead);
|
||||||
|
|
||||||
|
final reminders = [...state.reminders];
|
||||||
|
final updatedReminders = <ReminderPB>[];
|
||||||
|
for (final reminder in unreadReminders) {
|
||||||
|
reminders.remove(reminder);
|
||||||
|
|
||||||
|
reminder.isRead = true;
|
||||||
|
await reminderService.updateReminder(reminder: reminder);
|
||||||
|
|
||||||
|
updatedReminders.add(reminder);
|
||||||
|
}
|
||||||
|
|
||||||
|
reminders.addAll(updatedReminders);
|
||||||
|
emit(state.copyWith(reminders: reminders));
|
||||||
|
},
|
||||||
started: () async {
|
started: () async {
|
||||||
final remindersOrFailure = await reminderService.fetchReminders();
|
final remindersOrFailure = await reminderService.fetchReminders();
|
||||||
|
|
||||||
@ -169,6 +188,9 @@ class ReminderEvent with _$ReminderEvent {
|
|||||||
// Update a reminder (eg. isAck, isRead, etc.)
|
// Update a reminder (eg. isAck, isRead, etc.)
|
||||||
const factory ReminderEvent.update(ReminderUpdate update) = _Update;
|
const factory ReminderEvent.update(ReminderUpdate update) = _Update;
|
||||||
|
|
||||||
|
// Mark all unread reminders as read
|
||||||
|
const factory ReminderEvent.markAllRead() = _MarkAllRead;
|
||||||
|
|
||||||
const factory ReminderEvent.pressReminder({required String reminderId}) =
|
const factory ReminderEvent.pressReminder({required String reminderId}) =
|
||||||
_PressReminder;
|
_PressReminder;
|
||||||
}
|
}
|
||||||
@ -181,12 +203,14 @@ class ReminderUpdate {
|
|||||||
final bool? isAck;
|
final bool? isAck;
|
||||||
final bool? isRead;
|
final bool? isRead;
|
||||||
final DateTime? scheduledAt;
|
final DateTime? scheduledAt;
|
||||||
|
final bool? includeTime;
|
||||||
|
|
||||||
ReminderUpdate({
|
ReminderUpdate({
|
||||||
required this.id,
|
required this.id,
|
||||||
this.isAck,
|
this.isAck,
|
||||||
this.isRead,
|
this.isRead,
|
||||||
this.scheduledAt,
|
this.scheduledAt,
|
||||||
|
this.includeTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
ReminderPB merge({required ReminderPB a}) {
|
ReminderPB merge({required ReminderPB a}) {
|
||||||
@ -194,6 +218,11 @@ class ReminderUpdate {
|
|||||||
? scheduledAt!.isBefore(DateTime.now())
|
? scheduledAt!.isBefore(DateTime.now())
|
||||||
: a.isAck;
|
: a.isAck;
|
||||||
|
|
||||||
|
final meta = a.meta;
|
||||||
|
if (includeTime != a.includeTime) {
|
||||||
|
meta[ReminderMetaKeys.includeTime.name] = includeTime.toString();
|
||||||
|
}
|
||||||
|
|
||||||
return ReminderPB(
|
return ReminderPB(
|
||||||
id: a.id,
|
id: a.id,
|
||||||
objectId: a.objectId,
|
objectId: a.objectId,
|
||||||
@ -204,7 +233,7 @@ class ReminderUpdate {
|
|||||||
isRead: isRead ?? a.isRead,
|
isRead: isRead ?? a.isRead,
|
||||||
title: a.title,
|
title: a.title,
|
||||||
message: a.message,
|
message: a.message,
|
||||||
meta: a.meta,
|
meta: meta,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
|
|
||||||
|
enum ReminderMetaKeys {
|
||||||
|
includeTime("include_time");
|
||||||
|
|
||||||
|
const ReminderMetaKeys(this.name);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReminderExtension on ReminderPB {
|
||||||
|
bool? get includeTime {
|
||||||
|
final String? includeTimeStr = meta[ReminderMetaKeys.includeTime.name];
|
||||||
|
|
||||||
|
return includeTimeStr != null ? includeTimeStr == true.toString() : null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:appflowy/workspace/application/local_notifications/notification_action.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/local_notifications/notification_action.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
|
||||||
import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart';
|
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
|
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
|
||||||
|
@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|||||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/notification_button.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
@ -3,30 +3,24 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
|
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/notification_grouped_view.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_hub_title.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/notification_view.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:calendar_view/calendar_view.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.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/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
extension _ReminderSort on Iterable<ReminderPB> {
|
extension _ReminderSort on Iterable<ReminderPB> {
|
||||||
List<ReminderPB> sortByScheduledAt({
|
List<ReminderPB> sortByScheduledAt() =>
|
||||||
bool isDescending = true,
|
sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));
|
||||||
}) =>
|
|
||||||
sorted(
|
|
||||||
(a, b) => isDescending
|
|
||||||
? b.scheduledAt.compareTo(a.scheduledAt)
|
|
||||||
: a.scheduledAt.compareTo(b.scheduledAt),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationDialog extends StatefulWidget {
|
class NotificationDialog extends StatefulWidget {
|
||||||
@ -78,70 +72,25 @@ class _NotificationDialogState extends State<NotificationDialog>
|
|||||||
builder: (context, filterState) =>
|
builder: (context, filterState) =>
|
||||||
BlocBuilder<ReminderBloc, ReminderState>(
|
BlocBuilder<ReminderBloc, ReminderState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final sortDescending =
|
|
||||||
filterState.sortBy == NotificationSortOption.descending;
|
|
||||||
|
|
||||||
final List<ReminderPB> pastReminders = state.pastReminders
|
final List<ReminderPB> pastReminders = state.pastReminders
|
||||||
.where((r) => filterState.showUnreadsOnly ? !r.isRead : true)
|
.where((r) => filterState.showUnreadsOnly ? !r.isRead : true)
|
||||||
.sortByScheduledAt(isDescending: sortDescending);
|
.sortByScheduledAt();
|
||||||
|
|
||||||
final List<ReminderPB> upcomingReminders = state.upcomingReminders
|
final List<ReminderPB> upcomingReminders =
|
||||||
.sortByScheduledAt(isDescending: sortDescending);
|
state.upcomingReminders.sortByScheduledAt();
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
const NotificationHubTitle(),
|
||||||
children: [
|
NotificationTabBar(tabController: _controller),
|
||||||
DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
width: 215,
|
|
||||||
child: TabBar(
|
|
||||||
controller: _controller,
|
|
||||||
indicator: UnderlineTabIndicator(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
width: 1,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
tabs: [
|
|
||||||
Tab(
|
|
||||||
height: 26,
|
|
||||||
child: FlowyText.regular(
|
|
||||||
LocaleKeys.notificationHub_tabs_inbox.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
height: 26,
|
|
||||||
child: FlowyText.regular(
|
|
||||||
LocaleKeys.notificationHub_tabs_upcoming.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
NotificationViewFilters(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const VSpace(4),
|
|
||||||
// TODO(Xazin): Resolve issue with taking up
|
// TODO(Xazin): Resolve issue with taking up
|
||||||
// max amount of vertical space
|
// max amount of vertical space
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
children: [
|
children: [
|
||||||
if (!filterState.groupByDate) ...[
|
|
||||||
NotificationsView(
|
NotificationsView(
|
||||||
shownReminders: pastReminders,
|
shownReminders: pastReminders,
|
||||||
reminderBloc: _reminderBloc,
|
reminderBloc: _reminderBloc,
|
||||||
@ -149,6 +98,10 @@ class _NotificationDialogState extends State<NotificationDialog>
|
|||||||
onDelete: _onDelete,
|
onDelete: _onDelete,
|
||||||
onAction: _onAction,
|
onAction: _onAction,
|
||||||
onReadChanged: _onReadChanged,
|
onReadChanged: _onReadChanged,
|
||||||
|
actionBar: _InboxActionBar(
|
||||||
|
hasUnreads: state.hasUnreads,
|
||||||
|
showUnreadsOnly: filterState.showUnreadsOnly,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
NotificationsView(
|
NotificationsView(
|
||||||
shownReminders: upcomingReminders,
|
shownReminders: upcomingReminders,
|
||||||
@ -157,33 +110,6 @@ class _NotificationDialogState extends State<NotificationDialog>
|
|||||||
isUpcoming: true,
|
isUpcoming: true,
|
||||||
onAction: _onAction,
|
onAction: _onAction,
|
||||||
),
|
),
|
||||||
] else ...[
|
|
||||||
NotificationsGroupView(
|
|
||||||
groupedReminders: groupBy<ReminderPB, DateTime>(
|
|
||||||
pastReminders,
|
|
||||||
(r) => DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
r.scheduledAt.toInt() * 1000,
|
|
||||||
).withoutTime,
|
|
||||||
),
|
|
||||||
reminderBloc: _reminderBloc,
|
|
||||||
views: widget.views,
|
|
||||||
onAction: _onAction,
|
|
||||||
onDelete: _onDelete,
|
|
||||||
onReadChanged: _onReadChanged,
|
|
||||||
),
|
|
||||||
NotificationsGroupView(
|
|
||||||
groupedReminders: groupBy<ReminderPB, DateTime>(
|
|
||||||
upcomingReminders,
|
|
||||||
(r) => DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
r.scheduledAt.toInt() * 1000,
|
|
||||||
).withoutTime,
|
|
||||||
),
|
|
||||||
reminderBloc: _reminderBloc,
|
|
||||||
views: widget.views,
|
|
||||||
isUpcoming: true,
|
|
||||||
onAction: _onAction,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -222,223 +148,164 @@ class _NotificationDialogState extends State<NotificationDialog>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationViewFilters extends StatelessWidget {
|
class _InboxActionBar extends StatelessWidget {
|
||||||
NotificationViewFilters({super.key});
|
const _InboxActionBar({
|
||||||
final PopoverMutex _mutex = PopoverMutex();
|
required this.hasUnreads,
|
||||||
|
required this.showUnreadsOnly,
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider<NotificationFilterBloc>.value(
|
|
||||||
value: context.read<NotificationFilterBloc>(),
|
|
||||||
child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return AppFlowyPopover(
|
|
||||||
mutex: _mutex,
|
|
||||||
offset: const Offset(0, 5),
|
|
||||||
constraints: BoxConstraints.loose(const Size(225, 200)),
|
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
|
||||||
popupBuilder: (popoverContext) {
|
|
||||||
// TODO(Xazin): This is a workaround until we have resolved
|
|
||||||
// the issues with closing popovers on leave/outside-clicks
|
|
||||||
return MouseRegion(
|
|
||||||
onExit: (_) => _mutex.close(),
|
|
||||||
child: NotificationFilterPopover(
|
|
||||||
bloc: context.read<NotificationFilterBloc>(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: FlowyIconButton(
|
|
||||||
isSelected: state.hasFilters,
|
|
||||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
|
||||||
icon: const FlowySvg(FlowySvgs.filter_s),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NotificationFilterPopover extends StatelessWidget {
|
|
||||||
const NotificationFilterPopover({
|
|
||||||
super.key,
|
|
||||||
required this.bloc,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final NotificationFilterBloc bloc;
|
final bool hasUnreads;
|
||||||
|
final bool showUnreadsOnly;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return DecoratedBox(
|
||||||
mainAxisSize: MainAxisSize.min,
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_SortByOption(bloc: bloc),
|
_MarkAsReadButton(
|
||||||
_ShowUnreadsToggle(bloc: bloc),
|
onMarkAllRead: !hasUnreads
|
||||||
_GroupByDateToggle(bloc: bloc),
|
? null
|
||||||
BlocProvider<NotificationFilterBloc>.value(
|
: () => context
|
||||||
value: bloc,
|
.read<ReminderBloc>()
|
||||||
child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
|
.add(const ReminderEvent.markAllRead()),
|
||||||
builder: (context, state) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 115,
|
|
||||||
child: FlowyButton(
|
|
||||||
disable: !state.hasFilters,
|
|
||||||
onTap: state.hasFilters
|
|
||||||
? () =>
|
|
||||||
bloc.add(const NotificationFilterEvent.reset())
|
|
||||||
: null,
|
|
||||||
text: FlowyText(
|
|
||||||
LocaleKeys.notificationHub_filters_resetToDefault.tr(),
|
|
||||||
),
|
),
|
||||||
),
|
_ToggleUnreadsButton(
|
||||||
),
|
showUnreadsOnly: showUnreadsOnly,
|
||||||
],
|
onToggled: (showUnreadsOnly) => context
|
||||||
);
|
.read<NotificationFilterBloc>()
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ShowUnreadsToggle extends StatelessWidget {
|
|
||||||
const _ShowUnreadsToggle({required this.bloc});
|
|
||||||
|
|
||||||
final NotificationFilterBloc bloc;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocProvider<NotificationFilterBloc>.value(
|
|
||||||
value: bloc,
|
|
||||||
child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
const HSpace(4),
|
|
||||||
Expanded(
|
|
||||||
child: FlowyText(
|
|
||||||
LocaleKeys.notificationHub_filters_showUnreadsOnly.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Toggle(
|
|
||||||
style: ToggleStyle.big,
|
|
||||||
onChanged: (value) => bloc
|
|
||||||
.add(const NotificationFilterEvent.toggleShowUnreadsOnly()),
|
.add(const NotificationFilterEvent.toggleShowUnreadsOnly()),
|
||||||
value: state.showUnreadsOnly,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GroupByDateToggle extends StatelessWidget {
|
class _ToggleUnreadsButton extends StatefulWidget {
|
||||||
const _GroupByDateToggle({required this.bloc});
|
const _ToggleUnreadsButton({
|
||||||
|
required this.onToggled,
|
||||||
|
this.showUnreadsOnly = false,
|
||||||
|
});
|
||||||
|
|
||||||
final NotificationFilterBloc bloc;
|
final Function(bool) onToggled;
|
||||||
|
final bool showUnreadsOnly;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ToggleUnreadsButton> createState() => _ToggleUnreadsButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> {
|
||||||
|
late bool showUnreadsOnly = widget.showUnreadsOnly;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<NotificationFilterBloc>.value(
|
return SegmentedButton<bool>(
|
||||||
value: bloc,
|
onSelectionChanged: (Set<bool> newSelection) {
|
||||||
child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
|
setState(() => showUnreadsOnly = newSelection.first);
|
||||||
builder: (context, state) {
|
widget.onToggled(showUnreadsOnly);
|
||||||
return Row(
|
},
|
||||||
children: [
|
showSelectedIcon: false,
|
||||||
const HSpace(4),
|
style: ButtonStyle(
|
||||||
Expanded(
|
side: MaterialStatePropertyAll(
|
||||||
child: FlowyText(
|
BorderSide(color: Theme.of(context).dividerColor),
|
||||||
LocaleKeys.notificationHub_filters_groupByDate.tr(),
|
|
||||||
),
|
),
|
||||||
|
shape: const MaterialStatePropertyAll(
|
||||||
|
RoundedRectangleBorder(borderRadius: Corners.s6Border),
|
||||||
),
|
),
|
||||||
Toggle(
|
foregroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
style: ToggleStyle.big,
|
(state) {
|
||||||
onChanged: (value) =>
|
if (state.contains(MaterialState.hovered) ||
|
||||||
bloc.add(const NotificationFilterEvent.toggleGroupByDate()),
|
state.contains(MaterialState.selected) ||
|
||||||
value: state.groupByDate,
|
state.contains(MaterialState.pressed)) {
|
||||||
),
|
return Theme.of(context).colorScheme.onSurface;
|
||||||
],
|
}
|
||||||
);
|
|
||||||
|
return AFThemeExtension.of(context).textColor;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
|
(state) {
|
||||||
|
if (state.contains(MaterialState.hovered) ||
|
||||||
|
state.contains(MaterialState.selected) ||
|
||||||
|
state.contains(MaterialState.pressed)) {
|
||||||
|
return Theme.of(context).colorScheme.primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Theme.of(context).cardColor;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
segments: [
|
||||||
|
ButtonSegment<bool>(
|
||||||
|
value: false,
|
||||||
|
label: Text(
|
||||||
|
LocaleKeys.notificationHub_actions_showAll.tr(),
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ButtonSegment<bool>(
|
||||||
|
value: true,
|
||||||
|
label: Text(
|
||||||
|
LocaleKeys.notificationHub_actions_showUnreads.tr(),
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: <bool>{showUnreadsOnly},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SortByOption extends StatefulWidget {
|
class _MarkAsReadButton extends StatefulWidget {
|
||||||
const _SortByOption({required this.bloc});
|
final VoidCallback? onMarkAllRead;
|
||||||
|
|
||||||
final NotificationFilterBloc bloc;
|
const _MarkAsReadButton({this.onMarkAllRead});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_SortByOption> createState() => _SortByOptionState();
|
State<_MarkAsReadButton> createState() => _MarkAsReadButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SortByOptionState extends State<_SortByOption> {
|
class _MarkAsReadButtonState extends State<_MarkAsReadButton> {
|
||||||
bool _isHovering = false;
|
bool _isHovering = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<NotificationFilterBloc>.value(
|
return Opacity(
|
||||||
value: widget.bloc,
|
opacity: widget.onMarkAllRead != null ? 1 : 0.5,
|
||||||
child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
final isSortDescending =
|
|
||||||
state.sortBy == NotificationSortOption.descending;
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
const Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(left: 4.0),
|
|
||||||
child: FlowyText('Sort'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
SizedBox(
|
|
||||||
width: 115,
|
|
||||||
child: FlowyHover(
|
child: FlowyHover(
|
||||||
|
onHover: (isHovering) => setState(() => _isHovering = isHovering),
|
||||||
resetHoverOnRebuild: false,
|
resetHoverOnRebuild: false,
|
||||||
child: FlowyButton(
|
child: FlowyTextButton(
|
||||||
onHover: (isHovering) => isHovering != _isHovering
|
LocaleKeys.notificationHub_actions_markAllRead.tr(),
|
||||||
? setState(() => _isHovering = isHovering)
|
fontColor: widget.onMarkAllRead != null && _isHovering
|
||||||
|
? Theme.of(context).colorScheme.onSurface
|
||||||
|
: AFThemeExtension.of(context).textColor,
|
||||||
|
heading: FlowySvg(
|
||||||
|
FlowySvgs.checklist_s,
|
||||||
|
color: widget.onMarkAllRead != null && _isHovering
|
||||||
|
? Theme.of(context).colorScheme.onSurface
|
||||||
|
: AFThemeExtension.of(context).textColor,
|
||||||
|
),
|
||||||
|
hoverColor: widget.onMarkAllRead != null && _isHovering
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
: null,
|
: null,
|
||||||
onTap: () => widget.bloc.add(
|
onPressed: widget.onMarkAllRead,
|
||||||
NotificationFilterEvent.changeSortBy(
|
|
||||||
isSortDescending
|
|
||||||
? NotificationSortOption.ascending
|
|
||||||
: NotificationSortOption.descending,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
leftIcon: FlowySvg(
|
|
||||||
isSortDescending
|
|
||||||
? FlowySvgs.sort_descending_s
|
|
||||||
: FlowySvgs.sort_ascending_s,
|
|
||||||
color: _isHovering
|
|
||||||
? Theme.of(context).colorScheme.onSurface
|
|
||||||
: Theme.of(context).iconTheme.color,
|
|
||||||
),
|
|
||||||
text: FlowyText.regular(
|
|
||||||
isSortDescending
|
|
||||||
? LocaleKeys.notificationHub_filters_descending.tr()
|
|
||||||
: LocaleKeys.notificationHub_filters_ascending.tr(),
|
|
||||||
color: _isHovering
|
|
||||||
? Theme.of(context).colorScheme.onSurface
|
|
||||||
: Theme.of(context).textTheme.bodyMedium?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import 'package:appflowy/workspace/presentation/notifications/notification_item.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class NotificationGroup extends StatelessWidget {
|
|
||||||
const NotificationGroup({
|
|
||||||
super.key,
|
|
||||||
required this.reminders,
|
|
||||||
required this.formattedDate,
|
|
||||||
required this.isUpcoming,
|
|
||||||
required this.onReadChanged,
|
|
||||||
required this.onDelete,
|
|
||||||
required this.onAction,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<ReminderPB> reminders;
|
|
||||||
final String formattedDate;
|
|
||||||
final bool isUpcoming;
|
|
||||||
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
|
|
||||||
final Function(ReminderPB reminder)? onDelete;
|
|
||||||
final Function(ReminderPB reminder)? onAction;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8),
|
|
||||||
child: FlowyText(formattedDate),
|
|
||||||
),
|
|
||||||
const VSpace(4),
|
|
||||||
...reminders
|
|
||||||
.map(
|
|
||||||
(reminder) => NotificationItem(
|
|
||||||
reminderId: reminder.id,
|
|
||||||
key: ValueKey(reminder.id),
|
|
||||||
title: reminder.title,
|
|
||||||
scheduled: reminder.scheduledAt,
|
|
||||||
body: reminder.message,
|
|
||||||
isRead: reminder.isRead,
|
|
||||||
readOnly: isUpcoming,
|
|
||||||
onReadChanged: (isRead) =>
|
|
||||||
onReadChanged?.call(reminder, isRead),
|
|
||||||
onDelete: () => onDelete?.call(reminder),
|
|
||||||
onAction: () => onAction?.call(reminder),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
|
||||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/notifications/notification_group.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/notifications/notifications_hub_empty.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class NotificationsGroupView extends StatelessWidget {
|
|
||||||
const NotificationsGroupView({
|
|
||||||
super.key,
|
|
||||||
required this.groupedReminders,
|
|
||||||
required this.reminderBloc,
|
|
||||||
required this.views,
|
|
||||||
this.isUpcoming = false,
|
|
||||||
this.onAction,
|
|
||||||
this.onDelete,
|
|
||||||
this.onReadChanged,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Map<DateTime, List<ReminderPB>> groupedReminders;
|
|
||||||
final ReminderBloc reminderBloc;
|
|
||||||
final List<ViewPB> views;
|
|
||||||
final bool isUpcoming;
|
|
||||||
final Function(ReminderPB reminder)? onAction;
|
|
||||||
final Function(ReminderPB reminder)? onDelete;
|
|
||||||
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (groupedReminders.isEmpty) {
|
|
||||||
return const Center(child: NotificationsHubEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
final dateFormat = context.read<AppearanceSettingsCubit>().state.dateFormat;
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
...groupedReminders.values.mapIndexed(
|
|
||||||
(index, reminders) {
|
|
||||||
final formattedDate = dateFormat.formatDate(
|
|
||||||
groupedReminders.keys.elementAt(index),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return NotificationGroup(
|
|
||||||
reminders: reminders,
|
|
||||||
formattedDate: formattedDate,
|
|
||||||
isUpcoming: isUpcoming,
|
|
||||||
onReadChanged: onReadChanged,
|
|
||||||
onDelete: onDelete,
|
|
||||||
onAction: onAction,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class NotificationsHubEmpty extends StatelessWidget {
|
|
||||||
const NotificationsHubEmpty({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
child: Center(
|
|
||||||
child: FlowyText.regular(
|
|
||||||
LocaleKeys.notificationHub_empty.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -31,7 +31,9 @@ class NotificationButton extends StatelessWidget {
|
|||||||
child: AppFlowyPopover(
|
child: AppFlowyPopover(
|
||||||
mutex: mutex,
|
mutex: mutex,
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 350),
|
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 400),
|
||||||
|
windowPadding: EdgeInsets.zero,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
popupBuilder: (_) =>
|
popupBuilder: (_) =>
|
||||||
NotificationDialog(views: views, mutex: mutex),
|
NotificationDialog(views: views, mutex: mutex),
|
||||||
child: _buildNotificationIcon(context, state.hasUnreads),
|
child: _buildNotificationIcon(context, state.hasUnreads),
|
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NotificationHubTitle extends StatelessWidget {
|
||||||
|
const NotificationHubTitle({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16) +
|
||||||
|
const EdgeInsets.only(top: 12, bottom: 4),
|
||||||
|
child: FlowyText.semibold(
|
||||||
|
LocaleKeys.notificationHub_title.tr(),
|
||||||
|
color: Theme.of(context).colorScheme.tertiary,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ class NotificationItem extends StatefulWidget {
|
|||||||
required this.scheduled,
|
required this.scheduled,
|
||||||
required this.body,
|
required this.body,
|
||||||
required this.isRead,
|
required this.isRead,
|
||||||
|
this.includeTime = false,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
this.onAction,
|
this.onAction,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
@ -28,8 +29,9 @@ class NotificationItem extends StatefulWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final Int64 scheduled;
|
final Int64 scheduled;
|
||||||
final String body;
|
final String body;
|
||||||
final bool isRead;
|
final bool includeTime;
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
|
final bool isRead;
|
||||||
|
|
||||||
final VoidCallback? onAction;
|
final VoidCallback? onAction;
|
||||||
final VoidCallback? onDelete;
|
final VoidCallback? onDelete;
|
||||||
@ -57,52 +59,51 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
onTap: widget.onAction,
|
onTap: widget.onAction,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
|
||||||
child: Container(
|
child: DecoratedBox(
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
|
||||||
color: _isHovering && widget.onAction != null
|
color: _isHovering && widget.onAction != null
|
||||||
? AFThemeExtension.of(context).lightGreyHover
|
? AFThemeExtension.of(context).lightGreyHover
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
|
border: widget.isRead || widget.readOnly
|
||||||
|
? null
|
||||||
|
: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
width: 2,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10,
|
||||||
|
horizontal: 16,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Stack(
|
FlowySvg(
|
||||||
children: [
|
FlowySvgs.time_s,
|
||||||
const FlowySvg(FlowySvgs.time_s, size: Size.square(20)),
|
size: const Size.square(20),
|
||||||
if (!widget.isRead && !widget.readOnly)
|
color: Theme.of(context).colorScheme.tertiary,
|
||||||
Positioned(
|
|
||||||
bottom: 1,
|
|
||||||
right: 1,
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: AFThemeExtension.of(context).warning,
|
|
||||||
),
|
),
|
||||||
child: const SizedBox(height: 8, width: 8),
|
const HSpace(16),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const HSpace(10),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
FlowyText.semibold(
|
FlowyText.semibold(
|
||||||
widget.title,
|
widget.title,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).colorScheme.tertiary,
|
||||||
),
|
),
|
||||||
const HSpace(8),
|
// TODO(Xazin): Relative time + View Name
|
||||||
FlowyText.regular(
|
FlowyText.regular(
|
||||||
_scheduledString(widget.scheduled),
|
_scheduledString(
|
||||||
fontSize: 10,
|
widget.scheduled,
|
||||||
|
widget.includeTime,
|
||||||
),
|
),
|
||||||
],
|
fontSize: 10,
|
||||||
),
|
),
|
||||||
const VSpace(5),
|
const VSpace(5),
|
||||||
FlowyText.regular(widget.body, maxLines: 4),
|
FlowyText.regular(widget.body, maxLines: 4),
|
||||||
@ -114,6 +115,7 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (_isHovering && !widget.readOnly)
|
if (_isHovering && !widget.readOnly)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 4,
|
right: 4,
|
||||||
@ -129,14 +131,14 @@ class _NotificationItemState extends State<NotificationItem> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _scheduledString(Int64 secondsSinceEpoch) => context
|
String _scheduledString(Int64 secondsSinceEpoch, bool includeTime) {
|
||||||
.read<AppearanceSettingsCubit>()
|
final appearance = context.read<AppearanceSettingsCubit>().state;
|
||||||
.state
|
return appearance.dateFormat.formatDate(
|
||||||
.dateFormat
|
|
||||||
.formatDate(
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),
|
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),
|
||||||
true,
|
includeTime,
|
||||||
|
appearance.timeFormat,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);
|
void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);
|
||||||
}
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NotificationTabBar extends StatelessWidget {
|
||||||
|
final TabController tabController;
|
||||||
|
|
||||||
|
const NotificationTabBar({
|
||||||
|
super.key,
|
||||||
|
required this.tabController,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TabBar(
|
||||||
|
controller: tabController,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
labelPadding: EdgeInsets.zero,
|
||||||
|
indicatorSize: TabBarIndicatorSize.label,
|
||||||
|
indicator: UnderlineTabIndicator(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isScrollable: true,
|
||||||
|
tabs: [
|
||||||
|
_FlowyTab(
|
||||||
|
label: LocaleKeys.notificationHub_tabs_inbox.tr(),
|
||||||
|
isSelected: tabController.index == 0,
|
||||||
|
),
|
||||||
|
_FlowyTab(
|
||||||
|
label: LocaleKeys.notificationHub_tabs_upcoming.tr(),
|
||||||
|
isSelected: tabController.index == 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlowyTab extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool isSelected;
|
||||||
|
|
||||||
|
const _FlowyTab({
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Tab(
|
||||||
|
height: 26,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: FlowyText.regular(
|
||||||
|
label,
|
||||||
|
color: isSelected ? Theme.of(context).colorScheme.tertiary : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
|
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/notification_item.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/notifications_hub_empty.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -15,6 +16,7 @@ class NotificationsView extends StatelessWidget {
|
|||||||
this.onAction,
|
this.onAction,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
this.onReadChanged,
|
this.onReadChanged,
|
||||||
|
this.actionBar,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<ReminderPB> shownReminders;
|
final List<ReminderPB> shownReminders;
|
||||||
@ -24,19 +26,27 @@ class NotificationsView extends StatelessWidget {
|
|||||||
final Function(ReminderPB reminder)? onAction;
|
final Function(ReminderPB reminder)? onAction;
|
||||||
final Function(ReminderPB reminder)? onDelete;
|
final Function(ReminderPB reminder)? onDelete;
|
||||||
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
|
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
|
||||||
|
final Widget? actionBar;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (shownReminders.isEmpty) {
|
if (shownReminders.isEmpty) {
|
||||||
return const Center(child: NotificationsHubEmpty());
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
if (actionBar != null) actionBar!,
|
||||||
|
const Expanded(child: NotificationsHubEmpty()),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
if (actionBar != null) actionBar!,
|
||||||
...shownReminders.map(
|
...shownReminders.map(
|
||||||
(reminder) {
|
(ReminderPB reminder) {
|
||||||
return NotificationItem(
|
return NotificationItem(
|
||||||
reminderId: reminder.id,
|
reminderId: reminder.id,
|
||||||
key: ValueKey(reminder.id),
|
key: ValueKey(reminder.id),
|
||||||
@ -44,6 +54,7 @@ class NotificationsView extends StatelessWidget {
|
|||||||
scheduled: reminder.scheduledAt,
|
scheduled: reminder.scheduledAt,
|
||||||
body: reminder.message,
|
body: reminder.message,
|
||||||
isRead: reminder.isRead,
|
isRead: reminder.isRead,
|
||||||
|
includeTime: reminder.includeTime ?? false,
|
||||||
readOnly: isUpcoming,
|
readOnly: isUpcoming,
|
||||||
onReadChanged: (isRead) =>
|
onReadChanged: (isRead) =>
|
||||||
onReadChanged?.call(reminder, isRead),
|
onReadChanged?.call(reminder, isRead),
|
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NotificationsHubEmpty extends StatelessWidget {
|
||||||
|
const NotificationsHubEmpty({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
FlowyText(
|
||||||
|
LocaleKeys.notificationHub_emptyTitle.tr(),
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
const VSpace(8),
|
||||||
|
FlowyText.regular(
|
||||||
|
LocaleKeys.notificationHub_emptyBody.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -31,9 +31,7 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.brightness_4,
|
icon: Icons.brightness_4,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 10),
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.language,
|
page: SettingsPage.language,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -41,9 +39,7 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.translate,
|
icon: Icons.translate,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 10),
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.files,
|
page: SettingsPage.files,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -51,9 +47,7 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.file_present_outlined,
|
icon: Icons.file_present_outlined,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 10),
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.user,
|
page: SettingsPage.user,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -61,6 +55,7 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.account_box_outlined,
|
icon: Icons.account_box_outlined,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.notifications,
|
page: SettingsPage.notifications,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -68,12 +63,9 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.notifications_outlined,
|
icon: Icons.notifications_outlined,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
if (showSyncSetting)
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
// Only show supabase setting if supabase is enabled and the current auth type is not local
|
// Only show supabase setting if supabase is enabled and the current auth type is not local
|
||||||
if (showSyncSetting)
|
if (showSyncSetting) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.syncSetting,
|
page: SettingsPage.syncSetting,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -81,9 +73,8 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.sync,
|
icon: Icons.sync,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
],
|
||||||
height: 10,
|
const SizedBox(height: 10),
|
||||||
),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.shortcuts,
|
page: SettingsPage.shortcuts,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
|
@ -77,30 +77,34 @@ class DatePickerMenu extends DatePickerService {
|
|||||||
}) {
|
}) {
|
||||||
dismiss();
|
dismiss();
|
||||||
|
|
||||||
// Use MediaQuery, since Stack takes up all window space
|
final editorSize = editorState.renderBox!.size;
|
||||||
// and not just the space of the current Editor
|
|
||||||
final windowSize = MediaQuery.of(context).size;
|
|
||||||
|
|
||||||
double offsetX = offset.dx;
|
double offsetX = offset.dx;
|
||||||
double offsetY = offset.dy;
|
double offsetY = offset.dy;
|
||||||
|
|
||||||
final showRight = (offset.dx + _datePickerWidth) < windowSize.width;
|
final showRight = (offset.dx + _datePickerWidth) < editorSize.width;
|
||||||
if (!showRight) {
|
if (!showRight) {
|
||||||
offsetX = offset.dx - _datePickerWidth;
|
offsetX = offset.dx - _datePickerWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
final showBelow = (offset.dy + _datePickerHeight) < windowSize.height;
|
final showBelow = (offset.dy + _datePickerHeight) < editorSize.height;
|
||||||
if (!showBelow) {
|
if (!showBelow) {
|
||||||
|
if ((offset.dy - _datePickerHeight) < 0) {
|
||||||
|
// Show dialog in the middle
|
||||||
|
offsetY = offset.dy - (_datePickerHeight / 3);
|
||||||
|
} else {
|
||||||
|
// Show above
|
||||||
offsetY = offset.dy - _datePickerHeight;
|
offsetY = offset.dy - _datePickerHeight;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_menuEntry = OverlayEntry(
|
_menuEntry = OverlayEntry(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Material(
|
return Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: windowSize.height,
|
height: editorSize.height,
|
||||||
width: windowSize.width,
|
width: editorSize.width,
|
||||||
child: RawKeyboardListener(
|
child: RawKeyboardListener(
|
||||||
focusNode: FocusNode()..requestFocus(),
|
focusNode: FocusNode()..requestFocus(),
|
||||||
onKey: (event) {
|
onKey: (event) {
|
||||||
|
@ -8,9 +8,10 @@ import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class IncludeTimeButton extends StatefulWidget {
|
class IncludeTimeButton extends StatefulWidget {
|
||||||
@ -37,6 +38,7 @@ class IncludeTimeButton extends StatefulWidget {
|
|||||||
|
|
||||||
class _IncludeTimeButtonState extends State<IncludeTimeButton> {
|
class _IncludeTimeButtonState extends State<IncludeTimeButton> {
|
||||||
late bool _includeTime = widget.includeTime;
|
late bool _includeTime = widget.includeTime;
|
||||||
|
bool _showTimeTooltip = false;
|
||||||
String? _timeString;
|
String? _timeString;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -76,6 +78,35 @@ class _IncludeTimeButtonState extends State<IncludeTimeButton> {
|
|||||||
),
|
),
|
||||||
const HSpace(6),
|
const HSpace(6),
|
||||||
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
|
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
|
||||||
|
const HSpace(6),
|
||||||
|
FlowyTooltip(
|
||||||
|
message: LocaleKeys.datePicker_dateTimeFormatTooltip.tr(),
|
||||||
|
child: FlowyHover(
|
||||||
|
resetHoverOnRebuild: false,
|
||||||
|
style: HoverStyle(
|
||||||
|
foregroundColorOnHover:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
borderRadius: Corners.s10Border,
|
||||||
|
),
|
||||||
|
onHover: (isHovering) => setState(
|
||||||
|
() => _showTimeTooltip = isHovering,
|
||||||
|
),
|
||||||
|
child: FlowyTextButton(
|
||||||
|
'?',
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
fontColor: _showTimeTooltip
|
||||||
|
? Theme.of(context).colorScheme.onSurface
|
||||||
|
: null,
|
||||||
|
fillColor: _showTimeTooltip
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: null,
|
||||||
|
radius: Corners.s12Border,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Toggle(
|
Toggle(
|
||||||
value: _includeTime,
|
value: _includeTime,
|
||||||
@ -96,6 +127,9 @@ class _IncludeTimeButtonState extends State<IncludeTimeButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _maxLengthTwelveHour = 8;
|
||||||
|
const _maxLengthTwentyFourHour = 5;
|
||||||
|
|
||||||
class _TimeTextField extends StatefulWidget {
|
class _TimeTextField extends StatefulWidget {
|
||||||
const _TimeTextField({
|
const _TimeTextField({
|
||||||
required this.timeStr,
|
required this.timeStr,
|
||||||
@ -152,6 +186,10 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
|||||||
text: _timeString ?? "",
|
text: _timeString ?? "",
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
controller: _textController,
|
controller: _textController,
|
||||||
|
maxLength: widget.timeFormat == UserTimeFormatPB.TwelveHour
|
||||||
|
? _maxLengthTwelveHour
|
||||||
|
: _maxLengthTwentyFourHour,
|
||||||
|
showCounter: false,
|
||||||
submitOnLeave: true,
|
submitOnLeave: true,
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
errorText: errorText,
|
errorText: errorText,
|
||||||
|
@ -21,8 +21,10 @@ class FlowyTextField extends StatefulWidget {
|
|||||||
final Duration? debounceDuration;
|
final Duration? debounceDuration;
|
||||||
final String? errorText;
|
final String? errorText;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
|
final bool showCounter;
|
||||||
|
|
||||||
const FlowyTextField({
|
const FlowyTextField({
|
||||||
|
super.key,
|
||||||
this.hintText = "",
|
this.hintText = "",
|
||||||
this.text,
|
this.text,
|
||||||
this.textStyle,
|
this.textStyle,
|
||||||
@ -39,8 +41,8 @@ class FlowyTextField extends StatefulWidget {
|
|||||||
this.debounceDuration,
|
this.debounceDuration,
|
||||||
this.errorText,
|
this.errorText,
|
||||||
this.maxLines = 1,
|
this.maxLines = 1,
|
||||||
Key? key,
|
this.showCounter = true,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FlowyTextField> createState() => FlowyTextFieldState();
|
State<FlowyTextField> createState() => FlowyTextFieldState();
|
||||||
@ -133,7 +135,7 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
|||||||
.textTheme
|
.textTheme
|
||||||
.bodySmall!
|
.bodySmall!
|
||||||
.copyWith(color: Theme.of(context).hintColor),
|
.copyWith(color: Theme.of(context).hintColor),
|
||||||
suffixText: _suffixText(),
|
suffixText: widget.showCounter ? _suffixText() : "",
|
||||||
counterText: "",
|
counterText: "",
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
|
@ -35,7 +35,6 @@ void main() {
|
|||||||
AppTheme.fallback,
|
AppTheme.fallback,
|
||||||
),
|
),
|
||||||
verify: (bloc) {
|
verify: (bloc) {
|
||||||
// expect(bloc.state.appTheme.info.name, "light");
|
|
||||||
expect(bloc.state.font, 'Poppins');
|
expect(bloc.state.font, 'Poppins');
|
||||||
expect(bloc.state.monospaceFont, 'SF Mono');
|
expect(bloc.state.monospaceFont, 'SF Mono');
|
||||||
expect(bloc.state.themeMode, ThemeMode.system);
|
expect(bloc.state.themeMode, ThemeMode.system);
|
||||||
|
@ -814,6 +814,9 @@
|
|||||||
"shortKeyword": "remind"
|
"shortKeyword": "remind"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"datePicker": {
|
||||||
|
"dateTimeFormatTooltip": "Change the date and time format in settings"
|
||||||
|
},
|
||||||
"relativeDates": {
|
"relativeDates": {
|
||||||
"yesterday": "Yesterday",
|
"yesterday": "Yesterday",
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
@ -822,11 +825,17 @@
|
|||||||
},
|
},
|
||||||
"notificationHub": {
|
"notificationHub": {
|
||||||
"title": "Notifications",
|
"title": "Notifications",
|
||||||
"empty": "Nothing to see here!",
|
"emptyTitle": "All caught up!",
|
||||||
|
"emptyBody": "No pending notifications or actions. Enjoy the calm.",
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"inbox": "Inbox",
|
"inbox": "Inbox",
|
||||||
"upcoming": "Upcoming"
|
"upcoming": "Upcoming"
|
||||||
},
|
},
|
||||||
|
"actions": {
|
||||||
|
"markAllRead": "Mark all as read",
|
||||||
|
"showAll": "All",
|
||||||
|
"showUnreads": "Unread"
|
||||||
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"ascending": "Ascending",
|
"ascending": "Ascending",
|
||||||
"descending": "Descending",
|
"descending": "Descending",
|
||||||
|
Loading…
Reference in New Issue
Block a user