added comment
This commit is contained in:
parent
608222e8a3
commit
ce62c567c5
|
|
@ -8,4 +8,6 @@ class ApiConfig {
|
||||||
static const String addReservation = "/reservation/add";
|
static const String addReservation = "/reservation/add";
|
||||||
static const String getReservations = "/reservation/get";
|
static const String getReservations = "/reservation/get";
|
||||||
static const String updateFcmToken = "/user/firebaseUpdate";
|
static const String updateFcmToken = "/user/firebaseUpdate";
|
||||||
|
static const String addComment = "/comment/add";
|
||||||
|
static const String getComments = "/comment/get/";
|
||||||
}
|
}
|
||||||
|
|
@ -23,13 +23,18 @@ class CommentModel extends Equatable {
|
||||||
List<Object?> get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls];
|
List<Object?> get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls];
|
||||||
|
|
||||||
factory CommentModel.fromJson(Map<String, dynamic> json) {
|
factory CommentModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
final List<String> images = (json['UserImages'] as List<dynamic>?)
|
||||||
|
?.map((image) => image['Url'] as String)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
return CommentModel(
|
return CommentModel(
|
||||||
id: json['id'],
|
id: json['ID'] ?? '',
|
||||||
userName: json['userName'],
|
userName: json['User']?['Name'] ?? 'کاربر ناشناس',
|
||||||
rating: (json['rating'] as num).toDouble(),
|
rating: (json['Score'] as num?)?.toDouble() ?? 0.0,
|
||||||
comment: json['comment'],
|
comment: json['Text'] ?? '',
|
||||||
publishedAt: DateTime.parse(json['publishedAt']),
|
publishedAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||||
uploadedImageUrls: List<String>.from(json['uploadedImageUrls'] ?? []),
|
uploadedImageUrls: images,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:proxibuy/core/config/http_overrides.dart';
|
import 'package:proxibuy/core/config/http_overrides.dart';
|
||||||
import 'package:proxibuy/firebase_options.dart';
|
import 'package:proxibuy/firebase_options.dart';
|
||||||
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
|
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
|
||||||
|
import 'package:proxibuy/presentation/comment/bloc/comment_bloc.dart'; // این ایمپورت اضافه شد
|
||||||
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
|
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
|
||||||
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
|
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
|
||||||
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
|
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
|
||||||
|
|
@ -26,16 +27,6 @@ void main() async {
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FirebaseMessaging.instance.onTokenRefresh
|
|
||||||
// .listen((fcmToken) {
|
|
||||||
// // TODO: If necessary send token to application server.
|
|
||||||
// // Note: This callback is fired at each app startup and whenever a new
|
|
||||||
// // token is generated.
|
|
||||||
// })
|
|
||||||
// .onError((err) {
|
|
||||||
// // Error getting token.
|
|
||||||
// });
|
|
||||||
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
await initializeService();
|
await initializeService();
|
||||||
|
|
@ -62,6 +53,9 @@ class MyApp extends StatelessWidget {
|
||||||
BlocProvider<NotificationPreferencesBloc>(
|
BlocProvider<NotificationPreferencesBloc>(
|
||||||
create: (context) => NotificationPreferencesBloc(),
|
create: (context) => NotificationPreferencesBloc(),
|
||||||
),
|
),
|
||||||
|
BlocProvider<CommentBloc>(
|
||||||
|
create: (context) => CommentBloc(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'Proxibuy',
|
title: 'Proxibuy',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:proxibuy/core/config/api_config.dart';
|
||||||
|
import 'package:proxibuy/presentation/comment/bloc/comment_event.dart';
|
||||||
|
import 'package:proxibuy/presentation/comment/bloc/comment_state.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class CommentBloc extends Bloc<CommentEvent, CommentState> {
|
||||||
|
late final Dio _dio;
|
||||||
|
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||||
|
|
||||||
|
CommentBloc() : super(CommentInitial()) {
|
||||||
|
_dio = Dio();
|
||||||
|
_dio.interceptors.add(
|
||||||
|
LogInterceptor(
|
||||||
|
requestHeader: true,
|
||||||
|
requestBody: true,
|
||||||
|
responseBody: true,
|
||||||
|
responseHeader: false,
|
||||||
|
error: true,
|
||||||
|
logPrint: (obj) => debugPrint(obj.toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
on<SubmitComment>(_onSubmitComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSubmitComment(SubmitComment event, Emitter<CommentState> emit) async {
|
||||||
|
if (event.text.isEmpty && event.score == 0) {
|
||||||
|
emit(const CommentSubmissionFailure("لطفا امتیاز یا نظری برای این تخفیف ثبت کنید."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(CommentSubmitting());
|
||||||
|
try {
|
||||||
|
final token = await _storage.read(key: 'accessToken');
|
||||||
|
if (token == null) {
|
||||||
|
emit(const CommentSubmissionFailure("شما وارد نشدهاید."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
'Discount': event.discountId,
|
||||||
|
'Text': event.text,
|
||||||
|
'Score': event.score,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (File imageFile in event.images) {
|
||||||
|
formData.files.add(MapEntry(
|
||||||
|
'Images',
|
||||||
|
await MultipartFile.fromFile(
|
||||||
|
imageFile.path,
|
||||||
|
filename: imageFile.path.split('/').last,
|
||||||
|
contentType: MediaType('image', 'jpeg'),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _dio.post(
|
||||||
|
ApiConfig.baseUrl + ApiConfig.addComment,
|
||||||
|
data: formData,
|
||||||
|
options: Options(
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
emit(CommentSubmissionSuccess());
|
||||||
|
} else {
|
||||||
|
emit(CommentSubmissionFailure(response.data['message'] ?? 'خطا در ارسال نظر'));
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
emit(CommentSubmissionFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
|
||||||
|
} catch (e) {
|
||||||
|
emit(CommentSubmissionFailure('خطایی ناشناخته رخ داد: $e'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
abstract class CommentEvent extends Equatable {
|
||||||
|
const CommentEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubmitComment extends CommentEvent {
|
||||||
|
final String discountId;
|
||||||
|
final String text;
|
||||||
|
final double score;
|
||||||
|
final List<File> images;
|
||||||
|
|
||||||
|
const SubmitComment({
|
||||||
|
required this.discountId,
|
||||||
|
required this.text,
|
||||||
|
required this.score,
|
||||||
|
required this.images,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [discountId, text, score, images];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
abstract class CommentState extends Equatable {
|
||||||
|
const CommentState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommentInitial extends CommentState {}
|
||||||
|
|
||||||
|
class CommentSubmitting extends CommentState {}
|
||||||
|
|
||||||
|
class CommentSubmissionSuccess extends CommentState {}
|
||||||
|
|
||||||
|
class CommentSubmissionFailure extends CommentState {
|
||||||
|
final String error;
|
||||||
|
|
||||||
|
const CommentSubmissionFailure(this.error);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [error];
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,6 @@ class AddPhotoScreen extends StatelessWidget {
|
||||||
required this.offer,
|
required this.offer,
|
||||||
});
|
});
|
||||||
|
|
||||||
// متد ساخت توکن
|
|
||||||
Future<String> _generateQrToken(BuildContext context) async {
|
Future<String> _generateQrToken(BuildContext context) async {
|
||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
final userID = await storage.read(key: 'userID');
|
final userID = await storage.read(key: 'userID');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:proxibuy/core/config/app_colors.dart';
|
||||||
|
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||||
|
import 'package:proxibuy/presentation/comment/bloc/comment_bloc.dart';
|
||||||
|
import 'package:proxibuy/presentation/comment/bloc/comment_event.dart';
|
||||||
|
import 'package:proxibuy/presentation/comment/bloc/comment_state.dart';
|
||||||
|
import 'package:proxibuy/presentation/pages/offers_page.dart';
|
||||||
|
|
||||||
|
class CommentPage extends StatefulWidget {
|
||||||
|
final String discountId;
|
||||||
|
|
||||||
|
const CommentPage({super.key, required this.discountId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CommentPage> createState() => _CommentPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CommentPageState extends State<CommentPage> {
|
||||||
|
final _commentController = TextEditingController();
|
||||||
|
double _rating = 0.0;
|
||||||
|
final List<File> _images = [];
|
||||||
|
final ImagePicker _picker = ImagePicker();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_commentController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pickImage(ImageSource source) async {
|
||||||
|
if (_images.length >= 2) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('شما فقط میتوانید ۲ عکس اضافه کنید.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final pickedFile = await _picker.pickImage(source: source, imageQuality: 80);
|
||||||
|
if (pickedFile != null) {
|
||||||
|
setState(() {
|
||||||
|
_images.add(File(pickedFile.path));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showImageSourceActionSheet() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
|
||||||
|
),
|
||||||
|
builder: (context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Wrap(
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.photo_library, color: AppColors.primary),
|
||||||
|
title: const Text('انتخاب از گالری'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
_pickImage(ImageSource.gallery);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.camera_alt, color: AppColors.primary),
|
||||||
|
title: const Text('گرفتن عکس با دوربین'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
_pickImage(ImageSource.camera);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => CommentBloc(),
|
||||||
|
child: Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.asset(Assets.images.userinfo.path, fit: BoxFit.cover),
|
||||||
|
),
|
||||||
|
DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.65,
|
||||||
|
minChildSize: 0.65,
|
||||||
|
maxChildSize: 0.65,
|
||||||
|
builder: (context, scrollController) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 50,
|
||||||
|
height: 5,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
'خریدت با موفقیت انجام شد. منتظر دیدار دوبارهات هستیم. لطفا نظرت رو در مورد این تخفیف بهمون بگو.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Center(
|
||||||
|
child: RatingBar.builder(
|
||||||
|
initialRating: 0,
|
||||||
|
minRating: 1,
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
itemCount: 5,
|
||||||
|
itemPadding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||||
|
itemBuilder: (context, _) => Icon(
|
||||||
|
Icons.star,
|
||||||
|
color: Colors.amber.shade700,
|
||||||
|
),
|
||||||
|
onRatingUpdate: (rating) => setState(() => _rating = rating),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
TextField(
|
||||||
|
controller: _commentController,
|
||||||
|
maxLines: 4,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "گوشمون به شماست",
|
||||||
|
hintText: "نظراتت رو بگو...",
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: IconButton(
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
Assets.icons.galleryAdd.path,
|
||||||
|
color: _images.length >= 2 ? Colors.grey : AppColors.primary,
|
||||||
|
),
|
||||||
|
onPressed: _images.length >= 2 ? null : _showImageSourceActionSheet,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (_images.isNotEmpty)
|
||||||
|
SizedBox(
|
||||||
|
height: 80,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: _images.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
child: Image.file(
|
||||||
|
_images[index],
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildActionButtons(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButtons() {
|
||||||
|
return BlocConsumer<CommentBloc, CommentState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state is CommentSubmissionSuccess) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('نظر شما با موفقیت ثبت شد. ممنونیم!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(builder: (_) => const OffersPage()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
} else if (state is CommentSubmissionFailure) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(state.error), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
final isLoading = state is CommentSubmitting;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: isLoading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
context.read<CommentBloc>().add(
|
||||||
|
SubmitComment(
|
||||||
|
discountId: widget.discountId,
|
||||||
|
text: _commentController.text,
|
||||||
|
score: _rating,
|
||||||
|
images: _images,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.confirm,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
|
||||||
|
),
|
||||||
|
child: isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
)
|
||||||
|
: const Text('ارسال'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextButton(
|
||||||
|
onPressed: isLoading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
Navigator.of(context).pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(builder: (_) => const OffersPage()),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text('رد شدن', style: TextStyle(color: Colors.black)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
@ -11,6 +12,7 @@ import 'package:maps_launcher/maps_launcher.dart';
|
||||||
import 'package:proxibuy/core/config/api_config.dart';
|
import 'package:proxibuy/core/config/api_config.dart';
|
||||||
import 'package:proxibuy/core/config/app_colors.dart';
|
import 'package:proxibuy/core/config/app_colors.dart';
|
||||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||||
|
import 'package:proxibuy/data/models/comment_model.dart';
|
||||||
import 'package:proxibuy/data/models/offer_model.dart';
|
import 'package:proxibuy/data/models/offer_model.dart';
|
||||||
import 'package:proxibuy/presentation/pages/add_photo_screen.dart';
|
import 'package:proxibuy/presentation/pages/add_photo_screen.dart';
|
||||||
import 'package:proxibuy/presentation/pages/reservation_details_screen.dart';
|
import 'package:proxibuy/presentation/pages/reservation_details_screen.dart';
|
||||||
|
|
@ -87,20 +89,9 @@ class ProductDetailPage extends StatelessWidget {
|
||||||
headers: {'Authorization': 'Bearer $token'},
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint("----------- REQUEST-----------");
|
|
||||||
debugPrint("URL: POST $url");
|
|
||||||
debugPrint("Headers: ${options.headers}");
|
|
||||||
debugPrint("Body: $data");
|
|
||||||
debugPrint("-----------------------------");
|
|
||||||
|
|
||||||
final response =
|
final response =
|
||||||
await dio.post(url, data: data, options: options);
|
await dio.post(url, data: data, options: options);
|
||||||
|
|
||||||
debugPrint("---------- RESPONSE-----------");
|
|
||||||
debugPrint("StatusCode: ${response.statusCode}");
|
|
||||||
debugPrint("Data: ${response.data}");
|
|
||||||
debugPrint("-----------------------------");
|
|
||||||
|
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
|
@ -131,11 +122,6 @@ class ProductDetailPage extends StatelessWidget {
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
|
|
||||||
debugPrint("---------- ERROR-----------");
|
|
||||||
debugPrint("StatusCode: ${e.response?.statusCode}");
|
|
||||||
debugPrint("Data: ${e.response?.data}");
|
|
||||||
debugPrint("--------------------------");
|
|
||||||
|
|
||||||
final errorMessage = e.response?.data?['message'] ??
|
final errorMessage = e.response?.data?['message'] ??
|
||||||
'خطای سرور هنگام رزرو. لطفاً دوباره تلاش کنید.';
|
'خطای سرور هنگام رزرو. لطفاً دوباره تلاش کنید.';
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -145,10 +131,6 @@ class ProductDetailPage extends StatelessWidget {
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
debugPrint("---------- GENERAL ERROR -----------");
|
|
||||||
debugPrint(e.toString());
|
|
||||||
debugPrint("------------------------------------");
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(e.toString()),
|
content: Text(e.toString()),
|
||||||
|
|
@ -205,14 +187,56 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
||||||
late List<String> imageList;
|
late List<String> imageList;
|
||||||
late String selectedImage;
|
late String selectedImage;
|
||||||
final String _uploadKey = 'upload_image';
|
final String _uploadKey = 'upload_image';
|
||||||
|
late Future<List<CommentModel>> _commentsFuture;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
imageList = List.from(widget.offer.imageUrls)..add(_uploadKey);
|
imageList = List.from(widget.offer.imageUrls)..add(_uploadKey);
|
||||||
selectedImage = imageList.first;
|
selectedImage = imageList.isNotEmpty ? imageList.first : 'https://via.placeholder.com/400x200.png?text=No+Image';
|
||||||
|
_commentsFuture = _fetchComments();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<CommentModel>> _fetchComments() async {
|
||||||
|
// 1. توکن را از حافظه امن بخوان
|
||||||
|
const storage = FlutterSecureStorage();
|
||||||
|
final token = await storage.read(key: 'accessToken');
|
||||||
|
|
||||||
|
// 2. اگر توکن وجود نداشت، خطا برگردان
|
||||||
|
// هرچند کاربر لاگین نکرده معمولا به این صفحه دسترسی ندارد
|
||||||
|
if (token == null) {
|
||||||
|
throw Exception('Authentication token not found!');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final dio = Dio();
|
||||||
|
// 3. هدر Authorization را به درخواست اضافه کن
|
||||||
|
final response = await dio.get(
|
||||||
|
ApiConfig.baseUrl + ApiConfig.getComments + widget.offer.id,
|
||||||
|
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final List<dynamic> commentsJson = response.data['data']['comments'];
|
||||||
|
return commentsJson.map((json) => CommentModel.fromJson(json)).toList();
|
||||||
|
} else {
|
||||||
|
// خطاهای دیگر سرور
|
||||||
|
throw Exception('Failed to load comments with status code: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
// چاپ خطای کامل Dio برای دیباگ بهتر
|
||||||
|
debugPrint("DioException fetching comments: $e");
|
||||||
|
if (e.response != null) {
|
||||||
|
debugPrint("Response data: ${e.response?.data}");
|
||||||
|
}
|
||||||
|
throw Exception('Failed to load comments: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error fetching comments: $e");
|
||||||
|
throw Exception('An unknown error occurred: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ############ END: FIX SECTION ############
|
||||||
|
|
||||||
void _launchMaps(double lat, double lon, String title) {
|
void _launchMaps(double lat, double lon, String title) {
|
||||||
MapsLauncher.launchCoordinates(lat, lon, title);
|
MapsLauncher.launchCoordinates(lat, lon, title);
|
||||||
}
|
}
|
||||||
|
|
@ -405,8 +429,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
||||||
? widget.offer.expiryTime.difference(DateTime.now())
|
? widget.offer.expiryTime.difference(DateTime.now())
|
||||||
: Duration.zero;
|
: Duration.zero;
|
||||||
|
|
||||||
final formatCurrency =
|
final formatCurrency = NumberFormat.decimalPattern('fa_IR');
|
||||||
NumberFormat.decimalPattern('fa_IR'); // Or 'en_US'
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0)
|
padding: const EdgeInsets.symmetric(horizontal: 24.0)
|
||||||
|
|
@ -687,7 +710,26 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildDiscountTypeSection(),
|
_buildDiscountTypeSection(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
CommentsSection(comments: widget.offer.comments),
|
FutureBuilder<List<CommentModel>>(
|
||||||
|
future: _commentsFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text('خطا در بارگذاری نظرات. لطفاً صفحه را رفرش کنید.'));
|
||||||
|
}
|
||||||
|
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 32.0),
|
||||||
|
child: Text('هنوز نظری برای این تخفیف ثبت نشده است.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CommentsSection(comments: snapshot.data!);
|
||||||
|
},
|
||||||
|
),
|
||||||
].animate(interval: 80.ms).slideX(begin: -0.05).fadeIn(
|
].animate(interval: 80.ms).slideX(begin: -0.05).fadeIn(
|
||||||
duration: 400.ms,
|
duration: 400.ms,
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:audioplayers/audioplayers.dart';
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:proxibuy/core/config/app_colors.dart';
|
import 'package:proxibuy/core/config/app_colors.dart';
|
||||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||||
import 'package:proxibuy/data/models/offer_model.dart';
|
import 'package:proxibuy/data/models/offer_model.dart';
|
||||||
|
import 'package:proxibuy/presentation/pages/comment_page.dart';
|
||||||
|
import 'package:proxibuy/services/mqtt_service.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
|
||||||
|
|
@ -30,17 +33,17 @@ class _ReservationConfirmationPageState
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
Duration _remaining = Duration.zero;
|
Duration _remaining = Duration.zero;
|
||||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||||
|
StreamSubscription? _mqttSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_playSound();
|
_playSound();
|
||||||
|
|
||||||
_calculateRemainingTime();
|
_calculateRemainingTime();
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
_calculateRemainingTime();
|
_calculateRemainingTime();
|
||||||
});
|
});
|
||||||
|
_listenToMqtt();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _playSound() async {
|
void _playSound() async {
|
||||||
|
|
@ -69,14 +72,46 @@ class _ReservationConfirmationPageState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _listenToMqtt() async {
|
||||||
|
final mqttService = context.read<MqttService>();
|
||||||
|
const storage = FlutterSecureStorage();
|
||||||
|
final userID = await storage.read(key: 'userID');
|
||||||
|
final discountId = widget.offer.id;
|
||||||
|
|
||||||
|
if (userID == null) {
|
||||||
|
debugPrint("MQTT Listener: UserID not found, cannot subscribe.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final topic = 'user-order/$userID/$discountId';
|
||||||
|
mqttService.subscribe(topic);
|
||||||
|
debugPrint("✅ Subscribed to MQTT topic: $topic");
|
||||||
|
|
||||||
|
_mqttSubscription = mqttService.messages.listen((message) {
|
||||||
|
debugPrint("✅ MQTT Message received on details page: $message");
|
||||||
|
final receivedDiscountId = message['Discount'];
|
||||||
|
if (receivedDiscountId == discountId) {
|
||||||
|
if (mounted) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => CommentPage(discountId: discountId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_audioPlayer.dispose();
|
_audioPlayer.dispose();
|
||||||
|
_mqttSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Directionality(
|
return Directionality(
|
||||||
|
|
@ -227,7 +262,7 @@ class _ReservationConfirmationPageState
|
||||||
SvgPicture.asset(Assets.icons.ticketDiscount.path),
|
SvgPicture.asset(Assets.icons.ticketDiscount.path),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'(${(100-widget.offer.finalPrice/widget.offer.originalPrice*100).toInt()}%)',
|
'(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: AppColors.singleOfferType,
|
color: AppColors.singleOfferType,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||||
|
import 'package:proxibuy/presentation/pages/comment_page.dart';
|
||||||
|
import 'package:proxibuy/services/mqtt_service.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:proxibuy/core/config/app_colors.dart';
|
import 'package:proxibuy/core/config/app_colors.dart';
|
||||||
import 'package:proxibuy/data/models/offer_model.dart';
|
import 'package:proxibuy/data/models/offer_model.dart';
|
||||||
|
|
@ -25,6 +29,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
Duration _remaining = Duration.zero;
|
Duration _remaining = Duration.zero;
|
||||||
Future<String>? _qrTokenFuture;
|
Future<String>? _qrTokenFuture;
|
||||||
|
StreamSubscription? _mqttSubscription; // برای مدیریت لیسنر MQTT
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -57,11 +62,53 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleExpansion() {
|
void _toggleExpansion() {
|
||||||
|
final isExpired = _remaining <= Duration.zero;
|
||||||
|
if (isExpired) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isExpanded = !_isExpanded;
|
_isExpanded = !_isExpanded;
|
||||||
if (_isExpanded && _qrTokenFuture == null) {
|
if (_isExpanded) {
|
||||||
|
if (_qrTokenFuture == null) {
|
||||||
_qrTokenFuture = _generateQrToken();
|
_qrTokenFuture = _generateQrToken();
|
||||||
}
|
}
|
||||||
|
_listenToMqtt();
|
||||||
|
} else {
|
||||||
|
_mqttSubscription?.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenToMqtt() async {
|
||||||
|
final mqttService = context.read<MqttService>();
|
||||||
|
const storage = FlutterSecureStorage();
|
||||||
|
final userID = await storage.read(key: 'userID');
|
||||||
|
final discountId = widget.offer.id;
|
||||||
|
|
||||||
|
if (userID == null) {
|
||||||
|
debugPrint("MQTT Listener: UserID not found, cannot subscribe.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final topic = 'user-order/$userID/$discountId';
|
||||||
|
mqttService.subscribe(topic);
|
||||||
|
debugPrint("✅ Subscribed to MQTT topic: $topic");
|
||||||
|
|
||||||
|
|
||||||
|
_mqttSubscription = mqttService.messages.listen((message) {
|
||||||
|
debugPrint("✅ MQTT Message received on reserved card: $message");
|
||||||
|
final receivedDiscountId = message['Discount'];
|
||||||
|
|
||||||
|
if (receivedDiscountId == discountId) {
|
||||||
|
if (mounted) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => CommentPage(discountId: discountId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +131,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
_mqttSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,13 +152,15 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: _buildOfferPrimaryDetails(),
|
child: _buildOfferPrimaryDetails(),
|
||||||
),
|
),
|
||||||
_buildActionsRow(),
|
_buildActionsRow(isExpired),
|
||||||
_buildExpansionPanel(),
|
if (!isExpired) _buildExpansionPanel(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isExpired) {
|
return Stack(
|
||||||
return ColorFiltered(
|
children: [
|
||||||
|
if (isExpired)
|
||||||
|
ColorFiltered(
|
||||||
colorFilter: const ColorFilter.matrix(<double>[
|
colorFilter: const ColorFilter.matrix(<double>[
|
||||||
0.2126, 0.7152, 0.0722, 0, 0,
|
0.2126, 0.7152, 0.0722, 0, 0,
|
||||||
0.2126, 0.7152, 0.0722, 0, 0,
|
0.2126, 0.7152, 0.0722, 0, 0,
|
||||||
|
|
@ -118,12 +168,33 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
0, 0, 0, 1, 0,
|
0, 0, 0, 1, 0,
|
||||||
]),
|
]),
|
||||||
child: cardContent,
|
child: cardContent,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
cardContent,
|
||||||
|
if (isExpired)
|
||||||
|
Positioned(
|
||||||
|
top: 12,
|
||||||
|
left: -35,
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: -45 * (3.1415926535 / 180),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 4),
|
||||||
|
color: Colors.red,
|
||||||
|
child: const Text(
|
||||||
|
'منقضی شده',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cardContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildOfferPrimaryDetails() {
|
Widget _buildOfferPrimaryDetails() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(15, 25, 15, 25),
|
padding: const EdgeInsets.fromLTRB(15, 25, 15, 25),
|
||||||
|
|
@ -195,13 +266,13 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionsRow() {
|
Widget _buildActionsRow(bool isExpired) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
if (_remaining > Duration.zero)
|
if (!isExpired)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Localizations.override(
|
Localizations.override(
|
||||||
|
|
@ -220,7 +291,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
color: AppColors.countdown,
|
color: AppColors.countdown,
|
||||||
),
|
),
|
||||||
decoration: const BoxDecoration(color: Colors.white),
|
decoration: const BoxDecoration(color: Colors.transparent),
|
||||||
shouldShowDays: (d) => d.inDays > 0,
|
shouldShowDays: (d) => d.inDays > 0,
|
||||||
shouldShowHours: (d) => d.inHours > 0,
|
shouldShowHours: (d) => d.inHours > 0,
|
||||||
shouldShowMinutes: (d) => d.inSeconds > 0,
|
shouldShowMinutes: (d) => d.inSeconds > 0,
|
||||||
|
|
@ -231,15 +302,9 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const Text(
|
const SizedBox(height: 0),
|
||||||
'منقضی شده',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
|
if (!isExpired)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _toggleExpansion,
|
onPressed: _toggleExpansion,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -324,6 +389,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
|
|
||||||
Widget _buildExpansionPanel() {
|
Widget _buildExpansionPanel() {
|
||||||
final formatCurrency = NumberFormat.decimalPattern('fa_IR');
|
final formatCurrency = NumberFormat.decimalPattern('fa_IR');
|
||||||
|
final isExpired = _remaining <= Duration.zero;
|
||||||
|
|
||||||
return AnimatedCrossFade(
|
return AnimatedCrossFade(
|
||||||
firstChild: Container(),
|
firstChild: Container(),
|
||||||
|
|
@ -395,6 +461,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (!isExpired) ...[
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
|
@ -429,6 +496,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class CustomStarRating extends StatelessWidget {
|
||||||
stars.add(_buildStar(Assets.icons.star2.path));
|
stars.add(_buildStar(Assets.icons.star2.path));
|
||||||
remaining = 0;
|
remaining = 0;
|
||||||
} else {
|
} else {
|
||||||
stars.add(_buildStar(Assets.icons.starHalf.path,));
|
stars.add(_buildStar(Assets.icons.star2.path,));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Row(mainAxisSize: MainAxisSize.min, children: stars);
|
return Row(mainAxisSize: MainAxisSize.min, children: stars);
|
||||||
|
|
|
||||||
|
|
@ -739,6 +739,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.28"
|
version: "2.0.28"
|
||||||
|
flutter_rating_bar:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_rating_bar
|
||||||
|
sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.1"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ dependencies:
|
||||||
workmanager: ^0.7.0
|
workmanager: ^0.7.0
|
||||||
firebase_messaging: ^15.2.10
|
firebase_messaging: ^15.2.10
|
||||||
firebase_crashlytics: ^4.3.10
|
firebase_crashlytics: ^4.3.10
|
||||||
|
flutter_rating_bar: ^4.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue