forked from eatvio/eatvio_app
feat(personalized_feed): added video and multiple images handling to detailed blog post screen
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/enums/complained_content.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
@@ -9,7 +9,10 @@ import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit
|
||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/post_feed_mapper.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_media_content.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -33,6 +36,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
final CommentsCubit commentsCubit = sl<CommentsCubit>();
|
||||
final textEditingController = TextEditingController();
|
||||
int? replyId;
|
||||
int currentGalleryIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -40,6 +44,10 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _openGallery(List<String> imageUrls, int index) {
|
||||
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: imageUrls, index: index));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
@@ -242,8 +250,33 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
child: MarkdownHtmlWidget(markdownText: blogPostState.blogPostModel!.description),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
|
||||
CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${blogPostState.blogPostModel?.image ?? ''}'),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final post = blogPostState.blogPostModel;
|
||||
if (post == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final imageUrl = PostFeedMapper.resolveMediaUrl(post.image);
|
||||
final galleryUrls = post.images
|
||||
.map((e) => PostFeedMapper.resolveMediaUrl(e.url))
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
final embedUrl = post.videoEmbedUrl?.trim();
|
||||
return BlogPostMediaContent(
|
||||
galleryUrls: galleryUrls,
|
||||
imageUrl: imageUrl,
|
||||
videoEmbedUrl: embedUrl != null && embedUrl.isNotEmpty ? embedUrl : null,
|
||||
videoUrlThumb: post.videoUrlThumb != null ? PostFeedMapper.resolveMediaUrl(post.videoUrlThumb) : null,
|
||||
currentGalleryIndex: currentGalleryIndex,
|
||||
onOpenGallery: _openGallery,
|
||||
onGalleryChanged: (index) {
|
||||
setState(() {
|
||||
currentGalleryIndex = index;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
|
||||
@@ -22,7 +22,7 @@ abstract class FeedBlogPostModel with _$FeedBlogPostModel {
|
||||
@JsonKey(name: "total_comments") required int totalComments,
|
||||
@JsonKey(name: "was_liked_by_me") required bool wasLikedByMe,
|
||||
@Default([]) List<FeedGalleryImage> images,
|
||||
@JsonKey(name: "video_embed_url") String? videoEmbedUrl,
|
||||
@JsonKey(name: "video_url_embed") String? videoEmbedUrl,
|
||||
@JsonKey(name: "video_url_thumb") String? videoUrlThumb,
|
||||
}) = _FeedBlogPostModel;
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ abstract class PostFeedItemModel with _$PostFeedItemModel {
|
||||
abstract class FeedGalleryImage with _$FeedGalleryImage {
|
||||
const factory FeedGalleryImage({
|
||||
required String url,
|
||||
required String orientation,
|
||||
@Default('horizontal') String orientation,
|
||||
int? id,
|
||||
int? position,
|
||||
}) = _FeedGalleryImage;
|
||||
|
||||
factory FeedGalleryImage.fromJson(Map<String, Object?> json) => _$FeedGalleryImageFromJson(json);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:carousel_slider_plus/carousel_slider_plus.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/feed_video_player/feed_video_player_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Gallery + optional embed video for feed card and blog post detail.
|
||||
class BlogPostMediaContent extends StatefulWidget {
|
||||
const BlogPostMediaContent({
|
||||
super.key,
|
||||
required this.galleryUrls,
|
||||
required this.imageUrl,
|
||||
required this.videoEmbedUrl,
|
||||
required this.currentGalleryIndex,
|
||||
required this.onOpenGallery,
|
||||
required this.onGalleryChanged,
|
||||
required this.videoUrlThumb,
|
||||
});
|
||||
|
||||
final List<String> galleryUrls;
|
||||
final String imageUrl;
|
||||
final String? videoEmbedUrl;
|
||||
final String? videoUrlThumb;
|
||||
final int currentGalleryIndex;
|
||||
final void Function(List<String> urls, int index) onOpenGallery;
|
||||
final ValueChanged<int> onGalleryChanged;
|
||||
|
||||
@override
|
||||
State<BlogPostMediaContent> createState() => _BlogPostMediaContentState();
|
||||
}
|
||||
|
||||
class _BlogPostMediaContentState extends State<BlogPostMediaContent> {
|
||||
static const _counterDisplayDuration = Duration(seconds: 1);
|
||||
static const _carouselHeight = 260.0;
|
||||
|
||||
bool _isCounterVisible = false;
|
||||
Timer? _counterHideTimer;
|
||||
|
||||
bool get _hasVideo => widget.videoEmbedUrl != null && widget.videoEmbedUrl!.trim().isNotEmpty;
|
||||
|
||||
int get _slideCount {
|
||||
if (widget.galleryUrls.isNotEmpty) {
|
||||
return widget.galleryUrls.length + (_hasVideo ? 1 : 0);
|
||||
}
|
||||
if (_hasVideo) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool get _isCurrentSlideVideo => _hasVideo && widget.currentGalleryIndex == widget.galleryUrls.length;
|
||||
|
||||
bool get _shouldShowCounter => !_isCurrentSlideVideo || _isCounterVisible;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_counterHideTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant BlogPostMediaContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.currentGalleryIndex != widget.currentGalleryIndex) {
|
||||
_scheduleCounterHideOnVideo();
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleCounterHideOnVideo() {
|
||||
_counterHideTimer?.cancel();
|
||||
if (!_isCurrentSlideVideo) return;
|
||||
|
||||
_counterHideTimer = Timer(_counterDisplayDuration, () {
|
||||
if (mounted && _isCurrentSlideVideo) {
|
||||
setState(() => _isCounterVisible = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _handleCarouselScroll(ScrollNotification notification) {
|
||||
if (notification is ScrollStartNotification || notification is ScrollUpdateNotification) {
|
||||
_counterHideTimer?.cancel();
|
||||
if (!_isCounterVisible) {
|
||||
setState(() => _isCounterVisible = true);
|
||||
}
|
||||
} else if (notification is ScrollEndNotification && _isCounterVisible && _isCurrentSlideVideo) {
|
||||
_scheduleCounterHideOnVideo();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget _imageSlide(String itemUrl, int itemIndex) {
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onOpenGallery(widget.galleryUrls, itemIndex),
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: _carouselHeight,
|
||||
imageUrl: itemUrl,
|
||||
errorWidget: (context, str, object) => const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _videoSlide() {
|
||||
return FeedVideoPlayer(embedUrl: widget.videoEmbedUrl!, videoUrlThumb: widget.videoUrlThumb);
|
||||
}
|
||||
|
||||
Widget _carousel({required int itemCount, required NullableIndexedWidgetBuilder itemBuilder}) {
|
||||
return Stack(
|
||||
children: [
|
||||
NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleCarouselScroll,
|
||||
child: CarouselSlider.builder(
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, itemIndex, _) => itemBuilder(context, itemIndex) ?? const SizedBox.shrink(),
|
||||
options: CarouselOptions(
|
||||
viewportFraction: 1,
|
||||
height: _carouselHeight,
|
||||
enableInfiniteScroll: false,
|
||||
onPageChanged: (index, _) => widget.onGalleryChanged(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (itemCount > 1)
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
child: IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
opacity: _shouldShowCounter ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: _GalleryPhotoCounter(currentIndex: widget.currentGalleryIndex, total: itemCount),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.galleryUrls.length > 1 || (widget.galleryUrls.length == 1 && _hasVideo)) {
|
||||
final itemCount = _slideCount;
|
||||
return _carousel(
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, itemIndex) {
|
||||
if (itemIndex < widget.galleryUrls.length) {
|
||||
return _imageSlide(widget.galleryUrls[itemIndex], itemIndex);
|
||||
}
|
||||
if (_hasVideo) {
|
||||
return _videoSlide();
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.galleryUrls.length == 1) {
|
||||
return _imageSlide(widget.galleryUrls.first, 0);
|
||||
}
|
||||
|
||||
if (_hasVideo) {
|
||||
return SizedBox(
|
||||
height: _carouselHeight,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: _videoSlide(),
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.imageUrl.isNotEmpty) {
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onOpenGallery([widget.imageUrl], 0),
|
||||
child: Hero(
|
||||
tag: widget.imageUrl,
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.fitWidth,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
imageUrl: widget.imageUrl,
|
||||
errorWidget: (context, str, object) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class _GalleryPhotoCounter extends StatelessWidget {
|
||||
const _GalleryPhotoCounter({required this.currentIndex, required this.total});
|
||||
|
||||
final int currentIndex;
|
||||
final int total;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(999)),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.photo_library_outlined, size: 14, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${currentIndex + 1} / $total',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, height: 1.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:carousel_slider_plus/carousel_slider_plus.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/enums/complained_content.dart';
|
||||
@@ -15,7 +11,7 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/post_feed_mapper.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/feed_video_player/feed_video_player_widget.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_media_content.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -98,7 +94,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
const SizedBox(height: 12),
|
||||
_BlogPostHeader(post: post),
|
||||
_BlogPostTextContent(post: post),
|
||||
_BlogPostMediaContent(
|
||||
BlogPostMediaContent(
|
||||
galleryUrls: galleryUrls,
|
||||
imageUrl: imageUrl,
|
||||
videoEmbedUrl: embedUrl != null && embedUrl.isNotEmpty ? embedUrl : null,
|
||||
@@ -220,186 +216,6 @@ class _BlogPostTextContent extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _BlogPostMediaContent extends StatefulWidget {
|
||||
const _BlogPostMediaContent({
|
||||
required this.galleryUrls,
|
||||
required this.imageUrl,
|
||||
required this.videoEmbedUrl,
|
||||
required this.currentGalleryIndex,
|
||||
required this.onOpenGallery,
|
||||
required this.onGalleryChanged,
|
||||
required this.videoUrlThumb,
|
||||
});
|
||||
|
||||
final List<String> galleryUrls;
|
||||
final String imageUrl;
|
||||
final String? videoEmbedUrl;
|
||||
final String? videoUrlThumb;
|
||||
final int currentGalleryIndex;
|
||||
final void Function(List<String> urls, int index) onOpenGallery;
|
||||
final ValueChanged<int> onGalleryChanged;
|
||||
|
||||
@override
|
||||
State<_BlogPostMediaContent> createState() => _BlogPostMediaContentState();
|
||||
}
|
||||
|
||||
class _BlogPostMediaContentState extends State<_BlogPostMediaContent> {
|
||||
static const _counterDisplayDuration = Duration(seconds: 1);
|
||||
|
||||
bool _isCounterVisible = false;
|
||||
Timer? _counterHideTimer;
|
||||
|
||||
bool get _isCurrentSlideVideo =>
|
||||
widget.videoEmbedUrl != null && widget.currentGalleryIndex == widget.galleryUrls.length;
|
||||
|
||||
bool get _shouldShowCounter => !_isCurrentSlideVideo || _isCounterVisible;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_counterHideTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _BlogPostMediaContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.currentGalleryIndex != widget.currentGalleryIndex) {
|
||||
_scheduleCounterHideOnVideo();
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleCounterHideOnVideo() {
|
||||
_counterHideTimer?.cancel();
|
||||
if (!_isCurrentSlideVideo) return;
|
||||
|
||||
_counterHideTimer = Timer(_counterDisplayDuration, () {
|
||||
if (mounted && _isCurrentSlideVideo) {
|
||||
setState(() => _isCounterVisible = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _handleCarouselScroll(ScrollNotification notification) {
|
||||
if (notification is ScrollStartNotification || notification is ScrollUpdateNotification) {
|
||||
_counterHideTimer?.cancel();
|
||||
if (!_isCounterVisible) {
|
||||
setState(() => _isCounterVisible = true);
|
||||
}
|
||||
} else if (notification is ScrollEndNotification && _isCounterVisible && _isCurrentSlideVideo) {
|
||||
_scheduleCounterHideOnVideo();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasVideo = widget.videoEmbedUrl != null;
|
||||
final mediaContentCount = hasVideo ? widget.galleryUrls.length + 1 : widget.galleryUrls.length;
|
||||
final showCarousel = mediaContentCount > 1;
|
||||
return Column(
|
||||
children: [
|
||||
if (showCarousel) ...[
|
||||
Stack(
|
||||
children: [
|
||||
NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleCarouselScroll,
|
||||
child: CarouselSlider.builder(
|
||||
itemCount: mediaContentCount,
|
||||
itemBuilder: (context, itemIndex, _) {
|
||||
if (itemIndex == widget.galleryUrls.length) {
|
||||
if (widget.videoEmbedUrl != null) {
|
||||
return FeedVideoPlayer(embedUrl: widget.videoEmbedUrl!, videoUrlThumb: widget.videoUrlThumb);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final itemUrl = widget.galleryUrls[itemIndex];
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onOpenGallery(widget.galleryUrls, itemIndex),
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
imageUrl: itemUrl,
|
||||
errorWidget: (context, str, object) => const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
},
|
||||
options: CarouselOptions(
|
||||
viewportFraction: 1,
|
||||
height: 260,
|
||||
enableInfiniteScroll: false,
|
||||
onPageChanged: (index, _) => widget.onGalleryChanged(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
child: IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
opacity: _shouldShowCounter ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: _GalleryPhotoCounter(currentIndex: widget.currentGalleryIndex, total: mediaContentCount),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
] else if (widget.galleryUrls.length == 1) ...[
|
||||
GestureDetector(
|
||||
onTap: () => widget.onOpenGallery(widget.galleryUrls, 0),
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 260,
|
||||
imageUrl: widget.galleryUrls.first,
|
||||
errorWidget: (context, str, object) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
] else if (widget.imageUrl.isNotEmpty) ...[
|
||||
GestureDetector(
|
||||
onTap: () => widget.onOpenGallery([widget.imageUrl], 0),
|
||||
child: Hero(
|
||||
tag: widget.imageUrl,
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.fitWidth,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
imageUrl: widget.imageUrl,
|
||||
errorWidget: (context, str, object) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GalleryPhotoCounter extends StatelessWidget {
|
||||
const _GalleryPhotoCounter({required this.currentIndex, required this.total});
|
||||
|
||||
final int currentIndex;
|
||||
final int total;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(999)),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.photo_library_outlined, size: 14, color: Colors.white),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${currentIndex + 1} / $total',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, height: 1.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BlogPostActionsRow extends StatelessWidget {
|
||||
const _BlogPostActionsRow({required this.post, required this.isLiked, required this.totalLikes, required this.onToggleLike});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ abstract class FeedService {
|
||||
@Query('cursor') String? cursor,
|
||||
});
|
||||
|
||||
@GET('/api/blogPosts/show/{postId}')
|
||||
@GET('/api/v2/blogPosts/show/{postId}')
|
||||
Future<FeedBlogPostModel> getBlogPost({@Path('postId') required int postId});
|
||||
|
||||
@GET('/api/recipes/index')
|
||||
|
||||
@@ -7,6 +7,32 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
void main() {
|
||||
group('AppRouter', () {
|
||||
group('resolveDeepLinkTarget - blog_post', () {
|
||||
test('ru/blog_post/{id} → /blog-post-screen?id=', () {
|
||||
final target = AppRouter.resolveDeepLinkTarget(
|
||||
Uri.parse('https://eatvio.ru/ru/blog_post/123'),
|
||||
);
|
||||
|
||||
expect(target, '/blog-post-screen?id=123');
|
||||
});
|
||||
|
||||
test('en/blog_post/{id} → /blog-post-screen?id=', () {
|
||||
final target = AppRouter.resolveDeepLinkTarget(
|
||||
Uri.parse('https://eatvio.com/en/blog_post/456'),
|
||||
);
|
||||
|
||||
expect(target, '/blog-post-screen?id=456');
|
||||
});
|
||||
|
||||
test('eatvio.com/blog_post/{id} without locale → /blog-post-screen?id=', () {
|
||||
final target = AppRouter.resolveDeepLinkTarget(
|
||||
Uri.parse('https://eatvio.com/blog_post/789'),
|
||||
);
|
||||
|
||||
expect(target, '/blog-post-screen?id=789');
|
||||
});
|
||||
});
|
||||
|
||||
group('resolveDeepLinkTarget - finance/alfa/return', () {
|
||||
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
|
||||
|
||||
|
||||
@@ -51,8 +51,11 @@ void main() {
|
||||
final blogPostRoute = routes.firstWhere(
|
||||
(route) => route is GoRoute && route.path == '/blog-post-screen',
|
||||
orElse: () => throw Exception('Route not found'),
|
||||
);
|
||||
) as GoRoute;
|
||||
expect(blogPostRoute, isA<GoRoute>());
|
||||
expect(blogPostRoute.path, '/blog-post-screen');
|
||||
expect(blogPostRoute.name, 'blogPostScreen');
|
||||
expect(blogPostRoute.builder, isNotNull);
|
||||
});
|
||||
|
||||
test('aboutPlanScreen builder обрабатывает slug из query параметров', () {
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../../../fixtures/feed_fixtures.dart';
|
||||
|
||||
class MockFeedRepository extends Mock implements FeedRepository {}
|
||||
|
||||
void main() {
|
||||
@@ -29,21 +31,7 @@ void main() {
|
||||
|
||||
test('getBlogPost - успешная загрузка поста', () async {
|
||||
const postId = 1;
|
||||
final mockPost = FeedBlogPostModel(
|
||||
id: postId,
|
||||
title: 'Test Post',
|
||||
state: 'active',
|
||||
image: '',
|
||||
description: 'Test Content',
|
||||
videoUrl: '',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
totalLikes: 0,
|
||||
totalViews: 0,
|
||||
totalComments: 0,
|
||||
wasLikedByMe: false,
|
||||
author: const Author(authorName: 'Test Author', authorSlug: 'test-author', authorProfileIcon: '', authorIsMe: false),
|
||||
);
|
||||
final mockPost = FeedFixtures.feedBlogPostModel();
|
||||
|
||||
when(() => mockRepository.getBlogPost(id: postId)).thenAnswer((_) async => mockPost);
|
||||
|
||||
@@ -51,6 +39,8 @@ void main() {
|
||||
|
||||
expect(cubit.state.isLoading, false);
|
||||
expect(cubit.state.blogPostModel, mockPost);
|
||||
expect(cubit.state.blogPostModel?.images, hasLength(2));
|
||||
expect(cubit.state.blogPostModel?.videoEmbedUrl, contains('rutube.ru/play/embed'));
|
||||
verify(() => mockRepository.getBlogPost(id: postId)).called(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_media_content.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -21,6 +20,7 @@ import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:network_image_mock/network_image_mock.dart';
|
||||
|
||||
import '../../../../fixtures/feed_fixtures.dart';
|
||||
import 'blog_post_screen_smoke_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<BlogPostCubit>(), MockSpec<FeedCubit>(), MockSpec<CommentsCubit>()])
|
||||
@@ -167,21 +167,7 @@ void main() {
|
||||
final blogPostCubit = MockBlogPostCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
final blogPostModel = FeedBlogPostModel(
|
||||
id: 1,
|
||||
title: 'Test Post Title',
|
||||
description: 'Test post description',
|
||||
image: 'https://example.com/image.jpg',
|
||||
state: 'published',
|
||||
author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
|
||||
videoUrl: '',
|
||||
createdAt: DateTime(2024, 1, 15),
|
||||
updatedAt: DateTime(2024, 1, 15),
|
||||
totalLikes: 10,
|
||||
totalViews: 100,
|
||||
totalComments: 5,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
final blogPostModel = FeedFixtures.feedBlogPostModel();
|
||||
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
|
||||
|
||||
when(blogPostCubit.state).thenReturn(state);
|
||||
@@ -202,7 +188,8 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(BlogPostScreen), findsOneWidget);
|
||||
expect(find.text('Test Post Title'), findsOneWidget);
|
||||
expect(find.text('Заголовок'), findsOneWidget);
|
||||
expect(find.byType(BlogPostMediaContent), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -211,21 +198,7 @@ void main() {
|
||||
final blogPostCubit = MockBlogPostCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
final blogPostModel = FeedBlogPostModel(
|
||||
id: 1,
|
||||
title: 'Test Post Title',
|
||||
description: 'Test post description',
|
||||
image: 'https://example.com/image.jpg',
|
||||
state: 'published',
|
||||
author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
|
||||
videoUrl: '',
|
||||
createdAt: DateTime(2024, 1, 15),
|
||||
updatedAt: DateTime(2024, 1, 15),
|
||||
totalLikes: 10,
|
||||
totalViews: 100,
|
||||
totalComments: 5,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
final blogPostModel = FeedFixtures.feedBlogPostModel();
|
||||
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
|
||||
|
||||
when(blogPostCubit.state).thenReturn(state);
|
||||
@@ -254,5 +227,95 @@ void main() {
|
||||
verify(blogPostCubit.toggleLike()).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('renders gallery from fixture', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final blogPostCubit = MockBlogPostCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
final blogPostModel = FeedFixtures.feedBlogPostWithGallery();
|
||||
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
|
||||
|
||||
when(blogPostCubit.state).thenReturn(state);
|
||||
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(blogPostCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, BlogPostScreen(id: blogPostModel.id)));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(BlogPostMediaContent), findsOneWidget);
|
||||
expect(find.text('Галерея'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('renders video-only post', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final blogPostCubit = MockBlogPostCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
final blogPostModel = FeedFixtures.feedBlogPostVideoOnly();
|
||||
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
|
||||
|
||||
when(blogPostCubit.state).thenReturn(state);
|
||||
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(blogPostCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, BlogPostScreen(id: blogPostModel.id)));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(BlogPostMediaContent), findsOneWidget);
|
||||
expect(find.text('Видео'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('renders gallery and video post', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final blogPostCubit = MockBlogPostCubit();
|
||||
final feedCubit = MockFeedCubit();
|
||||
final commentsCubit = MockCommentsCubit();
|
||||
final blogPostModel = FeedFixtures.feedBlogPostGalleryAndVideo();
|
||||
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
|
||||
|
||||
when(blogPostCubit.state).thenReturn(state);
|
||||
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
|
||||
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
|
||||
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.state).thenReturn(const CommentsState());
|
||||
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
|
||||
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
|
||||
|
||||
registerMocks(blogPostCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, BlogPostScreen(id: blogPostModel.id)));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(BlogPostMediaContent), findsOneWidget);
|
||||
expect(find.text('Галерея и видео'), findsWidgets);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../fixtures/feed_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
group('FeedBlogPostModel', () {
|
||||
test('creates FeedBlogPostModel with all parameters', () {
|
||||
@@ -102,6 +104,52 @@ void main() {
|
||||
expect(model.totalLikes, 10);
|
||||
});
|
||||
|
||||
test('parses v2 show JSON with gallery images and video_url_embed', () {
|
||||
final model = FeedBlogPostModel.fromJson({
|
||||
'id': 1,
|
||||
'state': 'active',
|
||||
'image': 'media/1/cover.jpg',
|
||||
'images': [
|
||||
{'id': 10, 'url': 'media/1/a.jpg', 'position': 1, 'orientation': 'horizontal'},
|
||||
{'id': 11, 'url': 'media/1/b.jpg', 'position': 2},
|
||||
],
|
||||
'description': 'Описание',
|
||||
'author': {
|
||||
'author_slug': 'author-slug',
|
||||
'author_name': 'Автор',
|
||||
'author_profile_icon': 'icon.jpg',
|
||||
'author_is_me': false,
|
||||
},
|
||||
'video_url': 'https://rutube.ru/video/abc/',
|
||||
'video_url_embed': 'https://rutube.ru/play/embed/abc/',
|
||||
'video_url_thumb': 'media/1/thumb.jpg',
|
||||
'created_at': '2024-01-15T12:00:00Z',
|
||||
'updated_at': '2024-01-15T12:00:00Z',
|
||||
'title': 'Заголовок',
|
||||
'total_likes': 10,
|
||||
'total_views': 100,
|
||||
'total_comments': 5,
|
||||
'was_liked_by_me': false,
|
||||
});
|
||||
|
||||
expect(model.images, hasLength(2));
|
||||
expect(model.images.first.id, 10);
|
||||
expect(model.images.first.url, 'media/1/a.jpg');
|
||||
expect(model.images.first.orientation, 'horizontal');
|
||||
expect(model.images[1].orientation, 'horizontal');
|
||||
expect(model.images[1].position, 2);
|
||||
expect(model.videoEmbedUrl, 'https://rutube.ru/play/embed/abc/');
|
||||
expect(model.videoUrlThumb, 'media/1/thumb.jpg');
|
||||
});
|
||||
|
||||
test('parses fixture feedBlogPostModel with gallery and embed', () {
|
||||
final model = FeedFixtures.feedBlogPostModel();
|
||||
|
||||
expect(model.images, hasLength(2));
|
||||
expect(model.videoEmbedUrl, contains('rutube.ru/play/embed'));
|
||||
expect(model.videoUrlThumb, 'media/1/thumb.jpg');
|
||||
});
|
||||
|
||||
test('creates FeedBlogPostModel with different states', () {
|
||||
const author = Author(
|
||||
authorSlug: 'author',
|
||||
|
||||
@@ -85,6 +85,9 @@ void main() {
|
||||
|
||||
expect(result, isA<FeedBlogPostModel>());
|
||||
expect(result.id, postId);
|
||||
expect(result.images, hasLength(2));
|
||||
expect(result.videoEmbedUrl, contains('rutube.ru/play/embed'));
|
||||
expect(result.videoUrlThumb, isNotEmpty);
|
||||
verify(mockFeedService.getBlogPost(postId: postId)).called(1);
|
||||
});
|
||||
|
||||
|
||||
39
test/fixtures/feed/feed_blog_post_gallery_and_video_response.json
vendored
Normal file
39
test/fixtures/feed/feed_blog_post_gallery_and_video_response.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": 4,
|
||||
"state": "active",
|
||||
"image": "media/4/a.jpg",
|
||||
"images": [
|
||||
{
|
||||
"id": 40,
|
||||
"url": "media/4/a.jpg",
|
||||
"position": 1,
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"url": "media/4/b.jpg",
|
||||
"position": 2,
|
||||
"orientation": "horizontal"
|
||||
}
|
||||
],
|
||||
"description": "Галерея и видео",
|
||||
"author": {
|
||||
"author_slug": "author-slug",
|
||||
"author_name": "Автор",
|
||||
"author_profile_icon": "https://example.com/photo.jpg",
|
||||
"author_is_me": false,
|
||||
"is_entity_subscribed_by_me": true,
|
||||
"author_place_in_rating": null
|
||||
},
|
||||
"video_url": "https://rutube.ru/video/6a62f217aa99daf00af8fa6cebebf218/",
|
||||
"video_url_embed": "https://rutube.ru/play/embed/6a62f217aa99daf00af8fa6cebebf218/",
|
||||
"video_url_thumb": "media/4/thumb.jpg",
|
||||
"created_at": "2024-01-18T12:00:00Z",
|
||||
"updated_at": "2024-01-18T12:00:00Z",
|
||||
"title": "Галерея и видео",
|
||||
"total_likes": 12,
|
||||
"total_views": 200,
|
||||
"total_comments": 4,
|
||||
"was_liked_by_me": false,
|
||||
"is_favorite": false
|
||||
}
|
||||
45
test/fixtures/feed/feed_blog_post_gallery_response.json
vendored
Normal file
45
test/fixtures/feed/feed_blog_post_gallery_response.json
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"id": 2,
|
||||
"state": "active",
|
||||
"image": "media/2/a.jpg",
|
||||
"images": [
|
||||
{
|
||||
"id": 20,
|
||||
"url": "media/2/a.jpg",
|
||||
"position": 1,
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"url": "media/2/b.jpg",
|
||||
"position": 2,
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"url": "media/2/c.jpg",
|
||||
"position": 3,
|
||||
"orientation": "vertical"
|
||||
}
|
||||
],
|
||||
"description": "Пост только с галереей",
|
||||
"author": {
|
||||
"author_slug": "author-slug",
|
||||
"author_name": "Автор",
|
||||
"author_profile_icon": "https://example.com/photo.jpg",
|
||||
"author_is_me": false,
|
||||
"is_entity_subscribed_by_me": false,
|
||||
"author_place_in_rating": null
|
||||
},
|
||||
"video_url": "",
|
||||
"video_url_embed": null,
|
||||
"video_url_thumb": null,
|
||||
"created_at": "2024-01-16T12:00:00Z",
|
||||
"updated_at": "2024-01-16T12:00:00Z",
|
||||
"title": "Галерея",
|
||||
"total_likes": 3,
|
||||
"total_views": 40,
|
||||
"total_comments": 1,
|
||||
"was_liked_by_me": false,
|
||||
"is_favorite": false
|
||||
}
|
||||
26
test/fixtures/feed/feed_blog_post_response.json
vendored
26
test/fixtures/feed/feed_blog_post_response.json
vendored
@@ -1,7 +1,21 @@
|
||||
{
|
||||
"id": 1,
|
||||
"state": "published",
|
||||
"image": "https://example.com/image.jpg",
|
||||
"state": "active",
|
||||
"image": "media/1/cover.jpg",
|
||||
"images": [
|
||||
{
|
||||
"id": 10,
|
||||
"url": "media/1/a.jpg",
|
||||
"position": 1,
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"url": "media/1/b.jpg",
|
||||
"position": 2,
|
||||
"orientation": "vertical"
|
||||
}
|
||||
],
|
||||
"description": "Описание поста",
|
||||
"author": {
|
||||
"author_slug": "author-slug",
|
||||
@@ -11,13 +25,15 @@
|
||||
"is_entity_subscribed_by_me": null,
|
||||
"author_place_in_rating": null
|
||||
},
|
||||
"video_url": "",
|
||||
"video_url": "https://rutube.ru/video/6a62f217aa99daf00af8fa6cebebf218/",
|
||||
"video_url_embed": "https://rutube.ru/play/embed/6a62f217aa99daf00af8fa6cebebf218/",
|
||||
"video_url_thumb": "media/1/thumb.jpg",
|
||||
"created_at": "2024-01-15T12:00:00Z",
|
||||
"updated_at": "2024-01-15T12:00:00Z",
|
||||
"title": "Заголовок",
|
||||
"total_likes": 10,
|
||||
"total_views": 100,
|
||||
"total_comments": 5,
|
||||
"was_liked_by_me": false
|
||||
"was_liked_by_me": false,
|
||||
"is_favorite": false
|
||||
}
|
||||
|
||||
|
||||
26
test/fixtures/feed/feed_blog_post_video_only_response.json
vendored
Normal file
26
test/fixtures/feed/feed_blog_post_video_only_response.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": 3,
|
||||
"state": "active",
|
||||
"image": "images/element-image-placeholder.svg",
|
||||
"images": [],
|
||||
"description": "Пост только с видео",
|
||||
"author": {
|
||||
"author_slug": "author-slug",
|
||||
"author_name": "Автор",
|
||||
"author_profile_icon": "https://example.com/photo.jpg",
|
||||
"author_is_me": false,
|
||||
"is_entity_subscribed_by_me": false,
|
||||
"author_place_in_rating": null
|
||||
},
|
||||
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"video_url_embed": "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
||||
"video_url_thumb": "media/3/thumb.jpg",
|
||||
"created_at": "2024-01-17T12:00:00Z",
|
||||
"updated_at": "2024-01-17T12:00:00Z",
|
||||
"title": "Видео",
|
||||
"total_likes": 8,
|
||||
"total_views": 90,
|
||||
"total_comments": 2,
|
||||
"was_liked_by_me": true,
|
||||
"is_favorite": false
|
||||
}
|
||||
24
test/fixtures/feed_fixtures.dart
vendored
24
test/fixtures/feed_fixtures.dart
vendored
@@ -23,6 +23,18 @@ class FeedFixtures {
|
||||
return FixtureReader.readAsMap('feed/feed_blog_post_response.json');
|
||||
}
|
||||
|
||||
static Map<String, dynamic> feedBlogPostGalleryResponseJson() {
|
||||
return FixtureReader.readAsMap('feed/feed_blog_post_gallery_response.json');
|
||||
}
|
||||
|
||||
static Map<String, dynamic> feedBlogPostVideoOnlyResponseJson() {
|
||||
return FixtureReader.readAsMap('feed/feed_blog_post_video_only_response.json');
|
||||
}
|
||||
|
||||
static Map<String, dynamic> feedBlogPostGalleryAndVideoResponseJson() {
|
||||
return FixtureReader.readAsMap('feed/feed_blog_post_gallery_and_video_response.json');
|
||||
}
|
||||
|
||||
static PostFeedPaginationModel postFeedPaginationModel() {
|
||||
return PostFeedPaginationModel.fromJson(postFeedResponseJson());
|
||||
}
|
||||
@@ -48,4 +60,16 @@ class FeedFixtures {
|
||||
static FeedBlogPostModel feedBlogPostModel() {
|
||||
return FeedBlogPostModel.fromJson(feedBlogPostResponseJson());
|
||||
}
|
||||
|
||||
static FeedBlogPostModel feedBlogPostWithGallery() {
|
||||
return FeedBlogPostModel.fromJson(feedBlogPostGalleryResponseJson());
|
||||
}
|
||||
|
||||
static FeedBlogPostModel feedBlogPostVideoOnly() {
|
||||
return FeedBlogPostModel.fromJson(feedBlogPostVideoOnlyResponseJson());
|
||||
}
|
||||
|
||||
static FeedBlogPostModel feedBlogPostGalleryAndVideo() {
|
||||
return FeedBlogPostModel.fromJson(feedBlogPostGalleryAndVideoResponseJson());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user