forked from eatvio/eatvio_app
fix: issue-942, issue-967, fixed images positioning for blog posts
This commit is contained in:
@@ -86,6 +86,8 @@ int parseDuration(dynamic duration) {
|
||||
|
||||
@freezed
|
||||
abstract class Author with _$Author {
|
||||
const Author._();
|
||||
|
||||
const factory Author({
|
||||
@JsonKey(name: "author_slug") required String authorSlug,
|
||||
@JsonKey(name: "author_name") required String authorName,
|
||||
@@ -93,9 +95,12 @@ abstract class Author with _$Author {
|
||||
@JsonKey(name: "author_is_me") required bool authorIsMe,
|
||||
@JsonKey(name: "is_entity_subscribed_by_me") bool? isEntitySubscribedByMe,
|
||||
@JsonKey(name: "author_place_in_rating") int? authorPlaceInRating,
|
||||
@JsonKey(name: "author_roles") @Default([]) List<String> authorRoles,
|
||||
}) = _Author;
|
||||
|
||||
factory Author.fromJson(Map<String, dynamic> json) => _$AuthorFromJson(json);
|
||||
|
||||
bool get isAdmin => authorRoles.contains('admin');
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -39,15 +39,17 @@ class BlogPostMediaContent extends StatefulWidget {
|
||||
class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
|
||||
static const _counterDisplayDuration = Duration(seconds: 1);
|
||||
|
||||
/// Landscape / video slide aspect (width / height).
|
||||
/// Fallback while the real photo size is unknown, and for video-only.
|
||||
static const _horizontalAspectRatio = 16 / 9;
|
||||
|
||||
/// Portrait slide aspect (width / height).
|
||||
static const _verticalAspectRatio = 3 / 4;
|
||||
|
||||
bool _isCounterVisible = false;
|
||||
Timer? _counterHideTimer;
|
||||
|
||||
final Map<String, double> _aspectByUrl = {};
|
||||
final Map<String, ImageStream> _aspectStreams = {};
|
||||
final Map<String, ImageStreamListener> _aspectListeners = {};
|
||||
|
||||
bool get _hasVideo =>
|
||||
widget.videoEmbedUrl != null && widget.videoEmbedUrl!.trim().isNotEmpty;
|
||||
|
||||
@@ -82,7 +84,6 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
|
||||
|
||||
bool get _shouldShowCounter => !_isCurrentSlideVideo || _isCounterVisible;
|
||||
|
||||
/// Height is fixed for the whole post from the first gallery image (web-like).
|
||||
String get _postOrientation {
|
||||
if (_gallery.isNotEmpty) {
|
||||
return _gallery.first.orientation;
|
||||
@@ -90,23 +91,89 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
|
||||
return 'horizontal';
|
||||
}
|
||||
|
||||
double _heightForAspect(double aspectRatio, double width) {
|
||||
if (aspectRatio <= 0) {
|
||||
return width / _horizontalAspectRatio;
|
||||
}
|
||||
return width / aspectRatio;
|
||||
}
|
||||
|
||||
double _heightForOrientation(String orientation, double width) {
|
||||
final aspectRatio = orientation == 'vertical'
|
||||
? _verticalAspectRatio
|
||||
: _horizontalAspectRatio;
|
||||
final maxHeight = MediaQuery.sizeOf(context).height * 0.7;
|
||||
return (width / aspectRatio).clamp(180.0, maxHeight);
|
||||
return _heightForAspect(aspectRatio, width);
|
||||
}
|
||||
|
||||
/// Window height follows the current photo's real size, not a fake 16:9 / 3:4 box.
|
||||
double _mediaHeight(double width) {
|
||||
final gallery = _gallery;
|
||||
if (gallery.isEmpty) {
|
||||
return _heightForOrientation(_postOrientation, width);
|
||||
}
|
||||
|
||||
final index = widget.currentGalleryIndex;
|
||||
final image = index < gallery.length ? gallery[index] : gallery.first;
|
||||
final aspect = _aspectByUrl[image.url];
|
||||
if (aspect != null && aspect > 0) {
|
||||
return _heightForAspect(aspect, width);
|
||||
}
|
||||
return _heightForOrientation(image.orientation, width);
|
||||
}
|
||||
|
||||
void _syncAspectListeners() {
|
||||
final urls = _gallery.map((image) => image.url).toSet();
|
||||
for (final url in _aspectStreams.keys.toList()) {
|
||||
if (urls.contains(url)) {
|
||||
continue;
|
||||
}
|
||||
final stream = _aspectStreams.remove(url);
|
||||
final listener = _aspectListeners.remove(url);
|
||||
if (stream != null && listener != null) {
|
||||
stream.removeListener(listener);
|
||||
}
|
||||
_aspectByUrl.remove(url);
|
||||
}
|
||||
|
||||
for (final url in urls) {
|
||||
if (_aspectStreams.containsKey(url)) {
|
||||
continue;
|
||||
}
|
||||
final stream = CachedNetworkImageProvider(
|
||||
url,
|
||||
).resolve(ImageConfiguration.empty);
|
||||
final listener = ImageStreamListener((info, _) {
|
||||
final aspect = info.image.width / info.image.height;
|
||||
if (!mounted || aspect <= 0 || _aspectByUrl[url] == aspect) {
|
||||
return;
|
||||
}
|
||||
setState(() => _aspectByUrl[url] = aspect);
|
||||
}, onError: (_, _) {});
|
||||
stream.addListener(listener);
|
||||
_aspectStreams[url] = stream;
|
||||
_aspectListeners[url] = listener;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_syncAspectListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_counterHideTimer?.cancel();
|
||||
for (final url in _aspectStreams.keys) {
|
||||
_aspectStreams[url]!.removeListener(_aspectListeners[url]!);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant BlogPostMediaContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_syncAspectListeners();
|
||||
if (oldWidget.currentGalleryIndex != widget.currentGalleryIndex) {
|
||||
_scheduleCounterHideOnVideo();
|
||||
}
|
||||
@@ -138,16 +205,26 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget _imageSlide(String itemUrl, int itemIndex, double height) {
|
||||
Widget _photo(String url, {required double width, double? height}) {
|
||||
return CachedNetworkImage(
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.center,
|
||||
width: width,
|
||||
height: height,
|
||||
imageUrl: url,
|
||||
errorWidget: (context, str, object) => const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _imageSlide(
|
||||
String itemUrl,
|
||||
int itemIndex, {
|
||||
required double width,
|
||||
required double height,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onOpenGallery(_galleryUrls, itemIndex),
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
height: height,
|
||||
imageUrl: itemUrl,
|
||||
errorWidget: (context, str, object) => const SizedBox.shrink(),
|
||||
),
|
||||
child: _photo(itemUrl, width: width, height: height),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,6 +252,8 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
|
||||
viewportFraction: 1,
|
||||
height: height,
|
||||
enableInfiniteScroll: false,
|
||||
disableCenter: true,
|
||||
padEnds: false,
|
||||
onPageChanged: (index, _) => widget.onGalleryChanged(index),
|
||||
),
|
||||
),
|
||||
@@ -200,29 +279,51 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth.isFinite
|
||||
? constraints.maxWidth
|
||||
: MediaQuery.sizeOf(context).width;
|
||||
return _buildMedia(width);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMedia(double width) {
|
||||
final gallery = _gallery;
|
||||
final width = MediaQuery.sizeOf(context).width;
|
||||
final mediaHeight = _heightForOrientation(_postOrientation, width);
|
||||
final mediaHeight = _mediaHeight(width);
|
||||
|
||||
if (gallery.length > 1 || (gallery.length == 1 && _hasVideo)) {
|
||||
final itemCount = _slideCount;
|
||||
return _carousel(
|
||||
itemCount: itemCount,
|
||||
height: mediaHeight,
|
||||
itemBuilder: (context, itemIndex) {
|
||||
if (itemIndex < gallery.length) {
|
||||
return _imageSlide(gallery[itemIndex].url, itemIndex, mediaHeight);
|
||||
}
|
||||
if (_hasVideo) {
|
||||
return _videoSlide();
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
return AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: _carousel(
|
||||
itemCount: itemCount,
|
||||
height: mediaHeight,
|
||||
itemBuilder: (context, itemIndex) {
|
||||
if (itemIndex < gallery.length) {
|
||||
return _imageSlide(
|
||||
gallery[itemIndex].url,
|
||||
itemIndex,
|
||||
width: width,
|
||||
height: mediaHeight,
|
||||
);
|
||||
}
|
||||
if (_hasVideo) {
|
||||
return _videoSlide();
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (gallery.length == 1) {
|
||||
return _imageSlide(gallery.first.url, 0, mediaHeight);
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onOpenGallery(_galleryUrls, 0),
|
||||
child: _photo(gallery.first.url, width: width),
|
||||
);
|
||||
}
|
||||
|
||||
if (_hasVideo) {
|
||||
@@ -230,12 +331,7 @@ class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
|
||||
}
|
||||
|
||||
if (widget.imageUrl.isNotEmpty) {
|
||||
final image = CachedNetworkImage(
|
||||
fit: BoxFit.fitWidth,
|
||||
width: width,
|
||||
imageUrl: widget.imageUrl,
|
||||
errorWidget: (context, str, object) => const SizedBox.shrink(),
|
||||
);
|
||||
final image = _photo(widget.imageUrl, width: width);
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onOpenGallery([widget.imageUrl], 0),
|
||||
child: widget.heroTag == null
|
||||
|
||||
@@ -199,6 +199,7 @@ class _BlogPostHeader extends StatelessWidget {
|
||||
),
|
||||
CustomPopup(
|
||||
isAuthor: post.author.authorIsMe,
|
||||
isAuthorAdmin: post.author.isAdmin,
|
||||
complainedContentType: ComplainedContent.posts,
|
||||
path: 'blog_post/${post.id}',
|
||||
id: post.id,
|
||||
|
||||
@@ -80,6 +80,7 @@ class _DietsWidgetState extends State<DietsWidget> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CustomPopup(
|
||||
isAuthor: widget.diet.author.authorIsMe,
|
||||
isAuthorAdmin: widget.diet.author.isAdmin,
|
||||
path: 'diet/${widget.diet.slug}',
|
||||
id: widget.diet.id,
|
||||
complainedContentType: ComplainedContent.diet,
|
||||
|
||||
@@ -85,6 +85,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CustomPopup(
|
||||
isAuthor: widget.plan.author.authorIsMe,
|
||||
isAuthorAdmin: widget.plan.author.isAdmin,
|
||||
path: 'plan/${widget.plan.slug}',
|
||||
id: widget.plan.id,
|
||||
slug: widget.plan.slug,
|
||||
|
||||
@@ -115,6 +115,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CustomPopup(
|
||||
isAuthor: widget.recipe.author.authorIsMe,
|
||||
isAuthorAdmin: widget.recipe.author.isAdmin,
|
||||
path: 'recipe/${widget.recipe.slug}',
|
||||
slug: widget.recipe.slug,
|
||||
complainedContentType: ComplainedContent.recipe,
|
||||
|
||||
Reference in New Issue
Block a user