From f789d0473b7c5a05ece4df442168b2fb47471048 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Mon, 28 Oct 2024 22:03:17 +0800 Subject: [PATCH] Rewrite double tap to exit code --- .../double_tap_exit_container/bloc.dart | 63 ++++++ .../double_tap_exit_container.dart | 72 +++++++ .../double_tap_exit_container.g.dart | 79 ++++++++ .../state_event.dart | 56 ++++++ .../handler/double_tap_exit_handler.dart | 35 ---- app/lib/widget/home_collections.dart | 190 +++++++++--------- app/lib/widget/home_photos2.dart | 75 +++---- 7 files changed, 405 insertions(+), 165 deletions(-) create mode 100644 app/lib/widget/double_tap_exit_container/bloc.dart create mode 100644 app/lib/widget/double_tap_exit_container/double_tap_exit_container.dart create mode 100644 app/lib/widget/double_tap_exit_container/double_tap_exit_container.g.dart create mode 100644 app/lib/widget/double_tap_exit_container/state_event.dart delete mode 100644 app/lib/widget/handler/double_tap_exit_handler.dart diff --git a/app/lib/widget/double_tap_exit_container/bloc.dart b/app/lib/widget/double_tap_exit_container/bloc.dart new file mode 100644 index 00000000..6362a8e1 --- /dev/null +++ b/app/lib/widget/double_tap_exit_container/bloc.dart @@ -0,0 +1,63 @@ +part of 'double_tap_exit_container.dart'; + +@npLog +class _Bloc extends Bloc<_Event, _State> with BlocLogger { + _Bloc({ + required this.prefController, + }) : super(_State.init( + isDoubleTapExit: prefController.isDoubleTapExitValue, + )) { + on<_SetDoubleTapExit>(_onSetDoubleTapExit); + on<_SetCanPop>(_onSetCanPop); + on<_OnPopInvoked>(_onOnPopInvoked); + + _subscriptions.add(prefController.isDoubleTapExitChange.listen((ev) { + add(_SetDoubleTapExit(ev)); + })); + } + + @override + Future close() { + for (final s in _subscriptions) { + s.cancel(); + } + _timer?.cancel(); + return super.close(); + } + + @override + String get tag => _log.fullName; + + void _onSetDoubleTapExit(_SetDoubleTapExit ev, _Emitter emit) { + _log.info(ev); + emit(state.copyWith(isDoubleTapExit: ev.value)); + } + + void _onSetCanPop(_SetCanPop ev, _Emitter emit) { + _log.info(ev); + emit(state.copyWith(canPop: ev.value)); + } + + void _onOnPopInvoked(_OnPopInvoked ev, _Emitter emit) { + _log.info(ev); + if (state.isDoubleTapExit && !state.canPop) { + emit(state.copyWith(canPop: true)); + _timer?.cancel(); + _timer = Timer( + const Duration(seconds: 5), + () { + add(const _SetCanPop(false)); + }, + ); + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().doubleTapExitNotification), + duration: k.snackBarDurationShort, + )); + } + } + + final PrefController prefController; + + final _subscriptions = []; + Timer? _timer; +} diff --git a/app/lib/widget/double_tap_exit_container/double_tap_exit_container.dart b/app/lib/widget/double_tap_exit_container/double_tap_exit_container.dart new file mode 100644 index 00000000..a9e337e8 --- /dev/null +++ b/app/lib/widget/double_tap_exit_container/double_tap_exit_container.dart @@ -0,0 +1,72 @@ +import 'dart:async'; + +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/k.dart' as k; +import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:to_string/to_string.dart'; + +part 'bloc.dart'; +part 'double_tap_exit_container.g.dart'; +part 'state_event.dart'; + +class DoubleTapExitContainer extends StatelessWidget { + const DoubleTapExitContainer({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => _Bloc( + prefController: context.read(), + ), + child: _WrappedDoubleTapExitContainer(child: child), + ); + } + + final Widget child; +} + +class _WrappedDoubleTapExitContainer extends StatelessWidget { + const _WrappedDoubleTapExitContainer({ + required this.child, + }); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => + previous.isDoubleTapExit != current.isDoubleTapExit || + previous.canPop != current.canPop, + builder: (context, state) => PopScope( + canPop: !state.isDoubleTapExit || state.canPop, + onPopInvoked: (didPop) { + context.addEvent(_OnPopInvoked(didPop)); + }, + child: child, + ), + ); + } + + final Widget child; +} + +typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +// typedef _BlocListener = BlocListener<_Bloc, _State>; +// typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; +// typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; +typedef _Emitter = Emitter<_State>; + +extension on BuildContext { + _Bloc get bloc => read<_Bloc>(); + // _State get state => bloc.state; + void addEvent(_Event event) => bloc.add(event); +} diff --git a/app/lib/widget/double_tap_exit_container/double_tap_exit_container.g.dart b/app/lib/widget/double_tap_exit_container/double_tap_exit_container.g.dart new file mode 100644 index 00000000..1a845daf --- /dev/null +++ b/app/lib/widget/double_tap_exit_container/double_tap_exit_container.g.dart @@ -0,0 +1,79 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'double_tap_exit_container.dart'; + +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $_StateCopyWithWorker { + _State call({bool? isDoubleTapExit, bool? canPop}); +} + +class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { + _$_StateCopyWithWorkerImpl(this.that); + + @override + _State call({dynamic isDoubleTapExit, dynamic canPop}) { + return _State( + isDoubleTapExit: isDoubleTapExit as bool? ?? that.isDoubleTapExit, + canPop: canPop as bool? ?? that.canPop); + } + + 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.double_tap_exit_container.double_tap_exit_container._Bloc"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$_StateToString on _State { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_State {isDoubleTapExit: $isDoubleTapExit, canPop: $canPop}"; + } +} + +extension _$_SetDoubleTapExitToString on _SetDoubleTapExit { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetDoubleTapExit {value: $value}"; + } +} + +extension _$_SetCanPopToString on _SetCanPop { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetCanPop {value: $value}"; + } +} + +extension _$_OnPopInvokedToString on _OnPopInvoked { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_OnPopInvoked {didPop: $didPop}"; + } +} diff --git a/app/lib/widget/double_tap_exit_container/state_event.dart b/app/lib/widget/double_tap_exit_container/state_event.dart new file mode 100644 index 00000000..4aee26fb --- /dev/null +++ b/app/lib/widget/double_tap_exit_container/state_event.dart @@ -0,0 +1,56 @@ +part of 'double_tap_exit_container.dart'; + +@genCopyWith +@toString +class _State { + const _State({ + required this.isDoubleTapExit, + required this.canPop, + }); + + factory _State.init({ + required bool isDoubleTapExit, + }) => + _State( + isDoubleTapExit: isDoubleTapExit, + canPop: false, + ); + + @override + String toString() => _$toString(); + + final bool isDoubleTapExit; + final bool canPop; +} + +abstract class _Event {} + +@toString +class _SetDoubleTapExit implements _Event { + const _SetDoubleTapExit(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} + +@toString +class _SetCanPop implements _Event { + const _SetCanPop(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} + +@toString +class _OnPopInvoked implements _Event { + const _OnPopInvoked(this.didPop); + + @override + String toString() => _$toString(); + + final bool didPop; +} diff --git a/app/lib/widget/handler/double_tap_exit_handler.dart b/app/lib/widget/handler/double_tap_exit_handler.dart deleted file mode 100644 index 06d61785..00000000 --- a/app/lib/widget/handler/double_tap_exit_handler.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:clock/clock.dart'; -import 'package:flutter/material.dart'; -import 'package:nc_photos/app_localizations.dart'; -import 'package:nc_photos/entity/pref.dart'; -import 'package:nc_photos/k.dart' as k; -import 'package:nc_photos/snack_bar_manager.dart'; - -class DoubleTapExitHandler { - factory DoubleTapExitHandler() => _inst; - - DoubleTapExitHandler._(); - - /// Return if this back button event should actually exit the app - bool call() { - if (!Pref().isDoubleTapExitOr()) { - return true; - } - final now = clock.now().toUtc(); - _lastBackButtonAt ??= now.subtract(const Duration(days: 1)); - if (now.difference(_lastBackButtonAt!) < const Duration(seconds: 5)) { - return true; - } else { - _lastBackButtonAt = now; - SnackBarManager().showSnackBar(SnackBar( - content: Text(L10n.global().doubleTapExitNotification), - duration: k.snackBarDurationShort, - )); - return false; - } - } - - static final _inst = DoubleTapExitHandler._(); - - DateTime? _lastBackButtonAt; -} diff --git a/app/lib/widget/home_collections.dart b/app/lib/widget/home_collections.dart index 5e8ee58a..d6eb4c16 100644 --- a/app/lib/widget/home_collections.dart +++ b/app/lib/widget/home_collections.dart @@ -33,9 +33,9 @@ import 'package:nc_photos/widget/album_importer.dart'; import 'package:nc_photos/widget/archive_browser.dart'; import 'package:nc_photos/widget/collection_browser.dart'; import 'package:nc_photos/widget/collection_grid_item.dart'; +import 'package:nc_photos/widget/double_tap_exit_container/double_tap_exit_container.dart'; import 'package:nc_photos/widget/enhanced_photo_browser.dart'; import 'package:nc_photos/widget/fade_out_list.dart'; -import 'package:nc_photos/widget/handler/double_tap_exit_handler.dart'; import 'package:nc_photos/widget/home_app_bar.dart'; import 'package:nc_photos/widget/map_browser.dart'; import 'package:nc_photos/widget/navigation_bar_blur_filter.dart'; @@ -46,7 +46,6 @@ import 'package:nc_photos/widget/selection_app_bar.dart'; import 'package:nc_photos/widget/sharing_browser.dart'; import 'package:nc_photos/widget/trashbin_browser.dart'; import 'package:np_codegen/np_codegen.dart'; -import 'package:np_platform_util/np_platform_util.dart'; import 'package:np_ui/np_ui.dart'; import 'package:to_string/to_string.dart'; @@ -96,7 +95,7 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections> @override Widget build(BuildContext context) { - final content = MultiBlocListener( + return MultiBlocListener( listeners: [ _BlocListener( listenWhen: (previous, current) => @@ -127,103 +126,108 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections> }, ), ], - child: Stack( - children: [ - RefreshIndicator( - onRefresh: () async { - _bloc.add(const _ReloadCollections()); - await _bloc.stream.first; - }, - child: CustomScrollView( - slivers: [ - _BlocBuilder( - buildWhen: (previous, current) => - previous.selectedItems.isEmpty != - current.selectedItems.isEmpty, - builder: (context, state) => state.selectedItems.isEmpty - ? const _AppBar() - : const _SelectionAppBar(), - ), - const SliverToBoxAdapter( - child: SizedBox(height: 8), - ), - _BlocBuilder( - buildWhen: (previous, current) => - previous.transformedItems != current.transformedItems || - previous.selectedItems != current.selectedItems, - builder: (context, state) => SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 8), - sliver: SelectableItemList( - maxCrossAxisExtent: 256, - childBorderRadius: BorderRadius.zero, - indicatorAlignment: const Alignment(-.92, -.92), - items: state.transformedItems, - itemBuilder: (_, __, item) { - return _BlocSelector( - selector: (state) => - state.itemCounts[item.collection.id], - builder: (context, itemCount) => _ItemView( - account: _bloc.account, - item: item, - collectionItemCountOverride: itemCount, - ), - ); - }, - staggeredTileBuilder: (_, __) => - const StaggeredTile.count(1, 1), - selectedItems: state.selectedItems, - onSelectionChange: (_, selected) { - _bloc.add(_SetSelectedItems(items: selected.cast())); - }, - onItemTap: (context, _, item) { - Navigator.of(context).pushNamed( - CollectionBrowser.routeName, - arguments: - CollectionBrowserArguments(item.collection), - ); - }, - ), - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: AppDimension.of(context).homeBottomAppBarHeight, - ), - ), - ], - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: NavigationBarBlurFilter( - height: AppDimension.of(context).homeBottomAppBarHeight, - ), - ), - ], + child: _BlocSelector( + selector: (state) => state.selectedItems.isEmpty, + builder: (context, isSelectedEmpty) => isSelectedEmpty + ? const DoubleTapExitContainer( + child: _BodyView(), + ) + : PopScope( + canPop: false, + onPopInvoked: (_) { + context.addEvent(const _SetSelectedItems(items: {})); + }, + child: const _BodyView(), + ), ), ); - if (getRawPlatform() == NpPlatform.android) { - return WillPopScope( - onWillPop: () => _onBackButtonPressed(context), - child: content, - ); - } else { - return content; - } - } - - Future _onBackButtonPressed(BuildContext context) async { - if (context.state.selectedItems.isEmpty) { - return DoubleTapExitHandler()(); - } else { - context.addEvent(const _SetSelectedItems(items: {})); - return false; - } } late final _Bloc _bloc = context.read(); } +class _BodyView extends StatelessWidget { + const _BodyView(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + RefreshIndicator( + onRefresh: () async { + context.addEvent(const _ReloadCollections()); + await context.bloc.stream.first; + }, + child: CustomScrollView( + slivers: [ + _BlocBuilder( + buildWhen: (previous, current) => + previous.selectedItems.isEmpty != + current.selectedItems.isEmpty, + builder: (context, state) => state.selectedItems.isEmpty + ? const _AppBar() + : const _SelectionAppBar(), + ), + const SliverToBoxAdapter( + child: SizedBox(height: 8), + ), + _BlocBuilder( + buildWhen: (previous, current) => + previous.transformedItems != current.transformedItems || + previous.selectedItems != current.selectedItems, + builder: (context, state) => SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 8), + sliver: SelectableItemList( + maxCrossAxisExtent: 256, + childBorderRadius: BorderRadius.zero, + indicatorAlignment: const Alignment(-.92, -.92), + items: state.transformedItems, + itemBuilder: (_, __, item) { + return _BlocSelector( + selector: (state) => + state.itemCounts[item.collection.id], + builder: (context, itemCount) => _ItemView( + account: context.bloc.account, + item: item, + collectionItemCountOverride: itemCount, + ), + ); + }, + staggeredTileBuilder: (_, __) => + const StaggeredTile.count(1, 1), + selectedItems: state.selectedItems, + onSelectionChange: (_, selected) { + context + .addEvent(_SetSelectedItems(items: selected.cast())); + }, + onItemTap: (context, _, item) { + Navigator.of(context).pushNamed( + CollectionBrowser.routeName, + arguments: CollectionBrowserArguments(item.collection), + ); + }, + ), + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: AppDimension.of(context).homeBottomAppBarHeight, + ), + ), + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: NavigationBarBlurFilter( + height: AppDimension.of(context).homeBottomAppBarHeight, + ), + ), + ], + ); + } +} + typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; typedef _BlocListener = BlocListener<_Bloc, _State>; // typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index e4000de8..07e3a139 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -45,9 +45,9 @@ import 'package:nc_photos/theme/dimension.dart'; import 'package:nc_photos/url_launcher_util.dart'; import 'package:nc_photos/widget/collection_browser.dart'; import 'package:nc_photos/widget/collection_picker.dart'; +import 'package:nc_photos/widget/double_tap_exit_container/double_tap_exit_container.dart'; import 'package:nc_photos/widget/file_sharer_dialog.dart'; import 'package:nc_photos/widget/finger_listener.dart'; -import 'package:nc_photos/widget/handler/double_tap_exit_handler.dart'; import 'package:nc_photos/widget/home_app_bar.dart'; import 'package:nc_photos/widget/navigation_bar_blur_filter.dart'; import 'package:nc_photos/widget/network_thumbnail.dart'; @@ -64,7 +64,6 @@ import 'package:np_common/object_util.dart'; import 'package:np_common/or_null.dart'; import 'package:np_datetime/np_datetime.dart'; import 'package:np_db/np_db.dart'; -import 'package:np_platform_util/np_platform_util.dart'; import 'package:np_ui/np_ui.dart'; import 'package:to_string/to_string.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -116,12 +115,12 @@ class _WrappedHomePhotosState extends State<_WrappedHomePhotos> { @override void initState() { super.initState(); - _bloc.add(const _LoadItems()); + context.addEvent(const _LoadItems()); } @override Widget build(BuildContext context) { - final content = VisibilityDetector( + return VisibilityDetector( key: _key, onVisibilityChanged: (info) { final isVisible = info.visibleFraction >= 0.2; @@ -177,47 +176,49 @@ class _WrappedHomePhotosState extends State<_WrappedHomePhotos> { }, ), ], - child: _BlocSelector( - selector: (state) => - state.files.isEmpty && state.syncProgress != null, - builder: (context, isInitialSyncing) { - if (isInitialSyncing) { - return const _InitialSyncBody(); - } else { - return Shimmer( - linearGradient: Theme.of(context).photoGridShimmerGradient, - child: const _Body(), - ); - } - }, + child: _BlocSelector( + selector: (state) => state.selectedItems.isEmpty, + builder: (context, isSelectedEmpty) => isSelectedEmpty + ? const DoubleTapExitContainer( + child: _BodyView(), + ) + : PopScope( + canPop: false, + onPopInvoked: (_) { + context.addEvent(const _SetSelectedItems(items: {})); + }, + child: const _BodyView(), + ), ), ), ); - if (getRawPlatform() == NpPlatform.android) { - return WillPopScope( - onWillPop: () => _onBackButtonPressed(context), - child: content, - ); - } else { - return content; - } } - Future _onBackButtonPressed(BuildContext context) async { - if (context.state.selectedItems.isEmpty) { - return DoubleTapExitHandler()(); - } else { - context.addEvent(const _SetSelectedItems(items: {})); - return false; - } - } - - late final _bloc = context.bloc; - final _key = GlobalKey(); bool? _isVisible; } +class _BodyView extends StatelessWidget { + const _BodyView(); + + @override + Widget build(BuildContext context) { + return _BlocSelector( + selector: (state) => state.files.isEmpty && state.syncProgress != null, + builder: (context, isInitialSyncing) { + if (isInitialSyncing) { + return const _InitialSyncBody(); + } else { + return Shimmer( + linearGradient: Theme.of(context).photoGridShimmerGradient, + child: const _Body(), + ); + } + }, + ); + } +} + class _InitialSyncBody extends StatelessWidget { const _InitialSyncBody(); @@ -521,7 +522,7 @@ typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; extension on BuildContext { _Bloc get bloc => read<_Bloc>(); - _State get state => bloc.state; + // _State get state => bloc.state; void addEvent(_Event event) => bloc.add(event); }