fix: editor page issues (#4055)

* fix: disable editor in card detail page

* fix: mobile toolbar disappears after editing link

* fix: favorite icon shows incorrectly

* fix: inkWell when pressing on the Trash is different from the rest of our list tiles in the app

* fix: recent view didn't update after deleting view

* fix: trash button too small

* feat: use new bottom sheet style in cover plugin

* feat: use new bottom sheet style in add new page

* feat: use new bottom sheet style in view page option

* feat: use new bottom sheet style in image block

* feat: use new bottom sheet style in settings block and edit link menu

* fix: data picker doesn't show

* fix: flutter analyze
This commit is contained in:
Lucas.Xu 2023-12-01 09:58:36 +08:00 committed by GitHub
parent afab3d5374
commit 0683483fd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 617 additions and 334 deletions

View File

@ -2,28 +2,21 @@ import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.
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/presentation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy/workspace/application/recent/recent_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileRouterRecord {
PropertyValueNotifier<String> lastPushedRouter =
PropertyValueNotifier<String>('');
}
extension MobileRouter on BuildContext {
Future<void> pushView(ViewPB view) async {
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
getIt<MobileRouterRecord>().lastPushedRouter.value = view.routeName;
push(
Uri(
path: view.routeName,
queryParameters: view.queryParameters,
).toString(),
);
).then((value) {
RecentService().updateRecentViews([view.id], true);
});
}
}

View File

@ -149,7 +149,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
return AppBarMoreButton(
onTap: (context) {
showMobileBottomSheet(
context: context,
context,
showDragHandle: true,
builder: (_) => _buildViewPageBottomSheet(context),
);
},

View File

@ -2,6 +2,7 @@ export 'bottom_sheet_action_widget.dart';
export 'bottom_sheet_add_new_page.dart';
export 'bottom_sheet_drag_handler.dart';
export 'bottom_sheet_rename_widget.dart';
export 'bottom_sheet_view_item.dart';
export 'bottom_sheet_view_item_body.dart';
export 'bottom_sheet_view_item_header.dart';
export 'bottom_sheet_view_page.dart';

View File

@ -52,50 +52,44 @@ class _MobileBottomSheetEditLinkWidgetState
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
vertical: 16.0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildTextField(textController, null),
const VSpace(12.0),
_buildTextField(hrefController, LocaleKeys.editor_linkTextHint.tr()),
const VSpace(12.0),
Row(
children: [
Expanded(
child: BottomSheetActionWidget(
text: LocaleKeys.button_cancel.tr(),
onTap: () => context.pop(),
),
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildTextField(textController, null),
const VSpace(12.0),
_buildTextField(hrefController, LocaleKeys.editor_linkTextHint.tr()),
const VSpace(12.0),
Row(
children: [
Expanded(
child: BottomSheetActionWidget(
text: LocaleKeys.button_cancel.tr(),
onTap: () => context.pop(),
),
),
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
text: LocaleKeys.button_done.tr(),
onTap: () {
widget.onEdit(textController.text, hrefController.text);
},
),
),
if (widget.href != null && isURL(widget.href)) ...[
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
text: LocaleKeys.button_done.tr(),
text: LocaleKeys.editor_openLink.tr(),
onTap: () {
widget.onEdit(textController.text, hrefController.text);
safeLaunchUrl(widget.href!);
},
),
),
if (widget.href != null && isURL(widget.href)) ...[
const HSpace(8),
Expanded(
child: BottomSheetActionWidget(
text: LocaleKeys.editor_openLink.tr(),
onTap: () {
safeLaunchUrl(widget.href!);
},
),
),
],
],
),
],
),
],
),
],
);
}

View File

