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

View File

@ -149,7 +149,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
return AppBarMoreButton( return AppBarMoreButton(
onTap: (context) { onTap: (context) {
showMobileBottomSheet( showMobileBottomSheet(
context: context, context,
showDragHandle: true,
builder: (_) => _buildViewPageBottomSheet(context), 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_add_new_page.dart';
export 'bottom_sheet_drag_handler.dart'; export 'bottom_sheet_drag_handler.dart';
export 'bottom_sheet_rename_widget.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_body.dart';
export 'bottom_sheet_view_item_header.dart'; export 'bottom_sheet_view_item_header.dart';
export 'bottom_sheet_view_page.dart'; export 'bottom_sheet_view_page.dart';

View File

@ -52,50 +52,44 @@ class _MobileBottomSheetEditLinkWidgetState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Column(
padding: const EdgeInsets.symmetric( mainAxisSize: MainAxisSize.min,
horizontal: 4.0, children: [
vertical: 16.0, _buildTextField(textController, null),
), const VSpace(12.0),
child: Column( _buildTextField(hrefController, LocaleKeys.editor_linkTextHint.tr()),
mainAxisSize: MainAxisSize.min, const VSpace(12.0),
children: [ Row(
_buildTextField(textController, null), children: [
const VSpace(12.0), Expanded(
_buildTextField(hrefController, LocaleKeys.editor_linkTextHint.tr()), child: BottomSheetActionWidget(
const VSpace(12.0), text: LocaleKeys.button_cancel.tr(),
Row( onTap: () => context.pop(),
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), const HSpace(8),
Expanded( Expanded(
child: BottomSheetActionWidget( child: BottomSheetActionWidget(
text: LocaleKeys.button_done.tr(), text: LocaleKeys.editor_openLink.tr(),
onTap: () { 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:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -18,6 +19,7 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// back button, // back button,
showBackButton 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 // title
Expanded( Text(
child: Text( view.name,
view.name, style: theme.textTheme.labelSmall,
style: theme.textTheme.labelSmall,
),
),
IconButton(
icon: Icon(
Icons.close,
color: theme.hintColor,
),
onPressed: () {
context.pop();
},
), ),
const HSpace(24.0),
], ],
); );
} }

View File

@ -117,33 +117,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ 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 // rename, duplicate
Row( Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,

View File

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

View File

@ -1,22 +1,26 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/plugins/base/drag_handler.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/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
Future<void> showMobileBottomSheet({ Future<T?> showMobileBottomSheet<T>(
required BuildContext context, BuildContext context, {
required WidgetBuilder builder, required WidgetBuilder builder,
bool isDragEnabled = true,
ShapeBorder? shape, ShapeBorder? shape,
bool isDragEnabled = true,
bool resizeToAvoidBottomInset = true, bool resizeToAvoidBottomInset = true,
EdgeInsets padding = const EdgeInsets.fromLTRB(16, 16, 16, 32), 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 { }) async {
showModalBottomSheet( assert(() {
if (showCloseButton || title.isNotEmpty) assert(showHeader);
return true;
}());
return showModalBottomSheet<T>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
enableDrag: isDragEnabled, enableDrag: isDragEnabled,
@ -29,128 +33,74 @@ Future<void> showMobileBottomSheet({
), ),
), ),
builder: (context) { builder: (context) {
final child = builder(context); final List<Widget> children = [];
if (resizeToAvoidBottomInset) {
return AnimatedPadding( if (showDragHandle) {
padding: padding + children.addAll([
EdgeInsets.only( const VSpace(4),
bottom: MediaQuery.of(context).viewInsets.bottom, const DragHandler(),
), ]);
duration: Duration.zero,
child: child,
);
} }
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/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.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/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/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/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.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/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/cell_builder.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -181,12 +180,6 @@ class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
fieldController: widget.fieldController, fieldController: widget.fieldController,
), ),
const Divider(), 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/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/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
@ -48,17 +47,9 @@ class MobileFolders extends StatelessWidget {
child: Builder( child: Builder(
builder: (context) { builder: (context) {
final menuState = context.watch<MenuBloc>().state; final menuState = context.watch<MenuBloc>().state;
final favoriteState = context.watch<FavoriteBloc>().state;
return SlidableAutoCloseBehavior( return SlidableAutoCloseBehavior(
child: Column( child: Column(
children: [ 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( MobilePersonalFolder(
views: menuState.views, views: menuState.views,
), ),

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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_folders.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.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'; 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-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'home.dart';
class MobileHomeScreen extends StatelessWidget { class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({super.key}); const MobileHomeScreen({super.key});
@ -103,9 +103,9 @@ class MobileHomePage extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: MobileFolders( child: MobileFolders(
showFavorite: false,
user: userProfile, user: userProfile,
workspaceSetting: workspaceSetting, workspaceSetting: workspaceSetting,
showFavorite: false,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -129,26 +129,19 @@ class _TrashButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// TODO(yijing): improve style UI later return FlowyButton(
return SizedBox( expand: true,
width: double.infinity, margin: const EdgeInsets.symmetric(vertical: 8),
child: TextButton.icon( leftIcon: FlowySvg(
onPressed: () { FlowySvgs.m_delete_m,
context.push(MobileHomeTrashPage.routeName); color: Theme.of(context).colorScheme.onSurface,
},
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,
),
), ),
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/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/trash/application/prelude.dart'; import 'package:appflowy/plugins/trash/application/prelude.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
@ -32,8 +32,12 @@ class MobileHomeTrashPage extends StatelessWidget {
icon: const Icon(Icons.more_horiz), icon: const Icon(Icons.more_horiz),
onPressed: () { onPressed: () {
final trashBloc = context.read<TrashBloc>(); final trashBloc = context.read<TrashBloc>();
showFlowyMobileBottomSheet( showMobileBottomSheet(
context, context,
showHeader: true,
showCloseButton: true,
showDragHandle: true,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
title: LocaleKeys.trash_mobile_actions.tr(), title: LocaleKeys.trash_mobile_actions.tr(),
builder: (_) => Row( builder: (_) => Row(
children: [ 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/home/personal_folder/mobile_home_personal_folder_header.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.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/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:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -56,13 +57,16 @@ class MobilePersonalFolder extends StatelessWidget {
onSelected: (view) async { onSelected: (view) async {
await context.pushView(view); await context.pushView(view);
}, },
endActionPane: (context) => buildEndActionPane(context, [ endActionPane: (context) {
MobilePaneActionType.delete, final view = context.read<ViewBloc>().state.view;
view.isFavorite return buildEndActionPane(context, [
? MobilePaneActionType.removeFromFavorites MobilePaneActionType.delete,
: MobilePaneActionType.addToFavorites, view.isFavorite
MobilePaneActionType.more, ? MobilePaneActionType.removeFromFavorites
]), : MobilePaneActionType.addToFavorites,
MobilePaneActionType.more,
]);
},
), ),
), ),
], ],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
@ -115,10 +115,12 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
if (PlatformExtension.isDesktopOrWeb) { if (PlatformExtension.isDesktopOrWeb) {
controller.show(); controller.show();
} else { } else {
showFlowyMobileBottomSheet( showMobileBottomSheet(
context, context,
title: LocaleKeys.editor_image.tr(), title: LocaleKeys.editor_image.tr(),
isScrollControlled: true, showHeader: true,
showCloseButton: true,
showDragHandle: true,
builder: (context) { builder: (context) {
return ConstrainedBox( return ConstrainedBox(
constraints: const BoxConstraints( 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/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.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/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/plugins/base/color/color_picker_screen.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -43,8 +42,12 @@ Future<void> _showBlockActionSheet(
Node node, Node node,
Selection selection, Selection selection,
) async { ) async {
final result = await showFlowyMobileBottomSheet<bool>( final result = await showMobileBottomSheet<bool>(
context, context,
showDragHandle: true,
showCloseButton: true,
showHeader: true,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
title: LocaleKeys.document_plugins_action.tr(), title: LocaleKeys.document_plugins_action.tr(),
builder: (context) { builder: (context) {
return BlockActionBottomSheet( return BlockActionBottomSheet(

View File

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

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.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_edit_link_widget.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:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -11,8 +11,11 @@ void showEditLinkBottomSheet(
void Function(BuildContext context, String text, String href) onEdit, void Function(BuildContext context, String text, String href) onEdit,
) { ) {
assert(text.isNotEmpty); assert(text.isNotEmpty);
showFlowyMobileBottomSheet( showMobileBottomSheet(
context, context,
showCloseButton: true,
showDragHandle: true,
showHeader: true,
title: LocaleKeys.editor_editLink.tr(), title: LocaleKeys.editor_editLink.tr(),
builder: (context) { builder: (context) {
return MobileBottomSheetEditLinkWidget( return MobileBottomSheetEditLinkWidget(

View File

@ -1,7 +1,6 @@
import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/core/network_monitor.dart';
import 'package:appflowy/env/cloud_env.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/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/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.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(FToast());
getIt.registerSingleton(MenuSharedState()); getIt.registerSingleton(MenuSharedState());
getIt.registerSingleton(MobileRouterRecord());
getIt.registerFactoryParam<UserListener, UserProfilePB, void>( getIt.registerFactoryParam<UserListener, UserProfilePB, void>(
(user, _) => UserListener(userProfile: user), (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.dart';
import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.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_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.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-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:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -16,12 +19,14 @@ part 'view_bloc.freezed.dart';
class ViewBloc extends Bloc<ViewEvent, ViewState> { class ViewBloc extends Bloc<ViewEvent, ViewState> {
final ViewBackendService viewBackendSvc; final ViewBackendService viewBackendSvc;
final ViewListener listener; final ViewListener listener;
final FavoriteListener favoriteListener;
final ViewPB view; final ViewPB view;
ViewBloc({ ViewBloc({
required this.view, required this.view,
}) : viewBackendSvc = ViewBackendService(), }) : viewBackendSvc = ViewBackendService(),
listener = ViewListener(viewId: view.id), listener = ViewListener(viewId: view.id),
favoriteListener = FavoriteListener(),
super(ViewState.init(view)) { super(ViewState.init(view)) {
on<ViewEvent>((event, emit) async { on<ViewEvent>((event, emit) async {
await event.map( 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); final isExpanded = await _getViewIsExpanded(view);
await _loadViewsWhenExpanded(emit, isExpanded); await _loadViewsWhenExpanded(emit, isExpanded);
}, },
@ -88,6 +104,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
(error) => state.copyWith(successOrFailure: right(error)), (error) => state.copyWith(successOrFailure: right(error)),
), ),
); );
RecentService().updateRecentViews([view.id], false);
}, },
duplicate: (e) async { duplicate: (e) async {
final result = await ViewBackendService.duplicate(view: view); final result = await ViewBackendService.duplicate(view: view);
@ -139,6 +156,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
@override @override
Future<void> close() async { Future<void> close() async {
await listener.stop(); await listener.stop();
await favoriteListener.stop();
return super.close(); return super.close();
} }

View File

@ -28,6 +28,7 @@ class FlowyButton extends StatelessWidget {
final MainAxisAlignment mainAxisAlignment; final MainAxisAlignment mainAxisAlignment;
final bool showDefaultBoxDecorationOnMobile; final bool showDefaultBoxDecorationOnMobile;
final double iconPadding; final double iconPadding;
final bool expand;
const FlowyButton({ const FlowyButton({
Key? key, Key? key,
@ -50,6 +51,7 @@ class FlowyButton extends StatelessWidget {
this.mainAxisAlignment = MainAxisAlignment.center, this.mainAxisAlignment = MainAxisAlignment.center,
this.showDefaultBoxDecorationOnMobile = false, this.showDefaultBoxDecorationOnMobile = false,
this.iconPadding = 6, this.iconPadding = 6,
this.expand = false,
}) : super(key: key); }) : super(key: key);
@override @override
@ -113,6 +115,7 @@ class FlowyButton extends StatelessWidget {
Widget child = Row( Widget child = Row(
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min,
children: children, 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 { // impl<'de> Deserialize<'de> for ViewDataType {
// fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error> // fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
// where // where

View File

@ -158,6 +158,20 @@ pub(crate) async fn toggle_favorites_handler(
Ok(()) 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( pub(crate) async fn set_latest_view_handler(
data: AFPluginData<ViewIdPB>, data: AFPluginData<ViewIdPB>,
folder: AFPluginState<Weak<FolderManager>>, 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::ReadFavorites, read_favorites_handler)
.event(FolderEvent::ReadRecentViews, read_recent_views_handler) .event(FolderEvent::ReadRecentViews, read_recent_views_handler)
.event(FolderEvent::ToggleFavorite, toggle_favorites_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)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@ -149,4 +150,8 @@ pub enum FolderEvent {
#[event(output = "RepeatedViewPB")] #[event(output = "RepeatedViewPB")]
ReadRecentViews = 36, 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::{ use crate::entities::{
view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams, view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams,
CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, FolderSnapshotStatePB, FolderSyncStatePB, CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, FolderSnapshotStatePB, FolderSyncStatePB,
RepeatedTrashPB, RepeatedViewPB, UpdateViewParams, UserFolderPB, ViewPB, WorkspacePB, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, UserFolderPB, ViewPB,
WorkspaceSettingPB, WorkspacePB, WorkspaceSettingPB,
}; };
use crate::notification::{ use crate::notification::{
send_notification, send_workspace_setting_notification, FolderNotification, send_notification, send_workspace_setting_notification, FolderNotification,
@ -782,6 +782,32 @@ impl FolderManager {
Ok(()) 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. // 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) { async fn send_toggle_favorite_notification(&self, view_id: &str) {
if let Ok(view) = self.get_view_pb(view_id).await { 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))] #[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn get_all_favorites(&self) -> Vec<SectionItem> { pub(crate) async fn get_all_favorites(&self) -> Vec<SectionItem> {
self.get_sections(Section::Favorite) self.get_sections(Section::Favorite)

View File

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