added deeplink for diets

This commit is contained in:
Username1548
2025-06-25 22:43:29 +07:00
parent afc36e6b63
commit 5f57bded51
8 changed files with 150 additions and 86 deletions

View File

@@ -91,7 +91,7 @@ class AppRouter {
case 'plan':
return '/about-plan-screen?slug=$contentIdentifier';
case 'diet':
return '/diet?slug=$contentIdentifier';
return '/about-diet-screen?slug=$contentIdentifier';
}
}
@@ -262,7 +262,7 @@ class AppRouter {
index: null,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
@@ -306,8 +306,16 @@ class AppRouter {
path: '/about-diet-screen',
name: 'aboutDietScreen',
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutDietScreen(
slug: slug,
index: null,
navigateToComments: false,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutDietScreen(diet: data['diet'] as Diet, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(

View File

@@ -6,6 +6,7 @@ import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_state.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -16,10 +17,11 @@ class AboutDietCubit extends Cubit<AboutDietState> {
bool isCurrentDietStarted = false;
bool hasStartedDiet = false;
String? userStartedDietName;
List<int> _selectedIndexes = [];
Future<void> getDiet(int id, {int? index}) async {
Future<void> getDiet(String slug, {int? index}) async {
try {
final diet = await dietsRepository.getDiet(id);
final diet = await dietsRepository.getDietBySlug(slug);
if (index != null) {
dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments));
}
@@ -30,10 +32,10 @@ class AboutDietCubit extends Cubit<AboutDietState> {
}
}
Future<void> initDietPage(int id) async {
Future<void> initDietPage(String slug) async {
try {
emit(const AboutDietState.loading());
final diet = await dietsRepository.getDiet(id);
final diet = await dietsRepository.getDietBySlug(slug);
final chosenDiets = await profileRepository.getChosenDiets();
final lastChosenDiet = chosenDiets.firstOrNull;
if (lastChosenDiet != null && ['inited', 'started'].contains(lastChosenDiet.state)) {
@@ -47,7 +49,13 @@ class AboutDietCubit extends Cubit<AboutDietState> {
}
}
}
emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
_selectedIndexes = List.generate(diet.dailySchedule.length, (index) => 0);
emit(AboutDietState.loaded(
diet: diet,
isStarted: isCurrentDietStarted,
hasStartedDiet: hasStartedDiet,
selectedIndexes: _selectedIndexes,
));
} catch (e) {
log(e.toString());
emit(AboutDietState.error(message: e.toString()));
@@ -67,7 +75,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
}
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes));
} else {
final updatedDiet = currentDiet.copyWith(
totalLikes: currentDiet.totalLikes + 1,
@@ -76,7 +84,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
}
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes));
}
EasyDebounce.debounce(
@@ -91,7 +99,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
dietsRepository
.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed));
}
emit(AboutDietState.loaded(diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
}
},
);
@@ -108,7 +116,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
EasyDebounce.debounce(
'add_or_remove_diet_to_favorites',
@@ -122,7 +130,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
}
},
);
@@ -139,7 +147,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
EasyDebounce.debounce(
'add_or_remove_diet_to_favorites',
@@ -153,7 +161,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
}
},
);
@@ -191,12 +199,31 @@ class AboutDietCubit extends Cubit<AboutDietState> {
void clearStartedDietsState() {
if (state is! DietStateLoaded) return;
final diet = (state as DietStateLoaded).diet;
emit(AboutDietState.loaded(diet: diet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet));
}
void emitCurrentStartedDietsState() {
if (state is! DietStateLoaded) return;
final diet = (state as DietStateLoaded).diet;
emit(AboutDietState.loaded(diet: diet, isStarted: true));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet, isStarted: true));
}
Future<Diet?> getDietBySlug(String slug) async {
try {
return await dietsRepository.getDietBySlug(slug);
} catch (e) {
log(e.toString());
}
return null;
}
void setSelectedIndex({required int dayNumber, required int index}) {
final currentState = state;
if (currentState is DietStateLoaded) {
final updatedSelectedIndexes = [...currentState.selectedIndexes];
updatedSelectedIndexes[dayNumber] = index;
_selectedIndexes = updatedSelectedIndexes;
emit(currentState.copyWith(selectedIndexes: _selectedIndexes));
}
}
}

View File