@ -0,0 +1,118 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
enum MobileBottomSheetType {
view,
rename,
}
class MobileViewItemBottomSheet extends StatefulWidget {
const MobileViewItemBottomSheet({
super.key,
required this.view,
this.defaultType = MobileBottomSheetType.view,
});
final ViewPB view;
final MobileBottomSheetType defaultType;
@override
State<MobileViewItemBottomSheet> createState() =>
_MobileViewItemBottomSheetState();
}
class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
MobileBottomSheetType type = MobileBottomSheetType.view;
@override
initState() {
super.initState();
type = widget.defaultType;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// header
_buildHeader(),
const VSpace(16),
// body
_buildBody(),
],
);
}
Widget _buildHeader() {
switch (type) {
case MobileBottomSheetType.view:
case MobileBottomSheetType.rename:
// header
return MobileViewItemBottomSheetHeader(
showBackButton: type != MobileBottomSheetType.view,
view: widget.view,
onBack: () {
setState(() {
type = MobileBottomSheetType.view;
});
},
);
}
}
Widget _buildBody() {
switch (type) {
case MobileBottomSheetType.view:
return MobileViewItemBottomSheetBody(
isFavorite: widget.view.isFavorite,
onAction: (action) {
switch (action) {
case MobileViewItemBottomSheetBodyAction.rename:
setState(() {
type = MobileBottomSheetType.rename;
});
break;
case MobileViewItemBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
case MobileViewItemBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewItemBottomSheetBodyAction.delete:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewItemBottomSheetBodyAction.addToFavorites:
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
context.pop();
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(widget.view));
break;
}
},
);
case MobileBottomSheetType.rename:
return MobileBottomSheetRenameWidget(
name: widget.view.name,
onRename: (name) {
if (name != widget.view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
}
}

View File

@ -1,4 +1,5 @@
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@ -18,6 +19,7 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// back button,
showBackButton
@ -31,23 +33,22 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget {
),
),
)
: const SizedBox.shrink(),
: FlowyButton(
useIntrinsicWidth: true,
text: const Icon(
Icons.close,
),
margin: EdgeInsets.zero,
onTap: () {
context.pop();
},
),
// title
Expanded(
child: Text(
view.name,
style: theme.textTheme.labelSmall,
),
),
IconButton(
icon: Icon(
Icons.close,
color: theme.hintColor,
),
onPressed: () {
context.pop();
},
Text(
view.name,
style: theme.textTheme.labelSmall,
),
const HSpace(24.0),
],
);
}

View File

