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';
static const String kDocumentAppearanceFontFamily =
'kDocumentAppearanceFontFamily';
static const String kDocumentAppearanceDefaultTextDirection =
'kDocumentAppearanceDefaultTextDirection';
/// 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/plugins.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_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
@ -74,6 +75,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
alignToolbarItem,
buildTextColorItem(),
buildHighlightColorItem(),
...textDirectionItems
];
late final List<SelectionMenuItem> slashMenuItems;
@ -175,13 +177,22 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
footer: const VSpace(200),
);
final layoutDirection =
context.read<AppearanceSettingsCubit>().state.layoutDirection ==
LayoutDirection.rtlLayout
? TextDirection.rtl
: TextDirection.ltr;
return Center(
child: FloatingToolbar(
style: styleCustomizer.floatingToolbarStyleBuilder(),
items: toolbarItems,
editorState: widget.editorState,
editorScrollController: editorScrollController,
child: editor,
child: Directionality(
textDirection: layoutDirection,
child: editor,
),
),
);
}

View File

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

View File

@ -6,18 +6,22 @@ class DocumentAppearance {
const DocumentAppearance({
required this.fontSize,
required this.fontFamily,
this.defaultTextDirection,
});
final double fontSize;
final String fontFamily;
final String? defaultTextDirection;
DocumentAppearance copyWith({
double? fontSize,
String? fontFamily,
String? defaultTextDirection,
}) {
return DocumentAppearance(
fontSize: fontSize ?? this.fontSize,
fontFamily: fontFamily ?? this.fontFamily,
defaultTextDirection: defaultTextDirection,
);
}
}
@ -32,6 +36,8 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0;
final fontFamily =
prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ?? 'Poppins';
final defaultTextDirection =
prefs.getString(KVKeys.kDocumentAppearanceDefaultTextDirection);
if (isClosed) {
return;
@ -41,6 +47,7 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
state.copyWith(
fontSize: fontSize,
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.font,
setting.monospaceFont,
setting.layoutDirection,
setting.textDirection,
setting.locale,
setting.isMenuCollapsed,
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
/// with the font name.
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
class AppearanceSettingsState with _$AppearanceSettingsState {
const AppearanceSettingsState._();
@ -201,6 +266,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
required ThemeMode themeMode,
required String font,
required String monospaceFont,
required LayoutDirection layoutDirection,
required AppFlowyTextDirection? textDirection,
required Locale locale,
required bool isMenuCollapsed,
required double menuOffset,
@ -211,6 +278,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
ThemeModePB themeModePB,
String font,
String monospaceFont,
LayoutDirectionPB layoutDirectionPB,
TextDirectionPB? textDirectionPB,
LocaleSettingsPB localePB,
bool isMenuCollapsed,
double menuOffset,
@ -219,6 +288,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
appTheme: appTheme,
font: font,
monospaceFont: monospaceFont,
layoutDirection: LayoutDirection.fromLayoutDirectionPB(layoutDirectionPB),
textDirection: AppFlowyTextDirection.fromTextDirectionPB(textDirectionPB),
themeMode: _themeModeFromPB(themeModePB),
locale: Locale(localePB.languageCode, localePB.countryCode),
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 'font_family_setting.dart';
export 'color_scheme.dart';
export 'direction_setting.dart';

View File

@ -10,11 +10,13 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
super.key,
this.resetButtonKey,
required this.label,
this.hint,
this.trailing,
this.onResetRequested,
});
final String label;
final String? hint;
final Key? resetButtonKey;
final List<Widget>? trailing;
final void Function()? onResetRequested;
@ -24,9 +26,23 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
return Row(
children: [
Expanded(
child: FlowyText.medium(
label,
overflow: TextOverflow.ellipsis,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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!,

View File

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

View File

@ -263,6 +263,20 @@
"dark": "Dark Mode",
"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": {
"button": "Upload",
"description": "Upload your own AppFlowy theme using the button below.",

View File

@ -50,6 +50,16 @@ pub struct AppearanceSettingsPB {
#[pb(index = 9)]
#[serde(default)]
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;
@ -62,6 +72,22 @@ pub enum ThemeModePB {
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)]
pub struct LocaleSettingsPB {
#[pb(index = 1)]
@ -99,6 +125,8 @@ impl std::default::Default for AppearanceSettingsPB {
setting_key_value: HashMap::default(),
is_menu_collapsed: APPEARANCE_DEFAULT_IS_MENU_COLLAPSED,
menu_offset: APPEARANCE_DEFAULT_MENU_OFFSET,
layout_direction: LayoutDirectionPB::default(),
text_direction: TextDirectionPB::default(),
}
}
}