@@ -11,6 +11,7 @@ abstract class AboutDietState with _$AboutDietState {
const factory AboutDietState.loading() = DietStateLoading;
const factory AboutDietState.loaded({
required Diet diet,
@Default([]) List<int> selectedIndexes,
@Default(false) bool isStarted,
@Default(false) bool hasStartedDiet,
}) = DietStateLoaded;

View File

@@ -32,16 +32,14 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
class AboutDietScreen extends StatefulWidget {
final Diet diet;
final String slug;
final int? index;
final bool navigateToComments;
late final List<int> selectedIndexes;
AboutDietScreen({super.key, required this.diet, required this.index, required this.navigateToComments}) {
selectedIndexes = List.generate(diet.dailySchedule.length, (_) => 0);
}
const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug});
@override
State<AboutDietScreen> createState() => _AboutDietScreenState();
@@ -55,11 +53,11 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
final textEditingController = TextEditingController();
bool isMoreOptionsVisible = false;
bool _jumped = false;
bool _isFirstLaunch = true;
@override
void initState() {
super.initState();
context.read<FeedCubit>().viewDiet(widget.diet.id);
scrollController.addListener(() {
if (isMoreOptionsVisible) {
setState(() {
@@ -79,15 +77,11 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocProvider(
create: (context) => dietsCubit..initDietPage(widget.diet.id),
create: (context) => dietsCubit..initDietPage(widget.slug),
child: BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit
..getComments(
widget.diet.id.toString(),
CommentSourceType.diet,
true,
),
create: (BuildContext context) => commentsCubit,
child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState commentsState) {
if (commentsState is CommentsStateLoaded) {
@@ -100,6 +94,15 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
return BlocBuilder<AboutDietCubit, AboutDietState>(
builder: (context, dietState) {
if (dietState is DietStateLoaded) {
if (_isFirstLaunch) {
context.read<FeedCubit>().viewDiet(dietState.diet.id);
context.read<CommentsCubit>().getComments(
dietState.diet.id.toString(),
CommentSourceType.diet,
true,
);
_isFirstLaunch = false;
}
Future.delayed(const Duration(milliseconds: 500), () {
if (widget.navigateToComments && !_jumped) {
_scrollToBottom();
@@ -176,9 +179,9 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
isFavorite: dietState.diet.isFavorite,
onTap: () {
if (dietState.diet.isFavorite) {
dietsCubit.removeFromFavorite(widget.diet.id, widget.index);
dietsCubit.removeFromFavorite(dietState.diet.id, widget.index);
} else {
dietsCubit.addToFavorite(widget.diet.id, widget.index);
dietsCubit.addToFavorite(dietState.diet.id, widget.index);
}
},
width: 52,
@@ -269,7 +272,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.diet.id.toString(),
id: dietState.diet.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.diet,
@@ -280,7 +283,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
replyId = null;
});
Future.delayed(const Duration(milliseconds: 500), () {
dietsCubit.getDiet(widget.diet.id, index: widget.index);
dietsCubit.getDiet(dietState.diet.slug, index: widget.index);
});
FocusManager.instance.primaryFocus?.unfocus();
}
@@ -307,8 +310,17 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
),
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
widget.diet.name,
dietState.diet.name,
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
@@ -317,11 +329,11 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
),
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}',
isAuthor: dietState.diet.author.authorIsMe,
path: 'diet/${dietState.diet.slug}',
complainedContentType: ComplainedContent.diet,
id: widget.diet.id,
diet: widget.diet,
id: dietState.diet.id,
diet: dietState.diet,
),
const SizedBox(width: 16),
],
@@ -340,7 +352,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
Stack(
children: [
CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.diet.image}',
imageUrl: '$baseUrl/storage/${dietState.diet.image}',
height: 300,
width: MediaQuery.sizeOf(context).width,
fit: BoxFit.fill,
@@ -366,7 +378,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
const SizedBox(width: 12),
GestureDetector(
onTap: () {
dietsCubit.likeDiet(widget.diet.id, widget.index);
dietsCubit.likeDiet(dietState.diet.id, widget.index);
},
child: InfoBouble(
backgroundColor: appTheme.whiteA700,
@@ -412,17 +424,17 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
),
const SizedBox(height: 12),
InfoRow(
diet: widget.diet,
diet: dietState.diet,
type: ProductType.diet,
),
ExpandableText(
text: widget.diet.details,
text: dietState.diet.details,
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AuthorCard(
author: widget.diet.author,
author: dietState.diet.author,
title: s.diet_author,
),
),
@@ -436,7 +448,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Text(
'${s.diet_plan}(${widget.diet.duration.toString()} ${getDayForm(widget.diet.duration, s)})',
'${s.diet_plan}(${dietState.diet.duration.toString()} ${getDayForm(dietState.diet.duration, s)})',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
@@ -449,9 +461,15 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
return _dayOfPlan(widget.diet.dailySchedule[index + 1] ?? {}, index, s);
return _dayOfPlan(
dietState.diet.dailySchedule[index + 1] ?? {},
index,
s,
dietState.selectedIndexes,
dietState.diet,
);
},
itemCount: widget.diet.dailySchedule.length,
itemCount: dietState.diet.dailySchedule.length,
),
MainBackgroundBouble(
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -471,7 +489,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded)
CommentsWidget(
id: widget.diet.id.toString(),
id: dietState.diet.id.toString(),
commentSourceType: CommentSourceType.diet,
comment: state.comment,
replyId: replyId,
@@ -488,51 +506,49 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
widget.diet.name,
'',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
id: widget.diet.id,
diet: widget.diet,
),
const SizedBox(width: 16),
],
),
body: DataErrorWidget(
onReload: () => context.read<AboutDietCubit>().getDiet(widget.diet.id, index: widget.index),
onReload: () => context.read<AboutDietCubit>().getDiet(widget.slug, index: widget.index),
),
);
}
return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
widget.diet.name,
'',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
actions: [
CustomPopup(
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
id: widget.diet.id,
diet: widget.diet,
),
const SizedBox(width: 16),
],
),
body: const Loader(),
);
@@ -544,7 +560,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
);
}
_dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s) {
_dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s, List<int> selectedIndexes, Diet diet) {
choiceChipWidget(int index) {
return ChoiceChip(
selectedColor: appTheme.blueA400,
@@ -557,14 +573,12 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
label: Text((index + 1).toString()),
labelStyle: TextStyles.textXSSemibold.copyWith(
color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600,
color: index == selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600,
),
selected: widget.selectedIndexes[dayNumber] == index,
selected: selectedIndexes[dayNumber] == index,
backgroundColor: appTheme.gray100,
onSelected: (bool selected) {
setState(() {
widget.selectedIndexes[dayNumber] = index;
});
dietsCubit.setSelectedIndex(dayNumber: dayNumber, index: index);
},
);
}
@@ -612,18 +626,18 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _daily(day[widget.selectedIndexes[dayNumber] + 1]?[index + 1], s),
itemCount: day[widget.selectedIndexes[dayNumber] + 1]?.length ?? 0,
itemBuilder: (context, index) => _daily(day[selectedIndexes[dayNumber] + 1]?[index + 1], s),
itemCount: day[selectedIndexes[dayNumber] + 1]?.length ?? 0,
),
const SizedBox(height: 8),
],
),
),
CpfcContainer(
calories: widget.diet.dailyNutrition[dayNumber].energy,
proteins: widget.diet.dailyNutrition[dayNumber].proteins,
fats: widget.diet.dailyNutrition[dayNumber].fats,
carbohydrates: widget.diet.dailyNutrition[dayNumber].carbohydrates,
calories: diet.dailyNutrition[dayNumber].energy,
proteins: diet.dailyNutrition[dayNumber].proteins,
fats: diet.dailyNutrition[dayNumber].fats,
carbohydrates: diet.dailyNutrition[dayNumber].carbohydrates,
),
],
),

