feat: text and layout direction settings (#3247)

* feat: text and layout direction settings

Added ltr|rtl|auto direction button to appflowy toolbar.
Introduced layout and default direction settings.

* chore: formate code

* feat: added hint for direction settings

* fix: flutter analyze

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Mohammad Zolfaghari 2023-09-12 14:41:13 +03:30 committed by GitHub
parent 5f4e3ecc76
commit 9565173baf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 325 additions and 4 deletions

View File

@ -29,6 +29,8 @@ class KVKeys {
'kDocumentAppearanceFontSize'; 'kDocumentAppearanceFontSize';
static const String kDocumentAppearanceFontFamily = static const String kDocumentAppearanceFontFamily =
'kDocumentAppearanceFontFamily'; 'kDocumentAppearanceFontFamily';
static const String kDocumentAppearanceDefaultTextDirection =
'kDocumentAppearanceDefaultTextDirection';
/// The key for saving the expanded views /// The key for saving the expanded views
/// ///

View File

@ -2,6 +2,7 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_page/inline_page_reference.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -74,6 +75,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
alignToolbarItem, alignToolbarItem,
buildTextColorItem(), buildTextColorItem(),
buildHighlightColorItem(), buildHighlightColorItem(),
...textDirectionItems
]; ];
late final List<SelectionMenuItem> slashMenuItems; late final List<SelectionMenuItem> slashMenuItems;
@ -175,13 +177,22 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
footer: const VSpace(200), footer: const VSpace(200),
); );
final layoutDirection =
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
LayoutDirection.rtlLayout
? TextDirection.rtl
: TextDirection.ltr;
return Center( return Center(
child: FloatingToolbar( child: FloatingToolbar(
style: styleCustomizer.floatingToolbarStyleBuilder(), style: styleCustomizer.floatingToolbarStyleBuilder(),
items: toolbarItems, items: toolbarItems,
editorState: widget.editorState, editorState: widget.editorState,
editorScrollController: editorScrollController, editorScrollController: editorScrollController,
child: editor, child: Directionality(
textDirection: layoutDirection,
child: editor,
),
), ),
); );
} }

View File

@ -30,9 +30,13 @@ class EditorStyleCustomizer {
final theme = Theme.of(context); final theme = Theme.of(context);
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize; final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily; final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
final defaultTextDirection =
context.read<DocumentAppearanceCubit>().state.defaultTextDirection;
return EditorStyle.desktop( return EditorStyle.desktop(
padding: padding, padding: padding,
cursorColor: theme.colorScheme.primary, cursorColor: theme.colorScheme.primary,
defaultTextDirection: defaultTextDirection,
textStyleConfiguration: TextStyleConfiguration( textStyleConfiguration: TextStyleConfiguration(
text: baseTextStyle(fontFamily).copyWith( text: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize, fontSize: fontSize,

View File

@ -6,18 +6,22 @@ class DocumentAppearance {
const DocumentAppearance({ const DocumentAppearance({
required this.fontSize, required this.fontSize,
required this.fontFamily, required this.fontFamily,
this.defaultTextDirection,
}); });
final double fontSize; final double fontSize;
final String fontFamily; final String fontFamily;
final String? defaultTextDirection;
DocumentAppearance copyWith({ DocumentAppearance copyWith({
double? fontSize, double? fontSize,
String? fontFamily, String? fontFamily,
String? defaultTextDirection,
}) { }) {
return DocumentAppearance( return DocumentAppearance(
fontSize: fontSize ?? this.fontSize, fontSize: fontSize ?? this.fontSize,
fontFamily: fontFamily ?? this.fontFamily, fontFamily: fontFamily ?? this.fontFamily,
defaultTextDirection: defaultTextDirection,
); );
} }
} }
@ -32,6 +36,8 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0; prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0;
final fontFamily = final fontFamily =
prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ?? 'Poppins'; prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ?? 'Poppins';
final defaultTextDirection =
prefs.getString(KVKeys.kDocumentAppearanceDefaultTextDirection);
if (isClosed) { if (isClosed) {
return; return;
@ -41,6 +47,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
state.copyWith( state.copyWith(
fontSize: fontSize, fontSize: fontSize,
fontFamily: fontFamily, fontFamily: fontFamily,
defaultTextDirection: defaultTextDirection,
), ),
); );
} }
@ -74,4 +81,26 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
), ),
); );
} }
Future<void> syncDefaultTextDirection(String? direction) async {
final prefs = await SharedPreferences.getInstance();
if (direction == null) {
prefs.remove(KVKeys.kDocumentAppearanceDefaultTextDirection);
} else {
prefs.setString(
KVKeys.kDocumentAppearanceDefaultTextDirection,
direction,
);
}
if (isClosed) {
return;
}
emit(
state.copyWith(
defaultTextDirection: direction,
),
);
}
} }

