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 kPlanWhiteLabel = 'white_label';
const String kBrightnessLight = 'light';
const String kBrightnessDark = 'dark';
const String kBrightnessSytem = 'system';
const String kColorThemeLight = 'light';
const String kColorThemeDark = 'dark';

View File

@ -236,6 +236,9 @@ Future<AppState> _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) {

View File

@ -172,6 +172,14 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
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);
}

View File

@ -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;

View File

@ -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<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) {
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';
abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
factory PrefState({ModuleLayout moduleLayout}) {
factory PrefState() {
return _$PrefState._(
appLayout: AppLayout.desktop,
moduleLayout: ModuleLayout.table,
@ -26,7 +26,8 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
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<PrefState, PrefStateBuilder> {
bool get isHistoryVisible;
bool get enableDarkMode;
String get darkModeType;
bool get enableDarkModeSystem;
bool get isFilterVisible;
@ -169,6 +172,10 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
BuiltMap<EntityType, PrefStateSortField> 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<PrefState, PrefStateBuilder> {
..useSidebarEditor.replace(BuiltMap<EntityType, bool>())
..useSidebarViewer.replace(BuiltMap<EntityType, bool>())
..sortFields.replace(BuiltMap<EntityType, PrefStateSortField>())
..customColors.replace(builder.enableDarkMode == true
..customColors.replace(builder.darkModeType == kBrightnessDark
? BuiltMap<String, String>()
: BuiltMap<String, String>(PrefState.CONTRAST_COLORS))
..showKanban = false
@ -260,8 +267,9 @@ abstract class PrefState implements Built<PrefState, PrefStateBuilder> {
..enableTooltips = true
..enableNativeBrowser = false
..textScaleFactor = 1
..colorTheme =
builder.enableDarkMode == true ? kColorThemeLight : kColorThemeLight;
..colorTheme = builder.darkModeType == kBrightnessDark
? kColorThemeDark
: kColorThemeLight;
static Serializer<PrefState> get serializer => _$prefStateSerializer;
}

View File

@ -165,8 +165,11 @@ class _$PrefStateSerializer implements StructuredSerializer<PrefState> {
'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<PrefState> {
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<PrefState, PrefStateBuilder> {
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<PrefState, PrefStateBuilder> {
_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<PrefState, PrefStateBuilder> {
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'),

View File

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

View File

@ -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<String, String>()
: BuiltMap<String, String>(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<String, String>) onCustomColorsChanged;
final Function(BuildContext, AppLayout) onLayoutChanged;
final Function(BuildContext, AppSidebarMode) onMenuModeChanged;

View File

@ -257,20 +257,26 @@ class _SettingsWizardState extends State<SettingsWizard> {
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<String, String>()

View File

@ -18,6 +18,7 @@ mixin LocalizationsProvider on LocaleCodeAware {
static final Map<String, Map<String, String>> _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) {