diff --git a/assets/i18n/en_US.json b/assets/i18n/en_US.json index 63e82677..05694ced 100644 --- a/assets/i18n/en_US.json +++ b/assets/i18n/en_US.json @@ -178,8 +178,10 @@ "exportSectionTitle": "Import & export", "logsSectionTitle": "Logs", - "darkThemeLabel": "Dark mode", - "darkThemeHint": "Welcome to the dark side", + "themeModeLabel": "App theme", + "systemThemeLabel": "System", + "lightThemeLabel": "Light", + "darkThemeLabel": "Dark", "dynamicThemeLabel": "Material You", "dynamicThemeHint": "Enjoy an experience closer to your device", diff --git a/lib/services/manager_api.dart b/lib/services/manager_api.dart index 0b2cb58d..af996fe2 100644 --- a/lib/services/manager_api.dart +++ b/lib/services/manager_api.dart @@ -199,12 +199,12 @@ class ManagerAPI { await _prefs.setBool('useDynamicTheme', value); } - bool getUseDarkTheme() { - return _prefs.getBool('useDarkTheme') ?? false; + int getThemeMode() { + return _prefs.getInt('themeMode') ?? 2; } - Future setUseDarkTheme(bool value) async { - await _prefs.setBool('useDarkTheme', value); + Future setThemeMode(int value) async { + await _prefs.setInt('themeMode', value); } bool areUniversalPatchesEnabled() { diff --git a/lib/ui/theme/dynamic_theme_builder.dart b/lib/ui/theme/dynamic_theme_builder.dart index 65d74c2c..5fcd8831 100644 --- a/lib/ui/theme/dynamic_theme_builder.dart +++ b/lib/ui/theme/dynamic_theme_builder.dart @@ -1,12 +1,16 @@ +import 'dart:ui'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_themes/dynamic_themes.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/app/app.router.dart'; +import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/theme.dart'; import 'package:stacked_services/stacked_services.dart'; -class DynamicThemeBuilder extends StatelessWidget { +class DynamicThemeBuilder extends StatefulWidget { const DynamicThemeBuilder({ Key? key, required this.title, @@ -17,6 +21,35 @@ class DynamicThemeBuilder extends StatelessWidget { final Widget home; final Iterable localizationsDelegates; + @override + State createState() => _DynamicThemeBuilderState(); +} + +class _DynamicThemeBuilderState extends State with WidgetsBindingObserver { + Brightness brightness = PlatformDispatcher.instance.platformBrightness; + final ManagerAPI _managerAPI = locator(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangePlatformBrightness() { + setState(() { + brightness = PlatformDispatcher.instance.platformBrightness; + }); + if (_managerAPI.getThemeMode() < 2) { + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + systemNavigationBarIconBrightness: + brightness == Brightness.light ? Brightness.dark : Brightness.light, + ), + ); + } + } + @override Widget build(BuildContext context) { return DynamicColorBuilder( @@ -50,24 +83,32 @@ class DynamicThemeBuilder extends StatelessWidget { return DynamicTheme( themeCollection: ThemeCollection( themes: { - 0: lightCustomTheme, - 1: darkCustomTheme, - 2: lightDynamicTheme, - 3: darkDynamicTheme, + 0: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme, + 1: brightness == Brightness.light ? lightDynamicTheme : darkDynamicTheme, + 2: lightCustomTheme, + 3: lightDynamicTheme, + 4: darkCustomTheme, + 5: darkDynamicTheme, }, - fallbackTheme: lightCustomTheme, + fallbackTheme: brightness == Brightness.light ? lightCustomTheme : darkCustomTheme, ), builder: (context, theme) => MaterialApp( - debugShowCheckedModeBanner: false, - title: title, - navigatorKey: StackedService.navigatorKey, - onGenerateRoute: StackedRouter().onGenerateRoute, - theme: theme, - home: home, - localizationsDelegates: localizationsDelegates, - ), + debugShowCheckedModeBanner: false, + title: widget.title, + navigatorKey: StackedService.navigatorKey, + onGenerateRoute: StackedRouter().onGenerateRoute, + theme: theme, + home: widget.home, + localizationsDelegates: widget.localizationsDelegates, + ), ); }, ); } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } } diff --git a/lib/ui/views/navigation/navigation_viewmodel.dart b/lib/ui/views/navigation/navigation_viewmodel.dart index 0967501d..8e36a630 100644 --- a/lib/ui/views/navigation/navigation_viewmodel.dart +++ b/lib/ui/views/navigation/navigation_viewmodel.dart @@ -30,11 +30,8 @@ class NavigationViewModel extends IndexTrackingViewModel { ); } - if (prefs.getBool('useDarkTheme') == null) { - final bool isDark = - MediaQuery.platformBrightnessOf(context) != Brightness.light; - await prefs.setBool('useDarkTheme', isDark); - await DynamicTheme.of(context)!.setTheme(isDark ? 1 : 0); + if (prefs.getInt('themeMode') == null) { + await prefs.setInt('themeMode', 0); } SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setSystemUIOverlayStyle( diff --git a/lib/ui/views/settings/settingsFragment/settings_update_theme.dart b/lib/ui/views/settings/settingsFragment/settings_update_theme.dart index 684abc96..1abd0f8c 100644 --- a/lib/ui/views/settings/settingsFragment/settings_update_theme.dart +++ b/lib/ui/views/settings/settingsFragment/settings_update_theme.dart @@ -8,6 +8,7 @@ import 'package:revanced_manager/app/app.locator.dart'; import 'package:revanced_manager/services/manager_api.dart'; import 'package:revanced_manager/ui/views/settings/settings_viewmodel.dart'; import 'package:revanced_manager/ui/widgets/settingsView/settings_section.dart'; +import 'package:revanced_manager/ui/widgets/shared/custom_material_button.dart'; import 'package:stacked/stacked.dart'; final _settingViewModel = SettingsViewModel(); @@ -24,37 +25,114 @@ class SUpdateTheme extends BaseViewModel { Future setUseDynamicTheme(BuildContext context, bool value) async { await _managerAPI.setUseDynamicTheme(value); - final int currentTheme = DynamicTheme.of(context)!.themeId; - if (currentTheme.isEven) { - await DynamicTheme.of(context)!.setTheme(value ? 2 : 0); - } else { - await DynamicTheme.of(context)!.setTheme(value ? 3 : 1); - } + final int currentTheme = (DynamicTheme.of(context)!.themeId ~/ 2) * 2; + await DynamicTheme.of(context)!.setTheme(currentTheme + (value ? 1 : 0)); notifyListeners(); } - bool getDarkThemeStatus() { - return _managerAPI.getUseDarkTheme(); + int getThemeMode() { + return _managerAPI.getThemeMode(); } - Future setUseDarkTheme(BuildContext context, bool value) async { - await _managerAPI.setUseDarkTheme(value); - final int currentTheme = DynamicTheme.of(context)!.themeId; - if (currentTheme < 2) { - await DynamicTheme.of(context)!.setTheme(value ? 1 : 0); - } else { - await DynamicTheme.of(context)!.setTheme(value ? 3 : 2); - } + Future setThemeMode(BuildContext context, int value) async { + await _managerAPI.setThemeMode(value); + final bool isDynamicTheme = DynamicTheme.of(context)!.themeId.isEven; + await DynamicTheme.of(context)!.setTheme(value * 2 + (isDynamicTheme ? 0 : 1)); + final bool isLight = value != 2 && (value == 1 || DynamicTheme.of(context)!.theme.brightness == Brightness.light); SystemChrome.setSystemUIOverlayStyle( SystemUiOverlayStyle( systemNavigationBarIconBrightness: - value ? Brightness.light : Brightness.dark, + isLight ? Brightness.dark : Brightness.light, ), ); notifyListeners(); } + + I18nText getThemeModeName() { + switch (getThemeMode()) { + case 0: + return I18nText('settingsView.systemThemeLabel'); + case 1: + return I18nText('settingsView.lightThemeLabel'); + case 2: + return I18nText('settingsView.darkThemeLabel'); + default: + return I18nText('settingsView.systemThemeLabel'); + } + } + + Future showThemeDialog(BuildContext context) async { + final ValueNotifier newTheme = ValueNotifier(getThemeMode()); + + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: I18nText('settingsView.themeModeLabel'), + icon: const Icon(Icons.palette), + contentPadding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + content: SingleChildScrollView( + child: ValueListenableBuilder( + valueListenable: newTheme, + builder: (context, value, child) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RadioListTile( + title: I18nText('settingsView.systemThemeLabel'), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + value: 0, + groupValue: value, + onChanged: (value) { + newTheme.value = value!; + }, + ), + RadioListTile( + title: I18nText('settingsView.lightThemeLabel'), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + value: 1, + groupValue: value, + onChanged: (value) { + newTheme.value = value!; + }, + ), + RadioListTile( + title: I18nText('settingsView.darkThemeLabel'), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + value: 2, + groupValue: value, + onChanged: (value) { + newTheme.value = value!; + }, + ), + ], + ); + }, + ), + ), + actions: [ + CustomMaterialButton( + isFilled: false, + label: I18nText('cancelButton'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + CustomMaterialButton( + label: I18nText('okButton'), + onPressed: () { + setThemeMode(context, newTheme.value); + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + } } +final sUpdateTheme = SUpdateTheme(); class SUpdateThemeUI extends StatelessWidget { const SUpdateThemeUI({super.key}); @@ -63,10 +141,10 @@ class SUpdateThemeUI extends StatelessWidget { return SettingsSection( title: 'settingsView.appearanceSectionTitle', children: [ - SwitchListTile( + ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 20.0), title: I18nText( - 'settingsView.darkThemeLabel', + 'settingsView.themeModeLabel', child: const Text( '', style: TextStyle( @@ -75,11 +153,9 @@ class SUpdateThemeUI extends StatelessWidget { ), ), ), - subtitle: I18nText('settingsView.darkThemeHint'), - value: SUpdateTheme().getDarkThemeStatus(), - onChanged: (value) => SUpdateTheme().setUseDarkTheme( - context, - value, + trailing: CustomMaterialButton( + label: sUpdateTheme.getThemeModeName(), + onPressed: () => { sUpdateTheme.showThemeDialog(context) }, ), ), FutureBuilder(