View File

@ -34,6 +34,8 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
setting.themeMode, setting.themeMode,
setting.font, setting.font,
setting.monospaceFont, setting.monospaceFont,
setting.layoutDirection,
setting.textDirection,
setting.locale, setting.locale,
setting.isMenuCollapsed, setting.isMenuCollapsed,
setting.menuOffset, setting.menuOffset,
@ -71,6 +73,19 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
); );
} }
void setLayoutDirection(LayoutDirection layoutDirection) {
_setting.layoutDirection = layoutDirection.toLayoutDirectionPB();
_saveAppearanceSettings();
emit(state.copyWith(layoutDirection: layoutDirection));
}
void setTextDirection(AppFlowyTextDirection? textDirection) {
_setting.textDirection =
textDirection?.toTextDirectionPB() ?? TextDirectionPB.FALLBACK;
_saveAppearanceSettings();
emit(state.copyWith(textDirection: textDirection));
}
/// Update selected font in the user's settings and emit an updated state /// Update selected font in the user's settings and emit an updated state
/// with the font name. /// with the font name.
void setFontFamily(String fontFamilyName) { void setFontFamily(String fontFamilyName) {
@ -192,6 +207,56 @@ ThemeModePB _themeModeToPB(ThemeMode themeMode) {
} }
} }
enum LayoutDirection {
ltrLayout,
rtlLayout;
static LayoutDirection fromLayoutDirectionPB(
LayoutDirectionPB layoutDirectionPB,
) =>
layoutDirectionPB == LayoutDirectionPB.RTLLayout
? LayoutDirection.rtlLayout
: LayoutDirection.ltrLayout;
LayoutDirectionPB toLayoutDirectionPB() => this == LayoutDirection.rtlLayout
? LayoutDirectionPB.RTLLayout
: LayoutDirectionPB.LTRLayout;
}
enum AppFlowyTextDirection {
ltr,
rtl,
auto;
static AppFlowyTextDirection? fromTextDirectionPB(
TextDirectionPB? textDirectionPB,
) {
switch (textDirectionPB) {
case TextDirectionPB.LTR:
return AppFlowyTextDirection.ltr;
case TextDirectionPB.RTL:
return AppFlowyTextDirection.rtl;
case TextDirectionPB.AUTO:
return AppFlowyTextDirection.auto;
default:
return null;
}
}
TextDirectionPB toTextDirectionPB() {
switch (this) {
case AppFlowyTextDirection.ltr:
return TextDirectionPB.LTR;
case AppFlowyTextDirection.rtl:
return TextDirectionPB.RTL;
case AppFlowyTextDirection.auto:
return TextDirectionPB.AUTO;
default:
return TextDirectionPB.FALLBACK;
}
}
}
@freezed @freezed
class AppearanceSettingsState with _$AppearanceSettingsState { class AppearanceSettingsState with _$AppearanceSettingsState {
const AppearanceSettingsState._(); const AppearanceSettingsState._();
@ -201,6 +266,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
required ThemeMode themeMode, required ThemeMode themeMode,
required String font, required String font,
required String monospaceFont, required String monospaceFont,
required LayoutDirection layoutDirection,
required AppFlowyTextDirection? textDirection,
required Locale locale, required Locale locale,
required bool isMenuCollapsed, required bool isMenuCollapsed,
required double menuOffset, required double menuOffset,
@ -211,6 +278,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
ThemeModePB themeModePB, ThemeModePB themeModePB,
String font, String font,
String monospaceFont, String monospaceFont,
LayoutDirectionPB layoutDirectionPB,
TextDirectionPB? textDirectionPB,
LocaleSettingsPB localePB, LocaleSettingsPB localePB,
bool isMenuCollapsed, bool isMenuCollapsed,
double menuOffset, double menuOffset,
@ -219,6 +288,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
appTheme: appTheme, appTheme: appTheme,
font: font, font: font,
monospaceFont: monospaceFont, monospaceFont: monospaceFont,
layoutDirection: LayoutDirection.fromLayoutDirectionPB(layoutDirectionPB),
textDirection: AppFlowyTextDirection.fromTextDirectionPB(textDirectionPB),
themeMode: _themeModeFromPB(themeModePB), themeMode: _themeModeFromPB(themeModePB),
locale: Locale(localePB.languageCode, localePB.countryCode), locale: Locale(localePB.languageCode, localePB.countryCode),
isMenuCollapsed: isMenuCollapsed, isMenuCollapsed: isMenuCollapsed,

View File

@ -0,0 +1,139 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/appearance.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 'theme_setting_entry_template.dart';
class LayoutDirectionSetting extends StatelessWidget {
const LayoutDirectionSetting({
super.key,
required this.currentLayoutDirection,
});
final LayoutDirection currentLayoutDirection;
@override
Widget build(BuildContext context) {
return ThemeSettingEntryTemplateWidget(
label: LocaleKeys.settings_appearance_layoutDirection_label.tr(),
hint: LocaleKeys.settings_appearance_layoutDirection_hint.tr(),
trailing: [
ThemeValueDropDown(
currentValue: _layoutDirectionLabelText(currentLayoutDirection),
popupBuilder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [
_layoutDirectionItemButton(context, LayoutDirection.ltrLayout),
_layoutDirectionItemButton(context, LayoutDirection.rtlLayout),
],
),
)
],
);
}
Widget _layoutDirectionItemButton(
BuildContext context,
LayoutDirection direction,
) {
return SizedBox(
height: 32,
child: FlowyButton(
text: FlowyText.medium(_layoutDirectionLabelText(direction)),
rightIcon: currentLayoutDirection == direction
? const FlowySvg(FlowySvgs.check_s)
: null,
onTap: () {
if (currentLayoutDirection != direction) {
context
.read<AppearanceSettingsCubit>()
.setLayoutDirection(direction);
}
},
),
);
}
String _layoutDirectionLabelText(LayoutDirection direction) {
switch (direction) {
case (LayoutDirection.ltrLayout):
return LocaleKeys.settings_appearance_layoutDirection_ltr.tr();
case (LayoutDirection.rtlLayout):
return LocaleKeys.settings_appearance_layoutDirection_rtl.tr();
default:
return '';
}
}
}
class TextDirectionSetting extends StatelessWidget {
const TextDirectionSetting({
super.key,
required this.currentTextDirection,
});
final AppFlowyTextDirection? currentTextDirection;
@override
Widget build(BuildContext context) => ThemeSettingEntryTemplateWidget(
label: LocaleKeys.settings_appearance_textDirection_label.tr(),
hint: LocaleKeys.settings_appearance_textDirection_hint.tr(),
trailing: [
ThemeValueDropDown(
currentValue: _textDirectionLabelText(currentTextDirection),
popupBuilder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [
_textDirectionItemButton(context, null),
_textDirectionItemButton(context, AppFlowyTextDirection.ltr),
_textDirectionItemButton(context, AppFlowyTextDirection.rtl),
_textDirectionItemButton(context, AppFlowyTextDirection.auto),
],
),
)
],
);
Widget _textDirectionItemButton(
BuildContext context,
AppFlowyTextDirection? textDirection,
) {
return SizedBox(
height: 32,
child: FlowyButton(
text: FlowyText.medium(_textDirectionLabelText(textDirection)),
rightIcon: currentTextDirection == textDirection
? const FlowySvg(FlowySvgs.check_s)
: null,
onTap: () {
if (currentTextDirection != textDirection) {
context
.read<AppearanceSettingsCubit>()
.setTextDirection(textDirection);
context
.read<DocumentAppearanceCubit>()
.syncDefaultTextDirection(textDirection?.name);
}
},
),
);
}
String _textDirectionLabelText(AppFlowyTextDirection? textDirection) {
switch (textDirection) {
case (AppFlowyTextDirection.ltr):
return LocaleKeys.settings_appearance_textDirection_ltr.tr();
case (AppFlowyTextDirection.rtl):
return LocaleKeys.settings_appearance_textDirection_rtl.tr();
case (AppFlowyTextDirection.auto):
return LocaleKeys.settings_appearance_textDirection_auto.tr();
default:
return LocaleKeys.settings_appearance_textDirection_fallback.tr();
}
}
}