View File

@@ -33,6 +33,15 @@ class DietRepository {
}
}
Future<Diet> getDietBySlug(String slug) async {
try {
final diet = await dietsService.getDietBySlug(slug: slug);
return diet;
} catch (e) {
rethrow;
}
}
Future<Diet> addToFavorite(int id) async {
try {
return await dietsService.addToFavorite(id: id);

View File

@@ -29,6 +29,11 @@ abstract class DietService {
@Path() required int id,
});
@GET('/api/diets/show_by_slug/{slug}')
Future<Diet> getDietBySlug({
@Path() required String slug,
});
@POST('/api/diets/{id}/start')
Future<void> startDiet({
@Path() required int id,

View File

@@ -39,7 +39,7 @@ class _DietsWidgetState extends State<DietsWidget> {
onTap: () => GoRouter.of(context).pushNamed(
'aboutDietScreen',
extra: {
'diet': widget.diet,
'slug': widget.diet.slug,
'index': widget.index,
'navigateToComments': false,
},
@@ -177,7 +177,7 @@ class _DietsWidgetState extends State<DietsWidget> {
icon: Icons.chat_bubble_outline,
text: widget.diet.totalComments.toString(),
onTap: () =>
GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'index': widget.index, 'navigateToComments': true}),
GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': true}),
),
const SizedBox(
width: 12,

View File

@@ -52,7 +52,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
Stack(
children: [
InkWell(
onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'navigateToComments': false}),
onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(