✨ Optimize attachment list
This commit is contained in:
parent
835b90bf5a
commit
f646dd9c0c
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
aspectRatio: (item.fileMeta?['ratio'] ?? 1).toDouble(),
|
||||
child: UniversalImage(uri: uri, blurHash: item.fileMeta?['blur']),
|
||||
);
|
||||
case "video":
|
||||
return AspectRatio(
|
||||
|
||||
final content = switch (item.mimeType?.split('/').firstOrNull) {
|
||||
"image" => AspectRatio(
|
||||
aspectRatio: (item.fileMeta?['ratio'] ?? 1).toDouble(),
|
||||
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(),
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
28
pubspec.lock
28
pubspec.lock
@ -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:
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user