From 7cee8e392f53cbce4f3fc1d4e05454d2d4223c0c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 13 Nov 2023 10:07:46 +0800 Subject: [PATCH] feat: adjust cover plugin and support recent section on mobile platform (#3921) --- frontend/appflowy_flutter/ios/Podfile.lock | 12 + .../appflowy_flutter/ios/Runner/Info.plist | 132 +++++------ .../lib/mobile/application/mobile_router.dart | 10 + .../presentation/base/mobile_view_page.dart | 10 +- .../presentation/home/mobile_home_page.dart | 5 +- .../home/mobile_home_page_recent_files.dart | 122 ---------- .../mobile_home_recent_views.dart | 104 +++++++++ .../recent_folder/mobile_recent_view.dart | 208 ++++++++++++++++++ .../show_flowy_mobile_bottom_sheet.dart | 1 + .../editor_plugins/header/cover_editor.dart | 14 +- .../header/document_header_node_widget.dart | 90 +++++++- .../image/flowy_image_picker.dart | 41 ++++ .../image/image_picker_screen.dart | 15 ++ .../image/upload_image_menu.dart | 50 ++++- .../lib/startup/deps_resolver.dart | 2 + .../lib/startup/tasks/generate_router.dart | 14 ++ frontend/appflowy_flutter/pubspec.lock | 64 ++++++ frontend/appflowy_flutter/pubspec.yaml | 1 + frontend/resources/translations/en.json | 3 +- .../flowy-folder2/src/event_handler.rs | 16 ++ .../rust-lib/flowy-folder2/src/event_map.rs | 4 + .../rust-lib/flowy-folder2/src/manager.rs | 40 +++- 22 files changed, 734 insertions(+), 224 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 84858fb29a..cf59c986e6 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -48,6 +48,9 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) - image_gallery_saver (2.0.2): - Flutter - image_picker_ios (0.0.1): @@ -72,6 +75,9 @@ PODS: - FlutterMacOS - sign_in_with_apple (0.0.1): - Flutter + - sqflite (0.0.3): + - Flutter + - FMDB (>= 2.7.5) - super_native_extensions (0.0.1): - Flutter - SwiftyGif (5.4.3) @@ -99,6 +105,7 @@ DEPENDENCIES: - rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) @@ -107,6 +114,7 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery + - FMDB - ReachabilitySwift - SDWebImage - SwiftyGif @@ -147,6 +155,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sign_in_with_apple: :path: ".symlinks/plugins/sign_in_with_apple/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" super_native_extensions: :path: ".symlinks/plugins/super_native_extensions/ios" url_launcher_ios: @@ -165,6 +175,7 @@ SPEC CHECKSUMS: flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 integration_test: 13825b8a9334a850581300559b8839134b124670 @@ -176,6 +187,7 @@ SPEC CHECKSUMS: SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440 + sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 diff --git a/frontend/appflowy_flutter/ios/Runner/Info.plist b/frontend/appflowy_flutter/ios/Runner/Info.plist index 91ee44ca33..8c605b9d3a 100644 --- a/frontend/appflowy_flutter/ios/Runner/Info.plist +++ b/frontend/appflowy_flutter/ios/Runner/Info.plist @@ -1,68 +1,70 @@ - - NSCameraUsageDescription - AppFlowy requires access to the camera. - NSPhotoLibraryUsageDescription - AppFlowy requires access to the photo library. - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - - CFBundleName - AppFlowy - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLName - - CFBundleURLSchemes - - appflowy-flutter - - - - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - + + NSCameraUsageDescription + AppFlowy requires access to the camera. + NSPhotoLibraryUsageDescription + AppFlowy requires access to the photo library. + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + + FLTEnableImpeller + + CFBundleName + AppFlowy + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLName + + CFBundleURLSchemes + + appflowy-flutter + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart index c32463c7d8..2a8a4ef75f 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart @@ -2,12 +2,22 @@ import 'package:appflowy/mobile/presentation/database/mobile_board_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/presentation.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/dispatch/dispatch.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 lastPushedRouter = + PropertyValueNotifier(''); +} + extension MobileRouter on BuildContext { Future pushView(ViewPB view) async { + await FolderEventSetLatestView(ViewIdPB(value: view.id)).send(); + getIt().lastPushedRouter.value = view.routeName; push( Uri( path: view.routeName, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 99fef7559e..77a975744f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -117,15 +117,19 @@ class _MobileViewPageState extends State { appBar: AppBar( titleSpacing: 0, title: Row( + mainAxisSize: MainAxisSize.min, children: [ if (icon != null) FlowyText( '$icon ', fontSize: 22.0, ), - FlowyText.regular( - view?.name ?? widget.title ?? '', - fontSize: 14.0, + Expanded( + child: FlowyText.regular( + view?.name ?? widget.title ?? '', + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), ), ], ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index 87da5a6b3c..ec54775d62 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.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_recent_files.dart'; +import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; @@ -97,8 +97,7 @@ class MobileHomePage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ // Recent files - const MobileHomePageRecentFilesWidget(), - const Divider(), + const MobileRecentFolder(), // Folders Padding( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart deleted file mode 100644 index 010e8d30a9..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_recent_files.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -// TODO(yijing): replace by real data later -class MockRecentFile { - MockRecentFile({ - required this.title, - }); - final String title; - final String icon = '🐼'; - - final image = Image.asset( - 'assets/images/app_flowy_abstract_cover_1.jpg', - fit: BoxFit.cover, - ); -} - -final recentFilesList = [ - MockRecentFile(title: 'Work out plan'), - MockRecentFile(title: 'Travel plan'), - MockRecentFile(title: 'Meeting notes'), - MockRecentFile(title: 'Recipes'), - MockRecentFile(title: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), -]; - -class MobileHomePageRecentFilesWidget extends StatelessWidget { - const MobileHomePageRecentFilesWidget({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - // TODO: implement the details later. - return SizedBox( - height: 168, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: FlowyText.semibold( - 'Recent', - fontSize: 20.0, - ), - ), - Expanded( - child: ListView.separated( - separatorBuilder: (context, index) => const HSpace(8), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - scrollDirection: Axis.horizontal, - itemCount: recentFilesList.length, - itemBuilder: (context, index) { - return Container( - width: 120, - decoration: BoxDecoration( - color: theme.colorScheme.background, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: theme.colorScheme.outline.withOpacity(0.5), - ), - ), - child: Stack( - children: [ - Align( - alignment: Alignment.topCenter, - child: SizedBox( - height: 60, - width: double.infinity, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), - child: recentFilesList[index].image, - ), - ), - ), - Align( - alignment: Alignment.centerLeft, - child: Container( - height: 32, - width: 32, - margin: const EdgeInsets.only(left: 8), - child: Text( - recentFilesList[index].icon, - style: const TextStyle(fontSize: 32), - ), - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 32, - width: double.infinity, - margin: const EdgeInsets.only( - left: 8, - right: 8, - bottom: 8, - ), - child: Text( - recentFilesList[index].title, - softWrap: true, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onBackground, - ), - maxLines: 2, - ), - ), - ), - ], - ), - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart new file mode 100644 index 0000000000..32156f74f1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart @@ -0,0 +1,104 @@ +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_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'; + +class MobileRecentFolder extends StatefulWidget { + const MobileRecentFolder({super.key}); + + @override + State createState() => _MobileRecentFolderState(); +} + +class _MobileRecentFolderState extends State { + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: getIt().lastPushedRouter, + builder: (context, value, child) { + return FutureBuilder>( + future: FolderEventReadRecentViews().send(), + builder: (context, snapshot) { + final recentViews = snapshot.data + ?.fold>( + (l) => l.items, + (r) => [], + ) + // only keep the first 10 items. + .reversed + .take(10) + .toList(); + + if (recentViews == null || 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) + ], + ); + }, + ); + }, + ); + } +} + +class _RecentViews extends StatelessWidget { + const _RecentViews({ + super.key, + required this.recentViews, + }); + + final List recentViews; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 168, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: FlowyText.semibold( + LocaleKeys.sideBar_recent.tr(), + fontSize: 20.0, + ), + ), + Expanded( + child: ListView.separated( + separatorBuilder: (context, index) => const HSpace(8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + scrollDirection: Axis.horizontal, + itemCount: recentViews.length, + itemBuilder: (context, index) { + return MobileRecentView( + view: recentViews[index], + height: 120, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart new file mode 100644 index 0000000000..de67e5b7dc --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart @@ -0,0 +1,208 @@ +import 'dart:io'; + +import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/workspace/application/doc/doc_listener.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:string_validator/string_validator.dart'; + +class MobileRecentView extends StatefulWidget { + const MobileRecentView({ + super.key, + required this.view, + required this.height, + }); + + final ViewPB view; + final double height; + + @override + State createState() => _MobileRecentViewState(); +} + +class _MobileRecentViewState extends State { + late final ViewListener viewListener; + late ViewPB view; + late final DocumentListener documentListener; + + @override + void initState() { + super.initState(); + + view = widget.view; + + viewListener = ViewListener( + viewId: view.id, + )..start( + onViewUpdated: (view) { + setState(() { + this.view = view; + }); + }, + ); + + documentListener = DocumentListener(id: view.id) + ..start( + didReceiveUpdate: (document) { + setState(() { + view = view; + }); + }, + ); + } + + @override + void dispose() { + viewListener.stop(); + documentListener.stop(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final icon = view.icon.value; + final theme = Theme.of(context); + + return GestureDetector( + onTap: () => context.pushView(view), + child: Container( + height: widget.height, + width: widget.height, + decoration: BoxDecoration( + color: theme.colorScheme.background, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: theme.colorScheme.outline.withOpacity(0.5), + ), + ), + child: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + child: SizedBox( + height: widget.height / 2.0, + width: double.infinity, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + child: _buildCoverWidget(), + ), + ), + ), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: icon.isNotEmpty + ? FlowyText( + icon, + fontSize: 30.0, + ) + : SizedBox.square( + dimension: 32.0, + child: view.defaultIcon(), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: widget.height / 2.0, + width: double.infinity, + padding: const EdgeInsets.only( + left: 8.0, + top: 14.0, + right: 8.0, + ), + child: FlowyText( + view.name, + maxLines: 2, + fontSize: 16.0, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCoverWidget() { + return FutureBuilder( + future: _getPageNode(), + builder: ((context, snapshot) { + final node = snapshot.data; + final placeholder = Container( + color: Theme.of(context).colorScheme.onSecondaryContainer, + ); + if (node == null) { + return placeholder; + } + final type = CoverType.fromString( + node.attributes[DocumentHeaderBlockKeys.coverType], + ); + final cover = + node.attributes[DocumentHeaderBlockKeys.coverDetails] as String?; + if (cover == null) { + return placeholder; + } + switch (type) { + case CoverType.file: + if (isURL(cover)) { + return CachedNetworkImage( + imageUrl: cover, + fit: BoxFit.cover, + ); + } + final imageFile = File(cover); + if (!imageFile.existsSync()) { + return placeholder; + } + return Image.file( + imageFile, + ); + case CoverType.asset: + return Image.asset( + cover, + fit: BoxFit.cover, + ); + case CoverType.color: + final color = cover.tryToColor() ?? Colors.white; + return Container( + color: color, + ); + case CoverType.none: + return placeholder; + } + }), + ); + } + + Future _getPageNode() async { + final data = await DocumentEventGetDocumentData( + OpenDocumentPayloadPB(documentId: view.id), + ).send(); + final document = data.fold((l) => l.toDocument(), (r) => null); + if (document != null) { + return document.root; + } + return null; + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart index 58d13ce921..b71a664eb5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart @@ -8,6 +8,7 @@ Future showFlowyMobileBottomSheet( }) async { return showModalBottomSheet( context: context, + isScrollControlled: true, builder: (context) => Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart index 5e17a753fd..919451adac 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart @@ -529,14 +529,12 @@ class ColorItem extends StatelessWidget { @override Widget build(BuildContext context) { - return InkWell( - customBorder: const RoundedRectangleBorder( - borderRadius: Corners.s6Border, - ), - hoverColor: hoverColor, - onTap: () => onTap(option.colorHex), - child: Padding( - padding: const EdgeInsets.only(right: 10.0), + return Padding( + padding: const EdgeInsets.only(right: 10.0), + child: InkWell( + customBorder: const CircleBorder(), + hoverColor: hoverColor, + onTap: () => onTap(option.colorHex), child: SizedBox.square( dimension: 25, child: DecoratedBox( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 59355d25c1..2905a32715 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -2,19 +2,23 @@ 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/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'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:string_validator/string_validator.dart'; import 'cover_editor.dart'; @@ -262,7 +266,9 @@ class _DocumentHeaderToolbarState extends State { FlowyButton( leftIconSize: const Size.square(18), onTap: () => widget.onCoverChanged( - cover: (CoverType.asset, builtInAssetImages.first), + cover: PlatformExtension.isDesktopOrWeb + ? (CoverType.asset, builtInAssetImages.first) + : (CoverType.color, '0xffe8e0ff'), ), useIntrinsicWidth: true, leftIcon: const FlowySvg(FlowySvgs.image_s), @@ -373,6 +379,12 @@ class DocumentCoverState extends State { @override Widget build(BuildContext context) { + return PlatformExtension.isDesktopOrWeb + ? _buildDesktopCover() + : _buildMobileCover(); + } + + Widget _buildDesktopCover() { return SizedBox( height: kCoverHeight, child: MouseRegion( @@ -393,10 +405,82 @@ class DocumentCoverState extends State { ); } + Widget _buildMobileCover() { + return SizedBox( + height: kCoverHeight, + child: Stack( + children: [ + SizedBox( + height: double.infinity, + width: double.infinity, + child: _buildCoverImage(), + ), + Positioned( + bottom: 8, + right: 12, + child: RoundedTextButton( + onPressed: () { + showFlowyMobileBottomSheet( + context, + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + builder: (context) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 340, + minHeight: 80, + ), + child: UploadImageMenu( + supportTypes: const [ + UploadImageType.color, + UploadImageType.local, + UploadImageType.url, + UploadImageType.unsplash, + ], + onSelectedLocalImage: (path) async { + context.pop(); + widget.onCoverChanged(CoverType.file, path); + }, + onSelectedAIImage: (_) { + throw UnimplementedError(); + }, + onSelectedNetworkImage: (url) async { + context.pop(); + widget.onCoverChanged(CoverType.file, url); + }, + onSelectedColor: (color) { + context.pop(); + widget.onCoverChanged(CoverType.color, color); + }, + ), + ); + }, + ); + }, + fillColor: Theme.of(context).colorScheme.onSurfaceVariant, + width: 120, + height: 32, + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + ), + ), + ], + ), + ); + } + Widget _buildCoverImage() { + final detail = widget.coverDetails; + if (detail == null) { + return const SizedBox.shrink(); + } switch (widget.coverType) { case CoverType.file: - final imageFile = File(widget.coverDetails ?? ""); + if (isURL(detail)) { + return CachedNetworkImage( + imageUrl: detail, + fit: BoxFit.cover, + ); + } + final imageFile = File(detail); if (!imageFile.existsSync()) { WidgetsBinding.instance.addPostFrameCallback((_) { widget.onCoverChanged(CoverType.none, null); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart new file mode 100644 index 0000000000..a1f4487562 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart @@ -0,0 +1,41 @@ +import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class ImagePickerPage extends StatefulWidget { + const ImagePickerPage({ + super.key, + // required this.onSelected, + }); + + // final void Function(EmojiPickerResult) onSelected; + + @override + State createState() => _ImagePickerPageState(); +} + +class _ImagePickerPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: const FlowyText.semibold( + 'Page icon', + fontSize: 14.0, + ), + leading: AppBarBackButton( + onTap: () => context.pop(), + ), + ), + body: SafeArea( + child: UploadImageMenu( + onSubmitted: (_) {}, + onUpload: (_) {}, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart new file mode 100644 index 0000000000..0aa10412bf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart @@ -0,0 +1,15 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart'; +import 'package:flutter/material.dart'; + +class MobileImagePickerScreen extends StatelessWidget { + static const routeName = '/image_picker'; + + const MobileImagePickerScreen({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const ImagePickerPage(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart index e7a9bfc7de..961e2deab6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart @@ -1,12 +1,15 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/util/platform_extension.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; @@ -16,7 +19,8 @@ enum UploadImageType { url, unsplash, stabilityAI, - openAI; + openAI, + color; String get description { switch (this) { @@ -30,6 +34,8 @@ enum UploadImageType { return LocaleKeys.document_imageBlock_ai_label.tr(); case UploadImageType.stabilityAI: return LocaleKeys.document_imageBlock_stability_ai_label.tr(); + case UploadImageType.color: + return LocaleKeys.document_plugins_cover_colors.tr(); } } } @@ -40,12 +46,14 @@ class UploadImageMenu extends StatefulWidget { required this.onSelectedLocalImage, required this.onSelectedAIImage, required this.onSelectedNetworkImage, + this.onSelectedColor, this.supportTypes = UploadImageType.values, }); final void Function(String? path) onSelectedLocalImage; final void Function(String url) onSelectedAIImage; final void Function(String url) onSelectedNetworkImage; + final void Function(String color)? onSelectedColor; final List supportTypes; @override @@ -128,18 +136,23 @@ class _UploadImageMenuState extends State { } Widget _buildTab() { - final type = UploadImageType.values[currentTabIndex]; + final constraints = + PlatformExtension.isMobile ? const BoxConstraints(minHeight: 92) : null; + final type = values[currentTabIndex]; switch (type) { case UploadImageType.local: - return Padding( + return Container( padding: const EdgeInsets.all(8.0), + alignment: Alignment.center, + constraints: constraints, child: UploadImageFileWidget( onPickFile: widget.onSelectedLocalImage, ), ); case UploadImageType.url: - return Padding( + return Container( padding: const EdgeInsets.all(8.0), + constraints: constraints, child: EmbedImageUrlWidget( onSubmit: widget.onSelectedNetworkImage, ), @@ -156,8 +169,9 @@ class _UploadImageMenuState extends State { case UploadImageType.openAI: return supportOpenAI ? Expanded( - child: Padding( + child: Container( padding: const EdgeInsets.all(8.0), + constraints: constraints, child: OpenAIImageWidget( onSelectNetworkImage: widget.onSelectedAIImage, ), @@ -172,7 +186,7 @@ class _UploadImageMenuState extends State { case UploadImageType.stabilityAI: return supportStabilityAI ? Expanded( - child: Padding( + child: Container( padding: const EdgeInsets.all(8.0), child: StabilityAIImageWidget( onSelectImage: widget.onSelectedLocalImage, @@ -186,6 +200,28 @@ class _UploadImageMenuState extends State { .tr(), ), ); + case UploadImageType.color: + final theme = Theme.of(context); + return Container( + constraints: constraints, + padding: const EdgeInsets.all(8.0), + alignment: Alignment.center, + child: CoverColorPicker( + pickerBackgroundColor: theme.cardColor, + pickerItemHoverColor: theme.hoverColor, + backgroundColorOptions: FlowyTint.values + .map( + (t) => ColorOption( + colorHex: t.color(context).toHex(), + name: t.tintName(AppFlowyEditorL10n.current), + ), + ) + .toList(), + onSubmittedBackgroundColorHex: (color) { + widget.onSelectedColor?.call(color); + }, + ), + ); } } } diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index a8d8bd8faa..acc04b3f0d 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -1,6 +1,7 @@ import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/env/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'; @@ -143,6 +144,7 @@ void _resolveHomeDeps(GetIt getIt) { getIt.registerSingleton(FToast()); getIt.registerSingleton(MenuSharedState()); + getIt.registerSingleton(MobileRouterRecord()); getIt.registerFactoryParam( (user, _) => UserListener(userProfile: user), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index bdb70ab5c4..ea27237129 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -4,6 +4,7 @@ import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; @@ -51,6 +52,7 @@ GoRouter generateRouter(Widget child) { // emoji picker _mobileEmojiPickerPageRoute(), + _mobileImagePickerPageRoute(), ], // Desktop and Mobile @@ -216,6 +218,18 @@ GoRoute _mobileEmojiPickerPageRoute() { ); } +GoRoute _mobileImagePickerPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: MobileImagePickerScreen.routeName, + pageBuilder: (context, state) { + return const MaterialPage( + child: MobileImagePickerScreen(), + ); + }, + ); +} + GoRoute _desktopHomeScreenRoute() { return GoRoute( path: DesktopHomeScreen.routeName, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 7c005d9ab5..0be54f2fa7 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -178,6 +178,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.6.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f + url: "https://pub.dev" + source: hosted + version: "3.3.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" + url: "https://pub.dev" + source: hosted + version: "1.1.0" calendar_view: dependency: "direct main" description: @@ -539,6 +563,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" flutter_colorpicker: dependency: "direct main" description: @@ -1082,6 +1114,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" package_config: dependency: transitive description: @@ -1575,6 +1615,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "8ed044102f3135add97be8653662052838859f5400075ef227f8ad72ae320803" + url: "https://pub.dev" + source: hosted + version: "2.5.0+1" stack_trace: dependency: transitive description: @@ -1672,6 +1728,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" table_calendar: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index ab901ec6f1..8e9eb9aecf 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -124,6 +124,7 @@ dependencies: flutter_slidable: ^3.0.0 image_picker: ^1.0.4 image_gallery_saver: ^2.0.3 + cached_network_image: ^3.3.0 dev_dependencies: flutter_lints: ^2.0.1 diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 41a9e9a713..95d30f92d2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -186,7 +186,8 @@ "favorites": "Favorites", "clickToHidePersonal": "Click to hide personal section", "clickToHideFavorites": "Click to hide favorite section", - "addAPage": "Add a page" + "addAPage": "Add a page", + "recent": "Recent" }, "notifications": { "export": { diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index d826c202a3..4ab5398a1e 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -228,6 +228,22 @@ pub(crate) async fn read_favorites_handler( } data_result_ok(RepeatedViewPB { items: views }) } + +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn read_recent_views_handler( + folder: AFPluginState>, +) -> DataResult { + let folder = upgrade_folder(folder)?; + let recent_items = folder.get_all_recent_sections().await; + let mut views = vec![]; + for item in recent_items { + if let Ok(view) = folder.get_view_pb(&item.id).await { + views.push(view); + } + } + data_result_ok(RepeatedViewPB { items: views }) +} + #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_trash_handler( folder: AFPluginState>, diff --git a/frontend/rust-lib/flowy-folder2/src/event_map.rs b/frontend/rust-lib/flowy-folder2/src/event_map.rs index e6d145c533..f27ff376c5 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_map.rs @@ -36,6 +36,7 @@ pub fn init(folder: Weak) -> AFPlugin { .event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler) .event(FolderEvent::UpdateViewIcon, update_view_icon_handler) .event(FolderEvent::ReadFavorites, read_favorites_handler) + .event(FolderEvent::ReadRecentViews, read_recent_views_handler) .event(FolderEvent::ToggleFavorite, toggle_favorites_handler) } @@ -145,4 +146,7 @@ pub enum FolderEvent { #[event(input = "UpdateViewIconPayloadPB")] UpdateViewIcon = 35, + + #[event(output = "RepeatedViewPB")] + ReadRecentViews = 36, } diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 8762299a29..ea4c9e7a5f 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -7,8 +7,8 @@ use collab::core::collab::{CollabRawData, MutexCollab}; use collab::core::collab_state::SyncState; use collab_entity::CollabType; use collab_folder::{ - Folder, FolderData, FolderNotify, SectionItem, TrashChange, TrashChangeReceiver, TrashInfo, - UserId, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace, + Folder, FolderData, FolderNotify, Section, SectionItem, TrashChange, TrashChangeReceiver, + TrashInfo, UserId, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace, }; use parking_lot::{Mutex, RwLock}; use tokio_stream::wrappers::WatchStream; @@ -745,6 +745,7 @@ impl FolderManager { || Err(FlowyError::record_not_found()), |folder| { folder.set_current_view(view_id); + folder.add_recent_view_ids(vec![view_id.to_string()]); Ok(folder.get_workspace_id()) }, )?; @@ -800,17 +801,12 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_all_favorites(&self) -> Vec { - self.with_folder(Vec::new, |folder| { - let trash_ids = folder - .get_all_trash() - .into_iter() - .map(|trash| trash.id) - .collect::>(); + self.get_sections(Section::Favorite) + } - let mut views = folder.get_all_favorites(); - views.retain(|view| !trash_ids.contains(&view.id)); - views - }) + #[tracing::instrument(level = "trace", skip(self))] + pub(crate) async fn get_all_recent_sections(&self) -> Vec { + self.get_sections(Section::Recent) } #[tracing::instrument(level = "trace", skip(self))] @@ -1039,6 +1035,26 @@ impl FolderManager { pub fn get_cloud_service(&self) -> &Arc { &self.cloud_service } + + fn get_sections(&self, section_type: Section) -> Vec { + self.with_folder(Vec::new, |folder| { + let trash_ids = folder + .get_all_trash() + .into_iter() + .map(|trash| trash.id) + .collect::>(); + + let mut views = match section_type { + Section::Favorite => folder.get_all_favorites(), + Section::Recent => folder.get_all_recent_sections(), + _ => vec![], + }; + + // filter the views that are in the trash + views.retain(|view| !trash_ids.contains(&view.id)); + views + }) + } } /// Listen on the [ViewChange] after create/delete/update events happened