diff --git a/app/lib/widget/collection_browser.dart b/app/lib/widget/collection_browser.dart index 14e3067a..5fa52621 100644 --- a/app/lib/widget/collection_browser.dart +++ b/app/lib/widget/collection_browser.dart @@ -72,8 +72,7 @@ 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>; +part 'collection_browser/view.dart'; class CollectionBrowserArguments { const CollectionBrowserArguments(this.collection); @@ -160,7 +159,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> child: Scaffold( body: MultiBlocListener( listeners: [ - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.items != current.items, listener: (context, state) { @@ -169,7 +168,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> )); }, ), - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.editItems != current.editItems, listener: (context, state) { @@ -180,7 +179,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> } }, ), - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.importResult != current.importResult, listener: (context, state) { @@ -192,7 +191,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> } }, ), - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.isEditMode != current.isEditMode, listener: (context, state) { @@ -212,7 +211,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> } }, ), - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.error != current.error, listener: (context, state) { @@ -225,7 +224,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> } }, ), - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.message != current.message, listener: (context, state) { @@ -300,10 +299,8 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> overlaySliver: const _ScalingList(), ); } else { - if (context - .read<_Bloc>() - .isCollectionCapabilityPermitted( - CollectionCapability.manualSort)) { + if (context.bloc.isCollectionCapabilityPermitted( + CollectionCapability.manualSort)) { return const _EditContentList(); } else { return const _UnmodifiableEditContentList(); @@ -338,8 +335,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> } void _onPointerMove(BuildContext context, PointerMoveEvent event) { - final bloc = context.read<_Bloc>(); - if (!bloc.state.isDragging) { + if (!context.state.isDragging) { return; } if (event.position.dy >= MediaQuery.of(context).size.height - 100) { @@ -391,197 +387,18 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser> } } - late final _bloc = context.read<_Bloc>(); + late final _bloc = context.bloc; final _scrollController = ScrollController(); bool? _isDragScrollingDown; - int _finger = 0; + var _finger = 0; } -class _ContentList extends StatelessWidget { - const _ContentList(); +typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +typedef _BlocListener = BlocListener<_Bloc, _State>; +// typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; - @override - Widget build(BuildContext context) { - return _BlocBuilder( - buildWhen: (previous, current) => previous.zoom != current.zoom, - builder: (context, state) => _ContentListBody( - maxCrossAxisExtent: photo_list_util.getThumbSize(state.zoom).toDouble(), - ), - ); - } -} - -class _ScalingList extends StatelessWidget { - const _ScalingList(); - - @override - Widget build(BuildContext context) { - return _BlocBuilder( - buildWhen: (previous, current) => previous.scale != current.scale, - builder: (context, state) { - if (state.scale == null) { - return const SizedBox.shrink(); - } - int nextZoom; - if (state.scale! > 1) { - nextZoom = state.zoom + 1; - } else { - nextZoom = state.zoom - 1; - } - nextZoom = nextZoom.clamp(-1, 2); - return _ContentListBody( - maxCrossAxisExtent: photo_list_util.getThumbSize(nextZoom).toDouble(), - ); - }, - ); - } -} - -class _ContentListBody extends StatelessWidget { - const _ContentListBody({ - required this.maxCrossAxisExtent, - }); - - @override - Widget build(BuildContext context) { - final bloc = context.read<_Bloc>(); - return _BlocBuilder( - buildWhen: (previous, current) => - previous.collection != current.collection || - previous.transformedItems != current.transformedItems || - previous.selectedItems != current.selectedItems, - builder: (context, state) => SelectableItemList<_Item>( - maxCrossAxisExtent: maxCrossAxisExtent, - items: state.transformedItems, - itemBuilder: (context, _, item) => item.buildWidget(context), - staggeredTileBuilder: (_, item) => item.staggeredTile, - selectedItems: state.selectedItems, - onSelectionChange: (_, selected) { - bloc.add(_SetSelectedItems(items: selected.cast())); - }, - onItemTap: (context, index, _) { - if (state.transformedItems[index] is! _FileItem) { - return; - } - final actualIndex = index - - state.transformedItems - .sublist(0, index) - .where((e) => e is! _FileItem) - .length; - Navigator.of(context).pushNamed( - Viewer.routeName, - arguments: ViewerArguments( - bloc.account, - state.transformedItems - .whereType<_FileItem>() - .map((e) => e.file) - .toList(), - actualIndex, - fromCollection: ViewerCollectionData( - state.collection, - state.transformedItems - .whereType<_ActualItem>() - .map((e) => e.original) - .toList(), - ), - ), - ); - }, - ), - ); - } - - final double maxCrossAxisExtent; -} - -class _EditContentList extends StatelessWidget { - const _EditContentList(); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: context.read().albumBrowserZoomLevel, - initialData: context.read().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 (context.read<_Bloc>().isCollectionCapabilityPermitted( - 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)); - }, - onDraggingChanged: (value) { - context.read<_Bloc>().add(_SetDragging(value)); - }, - ); - } 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, - ); - } - }, - ); - }, - ); - } -} - -/// Unmodifiable content list under edit mode -class _UnmodifiableEditContentList extends StatelessWidget { - const _UnmodifiableEditContentList(); - - @override - Widget build(BuildContext context) { - return SliverIgnorePointer( - ignoring: true, - sliver: SliverOpacity( - opacity: .25, - sliver: StreamBuilder( - stream: context.read().albumBrowserZoomLevel, - initialData: - context.read().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) { - 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, - ); - }, - ); - }, - ), - ), - ); - } +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/collection_browser/app_bar.dart b/app/lib/widget/collection_browser/app_bar.dart index 1931d675..a15e83c7 100644 --- a/app/lib/widget/collection_browser/app_bar.dart +++ b/app/lib/widget/collection_browser/app_bar.dart @@ -11,8 +11,8 @@ class _AppBar extends StatelessWidget { previous.items != current.items || previous.collection != current.collection, builder: (context, state) { - final bloc = context.read<_Bloc>(); - final adapter = CollectionAdapter.of(c, bloc.account, state.collection); + final adapter = + CollectionAdapter.of(c, context.bloc.account, state.collection); final canRename = adapter.isPermitted(CollectionCapability.rename); final canManualCover = adapter.isPermitted(CollectionCapability.manualCover); @@ -69,7 +69,8 @@ class _AppBar extends StatelessWidget { ], if (state.collection.contentProvider is CollectionAlbumProvider && - CollectionAdapter.of(c, bloc.account, state.collection) + CollectionAdapter.of( + c, context.bloc.account, state.collection) .isPermitted(CollectionCapability.share)) PopupMenuItem( value: _MenuOption.albumFixShare, diff --git a/app/lib/widget/collection_browser/view.dart b/app/lib/widget/collection_browser/view.dart new file mode 100644 index 00000000..16c11849 --- /dev/null +++ b/app/lib/widget/collection_browser/view.dart @@ -0,0 +1,189 @@ +part of '../collection_browser.dart'; + +class _ContentList extends StatelessWidget { + const _ContentList(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => previous.zoom != current.zoom, + builder: (context, state) => _ContentListBody( + maxCrossAxisExtent: photo_list_util.getThumbSize(state.zoom).toDouble(), + ), + ); + } +} + +class _ScalingList extends StatelessWidget { + const _ScalingList(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => previous.scale != current.scale, + builder: (context, state) { + if (state.scale == null) { + return const SizedBox.shrink(); + } + int nextZoom; + if (state.scale! > 1) { + nextZoom = state.zoom + 1; + } else { + nextZoom = state.zoom - 1; + } + nextZoom = nextZoom.clamp(-1, 2); + return _ContentListBody( + maxCrossAxisExtent: photo_list_util.getThumbSize(nextZoom).toDouble(), + ); + }, + ); + } +} + +class _ContentListBody extends StatelessWidget { + const _ContentListBody({ + required this.maxCrossAxisExtent, + }); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => + previous.collection != current.collection || + previous.transformedItems != current.transformedItems || + previous.selectedItems != current.selectedItems, + builder: (context, state) => SelectableItemList<_Item>( + maxCrossAxisExtent: maxCrossAxisExtent, + items: state.transformedItems, + itemBuilder: (context, _, item) => item.buildWidget(context), + staggeredTileBuilder: (_, item) => item.staggeredTile, + selectedItems: state.selectedItems, + onSelectionChange: (_, selected) { + context.addEvent(_SetSelectedItems(items: selected.cast())); + }, + onItemTap: (context, index, _) { + if (state.transformedItems[index] is! _FileItem) { + return; + } + final actualIndex = index - + state.transformedItems + .sublist(0, index) + .where((e) => e is! _FileItem) + .length; + Navigator.of(context).pushNamed( + Viewer.routeName, + arguments: ViewerArguments( + context.bloc.account, + state.transformedItems + .whereType<_FileItem>() + .map((e) => e.file) + .toList(), + actualIndex, + fromCollection: ViewerCollectionData( + state.collection, + state.transformedItems + .whereType<_ActualItem>() + .map((e) => e.original) + .toList(), + ), + ), + ); + }, + ), + ); + } + + final double maxCrossAxisExtent; +} + +class _EditContentList extends StatelessWidget { + const _EditContentList(); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: context.read().albumBrowserZoomLevel, + initialData: context.read().albumBrowserZoomLevel.value, + builder: (_, zoomLevel) { + if (zoomLevel.hasError) { + context.addEvent( + _SetMessage(L10n.global().writePreferenceFailureNotification)); + } + return _BlocBuilder( + buildWhen: (previous, current) => + previous.editTransformedItems != current.editTransformedItems, + builder: (context, state) { + if (context.bloc.isCollectionCapabilityPermitted( + 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.addEvent(_EditManualSort(results)); + }, + onDraggingChanged: (value) { + context.addEvent(_SetDragging(value)); + }, + ); + } 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, + ); + } + }, + ); + }, + ); + } +} + +/// Unmodifiable content list under edit mode +class _UnmodifiableEditContentList extends StatelessWidget { + const _UnmodifiableEditContentList(); + + @override + Widget build(BuildContext context) { + return SliverIgnorePointer( + ignoring: true, + sliver: SliverOpacity( + opacity: .25, + sliver: StreamBuilder( + stream: context.read().albumBrowserZoomLevel, + initialData: + context.read().albumBrowserZoomLevel.value, + builder: (_, zoomLevel) { + if (zoomLevel.hasError) { + context.addEvent(_SetMessage( + L10n.global().writePreferenceFailureNotification)); + } + return _BlocBuilder( + buildWhen: (previous, current) => + previous.editTransformedItems != current.editTransformedItems, + builder: (context, state) { + 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, + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/app/lib/widget/home_collections.dart b/app/lib/widget/home_collections.dart index cf53816f..4575122a 100644 --- a/app/lib/widget/home_collections.dart +++ b/app/lib/widget/home_collections.dart @@ -46,11 +46,11 @@ import 'package:np_codegen/np_codegen.dart'; import 'package:to_string/to_string.dart'; part 'home_collections.g.dart'; +part 'home_collections/app_bar.dart'; part 'home_collections/bloc.dart'; part 'home_collections/state_event.dart'; part 'home_collections/type.dart'; - -typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +part 'home_collections/view.dart'; /// Show and manage a list of [Collection]s class HomeCollections extends StatelessWidget { @@ -91,14 +91,14 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections> Widget build(BuildContext context) { return MultiBlocListener( listeners: [ - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.collections != current.collections, listener: (context, state) { _bloc.add(_TransformItems(state.collections)); }, ), - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.error != current.error, listener: (context, state) { if (state.error != null && isPageVisible()) { @@ -109,7 +109,7 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections> } }, ), - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.removeError != current.removeError, listener: (context, state) { @@ -266,336 +266,12 @@ class _WrappedHomeCollectionsState extends State<_WrappedHomeCollections> late final _Bloc _bloc = context.read(); } -class _AppBar extends StatelessWidget { - const _AppBar(); +typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +typedef _BlocListener = BlocListener<_Bloc, _State>; +// typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; - @override - Widget build(BuildContext context) { - return _BlocBuilder( - buildWhen: (previous, current) => previous.isLoading != current.isLoading, - builder: (context, state) => HomeSliverAppBar( - account: context.read<_Bloc>().account, - isShowProgressIcon: state.isLoading, - menuActions: [ - PopupMenuItem( - value: _menuValueSort, - child: Text(L10n.global().sortTooltip), - ), - PopupMenuItem( - value: _menuValueImport, - child: Text(L10n.global().importFoldersTooltip), - ), - ], - onSelectedMenuActions: (option) { - switch (option) { - case _menuValueSort: - _onSortPressed(context); - break; - - case _menuValueImport: - _onImportPressed(context); - break; - } - }, - ), - ); - } - - Future _onSortPressed(BuildContext context) async { - final sort = context.read<_Bloc>().state.sort; - final result = await showDialog( - context: context, - builder: (context) => FancyOptionPicker( - title: Text(L10n.global().sortOptionDialogTitle), - items: [ - FancyOptionPickerItem( - label: L10n.global().sortOptionTimeDescendingLabel, - isSelected: sort == collection_util.CollectionSort.dateDescending, - onSelect: () { - Navigator.of(context) - .pop(collection_util.CollectionSort.dateDescending); - }, - ), - FancyOptionPickerItem( - label: L10n.global().sortOptionTimeAscendingLabel, - isSelected: sort == collection_util.CollectionSort.dateAscending, - onSelect: () { - Navigator.of(context) - .pop(collection_util.CollectionSort.dateAscending); - }, - ), - FancyOptionPickerItem( - label: L10n.global().sortOptionAlbumNameLabel, - isSelected: sort == collection_util.CollectionSort.nameAscending, - onSelect: () { - Navigator.of(context) - .pop(collection_util.CollectionSort.nameAscending); - }, - ), - FancyOptionPickerItem( - label: L10n.global().sortOptionAlbumNameDescendingLabel, - isSelected: sort == collection_util.CollectionSort.nameDescending, - onSelect: () { - Navigator.of(context) - .pop(collection_util.CollectionSort.nameDescending); - }, - ), - ], - ), - ); - if (result == null) { - return; - } - context.read<_Bloc>().add(_SetCollectionSort(result)); - } - - void _onImportPressed(BuildContext context) { - Navigator.of(context).pushNamed(AlbumImporter.routeName, - arguments: AlbumImporterArguments(context.read<_Bloc>().account)); - } - - static const _menuValueImport = 0; - static const _menuValueSort = 1; -} - -@npLog -class _SelectionAppBar extends StatelessWidget { - const _SelectionAppBar(); - - @override - Widget build(BuildContext context) { - return _BlocBuilder( - buildWhen: (previous, current) => - previous.selectedItems != current.selectedItems, - builder: (context, state) => SelectionAppBar( - count: state.selectedItems.length, - onClosePressed: () { - context.read<_Bloc>().add(const _SetSelectedItems(items: {})); - }, - actions: [ - IconButton( - icon: const Icon(Icons.delete), - tooltip: L10n.global().deleteTooltip, - onPressed: () { - context.read<_Bloc>().add(const _RemoveSelectedItems()); - }, - ), - ], - ), - ); - } -} - -class _ButtonGrid extends StatelessWidget { - const _ButtonGrid({ - required this.account, - required this.isEnabled, - this.onSharingPressed, - this.onEnhancedPhotosPressed, - this.onArchivePressed, - this.onTrashbinPressed, - this.onNewCollectionPressed, - }); - - @override - Widget build(BuildContext context) { - // needed to workaround a scrolling bug when there are more than one - // SliverStaggeredGrids in a CustomScrollView - // see: https://github.com/letsar/flutter_staggered_grid_view/issues/98 and - // https://github.com/letsar/flutter_staggered_grid_view/issues/265 - return SliverToBoxAdapter( - child: StaggeredGridView.extent( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.all(0), - maxCrossAxisExtent: 256, - staggeredTiles: List.filled(5, const StaggeredTile.fit(1)), - children: [ - _ButtonGridItemView( - icon: Icons.share_outlined, - label: L10n.global().collectionSharingLabel, - isShowIndicator: AccountPref.of(account).hasNewSharedAlbumOr(), - isEnabled: isEnabled, - onTap: () { - onSharingPressed?.call(); - }, - ), - if (features.isSupportEnhancement) - _ButtonGridItemView( - icon: Icons.auto_fix_high_outlined, - label: L10n.global().collectionEditedPhotosLabel, - isEnabled: isEnabled, - onTap: () { - onEnhancedPhotosPressed?.call(); - }, - ), - _ButtonGridItemView( - icon: Icons.archive_outlined, - label: L10n.global().albumArchiveLabel, - isEnabled: isEnabled, - onTap: () { - onArchivePressed?.call(); - }, - ), - _ButtonGridItemView( - icon: Icons.delete_outlined, - label: L10n.global().albumTrashLabel, - isEnabled: isEnabled, - onTap: () { - onTrashbinPressed?.call(); - }, - ), - _ButtonGridItemView( - icon: Icons.add, - label: L10n.global().createCollectionTooltip, - isEnabled: isEnabled, - onTap: () { - onNewCollectionPressed?.call(); - }, - ), - ], - ), - ); - } - - final Account account; - final bool isEnabled; - final VoidCallback? onSharingPressed; - final VoidCallback? onEnhancedPhotosPressed; - final VoidCallback? onArchivePressed; - final VoidCallback? onTrashbinPressed; - final VoidCallback? onNewCollectionPressed; -} - -class _ButtonGridItemView extends StatelessWidget { - const _ButtonGridItemView({ - required this.icon, - required this.label, - this.isShowIndicator = false, - required this.isEnabled, - this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(4), - child: ActionChip( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - labelPadding: const EdgeInsetsDirectional.fromSTEB(8, 0, 0, 0), - // specify icon size explicitly to workaround size flickering during - // theme transition - avatar: Icon(icon, size: 18), - label: Row( - children: [ - Expanded( - child: Text(label), - ), - if (isShowIndicator) - Icon( - Icons.circle, - color: Theme.of(context).colorScheme.tertiary, - size: 8, - ), - ], - ), - onPressed: isEnabled ? onTap : null, - ), - ); - } - - final IconData icon; - final String label; - final bool isShowIndicator; - final bool isEnabled; - final VoidCallback? onTap; -} - -class _ItemView extends StatelessWidget { - const _ItemView({ - required this.account, - required this.item, - }); - - @override - Widget build(BuildContext context) { - Widget? icon; - switch (item.itemType) { - case _ItemType.ncAlbum: - icon = const Icon(Icons.cloud); - break; - case _ItemType.album: - icon = null; - break; - case _ItemType.tagAlbum: - icon = const Icon(Icons.local_offer); - break; - case _ItemType.dirAlbum: - icon = const Icon(Icons.folder); - break; - } - String subtitle = ""; - if (item.isShared) { - subtitle = "${L10n.global().albumSharedLabel} | "; - } - subtitle += item.subtitle ?? ""; - return CollectionGridItem( - cover: _CollectionCover( - account: account, - url: item.coverUrl, - ), - title: item.name, - subtitle: subtitle, - icon: icon, - ); - } - - final Account account; - final _Item item; -} - -class _CollectionCover extends StatelessWidget { - const _CollectionCover({ - required this.account, - required this.url, - }); - - @override - Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Container( - color: Theme.of(context).listPlaceholderBackgroundColor, - constraints: const BoxConstraints.expand(), - child: url != null - ? FittedBox( - clipBehavior: Clip.hardEdge, - fit: BoxFit.cover, - child: CachedNetworkImage( - cacheManager: CoverCacheManager.inst, - imageUrl: url!, - httpHeaders: { - "Authorization": - AuthUtil.fromAccount(account).toHeaderValue(), - }, - fadeInDuration: const Duration(), - filterQuality: FilterQuality.high, - errorWidget: (context, url, error) { - // just leave it empty - return Container(); - }, - imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, - ), - ) - : Icon( - Icons.panorama, - color: Theme.of(context).listPlaceholderForegroundColor, - size: 88, - ), - ), - ); - } - - final Account account; - final String? url; +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/home_collections/app_bar.dart b/app/lib/widget/home_collections/app_bar.dart new file mode 100644 index 00000000..4de3c709 --- /dev/null +++ b/app/lib/widget/home_collections/app_bar.dart @@ -0,0 +1,121 @@ +part of '../home_collections.dart'; + +class _AppBar extends StatelessWidget { + const _AppBar(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => previous.isLoading != current.isLoading, + builder: (context, state) => HomeSliverAppBar( + account: context.bloc.account, + isShowProgressIcon: state.isLoading, + menuActions: [ + PopupMenuItem( + value: _menuValueSort, + child: Text(L10n.global().sortTooltip), + ), + PopupMenuItem( + value: _menuValueImport, + child: Text(L10n.global().importFoldersTooltip), + ), + ], + onSelectedMenuActions: (option) { + switch (option) { + case _menuValueSort: + _onSortPressed(context); + break; + + case _menuValueImport: + _onImportPressed(context); + break; + } + }, + ), + ); + } + + Future _onSortPressed(BuildContext context) async { + final sort = context.state.sort; + final result = await showDialog( + context: context, + builder: (context) => FancyOptionPicker( + title: Text(L10n.global().sortOptionDialogTitle), + items: [ + FancyOptionPickerItem( + label: L10n.global().sortOptionTimeDescendingLabel, + isSelected: sort == collection_util.CollectionSort.dateDescending, + onSelect: () { + Navigator.of(context) + .pop(collection_util.CollectionSort.dateDescending); + }, + ), + FancyOptionPickerItem( + label: L10n.global().sortOptionTimeAscendingLabel, + isSelected: sort == collection_util.CollectionSort.dateAscending, + onSelect: () { + Navigator.of(context) + .pop(collection_util.CollectionSort.dateAscending); + }, + ), + FancyOptionPickerItem( + label: L10n.global().sortOptionAlbumNameLabel, + isSelected: sort == collection_util.CollectionSort.nameAscending, + onSelect: () { + Navigator.of(context) + .pop(collection_util.CollectionSort.nameAscending); + }, + ), + FancyOptionPickerItem( + label: L10n.global().sortOptionAlbumNameDescendingLabel, + isSelected: sort == collection_util.CollectionSort.nameDescending, + onSelect: () { + Navigator.of(context) + .pop(collection_util.CollectionSort.nameDescending); + }, + ), + ], + ), + ); + if (result == null) { + return; + } + context.addEvent(_SetCollectionSort(result)); + } + + void _onImportPressed(BuildContext context) { + Navigator.of(context).pushNamed(AlbumImporter.routeName, + arguments: AlbumImporterArguments(context.bloc.account)); + } + + static const _menuValueImport = 0; + static const _menuValueSort = 1; +} + +@npLog +class _SelectionAppBar extends StatelessWidget { + const _SelectionAppBar(); + + @override + Widget build(BuildContext context) { + return _BlocBuilder( + buildWhen: (previous, current) => + previous.selectedItems != current.selectedItems, + builder: (context, state) => SelectionAppBar( + count: state.selectedItems.length, + onClosePressed: () { + context.addEvent(const _SetSelectedItems(items: {})); + }, + actions: [ + IconButton( + icon: const Icon(Icons.delete), + tooltip: L10n.global().deleteTooltip, + onPressed: () { + context.addEvent(const _RemoveSelectedItems()); + }, + ), + ], + ), + ); + } +} diff --git a/app/lib/widget/home_collections/view.dart b/app/lib/widget/home_collections/view.dart new file mode 100644 index 00000000..13f9d362 --- /dev/null +++ b/app/lib/widget/home_collections/view.dart @@ -0,0 +1,215 @@ +part of '../home_collections.dart'; + +class _ButtonGrid extends StatelessWidget { + const _ButtonGrid({ + required this.account, + required this.isEnabled, + this.onSharingPressed, + this.onEnhancedPhotosPressed, + this.onArchivePressed, + this.onTrashbinPressed, + this.onNewCollectionPressed, + }); + + @override + Widget build(BuildContext context) { + // needed to workaround a scrolling bug when there are more than one + // SliverStaggeredGrids in a CustomScrollView + // see: https://github.com/letsar/flutter_staggered_grid_view/issues/98 and + // https://github.com/letsar/flutter_staggered_grid_view/issues/265 + return SliverToBoxAdapter( + child: StaggeredGridView.extent( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(0), + maxCrossAxisExtent: 256, + staggeredTiles: List.filled(5, const StaggeredTile.fit(1)), + children: [ + _ButtonGridItemView( + icon: Icons.share_outlined, + label: L10n.global().collectionSharingLabel, + isShowIndicator: AccountPref.of(account).hasNewSharedAlbumOr(), + isEnabled: isEnabled, + onTap: () { + onSharingPressed?.call(); + }, + ), + if (features.isSupportEnhancement) + _ButtonGridItemView( + icon: Icons.auto_fix_high_outlined, + label: L10n.global().collectionEditedPhotosLabel, + isEnabled: isEnabled, + onTap: () { + onEnhancedPhotosPressed?.call(); + }, + ), + _ButtonGridItemView( + icon: Icons.archive_outlined, + label: L10n.global().albumArchiveLabel, + isEnabled: isEnabled, + onTap: () { + onArchivePressed?.call(); + }, + ), + _ButtonGridItemView( + icon: Icons.delete_outlined, + label: L10n.global().albumTrashLabel, + isEnabled: isEnabled, + onTap: () { + onTrashbinPressed?.call(); + }, + ), + _ButtonGridItemView( + icon: Icons.add, + label: L10n.global().createCollectionTooltip, + isEnabled: isEnabled, + onTap: () { + onNewCollectionPressed?.call(); + }, + ), + ], + ), + ); + } + + final Account account; + final bool isEnabled; + final VoidCallback? onSharingPressed; + final VoidCallback? onEnhancedPhotosPressed; + final VoidCallback? onArchivePressed; + final VoidCallback? onTrashbinPressed; + final VoidCallback? onNewCollectionPressed; +} + +class _ButtonGridItemView extends StatelessWidget { + const _ButtonGridItemView({ + required this.icon, + required this.label, + this.isShowIndicator = false, + required this.isEnabled, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4), + child: ActionChip( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + labelPadding: const EdgeInsetsDirectional.fromSTEB(8, 0, 0, 0), + // specify icon size explicitly to workaround size flickering during + // theme transition + avatar: Icon(icon, size: 18), + label: Row( + children: [ + Expanded( + child: Text(label), + ), + if (isShowIndicator) + Icon( + Icons.circle, + color: Theme.of(context).colorScheme.tertiary, + size: 8, + ), + ], + ), + onPressed: isEnabled ? onTap : null, + ), + ); + } + + final IconData icon; + final String label; + final bool isShowIndicator; + final bool isEnabled; + final VoidCallback? onTap; +} + +class _ItemView extends StatelessWidget { + const _ItemView({ + required this.account, + required this.item, + }); + + @override + Widget build(BuildContext context) { + Widget? icon; + switch (item.itemType) { + case _ItemType.ncAlbum: + icon = const Icon(Icons.cloud); + break; + case _ItemType.album: + icon = null; + break; + case _ItemType.tagAlbum: + icon = const Icon(Icons.local_offer); + break; + case _ItemType.dirAlbum: + icon = const Icon(Icons.folder); + break; + } + var subtitle = ""; + if (item.isShared) { + subtitle = "${L10n.global().albumSharedLabel} | "; + } + subtitle += item.subtitle ?? ""; + return CollectionGridItem( + cover: _CollectionCover( + account: account, + url: item.coverUrl, + ), + title: item.name, + subtitle: subtitle, + icon: icon, + ); + } + + final Account account; + final _Item item; +} + +class _CollectionCover extends StatelessWidget { + const _CollectionCover({ + required this.account, + required this.url, + }); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + color: Theme.of(context).listPlaceholderBackgroundColor, + constraints: const BoxConstraints.expand(), + child: url != null + ? FittedBox( + clipBehavior: Clip.hardEdge, + fit: BoxFit.cover, + child: CachedNetworkImage( + cacheManager: CoverCacheManager.inst, + imageUrl: url!, + httpHeaders: { + "Authorization": + AuthUtil.fromAccount(account).toHeaderValue(), + }, + fadeInDuration: const Duration(), + filterQuality: FilterQuality.high, + errorWidget: (context, url, error) { + // just leave it empty + return Container(); + }, + imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, + ), + ) + : Icon( + Icons.panorama, + color: Theme.of(context).listPlaceholderForegroundColor, + size: 88, + ), + ), + ); + } + + final Account account; + final String? url; +}