✨ Optimize attachment list
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user