import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:dismissible_page/dismissible_page.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:gap/gap.dart'; import 'package:surface/types/attachment.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:uuid/uuid.dart'; class AttachmentList extends StatefulWidget { final List data; final bool bordered; final bool gridded; final bool noGrow; final BoxFit fit; final double? maxHeight; final double? minWidth; final EdgeInsets? padding; const AttachmentList({ super.key, required this.data, this.bordered = false, this.gridded = false, this.noGrow = false, this.fit = BoxFit.cover, this.maxHeight, this.minWidth, this.padding, }); static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8)); @override State createState() => _AttachmentListState(); } class _AttachmentListState extends State { late final List heroTags = List.generate( widget.data.length, (_) => const Uuid().v4(), ); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, layoutConstraints) { final borderSide = widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none; final backgroundColor = Theme.of(context).colorScheme.surfaceContainer; final constraints = BoxConstraints( minWidth: widget.minWidth ?? 80, maxHeight: widget.maxHeight ?? MediaQuery.of(context).size.height, ); if (widget.data.isEmpty) return const SizedBox.shrink(); if (widget.data.length == 1) { final singleAspectRatio = widget.data[0]?.data['ratio']?.toDouble() ?? switch (widget.data[0]?.mimetype.split('/').firstOrNull) { 'audio' => 16 / 9, 'video' => 16 / 9, _ => 1, } .toDouble(); return Container( padding: widget.padding ?? EdgeInsets.zero, constraints: constraints, child: GestureDetector( child: AspectRatio( aspectRatio: singleAspectRatio, child: Container( decoration: BoxDecoration( color: backgroundColor, border: Border.fromBorderSide(borderSide), borderRadius: AttachmentList.kDefaultRadius, ), child: ClipRRect( borderRadius: AttachmentList.kDefaultRadius, child: AttachmentItem( data: widget.data[0], heroTag: heroTags[0], fit: widget.fit, ), ), ), ), onTap: () { if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return; context.pushTransparentRoute( AttachmentZoomView( data: widget.data.where((ele) => ele != null).cast(), initialIndex: 0, heroTags: heroTags, ), backgroundColor: Colors.black.withOpacity(0.7), rootNavigator: true, ); }, ), ); } if (widget.gridded) { return Padding( padding: widget.padding ?? EdgeInsets.zero, child: Container( decoration: BoxDecoration( color: backgroundColor, border: Border( top: borderSide, bottom: borderSide, ), borderRadius: AttachmentList.kDefaultRadius, ), child: ClipRRect( borderRadius: AttachmentList.kDefaultRadius, child: StaggeredGrid.count( crossAxisCount: math.min(widget.data.length, 2), crossAxisSpacing: 4, mainAxisSpacing: 4, children: widget.data .mapIndexed( (idx, ele) => GestureDetector( child: Container( constraints: constraints, child: AttachmentItem( data: ele, heroTag: heroTags[idx], fit: BoxFit.cover, ), ), onTap: () { if (widget.data[idx]!.mediaType != SnMediaType.image) return; context.pushTransparentRoute( AttachmentZoomView( data: widget.data.where((ele) => ele != null).cast(), initialIndex: idx, heroTags: heroTags, ), backgroundColor: Colors.black.withOpacity(0.7), rootNavigator: true, ); }, ), ) .toList(), ), ), ), ); } return AspectRatio( aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(), child: Container( constraints: BoxConstraints(maxHeight: constraints.maxHeight), child: ScrollConfiguration( behavior: _AttachmentListScrollBehavior(), child: ListView.separated( shrinkWrap: true, itemCount: widget.data.length, itemBuilder: (context, idx) { return Container( constraints: constraints, child: AspectRatio( aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), child: GestureDetector( onTap: () { if (widget.data[idx]?.mediaType != SnMediaType.image) return; context.pushTransparentRoute( AttachmentZoomView( data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), initialIndex: idx, heroTags: heroTags, ), backgroundColor: Colors.black.withOpacity(0.7), rootNavigator: true, ); }, child: Stack( fit: StackFit.expand, children: [ Container( decoration: BoxDecoration( color: backgroundColor, border: Border( top: borderSide, bottom: borderSide, ), borderRadius: AttachmentList.kDefaultRadius, ), child: ClipRRect( borderRadius: AttachmentList.kDefaultRadius, child: AttachmentItem( data: widget.data[idx], heroTag: heroTags[idx], ), ), ), Positioned( right: 8, bottom: 8, child: Chip( label: Text('${idx + 1}/${widget.data.length}'), ), ), ], ), ), ), ); }, separatorBuilder: (context, index) => const Gap(8), padding: widget.padding, physics: const BouncingScrollPhysics(), scrollDirection: Axis.horizontal, ), ), ), ); }, ); } } class _AttachmentListScrollBehavior extends MaterialScrollBehavior { @override Set get dragDevices => { PointerDeviceKind.touch, PointerDeviceKind.mouse, }; }