diff --git a/README-ZH.md b/README-ZH.md index fa053f0c..072c6177 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -295,8 +295,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 10a4bb1e..242e8757 100644 --- a/README.md +++ b/README.md @@ -304,8 +304,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 00857410..97b84f6c 100644 --- a/example/lib/constants/picker_method.dart +++ b/example/lib/constants/picker_method.dart @@ -135,39 +135,45 @@ 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, + int length, + 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,50 +192,56 @@ 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 builder = - picker.builder as DefaultAssetPickerBuilderDelegate; - final p = builder.provider; - await p.switchPath( - PathWrapper( - path: - await p.currentPath!.path.obtainForNewProperties(), + specialItems: [ + SpecialItem( + position: SpecialItemPosition.prepend, + builder: ( + BuildContext context, + AssetPathEntity? path, + int length, + 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 builder = + picker.builder as DefaultAssetPickerBuilderDelegate; + final p = 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), ), - ), - ), - ); - }, + ); + }, + ), + ], ), ); }, @@ -295,16 +307,92 @@ 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, + int length, + 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, + int length, + PermissionState permissionState, + ) { + return const Center( + child: Text('Prepand Widget', textAlign: TextAlign.center), + ); + }, + ), + SpecialItem( + position: SpecialItemPosition.append, + builder: ( + BuildContext context, + AssetPathEntity? path, + int length, + 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, + int length, + PermissionState permissionState, + ) { + return null; + }, + ), + SpecialItem( + position: SpecialItemPosition.prepend, + builder: ( + BuildContext context, + AssetPathEntity? path, + int length, + 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 535cb316..0fc2d2ef 100644 --- a/example/lib/customs/pickers/directory_file_asset_picker.dart +++ b/example/lib/customs/pickers/directory_file_asset_picker.dart @@ -584,9 +584,28 @@ class FileAssetPickerBuilder Widget assetsGridBuilder(BuildContext context) { appBarPreferredSize ??= appBar(context).preferredSize; int totalCount = provider.currentAssets.length; - if (specialItemPosition != SpecialItemPosition.none) { - totalCount += 1; - } + + final List specialItemModels = specialItems + .map((item) { + final specialItem = item.builder?.call( + context, + provider.currentPath?.path, + totalCount, + permissionNotifier.value, + ); + if (specialItem != null) { + return SpecialItemModel( + position: item.position, + item: specialItem, + ); + } + return null; + }) + .whereType() + .toList(); + + totalCount += specialItemModels.length; + final int placeholderCount; if (isAppleOS(context) && totalCount % gridCount != 0) { placeholderCount = gridCount - totalCount % gridCount; @@ -626,6 +645,11 @@ class FileAssetPickerBuilder id: key.value, assets: assets, placeholderCount: placeholderCount, + specialItemModels: specialItemModels + .where( + (item) => item.position == SpecialItemPosition.prepend, + ) + .toList(), ); } return null; @@ -699,12 +723,36 @@ class FileAssetPickerBuilder Widget assetGridItemBuilder( BuildContext context, int index, - List currentAssets, - ) { - final int currentIndex = switch (specialItemPosition) { - SpecialItemPosition.none || SpecialItemPosition.append => index, - SpecialItemPosition.prepend => index - 1, - }; + List currentAssets, { + List specialItemModels = const [], + }) { + final int length = currentAssets.length; + + final prependItems = []; + final appendItems = []; + for (final model in specialItemModels) { + switch (model.position) { + case SpecialItemPosition.prepend: + prependItems.add(model); + case SpecialItemPosition.append: + appendItems.add(model); + } + } + + if (prependItems.isNotEmpty) { + if (index < prependItems.length) { + return specialItemModels[index].item; + } + } + + if (appendItems.isNotEmpty) { + if (index >= length + prependItems.length) { + return specialItemModels[index - length].item; + } + } + + final currentIndex = index - prependItems.length; + final File asset = currentAssets.elementAt(currentIndex); final Widget builder = imageAndVideoItemBuilder( context, @@ -727,6 +775,7 @@ class FileAssetPickerBuilder int index, File asset, Widget child, + List specialItemModels, ) { return Semantics(child: child); } @@ -737,12 +786,7 @@ class FileAssetPickerBuilder required List assets, 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; } @@ -1169,6 +1213,7 @@ class FileAssetPickerBuilder int findChildIndexBuilder({ required String id, required List assets, + required List specialItemModels, 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 879fd87d..c0ed62f3 100644 --- a/example/lib/customs/pickers/insta_asset_picker.dart +++ b/example/lib/customs/pickers/insta_asset_picker.dart @@ -297,7 +297,6 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { super.keepScrollOffset, }) : super( shouldRevertGrid: false, - specialItemPosition: SpecialItemPosition.none, ); /// Save last position of the grid view scroll controller 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 6419ff94..9d0bfce7 100644 --- a/example/lib/l10n/gen/app_localizations_en.dart +++ b/example/lib/l10n/gen/app_localizations_en.dart @@ -101,6 +101,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 3306b449..e5147914 100644 --- a/example/lib/l10n/gen/app_localizations_zh.dart +++ b/example/lib/l10n/gen/app_localizations_zh.dart @@ -95,6 +95,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 939f5fa8..b22bfd2d 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,6 @@ class AssetPickerConfig { this.themeColor, this.pickerTheme, this.textDelegate, - this.specialItemPosition = SpecialItemPosition.none, - this.specialItemBuilder, this.loadingIndicatorBuilder, this.selectPredicate, this.shouldRevertGrid, @@ -39,6 +38,7 @@ class AssetPickerConfig { this.assetsChangeCallback, this.assetsChangeRefreshPredicate, this.shouldAutoplayPreview = false, + this.specialItems = const [], }) : assert( pickerTheme == null || themeColor == null, 'pickerTheme and themeColor cannot be set at the same time.', @@ -55,13 +55,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. @@ -167,14 +160,6 @@ 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; - /// Indicates the loading status for the builder. /// 指示目前加载的状态 final LoadingIndicatorBuilder? loadingIndicatorBuilder; @@ -205,4 +190,8 @@ class AssetPickerConfig { /// Whether the preview should auto play. /// 预览是否自动播放 final bool shouldAutoplayPreview; + + /// List of special items. + /// 自定义item列表 + final List> specialItems; } 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 e8e5c689..601b86e6 100644 --- a/lib/src/constants/typedefs.dart +++ b/lib/src/constants/typedefs.dart @@ -29,6 +29,7 @@ typedef SpecialItemBuilder = Widget? Function( BuildContext context, Path? path, int length, + PermissionState permissionState, ); /// {@template wechat_assets_picker.AssetSelectPredicate} diff --git a/lib/src/delegates/asset_picker_builder_delegate.dart b/lib/src/delegates/asset_picker_builder_delegate.dart index aa85ec03..41333b7a 100644 --- a/lib/src/delegates/asset_picker_builder_delegate.dart +++ b/lib/src/delegates/asset_picker_builder_delegate.dart @@ -21,12 +21,25 @@ import '../constants/typedefs.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'; import '../widget/asset_picker_viewer.dart'; import '../widget/builder/asset_entity_grid_item_builder.dart'; +/// Class which contains non-null special item widget and its position which derived from the [SpecialItem] +/// 包含非空自定义item,并指定其位置。 +class SpecialItemModel { + const SpecialItemModel({ + required this.position, + required this.item, + }); + + final SpecialItemPosition position; + final Widget item; +} + /// The delegate to build the whole picker's components. /// /// By extending the delegate, you can customize every components on you own. @@ -38,8 +51,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, @@ -84,13 +96,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. /// 指示目前加载的状态 @@ -170,9 +178,7 @@ abstract class AssetPickerBuilderDelegate { /// Whether the delegate should build the special item. /// 是否需要构建自定义 item - bool get shouldBuildSpecialItem => - specialItemPosition != SpecialItemPosition.none && - specialItemBuilder != null; + bool get shouldBuildSpecialItem => specialItems.isNotEmpty; /// Space between assets item widget. /// 资源部件之间的间隔 @@ -324,6 +330,7 @@ abstract class AssetPickerBuilderDelegate { int? findChildIndexBuilder({ required String id, required List assets, + required List specialItemModels, int placeholderCount = 0, }) => null; @@ -351,6 +358,7 @@ abstract class AssetPickerBuilderDelegate { int index, Asset asset, Widget child, + List specialItemModels, ); /// The item builder for audio type of asset. @@ -743,8 +751,6 @@ class DefaultAssetPickerBuilderDelegate required super.initialPermission, super.gridCount, super.pickerTheme, - super.specialItemPosition, - super.specialItemBuilder, super.loadingIndicatorBuilder, super.selectPredicate, super.shouldRevertGrid, @@ -760,6 +766,7 @@ class DefaultAssetPickerBuilderDelegate this.specialPickerType, this.keepScrollOffset = false, this.shouldAutoplayPreview = false, + super.specialItems = const [], }) { // Add the listener if [keepScrollOffset] is true. if (keepScrollOffset) { @@ -1231,21 +1238,29 @@ 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 List specialItemModels = specialItems + .map((item) { + final specialItem = item.builder?.call( + context, + wrapper?.path, + totalCount, + permissionNotifier.value, + ); + if (specialItem != null) { + return SpecialItemModel( + position: item.position, + item: specialItem, + ); + } + return null; + }) + .whereType() + .toList(); + + totalCount += specialItemModels.length; + + if (totalCount == 0 && specialItemModels.isEmpty) { return loadingIndicator(context); } // Then we use the [totalCount] to calculate placeholders we need. @@ -1285,7 +1300,7 @@ class DefaultAssetPickerBuilderDelegate context, index, assets, - specialItem: specialItem, + specialItemModels: specialItemModels, ), ), ); @@ -1294,7 +1309,7 @@ class DefaultAssetPickerBuilderDelegate context: context, assets: assets, placeholderCount: placeholderCount, - specialItem: specialItem, + specialItemModels: specialItemModels, ), findChildIndexCallback: (Key? key) { if (key is ValueKey) { @@ -1302,6 +1317,7 @@ class DefaultAssetPickerBuilderDelegate id: key.value, assets: assets, placeholderCount: placeholderCount, + specialItemModels: specialItemModels, ); } return null; @@ -1404,7 +1420,7 @@ class DefaultAssetPickerBuilderDelegate BuildContext context, int index, List currentAssets, { - Widget? specialItem, + List specialItemModels = const [], }) { final DefaultAssetPickerProvider p = context.read(); @@ -1412,22 +1428,31 @@ class DefaultAssetPickerBuilderDelegate 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 specialItemModels) { + 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 specialItemModels[index].item; + } } + if (appendItems.isNotEmpty) { + if (index >= length + prependItems.length) { + return specialItemModels[index - length].item; + } + } + + final currentIndex = index - prependItems.length; + if (currentPathEntity == null) { return const SizedBox.shrink(); } @@ -1457,14 +1482,23 @@ class DefaultAssetPickerBuilderDelegate itemBannedIndicator(context, asset), ], ); - return assetGridItemSemanticsBuilder(context, index, asset, content); + return assetGridItemSemanticsBuilder( + context, + index, + asset, + content, + specialItemModels, + ); } - int semanticIndex(int index) { - if (specialItemPosition != SpecialItemPosition.prepend) { - return index + 1; - } - return index; + int semanticIndex( + int index, + List specialItemModels, + ) { + final prependSpecialItemModels = specialItemModels.where( + (SpecialItemModel model) => model.position == SpecialItemPosition.prepend, + ); + return index - prependSpecialItemModels.length; } @override @@ -1473,6 +1507,7 @@ class DefaultAssetPickerBuilderDelegate int index, AssetEntity asset, Widget child, + List specialItemModels, ) { return ValueListenableBuilder( valueListenable: isSwitchingPath, @@ -1506,7 +1541,7 @@ class DefaultAssetPickerBuilderDelegate excludeSemantics: true, focusable: !isSwitchingPath, label: '${semanticsTextDelegate.semanticTypeLabel(asset.type)}' - '${semanticIndex(index)}, ' + '${semanticIndex(index, specialItemModels)}, ' '${asset.createDateTime.toString().replaceAll('.000', '')}', hidden: isSwitchingPath, hint: hint, @@ -1524,7 +1559,7 @@ class DefaultAssetPickerBuilderDelegate onLongPressHint: semanticsTextDelegate.sActionPreviewHint, selected: isSelected, sortKey: OrdinalSortKey( - semanticIndex(index).toDouble(), + semanticIndex(index, specialItemModels).toDouble(), name: 'GridItem', ), value: selectedIndex > 0 ? '$selectedIndex' : null, @@ -1537,7 +1572,7 @@ class DefaultAssetPickerBuilderDelegate } : null, child: IndexedSemantics( - index: semanticIndex(index), + index: semanticIndex(index, specialItemModels), child: child, ), ), @@ -1553,12 +1588,14 @@ class DefaultAssetPickerBuilderDelegate int findChildIndexBuilder({ required String id, required List assets, + required List specialItemModels, int placeholderCount = 0, }) { + final prependSpecialItemModels = specialItemModels.where( + (SpecialItemModel model) => model.position == SpecialItemPosition.prepend, + ); int index = assets.indexWhere((AssetEntity e) => e.id == id); - if (specialItemPosition == SpecialItemPosition.prepend) { - index += 1; - } + index += prependSpecialItemModels.length; index += placeholderCount; return index; } @@ -1568,7 +1605,7 @@ class DefaultAssetPickerBuilderDelegate required BuildContext context, required List assets, int placeholderCount = 0, - Widget? specialItem, + List specialItemModels = const [], }) { final PathWrapper? currentWrapper = context .select?>( @@ -1578,19 +1615,17 @@ class DefaultAssetPickerBuilderDelegate final int length = assets.length + placeholderCount; // Return 1 if the [specialItem] build something. - if (currentPathEntity == null && specialItem != null) { - return placeholderCount + 1; + if (currentPathEntity == null && specialItemModels.isNotEmpty) { + return placeholderCount + specialItemModels.length; } // Return actual length if the current path is all. // 如果当前目录是全部内容,则返回实际的内容数量。 - if (currentPathEntity?.isAll != true && specialItem == null) { + if (currentPathEntity?.isAll != true && specialItemModels.isEmpty) { return length; } - return switch (specialItemPosition) { - SpecialItemPosition.none => length, - SpecialItemPosition.prepend || SpecialItemPosition.append => length + 1, - }; + + return length + specialItemModels.length; } @override diff --git a/lib/src/delegates/asset_picker_delegate.dart b/lib/src/delegates/asset_picker_delegate.dart index 3f788b3d..c6943fe4 100644 --- a/lib/src/delegates/asset_picker_delegate.dart +++ b/lib/src/delegates/asset_picker_delegate.dart @@ -105,8 +105,6 @@ class AssetPickerDelegate { gridThumbnailSize: pickerConfig.gridThumbnailSize, previewThumbnailSize: pickerConfig.previewThumbnailSize, specialPickerType: pickerConfig.specialPickerType, - specialItemPosition: pickerConfig.specialItemPosition, - specialItemBuilder: pickerConfig.specialItemBuilder, loadingIndicatorBuilder: pickerConfig.loadingIndicatorBuilder, selectPredicate: pickerConfig.selectPredicate, shouldRevertGrid: pickerConfig.shouldRevertGrid, @@ -119,6 +117,7 @@ class AssetPickerDelegate { themeColor: pickerConfig.themeColor, locale: Localizations.maybeLocaleOf(context), shouldAutoplayPreview: pickerConfig.shouldAutoplayPreview, + specialItems: pickerConfig.specialItems, ), ); final List? result = await Navigator.maybeOf( diff --git a/lib/src/models/special_item.dart b/lib/src/models/special_item.dart new file mode 100644 index 00000000..10b9c169 --- /dev/null +++ b/lib/src/models/special_item.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; + +/// Allow users to set a special item in the picker grid with specified [position]. +/// 允许用户在选择器中添加一个自定义item,并指定其位置。 +@immutable +class SpecialItem { + const SpecialItem({ + required this.builder, + required this.position, + }); + + /// The widget builder for the the special item. + /// 自定义item构建。 + final SpecialItemBuilder? builder; + + /// Define how the item will be positioned. + /// 定义如何摆放item。 + final SpecialItemPosition position; +} 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 c5d0a670..91eb9fdd 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -131,8 +131,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,