feature/medora-55 (#10)
Added Splash screen, logo and some minor changes in the code Co-authored-by: Jipson George <152465898+Jipson-cosq@users.noreply.github.com> Reviewed-on: cosqnet/telemednet#10 Co-authored-by: DhanshCOSQ <dhanshas@cosq.net> Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
@ -2,7 +2,7 @@
|
|||||||
<application
|
<application
|
||||||
android:label="Medora"
|
android:label="Medora"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/launcher_icon">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground>
|
||||||
|
<inset
|
||||||
|
android:drawable="@drawable/ic_launcher_foreground"
|
||||||
|
android:inset="16%" />
|
||||||
|
</foreground>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 2.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 7.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
4
android/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#ffffff</color>
|
||||||
|
</resources>
|
||||||
BIN
images/logo.jpeg
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
images/logo.jpg
|
Before Width: | Height: | Size: 324 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 888 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 645 B After Width: | Height: | Size: 452 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 956 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 701 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 956 B |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.7 KiB |
@ -4,7 +4,8 @@ enum PaymentStatus { pending, completed, failed, refunded }
|
|||||||
|
|
||||||
class Booking {
|
class Booking {
|
||||||
final String id;
|
final String id;
|
||||||
// final String doctorId;
|
final String doctorId;
|
||||||
|
final String profileImageUrl;
|
||||||
final String doctorName;
|
final String doctorName;
|
||||||
final String patientId;
|
final String patientId;
|
||||||
final String patientName;
|
final String patientName;
|
||||||
@ -18,7 +19,8 @@ class Booking {
|
|||||||
|
|
||||||
Booking({
|
Booking({
|
||||||
required this.id,
|
required this.id,
|
||||||
// required this.doctorId,
|
required this.doctorId,
|
||||||
|
required this.profileImageUrl,
|
||||||
required this.doctorName,
|
required this.doctorName,
|
||||||
required this.patientId,
|
required this.patientId,
|
||||||
required this.patientName,
|
required this.patientName,
|
||||||
@ -31,11 +33,11 @@ class Booking {
|
|||||||
required this.specialization,
|
required this.specialization,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert Booking object to a map for Firestore
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'id': id,
|
'id': id,
|
||||||
// 'doctorId': doctorId,
|
'doctorId': doctorId,
|
||||||
|
'profileImageUrl': profileImageUrl,
|
||||||
'doctorName': doctorName,
|
'doctorName': doctorName,
|
||||||
'patientId': patientId,
|
'patientId': patientId,
|
||||||
'patientName': patientName,
|
'patientName': patientName,
|
||||||
@ -52,7 +54,8 @@ class Booking {
|
|||||||
factory Booking.fromMap(Map<String, dynamic> map) {
|
factory Booking.fromMap(Map<String, dynamic> map) {
|
||||||
return Booking(
|
return Booking(
|
||||||
id: map['id'],
|
id: map['id'],
|
||||||
// doctorId: map['doctorId'],
|
doctorId: map['doctorId'],
|
||||||
|
profileImageUrl: map['profileImageUrl'],
|
||||||
doctorName: map['doctorName'],
|
doctorName: map['doctorName'],
|
||||||
patientId: map['patientId'],
|
patientId: map['patientId'],
|
||||||
patientName: map['patientName'],
|
patientName: map['patientName'],
|
||||||
@ -68,7 +71,6 @@ class Booking {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a copy of Booking with modified fields
|
|
||||||
Booking copyWith({
|
Booking copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
String? doctorId,
|
String? doctorId,
|
||||||
@ -85,7 +87,8 @@ class Booking {
|
|||||||
}) {
|
}) {
|
||||||
return Booking(
|
return Booking(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
// doctorId: doctorId ?? this.doctorId,
|
doctorId: doctorId ?? this.doctorId,
|
||||||
|
profileImageUrl: profileImageUrl,
|
||||||
doctorName: doctorName ?? this.doctorName,
|
doctorName: doctorName ?? this.doctorName,
|
||||||
patientId: patientId ?? this.patientId,
|
patientId: patientId ?? this.patientId,
|
||||||
patientName: patientName ?? this.patientName,
|
patientName: patientName ?? this.patientName,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ class Doctor {
|
|||||||
String? addressType;
|
String? addressType;
|
||||||
|
|
||||||
Doctor({
|
Doctor({
|
||||||
|
this.profileImageUrl,
|
||||||
this.addressType,
|
this.addressType,
|
||||||
this.achievements,
|
this.achievements,
|
||||||
this.profileImageUrl,
|
this.profileImageUrl,
|
||||||
@ -69,6 +70,7 @@ class Doctor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static Doctor fromJson(Map<String, dynamic> json) => Doctor(
|
static Doctor fromJson(Map<String, dynamic> json) => Doctor(
|
||||||
|
profileImageUrl: json['profileImageUrl'],
|
||||||
achievements: List<String>.from(json['achievements'] ?? []),
|
achievements: List<String>.from(json['achievements'] ?? []),
|
||||||
profileImageUrl: json['profileImageUrl'],
|
profileImageUrl: json['profileImageUrl'],
|
||||||
profileImage: json['profileImage'],
|
profileImage: json['profileImage'],
|
||||||
|
|||||||
@ -5,6 +5,8 @@ class BookingService {
|
|||||||
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
|
||||||
Future<String> createBooking({
|
Future<String> createBooking({
|
||||||
|
required String doctorId,
|
||||||
|
required String profileImageUrl,
|
||||||
required String doctorName,
|
required String doctorName,
|
||||||
required String patientId,
|
required String patientId,
|
||||||
required String patientName,
|
required String patientName,
|
||||||
@ -19,6 +21,7 @@ class BookingService {
|
|||||||
|
|
||||||
final booking = Booking(
|
final booking = Booking(
|
||||||
id: docRef.id,
|
id: docRef.id,
|
||||||
|
doctorId: doctorId,
|
||||||
doctorName: doctorName,
|
doctorName: doctorName,
|
||||||
patientId: patientId,
|
patientId: patientId,
|
||||||
patientName: patientName,
|
patientName: patientName,
|
||||||
@ -29,6 +32,7 @@ class BookingService {
|
|||||||
paymentStatus: PaymentStatus.pending,
|
paymentStatus: PaymentStatus.pending,
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
specialization: specialization,
|
specialization: specialization,
|
||||||
|
profileImageUrl: profileImageUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
docRef.set(booking.toMap());
|
docRef.set(booking.toMap());
|
||||||
|
|||||||
@ -141,6 +141,7 @@ final Map<String, Widget Function(BuildContext)> routes = {
|
|||||||
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
|
||||||
return DoctorDetailsScreen(
|
return DoctorDetailsScreen(
|
||||||
doctor: args['doctor'] as Doctor,
|
doctor: args['doctor'] as Doctor,
|
||||||
|
preloadedImage: args['imageProvider'] as ImageProvider?,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
RouteNames.consultationCenterScreen: (context) {
|
RouteNames.consultationCenterScreen: (context) {
|
||||||
|
|||||||
@ -254,7 +254,7 @@ class _ConsultationBookingScreenState extends State<ConsultationBookingScreen> {
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.network(
|
child: Image.network(
|
||||||
widget.doctor.profileImage!,
|
widget.doctor.profileImageUrl!,
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -454,6 +454,8 @@ class _ConsultationBookingScreenState extends State<ConsultationBookingScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final bookingId = await bookingService.createBooking(
|
final bookingId = await bookingService.createBooking(
|
||||||
|
doctorId: widget.doctor.uid!,
|
||||||
|
profileImageUrl: widget.doctor.profileImageUrl!,
|
||||||
doctorName: widget.doctor.firstName ?? 'Doctor',
|
doctorName: widget.doctor.firstName ?? 'Doctor',
|
||||||
patientId: currentUser!.uid,
|
patientId: currentUser!.uid,
|
||||||
patientName: patientName ?? 'Patient',
|
patientName: patientName ?? 'Patient',
|
||||||
|
|||||||
@ -153,7 +153,7 @@ class _ConsultationTimeScreenState extends State<ConsultationTimeScreen> {
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.network(
|
child: Image.network(
|
||||||
widget.doctor.profileImage!,
|
widget.doctor.profileImageUrl!,
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
|||||||
@ -144,7 +144,7 @@ class _ConsultationsCenterScreenState extends State<ConsultationsCenterScreen> {
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.network(
|
child: Image.network(
|
||||||
widget.doctor.profileImage!,
|
widget.doctor.profileImageUrl!,
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
|||||||
@ -2,15 +2,25 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:medora/data/models/doctor.dart';
|
import 'package:medora/data/models/doctor.dart';
|
||||||
import 'package:medora/route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
class DoctorDetailsScreen extends StatelessWidget {
|
class DoctorDetailsScreen extends StatefulWidget {
|
||||||
final Doctor doctor;
|
final Doctor doctor;
|
||||||
|
final ImageProvider? preloadedImage;
|
||||||
|
|
||||||
const DoctorDetailsScreen({
|
const DoctorDetailsScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.doctor,
|
required this.doctor,
|
||||||
|
this.preloadedImage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DoctorDetailsScreen> createState() => _DoctorDetailsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DoctorDetailsScreenState extends State<DoctorDetailsScreen> {
|
||||||
|
bool isDescriptionExpanded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -44,7 +54,7 @@ class DoctorDetailsScreen extends StatelessWidget {
|
|||||||
Navigator.pushNamed(
|
Navigator.pushNamed(
|
||||||
context, RouteNames.consultationCenterScreen,
|
context, RouteNames.consultationCenterScreen,
|
||||||
arguments: {
|
arguments: {
|
||||||
'doctor': doctor,
|
'doctor': widget.doctor,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@ -113,38 +123,21 @@ class DoctorDetailsScreen extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
_buildDoctorImage(),
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.network(
|
|
||||||
doctor.profileImage!,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return Container(
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
color: Colors.grey[300],
|
|
||||||
child:
|
|
||||||
Icon(Icons.person, size: 50, color: Colors.grey[600]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
doctor.firstName ?? '',
|
widget.doctor.firstName ?? '',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
doctor.speciality!,
|
widget.doctor.speciality!,
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
@ -158,7 +151,7 @@ class DoctorDetailsScreen extends StatelessWidget {
|
|||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
doctor.speciality!,
|
widget.doctor.speciality!,
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
@ -174,7 +167,7 @@ class DoctorDetailsScreen extends StatelessWidget {
|
|||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
doctor.city!,
|
widget.doctor.city!,
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
@ -188,7 +181,7 @@ class DoctorDetailsScreen extends StatelessWidget {
|
|||||||
Icon(Icons.star, size: 16, color: Colors.blue[400]),
|
Icon(Icons.star, size: 16, color: Colors.blue[400]),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${doctor.yearsOfExperience} Years',
|
'${widget.doctor.yearsOfExperience} Years',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
@ -206,6 +199,53 @@ class DoctorDetailsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDoctorImage() {
|
||||||
|
final imageProvider =
|
||||||
|
widget.preloadedImage ?? NetworkImage(widget.doctor.profileImageUrl!);
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image(
|
||||||
|
image: imageProvider,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||||
|
if (wasSynchronouslyLoaded || frame != null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Shimmer.fromColors(
|
||||||
|
baseColor: Colors.grey[300]!,
|
||||||
|
highlightColor: Colors.grey[100]!,
|
||||||
|
child: Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Container(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 50,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDescription() {
|
Widget _buildDescription() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@ -232,21 +272,33 @@ class DoctorDetailsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
doctor.profileDescription!,
|
widget.doctor.profileDescription!,
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: isDescriptionExpanded ? null : 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: isDescriptionExpanded ? null : TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
TextButton(
|
Align(
|
||||||
onPressed: () {},
|
alignment: Alignment.topLeft,
|
||||||
child: Text(
|
child: TextButton(
|
||||||
'Read more',
|
onPressed: () {
|
||||||
style: GoogleFonts.poppins(
|
setState(() {
|
||||||
color: Colors.blue,
|
isDescriptionExpanded = !isDescriptionExpanded;
|
||||||
fontWeight: FontWeight.w500,
|
});
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
isDescriptionExpanded ? 'Show less' : 'Read more',
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -280,16 +332,13 @@ class DoctorDetailsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
...doctor.qualifications!.map((qual) => Padding(
|
Text(
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
widget.doctor.qualifications?.join(', ') ?? '',
|
||||||
child: Text(
|
style: GoogleFonts.poppins(
|
||||||
qual.toString(),
|
fontSize: 14,
|
||||||
style: GoogleFonts.poppins(
|
color: Colors.grey[600],
|
||||||
fontSize: 14,
|
),
|
||||||
color: Colors.grey[600],
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:medora/data/models/doctor.dart';
|
import 'package:medora/data/models/doctor.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:medora/route/route_names.dart';
|
import 'package:medora/route/route_names.dart';
|
||||||
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
class DoctorsListScreen extends StatefulWidget {
|
class DoctorsListScreen extends StatefulWidget {
|
||||||
final String specialty;
|
final String specialty;
|
||||||
@ -29,7 +28,7 @@ class _DoctorsListScreenState extends State<DoctorsListScreen> {
|
|||||||
|
|
||||||
doctorsQuery = FirebaseFirestore.instance
|
doctorsQuery = FirebaseFirestore.instance
|
||||||
.collection('doctorprofiles')
|
.collection('doctorprofiles')
|
||||||
.where('speciality', isEqualTo: widget.specialty.toLowerCase());
|
.where('speciality', isEqualTo: widget.specialty);
|
||||||
_searchController.addListener(_onSearchChanged);
|
_searchController.addListener(_onSearchChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,8 +138,10 @@ class _DoctorsListScreenState extends State<DoctorsListScreen> {
|
|||||||
stream: doctorsQuery.snapshots(),
|
stream: doctorsQuery.snapshots(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const SliverFillRemaining(
|
return SliverList(
|
||||||
child: Center(child: CircularProgressIndicator()),
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => _buildShimmerDoctorCard(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,6 +193,67 @@ class _DoctorsListScreenState extends State<DoctorsListScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildShimmerDoctorCard() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.1),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Shimmer.fromColors(
|
||||||
|
baseColor: Colors.grey[300]!,
|
||||||
|
highlightColor: Colors.grey[100]!,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 120,
|
||||||
|
height: 20,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
width: 150,
|
||||||
|
height: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
width: 100,
|
||||||
|
height: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDoctorCard(Doctor doctor) {
|
Widget _buildDoctorCard(Doctor doctor) {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
@ -212,11 +274,13 @@ class _DoctorsListScreenState extends State<DoctorsListScreen> {
|
|||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
precacheImage(NetworkImage(doctor.profileImageUrl!), context);
|
||||||
Navigator.pushNamed(
|
Navigator.pushNamed(
|
||||||
context,
|
context,
|
||||||
RouteNames.doctorDetailsScreen,
|
RouteNames.doctorDetailsScreen,
|
||||||
arguments: {
|
arguments: {
|
||||||
'doctor': doctor,
|
'doctor': doctor,
|
||||||
|
'imageProvider': NetworkImage(doctor.profileImageUrl!),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -224,20 +288,7 @@ class _DoctorsListScreenState extends State<DoctorsListScreen> {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
_buildDoctorImage(doctor),
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
image: DecorationImage(
|
|
||||||
image: doctor.profileImage!.startsWith('http')
|
|
||||||
? NetworkImage(doctor.profileImage!)
|
|
||||||
: FileImage(File(doctor.profileImage!))
|
|
||||||
as ImageProvider,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -296,4 +347,41 @@ class _DoctorsListScreenState extends State<DoctorsListScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDoctorImage(Doctor doctor) {
|
||||||
|
return Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.network(
|
||||||
|
doctor.profileImageUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Shimmer.fromColors(
|
||||||
|
baseColor: Colors.grey[300]!,
|
||||||
|
highlightColor: Colors.grey[100]!,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 40,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -302,6 +302,7 @@ class _PatientHomeScreenState extends State<PatientHomeScreen>
|
|||||||
tag: 'consultation_${booking.id}',
|
tag: 'consultation_${booking.id}',
|
||||||
child: Material(
|
child: Material(
|
||||||
child: _consultationCard(
|
child: _consultationCard(
|
||||||
|
booking.profileImageUrl,
|
||||||
booking.doctorName,
|
booking.doctorName,
|
||||||
'${DateFormat('EEE, MMM d, yyyy').format(booking.appointmentDate)}\n${booking.appointmentTime}',
|
'${DateFormat('EEE, MMM d, yyyy').format(booking.appointmentDate)}\n${booking.appointmentTime}',
|
||||||
booking.specialization,
|
booking.specialization,
|
||||||
@ -320,6 +321,7 @@ class _PatientHomeScreenState extends State<PatientHomeScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _consultationCard(
|
Widget _consultationCard(
|
||||||
|
String? profileImageUrl,
|
||||||
String name,
|
String name,
|
||||||
String schedule,
|
String schedule,
|
||||||
String speciality,
|
String speciality,
|
||||||
@ -348,30 +350,50 @@ class _PatientHomeScreenState extends State<PatientHomeScreen>
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
if (profileImageUrl != null)
|
||||||
width: 64,
|
Container(
|
||||||
height: 64,
|
width: 64,
|
||||||
decoration: BoxDecoration(
|
height: 64,
|
||||||
gradient: LinearGradient(
|
decoration: BoxDecoration(
|
||||||
colors: [Colors.blue[400]!, Colors.blue[600]!],
|
shape: BoxShape.circle,
|
||||||
begin: Alignment.topLeft,
|
image: DecorationImage(
|
||||||
end: Alignment.bottomRight,
|
image: NetworkImage(profileImageUrl),
|
||||||
),
|
fit: BoxFit.cover,
|
||||||
shape: BoxShape.circle,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.blue[300]!.withOpacity(0.3),
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
),
|
||||||
],
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue[300]!.withOpacity(0.3),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Container(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.blue[400]!, Colors.blue[600]!],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue[300]!.withOpacity(0.3),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.person,
|
||||||
|
size: 36,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 36,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
13
pubspec.yaml
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.3.0+5
|
version: 1.3.5+6
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.3
|
sdk: ^3.5.3
|
||||||
@ -71,10 +71,14 @@ dev_dependencies:
|
|||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^4.0.0
|
flutter_lints: ^4.0.0
|
||||||
flutter_launcher_icons: ^0.14.1
|
flutter_launcher_icons: ^0.14.1
|
||||||
flutter_icons:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: "launcher_icon"
|
||||||
ios: true
|
ios: true
|
||||||
image_path: "images/logo.jpg"
|
remove_alpha_ios: true
|
||||||
|
image_path: "images/logo.jpeg"
|
||||||
|
min_sdk_android: 24
|
||||||
|
adaptive_icon_background: "#ffffff"
|
||||||
|
adaptive_icon_foreground: "images/logo.jpeg"
|
||||||
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
@ -98,6 +102,7 @@ flutter:
|
|||||||
- images/MaleDr.jpg.png
|
- images/MaleDr.jpg.png
|
||||||
- images/FemaleDr.jpg.png
|
- images/FemaleDr.jpg.png
|
||||||
- images/splash_screen.jpg
|
- images/splash_screen.jpg
|
||||||
|
- images/logo.jpeg
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|||||||