feat: customized cursor and selection color (#4168)

This commit is contained in:
Yijing Huang 2023-12-20 18:34:25 -07:00 committed by GitHub
parent 3aad1c5bcd
commit bbeae74ebd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 697 additions and 31 deletions

View File

@ -24,6 +24,10 @@ class KVKeys {
'kDocumentAppearanceFontFamily';
static const String kDocumentAppearanceDefaultTextDirection =
'kDocumentAppearanceDefaultTextDirection';
static const String kDocumentAppearanceCursorColor =
'kDocumentAppearanceCursorColor';
static const String kDocumentAppearanceSelectionColor =
'kDocumentAppearanceSelectionColor';
/// The key for saving the expanded views
///

View File

@ -6,6 +6,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:collection/collection.dart';
@ -39,15 +40,18 @@ class EditorStyleCustomizer {
EditorStyle desktop() {
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;
final appearance = context.read<DocumentAppearanceCubit>().state;
final fontSize = appearance.fontSize;
final fontFamily = appearance.fontFamily;
final codeFontSize = max(0.0, fontSize - 2);
return EditorStyle.desktop(
padding: padding,
cursorColor: theme.colorScheme.primary,
defaultTextDirection: defaultTextDirection,
cursorColor: appearance.cursorColor ??
DefaultAppearanceSettings.getDefaultDocumentCursorColor(context),
selectionColor: appearance.selectionColor ??
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(context),
defaultTextDirection: appearance.defaultTextDirection,
textStyleConfiguration: TextStyleConfiguration(
text: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,

View File

@ -1,28 +1,49 @@
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class DocumentAppearance {
const DocumentAppearance({
required this.fontSize,
required this.fontFamily,
this.cursorColor,
this.selectionColor,
this.defaultTextDirection,
});
final double fontSize;
final String fontFamily;
final Color? cursorColor;
final Color? selectionColor;
final String? defaultTextDirection;
/// For nullable fields (like `cursorColor`),
/// use the corresponding `isNull` flag (like `cursorColorIsNull`) to explicitly set the field to `null`.
///
/// This is necessary because simply passing `null` as the value does not distinguish between wanting to
/// set the field to `null` and not wanting to update the field at all.
DocumentAppearance copyWith({
double? fontSize,
String? fontFamily,
Color? cursorColor,
Color? selectionColor,
String? defaultTextDirection,
bool cursorColorIsNull = false,
bool selectionColorIsNull = false,
bool textDirectionIsNull = false,
}) {
return DocumentAppearance(
fontSize: fontSize ?? this.fontSize,
fontFamily: fontFamily ?? this.fontFamily,
defaultTextDirection: defaultTextDirection,
cursorColor: cursorColorIsNull ? null : cursorColor ?? this.cursorColor,
selectionColor:
selectionColorIsNull ? null : selectionColor ?? this.selectionColor,
defaultTextDirection: textDirectionIsNull
? null
: defaultTextDirection ?? this.defaultTextDirection,
);
}
}
@ -45,6 +66,16 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
final defaultTextDirection =
prefs.getString(KVKeys.kDocumentAppearanceDefaultTextDirection);
final cursorColorString =
prefs.getString(KVKeys.kDocumentAppearanceCursorColor);
final selectionColorString =
prefs.getString(KVKeys.kDocumentAppearanceSelectionColor);
final cursorColor =
cursorColorString != null ? Color(int.parse(cursorColorString)) : null;
final selectionColor = selectionColorString != null
? Color(int.parse(selectionColorString))
: null;
if (isClosed) {
return;
}
@ -53,7 +84,12 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
state.copyWith(
fontSize: fontSize,
fontFamily: fontFamily,
cursorColor: cursorColor,
selectionColor: selectionColor,
defaultTextDirection: defaultTextDirection,
cursorColorIsNull: cursorColor == null,
selectionColorIsNull: selectionColor == null,
textDirectionIsNull: defaultTextDirection == null,
),
);
}
@ -106,6 +142,55 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
emit(
state.copyWith(
defaultTextDirection: direction,
textDirectionIsNull: direction == null,
),
);
}
Future<void> syncCursorColor(Color? cursorColor) async {
final prefs = await SharedPreferences.getInstance();
if (cursorColor == null) {
prefs.remove(KVKeys.kDocumentAppearanceCursorColor);
} else {
prefs.setString(
KVKeys.kDocumentAppearanceCursorColor,
cursorColor.toHexString(),
);
}
if (isClosed) {
return;
}
emit(
state.copyWith(
cursorColor: cursorColor,
cursorColorIsNull: cursorColor == null,
),
);
}
Future<void> syncSelectionColor(Color? selectionColor) async {
final prefs = await SharedPreferences.getInstance();
if (selectionColor == null) {
prefs.remove(KVKeys.kDocumentAppearanceSelectionColor);
} else {
prefs.setString(
KVKeys.kDocumentAppearanceSelectionColor,
selectionColor.toHexString(),
);
}
if (isClosed) {
return;
}
emit(
state.copyWith(
selectionColor: selectionColor,
selectionColorIsNull: selectionColor == null,
),
);
}

View File

@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
extension ColorExtensionn on Color {
/// return a hex string in 0xff000000 format
String toHexString() {
return '0x${value.toRadixString(16).padLeft(8, '0')}';
}
}

View File

@ -7,4 +7,12 @@ class DefaultAppearanceSettings {
static const kDefaultThemeMode = ThemeMode.system;
static const kDefaultThemeName = "Default";
static const kDefaultTheme = BuiltInTheme.defaultTheme;
static Color getDefaultDocumentCursorColor(BuildContext context) {
return Theme.of(context).colorScheme.primary;
}
static Color getDefaultDocumentSelectionColor(BuildContext context) {
return Theme.of(context).colorScheme.primary.withOpacity(0.2);
}
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy_backend/log.dart';
@ -48,6 +49,20 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
dateTimeSettings.dateFormat,
dateTimeSettings.timeFormat,
dateTimeSettings.timezoneId,
appearanceSettings.documentSetting.cursorColor.isEmpty
? null
: Color(
int.parse(
appearanceSettings.documentSetting.cursorColor,
),
),
appearanceSettings.documentSetting.selectionColor.isEmpty
? null
: Color(
int.parse(
appearanceSettings.documentSetting.selectionColor,
),
),
),
);
@ -107,6 +122,34 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
void resetFontFamily() =>
setFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
/// Update document cursor color in the apperance settings and emit an updated state.
void setDocumentCursorColor(Color color) {
_appearanceSettings.documentSetting.cursorColor = color.toHexString();
_saveAppearanceSettings();
emit(state.copyWith(documentCursorColor: color));
}
/// Reset document cursor color in the apperance settings
void resetDocumentCursorColor() {
_appearanceSettings.documentSetting.cursorColor = '';
_saveAppearanceSettings();
emit(state.copyWith(documentCursorColor: null));
}
/// Update document selection color in the apperance settings and emit an updated state.
void setDocumentSelectionColor(Color color) {
_appearanceSettings.documentSetting.selectionColor = color.toHexString();
_saveAppearanceSettings();
emit(state.copyWith(documentSelectionColor: color));
}
/// Reset document selection color in the apperance settings
void resetDocumentSelectionColor() {
_appearanceSettings.documentSetting.selectionColor = '';
_saveAppearanceSettings();
emit(state.copyWith(documentSelectionColor: null));
}
/// Updates the current locale and notify the listeners the locale was
/// changed. Fallback to [en] locale if [newLocale] is not supported.
void setLocale(BuildContext context, Locale newLocale) {
@ -308,6 +351,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
required UserDateFormatPB dateFormat,
required UserTimeFormatPB timeFormat,
required String timezoneId,
required Color? documentCursorColor,
required Color? documentSelectionColor,
}) = _AppearanceSettingsState;
factory AppearanceSettingsState.initial(
@ -323,6 +368,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
UserDateFormatPB dateFormat,
UserTimeFormatPB timeFormat,
String timezoneId,
Color? documentCursorColor,
Color? documentSelectionColor,
) {
return AppearanceSettingsState(
appTheme: appTheme,
@ -337,6 +384,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
dateFormat: dateFormat,
timeFormat: timeFormat,
timezoneId: timezoneId,
documentCursorColor: documentCursorColor,
documentSelectionColor: documentSelectionColor,
);
}

View File

@ -18,12 +18,12 @@ class BrightnessSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ThemeSettingEntryTemplateWidget(
return FlowySettingListTile(
label: LocaleKeys.settings_appearance_themeMode_label.tr(),
hint: hintText,
onResetRequested: context.read<AppearanceSettingsCubit>().resetThemeMode,
trailing: [
ThemeValueDropDown(
FlowySettingValueDropDown(
currentValue: currentThemeMode.labelText,
popupBuilder: (context) => Column(
mainAxisSize: MainAxisSize.min,

View File

@ -27,7 +27,7 @@ class ColorSchemeSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ThemeSettingEntryTemplateWidget(
return FlowySettingListTile(
label: LocaleKeys.settings_appearance_theme.tr(),
onResetRequested: context.read<AppearanceSettingsCubit>().resetTheme,
trailing: [

View File

@ -16,7 +16,7 @@ class CreateFileSettings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ThemeSettingEntryTemplateWidget(
return FlowySettingListTile(
label:
LocaleKeys.settings_appearance_showNamingDialogWhenCreatingPage.tr(),
trailing: [

View File

@ -18,10 +18,10 @@ class DateFormatSetting extends StatelessWidget {
final UserDateFormatPB currentFormat;
@override
Widget build(BuildContext context) => ThemeSettingEntryTemplateWidget(
Widget build(BuildContext context) => FlowySettingListTile(
label: LocaleKeys.settings_appearance_dateFormat_label.tr(),
trailing: [
ThemeValueDropDown(
FlowySettingValueDropDown(
currentValue: _formatLabel(currentFormat),
popupBuilder: (_) => Column(
mainAxisSize: MainAxisSize.min,

View File

@ -20,11 +20,11 @@ class LayoutDirectionSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ThemeSettingEntryTemplateWidget(
return FlowySettingListTile(
label: LocaleKeys.settings_appearance_layoutDirection_label.tr(),
hint: LocaleKeys.settings_appearance_layoutDirection_hint.tr(),
trailing: [
ThemeValueDropDown(
FlowySettingValueDropDown(
key: const ValueKey('layout_direction_option_button'),
currentValue: _layoutDirectionLabelText(currentLayoutDirection),
popupBuilder: (context) => Column(
@ -83,11 +83,11 @@ class TextDirectionSetting extends StatelessWidget {
final AppFlowyTextDirection? currentTextDirection;
@override
Widget build(BuildContext context) => ThemeSettingEntryTemplateWidget(
Widget build(BuildContext context) => FlowySettingListTile(
label: LocaleKeys.settings_appearance_textDirection_label.tr(),
hint: LocaleKeys.settings_appearance_textDirection_hint.tr(),
trailing: [
ThemeValueDropDown(
FlowySettingValueDropDown(
currentValue: _textDirectionLabelText(currentTextDirection),
popupBuilder: (context) => Column(
mainAxisSize: MainAxisSize.min,

View File

@ -0,0 +1,266 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
class DocumentColorSettingButton extends StatelessWidget {
const DocumentColorSettingButton({
super.key,
required this.currentColor,
required this.previewWidgetBuilder,
required this.dialogTitle,
required this.onApply,
});
/// current color from backend
final Color currentColor;
/// Build a preview widget with the given color
/// It shows both on the [DocumentColorSettingButton] and [_DocumentColorSettingDialog]
final Widget Function(Color? color) previewWidgetBuilder;
final String dialogTitle;
final void Function(Color selectedColorOnDialog) onApply;
@override
Widget build(BuildContext context) {
return FlowyButton(
margin: const EdgeInsets.all(8),
text: previewWidgetBuilder.call(currentColor),
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
expandText: false,
onTap: () => Dialogs.show(
context,
child: _DocumentColorSettingDialog(
currentColor: currentColor,
previewWidgetBuilder: previewWidgetBuilder,
dialogTitle: dialogTitle,
onApply: onApply,
),
),
);
}
}
class _DocumentColorSettingDialog extends StatefulWidget {
const _DocumentColorSettingDialog({
required this.currentColor,
required this.previewWidgetBuilder,
required this.dialogTitle,
required this.onApply,
});
final Color currentColor;
final Widget Function(Color?) previewWidgetBuilder;
final String dialogTitle;
final void Function(Color selectedColorOnDialog) onApply;
@override
State<_DocumentColorSettingDialog> createState() =>
DocumentColorSettingDialogState();
}
class DocumentColorSettingDialogState
extends State<_DocumentColorSettingDialog> {
/// The color displayed in the dialog.
/// It is `null` when the user didn't enter a valid color value.
late Color? selectedColorOnDialog;
late String currentColorHexString;
late TextEditingController hexController;
late TextEditingController opacityController;
final _formKey = GlobalKey<FormState>(debugLabel: 'colorSettingForm');
void updateSelectedColor() {
if (_formKey.currentState!.validate()) {
setState(() {
final colorValue = int.tryParse(
hexController.text.combineHexWithOpacity(opacityController.text),
);
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
selectedColorOnDialog = Color(colorValue!);
});
}
}
@override
void initState() {
super.initState();
selectedColorOnDialog = widget.currentColor;
currentColorHexString = widget.currentColor.toHexString();
hexController = TextEditingController(
text: currentColorHexString.extractHex(),
);
opacityController = TextEditingController(
text: currentColorHexString.extractOpacity(),
);
}
@override
void dispose() {
hexController.dispose();
opacityController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FlowyDialog(
constraints: const BoxConstraints(maxWidth: 360, maxHeight: 320),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const Spacer(),
FlowyText(widget.dialogTitle),
const VSpace(8),
SizedBox(
width: 100,
height: 40,
child: Center(
child: widget.previewWidgetBuilder(
selectedColorOnDialog,
),
),
),
const VSpace(8),
SizedBox(
height: 160,
child: Form(
key: _formKey,
child: Column(
children: [
_ColorSettingTextField(
controller: hexController,
labelText: LocaleKeys.editor_hexValue.tr(),
hintText: '6fc9e7',
onFieldSubmitted: (_) => updateSelectedColor(),
validator: (hexValue) => validateHexValue(
hexValue,
opacityController.text,
),
),
const VSpace(8),
_ColorSettingTextField(
controller: opacityController,
labelText: LocaleKeys.editor_opacity.tr(),
hintText: '50',
onFieldSubmitted: (_) => updateSelectedColor(),
validator: (value) => validateOpacityValue(value),
),
],
),
),
),
const VSpace(8),
RoundedTextButton(
title: LocaleKeys.settings_appearance_documentSettings_apply.tr(),
width: 100,
height: 30,
onPressed: () {
if (_formKey.currentState!.validate()) {
if (selectedColorOnDialog != null &&
selectedColorOnDialog != widget.currentColor) {
widget.onApply.call(selectedColorOnDialog!);
}
} else {
// error message will be shown below the text field
return;
}
Navigator.of(context).pop();
},
),
],
),
),
);
}
}
class _ColorSettingTextField extends StatelessWidget {
const _ColorSettingTextField({
required this.controller,
required this.labelText,
required this.hintText,
required this.onFieldSubmitted,
required this.validator,
});
final TextEditingController controller;
final String labelText;
final String hintText;
final void Function(String) onFieldSubmitted;
final String? Function(String?)? validator;
@override
Widget build(BuildContext context) {
final style = Theme.of(context);
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
border: OutlineInputBorder(
borderSide: BorderSide(
color: style.colorScheme.outline,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: style.colorScheme.outline,
),
),
),
style: style.textTheme.bodyMedium,
onFieldSubmitted: onFieldSubmitted,
validator: validator,
autovalidateMode: AutovalidateMode.onUserInteraction,
);
}
}
String? validateHexValue(
String? hexValue,
String opacityValue,
) {
if (hexValue == null || hexValue.isEmpty) {
return LocaleKeys.settings_appearance_documentSettings_hexEmptyError.tr();
}
if (hexValue.length != 6) {
return LocaleKeys.settings_appearance_documentSettings_hexLengthError.tr();
}
if (validateOpacityValue(opacityValue) == null) {
final colorValue =
int.tryParse(hexValue.combineHexWithOpacity(opacityValue));
if (colorValue == null) {
return LocaleKeys.settings_appearance_documentSettings_hexInvalidError
.tr();
}
}
return null;
}
String? validateOpacityValue(String? value) {
if (value == null || value.isEmpty) {
return LocaleKeys.settings_appearance_documentSettings_opacityEmptyError
.tr();
}
final opacityInt = int.tryParse(value);
if (opacityInt == null || opacityInt > 100 || opacityInt <= 0) {
return LocaleKeys.settings_appearance_documentSettings_opacityRangeError
.tr();
}
return null;
}

View File

@ -0,0 +1,82 @@
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_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/document_color_setting_button.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.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';
class DocumentCursorColorSetting extends StatelessWidget {
const DocumentCursorColorSetting({
super.key,
required this.currentCursorColor,
});
final Color currentCursorColor;
@override
Widget build(BuildContext context) {
final label =
LocaleKeys.settings_appearance_documentSettings_cursorColor.tr();
return FlowySettingListTile(
label: label,
resetButtonKey: const Key('DocumentCursorColorResetButton'),
onResetRequested: () {
context.read<AppearanceSettingsCubit>().resetDocumentCursorColor();
context.read<DocumentAppearanceCubit>().syncCursorColor(null);
},
trailing: [
DocumentColorSettingButton(
key: const Key('DocumentCursorColorSettingButton'),
currentColor: currentCursorColor,
previewWidgetBuilder: (color) => _CursorColorValueWidget(
cursorColor: color ??
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
context,
),
),
dialogTitle: label,
onApply: (selectedColorOnDialog) {
context
.read<AppearanceSettingsCubit>()
.setDocumentCursorColor(selectedColorOnDialog);
// update the state of document appearance cubit with latest cursor color
context
.read<DocumentAppearanceCubit>()
.syncCursorColor(selectedColorOnDialog);
},
),
],
);
}
}
class _CursorColorValueWidget extends StatelessWidget {
const _CursorColorValueWidget({
required this.cursorColor,
});
final Color cursorColor;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: cursorColor,
width: 2,
height: 16,
),
FlowyText(
LocaleKeys.appName.tr(),
// To avoid the text color changes when it is hovered in dark mode
color: Theme.of(context).colorScheme.onBackground,
),
],
);
}
}

View File

@ -0,0 +1,85 @@
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_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/document_color_setting_button.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.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';
class DocumentSelectionColorSetting extends StatelessWidget {
const DocumentSelectionColorSetting({
super.key,
required this.currentSelectionColor,
});
final Color currentSelectionColor;
@override
Widget build(BuildContext context) {
final label =
LocaleKeys.settings_appearance_documentSettings_selectionColor.tr();
return FlowySettingListTile(
label: label,
resetButtonKey: const Key('DocumentSelectionColorResetButton'),
onResetRequested: () {
context.read<AppearanceSettingsCubit>().resetDocumentSelectionColor();
context.read<DocumentAppearanceCubit>().syncSelectionColor(null);
},
trailing: [
DocumentColorSettingButton(
currentColor: currentSelectionColor,
previewWidgetBuilder: (color) => _SelectionColorValueWidget(
selectionColor: color ??
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(
context,
),
),
dialogTitle: label,
onApply: (selectedColorOnDialog) {
context
.read<AppearanceSettingsCubit>()
.setDocumentSelectionColor(selectedColorOnDialog);
// update the state of document appearance cubit with latest selection color
context
.read<DocumentAppearanceCubit>()
.syncSelectionColor(selectedColorOnDialog);
},
),
],
);
}
}
class _SelectionColorValueWidget extends StatelessWidget {
const _SelectionColorValueWidget({
required this.selectionColor,
});
final Color selectionColor;
@override
Widget build(BuildContext context) {
// To avoid the text color changes when it is hovered in dark mode
final textColor = Theme.of(context).colorScheme.onBackground;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: selectionColor,
child: FlowyText(
LocaleKeys.settings_appearance_documentSettings_app.tr(),
color: textColor,
),
),
FlowyText(
LocaleKeys.settings_appearance_documentSettings_flowy.tr(),
color: textColor,
),
],
);
}
}

View File

@ -33,7 +33,7 @@ class ThemeFontFamilySetting extends StatefulWidget {
class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
@override
Widget build(BuildContext context) {
return ThemeSettingEntryTemplateWidget(
return FlowySettingListTile(
label: LocaleKeys.settings_appearance_fontFamily_label.tr(),
resetButtonKey: ThemeFontFamilySetting.resetButtonkey,
onResetRequested: () {
@ -91,7 +91,7 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
@override
Widget build(BuildContext context) {
return ThemeValueDropDown(
return FlowySettingValueDropDown(
popoverKey: ThemeFontFamilySetting.popoverKey,
popoverController: widget.popoverController,
currentValue: parseFontFamilyName(widget.currentFontFamily),

View File

@ -2,3 +2,5 @@ export 'brightness_setting.dart';
export 'font_family_setting.dart';
export 'color_scheme.dart';
export 'direction_setting.dart';
export 'document_cursor_color_setting.dart';
export 'document_selection_color_setting.dart';

View File

@ -5,9 +5,10 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class ThemeSettingEntryTemplateWidget extends StatelessWidget {
const ThemeSettingEntryTemplateWidget({
class FlowySettingListTile extends StatelessWidget {
const FlowySettingListTile({
super.key,
this.resetTooltipText,
this.resetButtonKey,
required this.label,
this.hint,
@ -17,6 +18,7 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
final String label;
final String? hint;
final String? resetTooltipText;
final Key? resetButtonKey;
final List<Widget>? trailing;
final void Function()? onResetRequested;
@ -57,7 +59,8 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
color: Theme.of(context).iconTheme.color,
),
iconColorOnHover: Theme.of(context).colorScheme.onPrimary,
tooltipText: LocaleKeys.settings_appearance_resetSetting.tr(),
tooltipText: resetTooltipText ??
LocaleKeys.settings_appearance_resetSetting.tr(),
onPressed: onResetRequested,
),
],
@ -65,8 +68,8 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
}
}
class ThemeValueDropDown extends StatefulWidget {
const ThemeValueDropDown({
class FlowySettingValueDropDown extends StatefulWidget {
const FlowySettingValueDropDown({
super.key,
required this.currentValue,
required this.popupBuilder,
@ -86,10 +89,11 @@ class ThemeValueDropDown extends StatefulWidget {
final Offset? offset;
@override
State<ThemeValueDropDown> createState() => _ThemeValueDropDownState();
State<FlowySettingValueDropDown> createState() =>
_FlowySettingValueDropDownState();
}
class _ThemeValueDropDownState extends State<ThemeValueDropDown> {
class _FlowySettingValueDropDownState extends State<FlowySettingValueDropDown> {
@override
Widget build(BuildContext context) {
return AppFlowyPopover(

View File

@ -18,10 +18,10 @@ class TimeFormatSetting extends StatelessWidget {
final UserTimeFormatPB currentFormat;
@override
Widget build(BuildContext context) => ThemeSettingEntryTemplateWidget(
Widget build(BuildContext context) => FlowySettingListTile(
label: LocaleKeys.settings_appearance_timeFormat_label.tr(),
trailing: [
ThemeValueDropDown(
FlowySettingValueDropDown(
currentValue: _formatLabel(currentFormat),
popupBuilder: (_) => Column(
mainAxisSize: MainAxisSize.min,

View File

@ -1,3 +1,4 @@
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/create_file_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/date_format_setting.dart';
@ -33,6 +34,20 @@ class SettingsAppearanceView extends StatelessWidget {
currentFontFamily: state.font,
),
const Divider(),
DocumentCursorColorSetting(
currentCursorColor: state.documentCursorColor ??
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
context,
),
),
DocumentSelectionColorSetting(
currentSelectionColor: state.documentSelectionColor ??
DefaultAppearanceSettings
.getDefaultDocumentSelectionColor(
context,
),
),
const Divider(),
LayoutDirectionSetting(
currentLayoutDirection: state.layoutDirection,
),

View File

@ -17,7 +17,7 @@ class SettingsNotificationsView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
ThemeSettingEntryTemplateWidget(
FlowySettingListTile(
label: LocaleKeys
.settings_notifications_enableNotifications_label
.tr(),

View File

@ -0,0 +1,19 @@
extension HexOpacityExtension on String {
/// Only used in a valid color String like '0xff00bcf0'
String extractHex() {
return substring(4);
}
/// Only used in a valid color String like '0xff00bcf0'
String extractOpacity() {
final opacityString = substring(2, 4);
final opacityInt = int.parse(opacityString, radix: 16) / 2.55;
return opacityInt.toStringAsFixed(0);
}
/// Apply on the hex string like '00bcf0', with opacity like '100'
String combineHexWithOpacity(String opacity) {
final opacityInt = (int.parse(opacity) * 2.55).round().toRadixString(16);
return '0x$opacityInt$this';
}
}

View File

@ -85,7 +85,7 @@ class DefaultColorScheme extends FlowyColorScheme {
shader1: _darkShader1,
shader2: _darkShader2,
shader3: _darkShader3,
shader4: const Color(0xff7C8CA5),
shader4: const Color(0xff505469),
shader5: _darkShader5,
shader6: _darkShader6,
shader7: _white,

View File

@ -308,7 +308,7 @@
}
},
"appearance": {
"resetSetting": "Reset this setting",
"resetSetting": "Reset",
"fontFamily": {
"label": "Font Family",
"search": "Search"
@ -319,6 +319,18 @@
"dark": "Dark Mode",
"system": "Adapt to System"
},
"documentSettings": {
"cursorColor": "Document cursor color",
"selectionColor": "Document selection color",
"hexEmptyError": "Hex color cannot be empty",
"hexLengthError": "Hex value must be 6 digits long",
"hexInvalidError": "Invalid hex value",
"opacityEmptyError": "Opacity cannot be empty",
"opacityRangeError": "Opacity must be between 1 and 100",
"app": "App",
"flowy": "Flowy",
"apply": "Apply"
},
"layoutDirection": {
"label": "Layout Direction",
"hint": "Control the flow of content on your screen, from left to right or right to left.",

View File

@ -64,6 +64,10 @@ pub struct AppearanceSettingsPB {
#[pb(index = 11)]
#[serde(default)]
pub text_direction: TextDirectionPB,
#[pb(index = 12)]
#[serde(default)]
pub document_setting: DocumentSettingsPB,
}
const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT;
@ -110,6 +114,24 @@ impl std::default::Default for LocaleSettingsPB {
}
}
#[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)]
pub struct DocumentSettingsPB {
#[pb(index = 1, one_of)]
pub cursor_color: Option<String>,
#[pb(index = 2, one_of)]
pub selection_color: Option<String>,
}
impl std::default::Default for DocumentSettingsPB {
fn default() -> Self {
Self {
cursor_color: None,
selection_color: None,
}
}
}
pub const APPEARANCE_DEFAULT_THEME: &str = "Default";
pub const APPEARANCE_DEFAULT_FONT: &str = "Poppins";
pub const APPEARANCE_DEFAULT_MONOSPACE_FONT: &str = "SF Mono";
@ -131,6 +153,7 @@ impl std::default::Default for AppearanceSettingsPB {
menu_offset: APPEARANCE_DEFAULT_MENU_OFFSET,
layout_direction: LayoutDirectionPB::default(),
text_direction: TextDirectionPB::default(),
document_setting: DocumentSettingsPB::default(),
}
}
}