From d0f60c2771950372da4880d36abd0a4162dd20f2 Mon Sep 17 00:00:00 2001 From: Frank Gregor Date: Thu, 2 Apr 2020 11:43:34 +0200 Subject: [PATCH] OT-717: Unreadable Status Bar Fixes #449 BUGFIX * Fixes a bug that happens on switching between Light and Dark mode, which results in an unreadable system status bar ADDITIONS & CHANGES * Adds a new `ThemeKey` enum value `system` * Changes setting for Light/Dark mode in user profile by removing the switch * Adds a new screen for switching application appearance to `Light`/`Dark` or `System` * Refactors all `ThemeKey` enum values from UPPERCASE to lowercase * Adds `vibrateLight` method for haptic feedback when user changes appearance * Cleans up code ADDITIONAL NOTES * Adds `activeColor` setting to all used `Switch.adaptive` widgets with the value of `CustomTheme.of(context).accent`, because on iOS they had the default green color which doesn't fit our application color theme. * Adds `Preference` class with just two static methods `set({@required value, @required String forKey})` and `value({@required String forKey})` for convenience and better readability * Adds static getter/setter on `Preference` class for easier handling of preference values (NOTE: Should be discussed if we adopt this system overall) So instead of calling: ``` setPreference(preferenceAppThemeKey, ThemeKey.light.toString()); ``` we can call: ``` Preference.themeKeyString = ThemeKey.light.toString(); ``` --- lib/main.dart | 19 +- lib/src/brandable/brandable_icon.dart | 4 +- lib/src/brandable/branded_theme.dart | 2 +- lib/src/brandable/custom_theme.dart | 79 +++++-- lib/src/l10n/l.dart | 8 +- lib/src/navigation/navigatable.dart | 1 + lib/src/navigation/navigation.dart | 5 +- lib/src/platform/preferences.dart | 18 +- lib/src/settings/settings_anti_mobbing.dart | 7 +- lib/src/settings/settings_appearance.dart | 199 ++++++++++++++++++ .../settings/settings_appearance_bloc.dart | 63 ++++++ .../settings_appearance_event_state.dart | 68 ++++++ lib/src/settings/settings_chat.dart | 10 +- lib/src/settings/settings_notifications.dart | 10 +- .../settings/settings_notifications_bloc.dart | 3 +- lib/src/user/user_profile.dart | 23 +- lib/src/utils/keyMapping.dart | 1 + lib/src/utils/vibration.dart | 4 +- lib/src/widgets/settings_item.dart | 1 + 19 files changed, 467 insertions(+), 58 deletions(-) create mode 100644 lib/src/settings/settings_appearance.dart create mode 100644 lib/src/settings/settings_appearance_bloc.dart create mode 100644 lib/src/settings/settings_appearance_event_state.dart diff --git a/lib/main.dart b/lib/main.dart index b7e4ff81..a27560e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -91,7 +91,7 @@ void main() { ), ], child: CustomTheme( - initialThemeKey: ThemeKey.LIGHT, + initialThemeKey: ThemeKey.light, child: OxCoiApp(), ), ), @@ -104,20 +104,23 @@ class OxCoiApp extends StatelessWidget { @override Widget build(BuildContext context) { var customTheme = CustomTheme.of(context); - return MaterialApp( - theme: ThemeData( + final themeData = ThemeData( brightness: customTheme.brightness, backgroundColor: customTheme.background, scaffoldBackgroundColor: customTheme.background, toggleableActiveColor: customTheme.accent, accentColor: customTheme.accent, primaryIconTheme: Theme.of(context).primaryIconTheme.copyWith( - color: customTheme.onSurface, - ), + color: customTheme.onSurface, + ), primaryTextTheme: Theme.of(context).primaryTextTheme.apply( - bodyColor: customTheme.onSurface, - ), - ), + bodyColor: customTheme.onSurface, + ), + ); + + return MaterialApp( + theme: themeData, + themeMode: customTheme.brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark, localizationsDelegates: getLocalizationsDelegates(), supportedLocales: L10n.supportedLocales, localeResolutionCallback: (deviceLocale, supportedLocales) { diff --git a/lib/src/brandable/brandable_icon.dart b/lib/src/brandable/brandable_icon.dart index ef86adf8..23efdd21 100644 --- a/lib/src/brandable/brandable_icon.dart +++ b/lib/src/brandable/brandable_icon.dart @@ -103,7 +103,7 @@ enum IconSource { retry, checkedCircle, circle, - darkMode, + appearance, qr, signature, serverSetting, @@ -173,7 +173,7 @@ const iconData = { IconSource.retry: Icons.autorenew, IconSource.checkedCircle: Icons.check_circle, IconSource.circle: Icons.radio_button_unchecked, - IconSource.darkMode: Icons.brightness_2, + IconSource.appearance: Icons.palette, IconSource.qr: Icons.filter_center_focus, IconSource.signature: Icons.gesture, IconSource.serverSetting: Icons.router, diff --git a/lib/src/brandable/branded_theme.dart b/lib/src/brandable/branded_theme.dart index 66e12c61..aca3129a 100644 --- a/lib/src/brandable/branded_theme.dart +++ b/lib/src/brandable/branded_theme.dart @@ -52,7 +52,7 @@ class BrandedTheme { final Color chatIcon = Colors.lightBlue[800]; final Color signatureIcon = Colors.blue[600]; final Color serverSettingsIcon = Colors.indigo[600]; - final Color darkModeIcon = Colors.deepPurple[500]; + final Color appearanceIcon = Colors.deepPurple[500]; final Color dataProtectionIcon = Colors.purple[400]; final Color blockIcon = Colors.pink[500]; final Color encryptionIcon = Colors.red[600]; diff --git a/lib/src/brandable/custom_theme.dart b/lib/src/brandable/custom_theme.dart index c0c6b641..efeac671 100644 --- a/lib/src/brandable/custom_theme.dart +++ b/lib/src/brandable/custom_theme.dart @@ -41,13 +41,14 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:ox_coi/src/platform/preferences.dart'; - -import 'branded_theme.dart'; +import 'package:ox_coi/src/brandable/branded_theme.dart'; enum ThemeKey { - LIGHT, - DARK, + system, + light, + dark, } class CustomerThemes { @@ -93,12 +94,13 @@ class CustomerThemes { static BrandedTheme getThemeFromKey(ThemeKey themeKey) { switch (themeKey) { - case ThemeKey.LIGHT: + case ThemeKey.light: return lightTheme; - case ThemeKey.DARK: + case ThemeKey.dark: return darkTheme; default: - return lightTheme; + final brightness = WidgetsBinding.instance.window.platformBrightness; + return brightness == Brightness.light ? lightTheme : darkTheme; } } } @@ -122,11 +124,7 @@ class CustomTheme extends StatefulWidget { final Widget child; final ThemeKey initialThemeKey; - const CustomTheme({ - Key key, - this.initialThemeKey, - @required this.child, - }) : super(key: key); + const CustomTheme({Key key, this.initialThemeKey, @required this.child}) : super(key: key); @override CustomThemeState createState() => new CustomThemeState(); @@ -140,6 +138,24 @@ class CustomTheme extends StatefulWidget { _CustomTheme inherited = (context.dependOnInheritedWidgetOfExactType<_CustomTheme>()); return inherited.data; } + + static ThemeKey getThemeKeyFor({@required String name}) { + if (name == ThemeKey.system.toString()) { + return ThemeKey.system; + } else if (name == ThemeKey.light.toString()) { + return ThemeKey.light; + } else { + return ThemeKey.dark; + } + } + + static ThemeKey get systemThemeKey { + final brightness = WidgetsBinding.instance.window.platformBrightness; + if (brightness == Brightness.light) { + return ThemeKey.light; + } + return ThemeKey.dark; + } } class CustomThemeState extends State with WidgetsBindingObserver { @@ -169,23 +185,42 @@ class CustomThemeState extends State with WidgetsBindingObserver { _checkSavedTheme(); } - void _checkSavedTheme() async{ - var newThemeKey; - String savedThemeKey = await getPreference(preferenceAppThemeKey); - if(savedThemeKey == null){ - final Brightness brightness = WidgetsBinding.instance.window.platformBrightness; - newThemeKey = brightness == Brightness.light ? ThemeKey.LIGHT : ThemeKey.DARK; - }else{ - newThemeKey = savedThemeKey.compareTo(ThemeKey.LIGHT.toString()) == 0 ? ThemeKey.LIGHT : ThemeKey.DARK; + void _checkSavedTheme() async { + var savedThemeKeyString = await Preference.themeKey; + + ThemeKey savedThemeKey; + if (savedThemeKeyString == null) { + savedThemeKey = ThemeKey.system; + savedThemeKeyString = savedThemeKey.toString(); + Preference.themeKey = savedThemeKeyString; + } + + ThemeKey newThemeKey; + if (savedThemeKey == ThemeKey.system) { + final platformBrightness = WidgetsBinding.instance.window.platformBrightness; + newThemeKey = platformBrightness == Brightness.light ? ThemeKey.light : ThemeKey.dark; + } else { + newThemeKey = CustomTheme.getThemeKeyFor(name: savedThemeKeyString); } - changeTheme(newThemeKey); + + changeTheme(themeKey: newThemeKey, preservePreference: true); } - void changeTheme(ThemeKey themeKey) { + void changeTheme({@required ThemeKey themeKey, bool preservePreference = false}) async { setState(() { _actualThemeKey = themeKey; _theme = CustomerThemes.getThemeFromKey(themeKey); }); + + if (!preservePreference) { + Preference.themeKey = themeKey.toString(); + } + + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarColor: CustomerThemes.getThemeFromKey(themeKey).background, // Android only + statusBarIconBrightness: themeKey == ThemeKey.dark ? Brightness.light : Brightness.dark, // Android only + statusBarBrightness: themeKey == ThemeKey.dark ? Brightness.dark : Brightness.light // iOS only + )); } @override diff --git a/lib/src/l10n/l.dart b/lib/src/l10n/l.dart index 5c7a6e56..26f947e0 100644 --- a/lib/src/l10n/l.dart +++ b/lib/src/l10n/l.dart @@ -429,7 +429,6 @@ class L { static final settingItemChatTitle = _translationKey("Chat"); static final settingItemSignatureTitle = _translationKey("Email signature"); static final settingItemServerSettingsTitle = _translationKey("Server settings"); - static final settingItemDarkModeTitle = _translationKey("Dark mode"); static final settingItemDataProtectionTitle = _translationKey("Data protection"); static final settingItemBlockedTitle = _translationKey("Blocked contacts"); static final settingItemEncryptionTitle = _translationKey("Encryption"); @@ -437,6 +436,13 @@ class L { static final settingItemFeedbackTitle = _translationKey("Feedback"); static final settingItemBugReportTitle = _translationKey("Report a bug"); + static final settingsAppearanceTitle = _translationKey("Appearance"); + static final settingsAppearanceSystemTitle = _translationKey("System"); + static final settingsAppearanceDarkTitle = _translationKey("Dark"); + static final settingsAppearanceLightTitle = _translationKey("Light"); + static final settingsAppearanceDescritpion = _translationKey("Here you can choose your favorite theme. If you choose '%s', the theme may change automatically. This depends on whether you have selected 'Automatic' in the system preferences or not."); + static final settingsAppearanceSystemThemeDescription = _translationKey("Current System theme is: %s"); + static List _translationKey(String key, [String pluralKey]) { String logging = "Registered localization key: '$key'"; if (!pluralKey.isNullOrEmpty()) { diff --git a/lib/src/navigation/navigatable.dart b/lib/src/navigation/navigatable.dart index 7e4e75df..ac7a7fc1 100644 --- a/lib/src/navigation/navigatable.dart +++ b/lib/src/navigation/navigatable.dart @@ -99,6 +99,7 @@ enum Type { settingsKeyTransferDoneDialog, settingsAutocryptImport, settingsNotifications, + settingsAppearance, splash, share, showQr, diff --git a/lib/src/navigation/navigation.dart b/lib/src/navigation/navigation.dart index 344f95f2..ec9f46f4 100644 --- a/lib/src/navigation/navigation.dart +++ b/lib/src/navigation/navigation.dart @@ -53,10 +53,11 @@ import 'package:ox_coi/src/navigation/navigatable.dart'; import 'package:ox_coi/src/settings/settings.dart'; import 'package:ox_coi/src/settings/settings_about.dart'; import 'package:ox_coi/src/settings/settings_anti_mobbing.dart'; +import 'package:ox_coi/src/settings/settings_appearance.dart'; import 'package:ox_coi/src/settings/settings_chat.dart'; import 'package:ox_coi/src/settings/settings_debug.dart'; -import 'package:ox_coi/src/settings/settings_notifications.dart'; import 'package:ox_coi/src/settings/settings_encryption.dart'; +import 'package:ox_coi/src/settings/settings_notifications.dart'; import 'package:ox_coi/src/user/user_account_settings.dart'; class Navigation { @@ -70,6 +71,7 @@ class Navigation { static const String settingsAbout = '/settings/about'; static const String settingsChat = '/settings/chat'; static const String settingsAntiMobbing = '/settings/antiMobbing'; + static const String settingsAppearance = '/settings/appearance'; static const String settingsNotifications = '/settings/notifications'; static const String settingsAntiMobbingList = '/settings/antiMobbingList'; static const String settingsDebug = '/settings/debug'; @@ -86,6 +88,7 @@ class Navigation { settingsChat: (context) => SettingsChat(), settingsAntiMobbing: (context) => SettingsAntiMobbing(), settingsAntiMobbingList: (context) => AntiMobbingList(), + settingsAppearance: (context) => SettingsAppearance(), settingsNotifications: (context) => SettingsNotifications(), settingsDebug: (context) => SettingsDebug(), chatCreate: (context) => ChatCreate(), diff --git a/lib/src/platform/preferences.dart b/lib/src/platform/preferences.dart index a7405fd0..2d4c726f 100644 --- a/lib/src/platform/preferences.dart +++ b/lib/src/platform/preferences.dart @@ -40,6 +40,7 @@ * for more details. */ +import 'package:flutter/cupertino.dart'; import 'package:shared_preferences/shared_preferences.dart'; const preferenceSystemContactsImportShown = "preferenceSystemContactsImportShown"; @@ -54,7 +55,6 @@ const preferenceNotificationsPushStatus = "preferenceNotificationsPushStatus"; const preferenceAppState = "preferenceAppState"; const preferenceInviteServiceUrl = "preferenceInviteServiceUrl"; const preferenceHasAuthenticationError = "preferenceHasAuthenticationError"; -const preferenceAppThemeKey = "preferenceAppThemeKey"; const preferenceNotificationsAuth = "preferenceNotificationsAuth"; // Unused const preferenceNotificationsP256dhPublic = "preferenceNotificationsP256dhPublic"; // Unused @@ -96,4 +96,20 @@ Future removePreference(String key) async { Future clearPreferences() async { SharedPreferences sharedPreferences = await getSharedPreferences(); await sharedPreferences.clear(); +} + +class Preference { + + static const _themeKey = "themeKey"; + static Future get themeKey async => await value(forKey: _themeKey); + static set themeKey(String value) => set(value: value, forKey: _themeKey); + + static set({@required value, @required String forKey}) async { + await setPreference(forKey, value); + } + + static value({@required String forKey}) async { + return await getPreference(forKey); + } + } \ No newline at end of file diff --git a/lib/src/settings/settings_anti_mobbing.dart b/lib/src/settings/settings_anti_mobbing.dart index bfe3418a..695ba6ef 100644 --- a/lib/src/settings/settings_anti_mobbing.dart +++ b/lib/src/settings/settings_anti_mobbing.dart @@ -44,6 +44,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ox_coi/src/brandable/brandable_icon.dart'; +import 'package:ox_coi/src/brandable/custom_theme.dart'; import 'package:ox_coi/src/l10n/l.dart'; import 'package:ox_coi/src/l10n/l10n.dart'; import 'package:ox_coi/src/navigation/navigatable.dart'; @@ -93,7 +94,11 @@ class _SettingsAntiMobbingState extends State { contentPadding: EdgeInsets.symmetric(vertical: listItemPadding, horizontal: listItemPadding), title: Text(L10n.get(L.settingAntiMobbing)), subtitle: Text(L10n.get(L.settingAntiMobbingText)), - trailing: Switch.adaptive(value: state.antiMobbingActive, onChanged: (value) => _changeAntiMobbingSetting()), + trailing: Switch.adaptive( + value: state.antiMobbingActive, + onChanged: (value) => _changeAntiMobbingSetting(), + activeColor: CustomTheme.of(context).accent, + ), ), Visibility( visible: state.antiMobbingActive, diff --git a/lib/src/settings/settings_appearance.dart b/lib/src/settings/settings_appearance.dart new file mode 100644 index 00000000..8c43d02c --- /dev/null +++ b/lib/src/settings/settings_appearance.dart @@ -0,0 +1,199 @@ +/* + * OPEN-XCHANGE legal information + * + * All intellectual property rights in the Software are protected by + * international copyright laws. + * + * + * In some countries OX, OX Open-Xchange and open xchange + * as well as the corresponding Logos OX Open-Xchange and OX are registered + * trademarks of the OX Software GmbH group of companies. + * The use of the Logos is not covered by the Mozilla Public License 2.0 (MPL 2.0). + * Instead, you are allowed to use these Logos according to the terms and + * conditions of the Creative Commons License, Version 2.5, Attribution, + * Non-commercial, ShareAlike, and the interpretation of the term + * Non-commercial applicable to the aforementioned license is published + * on the web site https://www.open-xchange.com/terms-and-conditions/. + * + * Please make sure that third-party modules and libraries are used + * according to their respective licenses. + * + * Any modifications to this package must retain all copyright notices + * of the original copyright holder(s) for the original code used. + * + * After any such modifications, the original and derivative code shall remain + * under the copyright of the copyright holder(s) and/or original author(s) as stated here: + * https://www.open-xchange.com/legal/. The contributing author shall be + * given Attribution for the derivative code and a license granting use. + * + * Copyright (C) 2016-2020 OX Software GmbH + * Mail: info@open-xchange.com + * + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public License 2.0 + * for more details. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ox_coi/src/brandable/brandable_icon.dart'; +import 'package:ox_coi/src/brandable/custom_theme.dart'; +import 'package:ox_coi/src/l10n/l.dart'; +import 'package:ox_coi/src/l10n/l10n.dart'; +import 'package:ox_coi/src/navigation/navigatable.dart'; +import 'package:ox_coi/src/navigation/navigation.dart'; +import 'package:ox_coi/src/platform/preferences.dart'; +import 'package:ox_coi/src/settings/settings_appearance_bloc.dart'; +import 'package:ox_coi/src/settings/settings_appearance_event_state.dart'; +import 'package:ox_coi/src/ui/dimensions.dart'; +import 'package:ox_coi/src/utils/vibration.dart'; +import 'package:ox_coi/src/widgets/dynamic_appbar.dart'; +import 'package:ox_coi/src/widgets/state_info.dart'; + +class SettingsAppearance extends StatefulWidget { + static get viewTitle => L10n.get(L.settingsAppearanceTitle); + + SettingsAppearance(); + + @override + _SettingsAppearanceState createState() => _SettingsAppearanceState(); +} + +class _SettingsAppearanceState extends State { + SettingsAppearanceBloc _settingsAppearanceBloc = SettingsAppearanceBloc(); + Navigation _navigation = Navigation(); + + @override + void initState() { + super.initState(); + _navigation.current = Navigatable(Type.settingsAppearance); + _settingsAppearanceBloc.add(LoadAppearance()); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: _settingsAppearanceBloc, + builder: (context, state) { + if (state is SettingsAppearanceStateInitial) { + return StateInfo(showLoading: true); + } else if (state is SettingsAppearanceStateLoaded) { + return _buildAppearanceSelector(selectedTheme: state.themeKey); + } else { + return Center( + child: AdaptiveIcon(icon: IconSource.error), + ); + } + }); + } + + Widget _buildAppearanceSelector({@required ThemeKey selectedTheme}) { + return Scaffold( + appBar: DynamicAppBar( + title: SettingsAppearance.viewTitle, + leading: AppBarBackButton(context: context), + ), + body: _AppearanceSelector( + themeItemData: { + ThemeKey.system: L10n.get(L.settingsAppearanceSystemTitle), + ThemeKey.light: L10n.get(L.settingsAppearanceLightTitle), + ThemeKey.dark: L10n.get(L.settingsAppearanceDarkTitle) + }, + selectedTheme: selectedTheme, + onChanged: _appearanceChanged, + ) + ); + } + + void _appearanceChanged(ThemeKey theme) async { + vibrateLight(); + CustomTheme.instanceOf(context).changeTheme(themeKey: theme); + _settingsAppearanceBloc.add(AppearanceLoaded(themeKey: theme)); + } +} + +class _AppearanceSelector extends StatelessWidget { + final Map themeItemData; + final ThemeKey selectedTheme; + final ValueChanged onChanged; + + const _AppearanceSelector({Key key, @required this.themeItemData, @required this.selectedTheme, @required this.onChanged}) : super(key: key); + + @override + Widget build(BuildContext context) { + final systemTheme = themeItemData[CustomTheme.systemThemeKey]; + + return Container( + padding: EdgeInsets.all(dimension32dp), + color: CustomTheme.of(context).background, + child: Column( + children: [ + Container( + padding: EdgeInsets.only(bottom: dimension32dp), + child: Text( + L10n.getFormatted(L.settingsAppearanceDescritpion, [L10n.get(L.settingsAppearanceSystemTitle)]), + ) + ), + Row( + children: [ + for (var themeKey in themeItemData.keys) + _buildAppearanceSelectorItem(context, themeKey), + ], + ), + Container( + padding: EdgeInsets.only(top: dimension16dp), + child: Text( + L10n.getFormatted(L.settingsAppearanceSystemThemeDescription, [systemTheme]), + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 12.0, + color: CustomTheme.of(context).onSecondary + ), + ) + ), + ], + ), + ); + } + + Widget _buildAppearanceSelectorItem(BuildContext context, ThemeKey themeKey) { + return Expanded( + child: GestureDetector( + onTap: () => onChanged(themeKey), + child: Container( + margin: EdgeInsets.only(left: dimension8dp, right: dimension8dp), + padding: EdgeInsets.only(top: dimension16dp), + decoration: BoxDecoration( + color: selectedTheme == themeKey ? CustomTheme.of(context).surface : CustomTheme.of(context).background, + borderRadius: BorderRadius.all(Radius.circular(dimension8dp)), + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: dimension16dp), + child: Icon( + Icons.mood, + color: selectedTheme == themeKey ? CustomTheme.of(context).accent : CustomTheme.of(context).onBackground, + size: dimension32dp, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: dimension16dp), + child: Text( + themeItemData[themeKey], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/settings/settings_appearance_bloc.dart b/lib/src/settings/settings_appearance_bloc.dart new file mode 100644 index 00000000..6315869a --- /dev/null +++ b/lib/src/settings/settings_appearance_bloc.dart @@ -0,0 +1,63 @@ +/* + * OPEN-XCHANGE legal information + * + * All intellectual property rights in the Software are protected by + * international copyright laws. + * + * + * In some countries OX, OX Open-Xchange and open xchange + * as well as the corresponding Logos OX Open-Xchange and OX are registered + * trademarks of the OX Software GmbH group of companies. + * The use of the Logos is not covered by the Mozilla Public License 2.0 (MPL 2.0). + * Instead, you are allowed to use these Logos according to the terms and + * conditions of the Creative Commons License, Version 2.5, Attribution, + * Non-commercial, ShareAlike, and the interpretation of the term + * Non-commercial applicable to the aforementioned license is published + * on the web site https://www.open-xchange.com/terms-and-conditions/. + * + * Please make sure that third-party modules and libraries are used + * according to their respective licenses. + * + * Any modifications to this package must retain all copyright notices + * of the original copyright holder(s) for the original code used. + * + * After any such modifications, the original and derivative code shall remain + * under the copyright of the copyright holder(s) and/or original author(s) as stated here: + * https://www.open-xchange.com/legal/. The contributing author shall be + * given Attribution for the derivative code and a license granting use. + * + * Copyright (C) 2016-2020 OX Software GmbH + * Mail: info@open-xchange.com + * + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public License 2.0 + * for more details. + */ + +import 'package:bloc/bloc.dart'; +import 'package:ox_coi/src/brandable/custom_theme.dart'; +import 'package:ox_coi/src/platform/preferences.dart'; +import 'package:ox_coi/src/settings/settings_appearance_event_state.dart'; + +class SettingsAppearanceBloc extends Bloc { + @override + SettingsAppearanceState get initialState => SettingsAppearanceStateInitial(); + + @override + Stream mapEventToState(SettingsAppearanceEvent event) async* { + if (event is LoadAppearance) { + final themeKeyString = await Preference.themeKey; + final savedThemeKey = CustomTheme.getThemeKeyFor(name: themeKeyString); + add(AppearanceLoaded(themeKey: savedThemeKey)); + + } else if (event is AppearanceLoaded) { + yield SettingsAppearanceStateLoaded(themeKey: event.themeKey); + } + } +} diff --git a/lib/src/settings/settings_appearance_event_state.dart b/lib/src/settings/settings_appearance_event_state.dart new file mode 100644 index 00000000..72cf0e32 --- /dev/null +++ b/lib/src/settings/settings_appearance_event_state.dart @@ -0,0 +1,68 @@ +/* + * OPEN-XCHANGE legal information + * + * All intellectual property rights in the Software are protected by + * international copyright laws. + * + * + * In some countries OX, OX Open-Xchange and open xchange + * as well as the corresponding Logos OX Open-Xchange and OX are registered + * trademarks of the OX Software GmbH group of companies. + * The use of the Logos is not covered by the Mozilla Public License 2.0 (MPL 2.0). + * Instead, you are allowed to use these Logos according to the terms and + * conditions of the Creative Commons License, Version 2.5, Attribution, + * Non-commercial, ShareAlike, and the interpretation of the term + * Non-commercial applicable to the aforementioned license is published + * on the web site https://www.open-xchange.com/terms-and-conditions/. + * + * Please make sure that third-party modules and libraries are used + * according to their respective licenses. + * + * Any modifications to this package must retain all copyright notices + * of the original copyright holder(s) for the original code used. + * + * After any such modifications, the original and derivative code shall remain + * under the copyright of the copyright holder(s) and/or original author(s) as stated here: + * https://www.open-xchange.com/legal/. The contributing author shall be + * given Attribution for the derivative code and a license granting use. + * + * Copyright (C) 2016-2020 OX Software GmbH + * Mail: info@open-xchange.com + * + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public License 2.0 + * for more details. + */ + +// Bloc Events + +import 'package:flutter/cupertino.dart'; +import 'package:ox_coi/src/brandable/custom_theme.dart'; + +mixin SettingsAppearanceEvent {} + +class LoadAppearance with SettingsAppearanceEvent {} + +class AppearanceLoaded with SettingsAppearanceEvent { + final ThemeKey themeKey; + + AppearanceLoaded({@required this.themeKey}); +} + +// Bloc States + +mixin SettingsAppearanceState {} + +class SettingsAppearanceStateInitial with SettingsAppearanceState {} + +class SettingsAppearanceStateLoaded with SettingsAppearanceState { + final ThemeKey themeKey; + + SettingsAppearanceStateLoaded({@required this.themeKey}); +} diff --git a/lib/src/settings/settings_chat.dart b/lib/src/settings/settings_chat.dart index 8a9057b4..66eee330 100644 --- a/lib/src/settings/settings_chat.dart +++ b/lib/src/settings/settings_chat.dart @@ -44,6 +44,7 @@ import 'package:delta_chat_core/delta_chat_core.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ox_coi/src/brandable/custom_theme.dart'; import 'package:ox_coi/src/l10n/l.dart'; import 'package:ox_coi/src/l10n/l10n.dart'; import 'package:ox_coi/src/navigation/navigatable.dart'; @@ -91,7 +92,11 @@ class _SettingsChatState extends State { contentPadding: EdgeInsets.symmetric(vertical: listItemPadding, horizontal: listItemPadding), title: Text(L10n.get(L.settingReadReceiptP, count: L10n.plural)), subtitle: Text(L10n.get(L.settingReadReceiptText)), - trailing: Switch.adaptive(value: state.readReceiptsEnabled, onChanged: (value) => _changeReadReceipts()), + trailing: Switch.adaptive( + value: state.readReceiptsEnabled, + onChanged: (value) => _changeReadReceipts(), + activeColor: CustomTheme.of(context).accent, + ), ), ListTile( contentPadding: EdgeInsets.symmetric(vertical: listItemPadding, horizontal: listItemPadding), @@ -130,18 +135,21 @@ class _SettingsChatState extends State { value: Context.showEmailsOff, groupValue: inviteSetting, onChanged: _onMessageSyncChooserTab, + activeColor: CustomTheme.of(context).accent, ), RadioListTile( title: Text(L10n.get(L.settingMessageSyncingTypeKnown)), value: Context.showEmailsAcceptedContacts, groupValue: inviteSetting, onChanged: _onMessageSyncChooserTab, + activeColor: CustomTheme.of(context).accent, ), RadioListTile( title: Text(L10n.get(L.settingMessageSyncingTypeAll)), value: Context.showEmailsAll, groupValue: inviteSetting, onChanged: _onMessageSyncChooserTab, + activeColor: CustomTheme.of(context).accent, ), ], ); diff --git a/lib/src/settings/settings_notifications.dart b/lib/src/settings/settings_notifications.dart index fe574d59..411ba575 100644 --- a/lib/src/settings/settings_notifications.dart +++ b/lib/src/settings/settings_notifications.dart @@ -45,17 +45,17 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ox_coi/src/brandable/brandable_icon.dart'; +import 'package:ox_coi/src/brandable/custom_theme.dart'; import 'package:ox_coi/src/l10n/l.dart'; import 'package:ox_coi/src/l10n/l10n.dart'; import 'package:ox_coi/src/navigation/navigatable.dart'; import 'package:ox_coi/src/navigation/navigation.dart'; +import 'package:ox_coi/src/settings/settings_notifications_bloc.dart'; import 'package:ox_coi/src/settings/settings_notifications_event_state.dart'; import 'package:ox_coi/src/ui/dimensions.dart'; import 'package:ox_coi/src/widgets/dynamic_appbar.dart'; import 'package:ox_coi/src/widgets/state_info.dart'; -import 'settings_notifications_bloc.dart'; - class SettingsNotifications extends StatefulWidget { @override _SettingsNotificationsState createState() => _SettingsNotificationsState(); @@ -97,7 +97,11 @@ class _SettingsNotificationsState extends State { contentPadding: const EdgeInsets.symmetric(vertical: listItemPadding, horizontal: listItemPadding), title: Text(L10n.get(L.settingNotificationPull)), subtitle: Text(L10n.get(L.settingNotificationPullText)), - trailing: Switch.adaptive(value: state.pullActive, onChanged: (value) => _changeNotificationsSetting()), + trailing: Switch.adaptive( + value: state.pullActive, + onChanged: (value) => _changeNotificationsSetting(), + activeColor: CustomTheme.of(context).accent, + ), ), ), Visibility( diff --git a/lib/src/settings/settings_notifications_bloc.dart b/lib/src/settings/settings_notifications_bloc.dart index 93454eae..7f6285db 100644 --- a/lib/src/settings/settings_notifications_bloc.dart +++ b/lib/src/settings/settings_notifications_bloc.dart @@ -44,8 +44,7 @@ import 'package:bloc/bloc.dart'; import 'package:delta_chat_core/delta_chat_core.dart'; import 'package:ox_coi/src/background_refresh/background_refresh_manager.dart'; import 'package:ox_coi/src/platform/preferences.dart'; - -import 'settings_notifications_event_state.dart'; +import 'package:ox_coi/src/settings/settings_notifications_event_state.dart'; class SettingsNotificationsBloc extends Bloc { @override diff --git a/lib/src/user/user_profile.dart b/lib/src/user/user_profile.dart index fa56770c..c69b5736 100644 --- a/lib/src/user/user_profile.dart +++ b/lib/src/user/user_profile.dart @@ -62,6 +62,7 @@ import 'package:ox_coi/src/navigation/navigation.dart'; import 'package:ox_coi/src/platform/app_information.dart'; import 'package:ox_coi/src/platform/preferences.dart'; import 'package:ox_coi/src/qr/qr.dart'; +import 'package:ox_coi/src/settings/settings_appearance.dart'; import 'package:ox_coi/src/settings/settings_signature.dart'; import 'package:ox_coi/src/user/user_bloc.dart'; import 'package:ox_coi/src/user/user_change_bloc.dart'; @@ -189,13 +190,11 @@ class _ProfileState extends State { text: L10n.get(L.settingGroupHeaderGeneralTitle), ), SettingsItem( - icon: IconSource.darkMode, - text: L10n.get(L.settingItemDarkModeTitle), - iconBackground: CustomTheme.of(context).darkModeIcon, - onTap: () => _changeTheme(), - showSwitch: true, - onSwitchChanged: () => _changeTheme(), - key: Key(keyUserProfileDarkModeIconSource), + icon: IconSource.appearance, + text: SettingsAppearance.viewTitle, + iconBackground: CustomTheme.of(context).appearanceIcon, + onTap: () => _settingsItemTapped(context, SettingsItemName.appearance), + key: Key(keyUserProfileAppearanceIconSource), ), SettingsItem( icon: IconSource.notifications, @@ -319,6 +318,9 @@ class _ProfileState extends State { case SettingsItemName.invite: _createInviteUrl(); break; + case SettingsItemName.appearance: + navigation.pushNamed(context, Navigation.settingsAppearance); + break; case SettingsItemName.notification: navigation.pushNamed(context, Navigation.settingsNotifications); break; @@ -381,13 +383,6 @@ class _ProfileState extends State { mainBloc.add(Logout()); } - void _changeTheme() async { - ThemeKey actualKey = CustomTheme.instanceOf(context).actualThemeKey; - var newTheme = actualKey == ThemeKey.DARK ? ThemeKey.LIGHT : ThemeKey.DARK; - await setPreference(preferenceAppThemeKey, newTheme.toString()); - CustomTheme.instanceOf(context).changeTheme(newTheme); - } - void _editPhotoCallback(String avatarPath) { setState(() { _avatarPath = avatarPath; diff --git a/lib/src/utils/keyMapping.dart b/lib/src/utils/keyMapping.dart index 29d44894..b09dc820 100644 --- a/lib/src/utils/keyMapping.dart +++ b/lib/src/utils/keyMapping.dart @@ -85,6 +85,7 @@ const keyUserProfileBlockIconButton = "keyContactListBlockIconButton"; const keyUserProfileBlockIconSource = "keyUserProfilBlockIconSource"; const keyUserProfileNotificationIconSource = "keyProfileNotificationIconSource"; const keyUserProfileDarkModeIconSource = "keyProfileDarkModeIconSource"; +const keyUserProfileAppearanceIconSource = "keyProfileAppearanceIconSource"; const keyUserProfileServerSettingIconSource = "keyUserProfileServerSettingIconSource"; const keyContactDetailOpenChatProfileActionIcon = "keyContactDetailOpenChatProfileActionIcon"; diff --git a/lib/src/utils/vibration.dart b/lib/src/utils/vibration.dart index 6473591d..96614df4 100644 --- a/lib/src/utils/vibration.dart +++ b/lib/src/utils/vibration.dart @@ -42,4 +42,6 @@ import 'package:vibrate/vibrate.dart'; -vibrateMedium ()=> Vibrate.feedback(FeedbackType.medium); \ No newline at end of file +void vibrateLight() => Vibrate.feedback(FeedbackType.light); + +void vibrateMedium() => Vibrate.feedback(FeedbackType.medium); diff --git a/lib/src/widgets/settings_item.dart b/lib/src/widgets/settings_item.dart index 118c8188..903c2e58 100644 --- a/lib/src/widgets/settings_item.dart +++ b/lib/src/widgets/settings_item.dart @@ -53,6 +53,7 @@ enum SettingsItemName { flagged, qrShow, invite, + appearance, notification, chat, signature,