♻️ Better image loading animation and more commonly used blurhash
This commit is contained in:
@@ -577,6 +577,9 @@ class ProfilePictureWidget extends ConsumerWidget {
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final String? id = file?.id ?? fileId;
|
||||
|
||||
final meta = file?.fileMeta is Map ? (file!.fileMeta as Map) : const {};
|
||||
final blurHash = meta['blur'] as String?;
|
||||
|
||||
final fallback = Icon(
|
||||
fallbackIcon ?? Symbols.account_circle,
|
||||
size: radius,
|
||||
@@ -590,6 +593,7 @@ class ProfilePictureWidget extends ConsumerWidget {
|
||||
placeholder: fallback,
|
||||
content: () => UniversalImage(
|
||||
uri: '$serverUrl/drive/files/$id',
|
||||
blurHash: blurHash,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
@@ -625,14 +629,14 @@ class ProfilePictureWidget extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class SplitAvatarWidget extends ConsumerWidget {
|
||||
final List<String?> filesId;
|
||||
final List<SnCloudFile?> files;
|
||||
final double radius;
|
||||
final IconData fallbackIcon;
|
||||
final Color? fallbackColor;
|
||||
|
||||
const SplitAvatarWidget({
|
||||
super.key,
|
||||
required this.filesId,
|
||||
required this.files,
|
||||
this.radius = 20,
|
||||
this.fallbackIcon = Symbols.account_circle,
|
||||
this.fallbackColor,
|
||||
@@ -640,17 +644,17 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (filesId.isEmpty) {
|
||||
if (files.isEmpty) {
|
||||
return ProfilePictureWidget(
|
||||
fileId: null,
|
||||
file: null,
|
||||
radius: radius,
|
||||
fallbackIcon: fallbackIcon,
|
||||
fallbackColor: fallbackColor,
|
||||
);
|
||||
}
|
||||
if (filesId.length == 1) {
|
||||
if (files.length == 1) {
|
||||
return ProfilePictureWidget(
|
||||
fileId: filesId[0],
|
||||
file: files[0],
|
||||
radius: radius,
|
||||
fallbackIcon: fallbackIcon,
|
||||
fallbackColor: fallbackColor,
|
||||
@@ -665,32 +669,32 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (filesId.length == 2)
|
||||
if (files.length == 2)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[0], ref, radius),
|
||||
child: _buildQuadrant(context, files[0], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[1], ref, radius),
|
||||
child: _buildQuadrant(context, files[1], ref, radius),
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (filesId.length == 3)
|
||||
else if (files.length == 3)
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[0], ref, radius),
|
||||
child: _buildQuadrant(context, files[0], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[1], ref, radius),
|
||||
child: _buildQuadrant(context, files[1], ref, radius),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(context, filesId[2], ref, radius),
|
||||
child: _buildQuadrant(context, files[2], ref, radius),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -701,20 +705,10 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[0],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
child: _buildQuadrant(context, files[0], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[1],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
child: _buildQuadrant(context, files[1], ref, radius),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -723,22 +717,17 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[2],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
child: _buildQuadrant(context, files[2], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: filesId.length > 4
|
||||
child: files.length > 4
|
||||
? Container(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'+${filesId.length - 3}',
|
||||
'+${files.length - 3}',
|
||||
style: TextStyle(
|
||||
fontSize: radius * 0.4,
|
||||
color: Theme.of(
|
||||
@@ -748,12 +737,7 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: _buildQuadrant(
|
||||
context,
|
||||
filesId[3],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
: _buildQuadrant(context, files[3], ref, radius),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -768,11 +752,11 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
|
||||
Widget _buildQuadrant(
|
||||
BuildContext context,
|
||||
String? fileId,
|
||||
SnCloudFile? file,
|
||||
WidgetRef ref,
|
||||
double radius,
|
||||
) {
|
||||
if (fileId == null) {
|
||||
if (file == null) {
|
||||
return Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
@@ -787,7 +771,7 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final serverUrl = ref.watch(serverUrlProvider);
|
||||
final uri = '$serverUrl/drive/files/$fileId';
|
||||
final uri = '$serverUrl/drive/files/${file.id}';
|
||||
|
||||
return SizedBox(
|
||||
width: radius,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class UniversalImage extends StatelessWidget {
|
||||
class UniversalImage extends HookWidget {
|
||||
final String uri;
|
||||
final String? blurHash;
|
||||
final BoxFit fit;
|
||||
@@ -27,6 +28,7 @@ class UniversalImage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loaded = useState(false);
|
||||
final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg');
|
||||
|
||||
if (isSvgImage) {
|
||||
@@ -35,9 +37,8 @@ class UniversalImage extends StatelessWidget {
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
placeholderBuilder:
|
||||
(BuildContext context) =>
|
||||
Center(child: CircularProgressIndicator()),
|
||||
placeholderBuilder: (BuildContext context) =>
|
||||
Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,8 +47,9 @@ class UniversalImage extends StatelessWidget {
|
||||
if (width != null && height != null && !noCacheOptimization) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
cacheWidth = width != null ? (width! * devicePixelRatio).round() : null;
|
||||
cacheHeight =
|
||||
height != null ? (height! * devicePixelRatio).round() : null;
|
||||
cacheHeight = height != null
|
||||
? (height! * devicePixelRatio).round()
|
||||
: null;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
@@ -66,21 +68,72 @@ class UniversalImage extends StatelessWidget {
|
||||
memCacheWidth: cacheWidth,
|
||||
progressIndicatorBuilder: (context, url, progress) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(value: progress.progress),
|
||||
child: AnimatedCircularProgressIndicator(
|
||||
value: progress.progress,
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorWidget:
|
||||
(context, url, error) =>
|
||||
useFallbackImage
|
||||
? Image.asset(
|
||||
'assets/images/media-offline.jpg',
|
||||
fit: BoxFit.cover,
|
||||
key: Key('image-broke-$uri'),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
imageBuilder: (context, imageProvider) {
|
||||
Future(() => loaded.value = true);
|
||||
return AnimatedOpacity(
|
||||
opacity: loaded.value ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Image(
|
||||
image: imageProvider,
|
||||
fit: fit,
|
||||
width: width,
|
||||
height: height,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) => useFallbackImage
|
||||
? Image.asset(
|
||||
'assets/images/media-offline.jpg',
|
||||
fit: BoxFit.cover,
|
||||
key: Key('image-broke-$uri'),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCircularProgressIndicator extends HookWidget {
|
||||
final double? value;
|
||||
final Color? color;
|
||||
final double strokeWidth;
|
||||
final Duration duration;
|
||||
|
||||
const AnimatedCircularProgressIndicator({
|
||||
super.key,
|
||||
this.value,
|
||||
this.color,
|
||||
this.strokeWidth = 4.0,
|
||||
this.duration = const Duration(milliseconds: 200),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final animationController = useAnimationController(duration: duration);
|
||||
final animation = useAnimation(
|
||||
Tween<double>(begin: 0.0, end: value ?? 0.0).animate(
|
||||
CurvedAnimation(parent: animationController, curve: Curves.linear),
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
animationController.animateTo(value ?? 0.0);
|
||||
return null;
|
||||
}, [value]);
|
||||
|
||||
return CircularProgressIndicator(
|
||||
value: animation,
|
||||
color: color,
|
||||
strokeWidth: strokeWidth,
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user