diff --git a/lib/cubits/preference/preference_state.dart b/lib/cubits/preference/preference_state.dart index d64035fa..d333de63 100644 --- a/lib/cubits/preference/preference_state.dart +++ b/lib/cubits/preference/preference_state.dart @@ -103,6 +103,8 @@ class PreferenceState extends Equatable { bool get isDevModeEnabled => _isOn(); + bool get isDynamicColorEnabled => _isOn(); + bool get isHackerNewsThemeEnabled => _isOn(); bool get isPreviewImageLeftAligned => diff --git a/lib/main.dart b/lib/main.dart index 084c5577..919e923d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:equatable/equatable.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter/foundation.dart'; @@ -232,6 +233,7 @@ class HackiApp extends StatelessWidget { previous.font != current.font || previous.textScaleFactor != current.textScaleFactor || previous.isTrueDarkModeEnabled != current.isTrueDarkModeEnabled || + previous.isDynamicColorEnabled != current.isDynamicColorEnabled || previous.isHackerNewsThemeEnabled != current.isHackerNewsThemeEnabled || previous.isDevModeEnabled != current.isDevModeEnabled, @@ -285,59 +287,81 @@ class HackiApp extends StatelessWidget { Brightness.dark); } }(); - final ColorScheme colorScheme = ColorScheme.fromSeed( - brightness: isDarkModeEnabled - ? Brightness.dark - : Brightness.light, - seedColor: state.appColor, - dynamicSchemeVariant: DynamicSchemeVariant.fidelity, - ); - return FeatureDiscovery( - child: MediaQuery( - data: state.textScaleFactor == 1 - ? MediaQuery.of(context) - : MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear( - state.textScaleFactor, + return DynamicColorBuilder( + builder: + ( + ColorScheme? lightDynamic, + ColorScheme? darkDynamic, + ) { + final ColorScheme fallbackColorScheme = + ColorScheme.fromSeed( + brightness: isDarkModeEnabled + ? Brightness.dark + : Brightness.light, + seedColor: state.appColor, + dynamicSchemeVariant: + DynamicSchemeVariant.fidelity, + ); + final ColorScheme colorScheme = + state.isDynamicColorEnabled + ? ((isDarkModeEnabled + ? darkDynamic + : lightDynamic) ?? + fallbackColorScheme) + : fallbackColorScheme; + return FeatureDiscovery( + child: MediaQuery( + data: state.textScaleFactor == 1 + ? MediaQuery.of(context) + : MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + state.textScaleFactor, + ), + ), + child: MaterialApp.router( + title: 'Hacki', + debugShowCheckedModeBanner: false, + darkTheme: state.isHackerNewsThemeEnabled + ? HackerNewsDarkTheme.theme + : null, + theme: state.isHackerNewsThemeEnabled + ? HackerNewsTheme.theme + : AppTheme.theme( + colorScheme, + state.font, + isDarkModeEnabled: + isDarkModeEnabled, + isTrueDarkModeEnabled: + state.isTrueDarkModeEnabled, + ), + routerConfig: router, + builder: state.isDevModeEnabled + ? ( + BuildContext context, + Widget? child, + ) => Stack( + children: [ + Positioned.fill(child: child!), + DraggableFloatingButton( + onTap: () { + router.push( + Paths.logs.landing, + ); + }, + child: Icon( + Icons.bug_report, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ], + ) + : null, ), ), - child: MaterialApp.router( - title: 'Hacki', - debugShowCheckedModeBanner: false, - darkTheme: state.isHackerNewsThemeEnabled - ? HackerNewsDarkTheme.theme - : null, - theme: state.isHackerNewsThemeEnabled - ? HackerNewsTheme.theme - : AppTheme.theme( - colorScheme, - state.font, - isDarkModeEnabled: isDarkModeEnabled, - isTrueDarkModeEnabled: - state.isTrueDarkModeEnabled, - ), - routerConfig: router, - builder: state.isDevModeEnabled - ? (BuildContext context, Widget? child) => - Stack( - children: [ - Positioned.fill(child: child!), - DraggableFloatingButton( - onTap: () { - router.push(Paths.logs.landing); - }, - child: Icon( - Icons.bug_report, - color: Theme.of( - context, - ).colorScheme.onPrimaryContainer, - ), - ), - ], - ) - : null, - ), - ), + ); + }, ); }, ); diff --git a/lib/models/preference.dart b/lib/models/preference.dart index fc7b644d..d09f96a5 100644 --- a/lib/models/preference.dart +++ b/lib/models/preference.dart @@ -85,6 +85,7 @@ abstract final class Preference extends Equatable with SettingsDisplayable { const WebViewBottomSheetPreference(), const DividerPlaceholder(label: 'Look And Feel'), const EyeCandyPreference(), + const DynamicColorPreference(), const HackerNewsThemePreference(), const HapticFeedbackPreference(), const TrueDarkModePreference(), @@ -117,6 +118,35 @@ final class DividerPlaceholder extends Preference { abstract final class BooleanPreference extends Preference { const BooleanPreference({required super.val}); + + String? dependencyErrorMessage(Iterable> preferences) { + final List unmetDependencies = []; + + for (final Preference dependency in dependencies) { + final Preference? concreteDependency = preferences + .singleWhereOrNull( + (Preference pref) => pref.key == dependency.key, + ); + if (concreteDependency?.val != dependency.val) { + final String action = dependency.val == true ? 'enable' : 'disable'; + unmetDependencies.add('$action ${dependency.title}'); + } + } + + if (unmetDependencies.isEmpty) { + return null; + } + + final String precedingDependencies = unmetDependencies + .sublist(0, unmetDependencies.length - 1) + .join(', '); + final String dependencyList = switch (unmetDependencies.length) { + 1 => unmetDependencies.single, + 2 => '${unmetDependencies.first} and ${unmetDependencies.last}', + _ => '$precedingDependencies, and ${unmetDependencies.last}', + }; + return 'Please $dependencyList first.'; + } } abstract final class IntPreference extends Preference { @@ -248,6 +278,11 @@ final class HackerNewsThemePreference extends BooleanPreference { return HackerNewsThemePreference(val: val); } + @override + Set> get dependencies => >{ + const DynamicColorPreference(val: false), + }; + @override String get key => 'hackerNewsThemePreference'; @@ -795,6 +830,35 @@ final class TrueDarkModePreference extends BooleanPreference { String get subtitle => 'real dark.'; } +final class DynamicColorPreference extends BooleanPreference { + const DynamicColorPreference({bool? val}) + : super(val: val ?? _dynamicColorPreferenceDefaultValue); + + static const bool _dynamicColorPreferenceDefaultValue = false; + + @override + DynamicColorPreference copyWith({required bool? val}) { + return DynamicColorPreference(val: val); + } + + @override + Set> get dependencies => >{ + const HackerNewsThemePreference(val: false), + }; + + @override + String get key => 'dynamicColor'; + + @override + String get title => 'Dynamic Colors'; + + @override + String get subtitle => 'wallpaper colors.'; + + @override + bool get isDisplayable => Platform.isAndroid; +} + final class HapticFeedbackPreference extends BooleanPreference { const HapticFeedbackPreference({bool? val}) : super(val: val ?? _hapticFeedbackModeDefaultValue); diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index d4578446..e610497e 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -298,17 +298,18 @@ class _SettingsViewState extends State selected: { preferenceState.isOn(preference as BooleanPreference), }, - onSelectionChanged: - preference.dependencies.satisfy( - preferenceState.preferences, - ) - ? (Set val) { - HapticFeedbackUtils.light(); - context.read().update( - preference.copyWith(val: val.single), - ); - } - : null, + onSelectionChanged: (Set val) { + HapticFeedbackUtils.light(); + if (!preference.dependencies.satisfy( + preferenceState.preferences, + )) { + showDependencyError(preference); + return; + } + context.read().update( + preference.copyWith(val: val.single), + ); + }, ), ), ) @@ -322,25 +323,26 @@ class _SettingsViewState extends State value: preferenceState.isOn( preference as BooleanPreference, ), - onChanged: - preference.dependencies.satisfy( - preferenceState.preferences, - ) - ? (bool val) { - HapticFeedbackUtils.light(); + onChanged: (bool val) { + HapticFeedbackUtils.light(); - context.read().update( - preference.copyWith(val: val), - ); + if (val && + !preference.dependencies.satisfy( + preferenceState.preferences, + )) { + showDependencyError(preference); + return; + } - if (preference is MarkReadStoriesModePreference && - val == false) { - context.read().add( - ClearAllReadStories(), - ); - } - } - : null, + context.read().update( + preference.copyWith(val: val), + ); + + if (preference is MarkReadStoriesModePreference && + val == false) { + context.read().add(ClearAllReadStories()); + } + }, activeThumbColor: Theme.of(context).colorScheme.primary, ), if (preference is MarkReadStoriesModePreference) ...[ @@ -519,10 +521,26 @@ class _SettingsViewState extends State ); } + void showDependencyError(Preference preference) { + if (preference is! BooleanPreference) { + return; + } + final String? message = preference.dependencyErrorMessage( + context.read().state.preferences, + ); + if (message != null) { + context + ..removeSnackBar() + ..showErrorSnackBar(message); + } + } + void showHackerNewsThemeError() { - context - ..removeSnackBar() - ..showErrorSnackBar('Please disable Hacker News Theme first.'); + showDependencyError(const DynamicColorPreference(val: true)); + } + + void showDynamicColorError() { + showDependencyError(const HackerNewsThemePreference(val: true)); } void showFontSettingDialog() { @@ -621,10 +639,17 @@ class _SettingsViewState extends State } void showColorPicker() { - if (context.read().state.isHackerNewsThemeEnabled) { + final PreferenceState preferenceState = context + .read() + .state; + if (preferenceState.isHackerNewsThemeEnabled) { showHackerNewsThemeError(); return; } + if (preferenceState.isDynamicColorEnabled) { + showDynamicColorError(); + return; + } showDialog( context: context, builder: (_) { diff --git a/pubspec.lock b/pubspec.lock index 03f74789..b029bb72 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -321,6 +321,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" + url: "https://pub.dev" + source: hosted + version: "1.8.1" equatable: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0ec52bc8..2153150f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: connectivity_plus: ^7.3.1 device_info_plus: ^13.1.0 dio: ^5.11.0 + dynamic_color: ^1.8.1 dio_smart_retry: ^7.0.1 equatable: ^2.1.0 fast_gbk: ^1.0.0