@ -117,33 +117,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// undo, redo
// Row(
// mainAxisSize: MainAxisSize.max,
// children: [
// Expanded(
// child: BottomSheetActionWidget(
// svg: FlowySvgs.m_undo_m,
// text: LocaleKeys.toolbar_undo.tr(),
// onTap: () => onAction(
// MobileViewBottomSheetBodyAction.undo,
// ),
// ),
// ),
// const HSpace(8),
// Expanded(
// child: BottomSheetActionWidget(
// svg: FlowySvgs.m_redo_m,
// text: LocaleKeys.toolbar_redo.tr(),
// onTap: () => onAction(
// MobileViewBottomSheetBodyAction.redo,
// ),
// ),
// ),
// ],
// ),
// const VSpace(8),
// rename, duplicate
Row(
mainAxisSize: MainAxisSize.max,

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@ -51,7 +51,8 @@ enum MobilePaneActionType {
final viewBloc = context.read<ViewBloc>();
final favoriteBloc = context.read<FavoriteBloc>();
showMobileBottomSheet(
context: context,
context,
showDragHandle: true,
builder: (context) {
return MultiBlocProvider(
providers: [

View File

@ -1,22 +1,26 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
Future<void> showMobileBottomSheet({
required BuildContext context,
Future<T?> showMobileBottomSheet<T>(
BuildContext context, {
required WidgetBuilder builder,
bool isDragEnabled = true,
ShapeBorder? shape,
bool isDragEnabled = true,
bool resizeToAvoidBottomInset = true,
EdgeInsets padding = const EdgeInsets.fromLTRB(16, 16, 16, 32),
bool showDragHandle = false,
bool showHeader = false,
bool showCloseButton = false,
String title = '', // only works if showHeader is true
}) async {
showModalBottomSheet(
assert(() {
if (showCloseButton || title.isNotEmpty) assert(showHeader);
return true;
}());
return showModalBottomSheet<T>(
context: context,
isScrollControlled: true,
enableDrag: isDragEnabled,
@ -29,128 +33,74 @@ Future<void> showMobileBottomSheet({
),
),
builder: (context) {
final child = builder(context);
if (resizeToAvoidBottomInset) {
return AnimatedPadding(
padding: padding +
EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
duration: Duration.zero,
child: child,
);
final List<Widget> children = [];
if (showDragHandle) {
children.addAll([
const VSpace(4),
const DragHandler(),
]);
}
return child;
if (showHeader) {
children.addAll([
const VSpace(4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
showCloseButton
? Padding(
padding: EdgeInsets.only(left: padding.left),
child: FlowyButton(
useIntrinsicWidth: true,
text: const Icon(
Icons.close,
size: 24,
),
margin: EdgeInsets.zero,
onTap: () => Navigator.of(context).pop(),
),
)
: const SizedBox.shrink(),
FlowyText(
title,
fontSize: 16.0,
),
showCloseButton
? HSpace(padding.right + 24)
: const SizedBox.shrink(),
],
),
const VSpace(4),
const Divider(),
]);
}
final child = builder(context);
if (resizeToAvoidBottomInset) {
children.add(
AnimatedPadding(
padding: padding +
EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
duration: Duration.zero,
child: child,
),
);
} else {
children.add(child);
}
if (children.length == 1) {
return children.first;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
},
);
}
enum MobileBottomSheetType {
view,
rename,
}
class MobileViewItemBottomSheet extends StatefulWidget {
const MobileViewItemBottomSheet({
super.key,
required this.view,
this.defaultType = MobileBottomSheetType.view,
});
final ViewPB view;
final MobileBottomSheetType defaultType;
@override
State<MobileViewItemBottomSheet> createState() =>
_MobileViewItemBottomSheetState();
}
class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
MobileBottomSheetType type = MobileBottomSheetType.view;
@override
initState() {
super.initState();
type = widget.defaultType;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// header
_buildHeader(),
const VSpace(16),
// body
_buildBody(),
],
);
}
Widget _buildHeader() {
switch (type) {
case MobileBottomSheetType.view:
case MobileBottomSheetType.rename:
// header
return MobileViewItemBottomSheetHeader(
showBackButton: type != MobileBottomSheetType.view,
view: widget.view,
onBack: () {
setState(() {
type = MobileBottomSheetType.view;
});
},
);
}
}
Widget _buildBody() {
switch (type) {
case MobileBottomSheetType.view:
return MobileViewItemBottomSheetBody(
isFavorite: widget.view.isFavorite,
onAction: (action) {
switch (action) {
case MobileViewItemBottomSheetBodyAction.rename:
setState(() {
type = MobileBottomSheetType.rename;
});
break;
case MobileViewItemBottomSheetBodyAction.duplicate:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
case MobileViewItemBottomSheetBodyAction.share:
// unimplemented
context.pop();
break;
case MobileViewItemBottomSheetBodyAction.delete:
context.pop();
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewItemBottomSheetBodyAction.addToFavorites:
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
context.pop();
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(widget.view));
break;
}
},
);
case MobileBottomSheetType.rename:
return MobileBottomSheetRenameWidget(
name: widget.view.name,
onRename: (name) {
if (name != widget.view.name) {
context.read<ViewBloc>().add(ViewEvent.rename(name));
}
context.pop();
},
);
}
}
}

View File

@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
@ -12,8 +13,6 @@ import 'package:appflowy/plugins/database_view/grid/application/row/row_action_s
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -181,12 +180,6 @@ class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
fieldController: widget.fieldController,
),
const Divider(),
const VSpace(16),
RowDocument(
viewId: widget.rowController.viewId,
rowId: widget.rowController.rowId,
scrollController: widget.scrollController ?? ScrollController(),
),
],
),
),

View File

@ -1,5 +1,4 @@
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart';
import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
@ -48,17 +47,9 @@ class MobileFolders extends StatelessWidget {
child: Builder(
builder: (context) {
final menuState = context.watch<MenuBloc>().state;
final favoriteState = context.watch<FavoriteBloc>().state;
return SlidableAutoCloseBehavior(
child: Column(
children: [
// TODO: Uncomment this when we have favorite folder in home page
if (showFavorite && favoriteState.views.isNotEmpty) ...[
MobileFavoriteFolder(
views: favoriteState.views,
),
const VSpace(18.0),
],
MobilePersonalFolder(
views: menuState.views,
),

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/mobile_folders.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart';
@ -10,11 +11,10 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'home.dart';
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({super.key});
@ -103,9 +103,9 @@ class MobileHomePage extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: MobileFolders(
showFavorite: false,
user: userProfile,
workspaceSetting: workspaceSetting,
showFavorite: false,
),
),
const SizedBox(height: 8),
@ -129,26 +129,19 @@ class _TrashButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO(yijing): improve style UI later
return SizedBox(
width: double.infinity,
child: TextButton.icon(
onPressed: () {
context.push(MobileHomeTrashPage.routeName);
},
icon: FlowySvg(
FlowySvgs.m_delete_m,
color: Theme.of(context).colorScheme.onSurface,
),
label: Text(
LocaleKeys.trash_text.tr(),
style: Theme.of(context).textTheme.labelMedium,
),
style: const ButtonStyle(
alignment: Alignment.centerLeft,
splashFactory: NoSplash.splashFactory,
),
return FlowyButton(
expand: true,
margin: const EdgeInsets.symmetric(vertical: 8),
leftIcon: FlowySvg(
FlowySvgs.m_delete_m,
color: Theme.of(context).colorScheme.onSurface,
),
leftIconSize: const Size.square(24),
text: FlowyText(
LocaleKeys.trash_text.tr(),
fontSize: 18.0,
),
onTap: () => context.push(MobileHomeTrashPage.routeName),
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/trash/application/prelude.dart';
import 'package:appflowy/startup/startup.dart';
@ -32,8 +32,12 @@ class MobileHomeTrashPage extends StatelessWidget {
icon: const Icon(Icons.more_horiz),
onPressed: () {
final trashBloc = context.read<TrashBloc>();
showFlowyMobileBottomSheet(
showMobileBottomSheet(
context,
showHeader: true,
showCloseButton: true,
showDragHandle: true,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
title: LocaleKeys.trash_mobile_actions.tr(),
builder: (_) => Row(
children: [

View File

@ -3,6 +3,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_
import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -56,13 +57,16 @@ class MobilePersonalFolder extends StatelessWidget {
onSelected: (view) async {
await context.pushView(view);
},
endActionPane: (context) => buildEndActionPane(context, [
MobilePaneActionType.delete,
view.isFavorite
? MobilePaneActionType.removeFromFavorites
: MobilePaneActionType.addToFavorites,
MobilePaneActionType.more,
]),
endActionPane: (context) {
final view = context.read<ViewBloc>().state.view;
return buildEndActionPane(context, [
MobilePaneActionType.delete,
view.isFavorite
? MobilePaneActionType.removeFromFavorites
: MobilePaneActionType.addToFavorites,
MobilePaneActionType.more,
]);
},
),
),
],

View File

@ -1,15 +1,12 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy/workspace/application/recent/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:dartz/dartz.dart' hide State;
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';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileRecentFolder extends StatefulWidget {
const MobileRecentFolder({super.key});
@ -21,39 +18,36 @@ class MobileRecentFolder extends StatefulWidget {
class _MobileRecentFolderState extends State<MobileRecentFolder> {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: getIt<MobileRouterRecord>().lastPushedRouter,
builder: (context, value, child) {
return FutureBuilder<Either<RepeatedViewPB, FlowyError>>(
future: FolderEventReadRecentViews().send(),
builder: (context, snapshot) {
final recentViews = snapshot.data
?.fold<List<ViewPB>>(
(l) => l.items,
(r) => [],
)
// only keep the first 10 items.
.reversed
.take(10)
.toList();
return BlocProvider(
create: (context) => RecentViewsBloc()
..add(
const RecentViewsEvent.initial(),
),
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
builder: (context, state) {
final recentViews = state
.views
// only keep the first 10 items.
.reversed
.take(10)
.toList();
if (recentViews == null || recentViews.isEmpty) {
return const SizedBox.shrink();
}
if (recentViews.isEmpty) {
return const SizedBox.shrink();
}
return Column(
children: [
_RecentViews(
key: ValueKey(recentViews),
// the recent views are in reverse order
recentViews: recentViews,
),
const VSpace(12.0),
],
);
},
);
},
return Column(
children: [
_RecentViews(
key: ValueKey(recentViews),
// the recent views are in reverse order
recentViews: recentViews,
),
const VSpace(12.0),
],
);
},
),
);
}
}

View File

@ -1,8 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item_add_button.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@ -397,9 +396,12 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
return MobileViewAddButton(
onPressed: () {
final title = widget.view.name;
showFlowyMobileBottomSheet(
showMobileBottomSheet(
context,
showHeader: true,
title: title,
showCloseButton: true,
showDragHandle: true,
builder: (_) {
return AddNewPageWidgetBottomSheet(
view: widget.view,

View File

@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/util/theme_mode_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
@ -31,9 +31,13 @@ class ThemeSetting extends StatelessWidget {
],
),
onTap: () {
showFlowyMobileBottomSheet(
showMobileBottomSheet(
context,
showHeader: true,
showCloseButton: true,
showDragHandle: true,
title: LocaleKeys.settings_appearance_themeMode_label.tr(),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 32),
builder: (_) {
return Column(
children: [

View File

@ -45,7 +45,7 @@ class PersonalInfoSettingGroup extends StatelessWidget {
trailing: const Icon(Icons.chevron_right),
onTap: () {
showMobileBottomSheet(
context: context,
context,
builder: (_) {
return EditUsernameBottomSheet(
context,

View File

@ -15,7 +15,7 @@ class SheetPage {
void showPaginatedBottomSheet(BuildContext context, {required SheetPage page}) {
showMobileBottomSheet(
context: context,
context,
// Workaround for not causing drag to rebuild
isDragEnabled: false,
builder: (context) => FlowyBottomSheet(root: page),

View File

@ -106,7 +106,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
text: child,
onTap: () {
showMobileBottomSheet(
context: context,
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileDateCellEditScreen(

View File

@ -207,7 +207,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
text: child,
onTap: () {
showMobileBottomSheet(
context: context,
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileSelectOptionEditor(

View File

@ -1,7 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -67,8 +67,11 @@ class MobileBlockActionButtons extends StatelessWidget {
}
void _showBottomSheet(BuildContext context) {
showFlowyMobileBottomSheet(
showMobileBottomSheet(
context,
showHeader: true,
showDragHandle: true,
showCloseButton: true,
title: LocaleKeys.document_plugins_action.tr(),
builder: (context) {
return BlockActionBottomSheet(

View File

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
@ -427,11 +427,13 @@ class DocumentCoverState extends State<DocumentCover> {
IntrinsicWidth(
child: RoundedTextButton(
onPressed: () {
showFlowyMobileBottomSheet(
showMobileBottomSheet(
context,
showHeader: true,
showDragHandle: true,
showCloseButton: true,
title:
LocaleKeys.document_plugins_cover_changeCover.tr(),
isScrollControlled: true,
builder: (context) {
return ConstrainedBox(
constraints: const BoxConstraints(

View File

@ -28,7 +28,7 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
onChanged: (value) => inputText = value,
onEditingComplete: () => widget.onSubmit(inputText),
),
const VSpace(5),
const VSpace(8),
SizedBox(
width: 160,
child: FlowyButton(

View File

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
@ -115,10 +115,12 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
if (PlatformExtension.isDesktopOrWeb) {
controller.show();
} else {
showFlowyMobileBottomSheet(
showMobileBottomSheet(
context,
title: LocaleKeys.editor_image.tr(),
isScrollControlled: true,
showHeader: true,
showCloseButton: true,
showDragHandle: true,
builder: (context) {
return ConstrainedBox(
constraints: const BoxConstraints(

View File

@ -2,7 +2,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/color/color_picker_screen.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
@ -43,8 +42,12 @@ Future<void> _showBlockActionSheet(
Node node,
Selection selection,
) async {
final result = await showFlowyMobileBottomSheet<bool>(
final result = await showMobileBottomSheet<bool>(
context,
showDragHandle: true,
showCloseButton: true,
showHeader: true,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
title: LocaleKeys.document_plugins_action.tr(),
builder: (context) {
return BlockActionBottomSheet(

View File

@ -79,6 +79,13 @@ class _TextDecorationMenuState extends State<_TextDecorationMenu> {
),
];
@override
void dispose() {
widget.editorState.selectionExtraInfo = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
final children = textDecorations

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -11,8 +11,11 @@ void showEditLinkBottomSheet(
void Function(BuildContext context, String text, String href) onEdit,
) {
assert(text.isNotEmpty);
showFlowyMobileBottomSheet(
showMobileBottomSheet(
context,
showCloseButton: true,
showDragHandle: true,
showHeader: true,
title: LocaleKeys.editor_editLink.tr(),
builder: (context) {
return MobileBottomSheetEditLinkWidget(

View File

@ -1,7 +1,6 @@
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/network_monitor.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
@ -164,7 +163,6 @@ void _resolveHomeDeps(GetIt getIt) {
getIt.registerSingleton(FToast());
getIt.registerSingleton(MenuSharedState());
getIt.registerSingleton(MobileRouterRecord());
getIt.registerFactoryParam<UserListener, UserProfilePB, void>(
(user, _) => UserListener(userProfile: user),

View File

@ -0,0 +1,2 @@
export 'recent_service.dart';
export 'recent_views_bloc.dart';

View File

@ -0,0 +1,61 @@
import 'dart:async';
import 'package:appflowy/core/notification/folder_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter/foundation.dart';
typedef RecentViewsUpdated = void Function(
Either<FlowyError, RepeatedViewIdPB> result,
);
class RecentViewsListener {
StreamSubscription<SubscribeObject>? _streamSubscription;
FolderNotificationParser? _parser;
RecentViewsUpdated? _recentViewsUpdated;
void start({
RecentViewsUpdated? recentViewsUpdated,
}) {
_recentViewsUpdated = recentViewsUpdated;
_parser = FolderNotificationParser(
id: 'recent_views',
callback: _observableCallback,
);
_streamSubscription = RustStreamReceiver.listen(
(observable) => _parser?.parse(observable),
);
}
void _observableCallback(
FolderNotification ty,
Either<Uint8List, FlowyError> result,
) {
if (_recentViewsUpdated == null) {
return;
}
result.fold(
(payload) {
final view = RepeatedViewIdPB.fromBuffer(payload);
_recentViewsUpdated?.call(
right(view),
);
},
(error) => _recentViewsUpdated?.call(
left(error),
),
);
}
Future<void> stop() async {
_parser = null;
await _streamSubscription?.cancel();
_recentViewsUpdated = null;
}
}

View File

@ -0,0 +1,19 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:dartz/dartz.dart';
class RecentService {
Future<Either<Unit, FlowyError>> updateRecentViews(
List<String> viewIds,
bool addInRecent,
) async {
return FolderEventUpdateRecentViews(
UpdateRecentViewPayloadPB(viewIds: viewIds, addInRecent: addInRecent),
).send();
}
Future<Either<RepeatedViewPB, FlowyError>> readRecentViews() {
return FolderEventReadRecentViews().send();
}
}

View File

@ -0,0 +1,78 @@
import 'package:appflowy/workspace/application/recent/recent_listener.dart';
import 'package:appflowy/workspace/application/recent/recent_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'recent_views_bloc.freezed.dart';
class RecentViewsBloc extends Bloc<RecentViewsEvent, RecentViewsState> {
final _service = RecentService();
final _listener = RecentViewsListener();
RecentViewsBloc() : super(RecentViewsState.initial()) {
on<RecentViewsEvent>(
(event, emit) async {
await event.map(
initial: (e) async {
_listener.start(
recentViewsUpdated: (result) => _onRecentViewsUpdated(
result,
),
);
add(const RecentViewsEvent.fetchRecentViews());
},
addRecentViews: (e) async {
await _service.updateRecentViews(e.viewIds, true);
},
removeRecentViews: (e) async {
await _service.updateRecentViews(e.viewIds, false);
},
fetchRecentViews: (e) async {
final result = await _service.readRecentViews();
result.fold(
(views) => emit(state.copyWith(views: views.items)),
(error) => Log.error(error),
);
},
);
},
);
}
@override
Future<void> close() async {
await _listener.stop();
return super.close();
}
void _onRecentViewsUpdated(
Either<FlowyError, RepeatedViewIdPB> result,
) {
add(const RecentViewsEvent.fetchRecentViews());
}
}
@freezed
class RecentViewsEvent with _$RecentViewsEvent {
const factory RecentViewsEvent.initial() = Initial;
const factory RecentViewsEvent.addRecentViews(List<String> viewIds) =
AddRecentViews;
const factory RecentViewsEvent.removeRecentViews(List<String> viewIds) =
RemoveRecentViews;
const factory RecentViewsEvent.fetchRecentViews() = FetchRecentViews;
}
@freezed
class RecentViewsState with _$RecentViewsState {
const factory RecentViewsState({
required List<ViewPB> views,
}) = _RecentViewsState;
factory RecentViewsState.initial() => const RecentViewsState(
views: [],
);
}

View File

@ -3,11 +3,14 @@ import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_listener.dart';
import 'package:appflowy/workspace/application/recent/recent_service.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:collection/collection.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -16,12 +19,14 @@ part 'view_bloc.freezed.dart';
class ViewBloc extends Bloc<ViewEvent, ViewState> {
final ViewBackendService viewBackendSvc;
final ViewListener listener;
final FavoriteListener favoriteListener;
final ViewPB view;
ViewBloc({
required this.view,
}) : viewBackendSvc = ViewBackendService(),
listener = ViewListener(viewId: view.id),
favoriteListener = FavoriteListener(),
super(ViewState.init(view)) {
on<ViewEvent>((event, emit) async {
await event.map(
@ -40,6 +45,17 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
);
},
);
favoriteListener.start(
favoritesUpdated: (result, isFavorite) {
result.fold((error) {}, (result) {
final current =
result.items.firstWhereOrNull((v) => v.id == view.id);
if (current != null) {
add(ViewEvent.viewDidUpdate(left(current)));
}
});
},
);
final isExpanded = await _getViewIsExpanded(view);
await _loadViewsWhenExpanded(emit, isExpanded);
},
@ -88,6 +104,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
(error) => state.copyWith(successOrFailure: right(error)),
),
);
RecentService().updateRecentViews([view.id], false);
},
duplicate: (e) async {
final result = await ViewBackendService.duplicate(view: view);
@ -139,6 +156,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
@override
Future<void> close() async {
await listener.stop();
await favoriteListener.stop();
return super.close();
}

View File

@ -28,6 +28,7 @@ class FlowyButton extends StatelessWidget {
final MainAxisAlignment mainAxisAlignment;
final bool showDefaultBoxDecorationOnMobile;
final double iconPadding;
final bool expand;
const FlowyButton({
Key? key,
@ -50,6 +51,7 @@ class FlowyButton extends StatelessWidget {
this.mainAxisAlignment = MainAxisAlignment.center,
this.showDefaultBoxDecorationOnMobile = false,
this.iconPadding = 6,
this.expand = false,
}) : super(key: key);
@override
@ -113,6 +115,7 @@ class FlowyButton extends StatelessWidget {
Widget child = Row(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min,
children: children,
);

View File

@ -431,6 +431,17 @@ impl TryInto<MoveNestedViewParams> for MoveNestedViewPayloadPB {
}
}
#[derive(Default, ProtoBuf)]
pub struct UpdateRecentViewPayloadPB {
#[pb(index = 1)]
pub view_ids: Vec<String>,
// If true, the view will be added to the recent view list.
// If false, the view will be removed from the recent view list.
#[pb(index = 2)]
pub add_in_recent: bool,
}
// impl<'de> Deserialize<'de> for ViewDataType {
// fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
// where

View File

@ -158,6 +158,20 @@ pub(crate) async fn toggle_favorites_handler(
Ok(())
}
pub(crate) async fn update_recent_views_handler(
data: AFPluginData<UpdateRecentViewPayloadPB>,
folder: AFPluginState<Weak<FolderManager>>,
) -> Result<(), FlowyError> {
let params: UpdateRecentViewPayloadPB = data.into_inner();
let folder = upgrade_folder(folder)?;
if params.add_in_recent {
let _ = folder.add_recent_views(params.view_ids).await;
} else {
let _ = folder.remove_recent_views(params.view_ids).await;
}
Ok(())
}
pub(crate) async fn set_latest_view_handler(
data: AFPluginData<ViewIdPB>,
folder: AFPluginState<Weak<FolderManager>>,

View File

@ -38,6 +38,7 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
.event(FolderEvent::ReadFavorites, read_favorites_handler)
.event(FolderEvent::ReadRecentViews, read_recent_views_handler)
.event(FolderEvent::ToggleFavorite, toggle_favorites_handler)
.event(FolderEvent::UpdateRecentViews, update_recent_views_handler)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@ -149,4 +150,8 @@ pub enum FolderEvent {
#[event(output = "RepeatedViewPB")]
ReadRecentViews = 36,
// used for add or remove recent views, like history
#[event(input = "UpdateRecentViewPayloadPB")]
UpdateRecentViews = 37,
}

View File

@ -25,8 +25,8 @@ use crate::entities::icon::UpdateViewIconParams;
use crate::entities::{
view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams,
CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, FolderSnapshotStatePB, FolderSyncStatePB,
RepeatedTrashPB, RepeatedViewPB, UpdateViewParams, UserFolderPB, ViewPB, WorkspacePB,
WorkspaceSettingPB,
RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, UserFolderPB, ViewPB,
WorkspacePB, WorkspaceSettingPB,
};
use crate::notification::{
send_notification, send_workspace_setting_notification, FolderNotification,
@ -782,6 +782,32 @@ impl FolderManager {
Ok(())
}
/// Add the view to the recent view list / history.
#[tracing::instrument(level = "debug", skip(self), err)]
pub async fn add_recent_views(&self, view_ids: Vec<String>) -> FlowyResult<()> {
self.with_folder(
|| (),
|folder| {
folder.add_recent_view_ids(view_ids);
},
);
self.send_update_recent_views_notification().await;
Ok(())
}
/// Add the view to the recent view list / history.
#[tracing::instrument(level = "debug", skip(self), err)]
pub async fn remove_recent_views(&self, view_ids: Vec<String>) -> FlowyResult<()> {
self.with_folder(
|| (),
|folder| {
folder.delete_recent_view_ids(view_ids);
},
);
self.send_update_recent_views_notification().await;
Ok(())
}
// Used by toggle_favorites to send notification to frontend, after the favorite status of view has been changed.It sends two distinct notifications: one to correctly update the concerned view's is_favorite status, and another to update the list of favorites that is to be displayed.
async fn send_toggle_favorite_notification(&self, view_id: &str) {
if let Ok(view) = self.get_view_pb(view_id).await {
@ -802,6 +828,15 @@ impl FolderManager {
}
}
async fn send_update_recent_views_notification(&self) {
let recent_views = self.get_all_recent_sections().await;
send_notification("recent_views", FolderNotification::DidUpdateRecentViews)
.payload(RepeatedViewIdPB {
items: recent_views.into_iter().map(|item| item.id).collect(),
})
.send();
}
#[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn get_all_favorites(&self) -> Vec<SectionItem> {
self.get_sections(Section::Favorite)

View File

@ -33,6 +33,8 @@ pub enum FolderNotification {
DidFavoriteView = 36,
DidUnfavoriteView = 37,
DidUpdateRecentViews = 38,
}
impl std::convert::From<FolderNotification> for i32 {