diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aa3c5a7..b158adce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ that can be found in the LICENSE file. --> ## Unreleased -*None.* +- Make delegate respect generic types as much as possible. + This is a breaking change for users who use custom delegates and providers. ## 9.6.0 diff --git a/example/lib/constants/picker_method.dart b/example/lib/constants/picker_method.dart index 00857410..6636452d 100644 --- a/example/lib/constants/picker_method.dart +++ b/example/lib/constants/picker_method.dart @@ -207,10 +207,9 @@ class PickMethod { return; } final picker = context.findAncestorWidgetOfExactType< - AssetPicker>()!; - final builder = - picker.builder as DefaultAssetPickerBuilderDelegate; - final p = builder.provider; + AssetPicker>()!; + final p = picker.builder.provider; await p.switchPath( PathWrapper( path: diff --git a/example/lib/customs/pickers/directory_file_asset_picker.dart b/example/lib/customs/pickers/directory_file_asset_picker.dart index 1b2654d7..2df4113d 100644 --- a/example/lib/customs/pickers/directory_file_asset_picker.dart +++ b/example/lib/customs/pickers/directory_file_asset_picker.dart @@ -163,18 +163,19 @@ class _DirectoryFileAssetPickerState extends State { return GestureDetector( onTap: isDisplayingDetail ? () async { - final Widget viewer = AssetPickerViewer( - builder: FileAssetPickerViewerBuilderDelegate( + final result = await AssetPickerViewer.pushToViewerWithDelegate< + File, + Directory, + FileAssetPickerViewerProvider, + FileAssetPickerViewerBuilderDelegate>( + context, + delegate: FileAssetPickerViewerBuilderDelegate( currentIndex: index, previewAssets: fileList, provider: FileAssetPickerViewerProvider(fileList), themeData: AssetPicker.themeData(themeColor), ), ); - final List? result = - await Navigator.maybeOf(context)?.push>( - AssetPickerViewerPageRoute(builder: (context) => viewer), - ); if (result != null && result != fileList) { fileList ..clear() @@ -261,7 +262,8 @@ class _DirectoryFileAssetPickerState extends State { } } -class FileAssetPickerProvider extends AssetPickerProvider { +final class FileAssetPickerProvider + extends AssetPickerProvider { FileAssetPickerProvider({ required List selectedAssets, }) : super(selectedAssets: selectedAssets) { @@ -346,7 +348,7 @@ class FileAssetPickerProvider extends AssetPickerProvider { } } -class FileAssetPickerBuilder +final class FileAssetPickerBuilder extends AssetPickerBuilderDelegate { FileAssetPickerBuilder({ required this.provider, @@ -368,8 +370,13 @@ class FileAssetPickerBuilder int? index, File currentAsset, ) async { - final Widget viewer = AssetPickerViewer( - builder: FileAssetPickerViewerBuilderDelegate( + final result = await AssetPickerViewer.pushToViewerWithDelegate< + File, + Directory, + FileAssetPickerViewerProvider, + FileAssetPickerViewerBuilderDelegate>( + context, + delegate: FileAssetPickerViewerBuilderDelegate( currentIndex: index ?? provider.selectedAssets.indexOf(currentAsset), previewAssets: provider.selectedAssets, provider: FileAssetPickerViewerProvider(provider.selectedAssets), @@ -378,39 +385,11 @@ class FileAssetPickerBuilder selectorProvider: provider, ), ); - final List? result = - await Navigator.maybeOf(context)?.push?>( - AssetPickerViewerPageRoute(builder: (context) => viewer), - ); if (result != null) { Navigator.maybeOf(context)?.maybePop(result); } } - Future?> pushToPicker( - BuildContext context, { - required int index, - required List previewAssets, - List? selectedAssets, - FileAssetPickerProvider? selectorProvider, - }) async { - final Widget viewer = AssetPickerViewer( - builder: FileAssetPickerViewerBuilderDelegate( - currentIndex: index, - previewAssets: previewAssets, - provider: selectedAssets != null - ? FileAssetPickerViewerProvider(selectedAssets) - : null, - themeData: AssetPicker.themeData(themeColor), - selectedAssets: selectedAssets, - selectorProvider: selectorProvider, - ), - ); - return await Navigator.maybeOf(context)?.push?>( - AssetPickerViewerPageRoute(builder: (context) => viewer), - ); - } - @override void selectAsset(BuildContext context, File asset, int index, bool selected) { if (selected) { @@ -975,23 +954,12 @@ class FileAssetPickerBuilder @override Widget previewButton(BuildContext context) { - return Selector( - selector: (_, FileAssetPickerProvider p) => p.isSelectedNotEmpty, - builder: (_, bool isSelectedNotEmpty, __) { + return Consumer( + builder: (_, p, __) { + final isSelectedNotEmpty = p.isSelectedNotEmpty; return GestureDetector( onTap: isSelectedNotEmpty - ? () async { - final List? result = await pushToPicker( - context, - index: 0, - previewAssets: provider.selectedAssets, - selectedAssets: provider.selectedAssets, - selectorProvider: provider, - ); - if (result != null) { - Navigator.maybeOf(context)?.pop(result); - } - } + ? () => viewAsset(context, null, p.selectedAssets.first) : null, child: Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), @@ -1099,15 +1067,8 @@ class FileAssetPickerBuilder selectedAssets.where((File f) => f.path == asset.path).isNotEmpty; return Positioned.fill( child: GestureDetector( - onTap: () async { - final List? result = await pushToPicker( - context, - index: index, - previewAssets: provider.currentAssets, - ); - if (result != null) { - Navigator.maybeOf(context)?.pop(result); - } + onTap: () { + viewAsset(context, index, asset); }, child: AnimatedContainer( duration: switchingPathDuration, @@ -1159,7 +1120,8 @@ class FileAssetPickerBuilder } } -class FileAssetPickerViewerProvider extends AssetPickerViewerProvider { +final class FileAssetPickerViewerProvider + extends AssetPickerViewerProvider { FileAssetPickerViewerProvider(List super.assets); @override @@ -1170,18 +1132,19 @@ class FileAssetPickerViewerProvider extends AssetPickerViewerProvider { } } -class FileAssetPickerViewerBuilderDelegate - extends AssetPickerViewerBuilderDelegate { +final class FileAssetPickerViewerBuilderDelegate + extends AssetPickerViewerBuilderDelegate { FileAssetPickerViewerBuilderDelegate({ required super.previewAssets, required super.themeData, required super.currentIndex, super.selectedAssets, - super.selectorProvider, + this.selectorProvider, super.provider, - }) : super( - maxAssets: selectorProvider?.maxAssets, - ); + }) : super(maxAssets: selectorProvider?.maxAssets); + + final FileAssetPickerProvider? selectorProvider; late final PageController _pageController = PageController( initialPage: currentIndex, diff --git a/example/lib/customs/pickers/insta_asset_picker.dart b/example/lib/customs/pickers/insta_asset_picker.dart index 879fd87d..d946ed6f 100644 --- a/example/lib/customs/pickers/insta_asset_picker.dart +++ b/example/lib/customs/pickers/insta_asset_picker.dart @@ -286,7 +286,7 @@ class _InstaAssetPickerState extends State { } } -class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { +final class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { InstaAssetPickerBuilder({ required super.provider, required super.initialPermission, @@ -488,8 +488,8 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { width: MediaQuery.sizeOf(context).width, height: previewHeight(context), child: Selector>( - selector: (_, DefaultAssetPickerProvider p) => p.selectedAssets, - builder: (_, List selected, __) { + selector: (_, p) => p.selectedAssets, + builder: (_, selected, __) { if (previewAsset == null && selected.isEmpty) { return loadingIndicator(context); } @@ -499,10 +499,13 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { if (previewAsset != null) { effectiveIndex = selected.indexOf(previewAsset); } - final List assets = - selected.isEmpty ? [previewAsset!] : selected; + final assets = selected.isEmpty ? [previewAsset!] : selected; - return AssetPickerViewer( + return AssetPickerViewer< + AssetEntity, + AssetPathEntity, + AssetPickerViewerProvider, + InstaAssetPickerViewerBuilder>( builder: InstaAssetPickerViewerBuilder( currentIndex: effectiveIndex == -1 ? 0 : effectiveIndex, previewAssets: assets, @@ -645,7 +648,7 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { Widget _buildListAlbums(BuildContext context) { appBarPreferredSize ??= appBar(context).preferredSize; return Consumer( - builder: (BuildContext context, DefaultAssetPickerProvider provider, __) { + builder: (context, provider, __) { if (isAppleOS(context)) { return pathEntityListWidget(context); } @@ -673,7 +676,7 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { Widget _buildGrid(BuildContext context) { appBarPreferredSize ??= appBar(context).preferredSize; return Consumer( - builder: (BuildContext context, DefaultAssetPickerProvider p, __) { + builder: (context, p, __) { final bool shouldDisplayAssets = p.hasAssetsToDisplay || shouldBuildSpecialItem; _initializePreviewAsset(p, shouldDisplayAssets); @@ -767,7 +770,7 @@ class InstaAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { const SizedBox.shrink(); } -class InstaAssetPickerViewerBuilder +final class InstaAssetPickerViewerBuilder extends DefaultAssetPickerViewerBuilderDelegate { InstaAssetPickerViewerBuilder({ required super.currentIndex, diff --git a/example/lib/customs/pickers/multi_tabs_assets_picker.dart b/example/lib/customs/pickers/multi_tabs_assets_picker.dart index f60689e6..3007b539 100644 --- a/example/lib/customs/pickers/multi_tabs_assets_picker.dart +++ b/example/lib/customs/pickers/multi_tabs_assets_picker.dart @@ -260,7 +260,8 @@ class _MultiTabAssetPickerState extends State { } } -class MultiTabAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { +final class MultiTabAssetPickerBuilder + extends DefaultAssetPickerBuilderDelegate { MultiTabAssetPickerBuilder({ required super.provider, required this.videosProvider, @@ -279,7 +280,10 @@ class MultiTabAssetPickerBuilder extends DefaultAssetPickerBuilderDelegate { late final TabController _tabController; @override - void initState(AssetPickerState state) { + void initState( + AssetPickerState + state, + ) { super.initState(state); _tabController = TabController(length: 3, vsync: state); } diff --git a/guides/migration_guide.md b/guides/migration_guide.md index 20edd8a3..fe0afda5 100644 --- a/guides/migration_guide.md +++ b/guides/migration_guide.md @@ -8,6 +8,7 @@ This document gathered all breaking changes and migrations requirement between m ## Breaking changes in versions +- [10.0.0](#1000) - [9.2.0](#920) - [9.1.0](#910) - [9.0.0](#900) @@ -19,6 +20,174 @@ This document gathered all breaking changes and migrations requirement between m - [6.0.0](#600) - [5.0.0](#500) +## 10.0.0 + +### Summary + +> [!NOTE] +> If you didn't extend `AssetPickerBuilderDelegate` or `AssetPickerViewerBuilderDelegate` +> to build delegates on your own, you can stop reading. + +This version introduces more generic types to increase flexibility and customization. +This means that if you have created your own custom delegates or providers, +you will need to update their signatures. +Specifically, `AssetPickerBuilderDelegate` and `AssetPickerViewerBuilderDelegate` +(and their default implementations) now require more generic type arguments. +This allows for greater control over the types of assets, paths, +and providers used within the picker. + +### Details + +#### `AssetPickerBuilderDelegate` and `DefaultAssetPickerBuilderDelegate` + +`AssetPickerBuilderDelegate` and `DefaultAssetPickerBuilderDelegate` are now more generic. + +- `AssetPickerBuilderDelegate`'s `initState` method now takes a generic `AssetPickerState`. +- `DefaultAssetPickerBuilderDelegate` now has a generic type `T` which should extend `DefaultAssetPickerProvider`. + +Before: + +```dart +// In AssetPickerBuilderDelegate +void initState(AssetPickerState state) {} + +// In DefaultAssetPickerBuilderDelegate +class DefaultAssetPickerBuilderDelegate extends AssetPickerBuilderDelegate { + final DefaultAssetPickerProvider provider; + // ... +} +``` + +After: + +```dart +// In AssetPickerBuilderDelegate +void initState( + covariant AssetPickerState> + state, +) {} + +// In DefaultAssetPickerBuilderDelegate +class DefaultAssetPickerBuilderDelegate + extends AssetPickerBuilderDelegate { + final T provider; + // ... +} +``` + +#### `AssetPickerViewerBuilderDelegate` and `DefaultAssetPickerViewerBuilderDelegate` + +Similar to the picker builder delegates, the viewer builder delegates are now more generic. + +- `AssetPickerViewerBuilderDelegate` now has generic types for `Asset`, `Path`, and `Provider`. +- `DefaultAssetPickerViewerBuilderDelegate` is now generic with types `T` (extends `AssetPickerViewerProvider`) and `P` (extends `DefaultAssetPickerProvider`). +- `initStateAndTicker` is renamed to `initState` and its signature has changed. +- `didUpdateViewer`'s signature has changed. + +Before: + +```dart +// In AssetPickerViewerBuilderDelegate +abstract class AssetPickerViewerBuilderDelegate { + void initStateAndTicker( + covariant AssetPickerViewerState state, + TickerProvider v, + ); + + void didUpdateViewer( + covariant AssetPickerViewerState state, + covariant AssetPickerViewer oldWidget, + covariant AssetPickerViewer newWidget, + ); +} + +// In DefaultAssetPickerViewerBuilderDelegate +class DefaultAssetPickerViewerBuilderDelegate + extends AssetPickerViewerBuilderDelegate { + // ... +} +``` + +After: + +```dart +// In AssetPickerViewerBuilderDelegate +abstract class AssetPickerViewerBuilderDelegate> { + void initState( + covariant AssetPickerViewerState state, + ); + + void didUpdateViewer( + covariant AssetPickerViewerState state, + covariant AssetPickerViewer oldWidget, + covariant AssetPickerViewer newWidget, + ); +} + +// In DefaultAssetPickerViewerBuilderDelegate +class DefaultAssetPickerViewerBuilderDelegate< + T extends AssetPickerViewerProvider, + P extends DefaultAssetPickerProvider> + extends AssetPickerViewerBuilderDelegate { + // ... +} +``` + +#### `AssetPicker` and `AssetPickerViewer` + +`AssetPicker`, `AssetPickerViewer` and their states are now generic. +When using `pickAssetsWithDelegate` or `pushToViewerWithDelegate`, +you now need to pass the delegate type. + +Before: + +```dart +// AssetPicker.pickAssetsWithDelegate +static Future?> pickAssetsWithDelegate>( + BuildContext context, { + required AssetPickerBuilderDelegate delegate, +}); + +// AssetPickerViewer.pushToViewerWithDelegate +static Future?> pushToViewerWithDelegate( + BuildContext context, { + required AssetPickerViewerBuilderDelegate delegate, +}); +``` + +After: + +```dart +// AssetPicker.pickAssetsWithDelegate +static Future?> pickAssetsWithDelegate< + Asset, + Path, + PickerProvider extends AssetPickerProvider, + Delegate extends AssetPickerBuilderDelegate>( + BuildContext context, { + required Delegate delegate, +}); + +// AssetPickerViewer.pushToViewerWithDelegate +static Future?> pushToViewerWithDelegate< + Asset, + Path, + Provider extends AssetPickerViewerProvider, + Delegate extends AssetPickerViewerBuilderDelegate>( + BuildContext context, { + required Delegate delegate, +}); +``` + +#### `AssetPickerViewerProvider` + +- The generic type `A` is renamed to `Asset`. +- Deprecated methods `selectAssetEntity` and `unselectAssetEntity` have been removed. + Use `selectAsset` and `unSelectAsset` instead. + ## 9.2.0 > [!NOTE] diff --git a/lib/builder.dart b/lib/builder.dart new file mode 100644 index 00000000..6b862abe --- /dev/null +++ b/lib/builder.dart @@ -0,0 +1,8 @@ +// Copyright 2019 The FlutterCandies author. All rights reserved. +// Use of this source code is governed by an Apache license that can be found +// in the LICENSE file. + +export 'src/widget/builder/asset_entity_grid_item_builder.dart'; +export 'src/widget/builder/audio_page_builder.dart'; +export 'src/widget/builder/image_page_builder.dart'; +export 'src/widget/builder/video_page_builder.dart'; diff --git a/lib/src/delegates/asset_picker_builder_delegate.dart b/lib/src/delegates/asset_picker_builder_delegate.dart index e16ef52d..5004c203 100644 --- a/lib/src/delegates/asset_picker_builder_delegate.dart +++ b/lib/src/delegates/asset_picker_builder_delegate.dart @@ -126,14 +126,13 @@ abstract class AssetPickerBuilderDelegate { final LimitedPermissionOverlayPredicate? limitedPermissionOverlayPredicate; /// {@macro wechat_assets_picker.PathNameBuilder} - final PathNameBuilder? pathNameBuilder; + final PathNameBuilder? pathNameBuilder; /// {@macro wechat_assets_picker.AssetsChangeCallback} - final AssetsChangeCallback? assetsChangeCallback; + final AssetsChangeCallback? assetsChangeCallback; /// {@macro wechat_assets_picker.AssetsChangeRefreshPredicate} - final AssetsChangeRefreshPredicate? - assetsChangeRefreshPredicate; + final AssetsChangeRefreshPredicate? assetsChangeRefreshPredicate; final bool viewerUseRootNavigator; final RouteSettings? viewerPageRouteSettings; @@ -238,7 +237,11 @@ abstract class AssetPickerBuilderDelegate { /// Keep a `initState` method to sync with [State]. /// 保留一个 `initState` 方法与 [State] 同步。 @mustCallSuper - void initState(AssetPickerState state) {} + void initState( + covariant AssetPickerState> + state, + ) {} /// Keep a `dispose` method to sync with [State]. /// 保留一个 `dispose` 方法与 [State] 同步。 @@ -481,11 +484,7 @@ abstract class AssetPickerBuilderDelegate { /// Loading indicator. /// 加载指示器 /// - /// Subclasses need to implement this due to the generic type limitation, and - /// not all delegates use [AssetPickerProvider]. - /// - /// See also: - /// - [DefaultAssetPickerBuilderDelegate.loadingIndicator] as an example. + /// See [DefaultAssetPickerBuilderDelegate.loadingIndicator] as an example. Widget loadingIndicator(BuildContext context); /// GIF image type indicator. @@ -820,7 +819,7 @@ abstract class AssetPickerBuilderDelegate { } } -class DefaultAssetPickerBuilderDelegate +class DefaultAssetPickerBuilderDelegate extends AssetPickerBuilderDelegate { DefaultAssetPickerBuilderDelegate({ required this.provider, @@ -857,7 +856,7 @@ class DefaultAssetPickerBuilderDelegate /// [ChangeNotifier] for asset picker. /// 资源选择器状态保持 - final DefaultAssetPickerProvider provider; + final T provider; /// Thumbnail size in the grid. /// 预览时网络的缩略图大小 @@ -950,7 +949,11 @@ class DefaultAssetPickerBuilderDelegate } @override - void initState(AssetPickerState state) { + void initState( + covariant AssetPickerState + state, + ) { super.initState(state); presentLimitedTapGestureRecognizer = TapGestureRecognizer() ..onTap = PhotoManager.presentLimited; @@ -984,8 +987,7 @@ class DefaultAssetPickerBuilderDelegate if (selectPredicateResult == false) { return; } - final DefaultAssetPickerProvider provider = - context.read(); + final provider = context.read(); if (selected) { provider.unSelectAsset(asset); return; @@ -1117,7 +1119,7 @@ class DefaultAssetPickerBuilderDelegate int? index, AssetEntity currentAsset, ) async { - final p = context.read(); + final p = context.read(); // - When we reached the maximum select count and the asset is not selected, // do nothing. // - When the special type is WeChat Moment, pictures and videos cannot @@ -1184,7 +1186,7 @@ class DefaultAssetPickerBuilderDelegate if (current.isEmpty) { throw StateError('Previewing empty assets is not allowed. $_debugFlow'); } - final List? result = await AssetPickerViewer.pushToViewer( + final result = await AssetPickerViewer.pushToViewer( context, currentIndex: effectiveIndex, previewAssets: current, @@ -1224,8 +1226,8 @@ class DefaultAssetPickerBuilderDelegate Widget androidLayout(BuildContext context) { return AssetPickerAppBarWrapper( appBar: appBar(context), - body: Consumer( - builder: (BuildContext context, DefaultAssetPickerProvider p, _) { + body: Consumer( + builder: (BuildContext context, T p, _) { final bool shouldDisplayAssets = p.hasAssetsToDisplay || shouldBuildSpecialItem; return AnimatedSwitcher( @@ -1275,8 +1277,8 @@ class DefaultAssetPickerBuilderDelegate return Stack( children: [ Positioned.fill( - child: Consumer( - builder: (_, DefaultAssetPickerProvider p, __) { + child: Consumer( + builder: (_, p, __) { final Widget child; final bool shouldDisplayAssets = p.hasAssetsToDisplay || shouldBuildSpecialItem; @@ -1317,8 +1319,8 @@ class DefaultAssetPickerBuilderDelegate @override Widget loadingIndicator(BuildContext context) { - return Selector( - selector: (_, DefaultAssetPickerProvider p) => p.isAssetsEmpty, + return Selector( + selector: (_, T p) => p.isAssetsEmpty, builder: (BuildContext context, bool isAssetsEmpty, Widget? w) { if (loadingIndicatorBuilder != null) { return loadingIndicatorBuilder!(context, isAssetsEmpty); @@ -1337,8 +1339,8 @@ class DefaultAssetPickerBuilderDelegate appBarPreferredSize ??= appBar(context).preferredSize; final bool gridRevert = effectiveShouldRevertGrid(context); final accessibleNavigation = MediaQuery.accessibleNavigationOf(context); - return Selector?>( - selector: (_, DefaultAssetPickerProvider p) => p.currentPath, + return Selector?>( + selector: (_, T p) => p.currentPath, builder: (context, wrapper, _) { // First, we need the count of the assets. int totalCount = wrapper?.assetCount ?? 0; @@ -1530,9 +1532,8 @@ class DefaultAssetPickerBuilderDelegate : Directionality.of(context), child: ColoredBox( color: theme.canvasColor, - child: Selector>( - selector: (_, DefaultAssetPickerProvider p) => - p.currentAssets, + child: Selector>( + selector: (_, T p) => p.currentAssets, builder: (BuildContext context, List assets, _) { final SliverGap bottomGap = SliverGap.v( context.bottomPadding + bottomSectionHeight, @@ -1590,8 +1591,7 @@ class DefaultAssetPickerBuilderDelegate List currentAssets, { Widget? specialItem, }) { - final DefaultAssetPickerProvider p = - context.read(); + final p = context.read(); final int length = currentAssets.length; final PathWrapper? currentWrapper = p.currentPath; final AssetPathEntity? currentPathEntity = currentWrapper?.path; @@ -1661,8 +1661,8 @@ class DefaultAssetPickerBuilderDelegate return ValueListenableBuilder( valueListenable: isSwitchingPath, builder: (_, bool isSwitchingPath, Widget? child) { - return Consumer( - builder: (_, DefaultAssetPickerProvider p, __) { + return Consumer( + builder: (_, T p, __) { final bool isBanned = (!p.selectedAssets.contains(asset) && p.selectedMaximumAssets) || (isWeChatMoment && @@ -1750,9 +1750,9 @@ class DefaultAssetPickerBuilderDelegate int placeholderCount = 0, Widget? specialItem, }) { - final PathWrapper? currentWrapper = context - .select?>( - (DefaultAssetPickerProvider p) => p.currentPath, + final PathWrapper? currentWrapper = + context.select?>( + (T p) => p.currentPath, ); final AssetPathEntity? currentPathEntity = currentWrapper?.path; final int length = assets.length + placeholderCount; @@ -1836,13 +1836,13 @@ class DefaultAssetPickerBuilderDelegate ); } - /// It'll pop with [AssetPickerProvider.selectedAssets] + /// It'll pop with provider's `selectedAssets` /// when there are any assets were chosen. /// 当有资源已选时,点击按钮将把已选资源通过路由返回。 @override Widget confirmButton(BuildContext context) { - return Consumer( - builder: (_, DefaultAssetPickerProvider p, __) { + return Consumer( + builder: (_, T p, __) { final bool isSelectedNotEmpty = p.isSelectedNotEmpty; final bool shouldAllowConfirm = isSelectedNotEmpty || p.previousSelectedAssets.isNotEmpty; @@ -2052,9 +2052,8 @@ class DefaultAssetPickerBuilderDelegate ), ), Flexible( - child: Selector>>( - selector: (_, DefaultAssetPickerProvider p) => p.paths, + child: Selector>>( + selector: (_, T p) => p.paths, builder: (_, List> paths, __) { final List> filtered = paths .where( @@ -2129,9 +2128,8 @@ class DefaultAssetPickerBuilderDelegate borderRadius: BorderRadius.circular(999), color: theme.focusColor, ), - child: Selector?>( - selector: (_, DefaultAssetPickerProvider p) => p.currentPath, + child: Selector?>( + selector: (_, T p) => p.currentPath, builder: (_, PathWrapper? p, Widget? w) { final AssetPathEntity? path = p?.path; return Row( @@ -2224,8 +2222,8 @@ class DefaultAssetPickerBuilderDelegate if (semanticsCount != null) { labelBuffer.write(': $semanticsCount'); } - return Selector?>( - selector: (_, DefaultAssetPickerProvider p) => p.currentPath, + return Selector?>( + selector: (_, T p) => p.currentPath, builder: (_, PathWrapper? currentWrapper, __) { final bool isSelected = currentWrapper?.path == pathEntity; return Semantics( @@ -2239,7 +2237,7 @@ class DefaultAssetPickerBuilderDelegate splashFactory: InkSplash.splashFactory, onTap: () { Feedback.forTap(context); - context.read().switchPath(wrapper); + context.read().switchPath(wrapper); isSwitchingPath.value = false; gridScrollController.jumpTo(0); }, @@ -2287,8 +2285,8 @@ class DefaultAssetPickerBuilderDelegate @override Widget previewButton(BuildContext context) { - return Consumer( - builder: (_, DefaultAssetPickerProvider p, Widget? child) { + return Consumer( + builder: (_, T p, Widget? child) { return ValueListenableBuilder( valueListenable: isSwitchingPath, builder: (_, bool isSwitchingPath, __) => Semantics( @@ -2300,16 +2298,15 @@ class DefaultAssetPickerBuilderDelegate ), ); }, - child: Consumer( - builder: (context, DefaultAssetPickerProvider p, __) => GestureDetector( + child: Consumer( + builder: (context, T p, __) => GestureDetector( onTap: p.isSelectedNotEmpty ? () { viewAsset(context, null, p.selectedAssets.first); } : null, - child: Selector( - selector: (_, DefaultAssetPickerProvider p) => - p.selectedDescriptions, + child: Selector( + selector: (_, T p) => p.selectedDescriptions, builder: (BuildContext c, __, ___) => Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: ScaleText( @@ -2334,8 +2331,8 @@ class DefaultAssetPickerBuilderDelegate @override Widget itemBannedIndicator(BuildContext context, AssetEntity asset) { - return Consumer( - builder: (_, DefaultAssetPickerProvider p, __) { + return Consumer( + builder: (_, T p, __) { final bool isDisabled = (!p.selectedAssets.contains(asset) && p.selectedMaximumAssets) || (isWeChatMoment && @@ -2356,8 +2353,8 @@ class DefaultAssetPickerBuilderDelegate final double indicatorSize = MediaQuery.sizeOf(context).width / gridCount / 3; final Duration duration = switchingPathDuration * 0.75; - return Selector( - selector: (_, DefaultAssetPickerProvider p) => p.selectedDescriptions, + return Selector( + selector: (_, T p) => p.selectedDescriptions, builder: (BuildContext context, String descriptions, __) { final bool selected = descriptions.contains(asset.toString()); final Widget innerSelector = AnimatedContainer( @@ -2422,8 +2419,8 @@ class DefaultAssetPickerBuilderDelegate viewAsset(context, index, asset); } : null, - child: Consumer( - builder: (_, DefaultAssetPickerProvider p, __) { + child: Consumer( + builder: (_, T p, __) { final int index = p.selectedAssets.indexOf(asset); final bool selected = index != -1; return AnimatedContainer( @@ -2617,7 +2614,7 @@ class DefaultAssetPickerBuilderDelegate data: theme, child: AnnotatedRegion( value: overlayStyle, - child: CNP.value( + child: CNP.value( value: provider, builder: (BuildContext context, _) => Scaffold( backgroundColor: theme.scaffoldBackgroundColor, diff --git a/lib/src/delegates/asset_picker_delegate.dart b/lib/src/delegates/asset_picker_delegate.dart index 56e31f78..45292346 100644 --- a/lib/src/delegates/asset_picker_delegate.dart +++ b/lib/src/delegates/asset_picker_delegate.dart @@ -96,7 +96,8 @@ class AssetPickerDelegate { filterOptions: pickerConfig.filterOptions, initializeDelayDuration: route.transitionDuration, ); - final Widget picker = AssetPicker( + final picker = AssetPicker( key: key, permissionRequestOption: permissionRequestOption, builder: DefaultAssetPickerBuilderDelegate( @@ -154,10 +155,13 @@ class AssetPickerDelegate { /// * [AssetPickerBuilderDelegate] for how to customize/override widgets /// during the picking process. /// {@endtemplate} - Future?> pickAssetsWithDelegate>( + Future?> pickAssetsWithDelegate< + Asset, + Path, + PickerProvider extends AssetPickerProvider, + Delegate extends AssetPickerBuilderDelegate>( BuildContext context, { - required AssetPickerBuilderDelegate delegate, + required Delegate delegate, PermissionRequestOption permissionRequestOption = const PermissionRequestOption(), Key? key, @@ -166,15 +170,15 @@ class AssetPickerDelegate { AssetPickerPageRouteBuilder>? pageRouteBuilder, }) async { await permissionCheck(requestOption: permissionRequestOption); - final Widget picker = AssetPicker( + final picker = AssetPicker( key: key, permissionRequestOption: permissionRequestOption, builder: delegate, ); - final List? result = await Navigator.maybeOf( + final result = await Navigator.maybeOf( context, rootNavigator: useRootNavigator, - )?.push>( + )?.push( pageRouteBuilder?.call(picker) ?? AssetPickerPageRoute>( builder: (_) => picker, diff --git a/lib/src/delegates/asset_picker_viewer_builder_delegate.dart b/lib/src/delegates/asset_picker_viewer_builder_delegate.dart index 6b61daac..8fc3816d 100644 --- a/lib/src/delegates/asset_picker_viewer_builder_delegate.dart +++ b/lib/src/delegates/asset_picker_viewer_builder_delegate.dart @@ -28,12 +28,12 @@ import '../widget/builder/fade_image_builder.dart'; import '../widget/builder/image_page_builder.dart'; import '../widget/builder/video_page_builder.dart'; -abstract class AssetPickerViewerBuilderDelegate { +abstract class AssetPickerViewerBuilderDelegate> { AssetPickerViewerBuilderDelegate({ required this.previewAssets, required this.themeData, required this.currentIndex, - this.selectorProvider, this.provider, this.selectedAssets, this.maxAssets, @@ -45,7 +45,7 @@ abstract class AssetPickerViewerBuilderDelegate { /// [ChangeNotifier] for photo selector viewer. /// 资源预览器的状态保持 - final AssetPickerViewerProvider? provider; + final Provider? provider; /// Assets provided to preview. /// 提供预览的资源 @@ -59,10 +59,6 @@ abstract class AssetPickerViewerBuilderDelegate { /// 已选的资源 final List? selectedAssets; - /// Provider for [AssetPicker]. - /// 资源选择器的状态保持 - final AssetPickerProvider? selectorProvider; - /// Whether the preview sequence is reversed. /// 预览时顺序是否为反向 /// @@ -94,7 +90,7 @@ abstract class AssetPickerViewerBuilderDelegate { /// The [State] for a viewer. /// 预览器的状态实例 - late AssetPickerViewerState viewerState; + late AssetPickerViewerState viewerState; /// [AnimationController] for double tap animation. /// 双击缩放的动画控制器 @@ -164,9 +160,8 @@ abstract class AssetPickerViewerBuilderDelegate { /// Call when viewer is calling [State.initState]. /// 当预览器调用 [State.initState] 时注册 [State]。 @mustCallSuper - void initStateAndTicker( - covariant AssetPickerViewerState state, - TickerProvider v, // TODO(Alex): Remove this in the next major version. + void initState( + covariant AssetPickerViewerState state, ) { initAnimations(state); } @@ -180,9 +175,9 @@ abstract class AssetPickerViewerBuilderDelegate { /// a new delegate and only calling [State.didUpdateWidget] at the moment. @mustCallSuper void didUpdateViewer( - covariant AssetPickerViewerState state, - covariant AssetPickerViewer oldWidget, - covariant AssetPickerViewer newWidget, + covariant AssetPickerViewerState state, + covariant AssetPickerViewer oldWidget, + covariant AssetPickerViewer newWidget, ) { // Widgets are useless in the default delegate. initAnimations(state); @@ -206,7 +201,9 @@ abstract class AssetPickerViewerBuilderDelegate { /// Initialize animations related to the zooming preview. /// 为缩放预览初始化动画 - void initAnimations(covariant AssetPickerViewerState state) { + void initAnimations( + covariant AssetPickerViewerState state, + ) { viewerState = state; doubleTapAnimationController = AnimationController( duration: const Duration(milliseconds: 200), @@ -262,23 +259,21 @@ abstract class AssetPickerViewerBuilderDelegate { late final ValueNotifier selectedNotifier = ValueNotifier(selectedCount); - void unSelectAsset(Asset entity) { - provider?.unSelectAsset(entity); - selectorProvider?.unSelectAsset(entity); + void unSelectAsset(Asset asset) { + provider?.unSelectAsset(asset); if (!isSelectedPreviewing) { - selectedAssets?.remove(entity); + selectedAssets?.remove(asset); } selectedNotifier.value = selectedCount; } - void selectAsset(Asset entity) { + void selectAsset(Asset asset) { if (maxAssets != null && selectedCount > maxAssets!) { return; } - provider?.selectAsset(entity); - selectorProvider?.selectAsset(entity); + provider?.selectAsset(asset); if (!isSelectedPreviewing) { - selectedAssets?.add(entity); + selectedAssets?.add(asset); } selectedNotifier.value = selectedCount; } @@ -310,19 +305,6 @@ abstract class AssetPickerViewerBuilderDelegate { isDisplayingDetail.value = value ?? !isDisplayingDetail.value; } - /// Sync selected assets currently with asset picker provider. - /// 在预览中当前已选的图片同步到选择器的状态 - @Deprecated( - 'No longer used by the package. ' - 'This will be removed in 10.0.0', - ) - Future syncSelectedAssetsWhenPop() async { - if (provider?.currentlySelectedAssets != null) { - selectorProvider?.selectedAssets = provider!.currentlySelectedAssets; - } - return true; - } - /// Split page builder according to type of asset. /// 根据资源类型使用不同的构建页 Widget assetPageBuilder(BuildContext context, int index); @@ -343,7 +325,7 @@ abstract class AssetPickerViewerBuilderDelegate { }; } - /// The item widget when [AssetEntity.thumbnailData] load failed. + /// The item widget when a thumb data load failed. /// 资源缩略数据加载失败时使用的部件 Widget failedItemBuilder(BuildContext context) { return Center( @@ -377,15 +359,17 @@ abstract class AssetPickerViewerBuilderDelegate { Widget build(BuildContext context); } -class DefaultAssetPickerViewerBuilderDelegate - extends AssetPickerViewerBuilderDelegate { +class DefaultAssetPickerViewerBuilderDelegate< + T extends AssetPickerViewerProvider, + P extends DefaultAssetPickerProvider> + extends AssetPickerViewerBuilderDelegate { DefaultAssetPickerViewerBuilderDelegate({ required super.currentIndex, required super.previewAssets, required super.themeData, - super.selectorProvider, super.provider, super.selectedAssets, + this.selectorProvider, this.previewThumbnailSize, this.specialPickerType, super.maxAssets, @@ -394,9 +378,9 @@ class DefaultAssetPickerViewerBuilderDelegate this.shouldAutoplayPreview = false, }); - /// Whether the preview should auto play. - /// 预览是否自动播放 - final bool shouldAutoplayPreview; + /// Provider for [AssetPicker]. + /// 资源选择器的状态保持 + final P? selectorProvider; /// Thumb size for the preview of images in the viewer. /// 预览时图片的缩略图大小 @@ -409,6 +393,10 @@ class DefaultAssetPickerViewerBuilderDelegate /// 如果类型不为空,则标题将不会显示。 final SpecialPickerType? specialPickerType; + /// Whether the preview should auto play. + /// 预览是否自动播放 + final bool shouldAutoplayPreview; + /// Whether the [SpecialPickerType.wechatMoment] is enabled. /// 当前是否为微信朋友圈选择模式 bool get isWeChatMoment => @@ -421,12 +409,56 @@ class DefaultAssetPickerViewerBuilderDelegate (selectedAssets?.any((AssetEntity e) => e.type == AssetType.video) ?? false); + @override + void unSelectAsset(AssetEntity asset) { + super.unSelectAsset(asset); + selectorProvider?.unSelectAsset(asset); + } + + @override + void selectAsset(AssetEntity asset) { + super.selectAsset(asset); + selectedNotifier.value = selectedCount; + } + + Widget assetSemanticsBuilder(BuildContext context, int index) { + final asset = previewAssets.elementAt( + shouldReversePreview ? previewAssets.length - index - 1 : index, + ); + return MergeSemantics( + child: Consumer( + builder: (context, T? p, child) { + final bool isSelected = + (p?.currentlySelectedAssets ?? selectedAssets)?.contains(asset) ?? + false; + final labels = [ + '${semanticsTextDelegate.semanticTypeLabel(asset.type)}' + '${index + 1}', + asset.createDateTime.toString().replaceAll('.000', ''), + if (asset.type == AssetType.audio || asset.type == AssetType.video) + '${semanticsTextDelegate.sNameDurationLabel}: ' + '${semanticsTextDelegate.durationIndicatorBuilder(asset.videoDuration)}', + if (asset.title case final title? when title.isNotEmpty) title, + ]; + return Semantics( + label: labels.join(', '), + selected: isSelected, + image: + asset.type == AssetType.image || asset.type == AssetType.video, + child: child, + ); + }, + child: assetPageBuilder(context, index), + ), + ); + } + @override Widget assetPageBuilder(BuildContext context, int index) { - final AssetEntity asset = previewAssets.elementAt( + final asset = previewAssets.elementAt( shouldReversePreview ? previewAssets.length - index - 1 : index, ); - final Widget builder = switch (asset.type) { + return switch (asset.type) { AssetType.audio => AudioPageBuilder( asset: asset, shouldAutoplayPreview: shouldAutoplayPreview, @@ -450,36 +482,6 @@ class DefaultAssetPickerViewerBuilderDelegate ), ), }; - return MergeSemantics( - child: Consumer?>( - builder: ( - BuildContext c, - AssetPickerViewerProvider? p, - Widget? w, - ) { - final bool isSelected = - (p?.currentlySelectedAssets ?? selectedAssets)?.contains(asset) ?? - false; - final labels = [ - '${semanticsTextDelegate.semanticTypeLabel(asset.type)}' - '${index + 1}', - asset.createDateTime.toString().replaceAll('.000', ''), - if (asset.type == AssetType.audio || asset.type == AssetType.video) - '${semanticsTextDelegate.sNameDurationLabel}: ' - '${semanticsTextDelegate.durationIndicatorBuilder(asset.videoDuration)}', - if (asset.title case final title? when title.isNotEmpty) title, - ]; - return Semantics( - label: labels.join(', '), - selected: isSelected, - image: - asset.type == AssetType.image || asset.type == AssetType.video, - child: w, - ); - }, - child: builder, - ), - ); } /// Preview item widgets for audios. @@ -571,47 +573,43 @@ class DefaultAssetPickerViewerBuilderDelegate height: context.bottomPadding + bottomDetailHeight, child: child!, ), - child: CNP?>.value( - value: provider, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (provider != null) - ValueListenableBuilder( - valueListenable: selectedNotifier, - builder: (_, int count, __) => Container( - width: count > 0 ? double.maxFinite : 0, - height: bottomPreviewHeight, - color: backgroundColor, - child: ListView.builder( - controller: previewingListController, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 5.0), - physics: const ClampingScrollPhysics(), - itemCount: count, - itemBuilder: bottomDetailItemBuilder, - ), - ), - ), - Container( - height: bottomBarHeight + context.bottomPadding, - padding: const EdgeInsets.symmetric(horizontal: 20.0) - .copyWith(bottom: context.bottomPadding), - decoration: BoxDecoration( - border: Border(top: BorderSide(color: themeData.canvasColor)), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (provider != null) + ValueListenableBuilder( + valueListenable: selectedNotifier, + builder: (_, int count, __) => Container( + width: count > 0 ? double.maxFinite : 0, + height: bottomPreviewHeight, color: backgroundColor, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (provider != null || isWeChatMoment) - confirmButton(context), - ], + child: ListView.builder( + controller: previewingListController, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 5.0), + physics: const ClampingScrollPhysics(), + itemCount: count, + itemBuilder: bottomDetailItemBuilder, + ), ), ), - ], - ), + Container( + height: bottomBarHeight + context.bottomPadding, + padding: const EdgeInsets.symmetric(horizontal: 20.0) + .copyWith(bottom: context.bottomPadding), + decoration: BoxDecoration( + border: Border(top: BorderSide(color: themeData.canvasColor)), + color: backgroundColor, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (provider != null || isWeChatMoment) confirmButton(context), + ], + ), + ), + ], ), ); } @@ -679,10 +677,8 @@ class DefaultAssetPickerViewerBuilderDelegate onTap: () { onTap(asset); }, - child: Selector?, - List?>( - selector: (_, AssetPickerViewerProvider? p) => - p?.currentlySelectedAssets, + child: Selector?>( + selector: (_, T? p) => p?.currentlySelectedAssets, child: item, builder: ( _, @@ -782,90 +778,87 @@ class DefaultAssetPickerViewerBuilderDelegate /// 资源选择器将识别并一同返回。 @override Widget confirmButton(BuildContext context) { - return CNP?>.value( - value: provider, - child: Consumer?>( - builder: (_, AssetPickerViewerProvider? provider, __) { - assert( - isWeChatMoment || provider != null, - 'Viewer provider must not be null ' - 'when the special type is not WeChat moment.', - ); - Future onPressed() async { - if (isWeChatMoment && hasVideo) { - if (await onChangingSelected(context, currentAsset, false)) { - Navigator.maybeOf(context)?.pop([currentAsset]); - } - return; + return Consumer( + builder: (context, T? provider, __) { + assert( + isWeChatMoment || provider != null, + 'Viewer provider must not be null ' + 'when the special type is not WeChat moment.', + ); + Future onPressed() async { + if (isWeChatMoment && hasVideo) { + if (await onChangingSelected(context, currentAsset, false)) { + Navigator.maybeOf(context)?.pop([currentAsset]); } + return; + } - if (provider!.isSelectedNotEmpty) { - Navigator.maybeOf(context)?.pop(provider.currentlySelectedAssets); - return; - } + if (provider!.isSelectedNotEmpty) { + Navigator.maybeOf(context)?.pop(provider.currentlySelectedAssets); + return; + } - if (await onChangingSelected(context, currentAsset, false)) { - Navigator.maybeOf(context)?.pop( - selectedAssets ?? [currentAsset], - ); - } + if (await onChangingSelected(context, currentAsset, false)) { + Navigator.maybeOf(context)?.pop( + selectedAssets ?? [currentAsset], + ); + return; } + } - String buildText() { - if (isWeChatMoment && hasVideo) { - return textDelegate.confirm; - } - if (provider!.isSelectedNotEmpty) { - return '${textDelegate.confirm}' - ' (${provider.currentlySelectedAssets.length}' - '/' - '${selectorProvider!.maxAssets})'; - } + String buildText() { + if (isWeChatMoment && hasVideo) { return textDelegate.confirm; } - - final isButtonEnabled = provider == null || - previewAssets.isEmpty || - (selectedAssets?.isNotEmpty ?? false); - return MaterialButton( - minWidth: - (isWeChatMoment && hasVideo) || provider!.isSelectedNotEmpty - ? 48 - : 20, - height: 32, - padding: const EdgeInsets.symmetric(horizontal: 12), - color: themeData.colorScheme.secondary, - disabledColor: themeData.splashColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(3), + if (provider!.isSelectedNotEmpty) { + return '${textDelegate.confirm}' + ' (${provider.currentlySelectedAssets.length}' + '/' + '${selectorProvider!.maxAssets})'; + } + return textDelegate.confirm; + } + + final isButtonEnabled = provider == null || + previewAssets.isEmpty || + (selectedAssets?.isNotEmpty ?? false); + return MaterialButton( + minWidth: (isWeChatMoment && hasVideo) || provider!.isSelectedNotEmpty + ? 48 + : 20, + height: 32, + padding: const EdgeInsets.symmetric(horizontal: 12), + color: themeData.colorScheme.secondary, + disabledColor: themeData.splashColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3), + ), + onPressed: isButtonEnabled ? onPressed : null, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + child: ScaleText( + buildText(), + style: TextStyle( + color: themeData.textTheme.bodyLarge?.color, + fontSize: 17, + fontWeight: FontWeight.normal, ), - onPressed: isButtonEnabled ? onPressed : null, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - child: ScaleText( - buildText(), - style: TextStyle( - color: themeData.textTheme.bodyLarge?.color, - fontSize: 17, - fontWeight: FontWeight.normal, - ), - overflow: TextOverflow.fade, - softWrap: false, - semanticsLabel: () { - if (isWeChatMoment && hasVideo) { - return semanticsTextDelegate.confirm; - } - if (provider!.isSelectedNotEmpty) { - return '${semanticsTextDelegate.confirm}' - ' (${provider.currentlySelectedAssets.length}' - '/' - '${selectorProvider!.maxAssets})'; - } + overflow: TextOverflow.fade, + softWrap: false, + semanticsLabel: () { + if (isWeChatMoment && hasVideo) { return semanticsTextDelegate.confirm; - }(), - ), - ); - }, - ), + } + if (provider!.isSelectedNotEmpty) { + return '${semanticsTextDelegate.confirm}' + ' (${provider.currentlySelectedAssets.length}' + '/' + '${selectorProvider!.maxAssets})'; + } + return semanticsTextDelegate.confirm; + }(), + ), + ); + }, ); } @@ -922,63 +915,59 @@ class DefaultAssetPickerViewerBuilderDelegate @override Widget selectButton(BuildContext context) { - return CNP>.value( - value: provider!, - builder: (_, Widget? w) => StreamBuilder( - initialData: currentIndex, - stream: pageStreamController.stream, - builder: (_, s) { - final index = s.data!; - final assetIndex = - shouldReversePreview ? previewAssets.length - index - 1 : index; - if (assetIndex < 0) { - throw IndexError.withLength( - assetIndex, - previewAssets.length, - indexable: previewAssets, - name: 'selectButton.assetIndex', - message: 'previewReversed: $shouldReversePreview\n' - 'stream.index: $index\n' - 'selectedAssets.length: ${selectedAssets?.length}\n' - 'previewAssets.length: ${previewAssets.length}\n' - 'currentIndex: $currentIndex\n' - 'maxAssets: $maxAssets', - ); - } - final asset = previewAssets.elementAt(assetIndex); - return Selector, - List>( - selector: (_, p) => p.currentlySelectedAssets, - builder: (context, assets, _) { - final bool isSelected = assets.contains(asset); - return Semantics( - selected: isSelected, - label: semanticsTextDelegate.select, - onTap: () { - onChangingSelected(context, asset, isSelected); - }, - onTapHint: semanticsTextDelegate.select, - excludeSemantics: true, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (isAppleOS(context)) - _appleOSSelectButton(context, isSelected, asset) - else - _androidSelectButton(context, isSelected, asset), - if (!isAppleOS(context)) - ScaleText( - textDelegate.select, - style: const TextStyle(fontSize: 17, height: 1.2), - semanticsLabel: semanticsTextDelegate.select, - ), - ], - ), - ); - }, + return StreamBuilder( + initialData: currentIndex, + stream: pageStreamController.stream, + builder: (_, s) { + final index = s.data!; + final assetIndex = + shouldReversePreview ? previewAssets.length - index - 1 : index; + if (assetIndex < 0) { + throw IndexError.withLength( + assetIndex, + previewAssets.length, + indexable: previewAssets, + name: 'selectButton.assetIndex', + message: 'previewReversed: $shouldReversePreview\n' + 'stream.index: $index\n' + 'selectedAssets.length: ${selectedAssets?.length}\n' + 'previewAssets.length: ${previewAssets.length}\n' + 'currentIndex: $currentIndex\n' + 'maxAssets: $maxAssets', ); - }, - ), + } + final asset = previewAssets.elementAt(assetIndex); + return Selector>( + selector: (_, p) => p.currentlySelectedAssets, + builder: (context, assets, _) { + final bool isSelected = assets.contains(asset); + return Semantics( + selected: isSelected, + label: semanticsTextDelegate.select, + onTap: () { + onChangingSelected(context, asset, isSelected); + }, + onTapHint: semanticsTextDelegate.select, + excludeSemantics: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (isAppleOS(context)) + _appleOSSelectButton(context, isSelected, asset) + else + _androidSelectButton(context, isSelected, asset), + if (!isAppleOS(context)) + ScaleText( + textDelegate.select, + style: const TextStyle(fontSize: 17, height: 1.2), + semanticsLabel: semanticsTextDelegate.select, + ), + ], + ), + ); + }, + ); + }, ); } @@ -991,7 +980,7 @@ class DefaultAssetPickerViewerBuilderDelegate : const CustomBouncingScrollPhysics(), controller: pageController, itemCount: previewAssets.length, - itemBuilder: assetPageBuilder, + itemBuilder: assetSemanticsBuilder, onPageChanged: (int index) { currentIndex = index; pageStreamController.add(index); @@ -1002,32 +991,38 @@ class DefaultAssetPickerViewerBuilderDelegate @override Widget build(BuildContext context) { - return Theme( - data: themeData, - child: AnnotatedRegion( - value: themeData.appBarTheme.systemOverlayStyle ?? - (themeData.effectiveBrightness.isDark - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark), - child: Scaffold( - resizeToAvoidBottomInset: false, - body: Stack( - children: [ - Positioned.fill(child: _pageViewBuilder(context)), - if (isWeChatMoment && hasVideo) ...[ - momentVideoBackButton(context), - PositionedDirectional( - end: 16, - bottom: context.bottomPadding + 16, - child: confirmButton(context), - ), - ] else ...[ - appBar(context), - if (selectedAssets != null || - (isWeChatMoment && hasVideo && isAppleOS(context))) - bottomDetailBuilder(context), - ], - ], + return CNP.value( + value: provider, + child: CNP.value( + value: selectorProvider, + builder: (context, _) => Theme( + data: themeData, + child: AnnotatedRegion( + value: themeData.appBarTheme.systemOverlayStyle ?? + (themeData.effectiveBrightness.isDark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark), + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + Positioned.fill(child: _pageViewBuilder(context)), + if (isWeChatMoment && hasVideo) ...[ + momentVideoBackButton(context), + PositionedDirectional( + end: 16, + bottom: context.bottomPadding + 16, + child: confirmButton(context), + ), + ] else ...[ + appBar(context), + if (selectedAssets != null || + (isWeChatMoment && hasVideo && isAppleOS(context))) + bottomDetailBuilder(context), + ], + ], + ), + ), ), ), ), diff --git a/lib/src/provider/asset_picker_viewer_provider.dart b/lib/src/provider/asset_picker_viewer_provider.dart index c8b3c177..c070bfb2 100644 --- a/lib/src/provider/asset_picker_viewer_provider.dart +++ b/lib/src/provider/asset_picker_viewer_provider.dart @@ -8,14 +8,14 @@ import '../constants/constants.dart'; /// [ChangeNotifier] for assets picker viewer. /// 资源选择查看器的 provider model. -class AssetPickerViewerProvider extends ChangeNotifier { - /// Copy selected assets for editing when constructing. - /// 构造时深拷贝已选择的资源集合,用于后续编辑。 +class AssetPickerViewerProvider extends ChangeNotifier { AssetPickerViewerProvider( - List? assets, { + List? assets, { this.maxAssets = defaultMaxAssetsCount, }) : assert(maxAssets > 0, 'maxAssets must be greater than 0.') { - _currentlySelectedAssets = (assets ?? []).toList(); + // Copy selected assets for editing when constructing. + // 构造时深拷贝已选择的资源集合,用于后续编辑。 + _currentlySelectedAssets = (assets ?? []).toList(); } /// Maximum count for asset selection. @@ -24,11 +24,11 @@ class AssetPickerViewerProvider extends ChangeNotifier { /// Selected assets in the viewer. /// 查看器中已选择的资源 - late List _currentlySelectedAssets; + late List _currentlySelectedAssets; - List get currentlySelectedAssets => _currentlySelectedAssets; + List get currentlySelectedAssets => _currentlySelectedAssets; - set currentlySelectedAssets(List value) { + set currentlySelectedAssets(List value) { if (value == _currentlySelectedAssets) { return; } @@ -41,33 +41,23 @@ class AssetPickerViewerProvider extends ChangeNotifier { /// Select asset. /// 选中资源 - void selectAsset(A item) { + void selectAsset(Asset item) { if (currentlySelectedAssets.length == maxAssets || currentlySelectedAssets.contains(item)) { return; } - final List newList = _currentlySelectedAssets.toList()..add(item); + final List newList = _currentlySelectedAssets.toList()..add(item); currentlySelectedAssets = newList; } /// Un-select asset. /// 取消选中资源 - void unSelectAsset(A item) { + void unSelectAsset(Asset item) { if (currentlySelectedAssets.isEmpty || !currentlySelectedAssets.contains(item)) { return; } - final List newList = _currentlySelectedAssets.toList()..remove(item); + final List newList = _currentlySelectedAssets.toList()..remove(item); currentlySelectedAssets = newList; } - - @Deprecated('Use selectAsset instead. This will be removed in 10.0.0') - void selectAssetEntity(A entity) { - selectAsset(entity); - } - - @Deprecated('Use unSelectAsset instead. This will be removed in 10.0.0') - void unselectAssetEntity(A entity) { - unSelectAsset(entity); - } } diff --git a/lib/src/widget/asset_picker.dart b/lib/src/widget/asset_picker.dart index 3db19a60..e5176b17 100644 --- a/lib/src/widget/asset_picker.dart +++ b/lib/src/widget/asset_picker.dart @@ -18,7 +18,9 @@ import 'asset_picker_page_route.dart'; AssetPickerDelegate _pickerDelegate = const AssetPickerDelegate(); -class AssetPicker extends StatefulWidget { +class AssetPicker> + extends StatefulWidget { const AssetPicker({ super.key, required this.permissionRequestOption, @@ -26,7 +28,7 @@ class AssetPicker extends StatefulWidget { }); final PermissionRequestOption permissionRequestOption; - final AssetPickerBuilderDelegate builder; + final Delegate builder; /// Provide another [AssetPickerDelegate] which override with /// custom methods during handling the picking, @@ -68,10 +70,13 @@ class AssetPicker extends StatefulWidget { } /// {@macro wechat_assets_picker.delegates.AssetPickerDelegate.pickAssetsWithDelegate} - static Future?> pickAssetsWithDelegate>( + static Future?> pickAssetsWithDelegate< + Asset, + Path, + PickerProvider extends AssetPickerProvider, + Delegate extends AssetPickerBuilderDelegate>( BuildContext context, { - required AssetPickerBuilderDelegate delegate, + required Delegate delegate, PermissionRequestOption permissionRequestOption = const PermissionRequestOption(), Key? key, @@ -79,7 +84,8 @@ class AssetPicker extends StatefulWidget { AssetPickerPageRouteBuilder>? pageRouteBuilder, bool useRootNavigator = true, }) { - return _pickerDelegate.pickAssetsWithDelegate( + return _pickerDelegate + .pickAssetsWithDelegate( context, key: key, delegate: delegate, @@ -106,11 +112,13 @@ class AssetPicker extends StatefulWidget { } @override - AssetPickerState createState() => - AssetPickerState(); + AssetPickerState createState() => + AssetPickerState(); } -class AssetPickerState extends State> +class AssetPickerState> + extends State> with TickerProviderStateMixin, WidgetsBindingObserver { Completer? permissionStateLock; diff --git a/lib/src/widget/asset_picker_viewer.dart b/lib/src/widget/asset_picker_viewer.dart index b01744f7..6034b2bd 100644 --- a/lib/src/widget/asset_picker_viewer.dart +++ b/lib/src/widget/asset_picker_viewer.dart @@ -16,34 +16,38 @@ import '../provider/asset_picker_viewer_provider.dart'; import 'asset_picker.dart'; import 'asset_picker_page_route.dart'; -class AssetPickerViewer extends StatefulWidget { +class AssetPickerViewer< + Asset, + Path, + Provider extends AssetPickerViewerProvider, + Delegate extends AssetPickerViewerBuilderDelegate> extends StatefulWidget { const AssetPickerViewer({ super.key, required this.builder, }); - final AssetPickerViewerBuilderDelegate builder; + final Delegate builder; @override - AssetPickerViewerState createState() => - AssetPickerViewerState(); + AssetPickerViewerState createState() => + AssetPickerViewerState(); /// Static method to push with the navigator. /// 跳转至选择预览的静态方法 - static Future?> pushToViewer( + static Future?> + pushToViewer

