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';
|
'kDocumentAppearanceFontFamily';
|
||||||
static const String kDocumentAppearanceDefaultTextDirection =
|
static const String kDocumentAppearanceDefaultTextDirection =
|
||||||
'kDocumentAppearanceDefaultTextDirection';
|
'kDocumentAppearanceDefaultTextDirection';
|
||||||
|
static const String kDocumentAppearanceCursorColor =
|
||||||
|
'kDocumentAppearanceCursorColor';
|
||||||
|
static const String kDocumentAppearanceSelectionColor =
|
||||||
|
'kDocumentAppearanceSelectionColor';
|
||||||
|
|
||||||
/// The key for saving the expanded views
|
/// 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/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||||
import 'package:appflowy/util/google_font_family_extension.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/workspace/application/settings/appearance/base_appearance.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -39,15 +40,18 @@ class EditorStyleCustomizer {
|
|||||||
|
|
||||||
EditorStyle desktop() {
|
EditorStyle desktop() {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;
|
final appearance = context.read<DocumentAppearanceCubit>().state;
|
||||||
final fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;
|
final fontSize = appearance.fontSize;
|
||||||
final defaultTextDirection =
|
final fontFamily = appearance.fontFamily;
|
||||||
context.read<DocumentAppearanceCubit>().state.defaultTextDirection;
|
|
||||||
final codeFontSize = max(0.0, fontSize - 2);
|
final codeFontSize = max(0.0, fontSize - 2);
|
||||||
|
|
||||||
return EditorStyle.desktop(
|
return EditorStyle.desktop(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
cursorColor: theme.colorScheme.primary,
|
cursorColor: appearance.cursorColor ??
|
||||||
defaultTextDirection: defaultTextDirection,
|
DefaultAppearanceSettings.getDefaultDocumentCursorColor(context),
|
||||||
|
selectionColor: appearance.selectionColor ??
|
||||||
|
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(context),
|
||||||
|
defaultTextDirection: appearance.defaultTextDirection,
|
||||||
textStyleConfiguration: TextStyleConfiguration(
|
textStyleConfiguration: TextStyleConfiguration(
|
||||||
text: baseTextStyle(fontFamily).copyWith(
|
text: baseTextStyle(fontFamily).copyWith(
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
|
@ -1,28 +1,49 @@
|
|||||||
import 'package:appflowy/core/config/kv_keys.dart';
|
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:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class DocumentAppearance {
|
class DocumentAppearance {
|
||||||
const DocumentAppearance({
|
const DocumentAppearance({
|
||||||
required this.fontSize,
|
required this.fontSize,
|
||||||
required this.fontFamily,
|
required this.fontFamily,
|
||||||
|
this.cursorColor,
|
||||||
|
this.selectionColor,
|
||||||
this.defaultTextDirection,
|
this.defaultTextDirection,
|
||||||
});
|
});
|
||||||
|
|
||||||
final double fontSize;
|
final double fontSize;
|
||||||
final String fontFamily;
|
final String fontFamily;
|
||||||
|
final Color? cursorColor;
|
||||||
|
final Color? selectionColor;
|
||||||
final String? defaultTextDirection;
|
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({
|
DocumentAppearance copyWith({
|
||||||
double? fontSize,
|
double? fontSize,
|
||||||
String? fontFamily,
|
String? fontFamily,
|
||||||
|
Color? cursorColor,
|
||||||
|
Color? selectionColor,
|
||||||
String? defaultTextDirection,
|
String? defaultTextDirection,
|
||||||
|
bool cursorColorIsNull = false,
|
||||||
|
bool selectionColorIsNull = false,
|
||||||
|
bool textDirectionIsNull = false,
|
||||||
}) {
|
}) {
|
||||||
return DocumentAppearance(
|
return DocumentAppearance(
|
||||||
fontSize: fontSize ?? this.fontSize,
|
fontSize: fontSize ?? this.fontSize,
|
||||||
fontFamily: fontFamily ?? this.fontFamily,
|
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 =
|
final defaultTextDirection =
|
||||||
prefs.getString(KVKeys.kDocumentAppearanceDefaultTextDirection);
|
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) {
|
if (isClosed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -53,7 +84,12 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
fontFamily: fontFamily,
|
fontFamily: fontFamily,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionColor: selectionColor,
|
||||||
defaultTextDirection: defaultTextDirection,
|
defaultTextDirection: defaultTextDirection,
|
||||||
|
cursorColorIsNull: cursorColor == null,
|
||||||
|
selectionColorIsNull: selectionColor == null,
|
||||||
|
textDirectionIsNull: defaultTextDirection == null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -106,6 +142,55 @@ class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
defaultTextDirection: direction,
|
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 kDefaultThemeMode = ThemeMode.system;
|
||||||
static const kDefaultThemeName = "Default";
|
static const kDefaultThemeName = "Default";
|
||||||
static const kDefaultTheme = BuiltInTheme.defaultTheme;
|
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/startup/startup.dart';
|
||||||
import 'package:appflowy/user/application/user_settings_service.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/appearance_defaults.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
@ -48,6 +49,20 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
|
|||||||
dateTimeSettings.dateFormat,
|
dateTimeSettings.dateFormat,
|
||||||
dateTimeSettings.timeFormat,
|
dateTimeSettings.timeFormat,
|
||||||
dateTimeSettings.timezoneId,
|
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() =>
|
void resetFontFamily() =>
|
||||||
setFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
|
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
|
/// Updates the current locale and notify the listeners the locale was
|
||||||
/// changed. Fallback to [en] locale if [newLocale] is not supported.
|
/// changed. Fallback to [en] locale if [newLocale] is not supported.
|
||||||
void setLocale(BuildContext context, Locale newLocale) {
|
void setLocale(BuildContext context, Locale newLocale) {
|
||||||
@ -308,6 +351,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
|
|||||||
required UserDateFormatPB dateFormat,
|
required UserDateFormatPB dateFormat,
|
||||||
required UserTimeFormatPB timeFormat,
|
required UserTimeFormatPB timeFormat,
|
||||||
required String timezoneId,
|
required String timezoneId,
|
||||||
|
required Color? documentCursorColor,
|
||||||
|
required Color? documentSelectionColor,
|
||||||
}) = _AppearanceSettingsState;
|
}) = _AppearanceSettingsState;
|
||||||
|
|
||||||
factory AppearanceSettingsState.initial(
|
factory AppearanceSettingsState.initial(
|
||||||
@ -323,6 +368,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
|
|||||||
UserDateFormatPB dateFormat,
|
UserDateFormatPB dateFormat,
|
||||||
UserTimeFormatPB timeFormat,
|
UserTimeFormatPB timeFormat,
|
||||||
String timezoneId,
|
String timezoneId,
|
||||||
|
Color? documentCursorColor,
|
||||||
|
Color? documentSelectionColor,
|
||||||
) {
|
) {
|
||||||
return AppearanceSettingsState(
|
return AppearanceSettingsState(
|
||||||
appTheme: appTheme,
|
appTheme: appTheme,
|
||||||
@ -337,6 +384,8 @@ class AppearanceSettingsState with _$AppearanceSettingsState {
|
|||||||
dateFormat: dateFormat,
|
dateFormat: dateFormat,
|
||||||
timeFormat: timeFormat,
|
timeFormat: timeFormat,
|
||||||
timezoneId: timezoneId,
|
timezoneId: timezoneId,
|
||||||
|
documentCursorColor: documentCursorColor,
|
||||||
|
documentSelectionColor: documentSelectionColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,12 +18,12 @@ class BrightnessSetting extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ThemeSettingEntryTemplateWidget(
|
return FlowySettingListTile(
|
||||||
label: LocaleKeys.settings_appearance_themeMode_label.tr(),
|
label: LocaleKeys.settings_appearance_themeMode_label.tr(),
|
||||||
hint: hintText,
|
hint: hintText,
|
||||||
onResetRequested: context.read<AppearanceSettingsCubit>().resetThemeMode,
|
onResetRequested: context.read<AppearanceSettingsCubit>().resetThemeMode,
|
||||||
trailing: [
|
trailing: [
|
||||||
ThemeValueDropDown(
|
FlowySettingValueDropDown(
|
||||||
currentValue: currentThemeMode.labelText,
|
currentValue: currentThemeMode.labelText,
|
||||||
popupBuilder: (context) => Column(
|
popupBuilder: (context) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -27,7 +27,7 @@ class ColorSchemeSetting extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ThemeSettingEntryTemplateWidget(
|
return FlowySettingListTile(
|
||||||
label: LocaleKeys.settings_appearance_theme.tr(),
|
label: LocaleKeys.settings_appearance_theme.tr(),
|
||||||
onResetRequested: context.read<AppearanceSettingsCubit>().resetTheme,
|
onResetRequested: context.read<AppearanceSettingsCubit>().resetTheme,
|
||||||
trailing: [
|
trailing: [
|
||||||
|
@ -16,7 +16,7 @@ class CreateFileSettings extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ThemeSettingEntryTemplateWidget(
|
return FlowySettingListTile(
|
||||||
label:
|
label:
|
||||||
LocaleKeys.settings_appearance_showNamingDialogWhenCreatingPage.tr(),
|
LocaleKeys.settings_appearance_showNamingDialogWhenCreatingPage.tr(),
|
||||||
trailing: [
|
trailing: [
|
||||||
|
@ -18,10 +18,10 @@ class DateFormatSetting extends StatelessWidget {
|
|||||||
final UserDateFormatPB currentFormat;
|
final UserDateFormatPB currentFormat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ThemeSettingEntryTemplateWidget(
|
Widget build(BuildContext context) => FlowySettingListTile(
|
||||||
label: LocaleKeys.settings_appearance_dateFormat_label.tr(),
|
label: LocaleKeys.settings_appearance_dateFormat_label.tr(),
|
||||||
trailing: [
|
trailing: [
|
||||||
ThemeValueDropDown(
|
FlowySettingValueDropDown(
|
||||||
currentValue: _formatLabel(currentFormat),
|
currentValue: _formatLabel(currentFormat),
|
||||||
popupBuilder: (_) => Column(
|
popupBuilder: (_) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -20,11 +20,11 @@ class LayoutDirectionSetting extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ThemeSettingEntryTemplateWidget(
|
return FlowySettingListTile(
|
||||||
label: LocaleKeys.settings_appearance_layoutDirection_label.tr(),
|
label: LocaleKeys.settings_appearance_layoutDirection_label.tr(),
|
||||||
hint: LocaleKeys.settings_appearance_layoutDirection_hint.tr(),
|
hint: LocaleKeys.settings_appearance_layoutDirection_hint.tr(),
|
||||||
trailing: [
|
trailing: [
|
||||||
ThemeValueDropDown(
|
FlowySettingValueDropDown(
|
||||||
key: const ValueKey('layout_direction_option_button'),
|
key: const ValueKey('layout_direction_option_button'),
|
||||||
currentValue: _layoutDirectionLabelText(currentLayoutDirection),
|
currentValue: _layoutDirectionLabelText(currentLayoutDirection),
|
||||||
popupBuilder: (context) => Column(
|
popupBuilder: (context) => Column(
|
||||||
@ -83,11 +83,11 @@ class TextDirectionSetting extends StatelessWidget {
|
|||||||
final AppFlowyTextDirection? currentTextDirection;
|
final AppFlowyTextDirection? currentTextDirection;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ThemeSettingEntryTemplateWidget(
|
Widget build(BuildContext context) => FlowySettingListTile(
|
||||||
label: LocaleKeys.settings_appearance_textDirection_label.tr(),
|
label: LocaleKeys.settings_appearance_textDirection_label.tr(),
|
||||||
hint: LocaleKeys.settings_appearance_textDirection_hint.tr(),
|
hint: LocaleKeys.settings_appearance_textDirection_hint.tr(),
|
||||||
trailing: [
|
trailing: [
|
||||||
ThemeValueDropDown(
|
FlowySettingValueDropDown(
|
||||||
currentValue: _textDirectionLabelText(currentTextDirection),
|
currentValue: _textDirectionLabelText(currentTextDirection),
|
||||||
popupBuilder: (context) => Column(
|
popupBuilder: (context) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
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> {
|
class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ThemeSettingEntryTemplateWidget(
|
return FlowySettingListTile(
|
||||||
label: LocaleKeys.settings_appearance_fontFamily_label.tr(),
|
label: LocaleKeys.settings_appearance_fontFamily_label.tr(),
|
||||||
resetButtonKey: ThemeFontFamilySetting.resetButtonkey,
|
resetButtonKey: ThemeFontFamilySetting.resetButtonkey,
|
||||||
onResetRequested: () {
|
onResetRequested: () {
|
||||||
@ -91,7 +91,7 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ThemeValueDropDown(
|
return FlowySettingValueDropDown(
|
||||||
popoverKey: ThemeFontFamilySetting.popoverKey,
|
popoverKey: ThemeFontFamilySetting.popoverKey,
|
||||||
popoverController: widget.popoverController,
|
popoverController: widget.popoverController,
|
||||||
currentValue: parseFontFamilyName(widget.currentFontFamily),
|
currentValue: parseFontFamilyName(widget.currentFontFamily),
|
||||||
|
@ -2,3 +2,5 @@ export 'brightness_setting.dart';
|
|||||||
export 'font_family_setting.dart';
|
export 'font_family_setting.dart';
|
||||||
export 'color_scheme.dart';
|
export 'color_scheme.dart';
|
||||||
export 'direction_setting.dart';
|
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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ThemeSettingEntryTemplateWidget extends StatelessWidget {
|
class FlowySettingListTile extends StatelessWidget {
|
||||||
const ThemeSettingEntryTemplateWidget({
|
const FlowySettingListTile({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.resetTooltipText,
|
||||||
this.resetButtonKey,
|
this.resetButtonKey,
|
||||||
required this.label,
|
required this.label,
|
||||||
this.hint,
|
this.hint,
|
||||||
@ -17,6 +18,7 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
|
|||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final String? hint;
|
final String? hint;
|
||||||
|
final String? resetTooltipText;
|
||||||
final Key? resetButtonKey;
|
final Key? resetButtonKey;
|
||||||
final List<Widget>? trailing;
|
final List<Widget>? trailing;
|
||||||
final void Function()? onResetRequested;
|
final void Function()? onResetRequested;
|
||||||
@ -57,7 +59,8 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
|
|||||||
color: Theme.of(context).iconTheme.color,
|
color: Theme.of(context).iconTheme.color,
|
||||||
),
|
),
|
||||||
iconColorOnHover: Theme.of(context).colorScheme.onPrimary,
|
iconColorOnHover: Theme.of(context).colorScheme.onPrimary,
|
||||||
tooltipText: LocaleKeys.settings_appearance_resetSetting.tr(),
|
tooltipText: resetTooltipText ??
|
||||||
|
LocaleKeys.settings_appearance_resetSetting.tr(),
|
||||||
onPressed: onResetRequested,
|
onPressed: onResetRequested,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -65,8 +68,8 @@ class ThemeSettingEntryTemplateWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThemeValueDropDown extends StatefulWidget {
|
class FlowySettingValueDropDown extends StatefulWidget {
|
||||||
const ThemeValueDropDown({
|
const FlowySettingValueDropDown({
|
||||||
super.key,
|
super.key,
|
||||||
required this.currentValue,
|
required this.currentValue,
|
||||||
required this.popupBuilder,
|
required this.popupBuilder,
|
||||||
@ -86,10 +89,11 @@ class ThemeValueDropDown extends StatefulWidget {
|
|||||||
final Offset? offset;
|
final Offset? offset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ThemeValueDropDown> createState() => _ThemeValueDropDownState();
|
State<FlowySettingValueDropDown> createState() =>
|
||||||
|
_FlowySettingValueDropDownState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ThemeValueDropDownState extends State<ThemeValueDropDown> {
|
class _FlowySettingValueDropDownState extends State<FlowySettingValueDropDown> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
|
@ -18,10 +18,10 @@ class TimeFormatSetting extends StatelessWidget {
|
|||||||
final UserTimeFormatPB currentFormat;
|
final UserTimeFormatPB currentFormat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ThemeSettingEntryTemplateWidget(
|
Widget build(BuildContext context) => FlowySettingListTile(
|
||||||
label: LocaleKeys.settings_appearance_timeFormat_label.tr(),
|
label: LocaleKeys.settings_appearance_timeFormat_label.tr(),
|
||||||
trailing: [
|
trailing: [
|
||||||
ThemeValueDropDown(
|
FlowySettingValueDropDown(
|
||||||
currentValue: _formatLabel(currentFormat),
|
currentValue: _formatLabel(currentFormat),
|
||||||
popupBuilder: (_) => Column(
|
popupBuilder: (_) => Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
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/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/create_file_setting.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/date_format_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,
|
currentFontFamily: state.font,
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
DocumentCursorColorSetting(
|
||||||
|
currentCursorColor: state.documentCursorColor ??
|
||||||
|
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DocumentSelectionColorSetting(
|
||||||
|
currentSelectionColor: state.documentSelectionColor ??
|
||||||
|
DefaultAppearanceSettings
|
||||||
|
.getDefaultDocumentSelectionColor(
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
LayoutDirectionSetting(
|
LayoutDirectionSetting(
|
||||||
currentLayoutDirection: state.layoutDirection,
|
currentLayoutDirection: state.layoutDirection,
|
||||||
),
|
),
|
||||||
|
@ -17,7 +17,7 @@ class SettingsNotificationsView extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ThemeSettingEntryTemplateWidget(
|
FlowySettingListTile(
|
||||||
label: LocaleKeys
|
label: LocaleKeys
|
||||||
.settings_notifications_enableNotifications_label
|
.settings_notifications_enableNotifications_label
|
||||||
.tr(),
|
.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,
|
shader1: _darkShader1,
|
||||||
shader2: _darkShader2,
|
shader2: _darkShader2,
|
||||||
shader3: _darkShader3,
|
shader3: _darkShader3,
|
||||||
shader4: const Color(0xff7C8CA5),
|
shader4: const Color(0xff505469),
|
||||||
shader5: _darkShader5,
|
shader5: _darkShader5,
|
||||||
shader6: _darkShader6,
|
shader6: _darkShader6,
|
||||||
shader7: _white,
|
shader7: _white,
|
||||||
|
@ -308,7 +308,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"resetSetting": "Reset this setting",
|
"resetSetting": "Reset",
|
||||||
"fontFamily": {
|
"fontFamily": {
|
||||||
"label": "Font Family",
|
"label": "Font Family",
|
||||||
"search": "Search"
|
"search": "Search"
|
||||||
@ -319,6 +319,18 @@
|
|||||||
"dark": "Dark Mode",
|
"dark": "Dark Mode",
|
||||||
"system": "Adapt to System"
|
"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": {
|
"layoutDirection": {
|
||||||
"label": "Layout Direction",
|
"label": "Layout Direction",
|
||||||
"hint": "Control the flow of content on your screen, from left to right or right to left.",
|
"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)]
|
#[pb(index = 11)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub text_direction: TextDirectionPB,
|
pub text_direction: TextDirectionPB,
|
||||||
|
|
||||||
|
#[pb(index = 12)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub document_setting: DocumentSettingsPB,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT;
|
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_THEME: &str = "Default";
|
||||||
pub const APPEARANCE_DEFAULT_FONT: &str = "Poppins";
|
pub const APPEARANCE_DEFAULT_FONT: &str = "Poppins";
|
||||||
pub const APPEARANCE_DEFAULT_MONOSPACE_FONT: &str = "SF Mono";
|
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,
|
menu_offset: APPEARANCE_DEFAULT_MENU_OFFSET,
|
||||||
layout_direction: LayoutDirectionPB::default(),
|
layout_direction: LayoutDirectionPB::default(),
|
||||||
text_direction: TextDirectionPB::default(),
|
text_direction: TextDirectionPB::default(),
|
||||||
|
document_setting: DocumentSettingsPB::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user