Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ class DefaultTranslations implements Translations {
String get somethingWentWrongError => 'Something went wrong';

@override
String get addMoreFilesLabel => 'Add more files';
String get addMoreFilesLabel => 'Add more';

@override
String get enablePhotoAndVideoAccessMessage =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,6 @@ class _StreamGalleryPickerState extends State<StreamGalleryPicker> {

return OptionDrawer(
margin: .zero,
actions: [
if (isLimited)
IconButton(
color: colorScheme.accentPrimary,
icon: const Icon(Icons.add_circle_outline_rounded),
onPressed: () async {
await PhotoManager.presentLimited();
_controller.doInitialLoad();
},
),
],
child: Builder(
builder: (context) {
if (!isPermissionGranted) {
Expand Down Expand Up @@ -143,6 +132,14 @@ class _StreamGalleryPickerState extends State<StreamGalleryPicker> {
thumbnailFormat: widget.config.mediaThumbnailFormat,
thumbnailQuality: widget.config.mediaThumbnailQuality,
thumbnailScale: widget.config.mediaThumbnailScale,
addMoreBuilder: isLimited
? (context) => _AddMoreTile(
onTap: () async {
await PhotoManager.presentLimited();
_controller.doInitialLoad();
},
)
: null,
itemBuilder: (context, mediaItems, index, defaultWidget) {
final media = mediaItems[index];
return defaultWidget.copyWith(
Expand All @@ -158,6 +155,50 @@ class _StreamGalleryPickerState extends State<StreamGalleryPicker> {
}
}

class _AddMoreTile extends StatelessWidget {
const _AddMoreTile({required this.onTap});

final VoidCallback onTap;

@override
Widget build(BuildContext context) {
final colorScheme = context.streamColorScheme;
final textTheme = context.streamTextTheme;
final spacing = context.streamSpacing;

return Material(
color: colorScheme.backgroundSurfaceCard,
child: InkWell(
onTap: onTap,
overlayColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.pressed)) return colorScheme.statePressed;
if (states.contains(WidgetState.hovered)) return colorScheme.stateHover;
if (states.contains(WidgetState.focused)) return colorScheme.stateFocused;
return null;
}),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
context.streamIcons.plusLarge,
size: 20,
color: colorScheme.textTertiary,
),
SizedBox(height: spacing.xs),
Text(
context.translations.addMoreFilesLabel,
style: textTheme.captionEmphasis.copyWith(
color: colorScheme.textTertiary,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}

/// Configuration for the [StreamGalleryPicker].
class GalleryPickerConfig {
/// Creates a [GalleryPickerConfig] instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,102 +267,30 @@ class _EndOfFrameCallbackWidgetState extends State<EndOfFrameCallbackWidget> {
}
}

const _kDefaultOptionDrawerShape = RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
);

/// A widget that will be shown in the attachment picker.
/// It can be used to show a custom view for each attachment picker option.
class OptionDrawer extends StatelessWidget {
/// Creates a widget that will be shown in the attachment picker.
const OptionDrawer({
super.key,
required this.child,
this.color,
this.elevation = 2,
this.margin,
this.clipBehavior = Clip.hardEdge,
this.shape = _kDefaultOptionDrawerShape,
this.title,
this.actions = const [],
});

/// The widget below this widget in the tree.
final Widget child;

/// The background color of the options card.
///
/// Defaults to [StreamColorTheme.barsBg].
final Color? color;

/// The elevation of the options card.
///
/// The default value is 2.
final double elevation;

/// The margin of the options card.
final EdgeInsetsGeometry? margin;

/// The clip behavior of the options card.
///
/// The default value is [Clip.hardEdge].
final Clip clipBehavior;

/// The shape of the options card.
final ShapeBorder shape;

/// The title of the options card.
final Widget? title;

/// The actions available for the options card.
final List<Widget> actions;

@override
Widget build(BuildContext context) {
var height = 0.0;
if (title != null || actions.isNotEmpty) {
height = 40.0;
}

final leading = title ?? const Empty();

Widget trailing;
if (actions.isNotEmpty) {
trailing = Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: actions,
);
} else {
trailing = const Empty();
}

final spacing = context.streamSpacing;
final effectiveMargin = margin ?? .symmetric(horizontal: spacing.md, vertical: spacing.xxxl);

return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: height,
child: Row(
children: [
Expanded(child: leading),
Expanded(child: trailing),
],
),
),
Expanded(
child: Container(
margin: effectiveMargin,
child: child,
),
),
],
return Container(
margin: effectiveMargin,
child: child,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class StreamPhotoGallery extends StatelessWidget {
this.thumbnailFormat = ThumbnailFormat.jpeg,
this.thumbnailQuality = 100,
this.thumbnailScale = 1,
this.addMoreBuilder,
});

/// The [StreamPhotoGalleryController] used to control the grid of users.
Expand Down Expand Up @@ -307,6 +308,11 @@ class StreamPhotoGallery extends StatelessWidget {
/// Scale of the image.
final double thumbnailScale;

/// An optional builder for a leading "Add more" tile shown as the first item
/// in the gallery grid. Useful when the user has limited photo library access
/// and needs a way to expand the selection.
final WidgetBuilder? addMoreBuilder;

@override
Widget build(BuildContext context) {
return PagedValueGridView<int, AssetEntity>(
Expand All @@ -328,6 +334,7 @@ class StreamPhotoGallery extends StatelessWidget {
restorationId: restorationId,
clipBehavior: clipBehavior,
loadMoreTriggerIndex: loadMoreTriggerIndex,
leadingItemBuilder: addMoreBuilder,
gridDelegate: gridDelegate,
itemBuilder: (context, mediaList, index) {
final media = mediaList[index];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ class PagedValueGridView<K, V> extends StatefulWidget {
required this.loadingBuilder,
required this.errorBuilder,
this.loadMoreTriggerIndex = 3,
this.leadingItemBuilder,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
Expand Down Expand Up @@ -413,6 +414,13 @@ class PagedValueGridView<K, V> extends StatefulWidget {
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;

/// An optional builder for a single item prepended before the paged items.
///
/// When provided, [itemBuilder] still receives regular item indices starting
/// at 0 — the leading item is handled separately, similar to
/// [loadMoreIndicatorBuilder].
final WidgetBuilder? leadingItemBuilder;

/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
Expand Down Expand Up @@ -665,13 +673,18 @@ class _PagedValueGridViewState<K, V> extends State<PagedValueGridView<K, V>> {
keyboardDismissBehavior: widget.keyboardDismissBehavior,
restorationId: widget.restorationId,
clipBehavior: widget.clipBehavior,
itemCount: value.itemCount,
itemCount: value.itemCount + (widget.leadingItemBuilder != null ? 1 : 0),
gridDelegate: widget.gridDelegate,
itemBuilder: (context, index) {
var adjustedIndex = index;
if (widget.leadingItemBuilder != null) {
if (index == 0) return widget.leadingItemBuilder!(context);
adjustedIndex = index - 1;
}

if (!_hasRequestedNextPage) {
final newPageRequestTriggerIndex = items.length - widget.loadMoreTriggerIndex;
final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex;
if (nextPageKey != null && isBuildingTriggerIndexItem) {
if (nextPageKey != null && adjustedIndex == newPageRequestTriggerIndex) {
// Schedules the request for the end of this frame.
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (error == null) {
Expand All @@ -683,14 +696,14 @@ class _PagedValueGridViewState<K, V> extends State<PagedValueGridView<K, V>> {
}
}

if (index == items.length) {
if (adjustedIndex == items.length) {
if (error != null) {
return widget.loadMoreErrorBuilder(context, error);
}
return widget.loadMoreIndicatorBuilder(context);
}

return widget.itemBuilder(context, items, index);
return widget.itemBuilder(context, items, adjustedIndex);
},
);
},
Expand Down
Loading
Loading