updated iternary api and updated buy pass flow
This commit is contained in:
@@ -27,10 +27,11 @@ class BuyPassRepository {
|
||||
required int totalChild,
|
||||
required int noOfAttractions,
|
||||
required int noOfDays,
|
||||
required double baseAmount,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiService.postApi(
|
||||
url: ApiUrls.addToCartPasses, // add this key in ApiUrls
|
||||
url: ApiUrls.addToCartPasses,
|
||||
data: {
|
||||
"cityXid": cityXid,
|
||||
"cardTypeXid": cardTypeXid,
|
||||
@@ -38,6 +39,8 @@ class BuyPassRepository {
|
||||
"cardMode": cardMode,
|
||||
"totalAdult": totalAdult,
|
||||
"totalChild": totalChild,
|
||||
"baseAmount": baseAmount,
|
||||
"taxAmount": 2, // Fixed tax amount
|
||||
"noOfAttractions": noOfAttractions,
|
||||
"noOfDays": noOfDays,
|
||||
},
|
||||
@@ -48,4 +51,4 @@ class BuyPassRepository {
|
||||
throw Exception('Failed to add passes to cart: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,11 +181,12 @@ class PaymentCard extends StatelessWidget {
|
||||
cityXid: cityXid,
|
||||
cardTypeXid: cardTypeXid,
|
||||
cardXid: cardXid,
|
||||
cardMode: isSelectivePass ? 'flexi' : 'fixed',
|
||||
cardMode: isSelectivePass ? 'flexi' : 'unlimited',
|
||||
totalAdult: adults,
|
||||
totalChild: children,
|
||||
noOfAttractions: isSelectivePass ? selectedValue : 0,
|
||||
noOfDays: isUnlimitedCard ? selectedValue : 0,
|
||||
baseAmount: totalPrice,
|
||||
);
|
||||
|
||||
// ✅ Extract bookingId from response
|
||||
|
||||
@@ -116,11 +116,11 @@ class _CheckoutContent extends StatelessWidget {
|
||||
|
||||
/// 🆕 Handle payment flow with client secret
|
||||
/// 🆕 Handle payment flow with client secret - SIMPLIFIED VERSION
|
||||
Future<void> _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId) async {
|
||||
Future<void> _handlePaymentFlow(BuildContext context, String clientSecret, int bookingId,double finalTotal) async {
|
||||
final paymentSuccess = await StripePaymentScreen.showAsBottomSheet(
|
||||
context: context,
|
||||
clientSecret: clientSecret,
|
||||
amount: checkoutData.totalPrice.toDouble(),
|
||||
amount: finalTotal,
|
||||
currencySymbol: '\$',
|
||||
title: 'Complete Payment',
|
||||
loadingMessage: 'Processing your pass payment...',
|
||||
@@ -149,6 +149,7 @@ class _CheckoutContent extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
onPaymentCancelled: () {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Payment cancelled'),
|
||||
@@ -182,22 +183,31 @@ class _CheckoutContent extends StatelessWidget {
|
||||
if (state is CheckoutCouponsLoadedState) {
|
||||
// Check if clientSecret is available (payment initiated)
|
||||
if (state.clientSecret != null && state.clientSecret!.isNotEmpty) {
|
||||
// Trigger payment flow
|
||||
// ✅ Calculate finalTotal here
|
||||
double discountPercentage = 0.0;
|
||||
if (state.appliedCoupon != null) {
|
||||
discountPercentage = state.appliedCoupon!.discountPercent.toDouble();
|
||||
}
|
||||
|
||||
final num subtotal = checkoutData.totalPrice;
|
||||
final double discountAmount = subtotal * (discountPercentage / 100);
|
||||
final double totalBeforeTax = subtotal - discountAmount;
|
||||
final double taxAmount = 2;
|
||||
final double finalTotal = totalBeforeTax + taxAmount;
|
||||
|
||||
// ✅ Trigger payment flow with finalTotal
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handlePaymentFlow(context, state.clientSecret!, state.bookingId ?? bookingId);
|
||||
_handlePaymentFlow(
|
||||
context,
|
||||
state.clientSecret!,
|
||||
state.bookingId ?? bookingId,
|
||||
finalTotal, // ✅ Pass the calculated finalTotal
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 🆕 Listen for payment confirmation success
|
||||
if (state.isPaymentConfirmed) {
|
||||
// Show success message
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// const SnackBar(
|
||||
// content: Text('Payment confirmed successfully!'),
|
||||
// backgroundColor: Colors.green,
|
||||
// ),
|
||||
// );
|
||||
|
||||
// Navigate to success page or back
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (context.mounted) {
|
||||
@@ -255,9 +265,10 @@ class _CheckoutContent extends StatelessWidget {
|
||||
|
||||
final num subtotal = checkoutData.totalPrice;
|
||||
final double discountAmount = subtotal * (discountPercentage / 100);
|
||||
final double taxRate = 0.05; // 5% tax
|
||||
// final double taxRate = 0.05; // 5% tax
|
||||
final double totalBeforeTax = subtotal - discountAmount;
|
||||
final double taxAmount = totalBeforeTax * taxRate;
|
||||
// final double taxAmount = totalBeforeTax * taxRate;
|
||||
final double taxAmount = 2;
|
||||
final double finalTotal = totalBeforeTax + taxAmount;
|
||||
|
||||
return Scaffold(
|
||||
@@ -531,11 +542,18 @@ class _CheckoutContent extends StatelessWidget {
|
||||
),
|
||||
builder: (_) => AllCouponsBottomsheet(
|
||||
onCouponSelected: (selectedCoupon) {
|
||||
final coupon = selectedCoupon as AllCouponsModel;
|
||||
// Apply the selected coupon
|
||||
context.read<CheckoutBloc>().add(
|
||||
ApplyCouponEvent(
|
||||
coupon: selectedCoupon),
|
||||
);
|
||||
context.read<CheckoutBloc>().add(
|
||||
ApplyCouponToBackendEvent(
|
||||
bookingId: bookingId,
|
||||
couponCode: coupon.couponCode,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
class MyItineraryResponse {
|
||||
bool isUnlimitedPass;
|
||||
List<MyItinerary> itineraries;
|
||||
|
||||
MyItineraryResponse({required this.itineraries});
|
||||
MyItineraryResponse({
|
||||
required this.isUnlimitedPass,
|
||||
required this.itineraries,
|
||||
});
|
||||
|
||||
factory MyItineraryResponse.fromJson(Map<String, dynamic>? json) {
|
||||
json ??= {};
|
||||
|
||||
factory MyItineraryResponse.fromJson(List<dynamic>? json) {
|
||||
return MyItineraryResponse(
|
||||
itineraries: json == null
|
||||
isUnlimitedPass: json['isUnlimitedPass'] ?? false,
|
||||
itineraries: json['itineraries'] == null
|
||||
? []
|
||||
: json.map((e) => MyItinerary.fromJson(e)).toList(),
|
||||
: List<Map<String, dynamic>>.from(json['itineraries'])
|
||||
.map((e) => MyItinerary.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> toJson() =>
|
||||
itineraries.map((e) => e.toJson()).toList();
|
||||
Map<String, dynamic> toJson() => {
|
||||
"isUnlimitedPass": isUnlimitedPass,
|
||||
"itineraries": itineraries.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
class MyItinerary {
|
||||
@@ -61,22 +72,21 @@ class MyItinerary {
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
userXid: (json['userXid'] as num?)?.toInt() ?? 0,
|
||||
cityXid: (json['cityXid'] as num?)?.toInt() ?? 0,
|
||||
address: json['Address'] ?? "",
|
||||
address: json['Address']?.toString() ?? "",
|
||||
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
|
||||
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
|
||||
tripEnergy: json['tripEnergy'] ?? "",
|
||||
tripEnergy: json['tripEnergy']?.toString() ?? "",
|
||||
travelingWithKids: json['travelingWithKids'] ?? false,
|
||||
dietaryPreferences: json['dietaryPreferences'] == null
|
||||
? []
|
||||
: List<String>.from(json['dietaryPreferences']),
|
||||
preferences:
|
||||
Preferences.fromJson(json['preferences'] ?? {}),
|
||||
preferences: Preferences.fromJson(json['preferences']),
|
||||
totalDays: (json['totalDays'] as num?)?.toInt() ?? 0,
|
||||
aiModel: json['aiModel'] ?? "",
|
||||
promptVersion: json['promptVersion'] ?? "",
|
||||
aiModel: json['aiModel']?.toString() ?? "",
|
||||
promptVersion: json['promptVersion']?.toString() ?? "",
|
||||
isActive: json['isActive'] ?? false,
|
||||
createdAt: json['createdAt'] ?? "",
|
||||
updatedAt: json['updatedAt'] ?? "",
|
||||
createdAt: json['createdAt']?.toString() ?? "",
|
||||
updatedAt: json['updatedAt']?.toString() ?? "",
|
||||
days: json['days'] == null
|
||||
? []
|
||||
: List<Map<String, dynamic>>.from(json['days'])
|
||||
@@ -166,8 +176,8 @@ class ItineraryDay {
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
itineraryXid: (json['itineraryXid'] as num?)?.toInt() ?? 0,
|
||||
dayNumber: (json['dayNumber'] as num?)?.toInt() ?? 0,
|
||||
title: json['title'] ?? "",
|
||||
summary: json['summary'] ?? "",
|
||||
title: json['title']?.toString() ?? "",
|
||||
summary: json['summary']?.toString() ?? "",
|
||||
items: json['items'] == null
|
||||
? []
|
||||
: List<Map<String, dynamic>>.from(json['items'])
|
||||
@@ -216,11 +226,11 @@ class DayItem {
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
itineraryDayXid:
|
||||
(json['itineraryDayXid'] as num?)?.toInt() ?? 0,
|
||||
timeSlot: json['timeSlot'] ?? "",
|
||||
title: json['title'] ?? "",
|
||||
description: json['description'] ?? "",
|
||||
locationName: json['locationName'] ?? "",
|
||||
imageUrl: json['imageUrl'] ?? "",
|
||||
timeSlot: json['timeSlot']?.toString() ?? "",
|
||||
title: json['title']?.toString() ?? "",
|
||||
description: json['description']?.toString() ?? "",
|
||||
locationName: json['locationName']?.toString() ?? "",
|
||||
imageUrl: json['imageUrl']?.toString() ?? "",
|
||||
latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0,
|
||||
longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0,
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
import 'package:citycards_customer/itinerary_creation/models/itinerary_city_model.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../localPreference/local_preference.dart';
|
||||
import '../../networkApiServices/api_urls.dart';
|
||||
import '../../networkApiServices/network_api_services.dart';
|
||||
import '../models/my_itinerary_model.dart';
|
||||
@@ -11,8 +12,9 @@ class ItineraryRepository {
|
||||
final NetworkApiService _apiService = NetworkApiService();
|
||||
|
||||
Future<MyItineraryResponse> fetchMyItineraries() async {
|
||||
final int cityId = await LocalPreference.getSelectedCityId();
|
||||
final response = await _apiService.getApi(
|
||||
url: ApiUrls.myItineraries, // 👈 Make sure this endpoint exists
|
||||
url: '${ApiUrls.myItineraries}/$cityId', // 👈 Make sure this endpoint exists
|
||||
);
|
||||
|
||||
/// Because API returns LIST
|
||||
|
||||
@@ -248,4 +248,18 @@ class PostcardCreationBloc
|
||||
emit(state.copyWith(isGift: event.isGift));
|
||||
});
|
||||
}
|
||||
|
||||
// Add this getter method in PostcardCreationBloc class
|
||||
String getFormattedMessage() {
|
||||
if (state.message == null || state.message!.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (state.selectedFont == null || state.selectedFont!.isEmpty) {
|
||||
// Default font (Poppins)
|
||||
return '<span style="font-family: Poppins;">${state.message}</span>';
|
||||
}
|
||||
|
||||
return '<span style="font-family: ${state.selectedFont};">${state.message}</span>';
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
|
||||
if (creationState.imagePath != null && creationState.imagePath!.isNotEmpty) {
|
||||
imageFile = File(creationState.imagePath!);
|
||||
}
|
||||
|
||||
final postcardBloc = context.read<PostcardCreationBloc>();
|
||||
context.read<PostcardCheckoutBloc>().add(
|
||||
UpdateCheckoutDataEvent(
|
||||
countryName: widget.countryName,
|
||||
@@ -90,7 +90,7 @@ class _PostcardCheckoutPageViewState extends State<PostcardCheckoutPageView> {
|
||||
address1: creationState.address,
|
||||
address2: widget.address2,
|
||||
pcTitle: widget.pcTitle,
|
||||
pcContent: creationState.message ?? '',
|
||||
pcContent: postcardBloc.getFormattedMessage(),
|
||||
pcImageFile: imageFile,
|
||||
pcNumber: widget.pcNumber,
|
||||
pcDatetime: widget.pcDatetime,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@@ -8,6 +10,7 @@ import '../blocs/postcard_creation_events.dart';
|
||||
import '../blocs/postcard_creation_state.dart';
|
||||
import '../widgets/postcard_preview_widget.dart';
|
||||
import '../widgets/step_progressbar.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
class WriteMessageStepPageView extends StatefulWidget {
|
||||
const WriteMessageStepPageView({super.key});
|
||||
@@ -45,10 +48,10 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
TextPosition(offset: _controller.text.length));
|
||||
|
||||
final fonts = [
|
||||
{"name": "Default", "font": GoogleFonts.poppins()},
|
||||
{"name": "Classic", "font": GoogleFonts.playfairDisplay()},
|
||||
{"name": "Handwriting", "font": GoogleFonts.dancingScript()},
|
||||
{"name": "Elegant", "font": GoogleFonts.cormorantGaramond()},
|
||||
{"name": "Default", "font": GoogleFonts.poppins(), "cleanName": "Poppins"},
|
||||
{"name": "Patrick Hand", "font": GoogleFonts.patrickHand(), "cleanName": "Patrick Hand"},
|
||||
{"name": "Indie Flower", "font": GoogleFonts.indieFlower(), "cleanName": "Indie Flower"},
|
||||
{"name": "Gloria Hallelujah", "font": GoogleFonts.gloriaHallelujah(), "cleanName": "Gloria Hallelujah"},
|
||||
];
|
||||
|
||||
return SafeArea(
|
||||
@@ -91,12 +94,7 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
maxLines: 8,
|
||||
maxLength: 400,
|
||||
cursorColor: const Color(0xffF95F62),
|
||||
style: TextStyle(
|
||||
fontFamily: state.selectedFont ??
|
||||
GoogleFonts.poppins().fontFamily,
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: _getTextFieldStyle(state.selectedFont, fonts),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "Add Your Message Here",
|
||||
@@ -133,43 +131,52 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
children: fonts.map((font) {
|
||||
final TextStyle fontStyle = font['font'] as TextStyle;
|
||||
final String fontName = font["name"] as String;
|
||||
final isSelected = state.selectedFont ==
|
||||
fontStyle.fontFamily ||
|
||||
(state.selectedFont == null &&
|
||||
fontName == "Default");
|
||||
final String cleanName = font["cleanName"] as String;
|
||||
|
||||
final isSelected = state.selectedFont == cleanName ||
|
||||
(state.selectedFont == null && fontName == "Default");
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => bloc
|
||||
.add(ChangeFontStyle(fontStyle.fontFamily ?? "")),
|
||||
onTap: () => bloc.add(ChangeFontStyle(cleanName)),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: 90.w,
|
||||
padding: const EdgeInsets.all(6),
|
||||
width: 100.w,
|
||||
height: 100.h,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DottedBorderPainter(
|
||||
color: isSelected
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xffE0E0E0),
|
||||
width: 1.5,
|
||||
strokeWidth: 1.5,
|
||||
dashWidth: 4,
|
||||
dashSpace: 3,
|
||||
borderRadius: 12,
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("Aa",
|
||||
style: fontStyle.copyWith(
|
||||
fontSize: 24.sp,
|
||||
color: const Color(0xff1A1A1A),
|
||||
)),
|
||||
const SizedBox(height: 4),
|
||||
Text(fontName,
|
||||
textAlign: TextAlign.center,
|
||||
style: fontStyle.copyWith(
|
||||
fontSize: 11.sp,
|
||||
color: isSelected
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xff2D3134),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text("Aa",
|
||||
style: fontStyle.copyWith(
|
||||
fontSize: 20.sp,
|
||||
color: const Color(0xff1A1A1A),
|
||||
)),
|
||||
const SizedBox(height: 4),
|
||||
Text(fontName,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: isSelected
|
||||
? const Color(0xffF95F62)
|
||||
: const Color(0xff2D3134),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -209,5 +216,117 @@ class _WriteMessageStepPageViewState extends State<WriteMessageStepPageView> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Helper method to get the correct font style for the text field
|
||||
TextStyle _getTextFieldStyle(String? selectedFont, List<Map<String, dynamic>> fonts) {
|
||||
if (selectedFont == null || selectedFont.isEmpty) {
|
||||
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
|
||||
}
|
||||
|
||||
// Find matching font by cleanName
|
||||
for (var font in fonts) {
|
||||
if (font['cleanName'] == selectedFont) {
|
||||
final TextStyle fontStyle = font['font'] as TextStyle;
|
||||
return fontStyle.copyWith(fontSize: 14.sp, color: Colors.black);
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback to Poppins
|
||||
return GoogleFonts.poppins(fontSize: 14.sp, color: Colors.black);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Painter for Dotted Border
|
||||
class DottedBorderPainter extends CustomPainter {
|
||||
final Color color;
|
||||
final double strokeWidth;
|
||||
final double dashWidth;
|
||||
final double dashSpace;
|
||||
final double borderRadius;
|
||||
|
||||
DottedBorderPainter({
|
||||
required this.color,
|
||||
this.strokeWidth = 1.5,
|
||||
this.dashWidth = 4,
|
||||
this.dashSpace = 3,
|
||||
this.borderRadius = 12,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final path = Path()
|
||||
..addRRect(RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Radius.circular(borderRadius),
|
||||
));
|
||||
|
||||
// Create dashed path
|
||||
final dashPath = _createDashedPath(path, dashWidth, dashSpace);
|
||||
canvas.drawPath(dashPath, paint);
|
||||
}
|
||||
|
||||
Path _createDashedPath(Path source, double dashWidth, double dashSpace) {
|
||||
final Path dest = Path();
|
||||
for (final PathMetric metric in source.computeMetrics()) {
|
||||
double distance = 0.0;
|
||||
bool draw = true;
|
||||
while (distance < metric.length) {
|
||||
final double length = draw ? dashWidth : dashSpace;
|
||||
if (distance + length > metric.length) {
|
||||
if (draw) {
|
||||
dest.addPath(
|
||||
metric.extractPath(distance, metric.length),
|
||||
Offset.zero,
|
||||
);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
if (draw) {
|
||||
dest.addPath(
|
||||
metric.extractPath(distance, distance + length),
|
||||
Offset.zero,
|
||||
);
|
||||
}
|
||||
distance += length;
|
||||
draw = !draw;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(DottedBorderPainter oldDelegate) {
|
||||
return oldDelegate.color != color ||
|
||||
oldDelegate.strokeWidth != strokeWidth ||
|
||||
oldDelegate.dashWidth != dashWidth ||
|
||||
oldDelegate.dashSpace != dashSpace;
|
||||
}
|
||||
}
|
||||
|
||||
// Lined Paper Painter (assuming this exists in your original code)
|
||||
class LinedPaperPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = const Color(0xFFE0E0E0)
|
||||
..strokeWidth = 1;
|
||||
|
||||
const lineSpacing = 30.0;
|
||||
for (double i = lineSpacing; i < size.height; i += lineSpacing) {
|
||||
canvas.drawLine(
|
||||
Offset(0, i),
|
||||
Offset(size.width, i),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(LinedPaperPainter oldDelegate) => false;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:html/parser.dart' as html_parser;
|
||||
|
||||
class BackCardWidget extends StatelessWidget {
|
||||
final String message;
|
||||
@@ -26,8 +27,77 @@ class BackCardWidget extends StatelessWidget {
|
||||
this.scale = 1.08,
|
||||
});
|
||||
|
||||
// Parse HTML message and extract font family and text
|
||||
Map<String, String> _parseHtmlMessage(String htmlMessage) {
|
||||
if (htmlMessage.isEmpty) {
|
||||
return {'text': '', 'fontFamily': ''};
|
||||
}
|
||||
|
||||
// Check if message contains HTML tags
|
||||
if (!htmlMessage.contains('<span') && !htmlMessage.contains('style=')) {
|
||||
// Plain text message - no font specified
|
||||
return {'text': htmlMessage, 'fontFamily': ''};
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse HTML
|
||||
final document = html_parser.parse(htmlMessage);
|
||||
final spanElement = document.querySelector('span');
|
||||
|
||||
if (spanElement != null) {
|
||||
// Extract text content
|
||||
final text = spanElement.text;
|
||||
|
||||
// Extract font-family from style attribute
|
||||
final style = spanElement.attributes['style'] ?? '';
|
||||
final fontFamilyMatch = RegExp(r'font-family:\s*([^;]+)').firstMatch(style);
|
||||
final fontFamily = fontFamilyMatch?.group(1)?.trim() ?? '';
|
||||
|
||||
return {'text': text, 'fontFamily': fontFamily};
|
||||
}
|
||||
|
||||
// Fallback: return plain text
|
||||
return {'text': document.body?.text ?? htmlMessage, 'fontFamily': ''};
|
||||
} catch (e) {
|
||||
// If parsing fails, return original message
|
||||
return {'text': htmlMessage, 'fontFamily': ''};
|
||||
}
|
||||
}
|
||||
|
||||
// Get TextStyle with any Google Font
|
||||
TextStyle _getFontStyle(String fontFamily, double fontSize, double height) {
|
||||
// If no font family specified, use default Caveat
|
||||
if (fontFamily.isEmpty) {
|
||||
return GoogleFonts.caveat(fontSize: fontSize, height: height);
|
||||
}
|
||||
|
||||
try {
|
||||
// Normalize font name: remove extra spaces, handle common variations
|
||||
final normalizedFont = fontFamily
|
||||
.trim()
|
||||
.replaceAll(RegExp(r'\s+'), ' '); // Replace multiple spaces with single space
|
||||
|
||||
// Try to get the font from Google Fonts
|
||||
// GoogleFonts.getFont() can load ANY Google Font dynamically
|
||||
return GoogleFonts.getFont(
|
||||
normalizedFont,
|
||||
fontSize: fontSize,
|
||||
height: height,
|
||||
);
|
||||
} catch (e) {
|
||||
// If font not found in Google Fonts, fallback to default
|
||||
debugPrint('⚠️ Font "$fontFamily" not found in Google Fonts. Using default Caveat font.');
|
||||
return GoogleFonts.caveat(fontSize: fontSize, height: height);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Parse the message to extract text and font
|
||||
final parsedMessage = _parseHtmlMessage(message);
|
||||
final messageText = parsedMessage['text'] ?? '';
|
||||
final fontFamily = parsedMessage['fontFamily'] ?? '';
|
||||
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: Container(
|
||||
@@ -58,11 +128,8 @@ class BackCardWidget extends StatelessWidget {
|
||||
padding: EdgeInsets.fromLTRB(22.w, 22.h, 18.w, 18.h),
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
message,
|
||||
style: GoogleFonts.caveat(
|
||||
fontSize: 16.sp,
|
||||
height: 1.7,
|
||||
),
|
||||
messageText,
|
||||
style: _getFontStyle(fontFamily, 16.sp, 1.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user