View File

@ -1,3 +1,4 @@
export 'brightness_setting.dart'; export 'brightness_setting.dart';
export 'font_family_setting.dart'; export 'font_family_setting.dart';
export 'color_scheme.dart'; export 'color_scheme.dart';
export 'direction_setting.dart';

View File

@ -10,11 +10,13 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
super.key, super.key,
this.resetButtonKey, this.resetButtonKey,
required this.label, required this.label,
this.hint,
this.trailing, this.trailing,
this.onResetRequested, this.onResetRequested,
}); });
final String label; final String label;
final String? hint;
final Key? resetButtonKey; final Key? resetButtonKey;
final List<Widget>? trailing; final List<Widget>? trailing;
final void Function()? onResetRequested; final void Function()? onResetRequested;
@ -24,9 +26,23 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
return Row( return Row(
children: [ children: [
Expanded( Expanded(
child: FlowyText.medium( child: Column(
label, crossAxisAlignment: CrossAxisAlignment.start,
overflow: TextOverflow.ellipsis, children: [
FlowyText.medium(
label,
overflow: TextOverflow.ellipsis,
),
if (hint != null)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: FlowyText.regular(
hint!,
fontSize: 10,
color: Theme.of(context).hintColor,
),
),
],
), ),
), ),
if (trailing != null) ...trailing!, if (trailing != null) ...trailing!,

View File

@ -28,6 +28,12 @@ class SettingsAppearanceView extends StatelessWidget {
ThemeFontFamilySetting( ThemeFontFamilySetting(
currentFontFamily: state.font, currentFontFamily: state.font,
), ),
LayoutDirectionSetting(
currentLayoutDirection: state.layoutDirection,
),
TextDirectionSetting(
currentTextDirection: state.textDirection,
),
], ],
); );
}, },

