|
|
|
@ -765,7 +765,10 @@ class _RecipeCard extends StatelessWidget {
|
|
|
|
Widget _buildRecipeImage(String imageUrl) {
|
|
|
|
Widget _buildRecipeImage(String imageUrl) {
|
|
|
|
final mediaService = ServiceLocator.instance.mediaService;
|
|
|
|
final mediaService = ServiceLocator.instance.mediaService;
|
|
|
|
if (mediaService == null) {
|
|
|
|
if (mediaService == null) {
|
|
|
|
return Image.network(imageUrl, fit: BoxFit.cover);
|
|
|
|
return _FadeInImageWidget(
|
|
|
|
|
|
|
|
imageUrl: imageUrl,
|
|
|
|
|
|
|
|
isNetwork: true,
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Try to extract asset ID from Immich URL (format: .../api/assets/{id}/original)
|
|
|
|
// Try to extract asset ID from Immich URL (format: .../api/assets/{id}/original)
|
|
|
|
@ -799,18 +802,96 @@ class _RecipeCard extends StatelessWidget {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Image.memory(
|
|
|
|
return _FadeInImageWidget(
|
|
|
|
snapshot.data!,
|
|
|
|
imageBytes: snapshot.data!,
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
|
|
height: double.infinity,
|
|
|
|
|
|
|
|
);
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Image.network(
|
|
|
|
return _FadeInImageWidget(
|
|
|
|
imageUrl,
|
|
|
|
imageUrl: imageUrl,
|
|
|
|
|
|
|
|
isNetwork: true,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildVideoThumbnail(String videoUrl) {
|
|
|
|
|
|
|
|
return _VideoThumbnailPreview(videoUrl: videoUrl);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Widget that displays an image with a smooth fade-in animation.
|
|
|
|
|
|
|
|
class _FadeInImageWidget extends StatefulWidget {
|
|
|
|
|
|
|
|
final String? imageUrl;
|
|
|
|
|
|
|
|
final Uint8List? imageBytes;
|
|
|
|
|
|
|
|
final bool isNetwork;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const _FadeInImageWidget({
|
|
|
|
|
|
|
|
this.imageUrl,
|
|
|
|
|
|
|
|
this.imageBytes,
|
|
|
|
|
|
|
|
this.isNetwork = false,
|
|
|
|
|
|
|
|
}) : assert(imageUrl != null || imageBytes != null, 'Either imageUrl or imageBytes must be provided');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
|
|
State<_FadeInImageWidget> createState() => _FadeInImageWidgetState();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FadeInImageWidgetState extends State<_FadeInImageWidget>
|
|
|
|
|
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
|
|
|
|
late AnimationController _controller;
|
|
|
|
|
|
|
|
late Animation<double> _fadeAnimation;
|
|
|
|
|
|
|
|
bool _imageLoaded = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
|
|
super.initState();
|
|
|
|
|
|
|
|
_controller = AnimationController(
|
|
|
|
|
|
|
|
duration: const Duration(milliseconds: 500),
|
|
|
|
|
|
|
|
vsync: this,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
_fadeAnimation = CurvedAnimation(
|
|
|
|
|
|
|
|
parent: _controller,
|
|
|
|
|
|
|
|
curve: Curves.easeOut,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
// Start with opacity 0
|
|
|
|
|
|
|
|
_controller.value = 0.0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
|
|
_controller.dispose();
|
|
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void _onImageLoaded() {
|
|
|
|
|
|
|
|
if (!_imageLoaded && mounted) {
|
|
|
|
|
|
|
|
_imageLoaded = true;
|
|
|
|
|
|
|
|
_controller.forward();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
|
|
Widget imageWidget;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (widget.imageBytes != null) {
|
|
|
|
|
|
|
|
// For memory images, use frameBuilder to detect when image is ready
|
|
|
|
|
|
|
|
imageWidget = Image.memory(
|
|
|
|
|
|
|
|
widget.imageBytes!,
|
|
|
|
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
|
|
|
height: double.infinity,
|
|
|
|
|
|
|
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
|
|
|
|
|
|
|
if ((wasSynchronouslyLoaded || frame != null) && !_imageLoaded) {
|
|
|
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _onImageLoaded());
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return child;
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
} else if (widget.imageUrl != null) {
|
|
|
|
|
|
|
|
// For network images, use frameBuilder to detect when image is ready
|
|
|
|
|
|
|
|
imageWidget = Image.network(
|
|
|
|
|
|
|
|
widget.imageUrl!,
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
width: double.infinity,
|
|
|
|
width: double.infinity,
|
|
|
|
height: double.infinity,
|
|
|
|
height: double.infinity,
|
|
|
|
@ -821,7 +902,10 @@ class _RecipeCard extends StatelessWidget {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
loadingBuilder: (context, child, loadingProgress) {
|
|
|
|
loadingBuilder: (context, child, loadingProgress) {
|
|
|
|
if (loadingProgress == null) return child;
|
|
|
|
if (loadingProgress == null) {
|
|
|
|
|
|
|
|
// Image is loaded, return child and let frameBuilder handle fade-in
|
|
|
|
|
|
|
|
return child;
|
|
|
|
|
|
|
|
}
|
|
|
|
return Container(
|
|
|
|
return Container(
|
|
|
|
color: Colors.grey.shade200,
|
|
|
|
color: Colors.grey.shade200,
|
|
|
|
child: Center(
|
|
|
|
child: Center(
|
|
|
|
@ -834,11 +918,24 @@ class _RecipeCard extends StatelessWidget {
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
|
|
|
|
|
|
|
if ((wasSynchronouslyLoaded || frame != null) && !_imageLoaded) {
|
|
|
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _onImageLoaded());
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return child;
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
|
|
color: Colors.grey.shade200,
|
|
|
|
|
|
|
|
child: const Icon(Icons.broken_image, size: 48),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget _buildVideoThumbnail(String videoUrl) {
|
|
|
|
return FadeTransition(
|
|
|
|
return _VideoThumbnailPreview(videoUrl: videoUrl);
|
|
|
|
opacity: _fadeAnimation,
|
|
|
|
|
|
|
|
child: imageWidget,
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|