diff --git a/frontend/appflowy_flutter/assets/test/images/sample.svg b/frontend/appflowy_flutter/assets/test/images/sample.svg new file mode 100644 index 0000000000000..7dcd6907d84d6 --- /dev/null +++ b/frontend/appflowy_flutter/assets/test/images/sample.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart index dd40470ab9568..2236f03960e59 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; @@ -7,11 +5,9 @@ import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flutter/services.dart'; +import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; import '../../shared/base.dart'; import '../../shared/common_operations.dart'; @@ -215,17 +211,12 @@ void main() { } }); - testWidgets('Update page custom icon in title bar', (tester) async { + testWidgets('Update page custom image icon in title bar', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); /// prepare local image - final imagePath = await rootBundle.load('assets/test/images/sample.jpeg'); - final tempDirectory = await getTemporaryDirectory(); - final localImagePath = p.join(tempDirectory.path, 'sample.jpeg'); - final imageFile = File(localImagePath) - ..writeAsBytesSync(imagePath.buffer.asUint8List()); - final iconData = EmojiIconData.custom(imageFile.path); + final iconData = await tester.prepareImageIcon(); // create document, board, grid and calendar views for (final value in ViewLayoutPB.values) { @@ -259,4 +250,97 @@ void main() { ); } }); + + testWidgets('Update page custom svg icon in title bar', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + /// prepare local image + final iconData = await tester.prepareSvgIcon(); + + // create document, board, grid and calendar views + for (final value in ViewLayoutPB.values) { + if (value == ViewLayoutPB.Chat) { + continue; + } + + await tester.createNewPageWithNameUnderParent( + name: value.name, + parentName: gettingStarted, + layout: value, + ); + + // update its icon + await tester.updatePageIconInTitleBarByName( + name: value.name, + layout: value, + icon: iconData, + ); + + tester.expectViewHasIcon( + value.name, + value, + iconData, + ); + + tester.expectViewTitleHasIcon( + value.name, + value, + iconData, + ); + } + }); + + testWidgets('Update page custom svg icon in title bar by pasting a link', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + /// prepare local image + const testIconLink = + 'https://beta.appflowy.cloud/api/file_storage/008e6f23-516b-4d8d-b1fe-2b75c51eee26/v1/blob/6bdf8dff%2D0e54%2D4d35%2D9981%2Dcde68bef1141/BGpLnRtb3AGBNgSJsceu70j83zevYKrMLzqsTIJcBeI=.svg'; + + /// create document, board, grid and calendar views + for (final value in ViewLayoutPB.values) { + if (value == ViewLayoutPB.Chat) { + continue; + } + + await tester.createNewPageWithNameUnderParent( + name: value.name, + parentName: gettingStarted, + layout: value, + ); + + /// update its icon + await tester.updatePageIconInTitleBarByPasteALink( + name: value.name, + layout: value, + iconLink: testIconLink, + ); + + /// check if there is a svg in page + final pageName = tester.findPageName( + value.name, + layout: value, + ); + final imageInPage = find.descendant( + of: pageName, + matching: find.byType(SvgPicture), + ); + expect(imageInPage, findsOneWidget); + + /// check if there is a svg in title + final imageInTitle = find.descendant( + of: find.byType(ViewTitleBar), + matching: find.byWidgetPredicate((w) { + if (w is! SvgPicture) return false; + final loader = w.bytesLoader; + if (loader is! SvgFileLoader) return false; + return loader.file.path.endsWith('.svg'); + }), + ); + expect(imageInTitle, findsOneWidget); + } + }); } diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart index 1b94c654368e6..da7c7e92e7ca5 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart @@ -1,15 +1,12 @@ -import 'dart:io'; - +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; import '../../shared/emoji.dart'; import '../../shared/util.dart'; @@ -18,16 +15,11 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('document title:', () { - testWidgets('update page custom icon in title bar', (tester) async { + testWidgets('update page custom image icon in title bar', (tester) async { await tester.launchInAnonymousMode(); /// prepare local image - final imagePath = await rootBundle.load('assets/test/images/sample.jpeg'); - final tempDirectory = await getTemporaryDirectory(); - final localImagePath = p.join(tempDirectory.path, 'sample.jpeg'); - final imageFile = File(localImagePath) - ..writeAsBytesSync(imagePath.buffer.asUint8List()); - final iconData = EmojiIconData.custom(imageFile.path); + final iconData = await tester.prepareImageIcon(); /// create an empty page await tester @@ -50,16 +42,63 @@ void main() { /// check result final documentPage = find.byType(MobileDocumentScreen); - final rawEmojiIconWidget = find + final rawEmojiIconFinder = find .descendant( of: documentPage, matching: find.byType(RawEmojiIconWidget), ) - .evaluate() - .first - .widget as RawEmojiIconWidget; + .last; + final rawEmojiIconWidget = + rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget; final iconDataInWidget = rawEmojiIconWidget.emoji; expect(iconDataInWidget.type, FlowyIconType.custom); + final imageFinder = + find.descendant(of: rawEmojiIconFinder, matching: find.byType(Image)); + expect(imageFinder, findsOneWidget); + }); + + testWidgets('update page custom svg icon in title bar', (tester) async { + await tester.launchInAnonymousMode(); + + /// prepare local image + final iconData = await tester.prepareSvgIcon(); + + /// create an empty page + await tester + .tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey)); + + /// show Page style page + await tester.tapButton(find.byType(MobileViewPageLayoutButton)); + final pageStyleIcon = find.byType(PageStyleIcon); + final iconInPageStyleIcon = find.descendant( + of: pageStyleIcon, + matching: find.byType(RawEmojiIconWidget), + ); + expect(iconInPageStyleIcon, findsNothing); + + /// show icon picker + await tester.tapButton(pageStyleIcon); + + /// upload custom icon + await tester.pickImage(iconData); + + /// check result + final documentPage = find.byType(MobileDocumentScreen); + final rawEmojiIconFinder = find + .descendant( + of: documentPage, + matching: find.byType(RawEmojiIconWidget), + ) + .last; + final rawEmojiIconWidget = + rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget; + final iconDataInWidget = rawEmojiIconWidget.emoji; + expect(iconDataInWidget.type, FlowyIconType.custom); + final svgFinder = find.descendant( + of: rawEmojiIconFinder, + matching: find.byType(SvgPicture), + ); + expect(svgFinder, findsOneWidget); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 76d2d82b4aba7..d7da1f4d49ba6 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -50,6 +50,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'package:universal_platform/universal_platform.dart'; import 'emoji.dart'; @@ -677,6 +679,25 @@ extension CommonOperations on WidgetTester { await pumpAndSettle(); } + Future updatePageIconInTitleBarByPasteALink({ + required String name, + required ViewLayoutPB layout, + required String iconLink, + }) async { + await openPage( + name, + layout: layout, + ); + final title = find.descendant( + of: find.byType(ViewTitleBar), + matching: find.text(name), + ); + await tapButton(title); + await tapButton(find.byType(EmojiPickerButton)); + await pasteImageLinkAsIcon(iconLink); + await pumpAndSettle(); + } + Future openNotificationHub({int tabIndex = 0}) async { final finder = find.descendant( of: find.byType(NotificationButton), @@ -935,6 +956,24 @@ extension CommonOperations on WidgetTester { ), ); } + + Future prepareImageIcon() async { + final imagePath = await rootBundle.load('assets/test/images/sample.jpeg'); + final tempDirectory = await getTemporaryDirectory(); + final localImagePath = p.join(tempDirectory.path, 'sample.jpeg'); + final imageFile = File(localImagePath) + ..writeAsBytesSync(imagePath.buffer.asUint8List()); + return EmojiIconData.custom(imageFile.path); + } + + Future prepareSvgIcon() async { + final imagePath = await rootBundle.load('assets/test/images/sample.svg'); + final tempDirectory = await getTemporaryDirectory(); + final localImagePath = p.join(tempDirectory.path, 'sample.svg'); + final imageFile = File(localImagePath) + ..writeAsBytesSync(imagePath.buffer.asUint8List()); + return EmojiIconData.custom(imageFile.path); + } } extension SettingsFinder on CommonFinders { diff --git a/frontend/appflowy_flutter/integration_test/shared/emoji.dart b/frontend/appflowy_flutter/integration_test/shared/emoji.dart index 0f45098b23b3a..bc9be12ab2a74 100644 --- a/frontend/appflowy_flutter/integration_test/shared/emoji.dart +++ b/frontend/appflowy_flutter/integration_test/shared/emoji.dart @@ -1,20 +1,25 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_uploader.dart'; import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; import 'package:cross_file/cross_file.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart'; import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_test/flutter_test.dart'; import 'base.dart'; +import 'common_operations.dart'; extension EmojiTestExtension on WidgetTester { Future tapEmoji(String emoji) async { @@ -104,4 +109,37 @@ extension EmojiTestExtension on WidgetTester { ); await tapButton(confirmButton); } + + Future pasteImageLinkAsIcon(String link) async { + final pickTab = find.byType(PickerTab); + expect(pickTab, findsOneWidget); + await pumpAndSettle(); + + /// switch to custom tab + final iconTab = find.descendant( + of: pickTab, + matching: find.text(PickerTabType.custom.tr), + ); + expect(iconTab, findsOneWidget); + await tapButton(iconTab); + + // mock the clipboard + await getIt() + .setData(ClipboardServiceData(plainText: link)); + + // paste the link + await simulateKeyEvent( + LogicalKeyboardKey.keyV, + isControlPressed: Platform.isLinux || Platform.isWindows, + isMetaPressed: Platform.isMacOS, + ); + await pumpAndSettle(const Duration(seconds: 5)); + + /// confirm to upload + final confirmButton = find.descendant( + of: find.byType(IconUploader), + matching: find.byType(PrimaryRoundedButton), + ); + await tapButton(confirmButton); + } } diff --git a/frontend/appflowy_flutter/integration_test/shared/expectation.dart b/frontend/appflowy_flutter/integration_test/shared/expectation.dart index 7e9fe4bc38ce5..3b9ef0d75c83f 100644 --- a/frontend/appflowy_flutter/integration_test/shared/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/expectation.dart @@ -8,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/shared/appflowy_network_svg.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; @@ -252,16 +253,19 @@ extension Expectation on WidgetTester { ); expect(icon, findsOneWidget); } else if (type == FlowyIconType.custom) { + final isSvg = data.emoji.endsWith('.svg'); if (isURL(data.emoji)) { final image = find.descendant( of: pageName, - matching: find.byType(FlowyNetworkImage), + matching: isSvg + ? find.byType(FlowyNetworkSvg) + : find.byType(FlowyNetworkImage), ); expect(image, findsOneWidget); } else { final image = find.descendant( of: pageName, - matching: find.byType(Image), + matching: isSvg ? find.byType(SvgPicture) : find.byType(Image), ); expect(image, findsOneWidget); } @@ -290,16 +294,26 @@ extension Expectation on WidgetTester { ); expect(icon, findsOneWidget); } else if (type == FlowyIconType.custom) { + final isSvg = data.emoji.endsWith('.svg'); if (isURL(data.emoji)) { final image = find.descendant( of: find.byType(ViewTitleBar), - matching: find.byType(FlowyNetworkImage), + matching: isSvg + ? find.byType(FlowyNetworkSvg) + : find.byType(FlowyNetworkImage), ); expect(image, findsOneWidget); } else { final image = find.descendant( of: find.byType(ViewTitleBar), - matching: find.byType(Image), + matching: isSvg + ? find.byWidgetPredicate((w) { + if (w is! SvgPicture) return false; + final loader = w.bytesLoader; + if (loader is! SvgFileLoader) return false; + return loader.file.path.endsWith('.svg'); + }) + : find.byType(Image), ); expect(image, findsOneWidget); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart index f665455dbefd9..d78f8ce86522d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart @@ -75,7 +75,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget { enableBackgroundColorSelection: false, onSelectedEmoji: (r) { ViewBackendService.updateViewIcon( - viewId: view.id, + view: view, viewIcon: r.data, ); Navigator.pop(context); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart index 1a19845152669..6144fe37094ab 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -73,7 +73,14 @@ class _MobileSpaceTabState extends State listener: (context, state) { final lastCreatedPage = state.lastCreatedPage; if (lastCreatedPage != null) { - context.pushView(lastCreatedPage); + context.pushView( + lastCreatedPage, + tabs: [ + PickerTabType.emoji, + PickerTabType.icon, + PickerTabType.custom, + ].map((e) => e.name).toList(), + ); } }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart index c7cc314046c54..ea69f4054061f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart @@ -263,7 +263,7 @@ class _TabBarItemButtonState extends State { enableBackgroundColorSelection: false, onSelectedEmoji: (r) { ViewBackendService.updateViewIcon( - viewId: widget.view.id, + view: widget.view, viewIcon: r.data, ); if (!r.keepOpen) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 583af8af20223..7d5c888cac07b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -225,7 +225,7 @@ class _DocumentPageState extends State editorState: editorState, view: widget.view, onIconChanged: (icon) async => ViewBackendService.updateViewIcon( - viewId: widget.view.id, + view: widget.view, viewIcon: icon, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index 1d7f43004cd74..3b05bb27f288b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -3,9 +3,12 @@ import 'dart:io'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/shared/appflowy_network_svg.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; import 'package:string_validator/string_validator.dart'; @@ -59,7 +62,7 @@ class _EmojiIconWidgetState extends State { } } -class RawEmojiIconWidget extends StatelessWidget { +class RawEmojiIconWidget extends StatefulWidget { const RawEmojiIconWidget({ super.key, required this.emoji, @@ -71,64 +74,92 @@ class RawEmojiIconWidget extends StatelessWidget { final double emojiSize; final bool enableColor; + @override + State createState() => _RawEmojiIconWidgetState(); +} + +class _RawEmojiIconWidgetState extends State { + UserProfilePB? userProfile; + + EmojiIconData get emoji => widget.emoji; + + @override + void initState() { + super.initState(); + loadUserProfile(); + } + + @override + void didUpdateWidget(RawEmojiIconWidget oldWidget) { + super.didUpdateWidget(oldWidget); + loadUserProfile(); + } + @override Widget build(BuildContext context) { final defaultEmoji = SizedBox( - width: emojiSize, + width: widget.emojiSize, child: EmojiText( emoji: '❓', - fontSize: emojiSize, + fontSize: widget.emojiSize, textAlign: TextAlign.center, ), ); try { - switch (emoji.type) { + switch (widget.emoji.type) { case FlowyIconType.emoji: return SizedBox( - width: emojiSize, + width: widget.emojiSize, child: EmojiText( - emoji: emoji.emoji, - fontSize: emojiSize, + emoji: widget.emoji.emoji, + fontSize: widget.emojiSize, textAlign: TextAlign.justify, ), ); case FlowyIconType.icon: - IconsData iconData = IconsData.fromJson(jsonDecode(emoji.emoji)); - if (!enableColor) { + IconsData iconData = + IconsData.fromJson(jsonDecode(widget.emoji.emoji)); + if (!widget.enableColor) { iconData = iconData.noColor(); } /// Under the same width conditions, icons on macOS seem to appear /// larger than emojis, so 0.9 is used here to slightly reduce the /// size of the icons - final iconSize = Platform.isMacOS ? emojiSize * 0.9 : emojiSize; + final iconSize = + Platform.isMacOS ? widget.emojiSize * 0.9 : widget.emojiSize; return IconWidget( iconsData: iconData, size: iconSize, ); case FlowyIconType.custom: - final url = emoji.emoji; + final url = widget.emoji.emoji; + final isSvg = url.endsWith('.svg'); + final hasUserProfile = userProfile != null; if (isURL(url)) { - return SizedBox.square( - dimension: emojiSize, - child: FutureBuilder( - future: UserBackendService.getCurrentUserProfile(), - builder: (context, value) { - final userProfile = value.data?.fold( - (userProfile) => userProfile, - (l) => null, - ); - if (userProfile == null) return const SizedBox.shrink(); - return FlowyNetworkImage( - url: url, - width: emojiSize, - height: emojiSize, - userProfilePB: userProfile, - errorWidgetBuilder: (context, url, error) => - const SizedBox.shrink(), - ); + Widget child = const SizedBox.shrink(); + if (isSvg) { + child = FlowyNetworkSvg( + url, + headers: + hasUserProfile ? _buildRequestHeader(userProfile!) : {}, + width: widget.emojiSize, + height: widget.emojiSize, + ); + } else if (hasUserProfile) { + child = FlowyNetworkImage( + url: url, + width: widget.emojiSize, + height: widget.emojiSize, + userProfilePB: userProfile, + errorWidgetBuilder: (context, url, error) { + return const SizedBox.shrink(); }, - ), + ); + } + return SizedBox.square( + dimension: widget.emojiSize, + child: child, ); } final imageFile = File(url); @@ -136,13 +167,19 @@ class RawEmojiIconWidget extends StatelessWidget { throw PathNotFoundException(url, const OSError()); } return SizedBox.square( - dimension: emojiSize, - child: Image.file( - imageFile, - fit: BoxFit.cover, - width: emojiSize, - height: emojiSize, - ), + dimension: widget.emojiSize, + child: isSvg + ? SvgPicture.file( + File(url), + width: widget.emojiSize, + height: widget.emojiSize, + ) + : Image.file( + imageFile, + fit: BoxFit.cover, + width: widget.emojiSize, + height: widget.emojiSize, + ), ); default: return defaultEmoji; @@ -152,4 +189,32 @@ class RawEmojiIconWidget extends StatelessWidget { return defaultEmoji; } } + + Map _buildRequestHeader(UserProfilePB userProfilePB) { + final header = {}; + final token = userProfilePB.token; + try { + final decodedToken = jsonDecode(token); + header['Authorization'] = 'Bearer ${decodedToken['access_token']}'; + } catch (e) { + Log.error('Unable to decode token: $e'); + } + return header; + } + + Future loadUserProfile() async { + if (userProfile != null) return; + if (emoji.type == FlowyIconType.custom) { + final userProfile = + (await UserBackendService.getCurrentUserProfile()).fold( + (userProfile) => userProfile, + (l) => null, + ); + if (mounted) { + setState(() { + this.userProfile = userProfile; + }); + } + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart index 2578b6de070d3..b2cd77f312846 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart @@ -37,7 +37,7 @@ class PageStyleIconBloc extends Bloc { emit(state.copyWith(icon: icon)); if (shouldUpdateRemote && icon != null) { await ViewBackendService.updateViewIcon( - viewId: view.id, + view: view, viewIcon: icon, ); } diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart new file mode 100644 index 0000000000000..33c3bb2c0a29a --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart @@ -0,0 +1,197 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:flowy_svg/flowy_svg.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +import 'custom_image_cache_manager.dart'; + +class FlowyNetworkSvg extends StatefulWidget { + FlowyNetworkSvg( + this.url, { + Key? key, + this.cacheKey, + this.placeholder, + this.errorWidget, + this.width, + this.height, + this.headers, + this.fit = BoxFit.contain, + this.alignment = Alignment.center, + this.matchTextDirection = false, + this.allowDrawingOutsideViewBox = false, + this.semanticsLabel, + this.excludeFromSemantics = false, + this.theme = const SvgTheme(), + this.fadeDuration = Duration.zero, + this.colorFilter, + this.placeholderBuilder, + BaseCacheManager? cacheManager, + }) : cacheManager = cacheManager ?? CustomImageCacheManager(), + super(key: key ?? ValueKey(url)); + + final String url; + final String? cacheKey; + final Widget? placeholder; + final Widget? errorWidget; + final double? width; + final double? height; + final ColorFilter? colorFilter; + final Map? headers; + final BoxFit fit; + final AlignmentGeometry alignment; + final bool matchTextDirection; + final bool allowDrawingOutsideViewBox; + final String? semanticsLabel; + final bool excludeFromSemantics; + final SvgTheme theme; + final Duration fadeDuration; + final WidgetBuilder? placeholderBuilder; + final BaseCacheManager cacheManager; + + @override + State createState() => _FlowyNetworkSvgState(); + + static Future preCache( + String imageUrl, { + String? cacheKey, + BaseCacheManager? cacheManager, + }) { + final key = cacheKey ?? _generateKeyFromUrl(imageUrl); + cacheManager ??= DefaultCacheManager(); + return cacheManager.downloadFile(key); + } + + static Future clearCacheForUrl( + String imageUrl, { + String? cacheKey, + BaseCacheManager? cacheManager, + }) { + final key = cacheKey ?? _generateKeyFromUrl(imageUrl); + cacheManager ??= DefaultCacheManager(); + return cacheManager.removeFile(key); + } + + static Future clearCache({BaseCacheManager? cacheManager}) { + cacheManager ??= DefaultCacheManager(); + return cacheManager.emptyCache(); + } + + static String _generateKeyFromUrl(String url) => url.split('?').first; +} + +class _FlowyNetworkSvgState extends State + with SingleTickerProviderStateMixin { + bool _isLoading = false; + bool _isError = false; + File? _imageFile; + late String _cacheKey; + + late final AnimationController _controller; + late final Animation _animation; + + @override + void initState() { + super.initState(); + _cacheKey = + widget.cacheKey ?? FlowyNetworkSvg._generateKeyFromUrl(widget.url); + _controller = AnimationController( + vsync: this, + duration: widget.fadeDuration, + ); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller); + _loadImage(); + } + + Future _loadImage() async { + try { + _setToLoadingAfter15MsIfNeeded(); + + var file = (await widget.cacheManager.getFileFromMemory(_cacheKey))?.file; + + file ??= await widget.cacheManager.getSingleFile( + widget.url, + key: _cacheKey, + headers: widget.headers ?? {}, + ); + + _imageFile = file; + _isLoading = false; + + _setState(); + + await _controller.forward(); + } catch (e) { + log('CachedNetworkSVGImage: $e'); + + _isError = true; + _isLoading = false; + + _setState(); + } + } + + void _setToLoadingAfter15MsIfNeeded() => Future.delayed( + const Duration(milliseconds: 15), + () { + if (!_isLoading && _imageFile == null && !_isError) { + _isLoading = true; + _setState(); + } + }, + ); + + void _setState() => mounted ? setState(() {}) : null; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.width, + height: widget.height, + child: _buildImage(), + ); + } + + Widget _buildImage() { + if (_isLoading) return _buildPlaceholderWidget(); + + if (_isError) return _buildErrorWidget(); + + return FadeTransition( + opacity: _animation, + child: _buildSVGImage(), + ); + } + + Widget _buildPlaceholderWidget() => + Center(child: widget.placeholder ?? const SizedBox()); + + Widget _buildErrorWidget() => + Center(child: widget.errorWidget ?? const SizedBox()); + + Widget _buildSVGImage() { + if (_imageFile == null) return const SizedBox(); + + return SvgPicture.file( + _imageFile!, + fit: widget.fit, + width: widget.width, + height: widget.height, + alignment: widget.alignment, + matchTextDirection: widget.matchTextDirection, + allowDrawingOutsideViewBox: widget.allowDrawingOutsideViewBox, + colorFilter: widget.colorFilter, + semanticsLabel: widget.semanticsLabel, + excludeFromSemantics: widget.excludeFromSemantics, + placeholderBuilder: widget.placeholderBuilder, + theme: widget.theme, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart index 137ecbf94df87..b04b38a45afd8 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart @@ -243,6 +243,7 @@ class _FlowyIconEmojiPickerState extends State Widget _buildIconUploader() { return IconUploader( documentId: widget.documentId ?? '', + ensureFocus: true, onUrl: (url) { widget.onSelectedEmoji ?.call(SelectedEmojiIconResult(EmojiIconData.custom(url), false)); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart index 9ebcd78def8d3..9bc47e35dbd1e 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart @@ -1,7 +1,11 @@ import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; +import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/shared/appflowy_network_svg.dart'; +import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/shared/patterns/file_type_patterns.dart'; import 'package:appflowy/shared/permission/permission_checker.dart'; import 'package:appflowy/startup/startup.dart'; @@ -15,8 +19,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:string_validator/string_validator.dart'; import 'package:universal_platform/universal_platform.dart'; @visibleForTesting @@ -25,10 +32,12 @@ class IconUploader extends StatefulWidget { super.key, required this.onUrl, required this.documentId, + this.ensureFocus = false, }); final ValueChanged onUrl; final String documentId; + final bool ensureFocus; @override State createState() => _IconUploaderState(); @@ -38,53 +47,97 @@ class _IconUploaderState extends State { bool isHovering = false; bool isUploading = false; - final List pickedImages = []; + final List<_Image> pickedImages = []; + final FocusNode focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + + /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState] + /// this is to ensure that focus can be regained within a short period of time + if (widget.ensureFocus) { + Future.delayed(const Duration(milliseconds: 200), () { + if (!mounted || focusNode.hasFocus) return; + focusNode.requestFocus(); + }); + } + } + + @override + void dispose() { + super.dispose(); + focusNode.dispose(); + } @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Expanded( - child: DropTarget( - /// there is an issue with multiple DropTargets - /// see https://github.com/MixinNetwork/flutter-plugins/issues/2 - enable: false, - onDragEntered: (_) => setState(() => isHovering = true), - onDragExited: (_) => setState(() => isHovering = false), - onDragDone: (details) => loadImage(details.files), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => pickImage(), - child: DottedBorder( - dashPattern: const [3, 3], - radius: const Radius.circular(8), - borderType: BorderType.RRect, - color: isHovering - ? Theme.of(context).colorScheme.primary - : Theme.of(context).hintColor, - child: Center( - child: pickedImages.isEmpty ? dragHint() : previewImage(), + return Shortcuts( + shortcuts: { + LogicalKeySet( + Platform.isMacOS + ? LogicalKeyboardKey.meta + : LogicalKeyboardKey.control, + LogicalKeyboardKey.keyV, + ): _PasteIntent(), + }, + child: Actions( + actions: { + _PasteIntent: CallbackAction<_PasteIntent>( + onInvoke: (intent) => pasteAsAnImage(), + ), + }, + child: Focus( + autofocus: true, + focusNode: focusNode, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Expanded( + child: DropTarget( + /// there is an issue with multiple DropTargets + /// see https://github.com/MixinNetwork/flutter-plugins/issues/2 + enable: false, + onDragEntered: (_) => setState(() => isHovering = true), + onDragExited: (_) => setState(() => isHovering = false), + onDragDone: (details) => loadImage(details.files), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => pickImage(), + child: DottedBorder( + dashPattern: const [3, 3], + radius: const Radius.circular(8), + borderType: BorderType.RRect, + color: isHovering + ? Theme.of(context).colorScheme.primary + : Theme.of(context).hintColor, + child: Center( + child: pickedImages.isEmpty + ? dragHint() + : previewImage(), + ), + ), + ), ), ), ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Align( - alignment: Alignment.centerRight, - child: _ConfirmButton( - onTap: uploadImage, - enable: pickedImages.isNotEmpty, - ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Align( + alignment: Alignment.centerRight, + child: _ConfirmButton( + onTap: uploadImage, + enable: pickedImages.isNotEmpty, + ), + ), + ), + ], ), ), - ], + ), ), ); } @@ -96,26 +149,54 @@ class _IconUploaderState extends State { color: Theme.of(context).hintColor, ); - Widget previewImage() => Image.file( - File(pickedImages.first), + Widget previewImage() { + final image = pickedImages.first; + final url = image.url; + if (image is _FileImage) { + if (url.endsWith(_svgSuffix)) { + return SvgPicture.file( + File(url), + width: 200, + height: 200, + ); + } + return Image.file( + File(url), + width: 200, + height: 200, + ); + } else if (image is _NetworkImage) { + if (url.endsWith(_svgSuffix)) { + return FlowyNetworkSvg( + url, + width: 200, + height: 200, + ); + } + return FlowyNetworkImage( width: 200, height: 200, - fit: BoxFit.cover, + url: url, ); + } + return const SizedBox.shrink(); + } void loadImage(List files) { final imageFiles = files .where( (file) => file.mimeType?.startsWith('image/') ?? - false || imgExtensionRegex.hasMatch(file.name), + false || + imgExtensionRegex.hasMatch(file.name) || + file.name.endsWith(_svgSuffix), ) .toList(); if (imageFiles.isEmpty) return; if (mounted) { setState(() { pickedImages.clear(); - pickedImages.add(imageFiles.first.path); + pickedImages.add(_FileImage(imageFiles.first.path)); }); } } @@ -126,7 +207,7 @@ class _IconUploaderState extends State { final result = await getIt().pickFiles( dialogTitle: '', type: FileType.custom, - allowedExtensions: defaultImageExtensions, + allowedExtensions: List.of(defaultImageExtensions)..add('svg'), ); loadImage(result?.files.map((f) => f.xFile).toList() ?? const []); } else { @@ -154,22 +235,27 @@ class _IconUploaderState extends State { final isLocalMode = (userProfile?.authenticator ?? AuthenticatorPB.Local) == AuthenticatorPB.Local; if (isLocalMode) { - result = await saveImageToLocalStorage(pickedImages.first); + result = await pickedImages.first.saveToLocal(); } else { - final (url, errorMsg) = await saveImageToCloudStorage( - pickedImages.first, - widget.documentId, - ); - result = url; - if (errorMsg?.isNotEmpty ?? false) { - Log.error('upload icon image error :$errorMsg'); - } + result = await pickedImages.first.uploadToCloud(widget.documentId); } isUploading = false; if (result?.isNotEmpty ?? false) { widget.onUrl.call(result!); } } + + Future pasteAsAnImage() async { + final data = await getIt().getData(); + final plainText = data.plainText; + Log.info('pasteAsAnImage plainText:$plainText'); + if (isURL(plainText)) { + setState(() { + pickedImages.clear(); + pickedImages.add(_NetworkImage(plainText!)); + }); + } + } } class _ConfirmButton extends StatelessWidget { @@ -192,3 +278,65 @@ class _ConfirmButton extends StatelessWidget { ); } } + +const _svgSuffix = '.svg'; + +class _PasteIntent extends Intent {} + +abstract class _Image { + String get url; + + Future saveToLocal(); + + Future uploadToCloud(String documentId); + + String get pureUrl => url.split('?').first; +} + +class _FileImage extends _Image { + _FileImage(this.url); + + @override + final String url; + + @override + Future saveToLocal() => saveImageToLocalStorage(url); + + @override + Future uploadToCloud(String documentId) async { + final (url, errorMsg) = await saveImageToCloudStorage( + this.url, + documentId, + ); + if (errorMsg?.isNotEmpty ?? false) { + Log.error('upload icon image :${this.url} error :$errorMsg'); + } + return url; + } +} + +class _NetworkImage extends _Image { + _NetworkImage(this.url); + + @override + final String url; + + @override + Future saveToLocal() async { + final file = await CustomImageCacheManager().downloadFile(pureUrl); + return file.file.path; + } + + @override + Future uploadToCloud(String documentId) async { + final file = await CustomImageCacheManager().downloadFile(pureUrl); + final (url, errorMsg) = await saveImageToCloudStorage( + file.file.path, + documentId, + ); + if (errorMsg?.isNotEmpty ?? false) { + Log.error('upload icon image :${this.url} error :$errorMsg'); + } + return url; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 2a4c472397171..7c2a4d9b64ea8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -262,7 +262,7 @@ class ViewBloc extends Bloc { }, updateIcon: (value) async { await ViewBackendService.updateViewIcon( - viewId: view.id, + view: view, viewIcon: view.icon.toEmojiIconData(), ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index d20aed8c1bc6f..4caede578c138 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -4,6 +4,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/me import 'package:appflowy/plugins/trash/application/trash_service.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -190,14 +192,25 @@ class ViewBackendService { } static Future> updateViewIcon({ - required String viewId, + required ViewPB view, required EmojiIconData viewIcon, }) { + final viewId = view.id; + final oldIcon = view.icon.toEmojiIconData(); final icon = viewIcon.toViewIcon(); final payload = UpdateViewIconPayloadPB.create() ..viewId = viewId ..icon = icon; - + if (oldIcon.type == FlowyIconType.custom && + viewIcon.emoji != oldIcon.emoji) { + DocumentEventDeleteFile( + DeleteFilePB(url: oldIcon.emoji), + ).send().onFailure((e) { + Log.error( + 'updateViewIcon error while deleting :${oldIcon.emoji}, error: ${e.msg}, ${e.code}', + ); + }); + } return FolderEventUpdateViewIcon(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 34509dbb96021..917e54eada8d1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -615,7 +615,7 @@ class _SingleInnerViewItemState extends State { offset: const Offset(0, 5), direction: PopoverDirection.bottomWithLeftAligned, popupBuilder: (_) => RenameViewPopover( - viewId: widget.view.id, + view: widget.view, name: widget.view.name, emoji: widget.view.icon.toEmojiIconData(), popoverController: popoverController, @@ -661,7 +661,7 @@ class _SingleInnerViewItemState extends State { documentId: widget.view.id, onSelectedEmoji: (r) { ViewBackendService.updateViewIcon( - viewId: widget.view.id, + view: widget.view, viewIcon: r.data, ); if (!r.keepOpen) controller.close(); @@ -792,7 +792,7 @@ class _SingleInnerViewItemState extends State { return; } await ViewBackendService.updateViewIcon( - viewId: widget.view.id, + view: widget.view, viewIcon: data.data, ); break; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart index 8e339ca17c59b..954fc776031ec 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -12,7 +13,7 @@ import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; class RenameViewPopover extends StatefulWidget { const RenameViewPopover({ super.key, - required this.viewId, + required this.view, required this.name, required this.popoverController, required this.emoji, @@ -21,7 +22,7 @@ class RenameViewPopover extends StatefulWidget { this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); - final String viewId; + final ViewPB view; final String name; final PopoverController popoverController; final EmojiIconData emoji; @@ -64,7 +65,7 @@ class _RenameViewPopoverState extends State { direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 18), onSubmitted: _updateViewIcon, - documentId: widget.viewId, + documentId: widget.view.id, tabs: widget.tabs, ), ), @@ -88,7 +89,7 @@ class _RenameViewPopoverState extends State { Future _updateViewName(String name) async { if (name.isNotEmpty && name != widget.name) { await ViewBackendService.updateView( - viewId: widget.viewId, + viewId: widget.view.id, name: _controller.text, ); widget.popoverController.close(); @@ -100,7 +101,7 @@ class _RenameViewPopoverState extends State { PopoverController? _, ) async { await ViewBackendService.updateViewIcon( - viewId: widget.viewId, + view: widget.view, viewIcon: r.data, ); if (!r.keepOpen) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 05657307f90b7..7b5a3056e630f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -280,7 +280,7 @@ class _ViewTitleState extends State { // icon + textfield _resetTextEditingController(state); return RenameViewPopover( - viewId: widget.view.id, + view: widget.view, name: widget.view.name, popoverController: popoverController, icon: widget.view.defaultIcon(), diff --git a/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart b/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart index cba112dc2bfb6..d2d7738944bcd 100644 --- a/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart +++ b/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +export 'package:flutter_svg/flutter_svg.dart'; + /// The class for FlowySvgData that the code generator will implement class FlowySvgData { /// The svg data diff --git a/frontend/rust-lib/flowy-document/src/entities.rs b/frontend/rust-lib/flowy-document/src/entities.rs index 24611a8140007..74157c612487f 100644 --- a/frontend/rust-lib/flowy-document/src/entities.rs +++ b/frontend/rust-lib/flowy-document/src/entities.rs @@ -115,6 +115,13 @@ pub struct DownloadFilePB { pub local_file_path: String, } +#[derive(Default, ProtoBuf, Validate)] +pub struct DeleteFilePB { + #[pb(index = 1)] + #[validate(url)] + pub url: String, +} + #[derive(Default, ProtoBuf)] pub struct CreateDocumentPayloadPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index 774561cc4e7f6..387e216f082c8 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -485,13 +485,10 @@ pub(crate) async fn download_file_handler( // Handler for deleting file pub(crate) async fn delete_file_handler( - params: AFPluginData, + params: AFPluginData, manager: AFPluginState>, ) -> FlowyResult<()> { - let DownloadFilePB { - url, - local_file_path: _, - } = params.try_into_inner()?; + let DeleteFilePB { url } = params.try_into_inner()?; let manager = upgrade_document(manager)?; manager.delete_file(url).await } diff --git a/frontend/rust-lib/flowy-document/src/event_map.rs b/frontend/rust-lib/flowy-document/src/event_map.rs index e05519d81e444..1931d321615a9 100644 --- a/frontend/rust-lib/flowy-document/src/event_map.rs +++ b/frontend/rust-lib/flowy-document/src/event_map.rs @@ -126,7 +126,7 @@ pub enum DocumentEvent { UploadFile = 15, #[event(input = "DownloadFilePB")] DownloadFile = 16, - #[event(input = "DownloadFilePB")] + #[event(input = "DeleteFilePB")] DeleteFile = 17, #[event(input = "UpdateDocumentAwarenessStatePB")]