diff --git a/app/lib/bloc/settings/expert.dart b/app/lib/bloc/settings/expert.dart new file mode 100644 index 00000000..53deb132 --- /dev/null +++ b/app/lib/bloc/settings/expert.dart @@ -0,0 +1,79 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:copy_with/copy_with.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; +import 'package:np_codegen/np_codegen.dart'; +import 'package:to_string/to_string.dart'; + +part 'expert.g.dart'; + +@autoCopyWith +@toString +class ExpertSettingsState { + const ExpertSettingsState({ + this.lastSuccessful, + }); + + @override + String toString() => _$toString(); + + final ExpertSettingsEvent? lastSuccessful; +} + +abstract class ExpertSettingsEvent { + const ExpertSettingsEvent(); +} + +@toString +class ExpertSettingsClearCacheDatabase extends ExpertSettingsEvent { + ExpertSettingsClearCacheDatabase(); + + @override + String toString() => _$toString(); +} + +class ExpertSettingsError { + const ExpertSettingsError(this.ev, [this.error, this.stackTrace]); + + final ExpertSettingsEvent ev; + final Object? error; + final StackTrace? stackTrace; +} + +@npLog +class ExpertSettingsBloc + extends Bloc { + ExpertSettingsBloc(DiContainer c) + : _c = c, + super(const ExpertSettingsState()) { + on(_onClearCacheDatabase); + } + + static bool require(DiContainer c) => + DiContainer.has(c, DiType.pref) && DiContainer.has(c, DiType.sqliteDb); + + Stream errorStream() => _errorStream.stream; + + Future _onClearCacheDatabase(ExpertSettingsClearCacheDatabase ev, + Emitter emit) async { + try { + await _c.sqliteDb.use((db) async { + await db.truncate(); + final accounts = _c.pref.getAccounts3Or([]); + for (final a in accounts) { + await db.insertAccountOf(a); + } + }); + emit(state.copyWith(lastSuccessful: Nullable(ev))); + } catch (e, stackTrace) { + _log.shout("[_onClearCacheDatabase] Uncaught exception", e, stackTrace); + _errorStream.add(ExpertSettingsError(ev, e, stackTrace)); + } + } + + final DiContainer _c; + final _errorStream = StreamController.broadcast(); +} diff --git a/app/lib/bloc/settings/expert.g.dart b/app/lib/bloc/settings/expert.g.dart new file mode 100644 index 00000000..b8ccd958 --- /dev/null +++ b/app/lib/bloc/settings/expert.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'expert.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +extension $ExpertSettingsStateCopyWith on ExpertSettingsState { + ExpertSettingsState copyWith( + {Nullable? lastSuccessful}) => + _$copyWith(lastSuccessful: lastSuccessful); + + ExpertSettingsState _$copyWith( + {Nullable? lastSuccessful}) { + return ExpertSettingsState( + lastSuccessful: + lastSuccessful != null ? lastSuccessful.obj : this.lastSuccessful); + } +} + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$ExpertSettingsBlocNpLog on ExpertSettingsBloc { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("bloc.settings.expert.ExpertSettingsBloc"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$ExpertSettingsStateToString on ExpertSettingsState { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "ExpertSettingsState {lastSuccessful: $lastSuccessful}"; + } +} + +extension _$ExpertSettingsClearCacheDatabaseToString + on ExpertSettingsClearCacheDatabase { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "ExpertSettingsClearCacheDatabase {}"; + } +} diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index dc479b46..dc9984b6 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -453,6 +453,14 @@ }, "settingsExperimentalTitle": "Experimental", "settingsExperimentalDescription": "Features that are not ready for everyday use", + "settingsExpertTitle": "Advanced", + "@settingsExpertTitle": { + "description": "Settings that must be tweaked with caution" + }, + "settingsExpertWarningText": "Please make sure you fully understand what each option does before proceeding", + "settingsClearCacheDatabaseTitle": "Clear file database", + "settingsClearCacheDatabaseDescription": "Clear cached file info and trigger a complete resync with the server", + "settingsClearCacheDatabaseSuccessNotification": "Database cleared successfully. You are suggested to restart the app", "settingsAboutSectionTitle": "About", "@settingsAboutSectionTitle": { "description": "Title of the about section in settings widget" @@ -1501,7 +1509,6 @@ "@loopTooltip": { "description": "Enable or disable loop in the video player" }, - "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { "description": "Error message when server responds with HTTP401" diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index 55e65dc7..9eb584ed 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -40,6 +40,11 @@ "settingsPhotosTabSortByNameTitle", "settingsExperimentalTitle", "settingsExperimentalDescription", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "rootPickerSkipConfirmationDialogContent2", "sortOptionFilenameAscendingLabel", "sortOptionFilenameDescendingLabel", @@ -226,6 +231,11 @@ "settingsPhotosTabSortByNameTitle", "settingsExperimentalTitle", "settingsExperimentalDescription", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "rootPickerSkipConfirmationDialogContent2", "timeSecondInputHint", "sortOptionFilenameAscendingLabel", @@ -405,6 +415,11 @@ "settingsSeedColorDescription", "settingsSeedColorPickerTitle", "settingsDoubleTapExitTitle", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "slideshowSetupDialogReverseTitle", "shareMethodPreviewTitle", "shareMethodPreviewDescription", @@ -474,6 +489,11 @@ "es": [ "connectingToServerInstruction", "settingsEnhanceMaxResolutionTitle2", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "initialSyncMessage", "loopTooltip" ], @@ -485,6 +505,11 @@ "settingsSeedColorTitle", "settingsSeedColorDescription", "settingsSeedColorPickerTitle", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "initialSyncMessage", "loopTooltip" ], @@ -515,6 +540,11 @@ "settingsMiscellaneousTitle", "settingsDoubleTapExitTitle", "settingsPhotosTabSortByNameTitle", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "sortOptionFilenameAscendingLabel", "sortOptionFilenameDescendingLabel", "helpTooltip", @@ -622,6 +652,11 @@ "settingsMiscellaneousTitle", "settingsDoubleTapExitTitle", "settingsPhotosTabSortByNameTitle", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "sortOptionFilenameAscendingLabel", "sortOptionFilenameDescendingLabel", "slideshowSetupDialogReverseTitle", @@ -747,6 +782,11 @@ "settingsMiscellaneousTitle", "settingsDoubleTapExitTitle", "settingsPhotosTabSortByNameTitle", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "sortOptionFilenameAscendingLabel", "sortOptionFilenameDescendingLabel", "slideshowSetupDialogReverseTitle", @@ -851,6 +891,11 @@ "settingsMiscellaneousTitle", "settingsDoubleTapExitTitle", "settingsPhotosTabSortByNameTitle", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "sortOptionFilenameAscendingLabel", "sortOptionFilenameDescendingLabel", "slideshowSetupDialogReverseTitle", @@ -955,6 +1000,11 @@ "settingsMiscellaneousTitle", "settingsDoubleTapExitTitle", "settingsPhotosTabSortByNameTitle", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "sortOptionFilenameAscendingLabel", "sortOptionFilenameDescendingLabel", "slideshowSetupDialogReverseTitle", @@ -1059,6 +1109,11 @@ "settingsMiscellaneousTitle", "settingsDoubleTapExitTitle", "settingsPhotosTabSortByNameTitle", + "settingsExpertTitle", + "settingsExpertWarningText", + "settingsClearCacheDatabaseTitle", + "settingsClearCacheDatabaseDescription", + "settingsClearCacheDatabaseSuccessNotification", "sortOptionFilenameAscendingLabel", "sortOptionFilenameDescendingLabel", "slideshowSetupDialogReverseTitle", diff --git a/app/lib/widget/settings.dart b/app/lib/widget/settings.dart index 712b5a01..4afb1895 100644 --- a/app/lib/widget/settings.dart +++ b/app/lib/widget/settings.dart @@ -27,6 +27,7 @@ import 'package:nc_photos/widget/gps_map.dart'; import 'package:nc_photos/widget/home.dart'; import 'package:nc_photos/widget/list_tile_center_leading.dart'; import 'package:nc_photos/widget/root_picker.dart'; +import 'package:nc_photos/widget/settings/expert_settings.dart'; import 'package:nc_photos/widget/settings/theme_settings.dart'; import 'package:nc_photos/widget/share_folder_picker.dart'; import 'package:nc_photos/widget/simple_input_dialog.dart'; @@ -185,6 +186,12 @@ class _SettingsState extends State { description: L10n.global().settingsExperimentalDescription, builder: () => _ExperimentalSettings(), ), + _buildSubSettings( + context, + leading: const Icon(Icons.warning_amber), + label: L10n.global().settingsExpertTitle, + builder: () => const ExpertSettings(), + ), if (_isShowDevSettings) _buildSubSettings( context, @@ -1642,10 +1649,6 @@ class _DevSettingsState extends State<_DevSettings> { SliverList( delegate: SliverChildListDelegate( [ - ListTile( - title: const Text("Clear cache database"), - onTap: () => _clearCacheDb(), - ), ListTile( title: const Text("SQL:VACUUM"), onTap: () => _runSqlVacuum(), @@ -1657,29 +1660,6 @@ class _DevSettingsState extends State<_DevSettings> { ); } - Future _clearCacheDb() async { - try { - final c = KiwiContainer().resolve(); - await c.sqliteDb.use((db) async { - await db.truncate(); - final accounts = Pref().getAccounts3Or([]); - for (final a in accounts) { - await db.insertAccountOf(a); - } - }); - SnackBarManager().showSnackBar(const SnackBar( - content: Text("Database cleared"), - duration: k.snackBarDurationShort, - )); - } catch (e, stackTrace) { - SnackBarManager().showSnackBar(SnackBar( - content: Text(exception_util.toUserString(e)), - duration: k.snackBarDurationNormal, - )); - _log.shout("[_clearCacheDb] Uncaught exception", e, stackTrace); - } - } - Future _runSqlVacuum() async { try { final c = KiwiContainer().resolve(); diff --git a/app/lib/widget/settings/expert_settings.dart b/app/lib/widget/settings/expert_settings.dart new file mode 100644 index 00000000..416b568b --- /dev/null +++ b/app/lib/widget/settings/expert_settings.dart @@ -0,0 +1,134 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:kiwi/kiwi.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/bloc/settings/expert.dart'; +import 'package:nc_photos/di_container.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:np_codegen/np_codegen.dart'; + +part 'expert_settings.g.dart'; + +class ExpertSettings extends StatelessWidget { + const ExpertSettings({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ExpertSettingsBloc(KiwiContainer().resolve()), + child: const _WrappedExpertSettings(), + ); + } +} + +class _WrappedExpertSettings extends StatefulWidget { + const _WrappedExpertSettings(); + + @override + State createState() => _WrappedExpertSettingsState(); +} + +@npLog +class _WrappedExpertSettingsState extends State<_WrappedExpertSettings> { + @override + void initState() { + super.initState(); + _errorSubscription = + context.read().errorStream().listen((error) { + if (error.ev is ExpertSettingsClearCacheDatabase) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(error.error)), + duration: k.snackBarDurationNormal, + )); + } + }); + } + + @override + void dispose() { + _errorSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Builder( + builder: (context) => + BlocListener( + listenWhen: (previous, current) => + !identical(previous.lastSuccessful, current.lastSuccessful), + listener: (context, state) { + if (state.lastSuccessful is ExpertSettingsClearCacheDatabase) { + showDialog( + context: context, + builder: (_) => AlertDialog( + content: Text(L10n.global() + .settingsClearCacheDatabaseSuccessNotification), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + MaterialLocalizations.of(context).closeButtonLabel), + ), + ], + ), + ); + } + }, + child: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + title: Text(L10n.global().settingsExpertTitle), + ), + SliverPadding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + sliver: SliverToBoxAdapter( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.warning_amber, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 8), + Text(L10n.global().settingsExpertWarningText), + ], + ), + ), + ), + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: + Text(L10n.global().settingsClearCacheDatabaseTitle), + subtitle: Text( + L10n.global().settingsClearCacheDatabaseDescription), + onTap: () { + context + .read() + .add(ExpertSettingsClearCacheDatabase()); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + late final StreamSubscription _errorSubscription; +} diff --git a/app/lib/widget/settings/expert_settings.g.dart b/app/lib/widget/settings/expert_settings.g.dart new file mode 100644 index 00000000..3f9d7466 --- /dev/null +++ b/app/lib/widget/settings/expert_settings.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'expert_settings.dart'; + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$_WrappedExpertSettingsStateNpLog on _WrappedExpertSettingsState { + // ignore: unused_element + Logger get _log => log; + + static final log = + Logger("widget.settings.expert_settings._WrappedExpertSettingsState"); +}