nc-photos/app/lib/widget/viewer.dart

292 lines
10 KiB
Dart

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/set_as_handler.dart';
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/app_intermediate_circular_progress_indicator.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<int> fileIds;
final int startIndex;
final String? collectionId;
}
class Viewer extends StatelessWidget {
static const routeName = "/viewer";
static Route buildRoute(ViewerArguments args, RouteSettings settings) =>
CustomizableMaterialPageRoute(
transitionDuration: k.heroDurationNormal,
reverseTransitionDuration: k.heroDurationNormal,
builder: (_) => Viewer.fromArgs(args),
settings: settings,
);
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<AccountController>();
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<int> 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<StatefulWidget> 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<SystemUiOverlayStyle>(
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<DiContainer>(),
context: context,
).shareFiles(
context.bloc.account, [shareRequest.value!.file]);
}
},
),
_BlocListenerT(
selector: (state) => state.slideshowRequest,
listener: _onSlideshowRequest,
),
_BlocListenerT(
selector: (state) => state.setAsRequest,
listener: _onSetAsRequest,
),
_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<void> _onSlideshowRequest(
BuildContext context,
Unique<_SlideshowRequest?> slideshowRequest,
) async {
if (slideshowRequest.value == null) {
return;
}
final result = await showDialog<SlideshowConfig>(
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<int>(
SlideshowViewer.routeName,
arguments: SlideshowViewerArguments(
slideshowRequest.value!.fileIds,
slideshowRequest.value!.startIndex,
slideshowRequest.value!.collectionId,
result,
),
);
_log.info("[_onSlideshowRequest] Slideshow ended, jump to: $newIndex");
if (newIndex != null && context.mounted) {
context.addEvent(_RequestPage(newIndex));
}
}
void _onSetAsRequest(
BuildContext context,
Unique<_SetAsRequest?> setAsRequest,
) {
if (setAsRequest.value == null) {
return;
}
SetAsHandler(
KiwiContainer().resolve(),
context: context,
).setAsFile(setAsRequest.value!.account, setAsRequest.value!.file);
}
}
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>;
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
typedef _BlocSelector<T> = 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);
}