Automatic light vs dark theme #424

This commit is contained in:
Hillel Coren 2023-05-07 12:59:02 +03:00
parent e7562ec21b
commit c4ce409d5c
11 changed files with 128 additions and 38 deletions

View File

@ -163,6 +163,10 @@ const String kPlanPro = 'pro';
const String kPlanEnterprise = 'enterprise'; const String kPlanEnterprise = 'enterprise';
const String kPlanWhiteLabel = 'white_label'; const String kPlanWhiteLabel = 'white_label';
const String kBrightnessLight = 'light';
const String kBrightnessDark = 'dark';
const String kBrightnessSytem = 'system';
const String kColorThemeLight = 'light'; const String kColorThemeLight = 'light';
const String kColorThemeDark = 'dark'; const String kColorThemeDark = 'dark';

View File

@ -236,6 +236,9 @@ Future<AppState> _initialState(bool isTesting) async {
print('## Error: Failed to load prefs: $e'); print('## Error: Failed to load prefs: $e');
} }
} }
prefState = prefState.rebuild((b) => b
..enableDarkModeSystem =
WidgetsBinding.instance.window.platformBrightness == Brightness.dark);
String browserRoute; String browserRoute;
if (kIsWeb && prefState.isDesktop) { if (kIsWeb && prefState.isDesktop) {

View File

@ -172,6 +172,14 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
void initState() { void initState() {
super.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) { if (kIsWeb) {
WebUtils.warnChanges(widget.store); WebUtils.warnChanges(widget.store);
} }

View File

@ -156,7 +156,8 @@ class UpdateUserPreferences implements PersistPrefs {
this.appLayout, this.appLayout,
this.moduleLayout, this.moduleLayout,
this.sidebar, this.sidebar,
this.enableDarkMode, this.darkModeType,
this.enableDarkModeSystem,
this.requireAuthentication, this.requireAuthentication,
this.longPressSelectionIsDefault, this.longPressSelectionIsDefault,
this.textScaleFactor, this.textScaleFactor,
@ -187,7 +188,8 @@ class UpdateUserPreferences implements PersistPrefs {
final AppSidebar sidebar; final AppSidebar sidebar;
final AppSidebarMode menuMode; final AppSidebarMode menuMode;
final AppSidebarMode historyMode; final AppSidebarMode historyMode;
final bool enableDarkMode; final String darkModeType;
final bool enableDarkModeSystem;
final bool longPressSelectionIsDefault; final bool longPressSelectionIsDefault;
final bool requireAuthentication; final bool requireAuthentication;
final bool isPreviewVisible; final bool isPreviewVisible;

View File

@ -76,7 +76,9 @@ PrefState prefReducer(
..textScaleFactor = textScaleFactorReducer(state.textScaleFactor, action) ..textScaleFactor = textScaleFactorReducer(state.textScaleFactor, action)
..isMenuVisible = menuVisibleReducer(state.isMenuVisible, action) ..isMenuVisible = menuVisibleReducer(state.isMenuVisible, action)
..isHistoryVisible = historyVisibleReducer(state.isHistoryVisible, 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) ..enableTooltips = enableTooltipsReducer(state.enableTooltips, action)
..enableFlexibleSearch = ..enableFlexibleSearch =
enableFlexibleSearchReducer(state.enableFlexibleSearch, action) enableFlexibleSearchReducer(state.enableFlexibleSearch, action)
@ -337,9 +339,15 @@ Reducer<AppSidebarMode> historySidebarReducer = combineReducers([
}), }),
]); ]);
Reducer<bool> darkModeReducer = combineReducers([ Reducer<String> darkModeTypeReducer = combineReducers([
TypedReducer<String, UpdateUserPreferences>((enableDarkMode, action) {
return action.darkModeType ?? enableDarkMode;
}),
]);
Reducer<bool> darkModeSystemReducer = combineReducers([
TypedReducer<bool, UpdateUserPreferences>((enableDarkMode, action) { TypedReducer<bool, UpdateUserPreferences>((enableDarkMode, action) {
return action.enableDarkMode ?? enableDarkMode; return action.enableDarkModeSystem ?? enableDarkMode;
}), }),
]); ]);

View File

