diff --git a/CHANGELOG.md b/CHANGELOG.md index b158adce..198cf78d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ that can be found in the LICENSE file. --> ## Unreleased +**New features** + +- Support multiple append/prepend specials items. + +**Improvements** + - Make delegate respect generic types as much as possible. This is a breaking change for users who use custom delegates and providers. diff --git a/README-ZH.md b/README-ZH.md index c51bbb7a..cfee1ba7 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -294,8 +294,7 @@ final List? result = await AssetPicker.pickAssets( | themeColor | `Color?` | 选择器的主题色 | `Color(0xff00bc56)` | | pickerTheme | `ThemeData?` | 选择器的主题提供,包括查看器 | `null` | | textDelegate | `AssetPickerTextDelegate?` | 选择器的文本代理构建,用于自定义文本 | `AssetPickerTextDelegate()` | -| specialItemPosition | `SpecialItemPosition` | 允许用户在选择器中添加一个自定义item,并指定位置。 | `SpecialPosition.none` | -| specialItemBuilder | `SpecialItemBuilder?` | 自定义item的构造方法 | `null` | +| specialItems | `List` | 自定义item列表 | `const []` | | loadingIndicatorBuilder | `IndicatorBuilder?` | 加载器的实现 | `null` | | selectPredicate | `AssetSelectPredicate` | 判断资源可否被选择 | `null` | | shouldRevertGrid | `bool?` | 判断资源网格是否需要倒序排列 | `null` | diff --git a/README.md b/README.md index cd8306d9..75c6dfbe 100644 --- a/README.md +++ b/README.md @@ -303,8 +303,7 @@ Fields in `AssetPickerConfig`: | themeColor | `Color?` | Main theme color for the picker. | `Color(0xff00bc56)` | | pickerTheme | `ThemeData?` | Theme data provider for the picker and the viewer. | `null` | | textDelegate | `AssetPickerTextDelegate?` | Text delegate for the picker, for customize the texts. | `AssetPickerTextDelegate()` | -| specialItemPosition | `SpecialItemPosition` | Allow users set a special item in the picker with several positions. | `SpecialItemPosition.none` | -| specialItemBuilder | `SpecialItemBuilder?` | The widget builder for the special item. | `null` | +| specialItems | `List` | List of special items. | `const []` | | loadingIndicatorBuilder | `IndicatorBuilder?` | Indicates the loading status for the builder. | `null` | | selectPredicate | `AssetSelectPredicate` | Predicate whether an asset can be selected or unselected. | `null` | | shouldRevertGrid | `bool?` | Whether the assets grid should revert. | `null` | diff --git a/example/lib/constants/picker_method.dart b/example/lib/constants/picker_method.dart index 6636452d..63efbae4 100644 --- a/example/lib/constants/picker_method.dart +++ b/example/lib/constants/picker_method.dart @@ -135,39 +135,44 @@ class PickMethod { pickerConfig: AssetPickerConfig( maxAssets: maxAssetsCount, selectedAssets: assets, - specialItemPosition: SpecialItemPosition.prepend, - specialItemBuilder: ( - BuildContext context, - AssetPathEntity? path, - int length, - ) { - if (path?.isAll != true) { - return null; - } - return Semantics( - label: textDelegate.sActionUseCameraHint, - button: true, - onTapHint: textDelegate.sActionUseCameraHint, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - Feedback.forTap(context); - final AssetEntity? result = await _pickFromCamera(context); - if (result != null) { - handleResult(context, result); - } - }, - child: Container( - padding: const EdgeInsets.all(28.0), - color: Theme.of(context).dividerColor, - child: const FittedBox( - fit: BoxFit.fill, - child: Icon(Icons.camera_enhance), + specialItems: [ + SpecialItem( + position: SpecialItemPosition.prepend, + builder: ( + BuildContext context, + AssetPathEntity? path, + PermissionState permissionState, + ) { + if (path?.isAll != true) { + return null; + } + return Semantics( + label: textDelegate.sActionUseCameraHint, + button: true, + onTapHint: textDelegate.sActionUseCameraHint, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + Feedback.forTap(context); + final AssetEntity? result = + await _pickFromCamera(context); + if (result != null) { + handleResult(context, result); + } + }, + child: Container( + padding: const EdgeInsets.all(28.0), + color: Theme.of(context).dividerColor, + child: const FittedBox( + fit: BoxFit.fill, + child: Icon(Icons.camera_enhance), + ), + ), ), - ), - ), - ); - }, + ); + }, + ), + ], ), ); }, @@ -186,49 +191,54 @@ class PickMethod { pickerConfig: AssetPickerConfig( maxAssets: maxAssetsCount, selectedAssets: assets, - specialItemPosition: SpecialItemPosition.prepend, - specialItemBuilder: ( - BuildContext context, - AssetPathEntity? path, - int length, - ) { - if (path?.isAll != true) { - return null; - } - return Semantics( - label: textDelegate.sActionUseCameraHint, - button: true, - onTapHint: textDelegate.sActionUseCameraHint, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - final AssetEntity? result = await _pickFromCamera(context); - if (result == null) { - return; - } - final picker = context.findAncestorWidgetOfExactType< - AssetPicker>()!; - final p = picker.builder.provider; - await p.switchPath( - PathWrapper( - path: - await p.currentPath!.path.obtainForNewProperties(), + specialItems: [ + SpecialItem( + position: SpecialItemPosition.prepend, + builder: ( + BuildContext context, + AssetPathEntity? path, + PermissionState permissionState, + ) { + if (path?.isAll != true) { + return null; + } + return Semantics( + label: textDelegate.sActionUseCameraHint, + button: true, + onTapHint: textDelegate.sActionUseCameraHint, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + final AssetEntity? result = + await _pickFromCamera(context); + if (result == null) { + return; + } + final picker = context.findAncestorWidgetOfExactType< + AssetPicker>()!; + final p = picker.builder.provider; + await p.switchPath( + PathWrapper( + path: await p.currentPath!.path + .obtainForNewProperties(), + ), + ); + p.selectAsset(result); + }, + child: Container( + padding: const EdgeInsets.all(28.0), + color: Theme.of(context).dividerColor, + child: const FittedBox( + fit: BoxFit.fill, + child: Icon(Icons.camera_enhance), + ), ), - ); - p.selectAsset(result); - }, - child: Container( - padding: const EdgeInsets.all(28.0), - color: Theme.of(context).dividerColor, - child: const FittedBox( - fit: BoxFit.fill, - child: Icon(Icons.camera_enhance), ), - ), - ), - ); - }, + ); + }, + ), + ], ), ); }, @@ -294,16 +304,87 @@ class PickMethod { pickerConfig: AssetPickerConfig( maxAssets: maxAssetsCount, selectedAssets: assets, - specialItemPosition: SpecialItemPosition.prepend, - specialItemBuilder: ( - BuildContext context, - AssetPathEntity? path, - int length, - ) { - return const Center( - child: Text('Custom Widget', textAlign: TextAlign.center), - ); - }, + specialItems: [ + SpecialItem( + position: SpecialItemPosition.prepend, + builder: ( + BuildContext context, + AssetPathEntity? path, + PermissionState permissionState, + ) { + return const Center( + child: Text('Custom Widget', textAlign: TextAlign.center), + ); + }, + ), + ], + ), + ); + }, + ); + } + + factory PickMethod.multiSpecialItems( + BuildContext context, + int maxAssetsCount, + ) { + return PickMethod( + icon: '💡', + name: context.l10n.pickMethodMultiSpecialItemsName, + description: context.l10n.pickMethodMultiSpecialItemsDescription, + method: (BuildContext context, List assets) { + return AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + maxAssets: maxAssetsCount, + selectedAssets: assets, + specialItems: [ + SpecialItem( + position: SpecialItemPosition.prepend, + builder: ( + BuildContext context, + AssetPathEntity? path, + PermissionState permissionState, + ) { + return const Center( + child: Text('Prepand Widget', textAlign: TextAlign.center), + ); + }, + ), + SpecialItem( + position: SpecialItemPosition.append, + builder: ( + BuildContext context, + AssetPathEntity? path, + PermissionState permissionState, + ) { + return const Center( + child: Text('Append Widget', textAlign: TextAlign.center), + ); + }, + ), + //builder which return null will not be shown. + SpecialItem( + position: SpecialItemPosition.append, + builder: ( + BuildContext context, + AssetPathEntity? path, + PermissionState permissionState, + ) { + return null; + }, + ), + SpecialItem( + position: SpecialItemPosition.prepend, + builder: ( + BuildContext context, + AssetPathEntity? path, + PermissionState permissionState, + ) { + return null; + }, + ), + ], ), ); }, diff --git a/example/lib/customs/pickers/directory_file_asset_picker.dart b/example/lib/customs/pickers/directory_file_asset_picker.dart index 2df4113d..43efa803 100644 --- a/example/lib/customs/pickers/directory_file_asset_picker.dart +++ b/example/lib/customs/pickers/directory_file_asset_picker.dart @@ -524,9 +524,27 @@ final class FileAssetPickerBuilder Widget assetsGridBuilder(BuildContext context) { appBarPreferredSize ??= appBar(context).preferredSize; int totalCount = provider.currentAssets.length; - if (specialItemPosition != SpecialItemPosition.none) { - totalCount += 1; - } + + final specialItemsFinalized = specialItems + .map((item) { + final specialItem = item.builder?.call( + context, + provider.currentPath?.path, + permissionNotifier.value, + ); + if (specialItem != null) { + return SpecialItemFinalized( + position: item.position, + item: specialItem, + ); + } + return null; + }) + .whereType() + .toList(); + + totalCount += specialItemsFinalized.length; + final int placeholderCount; if (isAppleOS(context) && totalCount % gridCount != 0) { placeholderCount = gridCount - totalCount % gridCount; @@ -551,13 +569,19 @@ final class FileAssetPickerBuilder } return Directionality( textDirection: Directionality.of(context), - child: assetGridItemBuilder(c, index, assets), + child: assetGridItemBuilder( + context: c, + index: index, + currentAssets: assets, + specialItemsFinalized: specialItemsFinalized, + ), ); }, ), childCount: assetsGridItemCount( context: ctx, assets: assets, + specialItemsFinalized: specialItemsFinalized, placeholderCount: placeholderCount, ), findChildIndexCallback: (Key? key) { @@ -566,6 +590,7 @@ final class FileAssetPickerBuilder id: key.value, assets: assets, placeholderCount: placeholderCount, + specialItemsFinalized: specialItemsFinalized, ); } return null; @@ -636,15 +661,39 @@ final class FileAssetPickerBuilder } @override - Widget assetGridItemBuilder( - BuildContext context, - int index, - List currentAssets, - ) { - final int currentIndex = switch (specialItemPosition) { - SpecialItemPosition.none || SpecialItemPosition.append => index, - SpecialItemPosition.prepend => index - 1, - }; + Widget assetGridItemBuilder({ + required BuildContext context, + required int index, + required List currentAssets, + required List specialItemsFinalized, + }) { + final int length = currentAssets.length; + + final prependItems = []; + final appendItems = []; + for (final item in specialItemsFinalized) { + switch (item.position) { + case SpecialItemPosition.prepend: + prependItems.add(item); + case SpecialItemPosition.append: + appendItems.add(item); + } + } + + if (prependItems.isNotEmpty) { + if (index < prependItems.length) { + return specialItemsFinalized[index].item; + } + } + + if (appendItems.isNotEmpty) { + if (index >= length + prependItems.length) { + return specialItemsFinalized[index - length].item; + } + } + + final currentIndex = index - prependItems.length; + final File asset = currentAssets.elementAt(currentIndex); final Widget builder = imageAndVideoItemBuilder( context, @@ -667,6 +716,7 @@ final class FileAssetPickerBuilder int index, File asset, Widget child, + List specialItemsFinalized, ) { return Semantics(child: child); } @@ -675,14 +725,10 @@ final class FileAssetPickerBuilder int assetsGridItemCount({ required BuildContext context, required List assets, + required List specialItemsFinalized, int placeholderCount = 0, }) { - final int length = switch (specialItemPosition) { - SpecialItemPosition.none => assets.length, - SpecialItemPosition.prepend || - SpecialItemPosition.append => - assets.length + 1, - }; + final int length = assets.length + specialItems.length; return length + placeholderCount; } @@ -1091,6 +1137,7 @@ final class FileAssetPickerBuilder int findChildIndexBuilder({ required String id, required List assets, + required List specialItemsFinalized, int placeholderCount = 0, }) { return assets.indexWhere((File file) => file.path == id); diff --git a/example/lib/customs/pickers/insta_asset_picker.dart b/example/lib/customs/pickers/insta_asset_picker.dart index d946ed6f..3a02300f 100644 --- a/example/lib/customs/pickers/insta_asset_picker.dart +++ b/example/lib/customs/pickers/insta_asset_picker.dart @@ -297,7 +297,6 @@ final class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { super.keepScrollOffset, }) : super( shouldRevertGrid: false, - specialItemPosition: SpecialItemPosition.none, ); /// Save last position of the grid view scroll controller @@ -677,8 +676,14 @@ final class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { appBarPreferredSize ??= appBar(context).preferredSize; return Consumer( builder: (context, p, __) { - final bool shouldDisplayAssets = - p.hasAssetsToDisplay || shouldBuildSpecialItem; + final hasAssetsToDisplay = p.hasAssetsToDisplay; + final shouldBuildSpecialItems = assetsGridSpecialItemsFinalized( + context: context, + path: p.currentPath?.path, + ).isNotEmpty; + final shouldDisplayAssets = + hasAssetsToDisplay || shouldBuildSpecialItems; + _initializePreviewAsset(p, shouldDisplayAssets); return AnimatedSwitcher( diff --git a/example/lib/customs/pickers/multi_tabs_assets_picker.dart b/example/lib/customs/pickers/multi_tabs_assets_picker.dart index 3007b539..7cf1e59a 100644 --- a/example/lib/customs/pickers/multi_tabs_assets_picker.dart +++ b/example/lib/customs/pickers/multi_tabs_assets_picker.dart @@ -523,9 +523,14 @@ final class MultiTabAssetPickerBuilder Widget _buildGrid(BuildContext context) { return Consumer( - builder: (BuildContext context, DefaultAssetPickerProvider p, __) { - final bool shouldDisplayAssets = - p.hasAssetsToDisplay || shouldBuildSpecialItem; + builder: (context, p, __) { + final hasAssetsToDisplay = p.hasAssetsToDisplay; + final shouldBuildSpecialItems = assetsGridSpecialItemsFinalized( + context: context, + path: p.currentPath?.path, + ).isNotEmpty; + final shouldDisplayAssets = + hasAssetsToDisplay || shouldBuildSpecialItems; return AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: shouldDisplayAssets diff --git a/example/lib/l10n/app_en.arb b/example/lib/l10n/app_en.arb index d40489ed..029e46a9 100644 --- a/example/lib/l10n/app_en.arb +++ b/example/lib/l10n/app_en.arb @@ -28,6 +28,8 @@ "pickMethodCustomFilterOptionsDescription": "Add filter options for the picker.", "pickMethodPrependItemName": "Prepend special item", "pickMethodPrependItemDescription": "A special item will prepend to the assets grid.", + "pickMethodMultiSpecialItemsName": "Multiple special items", + "pickMethodMultiSpecialItemsDescription": "Multiple special items will prepend or append to the assets grid", "pickMethodNoPreviewName": "No preview", "pickMethodNoPreviewDescription": "You cannot preview assets during the picking, the behavior is like the WhatsApp/MegaTok pattern.", "pickMethodKeepScrollOffsetName": "Keep scroll offset", diff --git a/example/lib/l10n/app_zh.arb b/example/lib/l10n/app_zh.arb index 79df1bd6..151514b2 100644 --- a/example/lib/l10n/app_zh.arb +++ b/example/lib/l10n/app_zh.arb @@ -28,6 +28,8 @@ "pickMethodCustomFilterOptionsDescription": "为选择器添加自定义过滤条件。", "pickMethodPrependItemName": "往网格前插入 widget", "pickMethodPrependItemDescription": "网格的靠前位置会添加一个自定义的 widget。", + "pickMethodMultiSpecialItemsName": "多个特殊 widget", + "pickMethodMultiSpecialItemsDescription": "网格的靠前或靠后位置会可以多个自定义的 widget。", "pickMethodNoPreviewName": "禁止预览", "pickMethodNoPreviewDescription": "无法预览选择的资源,与 WhatsApp/MegaTok 的行为类似。", "pickMethodKeepScrollOffsetName": "保持滚动位置", diff --git a/example/lib/l10n/gen/app_localizations.dart b/example/lib/l10n/gen/app_localizations.dart index 9cf300a3..fbdf6b06 100644 --- a/example/lib/l10n/gen/app_localizations.dart +++ b/example/lib/l10n/gen/app_localizations.dart @@ -266,6 +266,18 @@ abstract class AppLocalizations { /// **'A special item will prepend to the assets grid.'** String get pickMethodPrependItemDescription; + /// No description provided for @pickMethodMultiSpecialItemsName. + /// + /// In en, this message translates to: + /// **'Multiple special items'** + String get pickMethodMultiSpecialItemsName; + + /// No description provided for @pickMethodMultiSpecialItemsDescription. + /// + /// In en, this message translates to: + /// **'Multiple special items will prepend or append to the assets grid'** + String get pickMethodMultiSpecialItemsDescription; + /// No description provided for @pickMethodNoPreviewName. /// /// In en, this message translates to: diff --git a/example/lib/l10n/gen/app_localizations_en.dart b/example/lib/l10n/gen/app_localizations_en.dart index 60672977..2e62e383 100644 --- a/example/lib/l10n/gen/app_localizations_en.dart +++ b/example/lib/l10n/gen/app_localizations_en.dart @@ -103,6 +103,13 @@ class AppLocalizationsEn extends AppLocalizations { String get pickMethodPrependItemDescription => 'A special item will prepend to the assets grid.'; + @override + String get pickMethodMultiSpecialItemsName => 'Multiple special items'; + + @override + String get pickMethodMultiSpecialItemsDescription => + 'Multiple special items will prepend or append to the assets grid'; + @override String get pickMethodNoPreviewName => 'No preview'; diff --git a/example/lib/l10n/gen/app_localizations_zh.dart b/example/lib/l10n/gen/app_localizations_zh.dart index fb1e4161..5ae2326c 100644 --- a/example/lib/l10n/gen/app_localizations_zh.dart +++ b/example/lib/l10n/gen/app_localizations_zh.dart @@ -97,6 +97,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pickMethodPrependItemDescription => '网格的靠前位置会添加一个自定义的 widget。'; + @override + String get pickMethodMultiSpecialItemsName => '多个特殊 widget'; + + @override + String get pickMethodMultiSpecialItemsDescription => + '网格的靠前或靠后位置会可以多个自定义的 widget。'; + @override String get pickMethodNoPreviewName => '禁止预览'; diff --git a/example/lib/pages/multi_assets_page.dart b/example/lib/pages/multi_assets_page.dart index 016486da..4d0b30cc 100644 --- a/example/lib/pages/multi_assets_page.dart +++ b/example/lib/pages/multi_assets_page.dart @@ -46,6 +46,7 @@ class _MultiAssetsPageState extends State PickMethod.changeLanguages(context, maxAssetsCount), PickMethod.threeItemsGrid(context, maxAssetsCount), PickMethod.prependItem(context, maxAssetsCount), + PickMethod.multiSpecialItems(context, maxAssetsCount), PickMethod( icon: '🎭', name: context.l10n.pickMethodWeChatMomentName, diff --git a/example/lib/pages/single_assets_page.dart b/example/lib/pages/single_assets_page.dart index 612277dd..ff5a39fb 100644 --- a/example/lib/pages/single_assets_page.dart +++ b/example/lib/pages/single_assets_page.dart @@ -46,6 +46,7 @@ class _SingleAssetPageState extends State PickMethod.changeLanguages(context, maxAssetsCount), PickMethod.threeItemsGrid(context, maxAssetsCount), PickMethod.prependItem(context, maxAssetsCount), + PickMethod.multiSpecialItems(context, maxAssetsCount), PickMethod.customFilterOptions(context, maxAssetsCount), PickMethod.preventGIFPicked(context, maxAssetsCount), PickMethod.noPreview(context, maxAssetsCount), diff --git a/lib/src/constants/config.dart b/lib/src/constants/config.dart index ae35dea4..feb9e87b 100644 --- a/lib/src/constants/config.dart +++ b/lib/src/constants/config.dart @@ -8,6 +8,7 @@ import 'package:photo_manager/photo_manager.dart'; import '../constants/typedefs.dart'; import '../delegates/asset_picker_text_delegate.dart'; import '../delegates/sort_path_delegate.dart'; +import '../models/special_item.dart'; import 'constants.dart'; import 'enums.dart'; @@ -29,8 +30,7 @@ class AssetPickerConfig { this.themeColor, this.pickerTheme, this.textDelegate, - this.specialItemPosition = SpecialItemPosition.none, - this.specialItemBuilder, + this.specialItems = const [], this.loadingIndicatorBuilder, this.selectPredicate, this.shouldRevertGrid, @@ -56,13 +56,6 @@ class AssetPickerConfig { requestType == RequestType.common, 'SpecialPickerType.wechatMoment and requestType ' 'cannot be set at the same time.', - ), - assert( - (specialItemBuilder == null && - identical(specialItemPosition, SpecialItemPosition.none)) || - (specialItemBuilder != null && - !identical(specialItemPosition, SpecialItemPosition.none)), - 'Custom item did not set properly.', ); /// Selected assets. @@ -168,13 +161,9 @@ class AssetPickerConfig { final AssetPickerTextDelegate? textDelegate; - /// Allow users set a special item in the picker with several positions. - /// 允许用户在选择器中添加一个自定义item,并指定位置 - final SpecialItemPosition specialItemPosition; - - /// The widget builder for the the special item. - /// 自定义item的构造方法 - final SpecialItemBuilder? specialItemBuilder; + /// List of special items. + /// 自定义 item 列表 + final List> specialItems; /// Indicates the loading status for the builder. /// 指示目前加载的状态 diff --git a/lib/src/constants/enums.dart b/lib/src/constants/enums.dart index 686c563d..ba3bdff9 100644 --- a/lib/src/constants/enums.dart +++ b/lib/src/constants/enums.dart @@ -30,10 +30,6 @@ enum SpecialPickerType { /// Provide an item slot for custom widget insertion. /// 提供一个自定义位置供特殊item放入资源列表中。 enum SpecialItemPosition { - /// Not insert to the list. - /// 不放入列表 - none, - /// Add as leading of the list. /// 在列表前放入 prepend, diff --git a/lib/src/constants/typedefs.dart b/lib/src/constants/typedefs.dart index 631ea449..5e0ff68e 100644 --- a/lib/src/constants/typedefs.dart +++ b/lib/src/constants/typedefs.dart @@ -28,7 +28,7 @@ typedef LoadingIndicatorBuilder = Widget Function( typedef SpecialItemBuilder = Widget? Function( BuildContext context, Path? path, - int length, + PermissionState permissionState, ); /// {@template wechat_assets_picker.AssetSelectPredicate} diff --git a/lib/src/delegates/asset_grid_drag_selection_coordinator.dart b/lib/src/delegates/asset_grid_drag_selection_coordinator.dart index b04f3503..a422a3cf 100644 --- a/lib/src/delegates/asset_grid_drag_selection_coordinator.dart +++ b/lib/src/delegates/asset_grid_drag_selection_coordinator.dart @@ -123,10 +123,16 @@ class AssetGridDragSelectionCoordinator { totalRows * (itemSize + delegate.itemSpacing) <= gridViewport; final reverted = gridRevert && !onlyOneScreen; + final specialItems = delegate.assetsGridSpecialItemsFinalized( + context: context, + path: provider.currentPath?.path, + ); + final double anchor = delegate.assetGridAnchor( context: context, constraints: constraints, pathWrapper: provider.currentPath, + specialItemsFinalized: specialItems, ); final scrolledOffset = delegate.gridScrollController.offset .abs(); // Offset is negative when reverted. @@ -157,6 +163,7 @@ class AssetGridDragSelectionCoordinator { context: context, pathWrapper: provider.currentPath, onlyOneScreen: onlyOneScreen, + specialItemsFinalized: specialItems, ); // Make the index starts with the bottom if the grid is reverted. if (reverted && placeholderCount > 0 && rowIndex > 0 && anchor < 1.0) { diff --git a/lib/src/delegates/asset_picker_builder_delegate.dart b/lib/src/delegates/asset_picker_builder_delegate.dart index 5004c203..6e48074d 100644 --- a/lib/src/delegates/asset_picker_builder_delegate.dart +++ b/lib/src/delegates/asset_picker_builder_delegate.dart @@ -22,6 +22,7 @@ import '../delegates/asset_grid_drag_selection_coordinator.dart'; import '../delegates/asset_picker_text_delegate.dart'; import '../internals/singleton.dart'; import '../models/path_wrapper.dart'; +import '../models/special_item.dart'; import '../provider/asset_picker_provider.dart'; import '../widget/asset_picker.dart'; import '../widget/asset_picker_app_bar.dart'; @@ -40,8 +41,7 @@ abstract class AssetPickerBuilderDelegate { required this.initialPermission, this.gridCount = 4, this.pickerTheme, - this.specialItemPosition = SpecialItemPosition.none, - this.specialItemBuilder, + this.specialItems = const [], this.loadingIndicatorBuilder, this.selectPredicate, this.shouldRevertGrid, @@ -89,13 +89,9 @@ abstract class AssetPickerBuilderDelegate { /// 但某些情况下开发者需要亮色或自定义主题。 final ThemeData? pickerTheme; - /// Allow users set a special item in the picker with several positions. - /// 允许用户在选择器中添加一个自定义 item,并指定位置 - final SpecialItemPosition specialItemPosition; - - /// The widget builder for the the special item. - /// 自定义 item 的构造方法 - final SpecialItemBuilder? specialItemBuilder; + /// List of special items. + /// 自定义item列表 + final List> specialItems; /// Indicates the loading status for the builder. /// 指示目前加载的状态 @@ -176,12 +172,6 @@ abstract class AssetPickerBuilderDelegate { /// 选择器是否为单选模式 bool get isSingleAssetMode; - /// Whether the delegate should build the special item. - /// 是否需要构建自定义 item - bool get shouldBuildSpecialItem => - specialItemPosition != SpecialItemPosition.none && - specialItemBuilder != null; - /// Space between assets item widget. /// 资源部件之间的间隔 double get itemSpacing => 2; @@ -336,6 +326,7 @@ abstract class AssetPickerBuilderDelegate { int? findChildIndexBuilder({ required String id, required List assets, + required List specialItemsFinalized, int placeholderCount = 0, }) => null; @@ -345,31 +336,49 @@ abstract class AssetPickerBuilderDelegate { int assetsGridItemCount({ required BuildContext context, required List assets, + required List specialItemsFinalized, int placeholderCount = 0, }); + List assetsGridSpecialItemsFinalized({ + required BuildContext context, + required Path? path, + }) { + return specialItems + .map((item) { + final specialItem = item.builder?.call( + context, + path, + permissionNotifier.value, + ); + if (specialItem != null) { + return SpecialItemFinalized( + position: item.position, + item: specialItem, + ); + } + return null; + }) + .nonNulls + .toList(); + } + /// Calculates the placeholder count in the assets grid. int assetsGridItemPlaceholderCount({ required BuildContext context, required PathWrapper? pathWrapper, required bool onlyOneScreen, + required List specialItemsFinalized, }) { if (onlyOneScreen) { return 0; } + final bool gridRevert = effectiveShouldRevertGrid(context); int totalCount = pathWrapper?.assetCount ?? 0; - // If user chose a special item's position, add 1 count. - if (specialItemPosition != SpecialItemPosition.none) { - final specialItem = specialItemBuilder?.call( - context, - pathWrapper?.path, - totalCount, - ); - if (specialItem != null) { - totalCount += 1; - } - } + // Add special items' count. + totalCount += specialItemsFinalized.length; + final int result; if (gridRevert && totalCount % gridCount != 0) { // When there are left items that not filled into one row, @@ -379,6 +388,7 @@ abstract class AssetPickerBuilderDelegate { // Otherwise, we don't need placeholders. result = 0; } + return result; } @@ -387,19 +397,12 @@ abstract class AssetPickerBuilderDelegate { required BuildContext context, required BoxConstraints constraints, required PathWrapper? pathWrapper, + required List specialItemsFinalized, }) { int totalCount = pathWrapper?.assetCount ?? 0; - // If user chose a special item's position, add 1 count. - if (specialItemPosition != SpecialItemPosition.none) { - final specialItem = specialItemBuilder?.call( - context, - pathWrapper?.path, - totalCount, - ); - if (specialItem != null) { - totalCount += 1; - } - } + // Add special items' count. + totalCount += specialItemsFinalized.length; + // Here we got a magic calculation. [itemSpacing] needs to be divided by // [gridCount] since every grid item is squeezed by the [itemSpacing], // and it's actual size is reduced with [itemSpacing / gridCount]. @@ -427,11 +430,12 @@ abstract class AssetPickerBuilderDelegate { /// The item builder for the assets' grid. /// 资源列表项的构建 - Widget assetGridItemBuilder( - BuildContext context, - int index, - List currentAssets, - ); + Widget assetGridItemBuilder({ + required BuildContext context, + required int index, + required List currentAssets, + required List specialItemsFinalized, + }); /// The [Semantics] builder for the assets' grid. /// 资源列表项的语义构建 @@ -440,6 +444,7 @@ abstract class AssetPickerBuilderDelegate { int index, Asset asset, Widget child, + List specialItemsFinalized, ); /// The item builder for audio type of asset. @@ -826,8 +831,7 @@ class DefaultAssetPickerBuilderDelegate required super.initialPermission, super.gridCount, super.pickerTheme, - super.specialItemPosition, - super.specialItemBuilder, + super.specialItems = const [], super.loadingIndicatorBuilder, super.selectPredicate, super.shouldRevertGrid, @@ -1227,9 +1231,14 @@ class DefaultAssetPickerBuilderDelegate return AssetPickerAppBarWrapper( appBar: appBar(context), body: Consumer( - builder: (BuildContext context, T p, _) { - final bool shouldDisplayAssets = - p.hasAssetsToDisplay || shouldBuildSpecialItem; + builder: (context, p, _) { + final hasAssetsToDisplay = p.hasAssetsToDisplay; + final shouldBuildSpecialItems = assetsGridSpecialItemsFinalized( + context: context, + path: p.currentPath?.path, + ).isNotEmpty; + final shouldDisplayAssets = + hasAssetsToDisplay || shouldBuildSpecialItems; return AnimatedSwitcher( duration: switchingPathDuration, child: shouldDisplayAssets @@ -1278,10 +1287,15 @@ class DefaultAssetPickerBuilderDelegate children: [ Positioned.fill( child: Consumer( - builder: (_, p, __) { + builder: (context, p, _) { + final hasAssetsToDisplay = p.hasAssetsToDisplay; + final shouldBuildSpecialItems = assetsGridSpecialItemsFinalized( + context: context, + path: p.currentPath?.path, + ).isNotEmpty; + final shouldDisplayAssets = + hasAssetsToDisplay || shouldBuildSpecialItems; final Widget child; - final bool shouldDisplayAssets = - p.hasAssetsToDisplay || shouldBuildSpecialItem; if (shouldDisplayAssets) { child = Stack( children: [ @@ -1344,21 +1358,13 @@ class DefaultAssetPickerBuilderDelegate builder: (context, wrapper, _) { // First, we need the count of the assets. int totalCount = wrapper?.assetCount ?? 0; - final Widget? specialItem; - // If user chose a special item's position, add 1 count. - if (specialItemPosition != SpecialItemPosition.none) { - specialItem = specialItemBuilder?.call( - context, - wrapper?.path, - totalCount, - ); - if (specialItem != null) { - totalCount += 1; - } - } else { - specialItem = null; - } - if (totalCount == 0 && specialItem == null) { + final specialItemsFinalized = assetsGridSpecialItemsFinalized( + context: context, + path: wrapper?.path, + ); + totalCount += specialItemsFinalized.length; + + if (totalCount == 0 && specialItemsFinalized.isEmpty) { return loadingIndicator(context); } @@ -1377,6 +1383,7 @@ class DefaultAssetPickerBuilderDelegate context: context, pathWrapper: wrapper, onlyOneScreen: onlyOneScreen, + specialItemsFinalized: specialItemsFinalized, ); return SliverGrid( delegate: SliverChildBuilderDelegate( @@ -1389,10 +1396,10 @@ class DefaultAssetPickerBuilderDelegate } Widget child = assetGridItemBuilder( - context, - index, - assets, - specialItem: specialItem, + context: context, + index: index, + currentAssets: assets, + specialItemsFinalized: specialItemsFinalized, ); // Enables drag-to-select when: @@ -1483,7 +1490,7 @@ class DefaultAssetPickerBuilderDelegate context: context, assets: assets, placeholderCount: placeholderCount, - specialItem: specialItem, + specialItemsFinalized: specialItemsFinalized, ), findChildIndexCallback: (Key? key) { if (key is ValueKey) { @@ -1491,6 +1498,7 @@ class DefaultAssetPickerBuilderDelegate id: key.value, assets: assets, placeholderCount: placeholderCount, + specialItemsFinalized: specialItemsFinalized, ); } return null; @@ -1523,6 +1531,7 @@ class DefaultAssetPickerBuilderDelegate context: context, constraints: constraints, pathWrapper: wrapper, + specialItemsFinalized: specialItemsFinalized, ); final reverted = gridRevert && !onlyOneScreen; @@ -1585,33 +1594,42 @@ class DefaultAssetPickerBuilderDelegate /// 图片和视频类型 /// * 在索引到达倒数第三列的时候加载更多资源。 @override - Widget assetGridItemBuilder( - BuildContext context, - int index, - List currentAssets, { - Widget? specialItem, + Widget assetGridItemBuilder({ + required BuildContext context, + required int index, + required List currentAssets, + required List specialItemsFinalized, }) { final p = context.read(); final int length = currentAssets.length; final PathWrapper? currentWrapper = p.currentPath; final AssetPathEntity? currentPathEntity = currentWrapper?.path; - if (specialItem != null) { - if ((index == 0 && specialItemPosition == SpecialItemPosition.prepend) || - (index == length && - specialItemPosition == SpecialItemPosition.append)) { - return specialItem; + final prependItems = []; + final appendItems = []; + for (final model in specialItemsFinalized) { + switch (model.position) { + case SpecialItemPosition.prepend: + prependItems.add(model); + case SpecialItemPosition.append: + appendItems.add(model); } } - final int currentIndex; - if (specialItem != null && - specialItemPosition == SpecialItemPosition.prepend) { - currentIndex = index - 1; - } else { - currentIndex = index; + if (prependItems.isNotEmpty) { + if (index < prependItems.length) { + return specialItemsFinalized[index].item; + } + } + + if (appendItems.isNotEmpty) { + if (index >= length + prependItems.length) { + return specialItemsFinalized[index - length].item; + } } + final currentIndex = index - prependItems.length; + if (currentPathEntity == null) { return const SizedBox.shrink(); } @@ -1641,14 +1659,23 @@ class DefaultAssetPickerBuilderDelegate itemBannedIndicator(context, asset), ], ); - return assetGridItemSemanticsBuilder(context, index, asset, content); + return assetGridItemSemanticsBuilder( + context, + index, + asset, + content, + specialItemsFinalized, + ); } - int semanticIndex(int index) { - if (specialItemPosition != SpecialItemPosition.prepend) { - return index + 1; - } - return index; + int assetGridItemSemanticIndex( + int index, + List specialItemsFinalized, + ) { + final prependItems = specialItemsFinalized.where( + (model) => model.position == SpecialItemPosition.prepend, + ); + return index - prependItems.length; } @override @@ -1657,6 +1684,7 @@ class DefaultAssetPickerBuilderDelegate int index, AssetEntity asset, Widget child, + List specialItemsFinalized, ) { return ValueListenableBuilder( valueListenable: isSwitchingPath, @@ -1674,7 +1702,7 @@ class DefaultAssetPickerBuilderDelegate final int selectedIndex = p.selectedAssets.indexOf(asset) + 1; final labels = [ '${semanticsTextDelegate.semanticTypeLabel(asset.type)}' - '${semanticIndex(index)}', + '${assetGridItemSemanticIndex(index, specialItemsFinalized)}', asset.createDateTime.toString().replaceAll('.000', ''), if (asset.type == AssetType.audio || asset.type == AssetType.video) @@ -1704,7 +1732,8 @@ class DefaultAssetPickerBuilderDelegate onLongPressHint: semanticsTextDelegate.sActionPreviewHint, selected: isSelected, sortKey: OrdinalSortKey( - semanticIndex(index).toDouble(), + assetGridItemSemanticIndex(index, specialItemsFinalized) + .toDouble(), name: 'GridItem', ), value: selectedIndex > 0 ? '$selectedIndex' : null, @@ -1717,7 +1746,8 @@ class DefaultAssetPickerBuilderDelegate } : null, child: IndexedSemantics( - index: semanticIndex(index), + index: + assetGridItemSemanticIndex(index, specialItemsFinalized), child: child, ), ), @@ -1733,12 +1763,14 @@ class DefaultAssetPickerBuilderDelegate int findChildIndexBuilder({ required String id, required List assets, + required List specialItemsFinalized, int placeholderCount = 0, }) { + final prependItems = specialItemsFinalized.where( + (model) => model.position == SpecialItemPosition.prepend, + ); int index = assets.indexWhere((AssetEntity e) => e.id == id); - if (specialItemPosition == SpecialItemPosition.prepend) { - index += 1; - } + index += prependItems.length; index += placeholderCount; return index; } @@ -1747,30 +1779,10 @@ class DefaultAssetPickerBuilderDelegate int assetsGridItemCount({ required BuildContext context, required List assets, + required List specialItemsFinalized, int placeholderCount = 0, - Widget? specialItem, }) { - final PathWrapper? currentWrapper = - context.select?>( - (T p) => p.currentPath, - ); - final AssetPathEntity? currentPathEntity = currentWrapper?.path; - final int length = assets.length + placeholderCount; - - // Return 1 if the [specialItem] build something. - if (currentPathEntity == null && specialItem != null) { - return placeholderCount + 1; - } - - // Return actual length if the current path is all. - // 如果当前目录是全部内容,则返回实际的内容数量。 - if (currentPathEntity?.isAll != true && specialItem == null) { - return length; - } - return switch (specialItemPosition) { - SpecialItemPosition.none => length, - SpecialItemPosition.prepend || SpecialItemPosition.append => length + 1, - }; + return assets.length + specialItemsFinalized.length + placeholderCount; } @override @@ -2055,11 +2067,8 @@ class DefaultAssetPickerBuilderDelegate child: Selector>>( selector: (_, T p) => p.paths, builder: (_, List> paths, __) { - final List> filtered = paths - .where( - (PathWrapper p) => p.assetCount != 0, - ) - .toList(); + final filtered = + paths.where((p) => p.assetCount != 0).toList(); return ListView.separated( padding: const EdgeInsetsDirectional.only(top: 1), shrinkWrap: true, diff --git a/lib/src/delegates/asset_picker_delegate.dart b/lib/src/delegates/asset_picker_delegate.dart index 45292346..7f41419d 100644 --- a/lib/src/delegates/asset_picker_delegate.dart +++ b/lib/src/delegates/asset_picker_delegate.dart @@ -108,8 +108,7 @@ class AssetPickerDelegate { gridThumbnailSize: pickerConfig.gridThumbnailSize, previewThumbnailSize: pickerConfig.previewThumbnailSize, specialPickerType: pickerConfig.specialPickerType, - specialItemPosition: pickerConfig.specialItemPosition, - specialItemBuilder: pickerConfig.specialItemBuilder, + specialItems: pickerConfig.specialItems, loadingIndicatorBuilder: pickerConfig.loadingIndicatorBuilder, selectPredicate: pickerConfig.selectPredicate, shouldRevertGrid: pickerConfig.shouldRevertGrid, diff --git a/lib/src/models/special_item.dart b/lib/src/models/special_item.dart new file mode 100644 index 00000000..fcc1e6e9 --- /dev/null +++ b/lib/src/models/special_item.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +import '../constants/enums.dart'; +import '../constants/typedefs.dart'; + +/// Allow users to set special items in the picker grid with [position]. +/// 允许用户在选择器中添加一个自定义item,并指定其位置。 +@immutable +class SpecialItem { + const SpecialItem({ + required this.position, + required this.builder, + }); + + /// Define how the item will be positioned. + /// 定义如何摆放item。 + final SpecialItemPosition position; + + /// The widget builder for the the special item. + /// 自定义item构建。 + final SpecialItemBuilder? builder; + + @override + String toString() { + return 'SpecialItem$Path(position: $position, builder: $builder)'; + } +} + +/// A finalized [SpecialItem] which contains its position and the built widget. +/// 已被构建的 [SpecialItem],包含其位置和 widget 信息。 +@immutable +final class SpecialItemFinalized { + const SpecialItemFinalized({ + required this.position, + required this.item, + }); + + final SpecialItemPosition position; + final Widget item; + + @override + String toString() { + return 'SpecialItemFinalized$Path(position: $position, item: $item)'; + } +} diff --git a/lib/wechat_assets_picker.dart b/lib/wechat_assets_picker.dart index ca03acff..dd2afb69 100644 --- a/lib/wechat_assets_picker.dart +++ b/lib/wechat_assets_picker.dart @@ -12,7 +12,6 @@ export 'src/constants/config.dart'; export 'src/constants/constants.dart' hide packageName; export 'src/constants/enums.dart'; export 'src/constants/typedefs.dart'; - export 'src/delegates/asset_picker_builder_delegate.dart'; export 'src/delegates/asset_picker_delegate.dart'; export 'src/delegates/asset_picker_text_delegate.dart'; @@ -20,6 +19,7 @@ export 'src/delegates/asset_picker_viewer_builder_delegate.dart'; export 'src/delegates/sort_path_delegate.dart'; export 'src/models/path_wrapper.dart'; +export 'src/models/special_item.dart'; export 'src/provider/asset_picker_provider.dart'; export 'src/provider/asset_picker_viewer_provider.dart'; diff --git a/test/test_utils.dart b/test/test_utils.dart index d83611f0..0edc4fe1 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -132,8 +132,7 @@ class TestAssetPickerDelegate extends AssetPickerDelegate { gridThumbnailSize: pickerConfig.gridThumbnailSize, previewThumbnailSize: pickerConfig.previewThumbnailSize, specialPickerType: pickerConfig.specialPickerType, - specialItemPosition: pickerConfig.specialItemPosition, - specialItemBuilder: pickerConfig.specialItemBuilder, + specialItems: pickerConfig.specialItems, loadingIndicatorBuilder: pickerConfig.loadingIndicatorBuilder, selectPredicate: pickerConfig.selectPredicate, shouldRevertGrid: pickerConfig.shouldRevertGrid,