Optimize attachment list

This commit is contained in:
LittleSheep 2025-05-22 22:33:32 +08:00
parent 835b90bf5a
commit f646dd9c0c
5 changed files with 245 additions and 39 deletions

View File

@ -1,20 +1,32 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:photo_view/photo_view.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:uuid/uuid.dart';
class CloudFileList extends StatelessWidget {
class CloudFileList extends HookConsumerWidget {
final List<SnCloudFile> files;
final double maxHeight;
final double maxWidth;
final double? minWidth;
final bool disableZoomIn;
const CloudFileList({
super.key,
required this.files,
this.maxHeight = 360,
this.maxWidth = double.infinity,
this.minWidth,
this.disableZoomIn = false,
});
double calculateAspectRatio() {
@ -30,16 +42,43 @@ class CloudFileList extends StatelessWidget {
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final heroTags = useMemoized(
() => List.generate(
files.length,
(index) => 'cloud-files#${const Uuid().v4()}',
),
[files],
);
if (files.isEmpty) return const SizedBox.shrink();
if (files.length == 1) {
final isImage = files.first.mimeType?.startsWith('image') ?? false;
return ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight),
constraints: BoxConstraints(
maxHeight: maxHeight,
minWidth: minWidth ?? 0,
),
child: AspectRatio(
aspectRatio: calculateAspectRatio(),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: CloudFileWidget(item: files.first),
child: _CloudFileListEntry(
file: files.first,
heroTag: heroTags.first,
isImage: isImage,
disableZoomIn: disableZoomIn,
onTap: () {
if (!isImage) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: files.first, heroTag: heroTags.first),
);
}
},
),
),
),
).padding(horizontal: 3);
@ -64,7 +103,25 @@ class CloudFileList extends StatelessWidget {
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
children: [for (final item in files) CloudFileWidget(item: item)],
children: [
for (var i = 0; i < files.length; i++)
_CloudFileListEntry(
file: files[i],
heroTag: heroTags[i],
isImage: files[i].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
),
],
onTap: (i) {
if (!(files[i].mimeType?.startsWith('image') ?? false)) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
);
}
},
),
),
);
@ -81,7 +138,25 @@ class CloudFileList extends StatelessWidget {
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: CloudFileWidget(item: files[index]),
child: _CloudFileListEntry(
file: files[index],
heroTag: heroTags[index],
isImage: files[index].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
onTap: () {
if (!(files[index].mimeType?.startsWith('image') ?? false)) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(
item: files[index],
heroTag: heroTags[index],
),
);
}
},
),
);
},
separatorBuilder: (_, __) => const Gap(8),
@ -90,3 +165,102 @@ class CloudFileList extends StatelessWidget {
);
}
}
class CloudFileZoomIn extends HookConsumerWidget {
final SnCloudFile item;
final String heroTag;
const CloudFileZoomIn({super.key, required this.item, required this.heroTag});
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverUrl = ref.watch(serverUrlProvider);
final photoViewController = useMemoized(() => PhotoViewController(), []);
return DismissiblePage(
isFullScreen: true,
backgroundColor: Colors.transparent,
direction: DismissiblePageDismissDirection.down,
onDismissed: () {
Navigator.of(context).pop();
},
child: Stack(
children: [
Positioned.fill(
child: PhotoView(
backgroundDecoration: BoxDecoration(
color: Colors.black.withOpacity(0.9),
),
controller: photoViewController,
heroAttributes: PhotoViewHeroAttributes(tag: heroTag),
imageProvider: CloudImageWidget.provider(
fileId: item.id,
serverUrl: serverUrl,
),
),
),
// Close button
Positioned(
top: MediaQuery.of(context).padding.top + 20,
right: 20,
child: IconButton(
icon: Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
),
],
),
);
}
}
class _CloudFileListEntry extends StatelessWidget {
final SnCloudFile file;
final String heroTag;
final bool isImage;
final bool disableZoomIn;
final VoidCallback? onTap;
const _CloudFileListEntry({
required this.file,
required this.heroTag,
required this.isImage,
required this.disableZoomIn,
this.onTap,
});
@override
Widget build(BuildContext context) {
final content = Stack(
children: [
if (isImage)
Positioned.fill(
child:
file.fileMeta?['blur'] != null
? BlurHash(hash: file.fileMeta?['blur'])
: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: CloudFileWidget(item: file, noBlurhash: true),
),
),
if (isImage)
CloudFileWidget(
item: file,
heroTag: heroTag,
noBlurhash: true,
).center()
else
CloudFileWidget(item: file, heroTag: heroTag),
],
);
if (onTap != null) {
return InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: onTap,
child: content,
);
}
return content;
}
}

