From 41e2b08bccec8e1f15c2d358ef2ad69e199067ae Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 25 Nov 2024 22:41:15 +0800 Subject: [PATCH] :sparkles: Better attachment list --- lib/widgets/attachment/attachment_detail.dart | 84 ++++++-- lib/widgets/attachment/attachment_item.dart | 33 +-- lib/widgets/attachment/attachment_list.dart | 191 +++++++++++------- lib/widgets/post/post_item.dart | 2 +- lib/widgets/post/post_media_pending_list.dart | 2 +- 5 files changed, 203 insertions(+), 109 deletions(-) diff --git a/lib/widgets/attachment/attachment_detail.dart b/lib/widgets/attachment/attachment_detail.dart index 24bf2d5..6f036fe 100644 --- a/lib/widgets/attachment/attachment_detail.dart +++ b/lib/widgets/attachment/attachment_detail.dart @@ -1,16 +1,37 @@ import 'package:dismissible_page/dismissible_page.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; import 'package:provider/provider.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; import 'package:surface/widgets/universal_image.dart'; import 'package:uuid/uuid.dart'; -class AttachmentDetailPopup extends StatelessWidget { - final SnAttachment data; - final String? heroTag; - const AttachmentDetailPopup({super.key, required this.data, this.heroTag}); +class AttachmentZoomView extends StatefulWidget { + final Iterable data; + final int? initialIndex; + final List? heroTags; + const AttachmentZoomView({ + super.key, + required this.data, + this.initialIndex, + this.heroTags, + }); + + @override + State createState() => _AttachmentZoomViewState(); +} + +class _AttachmentZoomViewState extends State { + late final PageController _pageController = + PageController(initialPage: widget.initialIndex ?? 0); + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -24,18 +45,51 @@ class AttachmentDetailPopup extends StatelessWidget { direction: DismissiblePageDismissDirection.down, backgroundColor: Colors.transparent, isFullScreen: true, - child: Hero( - tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}', - child: PhotoView( - key: Key('attachment-detail-${data.rid}-$heroTag'), - backgroundDecoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), + child: Builder(builder: (context) { + if (widget.data.length == 1) { + final heroTag = widget.heroTags?.first ?? uuid.v4(); + return Hero( + tag: 'attachment-${widget.data.first.rid}-$heroTag', + child: PhotoView( + key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), + backgroundDecoration: BoxDecoration(color: Colors.transparent), + imageProvider: UniversalImage.provider( + sn.getAttachmentUrl(widget.data.first.rid), + ), + ), + ); + } + + return PhotoViewGallery.builder( + pageController: _pageController, + scrollPhysics: const BouncingScrollPhysics(), + builder: (context, idx) { + final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4(); + return PhotoViewGalleryPageOptions( + imageProvider: UniversalImage.provider( + sn.getAttachmentUrl(widget.data.elementAt(idx).rid), + ), + heroAttributes: PhotoViewHeroAttributes( + tag: 'attachment-${widget.data.first.rid}-$heroTag', + ), + ); + }, + itemCount: widget.data.length, + loadingBuilder: (context, event) => Center( + child: SizedBox( + width: 20.0, + height: 20.0, + child: CircularProgressIndicator( + value: event == null + ? 0 + : event.cumulativeBytesLoaded / + (event.expectedTotalBytes ?? 1), + ), + ), ), - imageProvider: UniversalImage.provider( - sn.getAttachmentUrl(data.rid), - ), - ), - ), + backgroundDecoration: BoxDecoration(color: Colors.transparent), + ); + }), ); } } diff --git a/lib/widgets/attachment/attachment_item.dart b/lib/widgets/attachment/attachment_item.dart index e412220..ef7cd54 100644 --- a/lib/widgets/attachment/attachment_item.dart +++ b/lib/widgets/attachment/attachment_item.dart @@ -1,7 +1,6 @@ import 'dart:ui'; import 'dart:math' as math; -import 'package:dismissible_page/dismissible_page.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; @@ -13,20 +12,21 @@ import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; -import 'package:surface/widgets/attachment/attachment_detail.dart'; import 'package:surface/widgets/universal_image.dart'; import 'package:uuid/uuid.dart'; class AttachmentItem extends StatelessWidget { final SnAttachment? data; - final bool isExpandable; + final String? heroTag; const AttachmentItem({ super.key, required this.data, - this.isExpandable = false, + required this.heroTag, }); - Widget _buildContent(BuildContext context, String heroTag) { + Widget _buildContent(BuildContext context) { + final tag = heroTag ?? Uuid().v4(); + if (data == null) { return const Icon(Symbols.cancel).center(); } @@ -36,10 +36,10 @@ class AttachmentItem extends StatelessWidget { switch (tp) { case 'image': return Hero( - tag: 'attachment-${data!.rid}-$heroTag', + tag: 'attachment-${data!.rid}-$tag', child: AutoResizeUniversalImage( sn.getAttachmentUrl(data!.rid), - key: Key('attachment-${data!.rid}-$heroTag'), + key: Key('attachment-${data!.rid}-$tag'), fit: BoxFit.cover, ), ); @@ -60,28 +60,13 @@ class AttachmentItem extends StatelessWidget { @override Widget build(BuildContext context) { - final uuid = Uuid(); - final heroTag = uuid.v4(); - if (data!.isMature) { return _AttachmentItemSensitiveBlur( - child: _buildContent(context, heroTag), + child: _buildContent(context), ); } - if (isExpandable) { - return GestureDetector( - child: _buildContent(context, heroTag), - onTap: () { - context.pushTransparentRoute( - AttachmentDetailPopup(data: data!, heroTag: heroTag), - rootNavigator: true, - ); - }, - ); - } - - return _buildContent(context, heroTag); + return _buildContent(context); } } diff --git a/lib/widgets/attachment/attachment_list.dart b/lib/widgets/attachment/attachment_list.dart index 4f32525..bafbc47 100644 --- a/lib/widgets/attachment/attachment_list.dart +++ b/lib/widgets/attachment/attachment_list.dart @@ -1,11 +1,14 @@ +import 'package:dismissible_page/dismissible_page.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:surface/types/attachment.dart'; +import 'package:surface/widgets/attachment/attachment_detail.dart'; import 'package:surface/widgets/attachment/attachment_item.dart'; +import 'package:uuid/uuid.dart'; -class AttachmentList extends StatelessWidget { +class AttachmentList extends StatefulWidget { final List data; final bool bordered; final bool noGrow; @@ -23,96 +26,148 @@ class AttachmentList extends StatelessWidget { 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) { - final borderSide = bordered + 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: 80, - maxHeight: maxHeight ?? double.infinity, + maxHeight: widget.maxHeight ?? double.infinity, ); - if (data.isEmpty) return const SizedBox.shrink(); - if (data.length == 1) { - if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || noGrow) { - return Padding( - // Single child list-like displaying - padding: listPadding ?? EdgeInsets.zero, - child: Container( - constraints: constraints, - decoration: BoxDecoration( - color: backgroundColor, - border: Border(top: borderSide, bottom: borderSide), - borderRadius: kDefaultRadius, - ), - child: AspectRatio( - aspectRatio: data[0]?.metadata['ratio']?.toDouble() ?? - switch (data[0]?.mimetype.split('/').firstOrNull) { - 'audio' => 16 / 9, - 'video' => 16 / 9, - _ => 1, - }, - child: ClipRRect( - borderRadius: kDefaultRadius, - child: AttachmentItem(data: data[0], isExpandable: true), - ), - ), - ), - ); - } - - return Container( - decoration: BoxDecoration( - color: backgroundColor, - border: Border(top: borderSide, bottom: borderSide), - ), - child: AspectRatio( - aspectRatio: data[0]?.metadata['ratio']?.toDouble() ?? 1, - child: AttachmentItem(data: data[0], isExpandable: true), - ), - ); - } - - return Container( - constraints: BoxConstraints(maxHeight: maxHeight ?? 320), - child: ScrollConfiguration( - behavior: _AttachmentListScrollBehavior(), - child: ListView.separated( - shrinkWrap: true, - itemCount: data.length, - itemBuilder: (context, idx) { - return Stack( - children: [ - Container( + if (widget.data.isEmpty) return const SizedBox.shrink(); + if (widget.data.length == 1) { + return GestureDetector( + child: Builder( + builder: (context) { + if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || + widget.noGrow) { + return Padding( + // Single child list-like displaying + padding: widget.listPadding ?? EdgeInsets.zero, + child: Container( constraints: constraints, decoration: BoxDecoration( color: backgroundColor, border: Border(top: borderSide, bottom: borderSide), - borderRadius: kDefaultRadius, + borderRadius: AttachmentList.kDefaultRadius, ), child: AspectRatio( - aspectRatio: data[idx]?.metadata['ratio']?.toDouble() ?? 1, + aspectRatio: widget.data[0]?.metadata['ratio'] + ?.toDouble() ?? + switch ( + widget.data[0]?.mimetype.split('/').firstOrNull) { + 'audio' => 16 / 9, + 'video' => 16 / 9, + _ => 1, + }, child: ClipRRect( - borderRadius: kDefaultRadius, - child: - AttachmentItem(data: data[idx], isExpandable: true), + borderRadius: AttachmentList.kDefaultRadius, + child: AttachmentItem( + data: widget.data[0], + heroTag: heroTags[0], + ), ), ), ), - Positioned( - right: 12, - bottom: 12, - child: Chip( - label: Text('${idx + 1}/${data.length}'), - ), + ); + } + + return Container( + decoration: BoxDecoration( + color: backgroundColor, + border: Border(top: borderSide, bottom: borderSide), + ), + child: AspectRatio( + aspectRatio: widget.data[0]?.metadata['ratio']?.toDouble() ?? 1, + child: AttachmentItem( + data: widget.data[0], + heroTag: heroTags.first, ), - ], + ), + ); + }, + ), + onTap: () { + context.pushTransparentRoute( + AttachmentZoomView( + data: widget.data.where((ele) => ele != null).cast(), + initialIndex: 0, + heroTags: heroTags, + ), + backgroundColor: Colors.black.withOpacity(0.7), + rootNavigator: true, + ); + }, + ); + } + + return Container( + constraints: BoxConstraints(maxHeight: widget.maxHeight ?? 320), + child: ScrollConfiguration( + behavior: _AttachmentListScrollBehavior(), + child: ListView.separated( + shrinkWrap: true, + itemCount: widget.data.length, + itemBuilder: (context, idx) { + return GestureDetector( + onTap: () { + context.pushTransparentRoute( + AttachmentZoomView( + data: widget.data.where((ele) => ele != null).cast(), + initialIndex: idx, + heroTags: heroTags, + ), + backgroundColor: Colors.black.withOpacity(0.7), + rootNavigator: true, + ); + }, + child: Stack( + children: [ + Container( + constraints: constraints, + decoration: BoxDecoration( + color: backgroundColor, + border: Border(top: borderSide, bottom: borderSide), + borderRadius: AttachmentList.kDefaultRadius, + ), + child: AspectRatio( + aspectRatio: + widget.data[idx]?.metadata['ratio']?.toDouble() ?? 1, + child: ClipRRect( + borderRadius: AttachmentList.kDefaultRadius, + child: AttachmentItem( + data: widget.data[idx], + heroTag: heroTags[idx], + ), + ), + ), + ), + Positioned( + right: 12, + bottom: 12, + child: Chip( + label: Text('${idx + 1}/${widget.data.length}'), + ), + ), + ], + ), ); }, separatorBuilder: (context, index) => const Gap(8), - padding: listPadding, + padding: widget.listPadding, physics: const BouncingScrollPhysics(), scrollDirection: Axis.horizontal, ), diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 19f45db..02b1686 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -66,7 +66,7 @@ class PostItem extends StatelessWidget { AttachmentList( data: data.preload!.attachments!, bordered: true, - maxHeight: 520, + maxHeight: 480, listPadding: const EdgeInsets.symmetric(horizontal: 12), ), Container( diff --git a/lib/widgets/post/post_media_pending_list.dart b/lib/widgets/post/post_media_pending_list.dart index fe35510..3ff77df 100644 --- a/lib/widgets/post/post_media_pending_list.dart +++ b/lib/widgets/post/post_media_pending_list.dart @@ -92,7 +92,7 @@ class PostMediaPendingList extends StatelessWidget { icon: Symbols.preview, onSelected: () { context.pushTransparentRoute( - AttachmentDetailPopup(data: media.attachment!), + AttachmentZoomView(data: [media.attachment!]), rootNavigator: true, ); },