diff --git a/app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart b/app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart index 6b589fd2..55f36a8f 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart +++ b/app/lib/features/search_browser/presentation/widgets/tabs_action_button.dart @@ -23,7 +23,10 @@ class TabsActionButton extends HookConsumerWidget { ), child: Container( decoration: BoxDecoration( - border: Border.all(width: 2.0, color: Colors.white), + border: Border.all( + width: 2.0, + color: DefaultTextStyle.of(context).style.color!, + ), borderRadius: BorderRadius.circular(5.0), ), constraints: const BoxConstraints(minWidth: 25.0), @@ -31,7 +34,6 @@ class TabsActionButton extends HookConsumerWidget { child: Text( tabCount.toString(), style: const TextStyle( - color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14.0, ), diff --git a/app/lib/features/settings/data/models/settings.dart b/app/lib/features/settings/data/models/settings.dart index 329f4afc..4221b09b 100644 --- a/app/lib/features/settings/data/models/settings.dart +++ b/app/lib/features/settings/data/models/settings.dart @@ -1,6 +1,7 @@ import 'package:bang_navigator/features/content_block/data/models/host.dart'; import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter/material.dart'; part 'settings.g.dart'; @@ -13,6 +14,7 @@ class Settings with FastEquatable { final bool launchUrlExternal; final bool enableContentBlocking; final Set enableHostList; + final ThemeMode themeMode; Settings({ required this.kagiSession, @@ -22,6 +24,7 @@ class Settings with FastEquatable { required this.launchUrlExternal, required this.enableContentBlocking, required this.enableHostList, + required this.themeMode, }); Settings.withDefaults({ @@ -32,12 +35,14 @@ class Settings with FastEquatable { bool? launchUrlExternal, bool? enableContentBlocking, Set? enableHostList, + ThemeMode? themeMode, }) : showEarlyAccessFeatures = showEarlyAccessFeatures ?? true, incognitoMode = incognitoMode ?? true, enableJavascript = enableJavascript ?? true, launchUrlExternal = launchUrlExternal ?? false, enableContentBlocking = enableContentBlocking ?? true, - enableHostList = enableHostList ?? {HostSource.stevenBlackUnified}; + enableHostList = enableHostList ?? {HostSource.stevenBlackUnified}, + themeMode = themeMode ?? ThemeMode.dark; @override bool get cacheHash => true; @@ -51,5 +56,6 @@ class Settings with FastEquatable { launchUrlExternal, enableContentBlocking, enableHostList, + themeMode, ]; } diff --git a/app/lib/features/settings/data/models/settings.g.dart b/app/lib/features/settings/data/models/settings.g.dart index 71756614..3089f359 100644 --- a/app/lib/features/settings/data/models/settings.g.dart +++ b/app/lib/features/settings/data/models/settings.g.dart @@ -21,6 +21,8 @@ abstract class _$SettingsCWProxy { Settings enableHostList(Set enableHostList); + Settings themeMode(ThemeMode themeMode); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Settings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -35,6 +37,7 @@ abstract class _$SettingsCWProxy { bool? launchUrlExternal, bool? enableContentBlocking, Set? enableHostList, + ThemeMode? themeMode, }); } @@ -71,6 +74,9 @@ class _$SettingsCWProxyImpl implements _$SettingsCWProxy { Settings enableHostList(Set enableHostList) => this(enableHostList: enableHostList); + @override + Settings themeMode(ThemeMode themeMode) => this(themeMode: themeMode); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Settings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -87,6 +93,7 @@ class _$SettingsCWProxyImpl implements _$SettingsCWProxy { Object? launchUrlExternal = const $CopyWithPlaceholder(), Object? enableContentBlocking = const $CopyWithPlaceholder(), Object? enableHostList = const $CopyWithPlaceholder(), + Object? themeMode = const $CopyWithPlaceholder(), }) { return Settings( kagiSession: kagiSession == const $CopyWithPlaceholder() @@ -125,6 +132,10 @@ class _$SettingsCWProxyImpl implements _$SettingsCWProxy { ? _value.enableHostList // ignore: cast_nullable_to_non_nullable : enableHostList as Set, + themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null + ? _value.themeMode + // ignore: cast_nullable_to_non_nullable + : themeMode as ThemeMode, ); } } diff --git a/app/lib/features/settings/data/repositories/settings_repository.dart b/app/lib/features/settings/data/repositories/settings_repository.dart index 6689bcc7..d92d0671 100644 --- a/app/lib/features/settings/data/repositories/settings_repository.dart +++ b/app/lib/features/settings/data/repositories/settings_repository.dart @@ -1,6 +1,7 @@ import 'package:bang_navigator/features/content_block/data/models/host.dart'; import 'package:bang_navigator/features/settings/data/models/settings.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -9,6 +10,22 @@ part 'settings_repository.g.dart'; typedef UpdateSettingsFunc = Settings Function(Settings currentSettings); +Set? _parseHostSources(List? input) => input + ?.map( + (list) => + HostSource.values.firstWhereOrNull((source) => source.name == list), + ) + .whereNotNull() + .toSet(); + +ThemeMode? _parseThemeMode(int? index) { + if (index != null && index < ThemeMode.values.length) { + return ThemeMode.values[index]; + } + + return null; +} + @Riverpod(keepAlive: true) class SettingsRepository extends _$SettingsRepository { static const _sessionStorageKey = 'b4ng_kagi_session'; @@ -18,6 +35,7 @@ class SettingsRepository extends _$SettingsRepository { static const _launchExternalStorageKey = 'b4ng_settings_launch_external'; static const _contentBlockingStorageKey = 'b4ng_settings_content_blocking'; static const _enableHostListStorageKey = 'b4ng_settings_host_lists'; + static const _themeModeStorageKey = 'b4ng_settings_theme_mode'; final FlutterSecureStorage _flutterSecureStorage; final Future _sharedPreferences; @@ -88,6 +106,13 @@ class SettingsRepository extends _$SettingsRepository { ); } + if (newSettings.themeMode != oldSettings.themeMode) { + await sharedPreferences.setInt( + _themeModeStorageKey, + newSettings.themeMode.index, + ); + } + ref.invalidateSelf(); } } @@ -105,14 +130,11 @@ class SettingsRepository extends _$SettingsRepository { launchUrlExternal: sharedPreferences.getBool(_launchExternalStorageKey), enableContentBlocking: sharedPreferences.getBool(_contentBlockingStorageKey), - enableHostList: sharedPreferences - .getStringList(_enableHostListStorageKey) - ?.map( - (list) => HostSource.values - .firstWhereOrNull((source) => source.name == list), - ) - .whereNotNull() - .toSet(), + enableHostList: _parseHostSources( + sharedPreferences.getStringList(_enableHostListStorageKey), + ), + themeMode: + _parseThemeMode(sharedPreferences.getInt(_themeModeStorageKey)), ); } } diff --git a/app/lib/features/settings/data/repositories/settings_repository.g.dart b/app/lib/features/settings/data/repositories/settings_repository.g.dart index 24174e57..acf5fc17 100644 --- a/app/lib/features/settings/data/repositories/settings_repository.g.dart +++ b/app/lib/features/settings/data/repositories/settings_repository.g.dart @@ -7,7 +7,7 @@ part of 'settings_repository.dart'; // ************************************************************************** String _$settingsRepositoryHash() => - r'9d0e8b8452f85ca33b8f13a4bbcb69212e7d29bd'; + r'2fed76bdc3132729f1018f8e4c9d493ee11831a0'; /// See also [SettingsRepository]. @ProviderFor(SettingsRepository) diff --git a/app/lib/features/settings/presentation/screens/settings.dart b/app/lib/features/settings/presentation/screens/settings.dart index cdae47d1..1a7ccea9 100644 --- a/app/lib/features/settings/presentation/screens/settings.dart +++ b/app/lib/features/settings/presentation/screens/settings.dart @@ -46,8 +46,7 @@ class SettingsScreen extends HookConsumerWidget { final settings = ref.watch( settingsRepositoryProvider.select( - (value) => - value.valueOrNull ?? Settings.withDefaults(kagiSession: null), + (value) => value.valueOrNull ?? Settings.withDefaults(), ), ); @@ -146,6 +145,49 @@ class SettingsScreen extends HookConsumerWidget { height: 16, ), _buildSection(theme, 'General'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Theme', + style: theme.textTheme.bodyLarge, + ), + Center( + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: ThemeMode.system, + icon: Icon(Icons.brightness_auto), + label: Text('System'), + ), + ButtonSegment( + value: ThemeMode.light, + icon: Icon(Icons.light_mode), + label: Text('Light'), + ), + ButtonSegment( + value: ThemeMode.dark, + icon: Icon(Icons.dark_mode), + label: Text('Dark'), + ), + ], + selected: {settings.themeMode}, + onSelectionChanged: (value) async { + await ref + .read(saveSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.themeMode(value.first), + ); + }, + ), + ), + ], + ), + ), SwitchListTile.adaptive( title: const Text('Incognito Mode'), subtitle: const Text( @@ -269,7 +311,9 @@ class SettingsScreen extends HookConsumerWidget { content: Padding( padding: const EdgeInsets.only(top: 8.0), child: DefaultTextStyle( - style: GoogleFonts.robotoMono(), + style: GoogleFonts.robotoMono( + textStyle: DefaultTextStyle.of(context).style, + ), child: Table( columnWidths: const {0: FixedColumnWidth(100)}, children: [ diff --git a/app/lib/features/settings/presentation/widgets/sync_details_table.dart b/app/lib/features/settings/presentation/widgets/sync_details_table.dart index 7951fa64..b1ea418e 100644 --- a/app/lib/features/settings/presentation/widgets/sync_details_table.dart +++ b/app/lib/features/settings/presentation/widgets/sync_details_table.dart @@ -11,7 +11,9 @@ class SyncDetailsTable extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTextStyle( - style: GoogleFonts.robotoMono(), + style: GoogleFonts.robotoMono( + textStyle: DefaultTextStyle.of(context).style, + ), child: Table( columnWidths: const {0: FixedColumnWidth(100)}, children: [ diff --git a/app/lib/features/web_view/domain/providers.g.dart b/app/lib/features/web_view/domain/providers.g.dart index dc20407f..ee2c2a1f 100644 --- a/app/lib/features/web_view/domain/providers.g.dart +++ b/app/lib/features/web_view/domain/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$blockContentHostsHash() => r'5a6b90a2884342ddb068f2b66d18ed83e42e5108'; +String _$blockContentHostsHash() => r'41855093d8376152244ae37f98e66eea7c74aa32'; /// See also [blockContentHosts]. @ProviderFor(blockContentHosts) diff --git a/app/lib/main.dart b/app/lib/main.dart index de8e1bb9..b859800e 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,5 +1,7 @@ import 'package:bang_navigator/core/error_observer.dart'; import 'package:bang_navigator/domain/services/app_initialization.dart'; +import 'package:bang_navigator/features/settings/data/models/settings.dart'; +import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart'; import 'package:bang_navigator/presentation/hooks/on_initialization.dart'; import 'package:bang_navigator/presentation/widgets/main_app.dart'; import 'package:dynamic_color/dynamic_color.dart'; @@ -21,6 +23,13 @@ void main() async { observers: const [ErrorObserver()], child: HookConsumer( builder: (context, ref, child) { + final themeMode = ref.watch( + settingsRepositoryProvider.select( + (value) => + (value.valueOrNull ?? Settings.withDefaults()).themeMode, + ), + ); + useOnInitialization( () async { await ref @@ -40,7 +49,7 @@ void main() async { useMaterial3: true, colorScheme: darkDynamic?.harmonized(), ), - themeMode: ThemeMode.dark, + themeMode: themeMode, ); }, );