mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-27 09:24:45 +01:00
374 lines
14 KiB
Dart
374 lines
14 KiB
Dart
|
import 'dart:async';
|
||
|
|
||
|
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||
|
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
|
||
|
import 'package:clock/clock.dart';
|
||
|
import 'package:collection/collection.dart';
|
||
|
import 'package:copy_with/copy_with.dart';
|
||
|
import 'package:flutter/material.dart';
|
||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||
|
import 'package:kiwi/kiwi.dart';
|
||
|
import 'package:logging/logging.dart';
|
||
|
import 'package:nc_photos/account.dart';
|
||
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||
|
import 'package:nc_photos/app_localizations.dart';
|
||
|
import 'package:nc_photos/bloc_util.dart';
|
||
|
import 'package:nc_photos/cache_manager_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/pref_controller.dart';
|
||
|
import 'package:nc_photos/debug_util.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/collection_item/new_item.dart';
|
||
|
import 'package:nc_photos/entity/collection_item/sorter.dart';
|
||
|
import 'package:nc_photos/entity/collection_item/util.dart';
|
||
|
import 'package:nc_photos/entity/file_descriptor.dart';
|
||
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||
|
import 'package:nc_photos/exception_event.dart';
|
||
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||
|
import 'package:nc_photos/flutter_util.dart' as flutter_util;
|
||
|
import 'package:nc_photos/k.dart' as k;
|
||
|
import 'package:nc_photos/np_api_util.dart';
|
||
|
import 'package:nc_photos/object_extension.dart';
|
||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||
|
import 'package:nc_photos/use_case/archive_file.dart';
|
||
|
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
|
||
|
import 'package:nc_photos/use_case/remove.dart';
|
||
|
import 'package:nc_photos/widget/collection_picker.dart';
|
||
|
import 'package:nc_photos/widget/draggable_item_list.dart';
|
||
|
import 'package:nc_photos/widget/fancy_option_picker.dart';
|
||
|
import 'package:nc_photos/widget/file_sharer.dart';
|
||
|
import 'package:nc_photos/widget/network_thumbnail.dart';
|
||
|
import 'package:nc_photos/widget/page_visibility_mixin.dart';
|
||
|
import 'package:nc_photos/widget/photo_list_item.dart';
|
||
|
import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util;
|
||
|
import 'package:nc_photos/widget/selectable_item_list.dart';
|
||
|
import 'package:nc_photos/widget/selection_app_bar.dart';
|
||
|
import 'package:nc_photos/widget/simple_input_dialog.dart';
|
||
|
import 'package:nc_photos/widget/viewer.dart';
|
||
|
import 'package:nc_photos/widget/zoom_menu_button.dart';
|
||
|
import 'package:np_codegen/np_codegen.dart';
|
||
|
import 'package:to_string/to_string.dart';
|
||
|
|
||
|
part 'collection_browser.g.dart';
|
||
|
part 'collection_browser/app_bar.dart';
|
||
|
part 'collection_browser/bloc.dart';
|
||
|
part 'collection_browser/state_event.dart';
|
||
|
part 'collection_browser/type.dart';
|
||
|
|
||
|
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||
|
|
||
|
class CollectionBrowserArguments {
|
||
|
const CollectionBrowserArguments(this.collection);
|
||
|
|
||
|
final Collection collection;
|
||
|
}
|
||
|
|
||
|
/// Browse the content of a collection
|
||
|
class CollectionBrowser extends StatelessWidget {
|
||
|
static const routeName = "/collection-browser";
|
||
|
|
||
|
static Route buildRoute(CollectionBrowserArguments args) => MaterialPageRoute(
|
||
|
builder: (context) => CollectionBrowser.fromArgs(args),
|
||
|
);
|
||
|
|
||
|
const CollectionBrowser({
|
||
|
super.key,
|
||
|
required this.collection,
|
||
|
});
|
||
|
|
||
|
CollectionBrowser.fromArgs(
|
||
|
CollectionBrowserArguments args, {
|
||
|
Key? key,
|
||
|
}) : this(
|
||
|
key: key,
|
||
|
collection: args.collection,
|
||
|
);
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return BlocProvider(
|
||
|
create: (_) => _Bloc(
|
||
|
container: KiwiContainer().resolve(),
|
||
|
account: context.read<AccountController>().account,
|
||
|
collectionsController:
|
||
|
context.read<AccountController>().collectionsController,
|
||
|
collection: collection,
|
||
|
),
|
||
|
child: const _WrappedCollectionBrowser(),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
final Collection collection;
|
||
|
}
|
||
|
|
||
|
class _WrappedCollectionBrowser extends StatefulWidget {
|
||
|
const _WrappedCollectionBrowser();
|
||
|
|
||
|
@override
|
||
|
State<StatefulWidget> createState() => _WrappedCollectionBrowserState();
|
||
|
}
|
||
|
|
||
|
@npLog
|
||
|
class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
|
||
|
with RouteAware, PageVisibilityMixin {
|
||
|
@override
|
||
|
void initState() {
|
||
|
super.initState();
|
||
|
_bloc.add(const _LoadItems());
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return WillPopScope(
|
||
|
onWillPop: () async {
|
||
|
if (_bloc.state.isEditMode) {
|
||
|
_bloc.add(const _CancelEdit());
|
||
|
return false;
|
||
|
} else if (_bloc.state.selectedItems.isNotEmpty) {
|
||
|
_bloc.add(const _SetSelectedItems(items: {}));
|
||
|
return false;
|
||
|
} else {
|
||
|
return true;
|
||
|
}
|
||
|
},
|
||
|
child: Scaffold(
|
||
|
body: MultiBlocListener(
|
||
|
listeners: [
|
||
|
BlocListener<_Bloc, _State>(
|
||
|
listenWhen: (previous, current) =>
|
||
|
previous.items != current.items,
|
||
|
listener: (context, state) {
|
||
|
_bloc.add(_TransformItems(
|
||
|
items: state.items,
|
||
|
));
|
||
|
},
|
||
|
),
|
||
|
BlocListener<_Bloc, _State>(
|
||
|
listenWhen: (previous, current) =>
|
||
|
previous.editItems != current.editItems,
|
||
|
listener: (context, state) {
|
||
|
if (state.editItems != null) {
|
||
|
_bloc.add(_TransformEditItems(
|
||
|
items: state.editItems!,
|
||
|
));
|
||
|
}
|
||
|
},
|
||
|
),
|
||
|
BlocListener<_Bloc, _State>(
|
||
|
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,
|
||
|
));
|
||
|
}
|
||
|
},
|
||
|
),
|
||
|
BlocListener<_Bloc, _State>(
|
||
|
listenWhen: (previous, current) =>
|
||
|
previous.message != current.message,
|
||
|
listener: (context, state) {
|
||
|
if (state.message != null && isPageVisible()) {
|
||
|
SnackBarManager().showSnackBar(SnackBar(
|
||
|
content: Text(state.message!),
|
||
|
duration: k.snackBarDurationNormal,
|
||
|
));
|
||
|
}
|
||
|
},
|
||
|
),
|
||
|
],
|
||
|
child: Stack(
|
||
|
fit: StackFit.expand,
|
||
|
children: [
|
||
|
CustomScrollView(
|
||
|
slivers: [
|
||
|
_BlocBuilder(
|
||
|
buildWhen: (previous, current) =>
|
||
|
previous.selectedItems.isEmpty !=
|
||
|
current.selectedItems.isEmpty ||
|
||
|
previous.isEditMode != current.isEditMode,
|
||
|
builder: (context, state) {
|
||
|
if (state.isEditMode) {
|
||
|
return const _EditAppBar();
|
||
|
} else if (state.selectedItems.isNotEmpty) {
|
||
|
return const _SelectionAppBar();
|
||
|
} else {
|
||
|
return const _AppBar();
|
||
|
}
|
||
|
},
|
||
|
),
|
||
|
SliverToBoxAdapter(
|
||
|
child: _BlocBuilder(
|
||
|
buildWhen: (previous, current) =>
|
||
|
previous.isLoading != current.isLoading,
|
||
|
builder: (context, state) => state.isLoading
|
||
|
? const LinearProgressIndicator()
|
||
|
: const SizedBox(height: 4),
|
||
|
),
|
||
|
),
|
||
|
_BlocBuilder(
|
||
|
buildWhen: (previous, current) =>
|
||
|
previous.isEditMode != current.isEditMode,
|
||
|
builder: (context, state) {
|
||
|
if (!state.isEditMode) {
|
||
|
return const _ContentList();
|
||
|
} else {
|
||
|
if (state.collection.capabilities
|
||
|
.contains(CollectionCapability.manualSort)) {
|
||
|
return const _EditContentList();
|
||
|
} else {
|
||
|
return const SliverIgnorePointer(
|
||
|
ignoring: true,
|
||
|
sliver: SliverOpacity(
|
||
|
opacity: .25,
|
||
|
sliver: _ContentList(),
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
_BlocBuilder(
|
||
|
buildWhen: (previous, current) =>
|
||
|
previous.isEditBusy != current.isEditBusy,
|
||
|
builder: (context, state) {
|
||
|
if (state.isEditBusy) {
|
||
|
return Container(
|
||
|
color: Colors.black.withOpacity(.5),
|
||
|
alignment: Alignment.center,
|
||
|
child: const CircularProgressIndicator(),
|
||
|
);
|
||
|
} else {
|
||
|
return const SizedBox.shrink();
|
||
|
}
|
||
|
},
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
late final _bloc = context.read<_Bloc>();
|
||
|
}
|
||
|
|
||
|
class _ContentList extends StatelessWidget {
|
||
|
const _ContentList();
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return StreamBuilder<int>(
|
||
|
stream: context.read<PrefController>().albumBrowserZoomLevel,
|
||
|
initialData: context.read<PrefController>().albumBrowserZoomLevel.value,
|
||
|
builder: (_, zoomLevel) {
|
||
|
if (zoomLevel.hasError) {
|
||
|
context.read<_Bloc>().add(
|
||
|
_SetMessage(L10n.global().writePreferenceFailureNotification));
|
||
|
}
|
||
|
return _BlocBuilder(
|
||
|
buildWhen: (previous, current) =>
|
||
|
previous.transformedItems != current.transformedItems ||
|
||
|
previous.selectedItems != current.selectedItems,
|
||
|
builder: (context, state) {
|
||
|
return SelectableItemList<_Item>(
|
||
|
maxCrossAxisExtent: photo_list_util
|
||
|
.getThumbSize(zoomLevel.requireData)
|
||
|
.toDouble(),
|
||
|
items: state.transformedItems,
|
||
|
itemBuilder: (context, _, item) => item.buildWidget(context),
|
||
|
staggeredTileBuilder: (_, item) => item.staggeredTile,
|
||
|
selectedItems: state.selectedItems,
|
||
|
onSelectionChange: (_, selected) {
|
||
|
context
|
||
|
.read<_Bloc>()
|
||
|
.add(_SetSelectedItems(items: selected.cast()));
|
||
|
},
|
||
|
onItemTap: (context, index, _) {
|
||
|
final actualIndex = index -
|
||
|
state.transformedItems
|
||
|
.sublist(0, index)
|
||
|
.where((e) => e is! _ActualItem)
|
||
|
.length;
|
||
|
Navigator.of(context).pushNamed(
|
||
|
Viewer.routeName,
|
||
|
arguments: ViewerArguments(
|
||
|
context.read<_Bloc>().account,
|
||
|
state.transformedItems
|
||
|
.whereType<_FileItem>()
|
||
|
.map((e) => e.file)
|
||
|
.toList(),
|
||
|
actualIndex,
|
||
|
),
|
||
|
);
|
||
|
},
|
||
|
);
|
||
|
},
|
||
|
);
|
||
|
},
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class _EditContentList extends StatelessWidget {
|
||
|
const _EditContentList();
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return StreamBuilder<int>(
|
||
|
stream: context.read<PrefController>().albumBrowserZoomLevel,
|
||
|
initialData: context.read<PrefController>().albumBrowserZoomLevel.value,
|
||
|
builder: (_, zoomLevel) {
|
||
|
if (zoomLevel.hasError) {
|
||
|
context.read<_Bloc>().add(
|
||
|
_SetMessage(L10n.global().writePreferenceFailureNotification));
|
||
|
}
|
||
|
return _BlocBuilder(
|
||
|
buildWhen: (previous, current) =>
|
||
|
previous.editTransformedItems != current.editTransformedItems,
|
||
|
builder: (context, state) {
|
||
|
if (state.collection.capabilities
|
||
|
.contains(CollectionCapability.manualSort)) {
|
||
|
return DraggableItemList<_Item>(
|
||
|
maxCrossAxisExtent: photo_list_util
|
||
|
.getThumbSize(zoomLevel.requireData)
|
||
|
.toDouble(),
|
||
|
items: state.editTransformedItems ?? state.transformedItems,
|
||
|
itemBuilder: (context, _, item) => item.buildWidget(context),
|
||
|
itemDragFeedbackBuilder: (context, _, item) =>
|
||
|
item.buildDragFeedbackWidget(context),
|
||
|
staggeredTileBuilder: (_, item) => item.staggeredTile,
|
||
|
onDragResult: (results) {
|
||
|
context.read<_Bloc>().add(_EditManualSort(results));
|
||
|
},
|
||
|
);
|
||
|
} else {
|
||
|
return SelectableItemList<_Item>(
|
||
|
maxCrossAxisExtent: photo_list_util
|
||
|
.getThumbSize(zoomLevel.requireData)
|
||
|
.toDouble(),
|
||
|
items: state.editTransformedItems ?? state.transformedItems,
|
||
|
itemBuilder: (context, _, item) => item.buildWidget(context),
|
||
|
staggeredTileBuilder: (_, item) => item.staggeredTile,
|
||
|
);
|
||
|
}
|
||
|
},
|
||
|
);
|
||
|
},
|
||
|
);
|
||
|
}
|
||
|
}
|