mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: customized cursor and selection color (#4168)
This commit is contained in:
parent
3aad1c5bcd
commit
bbeae74ebd
@ -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
|
||||
///
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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')}';
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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: [
|
||||
|
@ -16,7 +16,7 @@ class CreateFileSettings extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ThemeSettingEntryTemplateWidget(
|
||||
return FlowySettingListTile(
|
||||
label:
|
||||
LocaleKeys.settings_appearance_showNamingDialogWhenCreatingPage.tr(),
|
||||
trailing: [
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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';
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -17,7 +17,7 @@ class SettingsNotificationsView extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ThemeSettingEntryTemplateWidget(
|
||||
FlowySettingListTile(
|
||||
label: LocaleKeys
|
||||
.settings_notifications_enableNotifications_label
|
||||
.tr(),
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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.",
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user