( BuildContext context, { int currentIndex = 0, required List previewAssets, required ThemeData themeData, - DefaultAssetPickerProvider? selectorProvider, + P? selectorProvider, ThumbnailSize? previewThumbnailSize, List? selectedAssets, SpecialPickerType? specialPickerType, int? maxAssets, bool shouldReversePreview = false, AssetSelectPredicate? selectPredicate, - PermissionRequestOption permissionRequestOption = - const PermissionRequestOption(), bool shouldAutoplayPreview = false, bool useRootNavigator = false, RouteSettings? pageRouteSettings, @@ -52,9 +56,13 @@ class AssetPickerViewer extends StatefulWidget { if (previewAssets.isEmpty) { throw StateError('Previewing empty assets is not allowed.'); } - await AssetPicker.permissionCheck(requestOption: permissionRequestOption); - final Widget viewer = AssetPickerViewer( - builder: DefaultAssetPickerViewerBuilderDelegate( + final viewer = AssetPickerViewer< + AssetEntity, + AssetPathEntity, + AssetPickerViewerProvider, + DefaultAssetPickerViewerBuilderDelegate>( + builder: DefaultAssetPickerViewerBuilderDelegate< + AssetPickerViewerProvider, P>( currentIndex: currentIndex, previewAssets: previewAssets, provider: selectedAssets != null @@ -76,7 +84,7 @@ class AssetPickerViewer extends StatefulWidget { shouldAutoplayPreview: shouldAutoplayPreview, ), ); - final List? result = await Navigator.maybeOf( + final result = await Navigator.maybeOf( context, rootNavigator: useRootNavigator, )?.push>( @@ -88,41 +96,51 @@ class AssetPickerViewer extends StatefulWidget { /// Call the viewer with provided delegate and provider. /// 通过指定的 [delegate] 调用查看器 - static Future?> pushToViewerWithDelegate( + static Future?> pushToViewerWithDelegate< + Asset, + Path, + Provider extends AssetPickerViewerProvider, + Delegate extends AssetPickerViewerBuilderDelegate>( BuildContext context, { - required AssetPickerViewerBuilderDelegate delegate, + required Delegate delegate, PermissionRequestOption permissionRequestOption = const PermissionRequestOption(), bool useRootNavigator = false, RouteSettings? pageRouteSettings, - AssetPickerViewerPageRouteBuilder>? pageRouteBuilder, + AssetPickerViewerPageRouteBuilder>? pageRouteBuilder, }) async { await AssetPicker.permissionCheck(requestOption: permissionRequestOption); - final Widget viewer = AssetPickerViewer(builder: delegate); - final List? result = await Navigator.maybeOf( - context, - rootNavigator: useRootNavigator, - )?.push>( - pageRouteBuilder?.call(viewer) ?? - AssetPickerViewerPageRoute(builder: (context) => viewer), + final viewer = AssetPickerViewer( + builder: delegate, ); + final pageRoute = pageRouteBuilder?.call(viewer) ?? + AssetPickerViewerPageRoute(builder: (context) => viewer); + final result = + await Navigator.maybeOf(context)?.push>(pageRoute); return result; } } -class AssetPickerViewerState - extends State> +class AssetPickerViewerState< + Asset, + Path, + Provider extends AssetPickerViewerProvider, + Delegate extends AssetPickerViewerBuilderDelegate> + extends State> with TickerProviderStateMixin { - AssetPickerViewerBuilderDelegate get builder => widget.builder; + Delegate get builder => widget.builder; @override void initState() { super.initState(); - builder.initStateAndTicker(this, this); + builder.initState(this); } @override - void didUpdateWidget(covariant AssetPickerViewer oldWidget) { + void didUpdateWidget( + covariant AssetPickerViewer oldWidget, + ) { super.didUpdateWidget(oldWidget); builder.didUpdateViewer(this, oldWidget, widget); } diff --git a/lib/src/widget/builder/image_page_builder.dart b/lib/src/widget/builder/image_page_builder.dart index 0ef0f793..d596d892 100644 --- a/lib/src/widget/builder/image_page_builder.dart +++ b/lib/src/widget/builder/image_page_builder.dart @@ -16,6 +16,7 @@ import 'package:wechat_picker_library/wechat_picker_library.dart'; import '../../constants/constants.dart'; import '../../delegates/asset_picker_text_delegate.dart'; import '../../delegates/asset_picker_viewer_builder_delegate.dart'; +import '../../provider/asset_picker_viewer_provider.dart'; class ImagePageBuilder extends StatefulWidget { const ImagePageBuilder({ @@ -30,7 +31,8 @@ class ImagePageBuilder extends StatefulWidget { /// 展示的资源 final AssetEntity asset; - final AssetPickerViewerBuilderDelegate delegate; + final AssetPickerViewerBuilderDelegate> delegate; final ThumbnailSize? previewThumbnailSize; @@ -118,6 +120,7 @@ class _ImagePageBuilderState extends State { ); if (_isLivePhoto && _livePhotoVideoController != null) { return _LivePhotoWidget( + asset: asset, controller: _livePhotoVideoController!, fit: BoxFit.contain, state: state, @@ -159,12 +162,14 @@ class _ImagePageBuilderState extends State { class _LivePhotoWidget extends StatefulWidget { const _LivePhotoWidget({ + required this.asset, required this.controller, required this.state, required this.fit, required this.textDelegate, }); + final AssetEntity asset; final VideoPlayerController controller; final ExtendedImageState state; final BoxFit fit; diff --git a/lib/src/widget/builder/video_page_builder.dart b/lib/src/widget/builder/video_page_builder.dart index 271a6614..69aac327 100644 --- a/lib/src/widget/builder/video_page_builder.dart +++ b/lib/src/widget/builder/video_page_builder.dart @@ -12,6 +12,7 @@ import 'package:wechat_picker_library/wechat_picker_library.dart'; import '../../constants/constants.dart'; import '../../delegates/asset_picker_viewer_builder_delegate.dart'; import '../../internals/singleton.dart'; +import '../../provider/asset_picker_viewer_provider.dart'; class VideoPageBuilder extends StatefulWidget { const VideoPageBuilder({ @@ -26,7 +27,8 @@ class VideoPageBuilder extends StatefulWidget { /// 展示的资源 final AssetEntity asset; - final AssetPickerViewerBuilderDelegate delegate; + final AssetPickerViewerBuilderDelegate> delegate; /// Only previewing one video and with the [SpecialPickerType.wechatMoment]. /// 是否处于 [SpecialPickerType.wechatMoment] 且只有一个视频 diff --git a/test/providers_test.dart b/test/providers_test.dart index 33b0eef1..b1228386 100644 --- a/test/providers_test.dart +++ b/test/providers_test.dart @@ -23,15 +23,15 @@ void main() async { ); await tester.tap(defaultButtonFinder); await tester.pumpAndSettle(); - final Finder pickerFinder = find.byType( - AssetPicker, - ); - final AssetPicker picker = tester.widget( - pickerFinder, + final picker = tester.widget< + AssetPicker>( + find.byType( + AssetPicker, + ), ); - final DefaultAssetPickerProvider provider = - (picker.builder as DefaultAssetPickerBuilderDelegate).provider; - expect(provider, isA()); + final provider = picker.builder.provider; await tester.tap(find.widgetWithIcon(IconButton, Icons.close)); await tester.pumpAndSettle(); expect( diff --git a/test/test_utils.dart b/test/test_utils.dart index 2f26d17d..d83611f0 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -120,7 +120,8 @@ class TestAssetPickerDelegate extends AssetPickerDelegate { ) ..hasAssetsToDisplay = true ..totalAssetsCount = 1; - final Widget picker = AssetPicker( + final picker = AssetPicker( key: key, permissionRequestOption: permissionRequestOption, builder: DefaultAssetPickerBuilderDelegate(