@ -14,7 +14,7 @@ import 'package:invoiceninja_flutter/data/models/static/color_theme_model.dart';
part 'pref_state.g.dart'; part 'pref_state.g.dart';
abstract class PrefState implements Built<PrefState, PrefStateBuilder> { abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
factory PrefState({ModuleLayout moduleLayout}) { factory PrefState() {
return _$PrefState._( return _$PrefState._(
appLayout: AppLayout.desktop, appLayout: AppLayout.desktop,
moduleLayout: ModuleLayout.table, moduleLayout: ModuleLayout.table,
@ -26,7 +26,8 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
rowsPerPage: 10, rowsPerPage: 10,
isMenuVisible: true, isMenuVisible: true,
isHistoryVisible: false, isHistoryVisible: false,
enableDarkMode: false, darkModeType: kBrightnessSytem,
enableDarkModeSystem: false,
enableFlexibleSearch: false, enableFlexibleSearch: false,
editAfterSaving: true, editAfterSaving: true,
requireAuthentication: false, requireAuthentication: false,
@ -131,7 +132,9 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
bool get isHistoryVisible; bool get isHistoryVisible;
bool get enableDarkMode; String get darkModeType;
bool get enableDarkModeSystem;
bool get isFilterVisible; bool get isFilterVisible;
@ -169,6 +172,10 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
BuiltMap<EntityType, PrefStateSortField> get sortFields; BuiltMap<EntityType, PrefStateSortField> get sortFields;
bool get enableDarkMode => darkModeType == kBrightnessSytem
? enableDarkModeSystem
: darkModeType == kBrightnessDark;
ColorTheme get colorThemeModel => colorThemesMap.containsKey(colorTheme) ColorTheme get colorThemeModel => colorThemesMap.containsKey(colorTheme)
? colorThemesMap[colorTheme] ? colorThemesMap[colorTheme]
: colorThemesMap[kColorThemeLight]; : colorThemesMap[kColorThemeLight];
@ -238,7 +245,7 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
..useSidebarEditor.replace(BuiltMap<EntityType, bool>()) ..useSidebarEditor.replace(BuiltMap<EntityType, bool>())
..useSidebarViewer.replace(BuiltMap<EntityType, bool>()) ..useSidebarViewer.replace(BuiltMap<EntityType, bool>())
..sortFields.replace(BuiltMap<EntityType, PrefStateSortField>()) ..sortFields.replace(BuiltMap<EntityType, PrefStateSortField>())
..customColors.replace(builder.enableDarkMode == true ..customColors.replace(builder.darkModeType == kBrightnessDark
? BuiltMap<String, String>() ? BuiltMap<String, String>()
: BuiltMap<String, String>(PrefState.CONTRAST_COLORS)) : BuiltMap<String, String>(PrefState.CONTRAST_COLORS))
..showKanban = false ..showKanban = false
@ -260,8 +267,9 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
..enableTooltips = true ..enableTooltips = true
..enableNativeBrowser = false ..enableNativeBrowser = false
..textScaleFactor = 1 ..textScaleFactor = 1
..colorTheme = ..colorTheme = builder.darkModeType == kBrightnessDark
builder.enableDarkMode == true ? kColorThemeLight : kColorThemeLight; ? kColorThemeDark
: kColorThemeLight;
static Serializer<PrefState> get serializer => _$prefStateSerializer; static Serializer<PrefState> get serializer => _$prefStateSerializer;
} }

View File

@ -165,8 +165,11 @@ class _$PrefStateSerializer implements StructuredSerializer<PrefState> {
'isHistoryVisible', 'isHistoryVisible',
serializers.serialize(object.isHistoryVisible, serializers.serialize(object.isHistoryVisible,
specifiedType: const FullType(bool)), specifiedType: const FullType(bool)),
'enableDarkMode', 'darkModeType',
serializers.serialize(object.enableDarkMode, serializers.serialize(object.darkModeType,
specifiedType: const FullType(String)),
'enableDarkModeSystem',
serializers.serialize(object.enableDarkModeSystem,
specifiedType: const FullType(bool)), specifiedType: const FullType(bool)),
'isFilterVisible', 'isFilterVisible',
serializers.serialize(object.isFilterVisible, serializers.serialize(object.isFilterVisible,
@ -316,8 +319,12 @@ class _$PrefStateSerializer implements StructuredSerializer<PrefState> {
result.isHistoryVisible = serializers.deserialize(value, result.isHistoryVisible = serializers.deserialize(value,
specifiedType: const FullType(bool)) as bool; specifiedType: const FullType(bool)) as bool;
break; break;
case 'enableDarkMode': case 'darkModeType':
result.enableDarkMode = serializers.deserialize(value, 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; specifiedType: const FullType(bool)) as bool;
break; break;
case 'isFilterVisible': case 'isFilterVisible':
@ -665,7 +672,9 @@ class _$PrefState extends PrefState {
@override @override
final bool isHistoryVisible; final bool isHistoryVisible;
@override @override
final bool enableDarkMode; final String darkModeType;
@override
final bool enableDarkModeSystem;
@override @override
final bool isFilterVisible; final bool isFilterVisible;
@override @override
@ -725,7 +734,8 @@ class _$PrefState extends PrefState {
this.enableTouchEvents, this.enableTouchEvents,
this.enableFlexibleSearch, this.enableFlexibleSearch,
this.isHistoryVisible, this.isHistoryVisible,
this.enableDarkMode, this.darkModeType,
this.enableDarkModeSystem,
this.isFilterVisible, this.isFilterVisible,
this.persistData, this.persistData,
this.persistUI, this.persistUI,
@ -778,7 +788,9 @@ class _$PrefState extends PrefState {
BuiltValueNullFieldError.checkNotNull( BuiltValueNullFieldError.checkNotNull(
isHistoryVisible, r'PrefState', 'isHistoryVisible'); isHistoryVisible, r'PrefState', 'isHistoryVisible');
BuiltValueNullFieldError.checkNotNull( BuiltValueNullFieldError.checkNotNull(
enableDarkMode, r'PrefState', 'enableDarkMode'); darkModeType, r'PrefState', 'darkModeType');
BuiltValueNullFieldError.checkNotNull(
enableDarkModeSystem, r'PrefState', 'enableDarkModeSystem');
BuiltValueNullFieldError.checkNotNull( BuiltValueNullFieldError.checkNotNull(
isFilterVisible, r'PrefState', 'isFilterVisible'); isFilterVisible, r'PrefState', 'isFilterVisible');
BuiltValueNullFieldError.checkNotNull( BuiltValueNullFieldError.checkNotNull(
@ -845,7 +857,8 @@ class _$PrefState extends PrefState {
enableTouchEvents == other.enableTouchEvents && enableTouchEvents == other.enableTouchEvents &&
enableFlexibleSearch == other.enableFlexibleSearch && enableFlexibleSearch == other.enableFlexibleSearch &&
isHistoryVisible == other.isHistoryVisible && isHistoryVisible == other.isHistoryVisible &&
enableDarkMode == other.enableDarkMode && darkModeType == other.darkModeType &&
enableDarkModeSystem == other.enableDarkModeSystem &&
isFilterVisible == other.isFilterVisible && isFilterVisible == other.isFilterVisible &&
persistData == other.persistData && persistData == other.persistData &&
persistUI == other.persistUI && persistUI == other.persistUI &&
@ -888,7 +901,8 @@ class _$PrefState extends PrefState {
_$hash = $jc(_$hash, enableTouchEvents.hashCode); _$hash = $jc(_$hash, enableTouchEvents.hashCode);
_$hash = $jc(_$hash, enableFlexibleSearch.hashCode); _$hash = $jc(_$hash, enableFlexibleSearch.hashCode);
_$hash = $jc(_$hash, isHistoryVisible.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, isFilterVisible.hashCode);
_$hash = $jc(_$hash, persistData.hashCode); _$hash = $jc(_$hash, persistData.hashCode);
_$hash = $jc(_$hash, persistUI.hashCode); _$hash = $jc(_$hash, persistUI.hashCode);
@ -931,7 +945,8 @@ class _$PrefState extends PrefState {
..add('enableTouchEvents', enableTouchEvents) ..add('enableTouchEvents', enableTouchEvents)
..add('enableFlexibleSearch', enableFlexibleSearch) ..add('enableFlexibleSearch', enableFlexibleSearch)
..add('isHistoryVisible', isHistoryVisible) ..add('isHistoryVisible', isHistoryVisible)
..add('enableDarkMode', enableDarkMode) ..add('darkModeType', darkModeType)
..add('enableDarkModeSystem', enableDarkModeSystem)
..add('isFilterVisible', isFilterVisible) ..add('isFilterVisible', isFilterVisible)
..add('persistData', persistData) ..add('persistData', persistData)
..add('persistUI', persistUI) ..add('persistUI', persistUI)
@ -1040,10 +1055,14 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
set isHistoryVisible(bool isHistoryVisible) => set isHistoryVisible(bool isHistoryVisible) =>
_$this._isHistoryVisible = isHistoryVisible; _$this._isHistoryVisible = isHistoryVisible;
bool _enableDarkMode; String _darkModeType;
bool get enableDarkMode => _$this._enableDarkMode; String get darkModeType => _$this._darkModeType;
set enableDarkMode(bool enableDarkMode) => set darkModeType(String darkModeType) => _$this._darkModeType = darkModeType;
_$this._enableDarkMode = enableDarkMode;
bool _enableDarkModeSystem;
bool get enableDarkModeSystem => _$this._enableDarkModeSystem;
set enableDarkModeSystem(bool enableDarkModeSystem) =>
_$this._enableDarkModeSystem = enableDarkModeSystem;
bool _isFilterVisible; bool _isFilterVisible;
bool get isFilterVisible => _$this._isFilterVisible; bool get isFilterVisible => _$this._isFilterVisible;
@ -1161,7 +1180,8 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
_enableTouchEvents = $v.enableTouchEvents; _enableTouchEvents = $v.enableTouchEvents;
_enableFlexibleSearch = $v.enableFlexibleSearch; _enableFlexibleSearch = $v.enableFlexibleSearch;
_isHistoryVisible = $v.isHistoryVisible; _isHistoryVisible = $v.isHistoryVisible;
_enableDarkMode = $v.enableDarkMode; _darkModeType = $v.darkModeType;
_enableDarkModeSystem = $v.enableDarkModeSystem;
_isFilterVisible = $v.isFilterVisible; _isFilterVisible = $v.isFilterVisible;
_persistData = $v.persistData; _persistData = $v.persistData;
_persistUI = $v.persistUI; _persistUI = $v.persistUI;
@ -1229,7 +1249,8 @@ class PrefStateBuilder implements Builder<PrefState, PrefStateBuilder> {
enableTouchEvents: BuiltValueNullFieldError.checkNotNull(enableTouchEvents, r'PrefState', 'enableTouchEvents'), enableTouchEvents: BuiltValueNullFieldError.checkNotNull(enableTouchEvents, r'PrefState', 'enableTouchEvents'),
enableFlexibleSearch: BuiltValueNullFieldError.checkNotNull(enableFlexibleSearch, r'PrefState', 'enableFlexibleSearch'), enableFlexibleSearch: BuiltValueNullFieldError.checkNotNull(enableFlexibleSearch, r'PrefState', 'enableFlexibleSearch'),
isHistoryVisible: BuiltValueNullFieldError.checkNotNull(isHistoryVisible, r'PrefState', 'isHistoryVisible'), 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'), isFilterVisible: BuiltValueNullFieldError.checkNotNull(isFilterVisible, r'PrefState', 'isFilterVisible'),
persistData: BuiltValueNullFieldError.checkNotNull(persistData, r'PrefState', 'persistData'), persistData: BuiltValueNullFieldError.checkNotNull(persistData, r'PrefState', 'persistData'),
persistUI: BuiltValueNullFieldError.checkNotNull(persistUI, r'PrefState', 'persistUI'), persistUI: BuiltValueNullFieldError.checkNotNull(persistUI, r'PrefState', 'persistUI'),

View File

@ -368,6 +368,7 @@ class _DeviceSettingsState extends State<DeviceSettings>
primary: true, primary: true,
children: [ children: [
FormCard(children: [ FormCard(children: [
/*
SwitchListTile( SwitchListTile(
title: Text(localization.darkMode), title: Text(localization.darkMode),
value: prefState.enableDarkMode, value: prefState.enableDarkMode,
@ -377,8 +378,31 @@ class _DeviceSettingsState extends State<DeviceSettings>
? Icons.lightbulb_outline ? Icons.lightbulb_outline
: MdiIcons.themeLightDark), : MdiIcons.themeLightDark),
activeColor: Theme.of(context).colorScheme.secondary, activeColor: Theme.of(context).colorScheme.secondary,
), ),
SizedBox(height: 16), SizedBox(height: 16),
*/
AppDropdownButton<String>(
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<String>( AppDropdownButton<String>(
labelText: localization.statusColorTheme, labelText: localization.statusColorTheme,
value: state.prefState.colorTheme, value: state.prefState.colorTheme,

View File

@ -74,11 +74,12 @@ class DeviceSettingsVM {
context, AppLocalization.of(context).endedAllSessions); context, AppLocalization.of(context).endedAllSessions);
store.dispatch(UserLogoutAll(completer: completer)); store.dispatch(UserLogoutAll(completer: completer));
}, },
onDarkModeChanged: (BuildContext context, bool value) async { onDarkModeChanged: (BuildContext context, String value) async {
store.dispatch(UpdateUserPreferences( store.dispatch(UpdateUserPreferences(
enableDarkMode: value, darkModeType: value,
colorTheme: value ? kColorThemeDark : kColorThemeLight, colorTheme:
customColors: value value == kBrightnessDark ? kColorThemeDark : kColorThemeLight,
customColors: value == kBrightnessDark
? BuiltMap<String, String>() ? BuiltMap<String, String>()
: BuiltMap<String, String>(PrefState.CONTRAST_COLORS))); : BuiltMap<String, String>(PrefState.CONTRAST_COLORS)));
store.dispatch(UpdatedSetting()); store.dispatch(UpdatedSetting());
@ -205,7 +206,7 @@ class DeviceSettingsVM {
final AppState state; final AppState state;
final Function(BuildContext) onRefreshTap; final Function(BuildContext) onRefreshTap;
final Function(BuildContext) onLogoutTap; final Function(BuildContext) onLogoutTap;
final Function(BuildContext, bool) onDarkModeChanged; final Function(BuildContext, String) onDarkModeChanged;
final Function(BuildContext, BuiltMap<String, String>) onCustomColorsChanged; final Function(BuildContext, BuiltMap<String, String>) onCustomColorsChanged;
final Function(BuildContext, AppLayout) onLayoutChanged; final Function(BuildContext, AppLayout) onLayoutChanged;
final Function(BuildContext, AppSidebarMode) onMenuModeChanged; final Function(BuildContext, AppSidebarMode) onMenuModeChanged;

View File

@ -257,20 +257,26 @@ class _SettingsWizardState extends State<SettingsWizard> {
final darkMode = LayoutBuilder(builder: (context, constraints) { final darkMode = LayoutBuilder(builder: (context, constraints) {
return ToggleButtons( return ToggleButtons(
children: [ children: [
Text(localization.system),
Text(localization.light), Text(localization.light),
Text(localization.dark), Text(localization.dark),
], ],
constraints: BoxConstraints.expand( constraints: BoxConstraints.expand(
width: (constraints.maxWidth / 2) - 2, height: 40), width: (constraints.maxWidth / 3) - 2, height: 40),
isSelected: [ isSelected: [
!state.prefState.enableDarkMode, state.prefState.darkModeType == kBrightnessSytem,
state.prefState.enableDarkMode, state.prefState.darkModeType == kBrightnessLight,
state.prefState.darkModeType == kBrightnessDark,
], ],
onPressed: (index) { onPressed: (index) {
final isDark = index == 1; final isDark = index == 2;
store.dispatch( store.dispatch(
UpdateUserPreferences( UpdateUserPreferences(
enableDarkMode: isDark, darkModeType: index == 0
? kBrightnessSytem
: index == 1
? kBrightnessLight
: kBrightnessDark,
colorTheme: isDark ? kColorThemeDark : kColorThemeLight, colorTheme: isDark ? kColorThemeDark : kColorThemeLight,
customColors: isDark customColors: isDark
? BuiltMap<String, String>() ? BuiltMap<String, String>()

View File

@ -18,6 +18,7 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map<String, Map<String, String>> _localizedValues = { static final Map<String, Map<String, String>> _localizedValues = {
'en': { 'en': {
// STARTER: lang key - do not remove comment // STARTER: lang key - do not remove comment
'light_dark_mode': 'Light/Dark Mode',
'activities': 'Activities', 'activities': 'Activities',
'routing_id': 'Routing ID', 'routing_id': 'Routing ID',
'enable_e_invoice': 'Enable E-Invoice', 'enable_e_invoice': 'Enable E-Invoice',
@ -102330,6 +102331,10 @@ mixin LocalizationsProvider on LocaleCodeAware {
_localizedValues[localeCode]['routing_id'] ?? _localizedValues[localeCode]['routing_id'] ??
_localizedValues['en']['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 // STARTER: lang field - do not remove comment
String lookup(String key) { String lookup(String key) {