diff --git a/app/lib/bloc/settings/theme.dart b/app/lib/bloc/settings/theme.dart new file mode 100644 index 00000000..e835e1d3 --- /dev/null +++ b/app/lib/bloc/settings/theme.dart @@ -0,0 +1,139 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:copy_with/copy_with.dart'; +import 'package:event_bus/event_bus.dart'; +import 'package:flutter/material.dart'; +import 'package:kiwi/kiwi.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/event/event.dart'; +import 'package:nc_photos/theme.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:to_string/to_string.dart'; + +part 'theme.g.dart'; + +@autoCopyWith +@toString +class ThemeSettingsState { + const ThemeSettingsState({ + required this.isFollowSystemTheme, + required this.isUseBlackInDarkTheme, + required this.seedColor, + }); + + @override + String toString() => _$toString(); + + final bool isFollowSystemTheme; + final bool isUseBlackInDarkTheme; + final Color seedColor; +} + +abstract class ThemeSettingsEvent { + const ThemeSettingsEvent(); +} + +@toString +class ThemeSettingsSetFollowSystemTheme extends ThemeSettingsEvent { + const ThemeSettingsSetFollowSystemTheme(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} + +@toString +class ThemeSettingsSetUseBlackInDarkTheme extends ThemeSettingsEvent { + const ThemeSettingsSetUseBlackInDarkTheme(this.value, this.theme); + + @override + String toString() => _$toString(); + + final bool value; + final ThemeData theme; +} + +@toString +class ThemeSettingsSetSeedColor extends ThemeSettingsEvent { + const ThemeSettingsSetSeedColor(this.value); + + @override + String toString() => _$toString(); + + final Color value; +} + +class ThemeSettingsError { + const ThemeSettingsError(this.ev, [this.error, this.stackTrace]); + + final ThemeSettingsEvent ev; + final Object? error; + final StackTrace? stackTrace; +} + +@npLog +class ThemeSettingsBloc extends Bloc<ThemeSettingsEvent, ThemeSettingsState> { + ThemeSettingsBloc(DiContainer c) + : assert(require(c)), + _c = c, + super(ThemeSettingsState( + isFollowSystemTheme: c.pref.isFollowSystemThemeOr(false), + isUseBlackInDarkTheme: c.pref.isUseBlackInDarkThemeOr(false), + seedColor: getSeedColor(), + )) { + on<ThemeSettingsSetFollowSystemTheme>(_onSetFollowSystemTheme); + on<ThemeSettingsSetUseBlackInDarkTheme>(_onSetUseBlackInDarkTheme); + on<ThemeSettingsSetSeedColor>(_onSetSeedColor); + } + + static bool require(DiContainer c) => DiContainer.has(c, DiType.pref); + + Stream<ThemeSettingsError> errorStream() => _errorStream.stream; + + Future<void> _onSetFollowSystemTheme(ThemeSettingsSetFollowSystemTheme ev, + Emitter<ThemeSettingsState> emit) async { + final oldValue = state.isFollowSystemTheme; + emit(state.copyWith(isFollowSystemTheme: ev.value)); + if (await _c.pref.setFollowSystemTheme(ev.value)) { + KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent()); + } else { + _log.severe("[_onSetFollowSystemTheme] Failed writing pref"); + _errorStream.add(ThemeSettingsError(ev)); + emit(state.copyWith(isFollowSystemTheme: oldValue)); + } + } + + Future<void> _onSetUseBlackInDarkTheme(ThemeSettingsSetUseBlackInDarkTheme ev, + Emitter<ThemeSettingsState> emit) async { + final oldValue = state.isUseBlackInDarkTheme; + emit(state.copyWith(isUseBlackInDarkTheme: ev.value)); + if (await _c.pref.setUseBlackInDarkTheme(ev.value)) { + if (ev.theme.brightness == Brightness.dark) { + KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent()); + } + } else { + _log.severe("[_onSetUseBlackInDarkTheme] Failed writing pref"); + _errorStream.add(ThemeSettingsError(ev)); + emit(state.copyWith(isUseBlackInDarkTheme: oldValue)); + } + } + + Future<void> _onSetSeedColor( + ThemeSettingsSetSeedColor ev, Emitter<ThemeSettingsState> emit) async { + final oldValue = state.seedColor; + emit(state.copyWith(seedColor: ev.value)); + if (await _c.pref.setSeedColor(ev.value.withAlpha(0xFF).value)) { + KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent()); + } else { + _log.severe("[_onSetSeedColor] Failed writing pref"); + _errorStream.add(ThemeSettingsError(ev)); + emit(state.copyWith(seedColor: oldValue)); + } + } + + final DiContainer _c; + final _errorStream = StreamController<ThemeSettingsError>.broadcast(); +} diff --git a/app/lib/bloc/settings/theme.g.dart b/app/lib/bloc/settings/theme.g.dart new file mode 100644 index 00000000..2d62caef --- /dev/null +++ b/app/lib/bloc/settings/theme.g.dart @@ -0,0 +1,74 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'theme.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +extension $ThemeSettingsStateCopyWith on ThemeSettingsState { + ThemeSettingsState copyWith( + {bool? isFollowSystemTheme, + bool? isUseBlackInDarkTheme, + Color? seedColor}) => + _$copyWith( + isFollowSystemTheme: isFollowSystemTheme, + isUseBlackInDarkTheme: isUseBlackInDarkTheme, + seedColor: seedColor); + + ThemeSettingsState _$copyWith( + {bool? isFollowSystemTheme, + bool? isUseBlackInDarkTheme, + Color? seedColor}) { + return ThemeSettingsState( + isFollowSystemTheme: isFollowSystemTheme ?? this.isFollowSystemTheme, + isUseBlackInDarkTheme: + isUseBlackInDarkTheme ?? this.isUseBlackInDarkTheme, + seedColor: seedColor ?? this.seedColor); + } +} + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$ThemeSettingsBlocNpLog on ThemeSettingsBloc { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("bloc.settings.theme.ThemeSettingsBloc"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$ThemeSettingsStateToString on ThemeSettingsState { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "ThemeSettingsState {isFollowSystemTheme: $isFollowSystemTheme, isUseBlackInDarkTheme: $isUseBlackInDarkTheme, seedColor: $seedColor}"; + } +} + +extension _$ThemeSettingsSetFollowSystemThemeToString + on ThemeSettingsSetFollowSystemTheme { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "ThemeSettingsSetFollowSystemTheme {value: $value}"; + } +} + +extension _$ThemeSettingsSetUseBlackInDarkThemeToString + on ThemeSettingsSetUseBlackInDarkTheme { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "ThemeSettingsSetUseBlackInDarkTheme {value: $value, theme: $theme}"; + } +} + +extension _$ThemeSettingsSetSeedColorToString on ThemeSettingsSetSeedColor { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "ThemeSettingsSetSeedColor {value: $value}"; + } +} diff --git a/app/lib/widget/settings/theme_settings.dart b/app/lib/widget/settings/theme_settings.dart index ae3ba688..7d761f83 100644 --- a/app/lib/widget/settings/theme_settings.dart +++ b/app/lib/widget/settings/theme_settings.dart @@ -1,39 +1,63 @@ -import 'package:event_bus/event_bus.dart'; +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/app_localizations.dart'; -import 'package:nc_photos/event/event.dart'; +import 'package:nc_photos/bloc/settings/theme.dart'; +import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/android/android_info.dart'; import 'package:nc_photos/platform/k.dart' as platform_k; -import 'package:nc_photos/pref.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:np_codegen/np_codegen.dart'; part 'theme_settings.g.dart'; -class ThemeSettings extends StatefulWidget { +class ThemeSettings extends StatelessWidget { const ThemeSettings({super.key}); @override - createState() => _ThemeSettingsState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ThemeSettingsBloc(KiwiContainer().resolve<DiContainer>()), + child: const _WrappedThemeSettings(), + ); + } +} + +class _WrappedThemeSettings extends StatefulWidget { + const _WrappedThemeSettings(); + + @override + State<StatefulWidget> createState() => _WrappedThemeSettingsState(); } @npLog -class _ThemeSettingsState extends State<ThemeSettings> { +class _WrappedThemeSettingsState extends State<_WrappedThemeSettings> { @override - initState() { + void initState() { super.initState(); - _isFollowSystemTheme = Pref().isFollowSystemThemeOr(false); - _isUseBlackInDarkTheme = Pref().isUseBlackInDarkThemeOr(false); - _seedColor = getSeedColor(); + _errorSubscription = + context.read<ThemeSettingsBloc>().errorStream().listen((_) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().writePreferenceFailureNotification), + duration: k.snackBarDurationNormal, + )); + }); } @override - build(BuildContext context) { + void dispose() { + _errorSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return Scaffold( body: Builder( builder: (context) => _buildContent(context), @@ -51,32 +75,60 @@ class _ThemeSettingsState extends State<ThemeSettings> { SliverList( delegate: SliverChildListDelegate( [ - ListTile( - title: Text(L10n.global().settingsSeedColorTitle), - subtitle: Text(L10n.global().settingsSeedColorDescription), - trailing: Icon( - Icons.circle, - size: 32, - color: _seedColor, - ), - onTap: () => _onSeedColorPressed(context), + BlocBuilder<ThemeSettingsBloc, ThemeSettingsState>( + buildWhen: (previous, current) => + previous.seedColor != current.seedColor, + builder: (context, state) { + return ListTile( + title: Text(L10n.global().settingsSeedColorTitle), + subtitle: Text(L10n.global().settingsSeedColorDescription), + trailing: Icon( + Icons.circle, + size: 32, + color: state.seedColor, + ), + onTap: () => _onSeedColorPressed(context), + ); + }, ), if (platform_k.isAndroid && AndroidInfo().sdkInt >= AndroidVersion.Q) - SwitchListTile( - title: Text(L10n.global().settingsFollowSystemThemeTitle), - value: _isFollowSystemTheme, - onChanged: (value) => _onFollowSystemThemeChanged(value), + BlocBuilder<ThemeSettingsBloc, ThemeSettingsState>( + buildWhen: (previous, current) => + previous.isFollowSystemTheme != + current.isFollowSystemTheme, + builder: (context, state) { + return SwitchListTile( + title: Text(L10n.global().settingsFollowSystemThemeTitle), + value: state.isFollowSystemTheme, + onChanged: (value) { + context + .read<ThemeSettingsBloc>() + .add(ThemeSettingsSetFollowSystemTheme(value)); + }, + ); + }, ), - SwitchListTile( - title: Text(L10n.global().settingsUseBlackInDarkThemeTitle), - subtitle: Text(_isUseBlackInDarkTheme - ? L10n.global().settingsUseBlackInDarkThemeTrueDescription - : L10n.global() - .settingsUseBlackInDarkThemeFalseDescription), - value: _isUseBlackInDarkTheme, - onChanged: (value) => - _onUseBlackInDarkThemeChanged(context, value), + BlocBuilder<ThemeSettingsBloc, ThemeSettingsState>( + buildWhen: (previous, current) => + previous.isUseBlackInDarkTheme != + current.isUseBlackInDarkTheme, + builder: (context, state) { + return SwitchListTile( + title: Text(L10n.global().settingsUseBlackInDarkThemeTitle), + subtitle: Text(state.isUseBlackInDarkTheme + ? L10n.global() + .settingsUseBlackInDarkThemeTrueDescription + : L10n.global() + .settingsUseBlackInDarkThemeFalseDescription), + value: state.isUseBlackInDarkTheme, + onChanged: (value) { + context.read<ThemeSettingsBloc>().add( + ThemeSettingsSetUseBlackInDarkTheme( + value, Theme.of(context))); + }, + ); + }, ), ], ), @@ -85,47 +137,6 @@ class _ThemeSettingsState extends State<ThemeSettings> { ); } - Future<void> _onFollowSystemThemeChanged(bool value) async { - final oldValue = _isFollowSystemTheme; - setState(() { - _isFollowSystemTheme = value; - }); - if (await Pref().setFollowSystemTheme(value)) { - KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent()); - } else { - _log.severe("[_onFollowSystemThemeChanged] Failed writing pref"); - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().writePreferenceFailureNotification), - duration: k.snackBarDurationNormal, - )); - setState(() { - _isFollowSystemTheme = oldValue; - }); - } - } - - Future<void> _onUseBlackInDarkThemeChanged( - BuildContext context, bool value) async { - final oldValue = _isUseBlackInDarkTheme; - setState(() { - _isUseBlackInDarkTheme = value; - }); - if (await Pref().setUseBlackInDarkTheme(value)) { - if (Theme.of(context).brightness == Brightness.dark) { - KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent()); - } - } else { - _log.severe("[_onUseBlackInDarkThemeChanged] Failed writing pref"); - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().writePreferenceFailureNotification), - duration: k.snackBarDurationNormal, - )); - setState(() { - _isUseBlackInDarkTheme = oldValue; - }); - } - } - Future<void> _onSeedColorPressed(BuildContext context) async { final result = await showDialog<Color>( context: context, @@ -134,28 +145,12 @@ class _ThemeSettingsState extends State<ThemeSettings> { if (result == null) { return; } - - final oldValue = _seedColor; - setState(() { - _seedColor = result; - }); - if (await Pref().setSeedColor(result.withAlpha(0xFF).value)) { - KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent()); - } else { - _log.severe("[_onSeedColorPressed] Failed writing pref"); - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().writePreferenceFailureNotification), - duration: k.snackBarDurationNormal, - )); - setState(() { - _seedColor = oldValue; - }); + if (mounted) { + context.read<ThemeSettingsBloc>().add(ThemeSettingsSetSeedColor(result)); } } - late bool _isFollowSystemTheme; - late bool _isUseBlackInDarkTheme; - late Color _seedColor; + late final StreamSubscription _errorSubscription; } class _SeedColorPicker extends StatefulWidget { diff --git a/app/lib/widget/settings/theme_settings.g.dart b/app/lib/widget/settings/theme_settings.g.dart index 6f679148..417759a2 100644 --- a/app/lib/widget/settings/theme_settings.g.dart +++ b/app/lib/widget/settings/theme_settings.g.dart @@ -6,10 +6,10 @@ part of 'theme_settings.dart'; // NpLogGenerator // ************************************************************************** -extension _$_ThemeSettingsStateNpLog on _ThemeSettingsState { +extension _$_WrappedThemeSettingsStateNpLog on _WrappedThemeSettingsState { // ignore: unused_element Logger get _log => log; static final log = - Logger("widget.settings.theme_settings._ThemeSettingsState"); + Logger("widget.settings.theme_settings._WrappedThemeSettingsState"); } diff --git a/app/pubspec.lock b/app/pubspec.lock index 0cdae77c..355cbaac 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -297,6 +297,24 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.2" + copy_with: + dependency: "direct main" + description: + path: copy_with + ref: "copy_with-1.0.0" + resolved-ref: c3ef6b3b5337f99ee7c0e1fb655bacf635d8b072 + url: "https://gitlab.com/nkming2/dart-copy-with" + source: git + version: "1.0.0" + copy_with_build: + dependency: "direct dev" + description: + path: copy_with_build + ref: "copy_with_build-1.0.0" + resolved-ref: "8303676d56dc16bc2fb1e38ad95c5d97b493e613" + url: "https://gitlab.com/nkming2/dart-copy-with" + source: git + version: "1.0.0" coverage: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 1d14bbcb..33086e21 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -40,6 +40,11 @@ dependencies: circular_reveal_animation: ^2.0.1 collection: ^1.15.0 connectivity_plus: ^2.0.2 + copy_with: + git: + url: https://gitlab.com/nkming2/dart-copy-with + path: copy_with + ref: copy_with-1.0.0 devicelocale: ^0.5.0 device_info_plus: ^4.0.0 draggable_scrollbar: @@ -124,6 +129,11 @@ dev_dependencies: test: any bloc_test: any build_runner: ^2.1.11 + copy_with_build: + git: + url: https://gitlab.com/nkming2/dart-copy-with + path: copy_with_build + ref: copy_with_build-1.0.0 drift_dev: ^1.7.0 flutter_lints: ^2.0.1 flutter_test: