From c4ce409d5cee98f2e1b673fb5ab55e57f38e27ba Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 7 May 2023 12:59:02 +0300 Subject: [PATCH] Automatic light vs dark theme #424 --- lib/constants.dart | 4 ++ lib/main.dart | 3 ++ lib/main_app.dart | 8 ++++ lib/redux/app/app_actions.dart | 6 ++- lib/redux/ui/pref_reducer.dart | 14 +++++-- lib/redux/ui/pref_state.dart | 20 +++++++--- lib/redux/ui/pref_state.g.dart | 53 +++++++++++++++++-------- lib/ui/settings/device_settings.dart | 26 +++++++++++- lib/ui/settings/device_settings_vm.dart | 11 ++--- lib/ui/settings/settings_wizard.dart | 16 +++++--- lib/utils/i18n.dart | 5 +++ 11 files changed, 128 insertions(+), 38 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 4c620c4fe..a58b12374 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -163,6 +163,10 @@ const String kPlanPro = 'pro'; const String kPlanEnterprise = 'enterprise'; const String kPlanWhiteLabel = 'white_label'; +const String kBrightnessLight = 'light'; +const String kBrightnessDark = 'dark'; +const String kBrightnessSytem = 'system'; + const String kColorThemeLight = 'light'; const String kColorThemeDark = 'dark'; diff --git a/lib/main.dart b/lib/main.dart index d82cdcc56..2f9c1374d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -236,6 +236,9 @@ Future _initialState(bool isTesting) async { print('## Error: Failed to load prefs: $e'); } } + prefState = prefState.rebuild((b) => b + ..enableDarkModeSystem = + WidgetsBinding.instance.window.platformBrightness == Brightness.dark); String browserRoute; if (kIsWeb && prefState.isDesktop) { diff --git a/lib/main_app.dart b/lib/main_app.dart index 806b33082..a85b1014c 100644 --- a/lib/main_app.dart +++ b/lib/main_app.dart @@ -172,6 +172,14 @@ class InvoiceNinjaAppState extends State { void initState() { super.initState(); + final window = WidgetsBinding.instance.window; + window.onPlatformBrightnessChanged = () { + WidgetsBinding.instance.handlePlatformBrightnessChanged(); + widget.store.dispatch(UpdateUserPreferences( + enableDarkModeSystem: window.platformBrightness == Brightness.dark)); + setState(() {}); + }; + if (kIsWeb) { WebUtils.warnChanges(widget.store); } diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index 3e11f3212..9fbcc2f9f 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_actions.dart @@ -156,7 +156,8 @@ class UpdateUserPreferences implements PersistPrefs { this.appLayout, this.moduleLayout, this.sidebar, - this.enableDarkMode, + this.darkModeType, + this.enableDarkModeSystem, this.requireAuthentication, this.longPressSelectionIsDefault, this.textScaleFactor, @@ -187,7 +188,8 @@ class UpdateUserPreferences implements PersistPrefs { final AppSidebar sidebar; final AppSidebarMode menuMode; final AppSidebarMode historyMode; - final bool enableDarkMode; + final String darkModeType; + final bool enableDarkModeSystem; final bool longPressSelectionIsDefault; final bool requireAuthentication; final bool isPreviewVisible; diff --git a/lib/redux/ui/pref_reducer.dart b/lib/redux/ui/pref_reducer.dart index 3b1af8c0f..8e0a2ccb0 100644 --- a/lib/redux/ui/pref_reducer.dart +++ b/lib/redux/ui/pref_reducer.dart @@ -76,7 +76,9 @@ PrefState prefReducer( ..textScaleFactor = textScaleFactorReducer(state.textScaleFactor, action) ..isMenuVisible = menuVisibleReducer(state.isMenuVisible, action) ..isHistoryVisible = historyVisibleReducer(state.isHistoryVisible, action) - ..enableDarkMode = darkModeReducer(state.enableDarkMode, action) + ..darkModeType = darkModeTypeReducer(state.darkModeType, action) + ..enableDarkModeSystem = + darkModeSystemReducer(state.enableDarkModeSystem, action) ..enableTooltips = enableTooltipsReducer(state.enableTooltips, action) ..enableFlexibleSearch = enableFlexibleSearchReducer(state.enableFlexibleSearch, action) @@ -337,9 +339,15 @@ Reducer historySidebarReducer = combineReducers([ }), ]); -Reducer darkModeReducer = combineReducers([ +Reducer darkModeTypeReducer = combineReducers([ + TypedReducer((enableDarkMode, action) { + return action.darkModeType ?? enableDarkMode; + }), +]); + +Reducer darkModeSystemReducer = combineReducers([ TypedReducer((enableDarkMode, action) { - return action.enableDarkMode ?? enableDarkMode; + return action.enableDarkModeSystem ?? enableDarkMode; }), ]); diff --git a/lib/redux/ui/pref_state.dart b/lib/redux/ui/pref_state.dart index 995e59f43..96ab19b42 100644 --- a/lib/redux/ui/pref_state.dart +++ b/lib/redux/ui/pref_state.dart @@ -14,7 +14,7 @@ import 'package:invoiceninja_flutter/data/models/static/color_theme_model.dart'; part 'pref_state.g.dart'; abstract class PrefState implements Built { - factory PrefState({ModuleLayout moduleLayout}) { + factory PrefState() { return _$PrefState._( appLayout: AppLayout.desktop, moduleLayout: ModuleLayout.table, @@ -26,7 +26,8 @@ abstract class PrefState implements Built { rowsPerPage: 10, isMenuVisible: true, isHistoryVisible: false, - enableDarkMode: false, + darkModeType: kBrightnessSytem, + enableDarkModeSystem: false, enableFlexibleSearch: false, editAfterSaving: true, requireAuthentication: false, @@ -131,7 +132,9 @@ abstract class PrefState implements Built { bool get isHistoryVisible; - bool get enableDarkMode; + String get darkModeType; + + bool get enableDarkModeSystem; bool get isFilterVisible; @@ -169,6 +172,10 @@ abstract class PrefState implements Built { BuiltMap get sortFields; + bool get enableDarkMode => darkModeType == kBrightnessSytem + ? enableDarkModeSystem + : darkModeType == kBrightnessDark; + ColorTheme get colorThemeModel => colorThemesMap.containsKey(colorTheme) ? colorThemesMap[colorTheme] : colorThemesMap[kColorThemeLight]; @@ -238,7 +245,7 @@ abstract class PrefState implements Built { ..useSidebarEditor.replace(BuiltMap()) ..useSidebarViewer.replace(BuiltMap()) ..sortFields.replace(BuiltMap()) - ..customColors.replace(builder.enableDarkMode == true + ..customColors.replace(builder.darkModeType == kBrightnessDark ? BuiltMap() : BuiltMap(PrefState.CONTRAST_COLORS)) ..showKanban = false @@ -260,8 +267,9 @@ abstract class PrefState implements Built { ..enableTooltips = true ..enableNativeBrowser = false ..textScaleFactor = 1 - ..colorTheme = - builder.enableDarkMode == true ? kColorThemeLight : kColorThemeLight; + ..colorTheme = builder.darkModeType == kBrightnessDark + ? kColorThemeDark + : kColorThemeLight; static Serializer get serializer => _$prefStateSerializer; } diff --git a/lib/redux/ui/pref_state.g.dart b/lib/redux/ui/pref_state.g.dart index f1b52aca1..ed2e3946e 100644 --- a/lib/redux/ui/pref_state.g.dart +++ b/lib/redux/ui/pref_state.g.dart @@ -165,8 +165,11 @@ class _$PrefStateSerializer implements StructuredSerializer { 'isHistoryVisible', serializers.serialize(object.isHistoryVisible, specifiedType: const FullType(bool)), - 'enableDarkMode', - serializers.serialize(object.enableDarkMode, + 'darkModeType', + serializers.serialize(object.darkModeType, + specifiedType: const FullType(String)), + 'enableDarkModeSystem', + serializers.serialize(object.enableDarkModeSystem, specifiedType: const FullType(bool)), 'isFilterVisible', serializers.serialize(object.isFilterVisible, @@ -316,8 +319,12 @@ class _$PrefStateSerializer implements StructuredSerializer { result.isHistoryVisible = serializers.deserialize(value, specifiedType: const FullType(bool)) as bool; break; - case 'enableDarkMode': - result.enableDarkMode = serializers.deserialize(value, + case 'darkModeType': + result.darkModeType = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; + case 'enableDarkModeSystem': + result.enableDarkModeSystem = serializers.deserialize(value, specifiedType: const FullType(bool)) as bool; break; case 'isFilterVisible': @@ -665,7 +672,9 @@ class _$PrefState extends PrefState { @override final bool isHistoryVisible; @override - final bool enableDarkMode; + final String darkModeType; + @override + final bool enableDarkModeSystem; @override final bool isFilterVisible; @override @@ -725,7 +734,8 @@ class _$PrefState extends PrefState { this.enableTouchEvents, this.enableFlexibleSearch, this.isHistoryVisible, - this.enableDarkMode, + this.darkModeType, + this.enableDarkModeSystem, this.isFilterVisible, this.persistData, this.persistUI, @@ -778,7 +788,9 @@ class _$PrefState extends PrefState { BuiltValueNullFieldError.checkNotNull( isHistoryVisible, r'PrefState', 'isHistoryVisible'); BuiltValueNullFieldError.checkNotNull( - enableDarkMode, r'PrefState', 'enableDarkMode'); + darkModeType, r'PrefState', 'darkModeType'); + BuiltValueNullFieldError.checkNotNull( + enableDarkModeSystem, r'PrefState', 'enableDarkModeSystem'); BuiltValueNullFieldError.checkNotNull( isFilterVisible, r'PrefState', 'isFilterVisible'); BuiltValueNullFieldError.checkNotNull( @@ -845,7 +857,8 @@ class _$PrefState extends PrefState { enableTouchEvents == other.enableTouchEvents && enableFlexibleSearch == other.enableFlexibleSearch && isHistoryVisible == other.isHistoryVisible && - enableDarkMode == other.enableDarkMode && + darkModeType == other.darkModeType && + enableDarkModeSystem == other.enableDarkModeSystem && isFilterVisible == other.isFilterVisible && persistData == other.persistData && persistUI == other.persistUI && @@ -888,7 +901,8 @@ class _$PrefState extends PrefState { _$hash = $jc(_$hash, enableTouchEvents.hashCode); _$hash = $jc(_$hash, enableFlexibleSearch.hashCode); _$hash = $jc(_$hash, isHistoryVisible.hashCode); - _$hash = $jc(_$hash, enableDarkMode.hashCode); + _$hash = $jc(_$hash, darkModeType.hashCode); + _$hash = $jc(_$hash, enableDarkModeSystem.hashCode); _$hash = $jc(_$hash, isFilterVisible.hashCode); _$hash = $jc(_$hash, persistData.hashCode); _$hash = $jc(_$hash, persistUI.hashCode); @@ -931,7 +945,8 @@ class _$PrefState extends PrefState { ..add('enableTouchEvents', enableTouchEvents) ..add('enableFlexibleSearch', enableFlexibleSearch) ..add('isHistoryVisible', isHistoryVisible) - ..add('enableDarkMode', enableDarkMode) + ..add('darkModeType', darkModeType) + ..add('enableDarkModeSystem', enableDarkModeSystem) ..add('isFilterVisible', isFilterVisible) ..add('persistData', persistData) ..add('persistUI', persistUI) @@ -1040,10 +1055,14 @@ class PrefStateBuilder implements Builder { set isHistoryVisible(bool isHistoryVisible) => _$this._isHistoryVisible = isHistoryVisible; - bool _enableDarkMode; - bool get enableDarkMode => _$this._enableDarkMode; - set enableDarkMode(bool enableDarkMode) => - _$this._enableDarkMode = enableDarkMode; + String _darkModeType; + String get darkModeType => _$this._darkModeType; + set darkModeType(String darkModeType) => _$this._darkModeType = darkModeType; + + bool _enableDarkModeSystem; + bool get enableDarkModeSystem => _$this._enableDarkModeSystem; + set enableDarkModeSystem(bool enableDarkModeSystem) => + _$this._enableDarkModeSystem = enableDarkModeSystem; bool _isFilterVisible; bool get isFilterVisible => _$this._isFilterVisible; @@ -1161,7 +1180,8 @@ class PrefStateBuilder implements Builder { _enableTouchEvents = $v.enableTouchEvents; _enableFlexibleSearch = $v.enableFlexibleSearch; _isHistoryVisible = $v.isHistoryVisible; - _enableDarkMode = $v.enableDarkMode; + _darkModeType = $v.darkModeType; + _enableDarkModeSystem = $v.enableDarkModeSystem; _isFilterVisible = $v.isFilterVisible; _persistData = $v.persistData; _persistUI = $v.persistUI; @@ -1229,7 +1249,8 @@ class PrefStateBuilder implements Builder { enableTouchEvents: BuiltValueNullFieldError.checkNotNull(enableTouchEvents, r'PrefState', 'enableTouchEvents'), enableFlexibleSearch: BuiltValueNullFieldError.checkNotNull(enableFlexibleSearch, r'PrefState', 'enableFlexibleSearch'), isHistoryVisible: BuiltValueNullFieldError.checkNotNull(isHistoryVisible, r'PrefState', 'isHistoryVisible'), - enableDarkMode: BuiltValueNullFieldError.checkNotNull(enableDarkMode, r'PrefState', 'enableDarkMode'), + darkModeType: BuiltValueNullFieldError.checkNotNull(darkModeType, r'PrefState', 'darkModeType'), + enableDarkModeSystem: BuiltValueNullFieldError.checkNotNull(enableDarkModeSystem, r'PrefState', 'enableDarkModeSystem'), isFilterVisible: BuiltValueNullFieldError.checkNotNull(isFilterVisible, r'PrefState', 'isFilterVisible'), persistData: BuiltValueNullFieldError.checkNotNull(persistData, r'PrefState', 'persistData'), persistUI: BuiltValueNullFieldError.checkNotNull(persistUI, r'PrefState', 'persistUI'), diff --git a/lib/ui/settings/device_settings.dart b/lib/ui/settings/device_settings.dart index 3f239a686..457b4efc8 100644 --- a/lib/ui/settings/device_settings.dart +++ b/lib/ui/settings/device_settings.dart @@ -368,6 +368,7 @@ class _DeviceSettingsState extends State primary: true, children: [ FormCard(children: [ + /* SwitchListTile( title: Text(localization.darkMode), value: prefState.enableDarkMode, @@ -377,8 +378,31 @@ class _DeviceSettingsState extends State ? Icons.lightbulb_outline : MdiIcons.themeLightDark), activeColor: Theme.of(context).colorScheme.secondary, - ), + ), SizedBox(height: 16), + */ + AppDropdownButton( + labelText: localization.lightDarkMode, + value: prefState.darkModeType, + onChanged: (dynamic brightness) { + viewModel.onDarkModeChanged(context, brightness); + }, + items: [ + DropdownMenuItem( + child: Text( + '${localization.system} (${prefState.enableDarkModeSystem ? localization.dark : localization.light})', + ), + value: kBrightnessSytem, + ), + DropdownMenuItem( + child: Text(localization.light), + value: kBrightnessLight, + ), + DropdownMenuItem( + child: Text(localization.dark), + value: kBrightnessDark, + ), + ]), AppDropdownButton( labelText: localization.statusColorTheme, value: state.prefState.colorTheme, diff --git a/lib/ui/settings/device_settings_vm.dart b/lib/ui/settings/device_settings_vm.dart index d5afe0e8a..8b7ff95f2 100644 --- a/lib/ui/settings/device_settings_vm.dart +++ b/lib/ui/settings/device_settings_vm.dart @@ -74,11 +74,12 @@ class DeviceSettingsVM { context, AppLocalization.of(context).endedAllSessions); store.dispatch(UserLogoutAll(completer: completer)); }, - onDarkModeChanged: (BuildContext context, bool value) async { + onDarkModeChanged: (BuildContext context, String value) async { store.dispatch(UpdateUserPreferences( - enableDarkMode: value, - colorTheme: value ? kColorThemeDark : kColorThemeLight, - customColors: value + darkModeType: value, + colorTheme: + value == kBrightnessDark ? kColorThemeDark : kColorThemeLight, + customColors: value == kBrightnessDark ? BuiltMap() : BuiltMap(PrefState.CONTRAST_COLORS))); store.dispatch(UpdatedSetting()); @@ -205,7 +206,7 @@ class DeviceSettingsVM { final AppState state; final Function(BuildContext) onRefreshTap; final Function(BuildContext) onLogoutTap; - final Function(BuildContext, bool) onDarkModeChanged; + final Function(BuildContext, String) onDarkModeChanged; final Function(BuildContext, BuiltMap) onCustomColorsChanged; final Function(BuildContext, AppLayout) onLayoutChanged; final Function(BuildContext, AppSidebarMode) onMenuModeChanged; diff --git a/lib/ui/settings/settings_wizard.dart b/lib/ui/settings/settings_wizard.dart index 2423e4ef1..d053b543a 100644 --- a/lib/ui/settings/settings_wizard.dart +++ b/lib/ui/settings/settings_wizard.dart @@ -257,20 +257,26 @@ class _SettingsWizardState extends State { final darkMode = LayoutBuilder(builder: (context, constraints) { return ToggleButtons( children: [ + Text(localization.system), Text(localization.light), Text(localization.dark), ], constraints: BoxConstraints.expand( - width: (constraints.maxWidth / 2) - 2, height: 40), + width: (constraints.maxWidth / 3) - 2, height: 40), isSelected: [ - !state.prefState.enableDarkMode, - state.prefState.enableDarkMode, + state.prefState.darkModeType == kBrightnessSytem, + state.prefState.darkModeType == kBrightnessLight, + state.prefState.darkModeType == kBrightnessDark, ], onPressed: (index) { - final isDark = index == 1; + final isDark = index == 2; store.dispatch( UpdateUserPreferences( - enableDarkMode: isDark, + darkModeType: index == 0 + ? kBrightnessSytem + : index == 1 + ? kBrightnessLight + : kBrightnessDark, colorTheme: isDark ? kColorThemeDark : kColorThemeLight, customColors: isDark ? BuiltMap() diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index db9d51bcf..4e02d0170 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'light_dark_mode': 'Light/Dark Mode', 'activities': 'Activities', 'routing_id': 'Routing ID', 'enable_e_invoice': 'Enable E-Invoice', @@ -102330,6 +102331,10 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]['routing_id'] ?? _localizedValues['en']['routing_id']; + String get lightDarkMode => + _localizedValues[localeCode]['light_dark_mode'] ?? + _localizedValues['en']['light_dark_mode']; + // STARTER: lang field - do not remove comment String lookup(String key) {