diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index b358068db8..0e508e8ca0 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -59,6 +59,8 @@ PODS: - Flutter - irondash_engine_context (0.0.1): - Flutter + - keyboard_height_plugin (0.0.1): + - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -100,6 +102,7 @@ DEPENDENCIES: - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) + - keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`) @@ -145,6 +148,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" irondash_engine_context: :path: ".symlinks/plugins/irondash_engine_context/ios" + keyboard_height_plugin: + :path: ".symlinks/plugins/keyboard_height_plugin/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -180,6 +185,7 @@ SPEC CHECKSUMS: image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 integration_test: 13825b8a9334a850581300559b8839134b124670 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 + keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart index 7d24ebc0a2..fc6786e7cb 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_theme_data.dart @@ -4,6 +4,7 @@ import 'package:flowy_infra/colorscheme/colorscheme.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; const _primaryColor = Color(0xFF2DA2F6); //primary 100 const _onBackgroundColor = Color(0xff2F3030); // text/title color @@ -17,6 +18,8 @@ ThemeData getMobileThemeData( String fontFamily, String monospaceFontFamily, ) { + fontFamily = GoogleFonts.getFont(fontFamily).fontFamily ?? fontFamily; + final mobileColorTheme = (brightness == Brightness.light) ? ColorScheme( brightness: brightness, @@ -140,14 +143,14 @@ ThemeData getMobileThemeData( textButtonTheme: TextButtonThemeData( style: ButtonStyle( textStyle: MaterialStateProperty.all( - const TextStyle( - fontFamily: 'Poppins', + TextStyle( + fontFamily: fontFamily, ), ), ), ), // text - fontFamily: 'Poppins', + fontFamily: fontFamily, textTheme: TextTheme( displayLarge: const TextStyle( color: Color(0xFF57B5F8), @@ -157,6 +160,7 @@ ThemeData getMobileThemeData( letterSpacing: 0.16, ), displayMedium: TextStyle( + fontFamily: fontFamily, color: mobileColorTheme.onBackground, fontSize: 32, fontWeight: FontWeight.w600, @@ -172,6 +176,7 @@ ThemeData getMobileThemeData( ), // body2 14 Regular bodyMedium: TextStyle( + fontFamily: fontFamily, color: mobileColorTheme.onBackground, fontSize: 14, fontWeight: FontWeight.w400, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart index b4aa9deee1..d913e9c58f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class BottomSheetActionWidget extends StatelessWidget { @@ -26,7 +27,7 @@ class BottomSheetActionWidget extends StatelessWidget { size: const Size.square(22.0), color: iconColor, ), - label: Text(text), + label: FlowyText(text), style: Theme.of(context) .outlinedButtonTheme .style diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index ec54775d62..60d025bc4e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -18,7 +18,7 @@ import 'home.dart'; class MobileHomeScreen extends StatelessWidget { const MobileHomeScreen({super.key}); - static const routeName = "/MobileHomeScreen"; + static const routeName = '/home'; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 3cc4108b45..c359438fcd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -1,10 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class MobileHomePageHeader extends StatelessWidget { @@ -18,77 +21,76 @@ class MobileHomePageHeader extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - // TODO: implement the details later. - return SizedBox( - height: 80, - child: Row( - children: [ - const FlowyText( - '🐻', - fontSize: 26, - ), - const HSpace(14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return BlocProvider( + create: (context) => getIt(param1: userProfile) + ..add(const SettingsUserEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final userIcon = state.userProfile.iconUrl; + return SizedBox( + height: 48, + child: Row( children: [ - // TODO: replace with the real data - Row( - children: [ - const FlowyText.medium( - 'AppFlowy', - fontSize: 18, - ), - // temporary placeholder for log out icon button - // needs to be replaced with workspace switcher and log out - IconButton( - onPressed: () => showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: const Text('Log out'), - content: - const Text('Are you sure you want to log out?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, 'Cancel'), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () async { - await getIt().signOut(); - runAppFlowy(); - }, - child: const Text('OK'), - ), - ], - ), - ), - icon: const Icon( - Icons.arrow_drop_down, - ), - ), - ], + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + // replace with user icon + userIcon.isNotEmpty ? userIcon : '🐻', + fontSize: 26, + ), + onTap: () async { + final icon = await context.push( + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: { + MobileEmojiPickerScreen.pageTitle: 'User icon', + }, + ).toString(), + ); + if (icon != null) { + if (context.mounted) { + context.read().add( + SettingsUserEvent.updateUserIcon( + iconUrl: icon.emoji, + ), + ); + } + } + }, ), - FlowyText.regular( - userProfile.email.isNotEmpty - ? userProfile.email - : userProfile.name, - fontSize: 12, - color: theme.colorScheme.onSurface, - overflow: TextOverflow.ellipsis, + const HSpace(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const FlowyText.medium( + 'AppFlowy', + fontSize: 18, + ), + FlowyText.regular( + userProfile.email.isNotEmpty + ? userProfile.email + : userProfile.name, + fontSize: 12, + color: theme.colorScheme.onSurface, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + IconButton( + onPressed: () { + context.push(MobileHomeSettingPage.routeName); + }, + icon: const FlowySvg( + FlowySvgs.m_setting_m, + ), ), ], ), - ), - IconButton( - onPressed: () { - context.push(MobileHomeSettingPage.routeName); - }, - icon: const FlowySvg( - FlowySvgs.m_setting_m, - ), - ), - ], + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart index 0623b77430..a678916da5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; +import 'package:appflowy/mobile/presentation/setting/logout_setting_group.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; @@ -11,7 +12,7 @@ class MobileHomeSettingPage extends StatefulWidget { super.key, }); - static const routeName = '/MobileHomeSettingPage'; + static const routeName = '/settings'; @override State createState() => _MobileHomeSettingPageState(); @@ -58,8 +59,10 @@ class _MobileHomeSettingPageState extends State { // TODO(yijing): implement this along with Notification Page const NotificationsSettingGroup(), const AppearanceSettingGroup(), + const LanguageSettingGroup(), const SupportSettingGroup(), const AboutSettingGroup(), + const LogoutSettingGroup(), ], ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart index 1870402b01..569e50eb8c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart @@ -13,7 +13,7 @@ import 'package:go_router/go_router.dart'; class MobileHomeTrashPage extends StatelessWidget { const MobileHomeTrashPage({super.key}); - static const routeName = "/MobileHomeTrashPage"; + static const routeName = '/trash'; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/appearance_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/appearance_setting_group.dart new file mode 100644 index 0000000000..c693425e5b --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/appearance_setting_group.dart @@ -0,0 +1,23 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/setting/appearance/theme_setting.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import '../setting.dart'; + +class AppearanceSettingGroup extends StatelessWidget { + const AppearanceSettingGroup({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return MobileSettingGroup( + groupTitle: LocaleKeys.settings_menu_appearance.tr(), + settingItemList: const [ + ThemeSetting(), + FontSetting(), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart new file mode 100644 index 0000000000..01aebf8fd6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart @@ -0,0 +1,90 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart'; +import 'package:appflowy/util/theme_mode_extension.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../setting.dart'; + +class ThemeSetting extends StatelessWidget { + const ThemeSetting({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final themeMode = context.watch().state.themeMode; + return MobileSettingItem( + name: LocaleKeys.settings_appearance_themeMode_label.tr(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText( + themeMode.labelText, + color: theme.colorScheme.onSurface, + ), + const Icon(Icons.chevron_right), + ], + ), + onTap: () { + showFlowyMobileBottomSheet( + context, + title: LocaleKeys.settings_appearance_themeMode_label.tr(), + builder: (_) { + return Column( + children: [ + _ThemeModeRadioListTile( + title: LocaleKeys.settings_appearance_themeMode_system.tr(), + value: ThemeMode.system, + ), + _ThemeModeRadioListTile( + title: LocaleKeys.settings_appearance_themeMode_light.tr(), + value: ThemeMode.light, + ), + _ThemeModeRadioListTile( + title: LocaleKeys.settings_appearance_themeMode_dark.tr(), + value: ThemeMode.dark, + ), + ], + ); + }, + ); + }, + ); + } +} + +class _ThemeModeRadioListTile extends StatelessWidget { + const _ThemeModeRadioListTile({ + required this.title, + required this.value, + }); + final String title; + final ThemeMode value; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return RadioListTile( + dense: true, + contentPadding: const EdgeInsets.fromLTRB(0, 0, 4, 0), + controlAffinity: ListTileControlAffinity.trailing, + title: Text( + title, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + groupValue: context.read().state.themeMode, + value: value, + onChanged: (selectedThemeMode) { + if (selectedThemeMode == null) return; + context.read().setThemeMode(selectedThemeMode); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart deleted file mode 100644 index d85e5fe273..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance_setting_group.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart'; -import 'package:appflowy/util/theme_mode_extension.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'setting.dart'; - -class AppearanceSettingGroup extends StatefulWidget { - const AppearanceSettingGroup({ - super.key, - }); - - @override - State createState() => _AppearanceSettingGroupState(); -} - -class _AppearanceSettingGroupState extends State { - @override - Widget build(BuildContext context) { - return BlocSelector( - selector: (state) { - return state.themeMode; - }, - builder: (context, themeMode) { - final theme = Theme.of(context); - return MobileSettingGroup( - groupTitle: LocaleKeys.settings_menu_appearance.tr(), - settingItemList: [ - MobileSettingItem( - name: LocaleKeys.settings_appearance_themeMode_label.tr(), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - themeMode.labelText, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface, - ), - ), - const Icon(Icons.chevron_right), - ], - ), - onTap: () { - showFlowyMobileBottomSheet( - context, - title: LocaleKeys.settings_appearance_themeMode_label.tr(), - builder: (_) { - return Column( - children: [ - _ThemeModeRadioListTile( - title: LocaleKeys.settings_appearance_themeMode_system - .tr(), - value: ThemeMode.system, - ), - _ThemeModeRadioListTile( - title: LocaleKeys.settings_appearance_themeMode_light - .tr(), - value: ThemeMode.light, - ), - _ThemeModeRadioListTile( - title: LocaleKeys.settings_appearance_themeMode_dark - .tr(), - value: ThemeMode.dark, - ), - ], - ); - }, - ); - }, - ), - ], - ); - }, - ); - } -} - -class _ThemeModeRadioListTile extends StatelessWidget { - const _ThemeModeRadioListTile({ - required this.title, - required this.value, - }); - final String title; - final ThemeMode value; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return RadioListTile( - dense: true, - contentPadding: const EdgeInsets.fromLTRB(0, 0, 4, 0), - controlAffinity: ListTileControlAffinity.trailing, - title: Text( - title, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface, - ), - ), - groupValue: context.read().state.themeMode, - value: value, - onChanged: (selectedThemeMode) { - if (selectedThemeMode == null) return; - context.read().setThemeMode(selectedThemeMode); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart new file mode 100644 index 0000000000..6e1c8c3fad --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart @@ -0,0 +1,127 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_bar.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; + +final List _availableFonts = GoogleFonts.asMap().keys.toList(); + +class FontPickerScreen extends StatelessWidget { + static const routeName = '/font_picker'; + + const FontPickerScreen({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const LanguagePickerPage(); + } +} + +class LanguagePickerPage extends StatefulWidget { + const LanguagePickerPage({ + super.key, + }); + + @override + State createState() => _LanguagePickerPageState(); +} + +class _LanguagePickerPageState extends State { + late List availableFonts; + + @override + initState() { + super.initState(); + + availableFonts = _availableFonts; + } + + @override + Widget build(BuildContext context) { + final selectedFontFamilyName = + context.watch().state.font; + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: FlowyText.semibold( + LocaleKeys.titleBar_font.tr(), + fontSize: 14.0, + ), + leading: AppBarBackButton( + onTap: () => context.pop(), + ), + ), + body: SafeArea( + child: ListView.separated( + itemBuilder: (context, index) { + if (index == 0) { + // search bar + return Padding( + padding: const EdgeInsets.all(8.0), + child: FlowyMobileSearchTextField( + onKeywordChanged: (keyword) { + setState(() { + availableFonts = _availableFonts + .where( + (element) => parseFontFamilyName(element) + .toLowerCase() + .contains(keyword.toLowerCase()), + ) + .toList(); + }); + }, + ), + ); + } + final fontFamilyName = availableFonts[index - 1]; + final displayName = parseFontFamilyName(fontFamilyName); + return InkWell( + onTap: () => context.pop(fontFamilyName), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 12.0, + ), + child: Row( + children: [ + const HSpace(12.0), + FlowyText( + displayName, + fontFamily: + GoogleFonts.getFont(fontFamilyName).fontFamily, + ), + const Spacer(), + if (selectedFontFamilyName == fontFamilyName) + const Icon( + Icons.check, + size: 16, + ), + const HSpace(12.0), + ], + ), + ), + ); + }, + separatorBuilder: (_, __) => const Divider( + height: 1, + ), + itemCount: availableFonts.length + 1, // with search bar + ), + ), + ); + } + + String parseFontFamilyName(String fontFamilyName) { + final camelCase = RegExp('(?<=[a-z])[A-Z]'); + return fontFamilyName + .replaceAll('_regular', '') + .replaceAllMapped(camelCase, (m) => ' ${m.group(0)}'); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart new file mode 100644 index 0000000000..aabea6305e --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart @@ -0,0 +1,47 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; +import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../setting.dart'; + +class FontSetting extends StatelessWidget { + const FontSetting({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final selectedFont = context.watch().state.font; + return MobileSettingItem( + name: LocaleKeys.settings_appearance_fontFamily_label.tr(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText( + selectedFont, + fontFamily: GoogleFonts.getFont(selectedFont).fontFamily, + color: theme.colorScheme.onSurface, + ), + const Icon(Icons.chevron_right), + ], + ), + onTap: () async { + final newFont = await context.push(FontPickerScreen.routeName); + if (newFont != null && newFont != selectedFont) { + if (context.mounted) { + context.read().setFontFamily(newFont); + context.read().syncFontFamily(newFont); + } + } + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language/language_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language/language_picker_screen.dart new file mode 100644 index 0000000000..ba89a28a23 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language/language_picker_screen.dart @@ -0,0 +1,83 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/language.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class LanguagePickerScreen extends StatelessWidget { + static const routeName = '/language_picker'; + + const LanguagePickerScreen({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const LanguagePickerPage(); + } +} + +class LanguagePickerPage extends StatefulWidget { + const LanguagePickerPage({ + super.key, + }); + + @override + State createState() => _LanguagePickerPageState(); +} + +class _LanguagePickerPageState extends State { + @override + Widget build(BuildContext context) { + final supportedLocales = EasyLocalization.of(context)!.supportedLocales; + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: FlowyText.semibold( + LocaleKeys.titleBar_language.tr(), + fontSize: 14.0, + ), + leading: AppBarBackButton( + onTap: () => context.pop(), + ), + ), + body: SafeArea( + child: ListView.separated( + itemBuilder: (context, index) { + final locale = supportedLocales[index]; + return InkWell( + onTap: () => context.pop(locale), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 12.0, + ), + child: Row( + children: [ + const HSpace(12.0), + FlowyText( + languageFromLocale(locale), + ), + const Spacer(), + if (EasyLocalization.of(context)!.locale == locale) + const Icon( + Icons.check, + size: 16, + ), + const HSpace(12.0), + ], + ), + ), + ); + }, + separatorBuilder: (_, __) => const Divider( + height: 1, + ), + itemCount: supportedLocales.length, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart new file mode 100644 index 0000000000..9a4bc494f0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart @@ -0,0 +1,64 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/language.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import 'setting.dart'; + +class LanguageSettingGroup extends StatefulWidget { + const LanguageSettingGroup({ + super.key, + }); + + @override + State createState() => _LanguageSettingGroupState(); +} + +class _LanguageSettingGroupState extends State { + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) { + return state.locale; + }, + builder: (context, locale) { + final theme = Theme.of(context); + return MobileSettingGroup( + groupTitle: LocaleKeys.settings_menu_language.tr(), + settingItemList: [ + MobileSettingItem( + name: LocaleKeys.settings_menu_language.tr(), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText( + languageFromLocale(locale), + color: theme.colorScheme.onSurface, + ), + const Icon(Icons.chevron_right), + ], + ), + onTap: () async { + final newLocale = + await context.push(LanguagePickerScreen.routeName); + if (newLocale != null && newLocale != locale) { + if (mounted) { + context + .read() + .setLocale(context, newLocale); + } + } + }, + ), + ], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/logout_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/logout_setting_group.dart new file mode 100644 index 0000000000..af200fd878 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/logout_setting_group.dart @@ -0,0 +1,38 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class LogoutSettingGroup extends StatelessWidget { + const LogoutSettingGroup({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: FlowyButton( + margin: const EdgeInsets.symmetric( + vertical: 16.0, + ), + text: FlowyText.medium( + LocaleKeys.settings_menu_logout.tr(), + textAlign: TextAlign.center, + fontSize: 14.0, + ), + onTap: () async { + await getIt().signOut(); + runAppFlowy(); + }, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/setting.dart index 75136748b3..992e9be903 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/setting.dart @@ -1,6 +1,8 @@ -export 'personal_info/personal_info.dart'; -export 'notifications_setting_group.dart'; -export 'appearance_setting_group.dart'; -export 'support_setting_group.dart'; export 'about/about.dart'; +export 'appearance/appearance_setting_group.dart'; +export 'font/font_setting.dart'; +export 'language_setting_group.dart'; +export 'notifications_setting_group.dart'; +export 'personal_info/personal_info.dart'; +export 'support_setting_group.dart'; export 'widgets/widgets.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart index 67c5d8015e..17e9a62867 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart @@ -1,7 +1,6 @@ +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'mobile_setting_item_widget.dart'; - class MobileSettingGroup extends StatelessWidget { const MobileSettingGroup({ required this.groupTitle, @@ -9,26 +8,21 @@ class MobileSettingGroup extends StatelessWidget { this.showDivider = true, super.key, }); + final String groupTitle; - final List settingItemList; + final List settingItemList; final bool showDivider; @override Widget build(BuildContext context) { - final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 8, - ), - Text( + const VSpace(4.0), + FlowyText.semibold( groupTitle, - style: theme.textTheme.labelSmall, - ), - const SizedBox( - height: 12, ), + const VSpace(4.0), ...settingItemList, showDivider ? const Divider() : const SizedBox.shrink(), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart index b11a22fa43..81e91b0b56 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart @@ -1,3 +1,4 @@ +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class MobileSettingItem extends StatelessWidget { @@ -15,13 +16,12 @@ class MobileSettingItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); return Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.only(bottom: 4), child: ListTile( - title: Text( + title: FlowyText.medium( name, - style: theme.textTheme.labelMedium, + fontSize: 14.0, ), subtitle: subtitle, trailing: trailing, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_search_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_search_bar.dart new file mode 100644 index 0000000000..41a9b09b0a --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_search_bar.dart @@ -0,0 +1,77 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class FlowyMobileSearchTextField extends StatefulWidget { + const FlowyMobileSearchTextField({ + super.key, + required this.onKeywordChanged, + }); + + final void Function(String keyword) onKeywordChanged; + + @override + State createState() => + _FlowyMobileSearchTextFieldState(); +} + +class _FlowyMobileSearchTextFieldState + extends State { + final TextEditingController controller = TextEditingController(); + final FocusNode focusNode = FocusNode(); + + @override + void dispose() { + controller.dispose(); + focusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 36.0, + ), + child: FlowyTextField( + focusNode: focusNode, + hintText: LocaleKeys.emoji_search.tr(), + controller: controller, + onChanged: widget.onKeywordChanged, + prefixIcon: const Padding( + padding: EdgeInsets.only( + left: 8.0, + right: 4.0, + ), + child: FlowySvg( + FlowySvgs.search_s, + ), + ), + prefixIconConstraints: const BoxConstraints( + maxHeight: 18.0, + ), + suffixIcon: Padding( + padding: const EdgeInsets.all(4.0), + child: FlowyButton( + text: const FlowySvg( + FlowySvgs.close_lg, + ), + margin: EdgeInsets.zero, + useIntrinsicWidth: true, + onTap: () { + if (controller.text.isNotEmpty) { + controller.clear(); + widget.onKeywordChanged(''); + } else { + focusNode.unfocus(); + } + }, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart index 4989bdb043..2d323a6abf 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -12,9 +12,11 @@ class FlowyEmojiPicker extends StatefulWidget { const FlowyEmojiPicker({ super.key, required this.onEmojiSelected, + this.emojiPerLine = 9, }); final EmojiSelectedCallback onEmojiSelected; + final int emojiPerLine; @override State createState() => _FlowyEmojiPickerState(); @@ -61,6 +63,7 @@ class _FlowyEmojiPickerState extends State { showSectionHeader: true, showTabs: false, defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, + perLine: widget.emojiPerLine, ), onEmojiSelected: widget.onEmojiSelected, headerBuilder: (context, category) { diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart index 6aaa307ef5..fffe500bf6 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart @@ -5,14 +5,19 @@ import 'package:go_router/go_router.dart'; class MobileEmojiPickerScreen extends StatelessWidget { static const routeName = '/emoji_picker'; + static const pageTitle = 'title'; const MobileEmojiPickerScreen({ super.key, + this.title, }); + final String? title; + @override Widget build(BuildContext context) { return IconPickerPage( + title: title, onSelected: (result) { context.pop(result); }, diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart index 34e897901a..9d1664c7d0 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart @@ -105,10 +105,12 @@ class _SearchTextField extends StatefulWidget { class _SearchTextFieldState extends State<_SearchTextField> { final TextEditingController controller = TextEditingController(); + final FocusNode focusNode = FocusNode(); @override void dispose() { controller.dispose(); + focusNode.dispose(); super.dispose(); } @@ -120,7 +122,7 @@ class _SearchTextFieldState extends State<_SearchTextField> { maxHeight: 32.0, ), child: FlowyTextField( - autoFocus: true, + focusNode: focusNode, hintText: LocaleKeys.emoji_search.tr(), controller: controller, onChanged: widget.onKeywordChanged, @@ -145,8 +147,12 @@ class _SearchTextFieldState extends State<_SearchTextField> { margin: EdgeInsets.zero, useIntrinsicWidth: true, onTap: () { - controller.clear(); - widget.onKeywordChanged(''); + if (controller.text.isNotEmpty) { + controller.clear(); + widget.onKeywordChanged(''); + } else { + focusNode.unfocus(); + } }, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart index 818a3bcbf7..296d450d40 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -72,6 +72,7 @@ class _FlowyIconPickerState extends State child: TabBarView( children: [ FlowyEmojiPicker( + emojiPerLine: _getEmojiPerLine(), onEmojiSelected: (_, emoji) { widget.onSelected( EmojiPickerResult( @@ -116,6 +117,11 @@ class _FlowyIconPickerState extends State ), ); } + + int _getEmojiPerLine() { + final width = MediaQuery.of(context).size.width; + return width ~/ 46.0; // the size of the emoji + } } class _RemoveIconButton extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart index 2ced391ccf..6ebf83c1f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart @@ -6,26 +6,23 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -class IconPickerPage extends StatefulWidget { +class IconPickerPage extends StatelessWidget { const IconPickerPage({ super.key, + this.title, required this.onSelected, }); final void Function(EmojiPickerResult) onSelected; + final String? title; - @override - State createState() => _IconPickerPageState(); -} - -class _IconPickerPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( titleSpacing: 0, title: FlowyText.semibold( - LocaleKeys.titleBar_pageIcon.tr(), + title ?? LocaleKeys.titleBar_pageIcon.tr(), fontSize: 14.0, ), leading: AppBarBackButton( @@ -34,7 +31,7 @@ class _IconPickerPageState extends State { ), body: SafeArea( child: FlowyIconPicker( - onSelected: widget.onSelected, + onSelected: onSelected, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index c1274193c9..3995020007 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -255,7 +255,7 @@ class _AppFlowyEditorPageState extends State { behavior: HitTestBehavior.translucent, onTap: () async { // if the last one isn't a empty node, insert a new empty node. - await _ensureLastNodeIsEmptyParagraph(); + await _focusOnLastEmptyParagraph(); }, child: VSpace(PlatformExtension.isDesktopOrWeb ? 200 : 400), ), @@ -266,51 +266,49 @@ class _AppFlowyEditorPageState extends State { _setInitialSelection(editorScrollController); if (PlatformExtension.isMobile) { - return Column( - children: [ - Expanded( - child: MobileFloatingToolbar( - editorState: editorState, - editorScrollController: editorScrollController, - toolbarBuilder: (context, anchor, closeToolbar) { - return AdaptiveTextSelectionToolbar.editable( - clipboardStatus: ClipboardStatus.pasteable, - onCopy: () { - copyCommand.execute(editorState); - closeToolbar(); - }, - onCut: () => cutCommand.execute(editorState), - onPaste: () => pasteCommand.execute(editorState), - onSelectAll: () => selectAllCommand.execute(editorState), - onLiveTextInput: null, - anchors: TextSelectionToolbarAnchors( - primaryAnchor: anchor, - ), - ); - }, - child: editor, - ), - ), - MobileToolbar( - editorState: editorState, - toolbarItems: [ - textDecorationMobileToolbarItem, - buildTextAndBackgroundColorMobileToolbarItem(), - headingMobileToolbarItem, - mobileBlocksToolbarItem, - linkMobileToolbarItem, - dividerMobileToolbarItem, - imageMobileToolbarItem, - mathEquationMobileToolbarItem, - codeMobileToolbarItem, - mobileAlignToolbarItem, - mobileIndentToolbarItem, - mobileOutdentToolbarItem, - undoMobileToolbarItem, - redoMobileToolbarItem, - ], - ), + return MobileToolbarV2( + toolbarHeight: 48.0, + editorState: editorState, + toolbarItems: [ + customTextDecorationMobileToolbarItem, + buildTextAndBackgroundColorMobileToolbarItem(), + mobileAddBlockToolbarItem, + mobileConvertBlockToolbarItem, + linkMobileToolbarItem, + imageMobileToolbarItem, + mobileAlignToolbarItem, + mobileIndentToolbarItem, + mobileOutdentToolbarItem, + undoMobileToolbarItem, + redoMobileToolbarItem, ], + child: Column( + children: [ + Expanded( + child: MobileFloatingToolbar( + editorState: editorState, + editorScrollController: editorScrollController, + toolbarBuilder: (context, anchor, closeToolbar) { + return AdaptiveTextSelectionToolbar.editable( + clipboardStatus: ClipboardStatus.pasteable, + onCopy: () { + copyCommand.execute(editorState); + closeToolbar(); + }, + onCut: () => cutCommand.execute(editorState), + onPaste: () => pasteCommand.execute(editorState), + onSelectAll: () => selectAllCommand.execute(editorState), + onLiveTextInput: null, + anchors: TextSelectionToolbarAnchors( + primaryAnchor: anchor, + ), + ); + }, + child: editor, + ), + ), + ], + ), ); } @@ -482,19 +480,23 @@ class _AppFlowyEditorPageState extends State { AppFlowyEditorL10n.current = EditorI18n(); } - Future _ensureLastNodeIsEmptyParagraph() async { + Future _focusOnLastEmptyParagraph() async { final editorState = widget.editorState; final root = editorState.document.root; final lastNode = root.children.lastOrNull; + final transaction = editorState.transaction; if (lastNode == null || lastNode.delta?.isEmpty == false || lastNode.type != ParagraphBlockKeys.type) { - final transaction = editorState.transaction; transaction.insertNode([root.children.length], paragraphNode()); transaction.afterSelection = Selection.collapsed( Position(path: [root.children.length]), ); - await editorState.apply(transaction); + } else { + transaction.afterSelection = Selection.collapsed( + Position(path: lastNode.path), + ); } + await editorState.apply(transaction); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index eba62b8734..c08204cee0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -229,7 +229,11 @@ class _DocumentHeaderToolbarState extends State { Widget child = Container( alignment: Alignment.bottomLeft, width: double.infinity, - padding: EditorStyleCustomizer.documentPadding, + padding: PlatformExtension.isDesktopOrWeb + ? EditorStyleCustomizer.documentPadding + : EdgeInsets.symmetric( + horizontal: EditorStyleCustomizer.documentPadding.left - 6.0, + ), child: SizedBox( height: 28, child: Row( @@ -420,48 +424,50 @@ class DocumentCoverState extends State { right: 12, child: Row( children: [ - RoundedTextButton( - onPressed: () { - showFlowyMobileBottomSheet( - context, - title: LocaleKeys.document_plugins_cover_changeCover.tr(), - builder: (context) { - return ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 340, - minHeight: 80, - ), - child: UploadImageMenu( - supportTypes: const [ - UploadImageType.color, - UploadImageType.local, - UploadImageType.url, - UploadImageType.unsplash, - ], - onSelectedLocalImage: (path) async { - context.pop(); - widget.onCoverChanged(CoverType.file, path); - }, - onSelectedAIImage: (_) { - throw UnimplementedError(); - }, - onSelectedNetworkImage: (url) async { - context.pop(); - widget.onCoverChanged(CoverType.file, url); - }, - onSelectedColor: (color) { - context.pop(); - widget.onCoverChanged(CoverType.color, color); - }, - ), - ); - }, - ); - }, - fillColor: Theme.of(context).colorScheme.onSurfaceVariant, - width: 120, - height: 32, - title: LocaleKeys.document_plugins_cover_changeCover.tr(), + IntrinsicWidth( + child: RoundedTextButton( + onPressed: () { + showFlowyMobileBottomSheet( + context, + title: + LocaleKeys.document_plugins_cover_changeCover.tr(), + builder: (context) { + return ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 340, + minHeight: 80, + ), + child: UploadImageMenu( + supportTypes: const [ + UploadImageType.color, + UploadImageType.local, + UploadImageType.url, + UploadImageType.unsplash, + ], + onSelectedLocalImage: (path) async { + context.pop(); + widget.onCoverChanged(CoverType.file, path); + }, + onSelectedAIImage: (_) { + throw UnimplementedError(); + }, + onSelectedNetworkImage: (url) async { + context.pop(); + widget.onCoverChanged(CoverType.file, url); + }, + onSelectedColor: (color) { + context.pop(); + widget.onCoverChanged(CoverType.color, color); + }, + ), + ); + }, + ); + }, + fillColor: Theme.of(context).colorScheme.onSurfaceVariant, + height: 32, + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + ), ), const HSpace(8.0), SizedBox.square( @@ -530,14 +536,16 @@ class DocumentCoverState extends State { constraints: BoxConstraints.loose(const Size(380, 450)), margin: EdgeInsets.zero, onClose: () => isPopoverOpen = false, - child: RoundedTextButton( - onPressed: () => popoverController.show(), - hoverColor: Theme.of(context).colorScheme.surface, - textColor: Theme.of(context).colorScheme.tertiary, - fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5), - width: 120, - height: 28, - title: LocaleKeys.document_plugins_cover_changeCover.tr(), + child: IntrinsicWidth( + child: RoundedTextButton( + height: 28.0, + onPressed: () => popoverController.show(), + hoverColor: Theme.of(context).colorScheme.surface, + textColor: Theme.of(context).colorScheme.tertiary, + fillColor: + Theme.of(context).colorScheme.surface.withOpacity(0.5), + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + ), ), popupBuilder: (BuildContext popoverContext) { isPopoverOpen = true; @@ -575,7 +583,7 @@ class DeleteCoverButton extends StatelessWidget { Widget build(BuildContext context) { return FlowyIconButton( hoverColor: Theme.of(context).colorScheme.surface, - fillColor: Theme.of(context).colorScheme.onSurfaceVariant, + fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.5), iconPadding: const EdgeInsets.all(5), width: 28, icon: FlowySvg( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart index a1f4487562..82e38ccfe7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart @@ -1,5 +1,7 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -22,8 +24,8 @@ class _ImagePickerPageState extends State { return Scaffold( appBar: AppBar( titleSpacing: 0, - title: const FlowyText.semibold( - 'Page icon', + title: FlowyText.semibold( + LocaleKeys.titleBar_pageIcon.tr(), fontSize: 14.0, ), leading: AppBarBackButton( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart index 7d5c95ea51..aa8c6fe496 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart @@ -5,8 +5,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final imageMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __) => const FlowySvg(FlowySvgs.m_toolbar_imae_lg), - actionHandler: (editorState, selection) async { + itemIconBuilder: (_, __, ___) => const FlowySvg(FlowySvgs.m_toolbar_imae_lg), + actionHandler: (_, editorState) async { final imagePlaceholderKey = GlobalKey(); await editorState.insertEmptyImageBlock(imagePlaceholderKey); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart index 0c97ccf0ae..62714ae93d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart @@ -4,10 +4,13 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final mathEquationMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __) => - const SizedBox(width: 22, child: FlowySvg(FlowySvgs.math_lg)), - actionHandler: (editorState, selection) async { - if (!selection.isCollapsed) { + itemIconBuilder: (_, __, ___) => const SizedBox( + width: 22, + child: FlowySvg(FlowySvgs.math_lg), + ), + actionHandler: (_, editorState) async { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { return; } final path = selection.start.path; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart new file mode 100644 index 0000000000..eb5ddde345 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart @@ -0,0 +1,327 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +// convert the current block to other block types +// only show in single selection and text type +final mobileAddBlockToolbarItem = MobileToolbarItem.withMenu( + itemIconBuilder: (_, editorState, ___) { + if (!onlyShowInSingleSelectionAndTextType(editorState)) { + return null; + } + return const FlowySvg( + FlowySvgs.add_m, + size: Size.square(48), + ); + }, + itemMenuBuilder: (_, editorState, service) { + final selection = editorState.selection; + if (selection == null) { + return null; + } + return BlocksMenu( + items: _addBlockMenuItems, + editorState: editorState, + service: service, + ); + }, +); + +final _addBlockMenuItems = [ + // paragraph + BlockMenuItem( + blockType: ParagraphBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_text_decoration_m), + label: LocaleKeys.editor_text.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + paragraphNode(), + ); + service.closeItemMenu(); + }, + ), + + // to-do list + BlockMenuItem( + blockType: TodoListBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_checkbox_m), + label: LocaleKeys.editor_checkbox.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + todoListNode(checked: false), + ); + service.closeItemMenu(); + }, + ), + + // heading 1 - 3 + BlockMenuItem( + blockType: HeadingBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_h1_m), + label: LocaleKeys.editor_heading1.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + headingNode(level: 1), + ); + service.closeItemMenu(); + }, + ), + BlockMenuItem( + blockType: HeadingBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_h2_m), + label: LocaleKeys.editor_heading2.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + headingNode(level: 2), + ); + service.closeItemMenu(); + }, + ), + BlockMenuItem( + blockType: HeadingBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_h3_m), + label: LocaleKeys.editor_heading3.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + headingNode(level: 3), + ); + service.closeItemMenu(); + }, + ), + + // bulleted list + BlockMenuItem( + blockType: BulletedListBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_bulleted_list_m), + label: LocaleKeys.editor_bulletedList.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + bulletedListNode(), + ); + service.closeItemMenu(); + }, + ), + + // numbered list + BlockMenuItem( + blockType: NumberedListBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_numbered_list_m), + label: LocaleKeys.editor_numberedList.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + numberedListNode(), + ); + service.closeItemMenu(); + }, + ), + + // toggle list + BlockMenuItem( + blockType: ToggleListBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_toggle_list_m), + label: LocaleKeys.document_plugins_toggleList.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + toggleListBlockNode(), + ); + service.closeItemMenu(); + }, + ), + + // quote + BlockMenuItem( + blockType: QuoteBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_quote_m), + label: LocaleKeys.editor_quote.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + quoteNode(), + ); + service.closeItemMenu(); + }, + ), + + // callout + BlockMenuItem( + blockType: CalloutBlockKeys.type, + // FIXME: update icon + icon: const Icon(Icons.note_rounded), + label: LocaleKeys.document_plugins_callout.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + calloutNode(), + ); + service.closeItemMenu(); + }, + ), + + // code + BlockMenuItem( + blockType: CodeBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_code_m), + label: LocaleKeys.document_selectionMenu_codeBlock.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertBlockOrReplaceCurrentBlock( + selection, + codeBlockNode(), + ); + service.closeItemMenu(); + }, + ), + + // divider + BlockMenuItem( + blockType: DividerBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_divider_m), + label: LocaleKeys.editor_divider.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertDivider(selection); + service.closeItemMenu(); + }, + ), + + // math equation + BlockMenuItem( + blockType: MathEquationBlockKeys.type, + icon: const FlowySvg( + FlowySvgs.math_lg, + size: Size.square(22), + ), + label: LocaleKeys.document_plugins_mathEquation_name.tr(), + isSelected: _unSelectable, + onTap: (editorState, selection, service) async { + await editorState.insertMathEquation(selection); + service.closeItemMenu(); + }, + ), +]; + +bool _unSelectable( + EditorState editorState, + Selection selection, +) { + return false; +} + +extension on EditorState { + Future insertBlockOrReplaceCurrentBlock( + Selection selection, + Node insertedNode, + ) async { + // If the current block is not an empty paragraph block, + // then insert a new block below the current block. + final node = getNodeAtPath(selection.start.path); + if (node == null) { + return; + } + final transaction = this.transaction; + if (node.type != ParagraphBlockKeys.type || + (node.delta?.isNotEmpty ?? true)) { + final path = node.path.next; + // insert the block below the current empty paragraph block + transaction + ..insertNode(path, insertedNode) + ..afterSelection = Selection.collapsed( + Position(path: path, offset: 0), + ); + } else { + final path = node.path; + // replace the current empty paragraph block with the inserted block + transaction + ..insertNode(path, insertedNode) + ..deleteNode(node) + ..afterSelection = Selection.collapsed( + Position(path: path, offset: 0), + ); + } + await apply(transaction); + } + + Future insertMathEquation( + Selection selection, + ) async { + final path = selection.start.path; + final node = getNodeAtPath(path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + final transaction = this.transaction; + final insertedNode = mathEquationNode(); + if (delta.isEmpty) { + transaction + ..insertNode(path, insertedNode) + ..deleteNode(node); + } else { + transaction.insertNode( + path.next, + insertedNode, + ); + } + + await apply(transaction); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + final mathEquationState = getNodeAtPath(path)?.key.currentState; + if (mathEquationState != null && + mathEquationState is MathEquationBlockComponentWidgetState) { + mathEquationState.showEditingDialog(); + } + }); + } + + Future insertDivider(Selection selection) async { + // same as the [handler] of [dividerMenuItem] in Desktop + + final path = selection.end.path; + final node = getNodeAtPath(path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + final insertedPath = delta.isEmpty ? path : path.next; + final transaction = this.transaction; + transaction.insertNode(insertedPath, dividerNode()); + // only insert a new paragraph node when the next node is not a paragraph node + // and its delta is not empty. + final next = node.next; + if (next == null || + next.type != ParagraphBlockKeys.type || + next.delta?.isNotEmpty == true) { + transaction.insertNode( + insertedPath, + paragraphNode(), + ); + } + transaction.afterSelection = Selection.collapsed( + Position(path: insertedPath.next), + ); + await apply(transaction); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart index 6171c362e3..38e0407185 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_align_toolbar_item.dart @@ -6,7 +6,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; final mobileAlignToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, editorState) { + itemIconBuilder: (_, editorState, __) { return onlyShowInTextType(editorState) ? const FlowySvg( FlowySvgs.toolbar_align_center_s, @@ -14,7 +14,11 @@ final mobileAlignToolbarItem = MobileToolbarItem.withMenu( ) : null; }, - itemMenuBuilder: (editorState, selection, _) { + itemMenuBuilder: (_, editorState, ___) { + final selection = editorState.selection; + if (selection == null) { + return null; + } return _MobileAlignMenu( editorState: editorState, selection: selection, @@ -34,6 +38,7 @@ class _MobileAlignMenu extends StatelessWidget { @override Widget build(BuildContext context) { return GridView.count( + padding: EdgeInsets.zero, crossAxisCount: 3, mainAxisSpacing: 8, crossAxisSpacing: 8, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart new file mode 100644 index 0000000000..2ca2098c61 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart @@ -0,0 +1,90 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class BlockMenuItem { + const BlockMenuItem({ + required this.blockType, + required this.icon, + required this.label, + required this.onTap, + this.isSelected, + }); + + // block type + final String blockType; + final Widget icon; + final String label; + // callback + final void Function( + EditorState editorState, + Selection selection, + // used to control the open or close the menu + MobileToolbarWidgetService service, + ) onTap; + + final bool Function( + EditorState editorState, + Selection selection, + )? isSelected; +} + +class BlocksMenu extends StatelessWidget { + const BlocksMenu({ + super.key, + required this.editorState, + required this.items, + required this.service, + }); + + final EditorState editorState; + final List items; + final MobileToolbarWidgetService service; + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 4, + padding: const EdgeInsets.only( + bottom: 36.0, + ), + shrinkWrap: true, + children: items.map((item) { + final selection = editorState.selection; + if (selection == null) { + return const SizedBox.shrink(); + } + bool isSelected = false; + if (item.isSelected != null) { + isSelected = item.isSelected!(editorState, selection); + } else { + isSelected = _isSelected(editorState, selection, item.blockType); + } + return MobileToolbarItemMenuBtn( + icon: item.icon, + label: FlowyText(item.label), + isSelected: isSelected, + onPressed: () async { + item.onTap(editorState, selection, service); + }, + ); + }).toList(), + ); + } + + bool _isSelected( + EditorState editorState, + Selection selection, + String blockType, + ) { + final node = editorState.getNodeAtPath(selection.start.path); + final type = node?.type; + if (node == null || type == null) { + return false; + } + return type == blockType; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_toolbar_item.dart deleted file mode 100644 index 005af9f8b1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_toolbar_item.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -final mobileBlocksToolbarItem = MobileToolbarItem.withMenu( - itemIconBuilder: (_, __) => - const AFMobileIcon(afMobileIcons: AFMobileIcons.list), - itemMenuBuilder: (editorState, selection, _) { - return _MobileListMenu( - editorState: editorState, - selection: selection, - ); - }, -); - -class _MobileListMenu extends StatelessWidget { - const _MobileListMenu({ - required this.editorState, - required this.selection, - }); - - final Selection selection; - final EditorState editorState; - - @override - Widget build(BuildContext context) { - return GridView.count( - crossAxisCount: 2, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 5, - shrinkWrap: true, - children: [ - // bulleted list, numbered list - _buildListButton( - context, - BulletedListBlockKeys.type, - const AFMobileIcon(afMobileIcons: AFMobileIcons.bulletedList), - LocaleKeys.document_plugins_bulletedList.tr(), - ), - _buildListButton( - context, - NumberedListBlockKeys.type, - const AFMobileIcon(afMobileIcons: AFMobileIcons.numberedList), - LocaleKeys.document_plugins_numberedList.tr(), - ), - - // todo list, quote list - _buildListButton( - context, - TodoListBlockKeys.type, - const AFMobileIcon(afMobileIcons: AFMobileIcons.checkbox), - LocaleKeys.document_plugins_todoList.tr(), - ), - _buildListButton( - context, - QuoteBlockKeys.type, - const AFMobileIcon(afMobileIcons: AFMobileIcons.quote), - LocaleKeys.document_plugins_quoteList.tr(), - ), - - // toggle list, callout - _buildListButton( - context, - ToggleListBlockKeys.type, - const FlowySvg( - FlowySvgs.toggle_list_s, - size: Size.square(24), - ), - LocaleKeys.document_plugins_toggleList.tr(), - ), - _buildListButton( - context, - CalloutBlockKeys.type, - const Icon(Icons.note_rounded), - LocaleKeys.document_plugins_callout.tr(), - ), - _buildListButton( - context, - CodeBlockKeys.type, - const Icon(Icons.abc), - LocaleKeys.document_selectionMenu_codeBlock.tr(), - ), - // code block - _buildListButton( - context, - CodeBlockKeys.type, - const Icon(Icons.abc), - LocaleKeys.document_selectionMenu_codeBlock.tr(), - ), - // outline - _buildListButton( - context, - OutlineBlockKeys.type, - const Icon(Icons.list_alt), - LocaleKeys.document_selectionMenu_outline.tr(), - ), - ], - ); - } - - Widget _buildListButton( - BuildContext context, - String listBlockType, - Widget icon, - String label, - ) { - final node = editorState.getNodeAtPath(selection.start.path); - final type = node?.type; - if (node == null || type == null) { - const SizedBox.shrink(); - } - final isSelected = type == listBlockType; - return MobileToolbarItemMenuBtn( - icon: icon, - label: FlowyText(label), - isSelected: isSelected, - onPressed: () async { - await editorState.formatNode( - selection, - (node) { - final attributes = { - ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(), - if (listBlockType == TodoListBlockKeys.type) - TodoListBlockKeys.checked: false, - if (listBlockType == CalloutBlockKeys.type) - CalloutBlockKeys.icon: '📌', - }; - return node.copyWith( - type: isSelected ? ParagraphBlockKeys.type : listBlockType, - attributes: attributes, - ); - }, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart new file mode 100644 index 0000000000..694e82dc18 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_convert_block_toolbar_item.dart @@ -0,0 +1,251 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +// convert the current block to other block types +// only show in single selection and text type +final mobileConvertBlockToolbarItem = MobileToolbarItem.withMenu( + itemIconBuilder: (_, editorState, ___) { + if (!onlyShowInSingleSelectionAndTextType(editorState)) { + return null; + } + return const FlowySvg( + FlowySvgs.convert_s, + size: Size.square(22), + ); + }, + itemMenuBuilder: (_, editorState, service) { + final selection = editorState.selection; + if (selection == null) { + return null; + } + return BlocksMenu( + items: _convertToBlockMenuItems, + editorState: editorState, + service: service, + ); + }, +); + +final _convertToBlockMenuItems = [ + // paragraph + BlockMenuItem( + blockType: ParagraphBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_text_decoration_m), + label: LocaleKeys.editor_text.tr(), + onTap: (editorState, selection, _) => editorState.convertBlockType( + selection, + ParagraphBlockKeys.type, + ), + ), + + // to-do list + BlockMenuItem( + blockType: TodoListBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_checkbox_m), + label: LocaleKeys.editor_checkbox.tr(), + onTap: (editorState, selection, _) => editorState.convertBlockType( + selection, + TodoListBlockKeys.type, + extraAttributes: { + TodoListBlockKeys.checked: false, + }, + ), + ), + + // heading 1 - 3 + BlockMenuItem( + blockType: HeadingBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_h1_m), + label: LocaleKeys.editor_heading1.tr(), + isSelected: (editorState, selection) => _isHeadingSelected( + editorState, + selection, + 1, + ), + onTap: (editorState, selection, _) { + final isSelected = _isHeadingSelected( + editorState, + selection, + 1, + ); + editorState.convertBlockType( + selection, + HeadingBlockKeys.type, + isSelected: isSelected, + extraAttributes: { + HeadingBlockKeys.level: 1, + }, + ); + }, + ), + BlockMenuItem( + blockType: HeadingBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_h2_m), + label: LocaleKeys.editor_heading2.tr(), + isSelected: (editorState, selection) => _isHeadingSelected( + editorState, + selection, + 2, + ), + onTap: (editorState, selection, _) { + final isSelected = _isHeadingSelected( + editorState, + selection, + 2, + ); + editorState.convertBlockType( + selection, + HeadingBlockKeys.type, + isSelected: isSelected, + extraAttributes: { + HeadingBlockKeys.level: 2, + }, + ); + }, + ), + BlockMenuItem( + blockType: HeadingBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_h3_m), + label: LocaleKeys.editor_heading3.tr(), + isSelected: (editorState, selection) => _isHeadingSelected( + editorState, + selection, + 3, + ), + onTap: (editorState, selection, _) { + final isSelected = _isHeadingSelected( + editorState, + selection, + 3, + ); + editorState.convertBlockType( + selection, + HeadingBlockKeys.type, + isSelected: isSelected, + extraAttributes: { + HeadingBlockKeys.level: 3, + }, + ); + }, + ), + + // bulleted list + BlockMenuItem( + blockType: BulletedListBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_bulleted_list_m), + label: LocaleKeys.editor_bulletedList.tr(), + onTap: (editorState, selection, _) => editorState.convertBlockType( + selection, + BulletedListBlockKeys.type, + ), + ), + + // numbered list + BlockMenuItem( + blockType: NumberedListBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_numbered_list_m), + label: LocaleKeys.editor_numberedList.tr(), + onTap: (editorState, selection, _) => editorState.convertBlockType( + selection, + NumberedListBlockKeys.type, + ), + ), + + // toggle list + BlockMenuItem( + blockType: ToggleListBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_toggle_list_m), + label: LocaleKeys.document_plugins_toggleList.tr(), + onTap: (editorState, selection, _) => editorState.convertBlockType( + selection, + ToggleListBlockKeys.type, + ), + ), + + // quote + BlockMenuItem( + blockType: QuoteBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_quote_m), + label: LocaleKeys.editor_quote.tr(), + onTap: (editorState, selection, _) => editorState.convertBlockType( + selection, + QuoteBlockKeys.type, + ), + ), + + // callout + BlockMenuItem( + blockType: CalloutBlockKeys.type, + // FIXME: update icon + icon: const Icon(Icons.note_rounded), + label: LocaleKeys.document_plugins_callout.tr(), + onTap: (editorState, selection, _) => editorState.convertBlockType( + selection, + CalloutBlockKeys.type, + extraAttributes: { + CalloutBlockKeys.icon: '📌', + }, + ), + ), + + // code + BlockMenuItem( + blockType: CodeBlockKeys.type, + icon: const FlowySvg(FlowySvgs.m_code_m), + label: LocaleKeys.document_selectionMenu_codeBlock.tr(), + onTap: (editorState, selection, _) => editorState.convertBlockType( + selection, + CodeBlockKeys.type, + ), + ), +]; + +extension on EditorState { + Future convertBlockType( + Selection selection, + String newBlockType, { + Attributes? extraAttributes, + bool? isSelected, + }) async { + final node = getNodeAtPath(selection.start.path); + final type = node?.type; + if (node == null || type == null) { + assert(false, 'node or type is null'); + return; + } + final selected = isSelected ?? type == newBlockType; + await formatNode( + selection, + (node) { + final attributes = { + ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(), + // for some block types, they have extra attributes, like todo list has checked attribute, callout has icon attribute, etc. + if (!selected && extraAttributes != null) ...extraAttributes, + }; + return node.copyWith( + type: selected ? ParagraphBlockKeys.type : newBlockType, + attributes: attributes, + ); + }, + ); + } +} + +bool _isHeadingSelected( + EditorState editorState, + Selection selection, + int level, +) { + final node = editorState.getNodeAtPath(selection.start.path); + final type = node?.type; + if (node == null || type == null) { + return false; + } + return type == HeadingBlockKeys.type && + node.attributes[HeadingBlockKeys.level] == level; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_items.dart index fb07a38a25..adc1026f91 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_indent_toolbar_items.dart @@ -2,23 +2,23 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final mobileIndentToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, editorState) { + itemIconBuilder: (_, editorState, __) { return onlyShowInTextType(editorState) ? const Icon(Icons.format_indent_increase_rounded) : null; }, - actionHandler: (editorState, selection) { + actionHandler: (_, editorState) { indentCommand.execute(editorState); }, ); final mobileOutdentToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, editorState) { + itemIconBuilder: (_, editorState, __) { return onlyShowInTextType(editorState) ? const Icon(Icons.format_indent_decrease_rounded) : null; }, - actionHandler: (editorState, selection) { + actionHandler: (_, editorState) { outdentCommand.execute(editorState); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart new file mode 100644 index 0000000000..3a5ca1ccd1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_text_decoration_item.dart @@ -0,0 +1,106 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu( + itemIconBuilder: (_, __, ___) => const AFMobileIcon( + afMobileIcons: AFMobileIcons.textDecoration, + ), + itemMenuBuilder: (_, editorState, __) { + final selection = editorState.selection; + if (selection == null) { + return const SizedBox.shrink(); + } + return _TextDecorationMenu(editorState, selection); + }, +); + +class _TextDecorationMenu extends StatefulWidget { + const _TextDecorationMenu( + this.editorState, + this.selection, + ); + + final EditorState editorState; + final Selection selection; + + @override + State<_TextDecorationMenu> createState() => _TextDecorationMenuState(); +} + +class _TextDecorationMenuState extends State<_TextDecorationMenu> { + final textDecorations = [ + // BIUS + TextDecorationUnit( + icon: AFMobileIcons.bold, + label: AppFlowyEditorL10n.current.bold, + name: AppFlowyRichTextKeys.bold, + ), + TextDecorationUnit( + icon: AFMobileIcons.italic, + label: AppFlowyEditorL10n.current.italic, + name: AppFlowyRichTextKeys.italic, + ), + TextDecorationUnit( + icon: AFMobileIcons.underline, + label: AppFlowyEditorL10n.current.underline, + name: AppFlowyRichTextKeys.underline, + ), + TextDecorationUnit( + icon: AFMobileIcons.strikethrough, + label: AppFlowyEditorL10n.current.strikethrough, + name: AppFlowyRichTextKeys.strikethrough, + ), + + // Code + TextDecorationUnit( + icon: AFMobileIcons.code, + label: AppFlowyEditorL10n.current.embedCode, + name: AppFlowyRichTextKeys.code, + ), + ]; + + @override + Widget build(BuildContext context) { + final bius = textDecorations.map((currentDecoration) { + // Check current decoration is active or not + final selection = widget.selection; + final nodes = widget.editorState.getNodesInSelection(selection); + final bool isSelected; + if (selection.isCollapsed) { + isSelected = widget.editorState.toggledStyle.containsKey( + currentDecoration.name, + ); + } else { + isSelected = nodes.allSatisfyInSelection(selection, (delta) { + return delta.everyAttributes( + (attributes) => attributes[currentDecoration.name] == true, + ); + }); + } + + return MobileToolbarItemMenuBtn( + icon: AFMobileIcon( + afMobileIcons: currentDecoration.icon, + ), + label: FlowyText(currentDecoration.label), + isSelected: isSelected, + onPressed: () { + setState(() { + widget.editorState.toggleAttribute(currentDecoration.name); + }); + }, + ); + }).toList(); + + return GridView.count( + shrinkWrap: true, + padding: EdgeInsets.zero, + crossAxisCount: 2, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 4, + children: bius, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index c019d6a5f9..9289d44ad8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -26,9 +26,11 @@ export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; export 'math_equation/math_equation_block_component.dart'; export 'math_equation/mobile_math_equation_toolbar_item.dart'; +export 'mobile_toolbar_item/mobile_add_block_toolbar_item.dart'; export 'mobile_toolbar_item/mobile_align_toolbar_item.dart'; -export 'mobile_toolbar_item/mobile_blocks_toolbar_item.dart'; +export 'mobile_toolbar_item/mobile_convert_block_toolbar_item.dart'; export 'mobile_toolbar_item/mobile_indent_toolbar_items.dart'; +export 'mobile_toolbar_item/mobile_text_decoration_item.dart'; export 'openai/widgets/auto_completion_node_widget.dart'; export 'openai/widgets/smart_edit_node_widget.dart'; export 'openai/widgets/smart_edit_toolbar_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart index abd445dbc8..7fc162868d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/redo_mobile_toolbar_item.dart @@ -2,8 +2,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; final redoMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __) => const FlowySvg(FlowySvgs.m_redo_m), - actionHandler: (editorState, selection) async { + itemIconBuilder: (_, __, ___) => const FlowySvg( + FlowySvgs.m_redo_m, + ), + actionHandler: (_, editorState) async { editorState.undoManager.redo(); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart index bc9d27aeb7..2b22ac1ada 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/undo_mobile_toolbar_item.dart @@ -2,8 +2,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; final undoMobileToolbarItem = MobileToolbarItem.action( - itemIconBuilder: (_, __) => const FlowySvg(FlowySvgs.m_undo_m), - actionHandler: (editorState, selection) async { + itemIconBuilder: (_, __, ___) => const FlowySvg( + FlowySvgs.m_undo_m, + ), + actionHandler: (_, editorState) async { editorState.undoManager.undo(); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 8a4d16b53d..43984f176a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -83,11 +83,14 @@ class EditorStyleCustomizer { EditorStyle mobile() { final theme = Theme.of(context); - const fontSize = 14.0; - final fontFamily = GoogleFonts.poppins().fontFamily ?? 'Poppins'; + final fontSize = context.read().state.fontSize; + final fontFamily = context.read().state.fontFamily; + final defaultTextDirection = + context.read().state.defaultTextDirection; final codeFontSize = max(0.0, fontSize - 2); return EditorStyle.mobile( padding: padding, + defaultTextDirection: defaultTextDirection, textStyleConfiguration: TextStyleConfiguration( text: baseTextStyle(fontFamily).copyWith( fontSize: fontSize, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 7ccf7ffb1d..2c02f4a32c 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -3,6 +3,8 @@ import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dar import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart'; import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; +import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; +import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart'; @@ -57,6 +59,8 @@ GoRouter generateRouter(Widget child) { // code language picker _mobileCodeLanguagePickerPageRoute(), + _mobileLanguagePickerPageRoute(), + _mobileFontPickerPageRoute(), ], // Desktop and Mobile @@ -215,8 +219,12 @@ GoRoute _mobileEmojiPickerPageRoute() { parentNavigatorKey: AppGlobals.rootNavKey, path: MobileEmojiPickerScreen.routeName, pageBuilder: (context, state) { - return const MaterialPage( - child: MobileEmojiPickerScreen(), + final title = + state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle]; + return MaterialPage( + child: MobileEmojiPickerScreen( + title: title, + ), ); }, ); @@ -246,6 +254,30 @@ GoRoute _mobileCodeLanguagePickerPageRoute() { ); } +GoRoute _mobileLanguagePickerPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: LanguagePickerScreen.routeName, + pageBuilder: (context, state) { + return const MaterialPage( + child: LanguagePickerScreen(), + ); + }, + ); +} + +GoRoute _mobileFontPickerPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: FontPickerScreen.routeName, + pageBuilder: (context, state) { + return const MaterialPage( + child: FontPickerScreen(), + ); + }, + ); +} + GoRoute _desktopHomeScreenRoute() { return GoRoute( path: DesktopHomeScreen.routeName, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart index 89bc9a7bb1..aa1f6eebe2 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart @@ -5,10 +5,11 @@ import 'package:appflowy/env/env.dart'; import 'package:appflowy/user/application/supabase_realtime.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:flutter/foundation.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:url_protocol/url_protocol.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path/path.dart' as p; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:url_protocol/url_protocol.dart'; + import '../startup.dart'; // ONLY supports in macOS and Windows now. @@ -58,7 +59,9 @@ class InitSupabaseTask extends LaunchTask { @override Future dispose() async { await realtimeService?.dispose(); + realtimeService = null; supabase?.dispose(); + supabase = null; } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index c9e21e273d..176cc7c3db 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -56,6 +56,14 @@ class FlowyButton extends StatelessWidget { final alpha = (255 * disableOpacity).toInt(); color.withAlpha(alpha); + if (Platform.isIOS || Platform.isAndroid) { + return InkWell( + onTap: disable ? null : onTap, + onSecondaryTap: disable ? null : onSecondaryTap, + child: _render(context), + ); + } + return GestureDetector( behavior: HitTestBehavior.opaque, onTap: disable ? null : onTap, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 05d56cb487..8ced118ee4 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -54,8 +54,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4f073f3" - resolved-ref: "4f073f3381a05a2379144f282c6f65462c4ce9c6" + ref: "2617f7" + resolved-ref: "2617f766a5a4aa83c9f4d5c4d7221c12dbe23b66" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "2.0.0-beta.1" @@ -970,6 +970,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + keyboard_height_plugin: + dependency: transitive + description: + name: keyboard_height_plugin + sha256: "9fd5cd9e3e80d8f530aaa26ad17b4d18d34a63956cf0d530920a54c228200510" + url: "https://pub.dev" + source: hosted + version: "0.0.4" linked_scroll_controller: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index b15ccd5c04..82d528eefa 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: 4f073f3 + ref: '2617f7' appflowy_popover: path: packages/appflowy_popover diff --git a/frontend/resources/flowy_icons/16x/convert.svg b/frontend/resources/flowy_icons/16x/convert.svg new file mode 100644 index 0000000000..edb7b67e87 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/convert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/flowy_icons/24x/m_bold.svg b/frontend/resources/flowy_icons/24x/m_bold.svg new file mode 100644 index 0000000000..7f7f70b909 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_bulleted_list.svg b/frontend/resources/flowy_icons/24x/m_bulleted_list.svg new file mode 100644 index 0000000000..6aafce1fae --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_bulleted_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_checkbox.svg b/frontend/resources/flowy_icons/24x/m_checkbox.svg new file mode 100644 index 0000000000..b770aa3555 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_checkbox.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_close.svg b/frontend/resources/flowy_icons/24x/m_close.svg new file mode 100644 index 0000000000..663a83d938 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_close.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_code.svg b/frontend/resources/flowy_icons/24x/m_code.svg new file mode 100644 index 0000000000..56757ab351 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_code.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_color.svg b/frontend/resources/flowy_icons/24x/m_color.svg new file mode 100644 index 0000000000..ed4b89307b --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_color.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_divider.svg b/frontend/resources/flowy_icons/24x/m_divider.svg new file mode 100644 index 0000000000..47d0bf344f --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_divider.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_h1.svg b/frontend/resources/flowy_icons/24x/m_h1.svg new file mode 100644 index 0000000000..e303ab359d --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_h1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_h2.svg b/frontend/resources/flowy_icons/24x/m_h2.svg new file mode 100644 index 0000000000..e0b5aceb93 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_h2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_h3.svg b/frontend/resources/flowy_icons/24x/m_h3.svg new file mode 100644 index 0000000000..f6d6e0cfec --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_h3.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_heading.svg b/frontend/resources/flowy_icons/24x/m_heading.svg new file mode 100644 index 0000000000..2835ea5b93 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_heading.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_italic.svg b/frontend/resources/flowy_icons/24x/m_italic.svg new file mode 100644 index 0000000000..e8aae78a7a --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_link.svg b/frontend/resources/flowy_icons/24x/m_link.svg new file mode 100644 index 0000000000..44c837597d --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_link.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_list.svg b/frontend/resources/flowy_icons/24x/m_list.svg new file mode 100644 index 0000000000..db13ed937d --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_numbered_list.svg b/frontend/resources/flowy_icons/24x/m_numbered_list.svg new file mode 100644 index 0000000000..d846118d76 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_numbered_list.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_quote.svg b/frontend/resources/flowy_icons/24x/m_quote.svg new file mode 100644 index 0000000000..cdf0e952ba --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_quote.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_strikethrough.svg b/frontend/resources/flowy_icons/24x/m_strikethrough.svg new file mode 100644 index 0000000000..08d7663a31 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_strikethrough.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_text_decoration.svg b/frontend/resources/flowy_icons/24x/m_text_decoration.svg new file mode 100644 index 0000000000..2bcedd3fd2 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_text_decoration.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_toggle_list.svg b/frontend/resources/flowy_icons/24x/m_toggle_list.svg new file mode 100644 index 0000000000..80e82a7344 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toggle_list.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_underline.svg b/frontend/resources/flowy_icons/24x/m_underline.svg new file mode 100644 index 0000000000..78329a9fff --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_underline.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 99c026955f..6c8778d55b 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -650,6 +650,7 @@ "alertDialogConfirmation": "Are you sure, you want to continue?" }, "mathEquation": { + "name": "Math Equation", "addMathEquation": "Add a TeX equation", "editMathEquation": "Edit Math Equation" }, @@ -1064,6 +1065,7 @@ }, "titleBar": { "pageIcon": "Page icon", - "language": "Language" + "language": "Language", + "font": "Font" } } \ No newline at end of file