mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
5f4e3ecc76
commit
9565173baf
@ -29,6 +29,8 @@ class KVKeys {
|
||||
'kDocumentAppearanceFontSize';
|
||||
static const String kDocumentAppearanceFontFamily =
|
||||
'kDocumentAppearanceFontFamily';
|
||||
static const String kDocumentAppearanceDefaultTextDirection =
|
||||
'kDocumentAppearanceDefaultTextDirection';
|
||||
|
||||
/// The key for saving the expanded views
|
||||
///
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export 'brightness_setting.dart';
|
||||
export 'font_family_setting.dart';
|
||||
export 'color_scheme.dart';
|
||||
export 'direction_setting.dart';
|
||||
|
@ -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!,
|
||||
|
@ -28,6 +28,12 @@ class SettingsAppearanceView extends StatelessWidget {
|
||||
ThemeFontFamilySetting(
|
||||
currentFontFamily: state.font,
|
||||
),
|
||||
LayoutDirectionSetting(
|
||||
currentLayoutDirection: state.layoutDirection,
|
||||
),
|
||||
TextDirectionSetting(
|
||||
currentTextDirection: state.textDirection,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -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.",
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user