✨ Optimize attachment list
This commit is contained in:
parent
835b90bf5a
commit
f646dd9c0c
@ -1,20 +1,32 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/material.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:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.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:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class CloudFileList extends StatelessWidget {
|
class CloudFileList extends HookConsumerWidget {
|
||||||
final List<SnCloudFile> files;
|
final List<SnCloudFile> files;
|
||||||
final double maxHeight;
|
final double maxHeight;
|
||||||
final double maxWidth;
|
final double maxWidth;
|
||||||
|
final double? minWidth;
|
||||||
|
final bool disableZoomIn;
|
||||||
const CloudFileList({
|
const CloudFileList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.files,
|
required this.files,
|
||||||
this.maxHeight = 360,
|
this.maxHeight = 360,
|
||||||
this.maxWidth = double.infinity,
|
this.maxWidth = double.infinity,
|
||||||
|
this.minWidth,
|
||||||
|
this.disableZoomIn = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
double calculateAspectRatio() {
|
double calculateAspectRatio() {
|
||||||
@ -30,16 +42,43 @@ class CloudFileList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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.isEmpty) return const SizedBox.shrink();
|
||||||
if (files.length == 1) {
|
if (files.length == 1) {
|
||||||
|
final isImage = files.first.mimeType?.startsWith('image') ?? false;
|
||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: maxHeight,
|
||||||
|
minWidth: minWidth ?? 0,
|
||||||
|
),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: calculateAspectRatio(),
|
aspectRatio: calculateAspectRatio(),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
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);
|
).padding(horizontal: 3);
|
||||||
@ -64,7 +103,25 @@ class CloudFileList extends StatelessWidget {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
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) {
|
itemBuilder: (context, index) {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
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),
|
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 {
|
class CloudFileWidget extends ConsumerWidget {
|
||||||
final SnCloudFile item;
|
final SnCloudFile item;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
|
final String? heroTag;
|
||||||
|
final bool noBlurhash;
|
||||||
const CloudFileWidget({
|
const CloudFileWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.item,
|
required this.item,
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
|
this.heroTag,
|
||||||
|
this.noBlurhash = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final serverUrl = ref.watch(serverUrlProvider);
|
final serverUrl = ref.watch(serverUrlProvider);
|
||||||
final uri = '$serverUrl/files/${item.id}';
|
final uri = '$serverUrl/files/${item.id}';
|
||||||
switch (item.mimeType?.split('/').firstOrNull) {
|
|
||||||
case "image":
|
final content = switch (item.mimeType?.split('/').firstOrNull) {
|
||||||
return AspectRatio(
|
"image" => AspectRatio(
|
||||||
aspectRatio: (item.fileMeta?['ratio'] ?? 1).toDouble(),
|
aspectRatio: (item.fileMeta?['ratio'] ?? 1).toDouble(),
|
||||||
child: UniversalImage(uri: uri, blurHash: item.fileMeta?['blur']),
|
child: UniversalImage(
|
||||||
);
|
uri: uri,
|
||||||
case "video":
|
blurHash: noBlurhash ? null : item.fileMeta?['blur'],
|
||||||
return AspectRatio(
|
),
|
||||||
|
),
|
||||||
|
"video" => AspectRatio(
|
||||||
aspectRatio: (item.fileMeta?['ratio'] ?? 16 / 9).toDouble(),
|
aspectRatio: (item.fileMeta?['ratio'] ?? 16 / 9).toDouble(),
|
||||||
child: UniversalVideo(
|
child: UniversalVideo(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
aspectRatio: (item.fileMeta?['ratio'] ?? 16 / 9).toDouble(),
|
aspectRatio: (item.fileMeta?['ratio'] ?? 16 / 9).toDouble(),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
default:
|
_ => Text('Unable render for ${item.mimeType}'),
|
||||||
return 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:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.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/network.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/route.gr.dart';
|
import 'package:island/route.gr.dart';
|
||||||
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/cloud_file_collection.dart';
|
import 'package:island/widgets/content/cloud_file_collection.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
@ -127,6 +130,18 @@ class PostItem extends HookConsumerWidget {
|
|||||||
Text(item.publisher.nick).bold(),
|
Text(item.publisher.nick).bold(),
|
||||||
if (item.content?.isNotEmpty ?? false)
|
if (item.content?.isNotEmpty ?? false)
|
||||||
MarkdownTextContent(content: item.content!),
|
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: () {
|
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(
|
PostReactionList(
|
||||||
parentId: item.id,
|
parentId: item.id,
|
||||||
reactions: item.reactionsCount,
|
reactions: item.reactionsCount,
|
||||||
|
28
pubspec.lock
28
pubspec.lock
@ -397,10 +397,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
|
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.1.0"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -441,6 +441,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
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:
|
drift:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -775,10 +783,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_math_fork
|
name: flutter_math_fork
|
||||||
sha256: "284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac"
|
sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.3"
|
version: "0.7.4"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1381,6 +1389,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.0"
|
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:
|
pixel_snap:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1734,10 +1750,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlite3_flutter_libs
|
name: sqlite3_flutter_libs
|
||||||
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
|
sha256: "7986c26234c0a5cf4fd83ff4ee39d4195b1f47cdb50a949ec7987ede4dcbdc2a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.32"
|
version: "0.5.33"
|
||||||
sqlparser:
|
sqlparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -98,6 +98,8 @@ dependencies:
|
|||||||
markdown_widget: ^2.3.2+8
|
markdown_widget: ^2.3.2+8
|
||||||
visibility_detector: ^0.4.0+2
|
visibility_detector: ^0.4.0+2
|
||||||
flutter_native_splash: ^2.4.6
|
flutter_native_splash: ^2.4.6
|
||||||
|
photo_view: ^0.15.0
|
||||||
|
dismissible_page: ^1.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user