diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index 487f8d75..e2d67d5f 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -109,6 +109,14 @@ class PrefController { value: value, ); + ValueStream get isDoubleTapExit => _isDoubleTapExitController.stream; + + Future setDoubleTapExit(bool value) => _set( + controller: _isDoubleTapExitController, + setter: (pref, value) => pref.setDoubleTapExit(value), + value: value, + ); + Future _set({ required BehaviorSubject controller, required Future Function(Pref pref, T value) setter, @@ -159,4 +167,6 @@ class PrefController { GpsMapProvider.fromValue(_c.pref.getGpsMapProviderOr(0))); late final _isAlbumBrowserShowDateController = BehaviorSubject.seeded(_c.pref.isAlbumBrowserShowDateOr(false)); + late final _isDoubleTapExitController = + BehaviorSubject.seeded(_c.pref.isDoubleTapExitOr(false)); } diff --git a/app/lib/widget/settings.dart b/app/lib/widget/settings.dart index 3406cde2..945db092 100644 --- a/app/lib/widget/settings.dart +++ b/app/lib/widget/settings.dart @@ -23,6 +23,7 @@ import 'package:nc_photos/widget/settings/developer_settings.dart'; import 'package:nc_photos/widget/settings/expert_settings.dart'; import 'package:nc_photos/widget/settings/language_settings.dart'; import 'package:nc_photos/widget/settings/metadata_settings.dart'; +import 'package:nc_photos/widget/settings/misc_settings.dart'; import 'package:nc_photos/widget/settings/photos_settings.dart'; import 'package:nc_photos/widget/settings/settings_list_caption.dart'; import 'package:nc_photos/widget/settings/theme_settings.dart'; @@ -136,7 +137,7 @@ class _SettingsState extends State { _SubPageItem( leading: const Icon(Icons.emoji_symbols_outlined), label: L10n.global().settingsMiscellaneousTitle, - pageBuilder: () => const _MiscSettings(), + pageBuilder: () => const MiscSettings(), ), // if (_enabledExperiments.isNotEmpty) // _SubPageItem( @@ -536,95 +537,5 @@ class _EnhanceResolutionSliderState extends State<_EnhanceResolutionSlider> { late int _height; } -class _MiscSettings extends StatefulWidget { - const _MiscSettings({Key? key}) : super(key: key); - - @override - createState() => _MiscSettingsState(); -} - -@npLog -class _MiscSettingsState extends State<_MiscSettings> { - @override - initState() { - super.initState(); - _isPhotosTabSortByName = Pref().isPhotosTabSortByNameOr(); - _isDoubleTapExit = Pref().isDoubleTapExitOr(); - } - - @override - build(BuildContext context) { - return Scaffold( - body: Builder( - builder: (context) => _buildContent(context), - ), - ); - } - - Widget _buildContent(BuildContext context) { - return CustomScrollView( - slivers: [ - SliverAppBar( - pinned: true, - title: Text(L10n.global().settingsMiscellaneousTitle), - ), - SliverList( - delegate: SliverChildListDelegate( - [ - SwitchListTile( - title: Text(L10n.global().settingsDoubleTapExitTitle), - value: _isDoubleTapExit, - onChanged: (value) => _onDoubleTapExitChanged(value), - ), - SwitchListTile( - title: Text(L10n.global().settingsPhotosTabSortByNameTitle), - value: _isPhotosTabSortByName, - onChanged: (value) => _onPhotosTabSortByNameChanged(value), - ), - ], - ), - ), - ], - ); - } - - Future _onDoubleTapExitChanged(bool value) async { - final oldValue = _isDoubleTapExit; - setState(() { - _isDoubleTapExit = value; - }); - if (!await Pref().setDoubleTapExit(value)) { - _log.severe("[_onDoubleTapExitChanged] Failed writing pref"); - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().writePreferenceFailureNotification), - duration: k.snackBarDurationNormal, - )); - setState(() { - _isDoubleTapExit = oldValue; - }); - } - } - - Future _onPhotosTabSortByNameChanged(bool value) async { - final oldValue = _isPhotosTabSortByName; - setState(() { - _isPhotosTabSortByName = value; - }); - if (!await Pref().setPhotosTabSortByName(value)) { - _log.severe("[_onPhotosTabSortByNameChanged] Failed writing pref"); - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().writePreferenceFailureNotification), - duration: k.snackBarDurationNormal, - )); - setState(() { - _isPhotosTabSortByName = oldValue; - }); - } - } - - late bool _isPhotosTabSortByName; - late bool _isDoubleTapExit; -} - // final _enabledExperiments = [ // ]; diff --git a/app/lib/widget/settings.g.dart b/app/lib/widget/settings.g.dart index cb5f1776..628387e5 100644 --- a/app/lib/widget/settings.g.dart +++ b/app/lib/widget/settings.g.dart @@ -19,10 +19,3 @@ extension _$_EnhancementSettingsStateNpLog on _EnhancementSettingsState { static final log = Logger("widget.settings._EnhancementSettingsState"); } - -extension _$_MiscSettingsStateNpLog on _MiscSettingsState { - // ignore: unused_element - Logger get _log => log; - - static final log = Logger("widget.settings._MiscSettingsState"); -} diff --git a/app/lib/widget/settings/misc/bloc.dart b/app/lib/widget/settings/misc/bloc.dart new file mode 100644 index 00000000..5ea0f10a --- /dev/null +++ b/app/lib/widget/settings/misc/bloc.dart @@ -0,0 +1,53 @@ +part of '../misc_settings.dart'; + +@npLog +class _Bloc extends Bloc<_Event, _State> with BlocLogger { + _Bloc({ + required this.prefController, + }) : super(_State( + isPhotosTabSortByName: prefController.isPhotosTabSortByName.value, + isDoubleTapExit: prefController.isDoubleTapExit.value, + )) { + on<_Init>(_onInit); + on<_SetPhotosTabSortByName>(_onSetPhotosTabSortByName); + on<_SetDoubleTapExit>(_onSetDoubleTapExit); + } + + @override + String get tag => _log.fullName; + + Future _onInit(_Init ev, Emitter<_State> emit) async { + _log.info(ev); + await Future.wait([ + emit.forEach( + prefController.isPhotosTabSortByName, + onData: (data) => state.copyWith(isPhotosTabSortByName: data), + onError: (e, stackTrace) { + _log.severe("[_onInit] Uncaught exception", e, stackTrace); + return state.copyWith(error: ExceptionEvent(e, stackTrace)); + }, + ), + emit.forEach( + prefController.isDoubleTapExit, + onData: (data) => state.copyWith(isDoubleTapExit: data), + onError: (e, stackTrace) { + _log.severe("[_onInit] Uncaught exception", e, stackTrace); + return state.copyWith(error: ExceptionEvent(e, stackTrace)); + }, + ), + ]); + } + + void _onSetPhotosTabSortByName( + _SetPhotosTabSortByName ev, Emitter<_State> emit) { + _log.info(ev); + prefController.setPhotosTabSortByName(ev.value); + } + + void _onSetDoubleTapExit(_SetDoubleTapExit ev, Emitter<_State> emit) { + _log.info(ev); + prefController.setDoubleTapExit(ev.value); + } + + final PrefController prefController; +} diff --git a/app/lib/widget/settings/misc/state_event.dart b/app/lib/widget/settings/misc/state_event.dart new file mode 100644 index 00000000..3afc393e --- /dev/null +++ b/app/lib/widget/settings/misc/state_event.dart @@ -0,0 +1,51 @@ +part of '../misc_settings.dart'; + +@genCopyWith +@toString +class _State { + const _State({ + required this.isPhotosTabSortByName, + required this.isDoubleTapExit, + this.error, + }); + + @override + String toString() => _$toString(); + + final bool isPhotosTabSortByName; + final bool isDoubleTapExit; + + final ExceptionEvent? error; +} + +abstract class _Event { + const _Event(); +} + +@toString +class _Init implements _Event { + const _Init(); + + @override + String toString() => _$toString(); +} + +@toString +class _SetPhotosTabSortByName implements _Event { + const _SetPhotosTabSortByName(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} + +@toString +class _SetDoubleTapExit implements _Event { + const _SetDoubleTapExit(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} diff --git a/app/lib/widget/settings/misc_settings.dart b/app/lib/widget/settings/misc_settings.dart new file mode 100644 index 00000000..fdf43c75 --- /dev/null +++ b/app/lib/widget/settings/misc_settings.dart @@ -0,0 +1,115 @@ +import 'package:copy_with/copy_with.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/bloc_util.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; +import 'package:nc_photos/exception_event.dart'; +import 'package:nc_photos/exception_util.dart' as exception_util; +import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/widget/page_visibility_mixin.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:to_string/to_string.dart'; + +part 'misc/bloc.dart'; +part 'misc/state_event.dart'; +part 'misc_settings.g.dart'; + +typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +typedef _BlocListener = BlocListener<_Bloc, _State>; +typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; + +class MiscSettings extends StatelessWidget { + const MiscSettings({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => _Bloc( + prefController: context.read(), + ), + child: const _WrappedMiscSettings(), + ); + } +} + +class _WrappedMiscSettings extends StatefulWidget { + const _WrappedMiscSettings(); + + @override + State createState() => _WrappedMiscSettingsState(); +} + +class _WrappedMiscSettingsState extends State<_WrappedMiscSettings> + with RouteAware, PageVisibilityMixin { + @override + void initState() { + super.initState(); + _bloc.add(const _Init()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MultiBlocListener( + listeners: [ + _BlocListener( + listenWhen: (previous, current) => previous.error != current.error, + listener: (context, state) { + if (state.error != null && isPageVisible()) { + SnackBarManager().showSnackBar(SnackBar( + content: + Text(exception_util.toUserString(state.error!.error)), + duration: k.snackBarDurationNormal, + )); + } + }, + ), + ], + child: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + title: Text(L10n.global().photosTabLabel), + ), + SliverList( + delegate: SliverChildListDelegate( + [ + _BlocSelector( + selector: (state) => state.isDoubleTapExit, + builder: (_, state) { + return SwitchListTile( + title: Text(L10n.global().settingsDoubleTapExitTitle), + value: state, + onChanged: (value) { + _bloc.add(_SetDoubleTapExit(value)); + }, + ); + }, + ), + _BlocSelector( + selector: (state) => state.isPhotosTabSortByName, + builder: (_, state) { + return SwitchListTile( + title: Text( + L10n.global().settingsPhotosTabSortByNameTitle), + value: state, + onChanged: (value) { + _bloc.add(_SetPhotosTabSortByName(value)); + }, + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } + + late final _bloc = context.read<_Bloc>(); +} diff --git a/app/lib/widget/settings/misc_settings.g.dart b/app/lib/widget/settings/misc_settings.g.dart new file mode 100644 index 00000000..9dae1322 --- /dev/null +++ b/app/lib/widget/settings/misc_settings.g.dart @@ -0,0 +1,86 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'misc_settings.dart'; + +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $_StateCopyWithWorker { + _State call( + {bool? isPhotosTabSortByName, + bool? isDoubleTapExit, + ExceptionEvent? error}); +} + +class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { + _$_StateCopyWithWorkerImpl(this.that); + + @override + _State call( + {dynamic isPhotosTabSortByName, + dynamic isDoubleTapExit, + dynamic error = copyWithNull}) { + return _State( + isPhotosTabSortByName: + isPhotosTabSortByName as bool? ?? that.isPhotosTabSortByName, + isDoubleTapExit: isDoubleTapExit as bool? ?? that.isDoubleTapExit, + error: error == copyWithNull ? that.error : error as ExceptionEvent?); + } + + final _State that; +} + +extension $_StateCopyWith on _State { + $_StateCopyWithWorker get copyWith => _$copyWith; + $_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this); +} + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$_BlocNpLog on _Bloc { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.settings.misc_settings._Bloc"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$_StateToString on _State { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_State {isPhotosTabSortByName: $isPhotosTabSortByName, isDoubleTapExit: $isDoubleTapExit, error: $error}"; + } +} + +extension _$_InitToString on _Init { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_Init {}"; + } +} + +extension _$_SetPhotosTabSortByNameToString on _SetPhotosTabSortByName { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetPhotosTabSortByName {value: $value}"; + } +} + +extension _$_SetDoubleTapExitToString on _SetDoubleTapExit { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetDoubleTapExit {value: $value}"; + } +}