Merge pull request 'fixed_paginatio_w_t_liked_tenders' (#92) from fixed_paginatio_w_t_liked_tenders into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/92
This commit is contained in:
a.ghabeli
2025-09-03 13:25:25 +03:30
7 changed files with 351 additions and 217 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg width="11" height="6" viewBox="0 0 11 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75977 1.71094L5.60352 5.62109C5.4668 5.75781 5.30273 5.8125 5.16602 5.8125C5.00195 5.8125 4.83789 5.75781 4.70117 5.64844L0.544922 1.71094C0.271484 1.46484 0.271484 1.05469 0.517578 0.78125C0.763672 0.507812 1.17383 0.507812 1.44727 0.753906L5.16602 4.25391L8.85742 0.753906C9.13086 0.507812 9.54102 0.507812 9.78711 0.78125C10.0332 1.05469 10.0332 1.46484 9.75977 1.71094Z" fill="black" fill-opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

+1
View File
@@ -62,5 +62,6 @@ class AssetsManager {
//Liked tenders
static const trash = 'assets/icons/trash.svg';
static const arrowdownSmall = 'assets/icons/arrow_down_small.svg';
}
+71 -54
View File
@@ -17,67 +17,75 @@ class LikedTendersViewModel with ChangeNotifier {
LikedTendersResponse? data;
int _offset = 0;
final int _limit = 20;
final int _limit = 7;
Future<void> getLikedTenders({bool reset = false}) async {
if (!reset && (isLoadingMore || !hasMore)) {
return;
}
int currentPage = 1;
if (reset) {
_offset = 0;
hasMore = true;
}
Future<void> getLikedTenders({bool reset = false}) async {
if (!reset && (isLoadingMore || !hasMore)) {
return;
}
if (reset) {
_offset = (currentPage - 1) * _limit;
hasMore = true;
data = null;
isLoading = true;
notifyListeners();
} else {
if (_offset == 0) {
isLoading = true;
} else {
isLoadingMore = true;
}
errorMessage = null;
notifyListeners();
final result = await likedTendersRepository.getLikedTenders(
offset: _offset,
limit: _limit,
);
switch (result) {
case Ok<LikedTendersResponse>():
final incoming = result.value;
final incomingList = incoming.data?.feedback ?? [];
if (reset || data == null) {
data = incoming;
} else {
final existingList = data!.data!.feedback ?? [];
final combined = List<LikedTenderFeedback>.of(existingList)
..addAll(incomingList);
final newMeta = incoming.data?.meta ?? data!.data!.meta;
data = data!.copyWith(
data: data!.data!.copyWith(feedback: combined, meta: newMeta),
);
}
// چون offset بر اساس limit حرکت می‌کنه
hasMore = incomingList.length == _limit;
_offset += _limit;
errorMessage = null;
break;
case Error<LikedTendersResponse>():
errorMessage = result.error.toString();
break;
}
isLoading = false;
isLoadingMore = false;
notifyListeners();
}
errorMessage = null;
final result = await likedTendersRepository.getLikedTenders(
offset: _offset,
limit: _limit,
);
switch (result) {
case Ok<LikedTendersResponse>():
final incoming = result.value;
final incomingList = incoming.data?.feedback ?? [];
if (reset) {
data = incoming;
} else {
final existingList = data?.data?.feedback ?? [];
final combined = List<LikedTenderFeedback>.of(existingList)
..addAll(incomingList);
data = data?.copyWith(
data: data?.data?.copyWith(
feedback: combined,
meta: incoming.data?.meta ?? data!.data!.meta,
),
);
}
hasMore = incomingList.length == _limit;
_offset += _limit;
errorMessage = null;
break;
case Error<LikedTendersResponse>():
errorMessage = result.error.toString();
break;
}
isLoading = false;
isLoadingMore = false;
notifyListeners();
}
Future<void> loadMoreIfNeeded() async {
if (isLoadingMore || !hasMore) {
return;
@@ -85,6 +93,13 @@ class LikedTendersViewModel with ChangeNotifier {
await getLikedTenders(reset: false);
}
Future<void> jumpToPage(int page) async {
currentPage = page;
_offset = (page - 1) * _limit;
hasMore = true;
await getLikedTenders(reset: true);
}
void removeTenderById(String id) {
final current = data;
final list = current?.data?.feedback;
@@ -92,14 +107,16 @@ class LikedTendersViewModel with ChangeNotifier {
return;
}
final List<LikedTenderFeedback> updatedList = List<LikedTenderFeedback>.of(
list,
)..removeWhere((e) => e.id == id);
final updatedList = List<LikedTenderFeedback>.of(list)
..removeWhere((e) => e.id == id);
final updatedMeta = current.data?.meta?.copyWith(total: updatedList.length);
data = current.copyWith(
data: current.data?.copyWith(feedback: updatedList, meta: updatedMeta),
data: current.data?.copyWith(
feedback: updatedList,
meta: updatedMeta,
),
);
notifyListeners();
@@ -19,39 +19,10 @@ class LikedTendersDesktopPage extends StatefulWidget {
}
class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(_onScroll);
}
void _onScroll() {
if (!_scrollController.hasClients) {
return;
}
final max = _scrollController.position.maxScrollExtent;
final pos = _scrollController.position.pixels;
const threshold = 200.0;
if (max - pos <= threshold) {
final vm = Provider.of<LikedTendersViewModel>(context, listen: false);
vm.loadMoreIfNeeded();
}
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.backgroundColor,
body: Column(
children: [
DesktopNavigationWidget(
@@ -72,93 +43,171 @@ class _LikedTendersDesktopPageState extends State<LikedTendersDesktopPage> {
Expanded(
child: SizedBox(
width: 740,
child: Column(
children: [
Expanded(
child: Consumer<LikedTendersViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading &&
(viewModel.data?.data?.feedback ?? []).isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
);
}
child: Consumer<LikedTendersViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading &&
(viewModel.data?.data?.feedback ?? []).isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
final feedback = viewModel.data?.data?.feedback ?? [];
final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) {
return const Center(
child: Text('چیزی برای نمایش نیست'),
);
}
if (feedback.isEmpty) {
return const Center(child: Text('Nothing to show'));
}
final itemCount =
feedback.length + (viewModel.isLoadingMore ? 1 : 0);
final currentPage = viewModel.currentPage;
final totalPages = viewModel.data?.data?.meta?.pages ?? 1;
return RefreshIndicator(
return Column(
children: [
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await viewModel.getLikedTenders(reset: true);
},
child: ListView.builder(
controller: _scrollController,
padding: EdgeInsets.symmetric(
horizontal: 24.0.w(),
vertical: 15.0.h(),
),
itemCount: itemCount,
itemCount: feedback.length,
itemBuilder: (context, index) {
if (index < feedback.length) {
final item = feedback[index];
return Padding(
padding: EdgeInsets.only(bottom: 20.0.h()),
child: Dismissible(
key: ValueKey(item.id ?? index),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(
horizontal: 20.0.w(),
),
child: SvgPicture.asset(
AssetsManager.trash,
),
final item = feedback[index];
return Padding(
padding: EdgeInsets.only(bottom: 20.0.h()),
child: Dismissible(
key: ValueKey(item.id ?? index),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(
horizontal: 20.0.w(),
),
onDismissed: (_) {
if (item.id != null) {
viewModel.removeTenderById(item.id!);
}
},
child: LikedListItem(tender: item.tender!),
),
);
} else {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: AppColors.secondary50,
strokeWidth: 2,
),
child: SvgPicture.asset(
AssetsManager.trash,
),
),
);
}
onDismissed: (_) {
if (item.id != null) {
viewModel.removeTenderById(item.id!);
}
},
child: LikedListItem(tender: item.tender!),
),
);
},
),
);
},
),
),
],
),
),
SizedBox(height: 10.0.h()),
Row(
children: [
const Spacer(),
Text(
'Page',
style: TextStyle(
fontSize: 13.0.sp(),
fontWeight: FontWeight.w300,
color: AppColors.grey60,
),
),
SizedBox(width: 10.0.w()),
GestureDetector(
onTap: () async {
final selectedPage = await showDialog<int>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Select Page'),
content: SizedBox(
width: 200,
height: 300,
child: ListView.builder(
itemCount: totalPages,
itemBuilder: (context, index) {
final pageNumber = index + 1;
return ListTile(
title: Text('Page $pageNumber'),
onTap: () =>
Navigator.pop(context, pageNumber),
);
},
),
),
);
},
);
if (selectedPage != null) {
await viewModel.jumpToPage(selectedPage);
}
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: AppColors.grey30,
width: 1,
),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
SizedBox(width: 5.0.w()),
Text(
'$currentPage',
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w500,
color: AppColors.grey70,
),
),
SizedBox(width: 5.0.w()),
SvgPicture.asset(
AssetsManager.arrowdownSmall,
),
SizedBox(width: 5.0.w()),
],
),
),
),
SizedBox(width: 5.0.w()),
Text(
'Of',
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
),
SizedBox(width: 5.0.w()),
Text(
'$totalPages',
style: TextStyle(
fontSize: 13.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
),
],
),
SizedBox(height: 30.0.h()),
],
);
},
),
),
),
@@ -71,7 +71,7 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) {
return const Center(child: Text('چیزی برای نمایش نیست'));
return const Center(child: Text('Nothing to show'));
}
final itemCount = feedback.length + (viewModel.isLoadingMore ? 1 : 0);
@@ -109,12 +109,12 @@ class _LikedTendersMobilePageState extends State<LikedTendersMobilePage> {
),
);
} else {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
return Padding(
padding: EdgeInsets.symmetric(vertical: 20.0.h()),
child: Center(
child: SizedBox(
width: 24,
height: 24,
width: 24.0.w(),
height: 24.0.h(),
child: CircularProgressIndicator(
color: AppColors.secondary50,
strokeWidth: 2,
@@ -21,34 +21,6 @@ class LikedTendersTabletPage extends StatefulWidget {
}
class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
late final ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(_onScroll);
}
void _onScroll() {
if (!_scrollController.hasClients) {
return;
}
final max = _scrollController.position.maxScrollExtent;
final pos = _scrollController.position.pixels;
const threshold = 200.0;
if (max - pos <= threshold) {
final vm = Provider.of<LikedTendersViewModel>(context, listen: false);
vm.loadMoreIfNeeded();
}
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> key = GlobalKey();
@@ -96,24 +68,26 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
final feedback = viewModel.data?.data?.feedback ?? [];
if (feedback.isEmpty) {
return const Center(child: Text('چیزی برای نمایش نیست'));
return const Center(child: Text('Nothing to show'));
}
final itemCount =
feedback.length + (viewModel.isLoadingMore ? 1 : 0);
return RefreshIndicator(
onRefresh: () async {
await viewModel.getLikedTenders(reset: true);
},
child: ListView.builder(
controller: _scrollController,
padding: EdgeInsets.symmetric(
horizontal: 24.0.w(),
vertical: 15.0.h(),
),
itemCount: itemCount,
itemBuilder: (context, index) {
if (index < feedback.length) {
final currentPage = viewModel.currentPage;
final totalPages = viewModel.data?.data?.meta?.pages ?? 1;
return Column(
children: [
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await viewModel.getLikedTenders(reset: true);
},
child: ListView.builder(
padding: EdgeInsets.symmetric(
horizontal: 24.0.w(),
vertical: 15.0.h(),
),
itemCount: feedback.length,
itemBuilder: (context, index) {
final item = feedback[index];
return Padding(
padding: EdgeInsets.only(bottom: 20.0.h()),
@@ -137,24 +111,114 @@ class _LikedTendersTabletPageState extends State<LikedTendersTabletPage> {
child: LikedListItem(tender: item.tender!),
),
);
} else {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: AppColors.secondary50,
strokeWidth: 2,
},
),
),
),
SizedBox(height: 10.0.h()),
Row(
children: [
const Spacer(),
Text(
'Page',
style: TextStyle(
fontSize: 13.0.sp(),
fontWeight: FontWeight.w300,
color: AppColors.grey60,
),
),
SizedBox(width: 10.0.w()),
GestureDetector(
onTap: () async {
final selectedPage = await showDialog<int>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Select Page'),
content: SizedBox(
width: 200,
height: 300,
child: ListView.builder(
itemCount: totalPages,
itemBuilder: (context, index) {
final pageNumber = index + 1;
return ListTile(
title: Text('Page $pageNumber'),
onTap:
() => Navigator.pop(
context,
pageNumber,
),
);
},
),
),
);
},
);
if (selectedPage != null) {
await viewModel.jumpToPage(selectedPage);
}
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: AppColors.grey30,
width: 1,
),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
SizedBox(width: 5.0.w()),
Text(
'$currentPage',
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w500,
color: AppColors.grey70,
),
),
),
);
}
},
),
); },
SizedBox(width: 5.0.w()),
SvgPicture.asset(
AssetsManager.arrowdownSmall,
),
SizedBox(width: 5.0.w()),
],
),
),
),
SizedBox(width: 5.0.w()),
Text(
'Of',
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
),
SizedBox(width: 5.0.w()),
Text(
'$totalPages',
style: TextStyle(
fontSize: 13.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
),
),
SizedBox(width: 30.0.w()),
],
),
SizedBox(height: 30.0.h()),
],
);
},
),
),
],
@@ -131,7 +131,7 @@ class _LoginDesktopPageState extends State<LoginDesktopPage> {
? Padding(
padding: EdgeInsets.symmetric(vertical: 20.0.h()),
child: ClipRRect(
borderRadius: BorderRadiusGeometry.circular(32),
borderRadius: BorderRadius.circular(32),
child: Image.asset(
AssetsManager.webLoginImage,
width: 796.0,