From b0790ea145fae2b287fcbaaf685294875c2cdd1b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 1 Dec 2024 23:56:56 +0800 Subject: [PATCH] :recycle: Better attachment list & zoom view --- assets/translations/en.json | 4 +- assets/translations/zh.json | 4 +- lib/main.dart | 2 +- lib/providers/post.dart | 10 + lib/widgets/attachment/attachment_detail.dart | 265 +++++++++++++++--- lib/widgets/attachment/attachment_list.dart | 235 ++++++++-------- 6 files changed, 361 insertions(+), 159 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index b195ef9..5648b0b 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -351,5 +351,7 @@ "friendRequestAccept": "Accept", "friendRequestDecline": "Decline", "subscribe": "Subscribe", - "unsubscribe": "Unsubscribe" + "unsubscribe": "Unsubscribe", + "attachmentUploadBy": "Upload by", + "attachmentShotOn": "Shot on {}" } diff --git a/assets/translations/zh.json b/assets/translations/zh.json index f2345c7..2fdc0c4 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -351,5 +351,7 @@ "friendRequestAccept": "接受", "friendRequestDecline": "拒绝", "subscribe": "订阅", - "unsubscribe": "取消订阅" + "unsubscribe": "取消订阅", + "attachmentUploadBy": "上传者", + "attachmentShotOn": "由 {} 拍摄" } diff --git a/lib/main.dart b/lib/main.dart index 51714e2..b8e44e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -81,9 +81,9 @@ class SolianApp extends StatelessWidget { // Data layer Provider(create: (_) => SnNetworkProvider()), + Provider(create: (ctx) => UserDirectoryProvider(ctx)), Provider(create: (ctx) => SnAttachmentProvider(ctx)), Provider(create: (ctx) => SnPostContentProvider(ctx)), - Provider(create: (ctx) => UserDirectoryProvider(ctx)), Provider(create: (ctx) => SnRelationshipProvider(ctx)), ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)), ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)), diff --git a/lib/providers/post.dart b/lib/providers/post.dart index 78b239d..62316f0 100644 --- a/lib/providers/post.dart +++ b/lib/providers/post.dart @@ -2,14 +2,17 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/user_directory.dart'; import 'package:surface/types/post.dart'; class SnPostContentProvider { late final SnNetworkProvider _sn; + late final UserDirectoryProvider _ud; late final SnAttachmentProvider _attach; SnPostContentProvider(BuildContext context) { _sn = context.read(); + _ud = context.read(); _attach = context.read(); } @@ -37,6 +40,13 @@ class SnPostContentProvider { ); } + await _ud.listAccount( + attachments + .where((ele) => ele != null) + .map((ele) => ele!.accountId) + .toSet(), + ); + return out; } diff --git a/lib/widgets/attachment/attachment_detail.dart b/lib/widgets/attachment/attachment_detail.dart index 6f036fe..40f7b09 100644 --- a/lib/widgets/attachment/attachment_detail.dart +++ b/lib/widgets/attachment/attachment_detail.dart @@ -1,10 +1,16 @@ import 'package:dismissible_page/dismissible_page.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/user_directory.dart'; import 'package:surface/types/attachment.dart'; +import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/universal_image.dart'; import 'package:uuid/uuid.dart'; @@ -27,17 +33,37 @@ class _AttachmentZoomViewState extends State { late final PageController _pageController = PageController(initialPage: widget.initialIndex ?? 0); + void _updatePage() { + setState(() {}); + } + + @override + void initState() { + super.initState(); + _pageController.addListener(_updatePage); + } + @override void dispose() { + _pageController.removeListener(_updatePage); _pageController.dispose(); super.dispose(); } + Color get _unFocusColor => + Theme.of(context).colorScheme.onSurface.withOpacity(0.75); + @override Widget build(BuildContext context) { final sn = context.read(); final uuid = Uuid(); + final metaTextStyle = GoogleFonts.roboto( + fontSize: 12, + color: _unFocusColor, + height: 1, + ); + return DismissiblePage( onDismissed: () { Navigator.of(context).pop(); @@ -45,51 +71,212 @@ class _AttachmentZoomViewState extends State { direction: DismissiblePageDismissDirection.down, backgroundColor: Colors.transparent, isFullScreen: true, - 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( + child: Stack( + children: [ + 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), + ), + ), ), + backgroundDecoration: BoxDecoration(color: Colors.transparent), ); - }, - 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), + }), + Align( + alignment: Alignment.bottomCenter, + child: IgnorePointer( + child: Container( + height: 300, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Theme.of(context).colorScheme.surface, + Colors.transparent, + ], + ), + ), ), ), ), - backgroundDecoration: BoxDecoration(color: Colors.transparent), - ); - }), + Positioned( + left: 16, + right: 16, + bottom: MediaQuery.of(context).padding.bottom > 16 + ? -MediaQuery.of(context).padding.bottom + : 16, + child: SizedBox( + height: 180, + child: Material( + color: Colors.transparent, + child: Builder(builder: (context) { + final ud = context.read(); + final item = widget.data.elementAt( + _pageController.page?.round() ?? 0, + ); + final account = ud.getAccountFromCache(item.accountId); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (item.accountId > 0) + Row( + children: [ + IgnorePointer( + child: AccountImage( + content: account!.avatar, + radius: 19, + ), + ), + const Gap(8), + Expanded( + child: IgnorePointer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'attachmentUploadBy'.tr(), + style: + Theme.of(context).textTheme.bodySmall, + ), + Text( + account.nick, + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ], + ), + ), + ), + if (widget.data.length > 1) + IgnorePointer( + child: Text( + '${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}', + style: GoogleFonts.robotoMono(fontSize: 13), + ).padding(right: 8), + ), + ], + ), + const Gap(4), + IgnorePointer( + child: Text( + item.alt, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ), + const Gap(2), + IgnorePointer( + child: Wrap( + spacing: 6, + children: [ + if (item.metadata['exif'] == null) + Text( + '#${item.rid}', + style: metaTextStyle, + ), + if (item.metadata['exif']?['Model'] != null) + Text( + 'attachmentShotOn'.tr(args: [ + item.metadata['exif']?['Model'], + ]), + style: metaTextStyle, + ).padding(right: 2), + if (item.metadata['exif']?['ShutterSpeed'] != null) + Text( + item.metadata['exif']?['ShutterSpeed'], + style: metaTextStyle, + ).padding(right: 2), + if (item.metadata['exif']?['ISO'] != null) + Text( + 'ISO${item.metadata['exif']?['ISO']}', + style: metaTextStyle, + ).padding(right: 2), + if (item.metadata['exif']?['Aperture'] != null) + Text( + 'f/${item.metadata['exif']?['Aperture']}', + style: metaTextStyle, + ).padding(right: 2), + if (item.metadata['exif']?['Megapixels'] != null && + item.metadata['exif']?['Model'] != null) + Text( + '${item.metadata['exif']?['Megapixels']}MP', + style: metaTextStyle, + ) + else + Text( + '${item.size} Bytes', + style: metaTextStyle, + ), + Text( + '${item.metadata['width']}x${item.metadata['height']}', + style: metaTextStyle, + ), + if (item.metadata['ratio'] != null) + Text( + (item.metadata['ratio'] as num) + .toStringAsFixed(2), + style: metaTextStyle, + ), + Text( + item.mimetype, + style: metaTextStyle, + ), + ], + ), + ), + ], + ); + }), + ), + ), + ), + ], + ), ); } } diff --git a/lib/widgets/attachment/attachment_list.dart b/lib/widgets/attachment/attachment_list.dart index bafbc47..7fe10d0 100644 --- a/lib/widgets/attachment/attachment_list.dart +++ b/lib/widgets/attachment/attachment_list.dart @@ -38,140 +38,141 @@ class _AttachmentListState extends State { @override Widget build(BuildContext context) { - 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: widget.maxHeight ?? double.infinity, - ); + final aspectRatio = widget.data[0]?.metadata['ratio']?.toDouble() ?? 1; - 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: AttachmentList.kDefaultRadius, - ), - child: AspectRatio( - aspectRatio: widget.data[0]?.metadata['ratio'] - ?.toDouble() ?? - switch ( - widget.data[0]?.mimetype.split('/').firstOrNull) { - 'audio' => 16 / 9, - 'video' => 16 / 9, - _ => 1, - }, - child: ClipRRect( - borderRadius: AttachmentList.kDefaultRadius, - child: AttachmentItem( - data: widget.data[0], - heroTag: heroTags[0], + 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: 80, + maxWidth: layoutConstraints.maxWidth - 20, + maxHeight: widget.maxHeight ?? double.infinity, + ); + + if (widget.data.isEmpty) return const SizedBox.shrink(); + if (widget.data.length == 1) { + return AspectRatio( + aspectRatio: widget.data[0]?.metadata['ratio']?.toDouble() ?? + switch (widget.data[0]?.mimetype.split('/').firstOrNull) { + 'audio' => 16 / 9, + 'video' => 16 / 9, + _ => 1, + }, + child: 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: AttachmentList.kDefaultRadius, + ), + child: ClipRRect( + borderRadius: AttachmentList.kDefaultRadius, + child: AttachmentItem( + data: widget.data[0], + heroTag: heroTags[0], + ), + ), ), + ); + } + + return Container( + decoration: BoxDecoration( + color: backgroundColor, + border: Border(top: borderSide, bottom: borderSide), ), - ), - ), - ); - } - - return Container( - decoration: BoxDecoration( - color: backgroundColor, - border: Border(top: borderSide, bottom: borderSide), + child: AttachmentItem( + data: widget.data[0], + heroTag: heroTags.first, + ), + ); + }, ), - 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, + initialIndex: 0, 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], + ), + ); + } + + return AspectRatio( + aspectRatio: aspectRatio, + child: 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: ClipRRect( + borderRadius: AttachmentList.kDefaultRadius, + child: AttachmentItem( + data: widget.data[idx], + heroTag: heroTags[idx], + ), + ), + ), + Positioned( + right: 8, + bottom: 12, + child: Chip( + label: Text('${idx + 1}/${widget.data.length}'), + ), + ), + ], ), - ), - Positioned( - right: 12, - bottom: 12, - child: Chip( - label: Text('${idx + 1}/${widget.data.length}'), - ), - ), - ], + ); + }, + separatorBuilder: (context, index) => const Gap(8), + padding: widget.listPadding, + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, ), - ); - }, - separatorBuilder: (context, index) => const Gap(8), - padding: widget.listPadding, - physics: const BouncingScrollPhysics(), - scrollDirection: Axis.horizontal, - ), - ), + ), + ), + ); + }, ); } }