View File

@ -263,6 +263,20 @@
"dark": "Dark Mode", "dark": "Dark Mode",
"system": "Adapt to System" "system": "Adapt to System"
}, },
"layoutDirection": {
"label": "Layout Direction",
"hint": "To start aligning elements from left or right of the screen.",
"ltr": "LTR",
"rtl": "RTL"
},
"textDirection": {
"label": "Default text direction",
"hint": "Default text direction when the text direction is not set on the element.",
"ltr": "LTR",
"rtl": "RTL",
"auto": "AUTO",
"fallback": "Same as layout direction"
},
"themeUpload": { "themeUpload": {
"button": "Upload", "button": "Upload",
"description": "Upload your own AppFlowy theme using the button below.", "description": "Upload your own AppFlowy theme using the button below.",

View File

@ -50,6 +50,16 @@ pub struct AppearanceSettingsPB {
#[pb(index = 9)] #[pb(index = 9)]
#[serde(default)] #[serde(default)]
pub menu_offset: f64, pub menu_offset: f64,
#[pb(index = 10)]
#[serde(default)]
pub layout_direction: LayoutDirectionPB,
// If the value is FALLBACK which is the default value then it will fall back
// to layout direction and it will use that as default text direction.
#[pb(index = 11)]
#[serde(default)]
pub text_direction: TextDirectionPB,
} }
const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT; const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT;
@ -62,6 +72,22 @@ pub enum ThemeModePB {
System = 2, System = 2,
} }
#[derive(ProtoBuf_Enum, Serialize, Deserialize, Clone, Debug, Default)]
pub enum LayoutDirectionPB {
#[default]
LTRLayout = 0,
RTLLayout = 1,
}
#[derive(ProtoBuf_Enum, Serialize, Deserialize, Clone, Debug, Default)]
pub enum TextDirectionPB {
LTR = 0,
RTL = 1,
AUTO = 2,
#[default]
FALLBACK = 3,
}
#[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)] #[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)]
pub struct LocaleSettingsPB { pub struct LocaleSettingsPB {
#[pb(index = 1)] #[pb(index = 1)]
@ -99,6 +125,8 @@ impl std::default::Default for AppearanceSettingsPB {
setting_key_value: HashMap::default(), setting_key_value: HashMap::default(),
is_menu_collapsed: APPEARANCE_DEFAULT_IS_MENU_COLLAPSED, is_menu_collapsed: APPEARANCE_DEFAULT_IS_MENU_COLLAPSED,
menu_offset: APPEARANCE_DEFAULT_MENU_OFFSET, menu_offset: APPEARANCE_DEFAULT_MENU_OFFSET,
layout_direction: LayoutDirectionPB::default(),
text_direction: TextDirectionPB::default(),
} }
} }
} }