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")]