View File

@ -12,33 +12,44 @@ import 'video.dart';
class CloudFileWidget extends ConsumerWidget {
final SnCloudFile item;
final BoxFit fit;
final String? heroTag;
final bool noBlurhash;
const CloudFileWidget({
super.key,
required this.item,
this.fit = BoxFit.cover,
this.heroTag,
this.noBlurhash = false,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/files/${item.id}';
switch (item.mimeType?.split('/').firstOrNull) {
case "image":
return AspectRatio(
final content = switch (item.mimeType?.split('/').firstOrNull) {
"image" => AspectRatio(
aspectRatio: (item.fileMeta?['ratio'] ?? 1).toDouble(),
child: UniversalImage(uri: uri, blurHash: item.fileMeta?['blur']),
);
case "video":
return AspectRatio(
child: UniversalImage(
uri: uri,
blurHash: noBlurhash ? null : item.fileMeta?['blur'],
),
),
"video" => AspectRatio(
aspectRatio: (item.fileMeta?['ratio'] ?? 16 / 9).toDouble(),
child: UniversalVideo(
uri: uri,
aspectRatio: (item.fileMeta?['ratio'] ?? 16 / 9).toDouble(),
),
);
default:
return Text('Unable render for ${item.mimeType}');
),
_ => Text('Unable render for ${item.mimeType}'),
};
if (heroTag != null) {
return Hero(tag: heroTag!, child: content);
}
return content;
}
}

View File

@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -8,6 +10,7 @@ import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_file_collection.dart';
import 'package:island/widgets/content/cloud_files.dart';
@ -127,6 +130,18 @@ class PostItem extends HookConsumerWidget {
Text(item.publisher.nick).bold(),
if (item.content?.isNotEmpty ?? false)
MarkdownTextContent(content: item.content!),
if (item.attachments.isNotEmpty)
CloudFileList(
files: item.attachments,
maxWidth: math.min(
MediaQuery.of(context).size.width * 0.85,
kWideScreenWidth - 160,
),
minWidth: math.min(
MediaQuery.of(context).size.width * 0.9,
kWideScreenWidth - 160,
),
).padding(top: 4),
],
),
onTap: () {
@ -138,18 +153,6 @@ class PostItem extends HookConsumerWidget {
),
],
),
if (item.attachments.isNotEmpty)
Container(
margin: EdgeInsets.only(left: 48),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
child: CloudFileList(files: item.attachments),
),
PostReactionList(
parentId: item.id,
reactions: item.reactionsCount,

View File

@ -397,10 +397,10 @@ packages:
dependency: transitive
description:
name: dart_style
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.1.0"
dbus:
dependency: transitive
description:
@ -441,6 +441,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
dismissible_page:
dependency: "direct main"
description:
name: dismissible_page
sha256: "5b2316f770fe83583f770df1f6505cb19102081c5971979806e77f2e507a9958"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
drift:
dependency: "direct main"
description:
@ -775,10 +783,10 @@ packages:
dependency: transitive
description:
name: flutter_math_fork
sha256: "284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac"
sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
version: "0.7.4"
flutter_native_splash:
dependency: "direct main"
description:
@ -1381,6 +1389,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.0"
photo_view:
dependency: "direct main"
description:
name: photo_view
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
url: "https://pub.dev"
source: hosted
version: "0.15.0"
pixel_snap:
dependency: transitive
description:
@ -1734,10 +1750,10 @@ packages:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
sha256: "7986c26234c0a5cf4fd83ff4ee39d4195b1f47cdb50a949ec7987ede4dcbdc2a"
url: "https://pub.dev"
source: hosted
version: "0.5.32"
version: "0.5.33"
sqlparser:
dependency: transitive
description:

View File

@ -98,6 +98,8 @@ dependencies:
markdown_widget: ^2.3.2+8
visibility_detector: ^0.4.0+2
flutter_native_splash: ^2.4.6
photo_view: ^0.15.0
dismissible_page: ^1.0.2
dev_dependencies:
flutter_test: