import 'dart:async'; import 'dart:math'; import 'package:copy_with/copy_with.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/asset.dart'; import 'package:nc_photos/bloc_util.dart'; import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/controller/collection_items_controller.dart'; import 'package:nc_photos/controller/collections_controller.dart'; import 'package:nc_photos/controller/files_controller.dart'; import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/entity/collection.dart'; import 'package:nc_photos/entity/collection/adapter.dart'; import 'package:nc_photos/entity/collection_item.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/flutter_util.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/live_photo_util.dart'; import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/share_handler.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/widget/disposable.dart'; import 'package:nc_photos/widget/file_content_view.dart'; import 'package:nc_photos/widget/handler/remove_selection_handler.dart'; import 'package:nc_photos/widget/horizontal_page_viewer.dart'; import 'package:nc_photos/widget/image_editor.dart'; import 'package:nc_photos/widget/image_enhancer.dart'; import 'package:nc_photos/widget/page_visibility_mixin.dart'; import 'package:nc_photos/widget/png_icon.dart'; import 'package:nc_photos/widget/slideshow_dialog.dart'; import 'package:nc_photos/widget/slideshow_viewer.dart'; import 'package:nc_photos/widget/viewer_detail_pane.dart'; import 'package:nc_photos/widget/viewer_mixin.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_collection/np_collection.dart'; import 'package:np_common/object_util.dart'; import 'package:np_common/or_null.dart'; import 'package:np_common/unique.dart'; import 'package:np_platform_util/np_platform_util.dart'; import 'package:to_string/to_string.dart'; part 'viewer.g.dart'; part 'viewer/app_bar.dart'; part 'viewer/app_bar_buttons.dart'; part 'viewer/bloc.dart'; part 'viewer/detail_pane.dart'; part 'viewer/state_event.dart'; part 'viewer/type.dart'; part 'viewer/view.dart'; class ViewerArguments { const ViewerArguments( this.fileIds, this.startIndex, { this.collectionId, }); final List fileIds; final int startIndex; final String? collectionId; } class Viewer extends StatelessWidget { static const routeName = "/viewer"; static Route buildRoute(ViewerArguments args) => CustomizableMaterialPageRoute( transitionDuration: k.heroDurationNormal, reverseTransitionDuration: k.heroDurationNormal, builder: (_) => Viewer.fromArgs(args), ); const Viewer({ super.key, required this.fileIds, required this.startIndex, this.collectionId, }); Viewer.fromArgs(ViewerArguments args, {Key? key}) : this( key: key, fileIds: args.fileIds, startIndex: args.startIndex, collectionId: args.collectionId, ); @override Widget build(BuildContext context) { final accountController = context.read(); return BlocProvider( create: (_) => _Bloc( KiwiContainer().resolve(), account: accountController.account, filesController: accountController.filesController, collectionsController: accountController.collectionsController, prefController: context.read(), fileIds: fileIds, startIndex: startIndex, brightness: Theme.of(context).brightness, collectionId: collectionId, )..add(const _Init()), child: const _WrappedViewer(), ); } final List fileIds; final int startIndex; /// ID of the collection these files belongs to, or null final String? collectionId; } class _WrappedViewer extends StatefulWidget { const _WrappedViewer(); @override State createState() => _WrappedViewerState(); } @npLog class _WrappedViewerState extends State<_WrappedViewer> with DisposableManagerMixin<_WrappedViewer>, ViewerControllersMixin<_WrappedViewer>, RouteAware, PageVisibilityMixin { @override Widget build(BuildContext context) { return Theme( data: buildDarkTheme(context), child: AnnotatedRegion( value: const SystemUiOverlayStyle( systemNavigationBarColor: Colors.black, systemNavigationBarIconBrightness: Brightness.dark, ), child: MultiBlocListener( listeners: [ _BlocListenerT( selector: (state) => state.imageEditorRequest, listener: (context, imageEditorRequest) { if (imageEditorRequest.value != null) { Navigator.of(context).pushNamed(ImageEditor.routeName, arguments: imageEditorRequest.value); } }, ), _BlocListenerT( selector: (state) => state.imageEnhancerRequest, listener: (context, imageEnhancerRequest) { if (imageEnhancerRequest.value != null) { Navigator.of(context).pushNamed(ImageEnhancer.routeName, arguments: imageEnhancerRequest.value); } }, ), _BlocListenerT( selector: (state) => state.shareRequest, listener: (context, shareRequest) { if (shareRequest.value != null) { ShareHandler( KiwiContainer().resolve(), context: context, ).shareFiles( context.bloc.account, [shareRequest.value!.file]); } }, ), _BlocListenerT( selector: (state) => state.slideshowRequest, listener: _onSlideshowRequest, ), _BlocListenerT( selector: (state) => state.error, listener: (context, error) { if (error != null && isPageVisible()) { SnackBarManager().showSnackBarForException(error.error); } }, ), ], child: _BlocBuilder( buildWhen: (previous, current) => previous.isShowAppBar != current.isShowAppBar || previous.isDetailPaneActive != current.isDetailPaneActive, builder: (context, state) => Scaffold( extendBodyBehindAppBar: true, extendBody: true, appBar: state.isShowAppBar ? const PreferredSize( preferredSize: Size.fromHeight(kToolbarHeight), child: _AppBar(), ) : null, bottomNavigationBar: state.isShowAppBar && !state.isDetailPaneActive ? const _BottomAppBar() : null, body: const _ContentBody(), ), ), ), ), ); } Future _onSlideshowRequest( BuildContext context, Unique<_SlideshowRequest?> slideshowRequest, ) async { if (slideshowRequest.value == null) { return; } final result = await showDialog( context: context, builder: (_) => SlideshowDialog( duration: context.bloc.prefController.slideshowDurationValue, isShuffle: context.bloc.prefController.isSlideshowShuffleValue, isRepeat: context.bloc.prefController.isSlideshowRepeatValue, isReverse: context.bloc.prefController.isSlideshowReverseValue, ), ); if (!context.mounted || result == null) { return; } unawaited( context.bloc.prefController.setSlideshowDuration(result.duration)); unawaited( context.bloc.prefController.setSlideshowShuffle(result.isShuffle)); unawaited(context.bloc.prefController.setSlideshowRepeat(result.isRepeat)); unawaited( context.bloc.prefController.setSlideshowReverse(result.isReverse)); final newIndex = await Navigator.of(context).pushNamed( SlideshowViewer.routeName, arguments: SlideshowViewerArguments( slideshowRequest.value!.account, slideshowRequest.value!.files, slideshowRequest.value!.startIndex, result, ), ); _log.info("[_onSlideshowRequest] Slideshow ended, jump to: $newIndex"); if (newIndex != null && context.mounted) { context.addEvent(_RequestPage(newIndex)); } } } 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); }