diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/100.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 00000000..c58db45d Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/1024.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 00000000..4598286b Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/114.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 00000000..c32a2d52 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/120.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 00000000..96ebea5e Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/144.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 00000000..bc29e159 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/152.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 00000000..8412a820 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/167.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 00000000..8d4dba19 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/180.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 00000000..44b2a86d Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/20.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 00000000..26b9d996 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/29.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 00000000..965753e5 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/40.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 00000000..8f553ae1 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/50.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 00000000..d100f81e Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/57.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 00000000..744aa3ba Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/58.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 00000000..4487359a Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/60.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 00000000..60de1354 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/72.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 00000000..7586d123 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/76.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 00000000..f5691889 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/80.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 00000000..340d82bc Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/87.png b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 00000000..4e751666 Binary files /dev/null and b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..65b74d7e --- /dev/null +++ b/AppIcons_NewApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]} \ No newline at end of file diff --git a/AppIcons_NewApp/appstore.png b/AppIcons_NewApp/appstore.png new file mode 100644 index 00000000..4598286b Binary files /dev/null and b/AppIcons_NewApp/appstore.png differ diff --git a/AppIcons_NewApp/playstore.png b/AppIcons_NewApp/playstore.png new file mode 100644 index 00000000..a1da25a3 Binary files /dev/null and b/AppIcons_NewApp/playstore.png differ diff --git a/New Patient App android keystore/NewPatientAppKeyStore.jks b/New Patient App android keystore/NewPatientAppKeyStore.jks new file mode 100644 index 00000000..e50988ea Binary files /dev/null and b/New Patient App android keystore/NewPatientAppKeyStore.jks differ diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9629f38b..347ab714 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -19,22 +19,33 @@ if (keystorePropertiesFile.exists()) { } android { -// namespace = "com.ejada.hmg" namespace = "com.cloudsolutions.HMGPatientApp" compileSdk = 36 - ndkVersion = "28.2.13676358" + // Using NDK 26 for better compatibility with Zoom SDK native libraries + ndkVersion = "27.0.12077973" +// ndkVersion = "25.1.8937393" + + sourceSets { + getByName("main") { + jniLibs.srcDirs("src/main/jniLibs") + } + } defaultConfig { -// applicationId = "com.ejada.hmg" applicationId = "com.cloudsolutions.HMGPatientApp" // minSdk = 24 - minSdk = 29 - targetSdk = 36 + minSdk = 26 + targetSdk = 35 compileSdk = 36 // targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName multiDexEnabled = true + + // Filter to arm64-v8a for better native library compatibility + ndk { + abiFilters += listOf("arm64-v8a", "armeabi-v7a") + } } buildFeatures { @@ -85,7 +96,7 @@ android { } } - packagingOptions { + packaging { jniLibs { pickFirsts += listOf( "lib/x86/libc++_shared.so", @@ -95,9 +106,16 @@ android { "**/*.so" ) useLegacyPackaging = true + // Keep Zoom SDK libraries unstripped to preserve symbols + keepDebugSymbols += listOf( + "*/libzoom_util.so", + "*/libzoom_check.so", + "*/libc++_shared.so" + ) } resources { excludes += listOf("META-INF/proguard/androidx-annotations.pro") + excludes.add("lib/*/libc++_shared.so") } } @@ -130,9 +148,13 @@ dependencies { implementation("com.google.code.gson:gson:2.12.0") // Zoom SDKs - implementation("us.zoom.videosdk:zoomvideosdk-core:1.12.10") - implementation("us.zoom.videosdk:zoomvideosdk-annotation:1.12.10") - implementation("us.zoom.videosdk:zoomvideosdk-videoeffects:1.12.10") +// implementation("us.zoom.videosdk:zoomvideosdk-core:1.12.10") +// implementation("us.zoom.videosdk:zoomvideosdk-annotation:1.12.10") +// implementation("us.zoom.videosdk:zoomvideosdk-videoeffects:1.12.10") + + implementation("us.zoom.videosdk:zoomvideosdk-core:2.1.10") + implementation("us.zoom.videosdk:zoomvideosdk-videoeffects:2.1.10") + implementation("us.zoom.videosdk:zoomvideosdk-annotation:2.1.10") // Networking implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11") @@ -193,5 +215,39 @@ dependencies { implementation("com.whatsapp.otp:whatsapp-otp-android-sdk:0.1.0") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") // implementation(project(":vitalSignEngine")) +} -} \ No newline at end of file +tasks.whenTaskAdded { + if (name.contains("merge") && name.contains("NativeLibs")) { + doLast { + val outputDir = File(project.buildDir, "intermediates/merged_native_libs/release/out/lib") + + // Handle both architectures + val abis = listOf("arm64-v8a", "armeabi-v7a") + abis.forEach { abi -> + val targetFile = File(outputDir, "$abi/libc++_shared.so") + val sourceFile = file("src/main/jniLibs/$abi/libc++_shared.so") + + if (sourceFile.exists() && targetFile.exists()) { + println("FORCING: Overwriting $abi version with known-good 1.2MB library") + sourceFile.copyTo(targetFile, overwrite = true) + } + } + } + } +} + +//tasks.whenTaskAdded { +// if (name.contains("merge") && name.contains("NativeLibs")) { +// doLast { +// val outputDir = File(project.buildDir, "intermediates/merged_native_libs/release/out/lib") +// val targetFile = File(outputDir, "arm64-v8a/libc++_shared.so") +// val sourceFile = file("src/main/jniLibs/arm64-v8a/libc++_shared.so") +// +// if (sourceFile.exists()) { +// println("FORCE REPLACING: Injecting 1.2MB libc++ into APK...") +// sourceFile.copyTo(targetFile, overwrite = true) +// } +// } +// } +//} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 9c1827a0..94ad242d 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -52,6 +52,11 @@ *; } +# Prevent stripping of standard C++ symbols +-keepclassmembers class * { + native ; +} + -dontwarn com.opentok.android.** -dontwarn com.opentok.otc.** diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 54e1a5ee..0e77b6b6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,7 +49,7 @@ - + @@ -62,7 +62,6 @@ - @@ -148,6 +148,9 @@ + - - - - - - - - - - - - Gradle Configuration Cache - - - -
- -
- Loading... -
- - - - - - diff --git a/assets/images/svg/dark_mode_icon.svg b/assets/images/svg/dark_mode_icon.svg new file mode 100644 index 00000000..f3a03f1c --- /dev/null +++ b/assets/images/svg/dark_mode_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/download.svg b/assets/images/svg/download.svg index 37aef2bd..7bedc456 100644 --- a/assets/images/svg/download.svg +++ b/assets/images/svg/download.svg @@ -1,4 +1,3 @@ - - + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 9fbaa828..51e27019 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -46,7 +46,7 @@ "nationality": "الجنسية", "neverWait": "لا تنتظر في الطابور أبدًا", "reminderTitle": "لديك موعد مع الدكتور.", - "appointmentCalendar": "هل ترغب في ضبط تذكير لهذا الموعد في تقويمك؟", + "appointmentCalendar": "هل ترغب في تعيين تذكير لهذا الموعد في التقويم الخاص بك؟", "appointmentSuccess": "تم حجز الموعد بنجاح", "docQualifications": "مؤهلات الطبيب", "pressAgain": "اضغط مرة أخرى للخروج من التطبيق", @@ -58,7 +58,7 @@ "patientShare": "نسبة المريض", "patientShareWithTax": "نسبة المريض مع الضريبة", "confirmAppo": "تأكيد الموعد", - "confirmLiveCare": "تأكيد الرعاية الحية", + "confirmLiveCare": "تأكيد ال لايف كير", "waitingForDoctor": "في انتظار الطبيب", "appointment": "موعد", "confirmLater": "تأكيد لاحقًا", @@ -70,20 +70,20 @@ "payNow": "ادفع الآن", "viewQR": "عرض رمز الاستجابة السريعة", "instruction": "تعليمات", - "livecare": "الرعاية الحية", - "livecareAppo": "موعد الرعاية الحية", + "livecare": "لايف كير", + "livecareAppo": "موعد لايف كير", "agreeTo": "أوافق على", "termsConditoins": "الشروط والأحكام", "cancelAppoMsg": "هل أنت متأكد أنك تريد إلغاء هذا الموعد؟", "changePayment": "تغيير طريقة الدفع", - "upcomingNoAction": "لا حاجة لإجراء أي إجراء", + "upcomingNoAction": "لا يتطلب أي إجراء", "upcomingConfirm": "يرجى تأكيد الموعد لتجنب الإلغاء", "bookSuccessConfirmMore2412": "ستكون عملية الدفع عبر الإنترنت متاحة قبل 24 ساعة ��ن الموعد.", "upcomingPaymentPending": "سيتم تفعيل الدفع عبر الإنترنت قبل 24 ساعة من موعد الموعد", "upcomingPaymentNow": "ادفع عبر الإنترنت الآن لتجنب الانتظار الطويل", "upcomingQR": "استخدم رمز الاستجابة السريعة لتسجيل الوصول في المستشفى", "upcomingVirtual": "هذا موعد افتراضي، يرجى الالتزام بالتعليمات لتجربة سلسة.", - "upcomingLivecare": "هذا موعد رعاية حية", + "upcomingLivecare": "هذا موعد لايف كير", "upcomingDetails": "المزيد من التفاصيل", "reschedule": "إعادة جدولة", "raise": "رفع", @@ -92,7 +92,7 @@ "reminder": "تذكير", "location": "المواقع", "online": "عبر الإنترنت", - "payment": "الالكتروني", + "payment": "الدفع", "onlinePayment": "عبر الإنترنت", "onlinePaymentSubtitle": "الدفع", "cancelNocaps": "إلغاء", @@ -161,7 +161,7 @@ "passQRAppoAttend": "مرر رمز الاستجابة السريعة عبر أجهزة الحضور المتاحة في المستشفى.", "sitWaitingQR": "اجلس في غرف الانتظار حتى يتم استدعاؤك من قبل الممرضة.", "attendRegisterCode": "رمز تسجيل الحضور", - "scanQRHospital": "اقترب من لوحة التسجيل عبر الإنترنت في المستشفى وامسح عبر NFC أو رمز الاستجابة السريعة لتسجيل الوصول", + "scanQRHospital": "توجّه إلى لوحة تسجيل الوصول الإلكتروني في المستشفى، وقم بالمسح عبر تقنية NFC أو رمز الاستجابة السريعة (QR) لإتمام تسجيل الوصول.", "scanNFC": "امسح NFC لتسجيل الوصول", "sendEmail": "إرسال بريد إلكتروني", "success": "تم بنجاح", @@ -174,7 +174,7 @@ "arrived": "وصل", "payNowBookSuccess": "ادفع الآن عبر تطبيق الحبيب", "payNowBookSuccesstext1": "ادفع الآن باستخدام خدمة الدفع عبر الإنترنت من بوابات الدفع الآمنة", - "payNowBookSuccesstext2": "يمكنك أيضًا الدفع لاحقًا عبر الدفع عبر الإنترنت أو في المستشفى", + "payNowBookSuccesstext2": "يمكنك أيضًا الدفع لاحقًا عبر الدفع الإلكتروني أو في المستشفى.", "payLater": "ادفع لاحقًا", "askDocNotAllowed": "هذه الخدمة ستكون متاحة فقط لزيارة الطبيب قبل 15 يومًا", "moreVerify": "خيارات تحقق إضافية", @@ -246,13 +246,13 @@ "myMedicalFileSubTitle": "جميع سجلاتك الطبية", "viewMore": "عرض المزيد", "homeHealthCareService": "خدمة الرعاية الصحية المنزلية", - "onlinePharmacy": "صيدلية عبر الإنترنت", + "onlinePharmacy": "الصيدلية الإلكترونية", "emergencyService": "خدمة الطوارئ", "onlinePaymentService": "خدمة الدفع عبر الإنترنت", "offersAndPackages": "طلب تحويل عبر الإنترنت", "comprehensiveMedicalCheckup": "فحص طبي شامل", "hmgService": "خدمة HMG", - "viewAllHabibMedicalService": "عرض جميع خدمات حبيب الطبية", + "viewAllHabibMedicalService": "عرض جميع خدمات الحبيب الطبية", "viewAll": "عرض الكل", "view": "عرض", "contactUs": "اتصل بنا", @@ -263,7 +263,7 @@ "consultation": "استشارة", "logs": "السجلات", "textToSpeech": "كيف يمكنني مساعدتك؟", - "locationDialogMessage": "سيساعدك السماح لتطبيق HMG بالوصول إلى موقعك في عرض المستشفيات الأقرب إليك.", + "locationDialogMessage": "السماح لتطبيق HMG بالوصول إلى موقعك سيساعدك في عرض المستشفيات الأقرب إليك.", "userViewRequester": "يريد المستخدم عرض ملفك الطبي", "userView": "يمكن للمستخدم عرض ملفك الطبي", "parking": "موقف السيارات", @@ -288,7 +288,7 @@ "requestA": "طلب", "noBookedAppointments": "لا توجد مواعيد محجوزة", "noConfirmedAppointments": "لا توجد مواعيد مؤكدة", - "noArrivedAppointments": "لا توجد مواعيد وصلت", + "noArrivedAppointments": "لا توجد مواعيد تم تسجيل الوصول إليها", "myAppointmentsList": "المواعيد", "myAppointments": "مواعيدي", "radiology": "الأشعة", @@ -358,11 +358,11 @@ "userAgreement": "اتفاقية المستخدم", "updateSuccessfully": "تم التحديث بنجاح", "checkVaccineAvailability": "تحقق من توفر اللقاح", - "myVaccinesAvailability": "توفر لقاحاتي", + "myVaccinesAvailability": "توفر اللقاحات الخاصة بي", "paymentService": "خدمة الدفع", "paymentOnline": "الخدمة", "onlineCheckIn": "تسجيل الوصول عبر الإنترنت", - "myBalances": "أرصدةي", + "myBalances": "رصيدي", "myWallet": "محف��تي", "balanceAmount": "مبلغ المحفظة", "totalBalance": "إجمالي الرصيد", @@ -383,7 +383,7 @@ "otherAccount": "حساب آخر", "selectBeneficiary": "اختر المستفيد", "confirmThePayment": "تأكيد الدفع", - "depositorName": "اسم الوديع", + "depositorName": "اسم مُقدِّم الإيداع", "mobileNumber": "رقم الجوال", "phoneNumber": "رقم الهاتف", "country": "البلد", @@ -410,7 +410,7 @@ "diameter": "القطر", "remarks": "ملاحظات: ", "activeMedications": "الأدوية النشطة", - "expDate": "تاريخ انتهاء الصلاحية النشط:", + "expDate": "تاريخ انتهاء الصلاحية:", "route": "الطريق", "frequency": "التكرار", "dailyQuantity": "��لكمية اليومية:", @@ -427,7 +427,7 @@ "doctorResponses": "ردود الطبيب", "ne_w": "جديد", "all": "الكل", - "dailyWater": "فحص الماء اليومي", + "dailyWater": "متابعة الماء اليومية", "questionHere": "أدخل السؤال هنا...", "viewDoctorResponses": "عرض ردود الطبيب", "serviceInformationButton": "تسجيل الدخول / التسجيل", @@ -452,7 +452,7 @@ "brands": "العلامات التجارية", "productDetails": "تفاصيل المنتج", "medicationRefill": "إعادة تعبئة الدواء", - "pillReminder": "تذكير بالحبوب", + "pillReminder": "تذكير الأدوية", "shippingAddresses": "عناوين الشحن", "reachUs": "تواصل معنا", "ourLocations": "مواقعنا", @@ -472,7 +472,7 @@ "cancelled": "ملغى", "writeReview": "اكتب تقييمًا", "shareReview": "شارك التقييم", - "review": " تقييمات", + "review": " التقييمات", "viewMedicalFile": "عرض التفاصيل", "viewAllServices": "عرض جميع الخدمات", "medicalFile": "الملف الطبي", @@ -488,7 +488,7 @@ "services2": "الخدمات", "cantSeeProfile": "لرؤية ملفك الطبي، يرجى تسجيل الدخول أو التسجيل الآن", "loginRegisterNow": "تسجيل الدخول أو التسجيل الآن", - "hmgPharmacy": "صيدلية HMG", + "hmgPharmacy": "صيدلية مجموعة الحبيب الطبية", "ecommerceSolution": "حلول التجارة الإلكترونية", "comprehensive": "شامل", "onlineConsulting": "استشارات عبر الإنترنت", @@ -525,7 +525,7 @@ "payOnline": "الدفع عبر الإنترنت", "cancelOrder": "إلغاء الطلب", "confirmAddress": "تأكيد العنوان ", - "confirmLocation": "أكيد الموقع ", + "confirmLocation": "أكد الموقع ", "conditionsHMG": "الشروط والأحكام ", "conditions": "الشروط والأحكام لكوم", "confirmDeleteMsg": "هل أنت متأكد! تريد الحذف ", @@ -635,12 +635,12 @@ "infoInsuranceCards": "تتيح لك هذه الخدمة عرض جميع بطاقات التأمين التي تم تسجيلها خلال زياراتك لمجموعة الحبيب الطبية بالإضافة إلى:", "infoInsuranceCardsPoints": "", "infoAllergies": "تتيح لك هذه الخدمة عرض جميع أنواع الحساسية المسجلة خلال زياراتك لمجموعة الحبيب الطبية.", - "infoSickLeaves": "تتيح لك هذه الخدمة عرض جميع إجازات المرضى التي تم أخذها في مجموعة الحبيب الطبية بالإضافة إلى:", + "infoSickLeaves": "تتيح لك هذه الخدمة عرض جميع الاجازات المرضيه التي تم أخذها في مجموعة الحبيب الطبية بالإضافة إلى:", "infoSickLeavePoints": "", "infoApprovals": "تتيح لك هذه الخدمة عرض جميع طلبات الموافقة التي تم إرسالها إلى شركات التأمين بالإضافة إلى:", "infoApprovalPoints": "", "infoMonthReport": "عند تفعيل هذه الخدمة، سيقوم النظام بإرسال تقرير شهري تلقائيًا إلى البريد الإلكتروني المسجل يتضمن العلامات الحيوية والنتائج لآخر الزيارات التي تمت في مجموعة الحبيب الطبية.", - "languageSetting": "لغة الرسائل القصيرة، واتساب، ومكالمات التأكيد", + "languageSetting": "الرسائل القصيرة، واتساب، لغة مكالمات التأكيد", "alert": "تنبيهات", "emailAlert": "تنبيه عبر البريد الإلكتروني", "smsAlert": "تنبيه عبر الرسائل القصيرة", @@ -648,7 +648,7 @@ "emrgName": "اسم جهة الاتصال في حالات الطوارئ", "emrgNo": "رقم جهة الاتصال في حالات الطوارئ", "modes": "الأوضاع", - "vibration": "اهتزاز ردود الفعل اللمسية", + "vibration": "اهتزاز عند اللمس", "blindModes": "أوضاع للمكفوفين جزئيًا", "invertTheme": "عكس", "offTheme": "إيقاف", @@ -666,7 +666,7 @@ "findUs": "اعثر علينا", "liveChat": "دردشة مباشرة", "service": "خدمة", - "hmgServiceLabel": "خدمة HMG", + "hmgServiceLabel": "خدمة مجموعة الحبيب الطبية", "healthWeatherIndicators": "مؤشرات الطقس الصحية", "healthTipsBasedOnCurrentWeather": "نصائح صحية بناءً على الطقس الحالي", "moreDetails": "المزيد من التفاصيل", @@ -705,7 +705,7 @@ "compliment": "تقدير", "suggestion": "اقتراح", "yourFeedback": "تم إرسال ملاحظاتك", - "selectPart": "يرجى اختيار الجزء الذي تشتك�� منه", + "selectPart": "يرجى اختيار الجزء الذي تشتكي منه", "number": "رقم", "notClassified": "غير مصنف", "selectClinic": "اختر العيادة", @@ -714,13 +714,13 @@ "youCanFind": "يمكنك العثور على ", "itemInSearch": " عنصر في البحث", "bloodDonation": "التبرع بالدم", - "bloodDonationInfo": "من خلال هذه الخدمة، يمكنك تسجيل اسمك كمتبرع بالدم ��يث سيتواصل معك بنك الدم في مجموعة الحبيب الطبية في حالة الحاجة إلى فصيلة الدم.", + "bloodDonationInfo": "من خلال هذه الخدمة، يمكنك تسجيل اسمك كمتبرع بالدم حيث سيتواصل معك بنك الدم في مجموعة الحبيب الطبية في حالة الحاجة إلى فصيلة الدم.", "bloodInstruction": "أدخل المعلومات المطلوبة، من أجل التسجيل في خدمة التبرع بالدم", "viewTerms": "لعرض الشروط والأحكام", - "wantConnectHmgNetwork": "عزيزي العميل، لا يوجد وصول إلى الإنترنت، هل ترغب في الاتصال بشبكة HMG لاستخدام تطبيقنا، تأكد من أنك في نطاق شبكة HMG", - "failedToAccessHmgServices": "متصل بشبكة HMG،\n\nلكن فشل في الوصول إلى خدمات HMG", + "wantConnectHmgNetwork": "عزيزي العميل، لا يوجد وصول إلى الإنترنت، هل ترغب في الاتصال بشبكة مجموعة الحبيب الطبية لاستخدام تطبيقنا، تأكد من أنك في نطاق شبكة مجموعة الحبيب الطبية", + "failedToAccessHmgServices": "متصل بشبكة مجموعة الحبيب الطبية،\n\nلكن فشل في الوصول إلى خدمات مجموعة الحبيب الطبية", "offerAndPackages": "عروض وباقات", - "offerAndPackagesDetails": "تتيح لك هذه الخدمة عرض جميع عروض HMG:", + "offerAndPackagesDetails": "تتيح لك هذه الخدمة عرض جميع عروض مجموعة الحبيب الطبية:", "invoiceNo": "رقم الفاتورة", "invoiceDate": "تاريخ الفاتورة", "specialResult": "نتيجة خاصة", @@ -739,25 +739,25 @@ "excellent": "ممتاز", "average": "متوسط", "infoSigns": "تتيح لك هذه الخدمة عرض جميع العلامات الحيوية التي تم قياسها في مجموعة الحبيب الطبية، مثل (الطول، الوزن، مؤشر كتلة الجسم، معدل ضربات القلب، إلخ.) بالإضافة إلى عرض بعض الرسوم البيانية الإحصائية.", - "infoAdvancePayment": "تم تصميم هذه الخدمة بحيث يمكنك إيداع مبلغ مقدم إما في حسابك أو في حساب شخص آخر في مجموعة الحبيب الطبية.", + "infoAdvancePayment": "تم تصميم هذه الخدمة بحيث يمكنك إيداع مبلغ مقدم إما في حسابك أو في حساب شخص آخر، في مجموعة الحبيب الطبية.", "infoMyBalance": "تتيح لك هذه الخدمة التحقق من رصيدك في جميع الفروع", "erContant": "تظهر هذه الخدمة أقرب فرع من بين جميع فروع مجموعة الحبيب الطبية بناءً على موقعك الحالي.", "infoAmbulance": "من خلال هذه الخدمة، يمكنك طلب الإخلاء بواسطة سيارة إسعاف، سواء من المنزل أو إلى المنزل، بالإضافة إلى مجموعة من الخدمات الأخرى", "infoMyAppointments": "تتيح لك هذه الخدمة رؤية جميع المواعيد التي زرتها في مجموعة الحبيب الطبية، ومن خلال هذه الخدمة:", - "infoTodo": "تم تصميم هذه الخدمة لتمكينك من الحصول على رابط سريع لقائمة المهام التي تحتاج إلى القيام بها", + "infoTodo": "تم تصميم هذه الخدمة لتمكينك من الحصول على رابط سريع لقائمة المهام التي تحتاج القيام بها", "familyInfo": "من خلال هذه الخدمة، ستتمكن من ربط ملفات عائلتك الطبية بملفك الطبي حتى تتمكن من إدارة سجلاتهم عن طريق تسجيل الدخول إلى ملفك الطبي.", "rrtdDetails": "تقدم فريق الاستجابة السريعة خدمة طبية شاملة لجميع أنواع الحالات العاجلة والمستقرة من جميع الأعمار، بما في ذلك البالغين والمرضى الأطفال، وتغطي الخدمة فريق طبي مدرب تدريبًا عاليًا ومجهز تجهيزًا كاملاً قادرًا على تقديم أفضل رعاية طبية في منزل المريض.", "homeHealthCareText": "تقدم هذه الخدمة مجموعة من خدمات الرعاية الصحية المنزلية، والمتابعة المستمرة والشاملة في أماكن إقامتهم لأولئك الذين لا يمكنهم الوصول إلى المرافق الصحية، مثل (تحليلات المختبر - الأشعة - التطعيمات - العلاج الطبيعي)، إلخ.", - "onlineCheckInAgreement": "تسجيل الوصول عبر الإنترنت مخصص للحال��ت التي لا تهدد الحياة. اتصل بالهلال الأحمر (997) أو اذهب إلى أقرب قسم طوارئ إذا كان هناك: \n\nعلامات السكتة الدماغية أو النوبة القلبية \nhالة من النوبات أو الإغماء \nهناك إصابة تهدد الطرف أو الحياة \nصورة لإصابات خطيرة", + "onlineCheckInAgreement": "تسجيل الوصول عبر الإنترنت مخصص للحالات التي لا تهدد الحياة. اتصل بالهلال الأحمر (997) أو اذهب إلى أقرب قسم طوارئ إذا كان هناك: \n\nعلامات السكتة الدماغية أو النوبة القلبية \nhالة من النوبات أو الإغماء \nهناك إصابة تهدد الطرف أو الحياة \nصورة لإصابات خطيرة", "infoEreferral": "تتيح لك هذه الخدمة تقديم طلب إحالة من أي مقدم رعاية صحية سواء داخل أو خارج المملكة العربية السعودية إلى أي من مستشفيات HMG، عن طريق ملء بعض بيانات المريض وإرفاق التقارير الطبية، علاوة على ذلك يمكنك تتبع حالة الطلب (قيد المعالجة، مقبول أو مرفوض)", "erConsultation": "تتيح لك هذه الخدمة إجراء استشارة افتراضية عبر الإنترنت عبر مكالمة فيديو مباشرة مع الطبيب من أي مكان وفي أي وقت.", "myInvoice": "القائمة", "invoiceList": "فواتيري", "thisItemIsNotAvailable": "هذا العنصر غير متوفر", "beforeAfterImages": "صور قبل وبعد", - "clinicAcceptLivecare": "لا حاجة للانتظار أو الزيارة يمكنك الآن الحصول على استشارة طبية عبر مكالمة فيديو (خدمة الرعاية الحية) في اسم العيادة وسيتصل بك الطبيب على الور", - "livecareModalTop": "تقبل هذه العيادة خدمات الرعاية الحية", - "livecarePoint2": "��يتصل بك طبيب متخصص", + "clinicAcceptLivecare": "لا حاجة للانتظار أو الزيارة يمكنك الآن الحصول على استشارة طبية عبر مكالمة فيديو (لايف كير) في اسم العيادة وسيتصل بك الطبيب على الفور", + "livecareModalTop": "تقبل هذه العيادة خدمات ال لايف كير", + "livecarePoint2": "سوف يتصل بك طبيب متخصص", "livecarePoint3": "سيكون لدى الطبيب القدرة على رؤية تاريخ ملفك الطبي الكامل", "livecarePoint4": "تتوفر خدمة توصيل الأدوية المجانية", "livecarePoint5": "لا حاجة للزيارة، الخدمة في مكانك", @@ -787,7 +787,7 @@ "enterEmailDesc": "أدخل عنوان بريدك الإلكتروني لإكمال عملية إنشاء ملف طبي", "enterPhoneDesc": "أدخل رقم هاتفك لتلقي رمز التحقق ", "pleaseChooseOption": "الرجاء اختيار من الخيارات أدناه لتلقي رمز التحقق OTP", - "prepareToElevate": "هل أنت مستعد لتحسين صحتك ورفاهتك؟", + "prepareToElevate": "هل أنت مستعد لتحسين صحتك ورفاهيتك؟", "iAcceptTermsConditions": "أوافق على الشروط والأحكام", "alreadyHaveAccount": "هل لديك حساب بالفعل؟", "loginNow": "تسجيل الدخول الآن", @@ -804,7 +804,7 @@ "guest": "زائر", "switchAccount": "تبديل الحساب", "lastLoginBy": "آخر تسجيل دخول بواسطة", - "allSet": "جاهز! الآن يمكنك تسجيل الدخول باستخدام Face ID / Biometric أو البصمة", + "allSet": "جاهز! الآن يمكنك تسجيل الدخول باستخدام Face ID أو البصمة", "enableQuickLogin": "تمكين تسجيل الدخول السريع", "enableMsg": "تمكين تسجيل الدخول السريع سيسمح بالتحقق من خلال Face ID / Biometric الخاص بجهازك الحالي", "notNow": "ليس الآن", @@ -849,12 +849,12 @@ "searchHospital": "بحث في المستشفى", "skip": "تخطي", "getStarted": "ابدأ الآن", - "onboardingHeading1": "حجز المواعيد لم يكن أسهل من قبل", + "onboardingHeading1": "حجز المواعيد لم يكن أسهل من الآن.", "onboardingBody1": "ببضع نقرات فقط يمكنك استشارة الطبيب الذي تختاره.", "onboardingHeading2": "الوصول إلى السجل الطبي بين يديك", "onboardingBody2": "تتبع تاريخك الطبي بما في ذلك الفحوصات المخبرية، الوصفات الطبية، التأمين، وغيرها.", - "hmgHospitals": "مستشفيات HMG", - "hmcMedicalClinic": "مراكز HMC الطبية", + "hmgHospitals": "مستشفيات مجموعة الحبيب الطبية", + "hmcMedicalClinic": "مراكز مجموعة الحبيب الطبية", "applyFilter": "تطبيق الفلتر", "facilityAndLocation": "المرفق والموقع", "regionAndLocation": "المنطقة والمواقع", @@ -874,7 +874,6 @@ "laserClinic": "عيادة الليزر", "continueString": "يكمل", "covid_info": "تجري مستشفيات د. سليمان الحبيب فحص فيروس كورونا المستجد وتصدر شهادات السفر على مدار الساعة، طوال أيام الأسبوع، وبسرعة ودقة عالية. يمكن للراغبين في الاستفادة من هذه الخدمة زيارة أحد فروع مستشفيات د. سليمان الحبيب وإجراء فحص كورونا خلال بضع دقائق والحصول على النتائج خلال عدة ساعات خدمة فحص فيروس كورونا Covid 19 بتقنية PCR للكشف عن الفيروس وفقاً لأعلى المعايير العالمية وبأحدث أجهزة RT-PCR عالية الدقة (GeneXpert الأمريكي وغيره)، وهي طرق معتمدة من قبل هيئة الغذاء والدواء وكذلك من قبل المركز السعودي للوقاية من الأمراض المُعدية", - "appointmentDetails": "تفاصيل الموعد", "checkingDoctorAvailability": "جاري التحقق من توفر الطبيب...", "cancellingAppointmentPleaseWait": "جاري إلغاء الموعد، يرجى الانتظار...", @@ -924,7 +923,7 @@ "offersFreeMedicineDelivery": "يوفر توصيل مجاني للأدوية لموعد لايف كير", "dentalChiefComplaints": "الشكاوى الرئيسة للأسنان", "viewAvailableAppointments": "عرض المواعيد المتاحة", - "doctorProfile": "الملف للطبيب", + "doctorProfile": " ملف الطبيب", "waitingAppointment": "موعد الانتظار", "hospitalInformation": "معلومات المستشفى", "fetchingAppointmentShare": "جاري جلب تفاصيل الموعد...", @@ -960,10 +959,10 @@ "visaOrMastercard": "فيزا أو ماستركارد", "tamara": "تمارا", "fetchingApplePayDetails": "جاري جلب تفاصيل Apple Pay، يرجى الانتظار...", - "liveCarePendingRequest": "لايف كير حية معلق", + "liveCarePendingRequest": "طلب لايف كير قيد الانتظار", "callLiveCareSupport": "اتصل بدعم لايف كير", "whatIsWaitingAppointment": "ما هو موعد الانتظار؟", - "waitingAppointmentsFeature": "تتيح لك ميزة مواعيد الانتظار حجز موعد أثناء تواجدك داخل مبنى المستشفى، وفي حال عدم توفر فتحة متاحة في جدول الطبيب.", + "waitingAppointmentsFeature": "تتيح لك ميزة مواعيد الانتظار حجز موعد أثناء تواجدك داخل مبنى المستشفى، وفي حال عدم توفر مواعيد متاحة في جدول الطبيب.", "appointmentWithDoctorConfirmed": "الموعد مع الطبيب مؤكد، ولكن وقت الدخول غير محدد.", "paymentWithinTenMinutes": "ملاحظة: يجب عليك الدفع خلال 10 دقائق من الحجز، وإلا سيتم إلغاء موعدك تلقائياً", "liveLocation": "الموقع المباشر", @@ -1233,7 +1232,7 @@ "invalidWeight": "الوزن غير صالح", "timeToDrinkWater": "حان وقت شرب الماء! 💧", "stayHydratedDrinkWater": "ابق رطبًا! اشرب {amount} مل من الماء.", - "visitPharmacyOnline": "زيارة الصيدلة على الانترنت", + "visitPharmacyOnline": "زيارة الصيدلية على الانترنت", "howAreYouFeelingToday": "كيف حالك اليوم؟", "checkYourSymptomsWithScale": "تحقق من أعراضك باستخدام ذا المقياس", "checkYourSymptoms": "تحقق من أعراضك", @@ -1241,7 +1240,7 @@ "youHaveEROnlineCheckInRequest": "لديك طلب تسجيل وصول عبر الإنترنت للطوارئ", "quickLinks": "روابط سريعة", "viewMedicalFileLandingPage": "عرض الملف الطبي", - "immediateLiveCareRequest": "طلب LiveCare الفوري", + "immediateLiveCareRequest": "طلب لايف كير فوري", "yourTurnIsAfterPatients": "دورك بعد {count} مريض.", "dontHaveHHCOrders": "ليس لديك أي أوامر رعاية صحية منزلية حتى الآن.", "hhcOrders": "أوامر الرعاية الصحية المنزلية", @@ -1257,7 +1256,7 @@ "insuranceActive": "التأمين نشط", "patientCardID": "رقم بطاقة المريض: {id}", "noInsuranceApprovals": "لم تحصل على أي موافقات تأمينية حتى الآن.", - "noInsuranceWithHMG": "ليس لديك تأمين مسجل لدى مجموعة حبيب الطبية.", + "noInsuranceWithHMG": "ليس لديك تأمين مسجل لدى مجموعة الحبيب الطبية.", "referenceRange": "النطاق المرجعي", "downloadReport": "تنزيل التقرير", "generatingReport": "جارٍ إنشاء التقرير، يرجى الانتظار...", @@ -1302,12 +1301,12 @@ "updatingMonthlyReportStatus": "جاري تحديث حالة التقرير الشهري...", "monthlyReportStatusUpdatedSuccessfully": "تم تحديث حالة التقرير الشهري بنجاح", "whoCanViewMyMedicalFile": "من يمكنه عرض ملفي الطبي؟", - "acceptedYourRequestToBeYourFamilyMember": "{status} طلبك لتكون فردًا من عائلتك", + "acceptedYourRequestToBeYourFamilyMember": "{status} طلب الانضمام الى أفراد عائلتك", "canViewYourFile": "يمكنه عرض ملفك", "hasARequestPendingToBeYourFamilyMember": "لديه طلب {status} ليكون فردًا من عائلتك", - "wantsToAddYouAsTheirFamilyMember": "يريد إضافتك كفرد من عائلته", - "rejectedYourRequestToBeYourFamilyMember": "{status} طلبك لتكون فردًا من عائلتك", - "rejectedYourFamilyMemberRequest": "{status} طلب فرد عائلتك", + "wantsToAddYouAsTheirFamilyMember": "يرغب في إضافتك كفرد من أفراد العائلة", + "rejectedYourRequestToBeYourFamilyMember": "{status} طلب الانضمام إلى عائلتك", + "rejectedYourFamilyMemberRequest": "{status} طلب أحد أفراد عائلتك", "notAvailable": "غير متاح", "selectAProfile": "الرجاء تحديد ملف تعريف", "switchFamilyFile": "قم بالتبديل من قائمة الملفات الطبية أدناه", @@ -1386,7 +1385,7 @@ "seekMedicalAdviceDescription": "ليست حالة طوارئ ولكن من الأفضل مراقبة الأعراض.", "emergencyDescription": "تحتاج إلى استشارة الطبيب في أقرب وقت ممكن قبل فوات الأوان.", "colorScience": "دلالات الألوان", - "noNeedToVisitDoctor": "لا حاجة لزيرة الطبيب", + "noNeedToVisitDoctor": "لا حاجة لزيارة الطبيب", "commonSymptom": "الأعراض الشائعة", "bookAppointment": "احجز موعد", "notApplicable": "لا ينطبق", @@ -1423,7 +1422,7 @@ "approved": "موافق عليه", "approvalRejectedPleaseVisitReceptionist": "تم رفض الموافقة - يرجى زيارة موظف الاستقبال", "sentForApproval": "تم إرساله للموافقة", - "ancillaryOrderDetails": "تفاصيل الطلب المساعد", + "ancillaryOrderDetails": "تفاصيل الطلبات المساندة", "noProceduresAvailableForSelectedOrder": "لا توجد إجراءات متاحة للطلب المحدد.", "procedures": "الإجراءات", "totalAmount": "المبلغ الإجمالي", @@ -1492,7 +1491,6 @@ "yourName": "اسمك", "ageYears": "العمر (11-120) سنة", "numberOfRemindersInADay": "عدد التذكيرات في اليوم", - "medications": "الأدوية", "someRemarksAboutPrescription": "ستجدون هنا بعض الملاحظات حول الوصفة الطبية", "notifyMeBeforeConsumptionTime": "أبلغني قبل وقت الاستهلاك", @@ -1545,19 +1543,19 @@ "bmrNormalDescription": "معدل الأيض الأساسي لديك ضمن النطاق المتوقع لحالتك. معدل الأيض الأساسي هو الطاقة التي يحتاجها جسمك في حالة الراحة للحفاظ على وظائفه الحيوية (التنفس، الدورة الدموية، درجة الحرارة). استخدم هذه القيمة كأساس لتقدير إجمالي احتياجاتك اليومية من الطاقة من خلال تطبيق مُعامل النشاط. حافظ على نظام غذائي متوازن وممارسة النشاط البدني بانتظام لدعم صحة التمثيل الغذائي.", "bmrLowDescription": "معدل الأيض الأساسي لديك أقل من المعدل الطبيعي لحالتك. تشمل الأسباب المحتملة انخفاض كتلة العضلات، أو تراجع التمثيل الغذائي المرتبط بالتقدم في السن، أو التكيف الأيضي الناتج عن تقييد السعرات الحرارية لفترة طويلة. يُنصح بالتركيز على تمارين المقاومة لبناء العضلات والحفاظ عليها، والتأكد من تناول كميات كافية من البروتين والمغذيات الدقيقة، واستشارة أخصائي رعاية صحية إذا كان هذا الأمر غير متوقع.", "bmrHighDescription": "معدل الأيض الأساسي لديك أعلى من المتوسط \u200B\u200Bبالنسبة لحالتك. قد يعكس هذا زيادة في كتلة العضلات، أو صغر السن، أو ارتفاع معدل الأيض لديك بشكل طبيعي، مما يعني أنك تحرق طاقة أكبر أثناء الراحة. استخدم هذه المعلومات لتحديد احتياجاتك من السعرات الحرارية: احتياجات أعلى للحفاظ على الوزن أو بناء العضلات، ونقص معتدل في السعرات الحرارية لإنقاص الوزن. استشر طبيبك إذا لاحظت أي تغيرات سريعة غير مبررة.", - "ibwAtTargetDescription": "Your weight is at the ideal target. Maintain your current habits — balanced diet and regular activity — to keep this.", - "ibwBelowTargetDescription": "You are below the ideal weight by {kg} kg. Aim to gradually increase weight with a modest calorie surplus, nutrient-dense foods, and resistance training.", - "ibwAboveTargetDescription": "You are above the ideal weight by {kg} kg. Aim for a gradual, sustainable weight reduction through a moderate calorie deficit, increased activity, and balanced nutrition.", - "ibwCloseToRangeDescription": "Your weight is close to the ideal range. Maintain a balanced diet and regular activity; follow personalized advice from a healthcare professional if needed.", - "ibwUnderweightDescription": "You are below the ideal weight by {kg} kg. Focus on increasing calorie intake with nutrient-dense foods, prioritize protein and resistance training to build healthy mass, and consider consulting a healthcare professional.", - "ibwUnderweightGenericDescription": "You appear underweight compared to the ideal. Consider increasing calorie intake with nutrient-dense foods and resistance training; seek professional guidance if needed.", - "ibwOverweightDescription": "You are above the ideal weight by {kg} kg. Aim for a gradual, sustainable weight loss strategy — moderate calorie deficit, regular physical activity, and balanced nutrition. Consult a professional before major changes.", - "ibwOverweightGenericDescription": "You appear above the ideal weight. Consider a gradual, sustainable calorie deficit combined with activity and balanced nutrition; consult a professional if needed.", - "dietVeryLowCarbsDescription": "Very low‑carb (ketogenic) approach: minimal carbohydrates, higher fat and moderate protein. May support rapid weight loss and improved blood sugar control for some, but can be hard to sustain and may cause nutrient gaps. Monitor hydration/electrolytes and consult a professional for long‑term use.", - "dietLowCarbsDescription": "Low‑carb, higher‑protein plan: reduced carbohydrates with increased protein to support satiety and muscle maintenance. Helpful for weight management and glycemic control when balanced with vegetables and healthy fats. Ensure adequate fiber and micronutrients.", - "dietModerateCarbsDescription": "Moderate carbohydrate intake with balanced protein and fats: provides steady energy for daily activity and exercise while allowing flexibility. Suitable for many people aiming for sustainable weight management and performance.", - "dietUSDAGuidelinesDescription": "USDA‑based balanced pattern: emphasizes fruits, vegetables, whole grains, lean proteins, and limited added sugars and saturated fats. Evidence‑based framework for general health, nutrient adequacy, and chronic disease prevention.", - "dietZoneDietDescription": "Zone Diet (~40% carbs / 30% protein / 30% fat): emphasizes hormonal balance and portion control. May improve body composition and energy for some, but requires planning; personalize with a nutrition professional.", + "ibwAtTargetDescription": "وزنك في النطاق المثالي. حافظ على عاداتك الحالية — نظام غذائي متوازن ونشاط بدني منتظم — للاستمرار على هذا المستوى.", + "ibwBelowTargetDescription": "أنتِ أقل من الوزن المثالي بمقدار {kg} كجم. يُنصح بزيادة الوزن تدريجيًا من خلال فائضٍ بسيط في السعرات الحرارية، وتناول أطعمة غنية بالعناصر الغذائية، مع ممارسة تمارين المقاومة.", + "ibwAboveTargetDescription": "أنتِ أعلى من الوزن المثالي بمقدار {kg} كجم. يُنصح باتباع خفضٍ تدريجي ومستدام للوزن من خلال عجزٍ معتدل في السعرات الحرارية، وزيادة مستوى النشاط البدني، واتباع تغذية متوازنة.", + "ibwCloseToRangeDescription": "وزنك قريب من النطاق المثالي. حافظ على نظام غذائي متوازن ونشاط بدني منتظم، مع اتباع إرشادات شخصية من مختص صحي عند الحاجة.", + "ibwUnderweightDescription": "أنت أقل من الوزن المثالي بمقدار {kg} كجم. ركّز على زيادة السعرات الحرارية من خلال أطعمة غنية بالعناصر الغذائية، وأعط الأولوية للبروتين وتمارين المقاومة لبناء كتلة صحية، مع اتباع إرشادات شخصية من مختص صحي.", + "ibwUnderweightGenericDescription": "يبدو أنكِ أقل من الوزن المثالي. يُنصح بزيادة السعرات الحرارية من خلال أطعمة غنية بالعناصر الغذائية وممارسة تمارين المقاومة؛ مع الحصول على استشارة مختص صحي عند الحاجة.", + "ibwOverweightDescription": "أنت أعلى من الوزن المثالي بمقدار {kg} كجم. يُنصح باتباع استراتيجية تدريجية ومستدامة لفقدان الوزن — من خلال عجزٍ معتدل في السعرات الحرارية، وممارسة نشاط بدني منتظم، وتغذية متوازنة. يُفضّل استشارة مختص صحي قبل إجراء تغييرات كبيرة.", + "ibwOverweightGenericDescription": "يبدو أنك أعلى من الوزن المثالي. يُنصح باتباع عجزٍ تدريجي ومستدام في السعرات الحرارية مع ممارسة النشاط البدني واتباع تغذية متوازنة؛ مع الحصول على استشارة مختص صحي عند الحاجة.", + "dietVeryLowCarbsDescription": "نهج منخفض الكربوهيدرات جدًا (الكيتوني): تقليل الكربوهيدرات إلى الحد الأدنى مع زيادة الدهون وبروتين معتدل. قد يساعد على فقدان الوزن بسرعة وتحسين التحكم في سكر الدم لدى بعض الأشخاص، لكنه قد يكون صعب الاستمرار عليه وقد يسبب نقصًا في بعض العناصر الغذائية. يجب مراقبة الترطيب ، والحصول على استشارة مختص صحي عند استخدامه لفترة طويلة.", + "dietLowCarbsDescription": "خطة منخفضة الكربوهيدرات وعالية البروتين: تقليل الكربوهيدرات مع زيادة البروتين لدعم الشعور بالشبع والحفاظ على الكتلة العضلية. مفيدة لإدارة الوزن وضبط سكر الدم عند موازنتها مع الخضروات والدهون الصحية. احرص على الحصول على كمية كافية من الألياف والعناصر الدقيقة (الفيتامينات والمعادن).", + "dietModerateCarbsDescription": "تناول معتدل للكربوهيدرات مع توازن في البروتين والدهون: يوفر طاقة مستقرة للأنشطة اليومية والتمارين مع قدر من المرونة. مناسب لكثير من الأشخاص الذين يسعون لإدارة وزن مستدامة وتحسين الأداء.", + "dietUSDAGuidelinesDescription": "نمط متوازن مستند إلى (USDA): يركز على الفواكه والخضروات والحبوب الكاملة والبروتينات قليلة الدهن، مع الحد من السكريات المضافة والدهون المشبعة. يُعد إطارًا قائمًا على الأدلة لدعم الصحة العامة، وكفاية العناصر الغذائية، والوقاية من الأمراض المزمنة.", + "dietZoneDietDescription": "حمية الزون (~40٪ كربوهيدرات / 30٪ بروتين / 30٪ دهون): تركز على التوازن الهرموني وضبط الحصص الغذائية. قد تُحسّن تركيب الجسم ومستوى الطاقة لدى بعض الأشخاص، لكنها تتطلب تخطيطًا جيدًا؛ ويُفضّل تخصيصها بالتعاون مع مختص تغذية.", "contextDependent": "يعتمد السياق", "calculatedValue": "القيمة المحسوبة", "calculationFormula": "صيغة الحساب", @@ -1595,5 +1593,9 @@ "whoopwatch": "ساعة Whoop", "whoophealthapplicationshouldbeinstalledinyourphone": "يجب تثبيت تطبيق Whoop Health على هاتفك", "whoopwatchshouldbeconnected": "يجب توصيل ساعة Whoop", - "updatetheinformation": "سيتيح ذلك جمع أحدث المعلومات من ساعة آبل الخاصة بك" + "updatetheinformation": "سيتيح ذلك جمع أحدث المعلومات من ساعة آبل الخاصة بك", + "continueCash": "متابعة الدفع نقدًا", + "timeFor": "الوقت", + "hmgPolicies": "سياسات مجموعة الحبيب الطبية", + "darkMode": "المظهر الداكن" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 73a8e8cf..fc0707f7 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -92,7 +92,7 @@ "reminder": "Reminder", "location": "Locations", "online": "Online", - "payment": "الالكتروني", + "payment": "Payment", "onlinePayment": "Online", "onlinePaymentSubtitle": "Payment", "cancelNocaps": "Cancel", @@ -272,9 +272,9 @@ "appointments": "Appointments", "all2": "All", "checkinOption": "Check-In", - "readBarcode": "قراءة الكود", - "showMyPark": "عرض الموقف", - "clearMyData": "امسح البيانات", + "readBarcode": "Read Barcode", + "showMyPark": "Show My Park", + "clearMyData": "Clear My Data", "floor": "Floor:", "gate": "Gate:", "building": "Building:", @@ -357,7 +357,7 @@ "userAgreement": "User Agreement", "updateSuccessfully": "Update Successfully", "checkVaccineAvailability": "CHECK VACCINE AVAILABILITY", - "myVaccinesAvailability": "MyVaccinesAvailability", + "myVaccinesAvailability": "My Vaccines Availability", "paymentService": "Payment Service", "paymentOnline": "Service", "onlineCheckIn": "Online Check-In", @@ -393,7 +393,7 @@ "theVerificationCodeExpiresIn": "The Verification Code Expires in", "pleaseEnterTheVerificationCode": "Please enter the verification code send to", "eyeMeasurements": "Eye Measurements", - "measurements": "قياسات", + "measurements": "Measurements", "classes": "Glasses", "contactLens": "Contact Lens", "rightEye": "Right Eye", @@ -566,8 +566,8 @@ "transferred": "Transferred", "riyal": "RIYAL", "membersince": "MEMBER SINCE", - "identification": "رقم الهوية", - "lakumMobile": "رقم الجوال", + "identification": "Identification Number", + "lakumMobile": "Mobile Number", "waitinggained": "Waiting gained", "expired": "Expired", "willBeExpired": "Will Be Expired", @@ -616,10 +616,10 @@ "bloodType": "Blood Type", "maritalStatus": "Marital status", "general": "General", - "profile": "ملفي", - "notifications": "إشعارات", + "profile": "My Profile", + "notifications": "Notifications", "notificationDetails": "Notification Details", - "notificationDetailsa": "تفاصيل الاشعار", + "notificationDetailsa": "Notification Details", "infoMyDoctorPoints": "", "infoMyDoctor": "This service allows you to see all the doctors you have visited in Al Habib Medical Group, and through this service:", "infoPrescriptions": "This service allows you to view all the medical prescriptions issued by Al Habib Medical Group, and through this service, you can:", @@ -1586,6 +1586,9 @@ "whoopwatch": "Whoop Watch", "whoophealthapplicationshouldbeinstalledinyourphone": "Whoop Health application should be installed in your phone", "whoopwatchshouldbeconnected": "Whoop Watch should be connected", - "updatetheinformation": "This will allow to gather the most up to date information from your apple watch" - + "updatetheinformation": "This will allow to gather the most up to date information from your apple watch", + "continueCash": "Continue as cash", + "timeFor": "Time For", + "hmgPolicies": "HMG Policies", + "darkMode": "Dark Mode" } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 970b1e42..921e274b 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -195,11 +195,13 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4773715; + // body['PatientID'] = 809289; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; + //VIP Patient: 1181868 + // body['IdentificationNo'] = "2235558844"; // body['MobileNo'] = "966533147722"; // body['PatientID'] = 809289; @@ -299,14 +301,16 @@ class ApiClientImp implements ApiClient { } else if (parsed['MessageStatus'] == 1 || parsed['SMSLoginRequired'] == true) { onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); - } else if (parsed['IsAuthenticated'] == false) { - onFailure( - "User is not Authenticated", - statusCode, - failureType: - UnAuthenticatedUserFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'] ?? "User is not Authenticated", url: url), - ); - } else if (parsed['MessageStatus'] == 2 && parsed['IsAuthenticated']) { + } + // else if (parsed['IsAuthenticated'] == false) { + // onFailure( + // "User is not Authenticated", + // statusCode, + // failureType: + // UnAuthenticatedUserFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'] ?? "User is not Authenticated", url: url), + // ); + // } + else if (parsed['MessageStatus'] == 2 && parsed['IsAuthenticated']) { if (parsed['SameClinicApptList'] != null) { onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index a57169d2..ecfbc70a 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -57,7 +57,7 @@ class ApiConsts { GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/uat/'; QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; - CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId"; + CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId="; break; case AppEnvironmentTypeEnum.uat: baseUrl = "https://uat.hmgwebservices.com/"; @@ -69,7 +69,7 @@ class ApiConsts { GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/uat/'; QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; - CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId"; + CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId="; break; case AppEnvironmentTypeEnum.preProd: baseUrl = "https://webservices.hmg.com/"; @@ -81,7 +81,7 @@ class ApiConsts { GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/'; QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; - CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId"; + CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId="; break; case AppEnvironmentTypeEnum.qa: baseUrl = "https://uat.hmgwebservices.com/"; @@ -93,7 +93,7 @@ class ApiConsts { GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/uat/'; QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; - CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId"; + CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId="; break; case AppEnvironmentTypeEnum.staging: baseUrl = "https://uat.hmgwebservices.com/"; @@ -105,7 +105,7 @@ class ApiConsts { GET_TAMARA_PAYMENT_STATUS = 'https://epharmacy.hmg.com/tamara/api/OnlineTamara/order_status?orderid='; rcBaseUrl = 'https://rc.hmg.com/uat/'; QLINE_URL = "https://ms.hmg.com/nscapi/api/PatientCall/PatientInQueue_Detail"; - CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId"; + CHAT_URL = "https://chat.hmg.com/geneysChat/Index.aspx?RequestedId="; break; } } @@ -229,7 +229,7 @@ class ApiConsts { static String getAiOverViewLabOrder = "Services/Patients.svc/REST/HMGAI_Lab_Analyzer_API"; // ************ static values for Api **************** - static final double appVersionID = 19.7; + static final double appVersionID = 20.2; // static final double appVersionID = 50.7; static final int appChannelId = 3; static final String appIpAddress = "10.20.10.20"; @@ -517,8 +517,6 @@ var IS_ALLOW_ASK_DOCTOR = 'Services/Doctors.svc/REST/GetPatientDoctorAppointment var GET_CALL_REQUEST_TYPE = 'Services/Doctors.svc/REST/GetCallRequestType_LOV'; var ADD_VIDA_REQUEST = 'Services/ER_VirtualCall.svc/REST/PatientER_VidaRequestInseart'; -var SEND_CALL_REQUEST = 'Services/Doctors.svc/REST/InsertCallInfo'; - var GET_LIVECARE_CLINICS = 'Services/ER_VirtualCall.svc/REST/PatientER_GetClinics'; var GET_LIVECARE_SCHEDULE_CLINICS = 'Services/Doctors.svc/REST/PatientER_GetClinicsHaveSchedule'; @@ -916,6 +914,8 @@ var GET_PRESCRIPTION_INSTRUCTIONS_PDF = 'Services/ChatBot_Service.svc/REST/Chatb const DASHBOARD = 'Services/Patients.svc/REST/PatientDashboard'; +const SEND_PATIENT_IMMEDIATE_UPDATE_INSURANCE_REQUEST = 'Services/OUTPs.svc/REST/PatientCompanyUpdate'; + class ApiKeyConstants { static final String googleMapsApiKey = 'AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng'; } diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index f7fe9f79..403ffc5a 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -346,6 +346,7 @@ class AppAssets { static const String doctorResponseIcon = '$svgBasePath/doctor_response_icon.svg'; static const String changeLanguageHomePageIcon = '$svgBasePath/change_language_home_page.svg'; static const String aiOverView = '$svgBasePath/ai_overview.svg'; + static const String darkModeIcon = '$svgBasePath/dark_mode_icon.svg'; // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index e7ac0515..4266aa3b 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -224,7 +224,7 @@ class AppDependencies { authenticationRepo: getIt(), cacheService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), localAuthService: getIt()), ); - getIt.registerLazySingleton(() => ProfileSettingsViewModel()); + getIt.registerLazySingleton(() => ProfileSettingsViewModel(cacheService: getIt())); getIt.registerLazySingleton(() => DateRangeSelectorRangeViewModel()); diff --git a/lib/core/utils/calendar_utils.dart b/lib/core/utils/calendar_utils.dart deleted file mode 100644 index 8b21111f..00000000 --- a/lib/core/utils/calendar_utils.dart +++ /dev/null @@ -1,316 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:ui'; - -import 'package:device_calendar/device_calendar.dart'; -import 'package:flutter/widgets.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.dart'; -import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; -import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_reminder_view.dart'; -import 'package:hmg_patient_app_new/services/dialog_service.dart'; -import 'package:hmg_patient_app_new/services/permission_service.dart'; -import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:manage_calendar_events/manage_calendar_events.dart' as ios; -import 'package:permission_handler/permission_handler.dart'; -import 'package:timezone/data/latest.dart' as tzl; - -final DeviceCalendarPlugin deviceCalendarPlugin = DeviceCalendarPlugin(); -final ios.CalendarPlugin _myPlugin = ios.CalendarPlugin(); - -class CalendarUtils { - static Completer? _completer; - - dynamic get writableCalendars => calendars.firstWhere((c) => !c.isReadOnly!); - dynamic calendars; - - CalendarUtils._(this.calendars); - - // static Future getInstance() async { - // if (_completer == null) { - // _completer = Completer(); - // print(_completer!.isCompleted); - // try { - // final dynamic calendarsResult; - // if (Platform.isIOS) { - // calendarsResult = await _myPlugin.getCalendars(); - // if (!_completer!.isCompleted) { - // _completer?.complete(CalendarUtils._(await calendarsResult!)); - // } - // } else { - // calendarsResult = await deviceCalendarPlugin.retrieveCalendars(); - // if (!_completer!.isCompleted) { - // _completer?.complete(CalendarUtils._(await calendarsResult.data!)); - // } - // } - // } on Exception catch (e) { - // if (!_completer!.isCompleted) { - // _completer!.completeError(e); - // } - // } - // } - // return _completer!.future; - // } - - static Future getInstance() async { - tzl.initializeTimeZones(); - if (_completer != null) { - return _completer!.future; - } - _completer = Completer(); - try { - final dynamic calendarsResult; - if (Platform.isIOS) { - calendarsResult = await _myPlugin.getCalendars(); - _completer!.complete(CalendarUtils._(calendarsResult)); - } else { - calendarsResult = await deviceCalendarPlugin.retrieveCalendars(); - _completer!.complete(CalendarUtils._(calendarsResult.data)); - } - } catch (e) { - _completer!.completeError(e); - } - - return _completer!.future; - } - - Future createOrUpdateEvents({List? scheduleList, String? title, String? description, List? scheduleDateTime, List? daysOfWeek}) async { - tzl.initializeTimeZones(); - List events = []; - Location _currentLocation; - if (DateTime.now().timeZoneName == "+04") - _currentLocation = getLocation('Asia/Dubai'); - else - _currentLocation = getLocation('Asia/Riyadh'); - - scheduleDateTime!.forEach((element) { - RecurrenceRule recurrenceRule = RecurrenceRule( - // RecurrenceFrequency.Daily, - // daysOfWeek: daysOfWeek, - // endDate: element, - until: element, frequency: Frequency.daily, - ); - //added byAamir Tz Time - Event event = Event(writableCalendars!.id, - recurrenceRule: recurrenceRule, - start: TZDateTime.from(element, _currentLocation), - end: TZDateTime.from(element.add(Duration(minutes: 30)), _currentLocation), - title: title, - description: description); - events.add(event); - }); - - events.forEach((element) { - deviceCalendarPlugin.createOrUpdateEvent(element); - }); - } - - Future createOrUpdateEvent({required String title, required String description, required String location, DateTime? scheduleDateTime, String? eventId}) async { - RecurrenceRule recurrenceRule = RecurrenceRule( - // RecurrenceFrequency.Daily, - // daysOfWeek: daysOfWeek, - // endDate: scheduleDateTime, - until: scheduleDateTime, frequency: Frequency.daily, - ); - - Location _currentLocation; - // if (DateTime.now().timeZoneName == "+04") - // _currentLocation = getLocation('Asia/Dubai'); - // else - _currentLocation = getLocation('Asia/Riyadh'); - - TZDateTime scheduleDateTimeUTZ = TZDateTime.from(scheduleDateTime!, _currentLocation); - - print("writableCalendars-name: " + writableCalendars.name); - print("writableCalendars-Id: " + writableCalendars.id); - print("writableCalendarsToString: " + writableCalendars.toString()); - print("writableCalendarsToString: " + writableCalendars!.id!); - Event event = Event( - writableCalendars!.id, - start: scheduleDateTimeUTZ, - end: scheduleDateTimeUTZ.add(Duration(minutes: 30)), - title: title, - description: description, - ); - - ios.CalendarEvent iosCalEvent = - ios.CalendarEvent(location: location, startDate: scheduleDateTimeUTZ, endDate: scheduleDateTimeUTZ.add(Duration(minutes: 30)), title: title, description: description, isAllDay: false); - - if (Platform.isAndroid) { - Result result = await deviceCalendarPlugin.hasPermissions(); - print(result); - await deviceCalendarPlugin.createOrUpdateEvent(event).catchError((e) { - print("catchError " + e.toString()); - }).whenComplete(() { - print("whenComplete Calender ID " + eventId!); - }); - } else { - await _myPlugin.createEvent(calendarId: writableCalendars.id!, event: iosCalEvent).catchError((e) { - print("catchError " + e.toString()); - }).whenComplete(() { - print("whenComplete Calender ID iOS " + eventId!); - }); - } - } - - deleteEvent(String _calendarId, String _eventId) async { - if (Platform.isIOS) { - await _myPlugin.deleteEvent(calendarId: _calendarId, eventId: _eventId); - } else { - await deviceCalendarPlugin.deleteEvent(_calendarId, _eventId); - } - } - - Future retrieveEvents( - String calendarId, - RetrieveEventsParams retrieveEventsParams, - ) async { - if (Platform.isIOS) { - return await _myPlugin.getEvents(calendarId: calendarId); - } else { - return await deviceCalendarPlugin.retrieveEvents(calendarId, retrieveEventsParams); - } - } - - Future createCalendar( - String calendarName, { - Color? calendarColor, - String? localAccountName, - }) async { - return await deviceCalendarPlugin.createCalendar(calendarName, calendarColor: calendarColor, localAccountName: localAccountName); - } -} - -Future> requestPermissions() async { - var permissionResults = [Permission.calendarFullAccess].request(); - return permissionResults; -} - -showReminderBottomSheet(BuildContext context, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, - {required Function() onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool isMultiAllowed = false}) async { - if (Platform.isAndroid) { - if (await PermissionService.isCalendarPermissionEnabled()) { - _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, - onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); - } else { - // Utils.showPermissionConsentDialog(context, TranslationBase.of(context).calendarPermission, () async { - // if (await Permission.calendarFullAccess.request().isGranted) { - // _showReminderDialog(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, - // onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); - // } - // }); - } - } else { - if (await Permission.calendarWriteOnly.request().isGranted) { - if (await Permission.calendarFullAccess.request().isGranted) { - _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, - onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); - } - } - } -} - -Future _showReminderBottomSheet(BuildContext providedContext, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, - {required Function onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool? isMultiAllowed}) async { - showCommonBottomSheetWithoutHeight(providedContext, title: "Set the timer of reminder", child: PrescriptionReminderView( - setReminder: (int value) async { - if (!isMultiAllowed!) { - if (onMultiDateSuccess == null) { - CalendarUtils calendarUtils = await CalendarUtils.getInstance(); - await calendarUtils.createOrUpdateEvent( - title: title ?? "You have appointment with Dr. $doctorName", - description: description ?? "At $appoDateFormatted $appoTimeFormatted", - scheduleDateTime: dateTime, - eventId: eventId, - location: ''); - onSuccess(); - } - } else { - onMultiDateSuccess!(value); - } - }, - ), callBackFunc: () {}, isFullScreen: false); -} - -setCalender(BuildContext context, - {required String eventId, required int selectedMinutes, int? frequencyNumber, required int days, required String orderDate, required String itemDescriptionN, required String route}) async { - DateTime actualDate = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, 8, 0); - frequencyNumber ??= 2; //Some time frequency number is null so by default will be 2 - - int remainingDays = days - (Jiffy.parseFromDateTime(DateTime.now()).diff(Jiffy.parseFromDateTime(DateUtil.convertStringToDate(orderDate)), unit: Unit.day) as int); - if (remainingDays.isNegative) { - getIt.get().showErrorBottomSheet(message: "Prescription date has been already passed you can not add a reminder for this prescription."); - return; - } - CalendarUtils calendarUtils = await CalendarUtils.getInstance(); - - try { - for (int i = 0; i < remainingDays; i++) { - //event for number of days. - for (int j = 0; j < frequencyNumber; j++) { - // event for number of times per day. - if (j != 0) { - actualDate.add(new Duration(hours: 8)); // 8 hours addition for daily dose. - } - //Time subtraction from actual reminder time. like before 30, or 1 hour. - - actualDate = Jiffy.parseFromDateTime(actualDate).subtract(minutes: selectedMinutes).dateTime; - - calendarUtils.createOrUpdateEvent( - title: "$itemDescriptionN} Prescription Reminder", - description: "$itemDescriptionN $frequencyNumber $route ", - scheduleDateTime: actualDate, - eventId: eventId + (i.toString() + j.toString()), - location: '', //event id with varitions - ); - print("Creating event #$j for day $i → $actualDate"); - actualDate = DateTime(actualDate.year, actualDate.month, actualDate.day, 8, 0); - } - actualDate = Jiffy.parseFromDateTime(actualDate).add(days: 1).dateTime; - } - } catch (ex) { - getIt.get().showErrorBottomSheet(message: "catch:$ex"); - } -} - -Future checkAndRemove(hasReminder, {bool delete = false, String itemDescriptionN = ""}) async { - final ios.CalendarPlugin _myPlugin = ios.CalendarPlugin(); - CalendarUtils calendarUtils = await CalendarUtils.getInstance(); - DateTime startEventsDate = Jiffy.parseFromDateTime(DateTime.now()).subtract(days: 30).dateTime; - DateTime endEventsDate = Jiffy.parseFromDateTime(DateTime.now()).add(days: 120).dateTime; - RetrieveEventsParams params = RetrieveEventsParams(startDate: startEventsDate, endDate: endEventsDate); - - if (calendarUtils.calendars != null) { - if (Platform.isAndroid) { - await processEvents(calendarUtils.calendars, calendarUtils, params, delete, itemDescriptionN, hasReminder); - } else { - List? iosCalendars = await _myPlugin.getCalendars(); - if (iosCalendars != null) { - await processEvents(iosCalendars.map((cal) => Calendar(id: cal.id, name: cal.name, accountName: cal.accountName)).toList(), calendarUtils, params, delete, itemDescriptionN, hasReminder); - } - } - } -} - -Future processEvents(List calendars, calendarUtils, params, delete, String itemDescriptionN, hasReminder) async { - for (var calendar in calendars) { - Result> events = await calendarUtils.retrieveEvents(calendar.id!, params); - for (var event in events.data!) { - if (event.title!.contains(itemDescriptionN)) { - if (delete) { - await calendarUtils.deleteEvent(calendar, event); - // AppToast.showSuccessToast(message: TranslationBase.of(context).reminderCancelSuccess); - hasReminder = false; - } else { - hasReminder = false; - // setState(() { - // hasReminder = true; - // }); - } - } - } - } -} diff --git a/lib/core/utils/calender_utils_new.dart b/lib/core/utils/calender_utils_new.dart index 5e9e91b2..9edfdced 100644 --- a/lib/core/utils/calender_utils_new.dart +++ b/lib/core/utils/calender_utils_new.dart @@ -51,7 +51,7 @@ class CalenderUtilsNew { startDate: scheduleDateTime!, endDate: scheduleDateTime!.add(Duration(minutes: 30)), - // reminderMinutes: reminderMinutes, // TODO : NEED TO CONFIRM THIS FROM TAHA + reminderMinutes: reminderMinutes, // TODO : NEED TO CONFIRM THIS FROM TAHA ); return eventResult.isNotEmpty; // } diff --git a/lib/core/utils/date_util.dart b/lib/core/utils/date_util.dart index f15214a1..6c4e7bec 100644 --- a/lib/core/utils/date_util.dart +++ b/lib/core/utils/date_util.dart @@ -504,7 +504,7 @@ class DateUtil { } static convertISODateToJsonDate(String isoDate) { - return "/Date(" + DateFormat('mm-dd-yyy').parse(isoDate).millisecondsSinceEpoch.toString() + ")/"; + return "/Date(" + DateFormat('mm-dd-yyy', "en-US").parse(isoDate).millisecondsSinceEpoch.toString() + ")/"; } static String getDay(DayOfWeek dayOfWeek) { diff --git a/lib/core/utils/push_notification_handler.dart b/lib/core/utils/push_notification_handler.dart index 8215d570..d438d449 100644 --- a/lib/core/utils/push_notification_handler.dart +++ b/lib/core/utils/push_notification_handler.dart @@ -398,7 +398,7 @@ class PushNotificationHandler { int sdkInt = androidInfo.version.sdkInt ?? 0; if (sdkInt >= 33) { - // await FlutterCallkitIncoming.requestFullIntentPermission(); + await FlutterCallkitIncoming.requestFullIntentPermission(); } } } diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 671312d1..0d600bd7 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -664,10 +664,12 @@ class Utils { double width = 24, double height = 24, BoxFit fit = BoxFit.cover, + bool applyThemeColor = true, }) { + final Color? resolvedColor = iconColor ?? (applyThemeColor && AppColors.isDarkMode ? AppColors.textColor : null); return SvgPicture.asset( icon, - colorFilter: iconColor != null ? ColorFilter.mode(isDisabled ? iconColor.withOpacity(0.5) : iconColor, BlendMode.srcIn) : null, + colorFilter: resolvedColor != null ? ColorFilter.mode(isDisabled ? resolvedColor.withOpacity(0.5) : resolvedColor, BlendMode.srcIn) : null, width: width, height: height, fit: fit, @@ -801,11 +803,13 @@ class Utils { double? fontSize, double? letterSpacing, FontWeight? fontWeight, - Color iconColor = AppColors.textColor, - Color textColor = AppColors.blackColor, + Color? iconColor, + Color? textColor, bool isSaudiCurrency = true, bool isExpanded = true, }) { + final resolvedIconColor = iconColor ?? AppColors.textColor; + final resolvedTextColor = textColor ?? AppColors.blackColor; return RichText( maxLines: 1, text: TextSpan( @@ -813,13 +817,13 @@ class Utils { WidgetSpan( alignment: PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic, - child: Utils.buildSvgWithAssets(icon: AppAssets.saudi_riyal_icon, width: iconSize.h, height: iconSize.h, iconColor: iconColor), + child: Utils.buildSvgWithAssets(icon: AppAssets.saudi_riyal_icon, width: iconSize.h, height: iconSize.h, iconColor: resolvedIconColor), ), TextSpan( // text: NumberFormat.currency(locale: 'en_US', symbol: " ", decimalDigits: 0).format(habibWalletAmount), text: " ${habibWalletAmount.toStringAsFixed(2)}", style: TextStyle( - color: textColor, + color: resolvedTextColor, fontSize: fontSize ?? 32.f, letterSpacing: letterSpacing ?? -4, fontFamily: "Poppins", @@ -860,7 +864,7 @@ class Utils { static String getAppointmentTransID(int projectID, int clinicID, int appoNo, {bool isAddMilliseconds = true}) { String currentMillis = DateTime.now().millisecondsSinceEpoch.toString(); - return '$projectID-$clinicID-$appoNo${isAddMilliseconds ? '-${currentMillis.substring(currentMillis.length - 5, currentMillis.length)}' : ""}'; + return '$projectID-$clinicID-$appoNo${isAddMilliseconds ? '-${currentMillis.substring(currentMillis.length - 5, currentMillis.length)}${currentMillis.substring(currentMillis.length - 5, currentMillis.length)}' : ""}'; } static String getAdvancePaymentTransID(int projectID, int fileNumber) { diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 3f67c89d..74204645 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -235,7 +235,7 @@ extension EmailValidator on String { TextOverflow? textOverflow, String? fontFamily, double? letterSpacing = -0.4, - Color decorationColor = AppColors.errorColor}) => + Color? decorationColor}) => Text( this, maxLines: maxlines, @@ -250,7 +250,7 @@ extension EmailValidator on String { fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), decoration: isUnderLine ? TextDecoration.underline : null, fontFamily: fontFamily ?? (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'GESSTwo' : 'Poppins'), - decorationColor: decorationColor), + decorationColor: decorationColor ?? AppColors.errorColor), ); Widget toText17({bool isEnglishOnly = false, Color? color, bool isBold = false, bool isCenter = false}) => Text( diff --git a/lib/extensions/widget_extensions.dart b/lib/extensions/widget_extensions.dart index 70f10bbf..64672f5f 100644 --- a/lib/extensions/widget_extensions.dart +++ b/lib/extensions/widget_extensions.dart @@ -28,10 +28,10 @@ extension WidgetExtensions on Widget { Widget toShimmer({bool isShow = true, bool isTransparent = false}) => isShow ? Shimmer.fromColors( - baseColor: Color(0xffb9bebe), - highlightColor: Colors.white, + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, child: Container( - color: isTransparent ? Colors.transparent : Colors.white.withOpacity(0.3), + color: isTransparent ? Colors.transparent : AppColors.whiteColor.withOpacity(0.3), child: this, ), ) @@ -41,14 +41,14 @@ extension WidgetExtensions on Widget { Widget toShimmer2({bool isShow = true, double radius = 20, double? width, double? height}) => isShow ? Shimmer.fromColors( - baseColor: const Color(0xffe8eff0), - highlightColor: Colors.white, + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, child: ClipRRect( borderRadius: BorderRadius.circular(radius), child: Container( width: width, height: height, - color: Colors.white, + color: AppColors.whiteColor, child: this, ), ), @@ -69,7 +69,7 @@ extension WidgetExtensions on Widget { return Container( padding: disablePadding ? EdgeInsets.zero : const EdgeInsets.only(top: 15, bottom: 15, left: 14, right: 14), decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(radius), boxShadow: [ BoxShadow( @@ -161,15 +161,17 @@ extension SmoothContainerExtension on ShapeBorder { smoothness: 1, side: side ?? BorderSide.none, ), - shadows: hasShadow - ? [ - BoxShadow( - color: const Color(0xff000000).withOpacity(.05), - blurRadius: 32, - offset: const Offset(0, 0), - ) - ] - : [], + shadows: + // hasShadow + // ? [ + // BoxShadow( + // color: const Color(0xff000000).withOpacity(.05), + // blurRadius: 32, + // offset: const Offset(0, 0), + // ) + // ] + // : + [], ); } } diff --git a/lib/features/ask_doctor/ask_doctor_view_model.dart b/lib/features/ask_doctor/ask_doctor_view_model.dart index 2b9600e1..b28ead16 100644 --- a/lib/features/ask_doctor/ask_doctor_view_model.dart +++ b/lib/features/ask_doctor/ask_doctor_view_model.dart @@ -67,7 +67,7 @@ class AskDoctorViewModel extends ChangeNotifier { (failure) async { isDoctorResponsesLoading = false; notifyListeners(); - await errorHandlerService.handleError(failure: failure); + // await errorHandlerService.handleError(failure: failure); if (onError != null) { onError(failure.toString()); } diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 9a3fffc5..6fc36191 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -44,6 +44,7 @@ import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; +import '../insurance/insurance_view_model.dart'; import 'models/request_models/get_user_mobile_device_data.dart'; import 'models/request_models/insert_patient_mobile_deviceinfo.dart'; import 'models/request_models/patient_insert_device_imei_request.dart'; @@ -629,11 +630,12 @@ class AuthenticationViewModel extends ChangeNotifier { // medicalVm.getAllPendingRecordsByResponseId(); } await clearDefaultInputValues(); + myAppointmentsVM.setIsAppointmentDataToBeLoaded(true); + getIt.get().setIsInsuranceDataToBeLoaded(true); if (isUserAgreedBefore) { LoaderBottomSheet.hideLoader(); navigateToHomeScreen(); } else { - myAppointmentsVM.setIsAppointmentDataToBeLoaded(true); LoaderBottomSheet.hideLoader(); navigateToHomeScreen(); //Agreement page not designed yet so we are navigating to home screen directly @@ -833,8 +835,8 @@ class AuthenticationViewModel extends ChangeNotifier { } Future onRegistrationComplete() async { - // LoaderBottomSheet.showLoader(); - LoadingUtils.showFullScreenLoader(loadingText: "Setting up your medical file.\nMay take a moment."); + LoaderBottomSheet.showLoader(); + // LoadingUtils.showFullScreenLoader(loadingText: "Setting up your medical file.\nMay take a moment."); var request = RequestUtils.getUserSignupCompletionRequest(fullName: nameController.text, emailAddress: emailController.text, gender: genderType, maritalStatus: maritalStatus); final resultEither = await _authenticationRepo.registerUser(registrationPayloadDataModelRequest: request); @@ -1031,7 +1033,8 @@ class AuthenticationViewModel extends ChangeNotifier { label: LocaleKeys.notice.tr(), onOkPressed: () { _dialogService.showPhoneNumberPickerSheet( - label: "Where would you like to receive OTP?", + // label: "Where would you like to receive OTP?", + label: LocaleKeys.receiveOtpToast.tr(), message: "Please select from the below options to receive OTP.", onSMSPress: () { checkUserAuthentication(otpTypeEnum: OTPTypeEnum.sms); diff --git a/lib/features/blood_donation/widgets/hospital_selection.dart b/lib/features/blood_donation/widgets/hospital_selection.dart index 9889410c..f49b42d6 100644 --- a/lib/features/blood_donation/widgets/hospital_selection.dart +++ b/lib/features/blood_donation/widgets/hospital_selection.dart @@ -75,7 +75,7 @@ class HospitalBottomSheetBodySelection extends StatelessWidget { children: [ Utils.buildSvgWithAssets(icon: AppAssets.hmg).paddingOnly(right: 10), Expanded( - child: Text(hospital.projectName ?? "", style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: AppColors.blackColor)), + child: Text(hospital.projectName ?? "", style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16.f, color: AppColors.blackColor)), ) ], ); diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index c6c5faf8..4dd103b8 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -464,7 +464,11 @@ class BookAppointmentsViewModel extends ChangeNotifier { final result = await bookAppointmentsRepo.getLiveCareScheduleClinics(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!); result.fold( - (failure) async => await errorHandlerService.handleError(failure: failure), + // (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + isClinicsListLoading = false; + notifyListeners(); + }, (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); @@ -688,6 +692,9 @@ class BookAppointmentsViewModel extends ChangeNotifier { result.fold( (failure) async { print(failure); + if (onError != null) { + onError(failure.message); + } }, (apiResponse) { if (apiResponse.messageStatus == 2) { diff --git a/lib/features/contact_us/contact_us_view_model.dart b/lib/features/contact_us/contact_us_view_model.dart index 72bcff76..d627c55e 100644 --- a/lib/features/contact_us/contact_us_view_model.dart +++ b/lib/features/contact_us/contact_us_view_model.dart @@ -153,7 +153,7 @@ class ContactUsViewModel extends ChangeNotifier { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { liveChatProjectsList = apiResponse.data!; - liveChatProjectsList.sort((a, b) => b.distanceInKilometers.compareTo(a.distanceInKilometers)); + liveChatProjectsList.sort((a, b) => a.distanceInKilometers.compareTo(b.distanceInKilometers)); isLiveChatProjectsListLoading = false; notifyListeners(); if (onSuccess != null) { diff --git a/lib/features/emergency_services/emergency_services_repo.dart b/lib/features/emergency_services/emergency_services_repo.dart index c63f0ee6..f8eb896f 100644 --- a/lib/features/emergency_services/emergency_services_repo.dart +++ b/lib/features/emergency_services/emergency_services_repo.dart @@ -183,7 +183,7 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo { @override Future>>> getProjectList() async { - Map request = {}; + Map request = {'IsOnlineCheckIn': false, 'IsAdvancePayment': false, 'IsForER': true}; try { GenericApiModel>? apiResponse; diff --git a/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart index 45b0ab65..328ffc93 100644 --- a/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart +++ b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart @@ -40,7 +40,7 @@ class VitalSignUiModel { } if (s.contains('high')) { - return const VitalSignUiModel( + return VitalSignUiModel( iconBg: AppColors.chipSecondaryLightRedColor, iconFg: AppColors.primaryRedColor, chipBg: AppColors.chipSecondaryLightRedColor, diff --git a/lib/features/immediate_livecare/immediate_livecare_repo.dart b/lib/features/immediate_livecare/immediate_livecare_repo.dart index eee4adba..cbce4ea2 100644 --- a/lib/features/immediate_livecare/immediate_livecare_repo.dart +++ b/lib/features/immediate_livecare/immediate_livecare_repo.dart @@ -89,7 +89,7 @@ class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo { body: mapDevice, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; - onError!(error); + // onError!(error); }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { @@ -142,7 +142,7 @@ class ImmediateLiveCareRepoImp implements ImmediateLiveCareRepo { body: mapDevice, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; - onError!(error); + // onError!(error); }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { diff --git a/lib/features/immediate_livecare/immediate_livecare_view_model.dart b/lib/features/immediate_livecare/immediate_livecare_view_model.dart index 90bf9a08..46f5a652 100644 --- a/lib/features/immediate_livecare/immediate_livecare_view_model.dart +++ b/lib/features/immediate_livecare/immediate_livecare_view_model.dart @@ -174,13 +174,21 @@ class ImmediateLiveCareViewModel extends ChangeNotifier { }, onConfirmTap: () async { Navigator.pop(GetIt.instance().navigatorKey.currentContext!); - Navigator.pop(GetIt.instance().navigatorKey.currentContext!); - Navigator.push( - GetIt.instance().navigatorKey.currentContext!, - CustomPageRoute( - page: InsuranceHomePage(), - ), - ); + + //Call the API to attempt updating patient insurance + LoaderBottomSheet.showLoader(); + myAppointmentsViewModel.sendPatientUpdateRequest(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + getLiveCareImmediateAppointmentFees(); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + Navigator.push( + GetIt.instance().navigatorKey.currentContext!, + CustomPageRoute( + page: InsuranceHomePage(), + ), + ); + }); }), ), callBackFunc: () { // Navigator.pop(GetIt.instance().navigatorKey.currentContext!); diff --git a/lib/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart b/lib/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart index f272f2dc..303b6edd 100644 --- a/lib/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart +++ b/lib/features/insurance/models/resp_models/patient_insurance_approval_response_model.dart @@ -52,7 +52,7 @@ class InsuranceApprovalResponseModel { String? eApprovalRemarks; bool? isEmailSentOnDelayApproval; int? eAuthorizationID; - InsuranceApprovalDetails? apporvalDetails; + List? apporvalDetails = []; String? approvalStatusDescption; String? clinicName; String? clinicNameN; @@ -217,7 +217,9 @@ class InsuranceApprovalResponseModel { isEmailSentOnDelayApproval = json['IsEmailSentOnDelayApproval']; eAuthorizationID = json['EAuthorizationID']; if (json['ApporvalDetails'] != null && json['ApporvalDetails'].length != 0) { - apporvalDetails = InsuranceApprovalDetails.fromJson(json['ApporvalDetails'][0]); + json['ApporvalDetails'].forEach((v) { + apporvalDetails!.add(InsuranceApprovalDetails.fromJson(v)); + }); } approvalStatusDescption = json['ApprovalStatusDescption']; clinicName = json['ClinicName']; @@ -302,9 +304,9 @@ class InsuranceApprovalResponseModel { data['EApprovalRemarks'] = this.eApprovalRemarks; data['IsEmailSentOnDelayApproval'] = this.isEmailSentOnDelayApproval; data['EAuthorizationID'] = this.eAuthorizationID; - if (this.apporvalDetails != null) { - data['ApporvalDetails'] = this.apporvalDetails!.toJson; - } + // if (this.apporvalDetails != null) { + // data['ApporvalDetails'] = this.apporvalDetails!.toJson; + // } data['ApprovalStatusDescption'] = this.approvalStatusDescption; data['ClinicName'] = this.clinicName; data['ClinicNameN'] = this.clinicNameN; diff --git a/lib/features/lab/lab_repo.dart b/lib/features/lab/lab_repo.dart index a4cdcf1c..10839b12 100644 --- a/lib/features/lab/lab_repo.dart +++ b/lib/features/lab/lab_repo.dart @@ -232,7 +232,7 @@ class LabRepoImp implements LabRepo { "DoctorID": labOrder.doctorID!, "OrderNo": labOrder.orderNo!, "InvoiceType": labOrder.invoiceType!, - "SetupID": labOrder.setupID!, + "SetupID": labOrder.setupID ?? "", "IsDownload": true, 'ClinicName': labOrder.clinicDescription, 'DateofBirth': Utils.appState.getAuthenticatedUser()!.dateofBirth, @@ -249,7 +249,8 @@ class LabRepoImp implements LabRepo { GenericApiModel? apiResponse; Failure? failure; await apiClient.post( - Utils.isVidaPlusProject(int.parse(labOrder.projectID!)) ? SEND_LAB_RESULT_EMAIL : SEND_LAB_RESULT_EMAIL_NEW, + // Utils.isVidaPlusProject(int.parse(labOrder.projectID!)) ? SEND_LAB_RESULT_EMAIL : SEND_LAB_RESULT_EMAIL_NEW, + SEND_LAB_RESULT_EMAIL_NEW, body: mapDevice, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index efb1c06c..07cbbde3 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -205,14 +205,36 @@ class LabViewModel extends ChangeNotifier { } filterLabReports(String query) { + final clinicMap = >{}; + final hospitalMap = >{}; if (query.isEmpty) { - filteredLabOrders = List.from(patientLabOrders); // reset + // filteredLabOrders = List.from(patientLabOrders); // reset + for (var order in patientLabOrders) { + final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(order); + + final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); + } + patientLabOrdersByClinic = clinicMap.values.toList(); + patientLabOrdersByHospital = hospitalMap.values.toList(); + patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital; } else { filteredLabOrders = patientLabOrders.where((order) { final descriptions = order.testDetails?.map((d) => d.description?.toLowerCase()).toList() ?? []; return descriptions.any((desc) => desc != null && desc.contains(query.toLowerCase())); }).toList(); - patientLabOrders = filteredLabOrders; + // patientLabOrders = filteredLabOrders; + for (var order in filteredLabOrders) { + final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(order); + + final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); + } + patientLabOrdersByClinic = clinicMap.values.toList(); + patientLabOrdersByHospital = hospitalMap.values.toList(); + patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital; } notifyListeners(); } @@ -505,7 +527,10 @@ class LabViewModel extends ChangeNotifier { rangeEnd = 100.0; break; default: - throw ArgumentError('Invalid flag: $flag'); + rangeStart = 40.0; + rangeEnd = 59.0; + break; + // throw ArgumentError('Invalid flag: $flag'); } // Clamp input value to 0-100 and map it to the range bounds diff --git a/lib/features/lab/models/resp_models/lab_result.dart b/lib/features/lab/models/resp_models/lab_result.dart index 23146849..2808dce9 100644 --- a/lib/features/lab/models/resp_models/lab_result.dart +++ b/lib/features/lab/models/resp_models/lab_result.dart @@ -27,6 +27,7 @@ class LabResult { num? resultTypeID; String? packageShortDescription; int? testId; + String? testShortDescription; LabResult( {this.description, @@ -54,6 +55,7 @@ class LabResult { this.criticalLow, this.referenceLow, this.testId, + this.testShortDescription, }); LabResult.fromJson(Map json) { @@ -85,6 +87,7 @@ class LabResult { packageShortDescription = json['PackageShortDescription']; resultTypeID = json['ResultTypeID']; testId = json['TestID']; + testShortDescription = json['TestShortDescription']; } Map toJson() { @@ -114,6 +117,7 @@ class LabResult { data['CriticalLow'] = this.criticalLow; data['ReferenceLow'] = this.referenceLow; data['TestID'] = this.testId; + data['TestShortDescription'] = this.testShortDescription; return data; } diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 9e50075f..b93bb62d 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -41,7 +41,7 @@ class MedicalFileViewModel extends ChangeNotifier { List patientSickLeavesViewList = []; bool isSickLeavesSortByClinic = true; - bool isSickLeavesDataNeedsReloading = true; + // bool isSickLeavesDataNeedsReloading = true; List patientAllergiesList = []; @@ -164,7 +164,7 @@ class MedicalFileViewModel extends ChangeNotifier { } setIsPatientSickLeaveListLoading(bool val) { - if (val && isSickLeavesDataNeedsReloading) { + // if (val && isSickLeavesDataNeedsReloading) { patientSickLeaveList.clear(); patientSickLeavesByClinic.clear(); patientSickLeavesByHospital.clear(); @@ -172,7 +172,7 @@ class MedicalFileViewModel extends ChangeNotifier { patientSickLeavePDFBase64 = ""; isSickLeavesSortByClinic = true; isPatientSickLeaveListLoading = val; - } + // } notifyListeners(); } @@ -268,11 +268,13 @@ class MedicalFileViewModel extends ChangeNotifier { } Future getPatientSickLeaveList({Function(dynamic)? onSuccess, Function(String)? onError}) async { - if (!isSickLeavesDataNeedsReloading) { - return; - } + // if (!isSickLeavesDataNeedsReloading) { + // return; + // } patientSickLeaveList.clear(); + notifyListeners(); + final result = await medicalFileRepo.getPatientSickLeavesList(); result.fold( @@ -314,7 +316,7 @@ class MedicalFileViewModel extends ChangeNotifier { } } patientSickLeavesViewList = patientSickLeavesByClinic; - isSickLeavesDataNeedsReloading = false; + // isSickLeavesDataNeedsReloading = false; notifyListeners(); if (onSuccess != null) { @@ -353,6 +355,11 @@ class MedicalFileViewModel extends ChangeNotifier { Future getPatientMedicalReportList({Function(dynamic)? onSuccess, Function(String)? onError}) async { patientMedicalReportList.clear(); + patientMedicalReportRequestedList.clear(); + patientMedicalReportReadyList.clear(); + patientMedicalReportCancelledList.clear(); + notifyListeners(); + final result = await medicalFileRepo.getPatientMedicalReportsList(); result.fold( diff --git a/lib/features/medical_file/models/patient_sickleave_response_model.dart b/lib/features/medical_file/models/patient_sickleave_response_model.dart index 53c886f6..601bb598 100644 --- a/lib/features/medical_file/models/patient_sickleave_response_model.dart +++ b/lib/features/medical_file/models/patient_sickleave_response_model.dart @@ -122,7 +122,7 @@ class PatientSickLeavesResponseModel { patientName = json['PatientName']; projectName = json['ProjectName']; qR = json['QR']; - speciality = json['Speciality'].cast(); + // speciality = json['Speciality'].cast(); startDate = json['StartDate']; status = json['Status']; strRequestDate = json['StrRequestDate']; diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index 0c76c9fa..567714c6 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -20,7 +20,8 @@ import 'models/req_model/appointment_rate_req_model.dart'; import 'models/resp_models/appointment_details_resp_model.dart'; abstract class MyAppointmentsRepo { - Future>>> getPatientAppointments({required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}); + Future>>> getPatientAppointments( + {required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}); Future>> getPatientShareAppointment( {required int projectID, required int clinicID, required String appointmentNo, required bool isLiveCareAppointment}); @@ -67,14 +68,16 @@ abstract class MyAppointmentsRepo { Future>> getAppointmentDetails(int appointmentID, int projectID); - Future>> sendAppointmentRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note); - Future>> sendDoctorRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName); + Future>> sendDoctorRate( + int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName); Future>>> getPatientAppointmentQueueDetails({required int appointmentNo, required int patientID}); Future>> convertPatientToCash({required int projectID}); + + Future>> sendPatientUpdateRequest(); } class MyAppointmentsRepoImp implements MyAppointmentsRepo { @@ -194,9 +197,7 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { "AppointmentNo": appointmentNo, "PaymentMethodName": paymentMethodName, "PaymentAmount": payedAmount == 0 ? "0" : payedAmount.toString(), - "PaymentDate": payedAmount == 0 ? "" : "/Date(${DateTime - .now() - .millisecondsSinceEpoch})/", + "PaymentDate": payedAmount == 0 ? "" : "/Date(${DateTime.now().millisecondsSinceEpoch})/", "PaymentReferenceNumber": payedAmount == 0 ? "" : paymentReference, "ProjectID": projectID, "PatientID": patientID, @@ -865,10 +866,8 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { } } - @override - Future>> sendAppointmentRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note) async { - + Future>> sendAppointmentRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note) async { AppointmentRate appointmentRate = AppointmentRate(); appointmentRate.rate = rate; appointmentRate.appointmentNo = appointmentNo; @@ -879,7 +878,6 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { appointmentRate.createdBy = 2; appointmentRate.editedBy = 2; - try { GenericApiModel? apiResponse; Failure? failure; @@ -908,7 +906,8 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { } @override - Future>> sendDoctorRate(int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName) async { + Future>> sendDoctorRate( + int rate, int appointmentNo, int projectID, int doctorID, int clinicID, String note, String appoDate, String docName, String projectName, String clinicName) async { Map request; request = { @@ -991,4 +990,35 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } -} \ No newline at end of file + + @override + Future> sendPatientUpdateRequest() async { + Map request = {}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post(SEND_PATIENT_IMMEDIATE_UPDATE_INSURANCE_REQUEST, onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = (response['MessageStatus'] == 1); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: list, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, body: request); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 35bf29bf..d2296a66 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -54,9 +54,6 @@ class MyAppointmentsViewModel extends ChangeNotifier { List patientQueueDetailsList = []; late PatientQueueDetails currentPatientQueueDetails; - - - List patientAppointmentsHistoryList = []; List filteredAppointmentList = []; @@ -344,10 +341,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } - Future getPatientShareAppointment(int projectID, int clinicID, String appointmentNo, bool isLiveCareAppointment, - {Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await myAppointmentsRepo.getPatientShareAppointment( - projectID: projectID, clinicID: clinicID, appointmentNo: appointmentNo, isLiveCareAppointment: isLiveCareAppointment); + Future getPatientShareAppointment(int projectID, int clinicID, String appointmentNo, bool isLiveCareAppointment, {Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.getPatientShareAppointment(projectID: projectID, clinicID: clinicID, appointmentNo: appointmentNo, isLiveCareAppointment: isLiveCareAppointment); result.fold( (failure) async { @@ -421,13 +416,23 @@ class MyAppointmentsViewModel extends ChangeNotifier { }, onConfirmTap: () async { Navigator.pop(GetIt.instance().navigatorKey.currentContext!); - Navigator.pop(GetIt.instance().navigatorKey.currentContext!); - Navigator.push( - GetIt.instance().navigatorKey.currentContext!, - CustomPageRoute( - page: InsuranceHomePage(), - ), - ); + + //Call the API to attempt updating patient insurance + LoaderBottomSheet.showLoader(); + sendPatientUpdateRequest(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + setIsPatientAppointmentShareLoading(true); + getPatientShareAppointment(projectID, clinicID, appointmentNo, isLiveCareAppointment); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + Navigator.pop(GetIt.instance().navigatorKey.currentContext!); + Navigator.push( + GetIt.instance().navigatorKey.currentContext!, + CustomPageRoute( + page: InsuranceHomePage(), + ), + ); + }); }), ), callBackFunc: () { // Navigator.pop(GetIt.instance().navigatorKey.currentContext!); @@ -443,6 +448,27 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } + Future sendPatientUpdateRequest({Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.sendPatientUpdateRequest(); + + result.fold( + // (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + onError!(failure.message); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + Future convertPatientToCash({required int projectID, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await myAppointmentsRepo.convertPatientToCash(projectID: projectID); @@ -465,13 +491,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { } Future addAdvanceNumberRequest( - {required String advanceNumber, - required String paymentReference, - required String appointmentNo, - Function(dynamic)? onSuccess, - Function(String)? onError}) async { - final result = - await myAppointmentsRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference, appointmentNo: appointmentNo); + {required String advanceNumber, required String paymentReference, required String appointmentNo, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference, appointmentNo: appointmentNo); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), @@ -489,14 +510,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { } Future generateAppointmentQR( - {required int clinicID, - required int projectID, - required String appointmentNo, - required int isFollowUp, - Function(dynamic)? onSuccess, - Function(String)? onError}) async { - final result = - await myAppointmentsRepo.generateAppointmentQR(clinicID: clinicID, projectID: projectID, appointmentNo: appointmentNo, isFollowUp: isFollowUp); + {required int clinicID, required int projectID, required String appointmentNo, required int isFollowUp, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.generateAppointmentQR(clinicID: clinicID, projectID: projectID, appointmentNo: appointmentNo, isFollowUp: isFollowUp); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), @@ -513,14 +528,13 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } - Future cancelAppointment( - {required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future cancelAppointment({required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await myAppointmentsRepo.cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel); result.fold( // (failure) async => await errorHandlerService.handleError(failure: failure), (failure) async { - if(onError != null) { + if (onError != null) { onError(failure.message); } }, @@ -538,8 +552,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } - Future confirmAppointment( - {required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future confirmAppointment({required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await myAppointmentsRepo.confirmAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel); result.fold( @@ -600,8 +613,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { required int checkInType, Function(dynamic)? onSuccess, Function(String)? onError}) async { - final result = await myAppointmentsRepo.sendCheckInNfcRequest( - patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, scannedCode: scannedCode, checkInType: checkInType); + final result = await myAppointmentsRepo.sendCheckInNfcRequest(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, scannedCode: scannedCode, checkInType: checkInType); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), @@ -647,12 +659,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { } Future insertLiveCareVIDARequest( - {required clientRequestID, - required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, - Function(dynamic)? onSuccess, - Function(String)? onError}) async { - final result = await myAppointmentsRepo.insertLiveCareVIDARequest( - clientRequestID: clientRequestID, patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel); + {required clientRequestID, required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await myAppointmentsRepo.insertLiveCareVIDARequest(clientRequestID: clientRequestID, patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), @@ -783,7 +791,11 @@ class MyAppointmentsViewModel extends ChangeNotifier { final result = await myAppointmentsRepo.getTamaraInstallmentsDetails(); result.fold( - (failure) async => await errorHandlerService.handleError(failure: failure), + // (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + isTamaraDetailsLoading = false; + notifyListeners(); + }, (apiResponse) { getTamaraInstallmentsDetailsResponseModel = apiResponse.data!; isTamaraDetailsLoading = false; @@ -928,5 +940,4 @@ class MyAppointmentsViewModel extends ChangeNotifier { }, ); } - } diff --git a/lib/features/my_invoices/models/get_invoices_list_response_model.dart b/lib/features/my_invoices/models/get_invoices_list_response_model.dart index e8056d91..fe59c045 100644 --- a/lib/features/my_invoices/models/get_invoices_list_response_model.dart +++ b/lib/features/my_invoices/models/get_invoices_list_response_model.dart @@ -13,10 +13,10 @@ class GetInvoicesListResponseModel { String? doctorName; String? doctorNameN; String? clinicName; - double? decimalDoctorRate; + num? decimalDoctorRate; String? doctorImageURL; - int? doctorRate; - int? patientNumber; + num? doctorRate; + num? patientNumber; String? projectName; GetInvoicesListResponseModel( diff --git a/lib/features/my_invoices/my_invoices_view_model.dart b/lib/features/my_invoices/my_invoices_view_model.dart index a02d741d..20d7869f 100644 --- a/lib/features/my_invoices/my_invoices_view_model.dart +++ b/lib/features/my_invoices/my_invoices_view_model.dart @@ -43,7 +43,7 @@ class MyInvoicesViewModel extends ChangeNotifier { } else if (apiResponse.messageStatus == 1) { allInvoicesList = apiResponse.data!; isInvoicesListLoading = false; - + allInvoicesList.sort((a, b) => b.appointmentDate!.compareTo(a.appointmentDate!)); notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/features/profile_settings/profile_settings_view_model.dart b/lib/features/profile_settings/profile_settings_view_model.dart index 61acb260..e001d1bd 100644 --- a/lib/features/profile_settings/profile_settings_view_model.dart +++ b/lib/features/profile_settings/profile_settings_view_model.dart @@ -1,8 +1,36 @@ import 'package:flutter/foundation.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; class ProfileSettingsViewModel extends ChangeNotifier { + static const String _darkModeKey = 'is_dark_mode'; - void notify(){ + final CacheService _cacheService; + + bool _isDarkMode = false; + + bool get isDarkMode => _isDarkMode; + + ProfileSettingsViewModel({required CacheService cacheService}) + : _cacheService = cacheService; + + /// Call once at app startup (before the first frame) to restore the + /// persisted dark-mode preference. + void loadDarkMode() { + final saved = _cacheService.getBool(key: _darkModeKey); + _isDarkMode = saved ?? false; + AppColors.isDarkMode = _isDarkMode; + // No notifyListeners() here — we are called before build. + } + + void toggleDarkMode(bool value) { + _isDarkMode = value; + AppColors.isDarkMode = value; + _cacheService.saveBool(key: _darkModeKey, value: value); + notifyListeners(); + } + + void notify() { notifyListeners(); } } diff --git a/lib/features/radiology/radiology_repo.dart b/lib/features/radiology/radiology_repo.dart index 5847718d..3ad4e54f 100644 --- a/lib/features/radiology/radiology_repo.dart +++ b/lib/features/radiology/radiology_repo.dart @@ -188,14 +188,16 @@ class RadiologyRepoImp implements RadiologyRepo { failure = failureType; }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { - final radOrders; + List radOrders = []; try { if (response['FinalRadiologyList'] != null && response['FinalRadiologyList'].length != 0) { final list = response['FinalRadiologyList']; radOrders = list.map((item) => PatientRadiologyResponseModel.fromJson(item as Map)).toList().cast(); } else { - final list = response['FinalRadiologyListAPI']; - radOrders = list.map((item) => PatientRadiologyResponseModel.fromJson(item as Map)).toList().cast(); + if (response['FinalRadiologyListAPI'] != null && response['FinalRadiologyListAPI'].length != 0) { + final list = response['FinalRadiologyListAPI']; + radOrders = list.map((item) => PatientRadiologyResponseModel.fromJson(item as Map)).toList().cast(); + } } apiResponse = GenericApiModel>( diff --git a/lib/features/radiology/radiology_view_model.dart b/lib/features/radiology/radiology_view_model.dart index c8150c12..ee5c970f 100644 --- a/lib/features/radiology/radiology_view_model.dart +++ b/lib/features/radiology/radiology_view_model.dart @@ -67,6 +67,19 @@ class RadiologyViewModel extends ChangeNotifier { patientRadiologyOrders = apiResponse.data!; filteredRadiologyOrders = List.from(patientRadiologyOrders); tempRadiologyOrders = [...patientRadiologyOrders]; + + final clinicMap = >{}; + final hospitalMap = >{}; + for (var order in patientRadiologyOrders) { + final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(order); + final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); + } + patientRadiologyOrdersByClinic = clinicMap.values.toList(); + patientRadiologyOrdersByHospital = hospitalMap.values.toList(); + patientRadiologyOrdersViewList = isSortByClinic ? patientRadiologyOrdersByClinic : patientRadiologyOrdersByHospital; + isRadiologyOrdersLoading = false; filterSuggestions(); notifyListeners(); @@ -168,9 +181,31 @@ class RadiologyViewModel extends ChangeNotifier { filterRadiologyReports(String query) { if (query.isEmpty) { patientRadiologyOrders = tempRadiologyOrders; // reset + final clinicMap = >{}; + final hospitalMap = >{}; + for (var order in patientRadiologyOrders) { + final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(order); + final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); + } + patientRadiologyOrdersByClinic = clinicMap.values.toList(); + patientRadiologyOrdersByHospital = hospitalMap.values.toList(); + patientRadiologyOrdersViewList = isSortByClinic ? patientRadiologyOrdersByClinic : patientRadiologyOrdersByHospital; } else { - filteredRadiologyOrders = filteredRadiologyOrders.where((desc) => desc.description!.toLowerCase().contains(query.toLowerCase())).toList(); - patientRadiologyOrders = filteredRadiologyOrders; + filteredRadiologyOrders = filteredRadiologyOrders.where((desc) => (desc.description ?? "").toLowerCase().contains(query.toLowerCase())).toList(); + + final clinicMap = >{}; + final hospitalMap = >{}; + for (var order in filteredRadiologyOrders) { + final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(order); + final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); + } + patientRadiologyOrdersByClinic = clinicMap.values.toList(); + patientRadiologyOrdersByHospital = hospitalMap.values.toList(); + patientRadiologyOrdersViewList = isSortByClinic ? patientRadiologyOrdersByClinic : patientRadiologyOrdersByHospital; } notifyListeners(); } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 864bbf90..62451a3f 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1587,5 +1587,8 @@ abstract class LocaleKeys { static const whoophealthapplicationshouldbeinstalledinyourphone = 'whoophealthapplicationshouldbeinstalledinyourphone'; static const whoopwatchshouldbeconnected = 'whoopwatchshouldbeconnected'; static const updatetheinformation = 'updatetheinformation'; + static const timeFor = 'timeFor'; + static const hmgPolicies = 'hmgPolicies'; + static const darkMode = 'darkMode'; } diff --git a/lib/main.dart b/lib/main.dart index cff0d043..d38c9aab 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -62,6 +62,9 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { getIt.get().logInfo("Firebase backgroundMessageHandler Main!!!"); } +// flutter3_32 pub run easy_localization:generate -O ./lib/generated -f keys -o locale_keys.g.dart --source-dir ./assets/langs + + class MyHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext? context) { @@ -108,6 +111,9 @@ Future callInitializations() async { SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); HttpOverrides.global = MyHttpOverrides(); await callAppStateInitializations(); + + // Restore persisted dark-mode preference before the first frame. + getIt.get().loadDarkMode(); } void main() async { @@ -247,27 +253,35 @@ class MyApp extends StatelessWidget { return Sizer( builder: (context, orientation, deviceType) { // SizeConfig().init(constraints, orientation); - return MaterialApp( - // title: 'Dr. AlHabib', - title: 'Dr. AlHabib Beta', - builder: (context, mchild) { - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.noScaling, - alwaysUse24HourFormat: true, - ), - child: mchild!, + return Consumer( + builder: (context, profileVm, _) { + final isArabic = EasyLocalization.of(context)?.locale.languageCode == "ar"; + return MaterialApp( + key: ValueKey('app_theme_${profileVm.isDarkMode}'), + // title: 'Dr. AlHabib', + title: 'Dr. AlHabib Beta', + builder: (context, mchild) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.noScaling, + alwaysUse24HourFormat: true, + ), + child: mchild!, + ); + }, + showSemanticsDebugger: false, + debugShowCheckedModeBanner: false, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + initialRoute: AppRoutes.initialRoute, + routes: AppRoutes.routes, + theme: AppTheme.getTheme(isArabic), + darkTheme: AppTheme.getDarkTheme(isArabic), + themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light, + navigatorKey: getIt.get().navigatorKey, ); }, - showSemanticsDebugger: false, - debugShowCheckedModeBanner: false, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - initialRoute: AppRoutes.initialRoute, - routes: AppRoutes.routes, - theme: AppTheme.getTheme(EasyLocalization.of(context)?.locale.languageCode == "ar"), - navigatorKey: getIt.get().navigatorKey, ); }, ); diff --git a/lib/presentation/active_medication/active_medication_page.dart b/lib/presentation/active_medication/active_medication_page.dart index 14da5b01..ae46d402 100644 --- a/lib/presentation/active_medication/active_medication_page.dart +++ b/lib/presentation/active_medication/active_medication_page.dart @@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:flutter/cupertino.dart'; import '../../core/app_assets.dart'; -import '../../core/utils/calendar_utils.dart'; import '../../features/active_prescriptions/active_prescriptions_view_model.dart'; import '../../features/active_prescriptions/models/active_prescriptions_response_model.dart'; import '../../generated/locale_keys.g.dart'; @@ -34,7 +34,6 @@ class _ActiveMedicationPageState extends State { List selectedDayMeds = []; ActivePrescriptionsViewModel? activePreVM; - Map medReminderStatus = {}; String _buildMedKey(ActivePrescriptionsResponseModel med) { @@ -172,7 +171,7 @@ class _ActiveMedicationPageState extends State { offset: const Offset(0, -4), child: Text( _getSuffix(selectedDate.day), - style: const TextStyle( + style: TextStyle( fontSize: 12, color: AppColors.textColor, fontWeight: FontWeight.w500, @@ -182,7 +181,7 @@ class _ActiveMedicationPageState extends State { ), TextSpan( text: " ${DateFormat.MMMM().format(selectedDate)}", - style: const TextStyle( + style: TextStyle( color: AppColors.textColor, fontSize: 16, fontWeight: FontWeight.w500, @@ -279,7 +278,7 @@ class _ActiveMedicationPageState extends State { ), ], ).paddingOnly(left: 16, right: 16), - const Divider( + Divider( color: AppColors.greyColor), GestureDetector( onTap: () => showDoseDialog(med), @@ -331,7 +330,7 @@ class _ActiveMedicationPageState extends State { ], ).paddingAll(16), ), - const Divider( + Divider( color: AppColors.greyColor), _buildButtons(), ], @@ -393,16 +392,16 @@ class _ActiveMedicationPageState extends State { children: [ AppCustomChipWidget( labelText: - LocaleKeys.route.tr(namedArgs: {'route': med.route ?? ''})), + LocaleKeys.route.tr(namedArgs: {LocaleKeys.route.tr(): med.route ?? ''})), AppCustomChipWidget( labelText: - LocaleKeys.frequency.tr(namedArgs: {'frequency': med.frequency ?? ''})), + LocaleKeys.frequency.tr(namedArgs: {LocaleKeys.frequency.tr(): med.frequency ?? ''})), AppCustomChipWidget( labelText: - LocaleKeys.instruction.tr(namedArgs: {'instruction': med.doseDailyQuantity?.toString() ?? ''})), + LocaleKeys.instruction.tr(namedArgs: {LocaleKeys.instruction.tr(): med.doseDailyQuantity?.toString() ?? ''})), AppCustomChipWidget( labelText: - LocaleKeys.duration.tr(namedArgs: {'days': med.days.toString() ?? ''})), + LocaleKeys.duration.tr(namedArgs: {LocaleKeys.days.tr(): med.days.toString() ?? ''})), ], ), ], @@ -658,7 +657,7 @@ class _ActiveMedicationPageState extends State { builder: (_) => Container( width: double.infinity, height: 460.h, - decoration: const BoxDecoration( + decoration: BoxDecoration( color: AppColors.bottomSheetBgColor, borderRadius: BorderRadius.only( topLeft: Radius.circular(24), @@ -773,6 +772,8 @@ class _ReminderTimerDialogState extends State { String bigTimeText = "00:00"; bool showPicker = false; + CalenderUtilsNew calender = CalenderUtilsNew.instance; + final List> presetTimes = [ ["06:00 AM", "07:00 AM", "08:00 AM", "09:00 AM"], // Morning ["12:00 PM", "01:00 PM", "02:00 PM", "03:00 PM"], // Noon @@ -813,7 +814,7 @@ class _ReminderTimerDialogState extends State { text: TextSpan( children: [ TextSpan( - text: "Time for ", + text: LocaleKeys.timeFor.tr(), style: TextStyle( fontSize: 18.f, fontWeight: FontWeight.bold, @@ -842,7 +843,7 @@ class _ReminderTimerDialogState extends State { ), ), TextSpan( - text: " reminder", + text: LocaleKeys.reminder.tr(), style: TextStyle( fontSize: 18.f, fontWeight: FontWeight.bold, @@ -972,16 +973,28 @@ class _ReminderTimerDialogState extends State { if (isPM && hour != 12) hour += 12; if (!isPM && hour == 12) hour = 0; int totalMinutes = hour * 60 + minute; - await setCalender( - context, - eventId: widget.med.itemId.toString(), - selectedMinutes: totalMinutes, + + await calender.createMultipleEvents( + reminderMinutes: totalMinutes, frequencyNumber: widget.frequencyNumber, days: widget.med.days ?? 1, orderDate: widget.med.orderDate ?? "", itemDescriptionN: widget.med.itemDescription ?? "", route: widget.med.route ?? "", + onFailure: (errorMessage) {}, + prescriptionNumber: widget.med.itemId.toString() ); + + // await setCalender( + // context, + // eventId: widget.med.itemId.toString(), + // selectedMinutes: totalMinutes, + // frequencyNumber: widget.frequencyNumber, + // days: widget.med.days ?? 1, + // orderDate: widget.med.orderDate ?? "", + // itemDescriptionN: widget.med.itemDescription ?? "", + // route: widget.med.route ?? "", + // ); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(LocaleKeys.reminderAddedToCalendar.tr())), ); diff --git a/lib/presentation/allergies/allergies_list_page.dart b/lib/presentation/allergies/allergies_list_page.dart index adb63f02..06ae2713 100644 --- a/lib/presentation/allergies/allergies_list_page.dart +++ b/lib/presentation/allergies/allergies_list_page.dart @@ -56,7 +56,7 @@ class AllergiesListPage extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.allergy_info_icon, width: 36.w, height: 36.h, fit: BoxFit.contain).toShimmer2(isShow: true), + Utils.buildSvgWithAssets(icon: AppAssets.allergy_info_icon, width: 36.w, height: 36.h, fit: BoxFit.contain, applyThemeColor: false).toShimmer2(isShow: true), SizedBox(width: 16.h), Expanded( child: Column( @@ -100,7 +100,7 @@ class AllergiesListPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.allergy_info_icon, width: 36.w, height: 36.h, fit: BoxFit.contain), + Utils.buildSvgWithAssets(icon: AppAssets.allergy_info_icon, width: 36.w, height: 36.h, fit: BoxFit.contain, applyThemeColor: false), SizedBox(height: 16.h), Row( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index e5961e30..cafb740e 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -241,13 +241,13 @@ class _AppointmentDetailsPageState extends State { }, backgroundColor: AppColors.successColor, borderColor: AppColors.successColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, height: 40.h, icon: AppAssets.confirm_appointment_icon, - iconColor: AppColors.whiteColor, + iconColor: Colors.white, iconSize: 16.h, ) : SizedBox.shrink()) @@ -305,16 +305,16 @@ class _AppointmentDetailsPageState extends State { } }, text: LocaleKeys.getDirections.tr(context: context), - backgroundColor: AppColors.textColor.withValues(alpha: 0.8), + backgroundColor: AppColors.bookAppointment.withValues(alpha: 0.8), borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), - textColor: AppColors.whiteColor, + textColor: Colors.white, fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), height: 40.h, icon: AppAssets.directions_icon, - iconColor: AppColors.whiteColor, + iconColor: Colors.white, iconSize: 14.h, ).paddingAll(12.h), ), @@ -353,7 +353,7 @@ class _AppointmentDetailsPageState extends State { child: Row( mainAxisSize: MainAxisSize.max, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.prescription_reminder_icon, width: 35.h, height: 35.h), + Utils.buildSvgWithAssets(icon: AppAssets.prescription_reminder_icon, width: 35.h, height: 35.h, applyThemeColor: false), SizedBox(width: 8.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -841,15 +841,15 @@ class _AppointmentDetailsPageState extends State { }, backgroundColor: AppColors.successColor, borderColor: AppColors.successColor, - textColor: AppColors.whiteColor, - fontSize: 16.f, + textColor: Colors.white, + fontSize: 16.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), height: 50.h, icon: AppAssets.add_icon, - iconColor: AppColors.whiteColor, - iconSize: 18.h, + iconColor: Colors.white, + iconSize: 18.h, ).paddingSymmetrical(16.h, 24.h) : SizedBox.shrink() : CustomButton( @@ -860,14 +860,14 @@ class _AppointmentDetailsPageState extends State { backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction), borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01), - textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : AppColors.whiteColor, + textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : Colors.white, fontSize: 16.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), height: 56.h, icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction), - iconColor: AppColors.whiteColor, + iconColor: Colors.white, iconSize: 18.h, ).paddingSymmetrical(16.h, 24.h), ], @@ -904,7 +904,7 @@ class _AppointmentDetailsPageState extends State { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, fontSize: 16.f, fontWeight: FontWeight.w500, borderRadius: 12.r, diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index e70ddf49..d762f795 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -318,6 +318,7 @@ class _AppointmentPaymentPageState extends State { width: 200.h, height: 80.h, fit: BoxFit.contain, + applyThemeColor: false ).paddingSymmetrical(24.h, 0.h).onPress(() { // payfortVM.setIsApplePayConfigurationLoading(true); if (Utils.havePrivilege(103)) { diff --git a/lib/presentation/appointments/my_doctors_page.dart b/lib/presentation/appointments/my_doctors_page.dart index fc7798f8..d1ea0e98 100644 --- a/lib/presentation/appointments/my_doctors_page.dart +++ b/lib/presentation/appointments/my_doctors_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -34,6 +36,13 @@ class _MyDoctorsPageState extends State { int? expandedIndex; final Map _groupKeys = {}; + @override + void initState() { + scheduleMicrotask(() { + myAppointmentsViewModel.getPatientMyDoctors(); + }); + super.initState(); + } @override Widget build(BuildContext context) { diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 971e9ca8..89a18bf5 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -96,10 +96,10 @@ class AppointmentCard extends StatelessWidget { children: [ AppCustomChipWidget( icon: isLoading ? AppAssets.walkin_appointment_icon : (isLiveCare ? AppAssets.small_livecare_icon : AppAssets.walkin_appointment_icon), - iconColor: isLoading ? AppColors.textColor : (isLiveCare ? AppColors.whiteColor : AppColors.textColor), + iconColor: isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), labelText: isLoading ? LocaleKeys.walkin.tr(context: context) : (isLiveCare ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context)), backgroundColor: isLoading ? AppColors.greyColor : (isLiveCare ? AppColors.successColor : AppColors.greyColor), - textColor: isLoading ? AppColors.textColor : (isLiveCare ? AppColors.whiteColor : AppColors.textColor), + textColor: isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), ).toShimmer2(isShow: isLoading), AppCustomChipWidget( labelText: @@ -145,7 +145,7 @@ class AppointmentCard extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, iconColor: AppColors.ratingColorYellow), SizedBox(height: 2.h), (isFoldable || isTablet) ? "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor) diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 9e56053a..298e8732 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -68,9 +68,9 @@ class AppointmentDoctorCard extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, iconColor: AppColors.ratingColorYellow), SizedBox(height: 2.h), - "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), + "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? 0.0}".toText11(isBold: true, color: AppColors.textColor), ], ), ).circle(100), @@ -115,13 +115,13 @@ class AppointmentDoctorCard extends StatelessWidget { icon: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppAssets.walkin_appointment_icon : AppAssets.small_livecare_icon, - iconColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.textColor : AppColors.whiteColor, + iconColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.textColor : Colors.white, labelText: patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context), backgroundColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.greyColor : AppColors.successColor, - textColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.textColor : AppColors.whiteColor, + textColor: !patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? AppColors.textColor : Colors.white, ), ], ), diff --git a/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart b/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart index 376c27a9..3002c1c9 100644 --- a/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart +++ b/lib/presentation/appointments/widgets/ask_doctor_request_type_select.dart @@ -5,6 +5,7 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/ask_doctor/models/ask_doctor_appointments_list.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart index 5d9ed8db..b0a717b7 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart @@ -56,6 +56,7 @@ class HospitalListItem extends StatelessWidget { children: [ Utils.buildSvgWithAssets( icon: (hospitalData?.isHMC == true) ? AppAssets.hmc : AppAssets.hmg, + applyThemeColor: false, ).paddingOnly(right: 10), Expanded( child: Text( diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart index 969e23af..99f31be6 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart @@ -63,6 +63,7 @@ class RegionListItem extends StatelessWidget { iconSize: 14.h, icon: svgPath, iconHasColor: false, + applyThemeColor: false, richText: RichText( text: TextSpan( text: count, diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart index 6588c2ea..489ee8b7 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart @@ -1,13 +1,21 @@ import 'dart:async'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart' show RegionListItem; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../../widgets/common_bottom_sheet.dart'; @@ -30,13 +38,52 @@ class _RegionBottomSheetBodyState extends State { } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { myAppointmentsViewModel.getMappedDoctors(onError: (err) { Navigator.pop(context); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); + if (myAppointmentsViewModel.selectedClinic.clinicID == 23) { + showCommonBottomSheetWithoutHeight( + context, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), + SizedBox(height: 8.h), + ("For booking appointments in the Physiotherapy clinic, please contact: 920066666").toText16(color: AppColors.blackColor), + SizedBox(height: 16.h), + CustomButton( + height: 40.h, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + text: LocaleKeys.callNow.tr(), + textColor: AppColors.whiteColor, + iconColor: AppColors.whiteColor, + onPressed: () { + launchUrl( + Uri.parse("tel://920066666"), + ); + }, + icon: AppAssets.call), + ], + ).center, + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + // showCommonBottomSheetWithoutHeight( + // context, + // child: Utils.getErrorWidget(loadingText: err), + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); }); } }); @@ -52,8 +99,8 @@ class _RegionBottomSheetBodyState extends State { if (myAppointmentsVM.isRegionListLoading) { return Container( height: MediaQuery.of(context).size.height * 0.3, - decoration: const BoxDecoration( - color: Colors.white, + decoration: BoxDecoration( + color: AppColors.whiteColor, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Center( diff --git a/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart b/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart index 4ff24c7a..d58b75e9 100644 --- a/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart +++ b/lib/presentation/ask_doctor/widgets/ask_doctor_appointment_card.dart @@ -109,7 +109,7 @@ class AskDoctorAppointmentCard extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, applyThemeColor: false), SizedBox(height: 2.h), "${askDoctorAppointmentHistoryList.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), ], @@ -171,7 +171,18 @@ class AskDoctorAppointmentCard extends StatelessWidget { child: AskDoctorRequestTypeSelect( askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList, myAppointmentsViewModel: myAppointmentsViewModel, - patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel() + patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel( + appointmentNo: askDoctorAppointmentHistoryList.appointmentNo, + doctorID: askDoctorAppointmentHistoryList.doctorID!, + doctorName: askDoctorAppointmentHistoryList.doctorName!, + doctorTitle: askDoctorAppointmentHistoryList.doctorTitle!, + doctorImageURL: askDoctorAppointmentHistoryList.doctorImageURL!, + clinicID: askDoctorAppointmentHistoryList.clinicID!, + clinicName: askDoctorAppointmentHistoryList.clinicName!, + projectID: askDoctorAppointmentHistoryList.projectID!, + projectName: askDoctorAppointmentHistoryList.projectName!, + appointmentDate: askDoctorAppointmentHistoryList.appointmentDate!, + ), // myAppointmentsVM.patientAppointmentsHistoryList[index], ), callBackFunc: () {}, diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index 9f64bde4..d68e933c 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -116,7 +116,7 @@ class LoginScreenState extends State { child: RichText( textAlign: TextAlign.center, text: TextSpan( - style: context.dynamicTextStyle(color: Colors.black, fontSize: 14.f, height: 26 / 16, fontWeight: FontWeight.w500), + style: context.dynamicTextStyle(color: AppColors.textColor, fontSize: 14.f, height: 26 / 16, fontWeight: FontWeight.w500), children: [ TextSpan(text: LocaleKeys.dontHaveAccount.tr(context: context), style: context.dynamicTextStyle()), TextSpan(text: " "), @@ -188,7 +188,8 @@ class LoginScreenState extends State { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedBorderColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, + iconColor: Colors.white, icon: AppAssets.message, ), ), @@ -217,11 +218,12 @@ class LoginScreenState extends State { await authViewModel.checkUserAuthentication(otpTypeEnum: OTPTypeEnum.whatsapp); } }, - backgroundColor: Colors.white, - borderColor: AppColors.borderOnlyColor, + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.textColor, textColor: AppColors.textColor, icon: AppAssets.whatsapp, iconColor: null, + applyThemeColor: false, ), ), ], diff --git a/lib/presentation/authentication/register.dart b/lib/presentation/authentication/register.dart index 2a26a726..9d67920f 100644 --- a/lib/presentation/authentication/register.dart +++ b/lib/presentation/authentication/register.dart @@ -85,7 +85,7 @@ class _RegisterNew extends State { Directionality( textDirection: Directionality.of(context), child: Container( - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(24)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24)), padding: EdgeInsets.symmetric(horizontal: 16.h), child: Column( children: [ diff --git a/lib/presentation/authentication/register_step2.dart b/lib/presentation/authentication/register_step2.dart index c4bdf0f9..f4968850 100644 --- a/lib/presentation/authentication/register_step2.dart +++ b/lib/presentation/authentication/register_step2.dart @@ -76,7 +76,7 @@ class _RegisterNew extends State { LocaleKeys.personalDetailsVerification.tr(context: context).toText26(color: AppColors.textColor, weight: FontWeight.w600, letterSpacing: -2), SizedBox(height: 24.h), Container( - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(24)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24)), padding: EdgeInsets.only(left: 16.h, right: 16.h), child: Column( children: [ diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index 231d7d14..98cbc37d 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -251,10 +251,11 @@ class _SavedLogin extends State { } }, backgroundColor: AppColors.whiteColor, - borderColor: Color(0xFF2E3039), - textColor: Color(0xFF2E3039), + borderColor: AppColors.textColor, + textColor: AppColors.textColor, borderWidth: 2.w, padding: EdgeInsets.fromLTRB(0, 14.h, 0, 14.h), + applyThemeColor: false, ), const Spacer(flex: 2), // OR divider diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 03de8332..ab548449 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -230,7 +230,7 @@ class _BookAppointmentPageState extends State { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.search_by_clinic_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.search_by_clinic_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 12.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -262,7 +262,7 @@ class _BookAppointmentPageState extends State { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.search_by_doctor_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.search_by_doctor_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 12.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -292,7 +292,7 @@ class _BookAppointmentPageState extends State { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.search_by_region_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.search_by_region_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 12.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -336,9 +336,9 @@ class _BookAppointmentPageState extends State { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.search_by_clinic_icon, width: 40.h, height: 40.h), - SizedBox(width: 12.h), - Column( + Utils.buildSvgWithAssets(icon: AppAssets.search_by_clinic_icon, width: 40.h, height: 40.h, applyThemeColor: false), + SizedBox(width: 12.h), + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.immediateConsultation.tr(context: context).toText14(color: AppColors.textColor, weight: FontWeight.w500), @@ -378,9 +378,9 @@ class _BookAppointmentPageState extends State { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.search_by_doctor_icon, width: 40.h, height: 40.h), - SizedBox(width: 12.h), - Column( + Utils.buildSvgWithAssets(icon: AppAssets.search_by_doctor_icon, width: 40.h, height: 40.h, applyThemeColor: false), + SizedBox(width: 12.h), + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.scheduledConsultation.tr(context: context).toText14(color: AppColors.textColor, weight: FontWeight.w500), @@ -400,30 +400,6 @@ class _BookAppointmentPageState extends State { ), ); }), - // SizedBox(height: 16.h), - // Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), - // SizedBox(height: 16.h), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Row( - // children: [ - // Utils.buildSvgWithAssets(icon: AppAssets.search_by_region_icon, width: 40.h, height: 40.h), - // SizedBox(width: 12.h), - // Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // LocaleKeys.pharmaLiveCare.tr(context: context).toText14(color: AppColors.textColor, weight: FontWeight.w500), - // "".toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), - // ], - // ), - // ], - // ), - // Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)), - // ], - // ).onPress(() { - // openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION); - // }), ], ), ), diff --git a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart index d9aa9e5e..9cfcc1f8 100644 --- a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart +++ b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart @@ -159,7 +159,7 @@ class DoctorsFilters extends StatelessWidget{ Spacer(), DecoratedBox( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, + color: AppColors.whiteColor, customBorder: BorderRadius.only(topLeft: Radius.circular(24.h), topRight: Radius.circular(24.h)) , ), diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index c62404ab..549b8b0d 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -41,7 +41,7 @@ class DoctorProfilePage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 24.h), + // SizedBox(height: 24.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -106,6 +106,7 @@ class DoctorProfilePage extends StatelessWidget { width: 48.w, height: 48.h, fit: BoxFit.contain, + applyThemeColor: false ), SizedBox(height: 16.h), "Ratings".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), @@ -132,6 +133,7 @@ class DoctorProfilePage extends StatelessWidget { width: 48.w, height: 48.h, fit: BoxFit.contain, + applyThemeColor: false ), SizedBox(height: 16.h), "Reviews".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), @@ -172,6 +174,10 @@ class DoctorProfilePage extends StatelessWidget { child: CustomButton( text: LocaleKeys.viewAvailableAppointments.tr(), onPressed: () async { + bookAppointmentsViewModel.selectedDoctor.speciality = bookAppointmentsViewModel.doctorsProfileResponseModel.specialty; + bookAppointmentsViewModel.selectedDoctor.specialityN = bookAppointmentsViewModel.doctorsProfileResponseModel.specialty; + bookAppointmentsViewModel.selectedDoctor.name = bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName; + bookAppointmentsViewModel.selectedDoctor.name = bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName; LoaderBottomSheet.showLoader(); bookAppointmentsViewModel.isLiveCareSchedule ? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots( @@ -223,14 +229,14 @@ class DoctorProfilePage extends StatelessWidget { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 50.h, icon: AppAssets.calendar, - iconColor: AppColors.whiteColor, + iconColor: Colors.white, iconSize: 20.h, ).paddingSymmetrical(24.h, 24.h), ), diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart index fb6ebe72..7c2784de 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -84,7 +84,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} ${LocaleKeys.yearsOld.tr(context: context)}"), AppCustomChipWidget( labelText: - "${LocaleKeys.clinic.tr()}: ${(appState.isArabic() ? immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceNameN : immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceName)!}"), + "${LocaleKeys.clinic.tr()}: ${(appState.isArabic() ? immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceNameN : immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceName) ?? ""}"), ], ), ], @@ -110,7 +110,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: getLiveCareTypeIcon(immediateLiveCareVM.liveCareSelectedCallType), width: 32.h, height: 32.h, fit: BoxFit.contain), + Utils.buildSvgWithAssets(icon: getLiveCareTypeIcon(immediateLiveCareVM.liveCareSelectedCallType), width: 32.h, height: 32.h, fit: BoxFit.contain, applyThemeColor: false), SizedBox(width: 8.h), getLiveCareType(context, immediateLiveCareVM.liveCareSelectedCallType).toText16(isBold: true), ], diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index 73da69f5..4018b42a 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -292,6 +292,7 @@ class _ImmediateLiveCarePaymentPageState extends State { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - fontSize: 16, + textColor: Colors.white, + fontSize: 16.f, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index cad8e2e7..aebb90fd 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -144,7 +144,14 @@ class _SelectClinicPageState extends State { padding: EdgeInsets.only(top: 24.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.liveCareClinicsList.length, + itemCount: bookAppointmentsVM.isClinicsListLoading + ? 5 + : bookAppointmentsVM.liveCareClinicsList.isNotEmpty + ? bookAppointmentsVM.liveCareClinicsList.length + : 1, + + // bookAppointmentsVM.isClinicsListLoading ? 5 : bookAppointmentsVM.liveCareClinicsList.length, + itemBuilder: (context, index) { return bookAppointmentsVM.isClinicsListLoading ? ClinicCard( @@ -163,14 +170,16 @@ class _SelectClinicPageState extends State { duration: Duration(milliseconds: 300), curve: Curves.easeInOut, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: ClinicCard( - bookAppointmentsVM: bookAppointmentsVM, - liveCareClinicsResponseModel: bookAppointmentsVM.liveCareClinicsList[index], - clinicsListResponseModel: GetClinicsListResponseModel(), - isLoading: bookAppointmentsVM.isClinicsListLoading, - ).onPress(() { - onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]); - }), + child: bookAppointmentsVM.liveCareClinicsList.isNotEmpty + ? ClinicCard( + bookAppointmentsVM: bookAppointmentsVM, + liveCareClinicsResponseModel: bookAppointmentsVM.liveCareClinicsList[index], + clinicsListResponseModel: GetClinicsListResponseModel(), + isLoading: bookAppointmentsVM.isClinicsListLoading, + ).onPress(() { + onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]); + }) + : Utils.getNoDataWidget(context), ), ), ), diff --git a/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart index bb347e5b..663fd5b4 100644 --- a/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart +++ b/lib/presentation/book_appointment/waiting_appointment/waiting_appointment_payment_page.dart @@ -313,6 +313,7 @@ class _WaitingAppointmentPaymentPageState extends State { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, fontSize: 16.f, fontWeight: FontWeight.w500, borderRadius: 10.r, @@ -344,7 +344,7 @@ class TimeSlotChip extends StatelessWidget { ? Container( padding: EdgeInsets.symmetric(horizontal: 14.h, vertical: 8.h), decoration: ShapeDecoration( - color: isSelected ? AppColors.warningColorYellow : AppColors.whiteColor, + color: isSelected ? AppColors.warningColorYellow : AppColors.textColor, shape: SmoothRectangleBorder( borderRadius: BorderRadius.circular(8.h), smoothness: 1, @@ -367,8 +367,8 @@ class TimeSlotChip extends StatelessWidget { ), ), child: label.toText12( - color: isSelected ? AppColors.primaryRedColor : Colors.black87, - fontWeight: FontWeight.w500, + color: isSelected ? AppColors.primaryRedColor : AppColors.textColor, + fontWeight: FontWeight.w500, ), ), ); diff --git a/lib/presentation/book_appointment/widgets/clinic_card.dart b/lib/presentation/book_appointment/widgets/clinic_card.dart index 7e3c8df1..e7a6ca3a 100644 --- a/lib/presentation/book_appointment/widgets/clinic_card.dart +++ b/lib/presentation/book_appointment/widgets/clinic_card.dart @@ -34,7 +34,7 @@ class ClinicCard extends StatelessWidget { Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Utils.buildSvgWithAssets(icon: "assets/images/clinicIcons/${clinicsListResponseModel.clinicID ?? 1}.svg", width: 24.w, height: 24.h, fit: BoxFit.contain).toShimmer2(isShow: isLoading), (clinicsListResponseModel.isLiveCareClinicAndOnline ?? true) - ? Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 32.w, height: 32.h, fit: BoxFit.contain).toShimmer2(isShow: isLoading) + ? Utils.buildSvgWithAssets(icon: AppAssets.livecare_clinic_icon, width: 32.w, height: 32.h, fit: BoxFit.contain, applyThemeColor: false).toShimmer2(isShow: isLoading) : SizedBox.shrink(), ], ), diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index ddb43e9d..6eecf5f8 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -15,6 +15,8 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:lottie/lottie.dart'; +import 'package:url_launcher/url_launcher.dart'; class DoctorCard extends StatelessWidget { const DoctorCard({ @@ -72,7 +74,7 @@ class DoctorCard extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, applyThemeColor: false), SizedBox(height: 2.h), "${isLoading ? 4.78 : doctorsListResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), ], @@ -198,13 +200,52 @@ class DoctorCard extends StatelessWidget { }, onError: (err) { LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); + if (bookAppointmentsViewModel.selectedClinic.clinicID == 23) { + showCommonBottomSheetWithoutHeight( + context, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), + SizedBox(height: 8.h), + ("For booking appointments in the Physiotherapy clinic, please contact: 920066666").toText16(color: AppColors.blackColor), + SizedBox(height: 16.h), + CustomButton( + height: 40.h, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + text: LocaleKeys.callNow.tr(), + textColor: AppColors.whiteColor, + iconColor: AppColors.whiteColor, + onPressed: () { + launchUrl( + Uri.parse("tel://920066666"), + ); + }, + icon: AppAssets.call), + ], + ).center, + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + // showCommonBottomSheetWithoutHeight( + // context, + // child: Utils.getErrorWidget(loadingText: err), + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); }); }, backgroundColor: Color(0xffFEE9EA), diff --git a/lib/presentation/book_appointment/widgets/doctor_rating_details.dart b/lib/presentation/book_appointment/widgets/doctor_rating_details.dart index 0ed54862..6d9e1cb2 100644 --- a/lib/presentation/book_appointment/widgets/doctor_rating_details.dart +++ b/lib/presentation/book_appointment/widgets/doctor_rating_details.dart @@ -69,13 +69,13 @@ class DoctorRatingDetails extends StatelessWidget { Container( width: 100.0, margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.excellent.tr(context: context), style: TextStyle(fontSize: 13.0, color: Colors.black, fontWeight: FontWeight.w600))), + child: Text(LocaleKeys.excellent.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), getRatingLine(bookAppointmentsVM.doctorDetailsList[0].ratio, Colors.green[700]!), ], ), Container( margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${getRatingWidth(bookAppointmentsVM.doctorDetailsList[0].ratio).round()}%", style: TextStyle(fontSize: 14.0, color: Colors.black, fontWeight: FontWeight.w600)), + child: Text("${getRatingWidth(bookAppointmentsVM.doctorDetailsList[0].ratio).round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600)), ), ], ), @@ -89,13 +89,13 @@ class DoctorRatingDetails extends StatelessWidget { Container( width: 100.0, margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.vGood.tr(context: context), style: TextStyle(fontSize: 13.0, color: Colors.black, fontWeight: FontWeight.w600))), + child: Text(LocaleKeys.vGood.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), getRatingLine(bookAppointmentsVM.doctorDetailsList[1].ratio, Color(0xffB7B723)), ], ), Container( margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${bookAppointmentsVM.doctorDetailsList[1].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: Colors.black, fontWeight: FontWeight.w600)), + child: Text("${bookAppointmentsVM.doctorDetailsList[1].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600)), ), ], ), @@ -109,13 +109,13 @@ class DoctorRatingDetails extends StatelessWidget { Container( width: 100.0, margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.good.tr(context: context), style: TextStyle(fontSize: 13.0, color: Colors.black, fontWeight: FontWeight.w600))), + child: Text(LocaleKeys.good.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), getRatingLine(bookAppointmentsVM.doctorDetailsList[2].ratio, Color(0xffEBA727)), ], ), Container( margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${bookAppointmentsVM.doctorDetailsList[2].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: Colors.black, fontWeight: FontWeight.w600)), + child: Text("${bookAppointmentsVM.doctorDetailsList[2].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600)), ), ], ), @@ -129,13 +129,13 @@ class DoctorRatingDetails extends StatelessWidget { Container( width: 100.0, margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: Colors.black, fontWeight: FontWeight.w600))), + child: Text(LocaleKeys.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), getRatingLine(bookAppointmentsVM.doctorDetailsList[3].ratio, Color(0xffEB7227)), ], ), Container( margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${bookAppointmentsVM.doctorDetailsList[3].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: Colors.black, fontWeight: FontWeight.w600)), + child: Text("${bookAppointmentsVM.doctorDetailsList[3].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600)), ), ], ), @@ -150,13 +150,13 @@ class DoctorRatingDetails extends StatelessWidget { Container( width: 100.0, margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: Colors.black, fontWeight: FontWeight.w600))), + child: Text(LocaleKeys.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), getRatingLine(bookAppointmentsVM.doctorDetailsList[4].ratio, Color(0xffE20C0C)), ], ), Container( margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${bookAppointmentsVM.doctorDetailsList[4].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: Colors.black, fontWeight: FontWeight.w600)), + child: Text("${bookAppointmentsVM.doctorDetailsList[4].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600)), ), ], ), diff --git a/lib/presentation/contact_us/live_chat_page.dart b/lib/presentation/contact_us/live_chat_page.dart index a602eb14..0350c316 100644 --- a/lib/presentation/contact_us/live_chat_page.dart +++ b/lib/presentation/contact_us/live_chat_page.dart @@ -122,12 +122,12 @@ class LiveChatPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ("${appState.isArabic() ? contactUsVM.liveChatProjectsList[index].projectNameN! : contactUsVM.liveChatProjectsList[index].projectName!}\n${contactUsVM.liveChatProjectsList[index].distanceInKilometers!} KM") - .toText14(isBold: true, color: contactUsVM.selectedLiveChatProjectIndex == index ? AppColors.whiteColor : AppColors.textColor), + .toText14(isBold: true, color: contactUsVM.selectedLiveChatProjectIndex == index ? Colors.white : AppColors.textColor), Transform.flip( flipX: getIt.get().isArabic(), child: Utils.buildSvgWithAssets( icon: AppAssets.forward_arrow_icon_small, - iconColor: contactUsVM.selectedLiveChatProjectIndex == index ? AppColors.whiteColor : AppColors.textColor, + iconColor: contactUsVM.selectedLiveChatProjectIndex == index ? Colors.white : AppColors.textColor, width: 18.h, height: 13.h, fit: BoxFit.contain, @@ -235,8 +235,8 @@ class LiveChatPage extends StatelessWidget { }, backgroundColor: contactUsVM.selectedLiveChatProjectIndex == -1 ? AppColors.greyColor : AppColors.primaryRedColor, borderColor: contactUsVM.selectedLiveChatProjectIndex == -1 ? AppColors.greyColor : AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - fontSize: 16, + textColor: Colors.white, + fontSize: 16.f, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), diff --git a/lib/presentation/contact_us/widgets/find_us_item_card.dart b/lib/presentation/contact_us/widgets/find_us_item_card.dart index 1774404d..4738a63a 100644 --- a/lib/presentation/contact_us/widgets/find_us_item_card.dart +++ b/lib/presentation/contact_us/widgets/find_us_item_card.dart @@ -102,9 +102,9 @@ class FindUsItemCard extends StatelessWidget { AppCustomChipWidget( labelText: LocaleKeys.callNow.tr(), icon: AppAssets.call_fill, - iconColor: AppColors.whiteColor, + iconColor: Colors.white, backgroundColor: AppColors.primaryRedColor.withValues(alpha: 1.0), - textColor: AppColors.whiteColor, + textColor: Colors.white, onChipTap: () { launchUrl(Uri.parse("tel://" + "${getHMGLocationsModel.phoneNumber}")); }, diff --git a/lib/presentation/covid19test/covid_payment_screen.dart b/lib/presentation/covid19test/covid_payment_screen.dart index e395a426..40bf2863 100644 --- a/lib/presentation/covid19test/covid_payment_screen.dart +++ b/lib/presentation/covid19test/covid_payment_screen.dart @@ -133,6 +133,7 @@ class _CovidPaymentScreenState extends State { width: 200.h, height: 80.h, fit: BoxFit.contain, + applyThemeColor: false ).paddingSymmetrical(24.h, 0.h).onPress(() { if (Utils.havePrivilege(103)) { startApplePay(); diff --git a/lib/presentation/e_referral/new_e_referral.dart b/lib/presentation/e_referral/new_e_referral.dart index 02881730..844ef5d5 100644 --- a/lib/presentation/e_referral/new_e_referral.dart +++ b/lib/presentation/e_referral/new_e_referral.dart @@ -171,7 +171,7 @@ class _NewReferralPageState extends State { ); }, bottomChild: Container( - color: Colors.white, + color: AppColors.whiteColor, padding: EdgeInsets.all(ResponsiveExtension(20).h), child: CustomButton( text: _currentStep <= 1 ? LocaleKeys.next.tr() : LocaleKeys.submit.tr(), diff --git a/lib/presentation/e_referral/widget/e-referral_otp.dart b/lib/presentation/e_referral/widget/e-referral_otp.dart index 390fc71f..17ae4272 100644 --- a/lib/presentation/e_referral/widget/e-referral_otp.dart +++ b/lib/presentation/e_referral/widget/e-referral_otp.dart @@ -90,7 +90,8 @@ class OTPService { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedBorderColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, + iconColor: Colors.white, icon: AppAssets.message, ), ), diff --git a/lib/presentation/e_referral/widget/e_referral_other_details.dart b/lib/presentation/e_referral/widget/e_referral_other_details.dart index da955f8b..114b3b24 100644 --- a/lib/presentation/e_referral/widget/e_referral_other_details.dart +++ b/lib/presentation/e_referral/widget/e_referral_other_details.dart @@ -67,6 +67,7 @@ class _OtherDetailsStepState extends State { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: ListView( + padding: EdgeInsets.zero, physics: const BouncingScrollPhysics(), children: [ const SizedBox(height: 12), @@ -97,7 +98,10 @@ class _OtherDetailsStepState extends State { InkWell( child: TextInputWidget( controller: _medicalReportController, - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(12).h, + horizontal: ResponsiveExtension(16).h, + ), hintText: LocaleKeys.medicalReport.tr(context: context), labelText: LocaleKeys.selectAttachment.tr(context: context), suffix: const Icon(Icons.attachment), @@ -169,6 +173,7 @@ class _OtherDetailsStepState extends State { if (!value) { _updateInsuranceText(); } + setState(() {}); }, ), Padding( @@ -194,7 +199,10 @@ class _OtherDetailsStepState extends State { InkWell( child: TextInputWidget( controller: _insuranceController, - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(12).h, + horizontal: ResponsiveExtension(16).h, + ), hintText: LocaleKeys.insuranceDocument.tr(context: context), labelText: LocaleKeys.selectAttachment.tr(context: context), suffix: const Icon(Icons.attachment), @@ -234,7 +242,6 @@ class _OtherDetailsStepState extends State { } void _showBranchBottomSheet(BuildContext context, ReferralFormManager formManager) { - showCommonBottomSheetWithoutHeight( context, title: LocaleKeys.selectBranch.tr(context: context), @@ -266,8 +273,9 @@ class _OtherDetailsStepState extends State { onTap: () { formManager.updateBranch(branch); Navigator.pop(context); - }, - ); + setState(() {}); + }, + ); }, // separatorBuilder: (context, index) => const Divider(), itemCount: hospitals.length, diff --git a/lib/presentation/e_referral/widget/e_referral_patient_info.dart b/lib/presentation/e_referral/widget/e_referral_patient_info.dart index 61b92c76..cbce6919 100644 --- a/lib/presentation/e_referral/widget/e_referral_patient_info.dart +++ b/lib/presentation/e_referral/widget/e_referral_patient_info.dart @@ -64,6 +64,7 @@ class PatientInformationStepState extends State { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: ListView( + padding: EdgeInsets.zero, physics: const BouncingScrollPhysics(), children: [ const SizedBox(height: 12), @@ -71,7 +72,6 @@ class PatientInformationStepState extends State { const SizedBox(height: 12), _buildIdentificationField(formManager), _buildPatientNameField(formManager), - // _buildPatientCountryField(context, formManager), _buildPatientPhoneField(formManager), const SizedBox(height: 20), _buildSectionTitle(LocaleKeys.patientLocation.tr(context: context)), @@ -95,7 +95,10 @@ class PatientInformationStepState extends State { focusNode: _identificationFocusNode, child: TextInputWidget( controller: _identificationController, - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(12).h, + horizontal: ResponsiveExtension(16).h, + ), hintText: LocaleKeys.enterIdentificationNumber.tr(context: context), labelText: LocaleKeys.identificationNumber.tr(context: context), errorMessage: formManager.errors.patientIdentification, @@ -115,7 +118,10 @@ class PatientInformationStepState extends State { focusNode: _nameFocusNode, child: TextInputWidget( controller: _nameController, - padding: const EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(12).h, + horizontal: ResponsiveExtension(16).h, + ), hintText: LocaleKeys.patientName.tr(context: context), labelText: LocaleKeys.name.tr(context: context), keyboardType: TextInputType.text, diff --git a/lib/presentation/e_referral/widget/e_referral_requester_form.dart b/lib/presentation/e_referral/widget/e_referral_requester_form.dart index d2b75ccb..d57abe23 100644 --- a/lib/presentation/e_referral/widget/e_referral_requester_form.dart +++ b/lib/presentation/e_referral/widget/e_referral_requester_form.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/e_referral/e_referral_form_manager.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; @@ -94,7 +95,6 @@ class RequesterFormStepState extends State { focusNode: _nameFocusNode, child: TextInputWidget( controller: _nameController, - padding: const EdgeInsets.symmetric(horizontal: 16.0), hintText: LocaleKeys.enterReferralRequesterName.tr(context: context), labelText: LocaleKeys.requesterName.tr(context: context), keyboardType: TextInputType.text, @@ -102,6 +102,10 @@ class RequesterFormStepState extends State { isAllowLeadingIcon: true, leadingIcon: AppAssets.user_circle, hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.requesterName), + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(12).h, + horizontal: ResponsiveExtension(16).h, + ), onChange: (value) { formManager.updateRequesterName(value ?? ''); }, @@ -118,7 +122,7 @@ class RequesterFormStepState extends State { errorMessage: formManager.errors.relationship, hasError: !ValidationUtils.isNullOrEmpty(formManager.errors.relationship), hasSelectionCustomIcon: false, - labelColor: Colors.black, + labelColor: AppColors.textColor, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), leadingIcon: AppAssets.user_circle, dropdownItems: [], @@ -168,8 +172,8 @@ class RequesterFormStepState extends State { return DecoratedBox( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, - customBorder: BorderRadius.all(Radius.circular(24.h)) , + color: AppColors.whiteColor, + customBorder: BorderRadius.all(Radius.circular(24.h)) , ), child: ListView.builder( shrinkWrap: true, diff --git a/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart b/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart index 91db0ded..c17f3560 100644 --- a/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart +++ b/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart @@ -28,7 +28,7 @@ class RrtRequestTypeSelect extends StatelessWidget { ), DecoratedBox( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: 20.r, ), child: OptionSelection(context)), diff --git a/lib/presentation/emergency_services/call_ambulance/call_ambulance_page.dart b/lib/presentation/emergency_services/call_ambulance/call_ambulance_page.dart index 5a368a3b..e5578a3e 100644 --- a/lib/presentation/emergency_services/call_ambulance/call_ambulance_page.dart +++ b/lib/presentation/emergency_services/call_ambulance/call_ambulance_page.dart @@ -79,7 +79,7 @@ class CallAmbulancePage extends StatelessWidget { ), Align( alignment: AlignmentDirectional.topStart, - child: Utils.buildSvgWithAssets(icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h).onPress(() { + child: Utils.buildSvgWithAssets(icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h, applyThemeColor: false).onPress(() { context.read().flushPickupInformation(); Navigator.pop(context); }), diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart b/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart index 3ae93d0d..9ad98cb6 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/type_selection_widget.dart @@ -52,6 +52,7 @@ class TypeSelectionWidget extends StatelessWidget { child: AppCustomChipWidget( icon: AppAssets.hmg, iconHasColor: false, + applyThemeColor: false, labelText: LocaleKeys.hmgHospitals.tr(context: context), shape: RoundedRectangleBorder( side: BorderSide( @@ -76,6 +77,7 @@ class TypeSelectionWidget extends StatelessWidget { child: AppCustomChipWidget( icon: AppAssets.hmc, iconHasColor: false, + applyThemeColor: false, labelText: LocaleKeys.hmcMedicalClinic.tr(context: context), shape: RoundedRectangleBorder( side: BorderSide( diff --git a/lib/presentation/emergency_services/emergency_services_page.dart b/lib/presentation/emergency_services/emergency_services_page.dart index a5281ca0..41597e40 100644 --- a/lib/presentation/emergency_services/emergency_services_page.dart +++ b/lib/presentation/emergency_services/emergency_services_page.dart @@ -53,7 +53,7 @@ class EmergencyServicesPage extends StatelessWidget { ), child: Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.call_ambulance_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.call_ambulance_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 12.h), Expanded( child: Column( @@ -87,7 +87,7 @@ class EmergencyServicesPage extends StatelessWidget { "".toText14(), Utils.buildSvgWithAssets( icon: AppAssets.cancel_circle_icon, - iconColor: AppColors.whiteColor, + iconColor: Colors.white, width: 24.h, height: 24.h, fit: BoxFit.contain, @@ -99,10 +99,10 @@ class EmergencyServicesPage extends StatelessWidget { Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), SizedBox(height: 8.h), - LocaleKeys.confirmation.tr().toText28(color: AppColors.whiteColor, isBold: true), + LocaleKeys.confirmation.tr().toText28(color: Colors.white, isBold: true), SizedBox(height: 8.h), LocaleKeys.areYouSureYouWantToCallAmbulance.tr() - .toText14(color: AppColors.whiteColor, weight: FontWeight.w500), + .toText14(color: Colors.white, weight: FontWeight.w500), SizedBox(height: 24.h), CustomButton( text: LocaleKeys.confirm.tr(context: context), @@ -111,8 +111,8 @@ class EmergencyServicesPage extends StatelessWidget { await emergencyServicesViewModel.getTransportationOption(); openTranportationSelectionBottomSheet(context); }, - backgroundColor: AppColors.whiteColor, - borderColor: AppColors.whiteColor, + backgroundColor: Colors.white, + borderColor: Colors.white, textColor: AppColors.primaryRedColor, icon: AppAssets.checkmark_icon, iconColor: AppColors.primaryRedColor, @@ -140,7 +140,7 @@ class EmergencyServicesPage extends StatelessWidget { ), child: Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.nearest_er_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.nearest_er_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 12.h), Expanded( child: Column( @@ -169,7 +169,7 @@ class EmergencyServicesPage extends StatelessWidget { ), child: Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rrt_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.rrt_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 12.h), Expanded( child: Column( @@ -199,10 +199,10 @@ class EmergencyServicesPage extends StatelessWidget { children: [ Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), SizedBox(height: 8.h), - LocaleKeys.confirm.tr().toText28(color: AppColors.whiteColor, isBold: true), + LocaleKeys.confirm.tr().toText28(color: Colors.white, isBold: true), SizedBox(height: 8.h), LocaleKeys.areYouSureYouWantToCallRRT.tr() - .toText14(color: AppColors.whiteColor, weight: FontWeight.w500), + .toText14(color: Colors.white, weight: FontWeight.w500), SizedBox(height: 24.h), CustomButton( text: LocaleKeys.confirm.tr(context: context), @@ -237,8 +237,8 @@ class EmergencyServicesPage extends StatelessWidget { ); }); }, - backgroundColor: AppColors.whiteColor, - borderColor: AppColors.whiteColor, + backgroundColor: Colors.white, + borderColor: Colors.white, textColor: AppColors.primaryRedColor, icon: AppAssets.checkmark_icon, iconColor: AppColors.primaryRedColor, @@ -268,7 +268,7 @@ class EmergencyServicesPage extends StatelessWidget { ), child: Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rrt_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.rrt_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 12.h), Expanded( child: Column( @@ -311,9 +311,9 @@ class EmergencyServicesPage extends StatelessWidget { Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), SizedBox(height: 8.h), - LocaleKeys.confirm.tr().toText28(color: AppColors.whiteColor, isBold: true), + LocaleKeys.confirm.tr().toText28(color: Colors.white, isBold: true), SizedBox(height: 8.h), - LocaleKeys.areYouSureYouWantToMakeERCheckIn.tr().toText14(color: AppColors.whiteColor, weight: FontWeight.w500), + LocaleKeys.areYouSureYouWantToMakeERCheckIn.tr().toText14(color: Colors.white, weight: FontWeight.w500), SizedBox(height: 24.h), CustomButton( text: LocaleKeys.confirm.tr(context: context), @@ -325,8 +325,8 @@ class EmergencyServicesPage extends StatelessWidget { context.read().navigateToEROnlineCheckIn(); }); }, - backgroundColor: AppColors.whiteColor, - borderColor: AppColors.whiteColor, + backgroundColor: Colors.white, + borderColor: Colors.white, textColor: AppColors.primaryRedColor, icon: AppAssets.checkmark_icon, iconColor: AppColors.primaryRedColor, diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart index bbaa5f5c..7f206c17 100644 --- a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -24,11 +25,13 @@ class ErOnlineCheckinHome extends StatelessWidget { ErOnlineCheckinHome({super.key}); late EmergencyServicesViewModel emergencyServicesViewModel; + late MyAppointmentsViewModel myAppointmentsViewModel; bool _supportsNFC = false; @override Widget build(BuildContext context) { emergencyServicesViewModel = Provider.of(context, listen: false); + myAppointmentsViewModel = Provider.of(context, listen: false); FlutterNfcKit.nfcAvailability.then((value) { _supportsNFC = (value == NFCAvailability.available); }); @@ -81,6 +84,9 @@ class ErOnlineCheckinHome extends StatelessWidget { await emergencyServicesViewModel.autoGenerateInvoiceERClinic( projectID: value, onSuccess: (value) { + myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false); LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.onlineCheckIn.tr(), diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart index 26456794..8b9633c1 100644 --- a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart @@ -288,6 +288,7 @@ class _ErOnlineCheckinPaymentPageState extends State width: 200.h, height: 80.h, fit: BoxFit.contain, + applyThemeColor: false ).paddingSymmetrical(24.h, 0.h).onPress(() { if (Utils.havePrivilege(103)) { startApplePay(); diff --git a/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart b/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart index 7b17eab8..e1ebc83c 100644 --- a/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart +++ b/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart @@ -26,7 +26,7 @@ class AmbulanceHistoryItem extends StatelessWidget { Widget build(BuildContext context) { return DecoratedBox( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, + color: AppColors.whiteColor, hasShadow: true, customBorder: BorderRadius.all( Radius.circular(20.h), diff --git a/lib/presentation/emergency_services/history/widget/rrt_item.dart b/lib/presentation/emergency_services/history/widget/rrt_item.dart index d345e2df..c8787791 100644 --- a/lib/presentation/emergency_services/history/widget/rrt_item.dart +++ b/lib/presentation/emergency_services/history/widget/rrt_item.dart @@ -26,7 +26,7 @@ class RRTItem extends StatelessWidget { Widget build(BuildContext context) { return DecoratedBox( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, + color: AppColors.whiteColor, hasShadow: true, customBorder: BorderRadius.all( Radius.circular(20.h), diff --git a/lib/presentation/emergency_services/widgets/nearestERItem.dart b/lib/presentation/emergency_services/widgets/nearestERItem.dart index 2226f82b..0c4a5a6a 100644 --- a/lib/presentation/emergency_services/widgets/nearestERItem.dart +++ b/lib/presentation/emergency_services/widgets/nearestERItem.dart @@ -27,7 +27,7 @@ class NearestERItem extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, + color: AppColors.whiteColor, customBorder: BorderRadius.only( topLeft: Radius.circular(24.h), topRight: Radius.circular(24.h), @@ -90,7 +90,7 @@ class NearestERItem extends StatelessWidget { Expanded( child: CustomButton( text: LocaleKeys.viewLocationGoogleMaps.tr(context: context), - iconSize: 18.h, + iconSize: 16.h, icon: AppAssets.location, onPressed: () { context.read().openDirections(destLat: double.parse(nearestERItem.latitude??"0.0"), destLng: double.parse(nearestERItem.longitude??"0.0") ); @@ -100,7 +100,7 @@ class NearestERItem extends StatelessWidget { textColor: AppColors.primaryRedColor, iconColor: AppColors.primaryRedColor, height: 40.h, - fontSize: 14, + fontSize: 14.f, fontWeight: FontWeight.w500, ).toShimmer2(isShow: isLoading), ), diff --git a/lib/presentation/habib_wallet/recharge_wallet_page.dart b/lib/presentation/habib_wallet/recharge_wallet_page.dart index 78608e8b..ef1642eb 100644 --- a/lib/presentation/habib_wallet/recharge_wallet_page.dart +++ b/lib/presentation/habib_wallet/recharge_wallet_page.dart @@ -133,7 +133,7 @@ class _RechargeWalletPageState extends State { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.my_account_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.my_account_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 8.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -171,7 +171,7 @@ class _RechargeWalletPageState extends State { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.select_hospital_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.select_hospital_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 8.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -185,19 +185,19 @@ class _RechargeWalletPageState extends State { ], ), ], - ).onPress(() { - showCommonBottomSheetWithoutHeight(context, - title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false, child: SelectHospitalBottomSheet(), callBackFunc: () {}); - }), + ), Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), ], - ), + ).onPress(() { + showCommonBottomSheetWithoutHeight(context, + title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false, child: SelectHospitalBottomSheet(), callBackFunc: () {}); + }), SizedBox(height: 16.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), SizedBox(height: 16.h), Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 8.h), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -281,14 +281,14 @@ class _RechargeWalletPageState extends State { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 50.h, icon: AppAssets.appointment_pay_icon, - iconColor: AppColors.whiteColor, + iconColor: Colors.white, iconSize: 18.h, ).paddingSymmetrical(24.h, 24.h), ), diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index 4585b7d0..9efcb01b 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -188,10 +188,7 @@ class _WalletPaymentConfirmPageState extends State { Platform.isIOS ? Utils.buildSvgWithAssets( icon: AppAssets.apple_pay_button, - width: 200.h, - height: 56.h, - fit: BoxFit.contain, - ).paddingSymmetrical(24.h, 0.h).onPress(() { + width: 200.h, height: 56.h, fit: BoxFit.contain, applyThemeColor: false).paddingSymmetrical(24.h, 0.h).onPress(() { if (Utils.havePrivilege(103)) { startApplePay(); } else { diff --git a/lib/presentation/habib_wallet/widgets/hospital_list_item.dart b/lib/presentation/habib_wallet/widgets/hospital_list_item.dart index a46f79db..67e174a3 100644 --- a/lib/presentation/habib_wallet/widgets/hospital_list_item.dart +++ b/lib/presentation/habib_wallet/widgets/hospital_list_item.dart @@ -54,6 +54,7 @@ class HospitalListItemAdvancePayment extends StatelessWidget { children: [ Utils.buildSvgWithAssets( icon: (hospitalModel.isHMC == true) ? AppAssets.hmc : AppAssets.hmg, + applyThemeColor: false ).paddingOnly(right: 10), Expanded( child: Text( diff --git a/lib/presentation/health_calculators_and_converts/health_calculators_page.dart b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart index aae25b03..4f519d66 100644 --- a/lib/presentation/health_calculators_and_converts/health_calculators_page.dart +++ b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart @@ -39,10 +39,10 @@ class _HealthCalculatorsPageState extends State { ? Column( children: [ Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), child: Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.general_health, height: 40.h, width: 40.w, fit: BoxFit.none), + Utils.buildSvgWithAssets(icon: AppAssets.general_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false), SizedBox(width: 12.w), Flexible( child: Column( @@ -70,10 +70,10 @@ class _HealthCalculatorsPageState extends State { }), SizedBox(height: 16.h), Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), child: Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.women_health, height: 40.h, width: 40.w, fit: BoxFit.none), + Utils.buildSvgWithAssets(icon: AppAssets.women_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false), SizedBox(width: 12.w), Flexible( child: Column( @@ -102,11 +102,11 @@ class _HealthCalculatorsPageState extends State { : Column( children: [ Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.bloodSugar, height: 40.h, width: 40.w, fit: BoxFit.none), + Utils.buildSvgWithAssets(icon: AppAssets.bloodSugar, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false), SizedBox(width: 12.w), Flexible( child: Column( @@ -136,11 +136,11 @@ class _HealthCalculatorsPageState extends State { }), SizedBox(height: 16.h), Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.bloodCholestrol, height: 40.h, width: 40.w, fit: BoxFit.none), + Utils.buildSvgWithAssets(icon: AppAssets.bloodCholestrol, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false), SizedBox(width: 12.w), Flexible( child: Column( @@ -166,11 +166,11 @@ class _HealthCalculatorsPageState extends State { }), SizedBox(height: 16.h), Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.triglycerides, height: 40.h, width: 40.w, fit: BoxFit.none), + Utils.buildSvgWithAssets(icon: AppAssets.triglycerides, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false), SizedBox(width: 12.w), Flexible( child: Column( diff --git a/lib/presentation/health_calculators_and_converts/widgets/bf.dart b/lib/presentation/health_calculators_and_converts/widgets/bf.dart index c8c949c4..481bfcbf 100644 --- a/lib/presentation/health_calculators_and_converts/widgets/bf.dart +++ b/lib/presentation/health_calculators_and_converts/widgets/bf.dart @@ -85,7 +85,7 @@ class _BodyFatWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( child: Row( @@ -111,7 +111,7 @@ class _BodyFatWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _genders.length, @@ -165,7 +165,7 @@ class _BodyFatWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -221,7 +221,7 @@ class _BodyFatWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _heightUnits.length, @@ -277,7 +277,7 @@ class _BodyFatWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -338,7 +338,7 @@ class _BodyFatWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _heightUnits.length, @@ -394,7 +394,7 @@ class _BodyFatWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -455,7 +455,7 @@ class _BodyFatWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _heightUnits.length, @@ -511,7 +511,7 @@ class _BodyFatWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -572,7 +572,7 @@ class _BodyFatWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _heightUnits.length, diff --git a/lib/presentation/health_calculators_and_converts/widgets/bmi.dart b/lib/presentation/health_calculators_and_converts/widgets/bmi.dart index 3969db41..a5ea06d1 100644 --- a/lib/presentation/health_calculators_and_converts/widgets/bmi.dart +++ b/lib/presentation/health_calculators_and_converts/widgets/bmi.dart @@ -67,7 +67,7 @@ class _BMIWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -123,7 +123,7 @@ class _BMIWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _heightUnits.length, @@ -179,7 +179,7 @@ class _BMIWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -240,7 +240,7 @@ class _BMIWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _weightUnits.length, diff --git a/lib/presentation/health_calculators_and_converts/widgets/bmr.dart b/lib/presentation/health_calculators_and_converts/widgets/bmr.dart index f19c6b5c..d1cb2c3a 100644 --- a/lib/presentation/health_calculators_and_converts/widgets/bmr.dart +++ b/lib/presentation/health_calculators_and_converts/widgets/bmr.dart @@ -80,7 +80,7 @@ class _BMRWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( child: Row( @@ -107,7 +107,7 @@ class _BMRWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _genders.length, @@ -162,7 +162,7 @@ class _BMRWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -199,7 +199,7 @@ class _BMRWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -255,7 +255,7 @@ class _BMRWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _heightUnits.length, @@ -312,7 +312,7 @@ class _BMRWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -373,7 +373,7 @@ class _BMRWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _weightUnits.length, @@ -430,7 +430,7 @@ class _BMRWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( child: Row( @@ -456,7 +456,7 @@ class _BMRWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _activity.length, diff --git a/lib/presentation/health_calculators_and_converts/widgets/calories.dart b/lib/presentation/health_calculators_and_converts/widgets/calories.dart index b1a4b773..1609c056 100644 --- a/lib/presentation/health_calculators_and_converts/widgets/calories.dart +++ b/lib/presentation/health_calculators_and_converts/widgets/calories.dart @@ -81,7 +81,7 @@ class _CaloriesWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.gender, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( child: Row( @@ -108,7 +108,7 @@ class _CaloriesWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _genders.length, @@ -162,7 +162,7 @@ class _CaloriesWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -199,7 +199,7 @@ class _CaloriesWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -255,7 +255,7 @@ class _CaloriesWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _heightUnits.length, @@ -311,7 +311,7 @@ class _CaloriesWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -372,7 +372,7 @@ class _CaloriesWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _weightUnits.length, @@ -429,7 +429,7 @@ class _CaloriesWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( child: Row( @@ -455,7 +455,7 @@ class _CaloriesWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _activity.length, diff --git a/lib/presentation/health_calculators_and_converts/widgets/crabs.dart b/lib/presentation/health_calculators_and_converts/widgets/crabs.dart index 83130fe2..c241e688 100644 --- a/lib/presentation/health_calculators_and_converts/widgets/crabs.dart +++ b/lib/presentation/health_calculators_and_converts/widgets/crabs.dart @@ -62,7 +62,7 @@ class _CrabsWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -93,7 +93,7 @@ class _CrabsWidgetState extends State { ], ), ), - Utils.buildSvgWithAssets(icon: AppAssets.ask_doctor_icon).onPress(() { + Utils.buildSvgWithAssets(icon: AppAssets.ask_doctor_icon, applyThemeColor: false).onPress(() { Navigator.pop(context); Navigator.of(context).push(CustomPageRoute(page: HealthCalculatorDetailedPage(calculatorType: HealthCalculatorsTypeEnum.calories))); }), @@ -104,7 +104,7 @@ class _CrabsWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( child: Row( @@ -130,7 +130,7 @@ class _CrabsWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _activity.length, diff --git a/lib/presentation/health_calculators_and_converts/widgets/ibw.dart b/lib/presentation/health_calculators_and_converts/widgets/ibw.dart index 1665ccd1..a783d2c8 100644 --- a/lib/presentation/health_calculators_and_converts/widgets/ibw.dart +++ b/lib/presentation/health_calculators_and_converts/widgets/ibw.dart @@ -73,7 +73,7 @@ class _IdealBodyWeightWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.height, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -129,7 +129,7 @@ class _IdealBodyWeightWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _heightUnits.length, @@ -185,7 +185,7 @@ class _IdealBodyWeightWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.weight, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -246,7 +246,7 @@ class _IdealBodyWeightWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _weightUnits.length, @@ -301,7 +301,7 @@ class _IdealBodyWeightWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.activity, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( child: Row( @@ -327,7 +327,7 @@ class _IdealBodyWeightWidgetState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: _activity.length, diff --git a/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart b/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart index 8d7591e6..556880d4 100644 --- a/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart +++ b/lib/presentation/health_calculators_and_converts/widgets/ovulation.dart @@ -89,7 +89,7 @@ class _OvulationWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, @@ -126,7 +126,7 @@ class _OvulationWidgetState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w), + Utils.buildSvgWithAssets(icon: AppAssets.age, width: 40.w, height: 40.w, applyThemeColor: false), SizedBox(width: 12.w), Expanded( flex: 3, diff --git a/lib/presentation/health_trackers/add_health_tracker_entry_page.dart b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart index 702875d9..8320972e 100644 --- a/lib/presentation/health_trackers/add_health_tracker_entry_page.dart +++ b/lib/presentation/health_trackers/add_health_tracker_entry_page.dart @@ -211,7 +211,7 @@ class _AddHealthTrackerEntryPageState extends State { child: Container( constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.7), padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: items.length, diff --git a/lib/presentation/health_trackers/health_trackers_page.dart b/lib/presentation/health_trackers/health_trackers_page.dart index 9c68281f..66630f28 100644 --- a/lib/presentation/health_trackers/health_trackers_page.dart +++ b/lib/presentation/health_trackers/health_trackers_page.dart @@ -26,7 +26,7 @@ Widget buildHealthTrackerCard({ required VoidCallback onTap, }) { return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart b/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart index 7e4ac2c9..39465b0f 100644 --- a/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart +++ b/lib/presentation/health_trackers/widgets/tracker_last_value_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -144,7 +145,16 @@ class TrackerLastValueCard extends StatelessWidget { break; } - final formattedDate = lastDate != null ? DateFormat('EEE DD MMM, yy').format(lastDate) : DateFormat('EEE DD MMM, yy').format(DateTime.now()); + // final formattedDate = lastDate != null ? DateFormat('EE DD MMM, yy').format(lastDate) : DateFormat('EE DD MMM, yy').format(DateTime.now()); + final formattedDate = lastDate != null + ? DateUtil.getWeekDayMonthDayYearDateFormatted( + lastDate, + "en", + ) + : DateUtil.getWeekDayMonthDayYearDateFormatted( + DateTime.now(), + "en", + ); // Show shimmer while loading if (viewModel.isLoading) { diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index fdc8c00d..de3235aa 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -389,7 +389,7 @@ class ServicesPage extends StatelessWidget { spacing: 8.w, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h, applyThemeColor: false), LocaleKeys.habibWallet.tr().toText14(weight: FontWeight.w600, maxlines: 2).expanded, Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), ], @@ -449,7 +449,7 @@ class ServicesPage extends StatelessWidget { spacing: 8.w, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false), LocaleKeys.medicalFile.tr().toText16(weight: FontWeight.w600, maxlines: 2).expanded, Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), ], @@ -692,6 +692,8 @@ class ServicesPage extends StatelessWidget { ) ], ), + SizedBox(height: 24.h), + LocaleKeys.hmgPolicies.tr().toText18(weight: FontWeight.bold), SizedBox(height: 16.h), Row( children: [ diff --git a/lib/presentation/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart index 3dd5c462..dd70c413 100644 --- a/lib/presentation/hmg_services/services_view.dart +++ b/lib/presentation/hmg_services/services_view.dart @@ -48,6 +48,7 @@ class ServiceGridViewItem extends StatelessWidget { icon: hmgServiceComponentModel.icon, iconColor: isHealthToolIcon ? null : AppColors.whiteColor, fit: BoxFit.contain, + applyThemeColor: false ), ), ), diff --git a/lib/presentation/home/data/landing_page_data.dart b/lib/presentation/home/data/landing_page_data.dart index c52d5e19..b74e57f3 100644 --- a/lib/presentation/home/data/landing_page_data.dart +++ b/lib/presentation/home/data/landing_page_data.dart @@ -1,19 +1,20 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/home/data/service_card_data.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; class LandingPageData { - static List getNotLoggedInServiceCardsList = [ + static List get getNotLoggedInServiceCardsList => [ ServiceCardData( serviceName: "emergency", icon: AppAssets.emergency_services_icon, title: LocaleKeys.emergency, subtitle: LocaleKeys.services2, backgroundColor: AppColors.primaryRedColor, - iconColor: AppColors.whiteColor, - textColor: AppColors.whiteColor, + iconColor: Colors.white, + textColor: Colors.white, isBold: true, ), ServiceCardData( @@ -68,15 +69,15 @@ class LandingPageData { ), ]; - static List getLoggedInServiceCardsList = [ + static List get getLoggedInServiceCardsList => [ ServiceCardData( serviceName: "emergency", icon: AppAssets.emergency_services_icon, title: LocaleKeys.emergency, subtitle: LocaleKeys.services2, backgroundColor: AppColors.primaryRedColor, - iconColor: AppColors.whiteColor, - textColor: AppColors.whiteColor, + iconColor: Colors.white, + textColor: Colors.white, isBold: true, ), ServiceCardData( @@ -141,7 +142,7 @@ class LandingPageData { ), ]; - static List getServiceCardsList = [ + static List get getServiceCardsList => [ ServiceCardData( serviceName: "livecare", icon: AppAssets.small_livecare_icon, diff --git a/lib/presentation/home/data/service_card_data.dart b/lib/presentation/home/data/service_card_data.dart index 3bc22d7c..c16147b1 100644 --- a/lib/presentation/home/data/service_card_data.dart +++ b/lib/presentation/home/data/service_card_data.dart @@ -8,8 +8,8 @@ class ServiceCardData { final String title; final String subtitle; final Color? iconColor; - final Color textColor; - final Color backgroundColor; + final Color? textColor; + final Color? backgroundColor; final bool isBold; final String largeCardIcon; @@ -19,9 +19,9 @@ class ServiceCardData { this.title = "", this.subtitle = "", this.largeCardIcon = "", - this.backgroundColor = AppColors.whiteColor, - this.iconColor = AppColors.blackColor, - this.textColor = AppColors.blackColor, + this.backgroundColor, + this.iconColor, + this.textColor, this.isBold = false }); } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index e8482bf0..607a47f7 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -296,9 +296,9 @@ class _LandingPageState extends State { context.navigateWithName(AppRoutes.userInfoSelection); }, padding: EdgeInsetsGeometry.zero, - backgroundColor: Color(0xFF2B353E), - borderColor: Color(0xFF2B353E), - textColor: AppColors.whiteColor, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, fontSize: 14.f, fontWeight: FontWeight.w600, borderRadius: 12.r, diff --git a/lib/presentation/home/widgets/habib_wallet_card.dart b/lib/presentation/home/widgets/habib_wallet_card.dart index ed48058d..0ac32443 100644 --- a/lib/presentation/home/widgets/habib_wallet_card.dart +++ b/lib/presentation/home/widgets/habib_wallet_card.dart @@ -45,7 +45,7 @@ class HabibWalletCard extends StatelessWidget { child: Stack(children: [ Positioned( right: 0, - child: ClipRRect(borderRadius: BorderRadius.circular(24.0), child: Utils.buildSvgWithAssets(icon: AppAssets.habib_background_icon, width: 150.h, height: 150.h)), + child: ClipRRect(borderRadius: BorderRadius.circular(24.0), child: Utils.buildSvgWithAssets(icon: AppAssets.habib_background_icon, width: 150.h, height: 150.h, applyThemeColor: false)), ), Padding( padding: EdgeInsets.all(16.h), @@ -123,7 +123,7 @@ class HabibWalletCard extends StatelessWidget { icon: AppAssets.recharge_icon, iconSize: 24.h, backgroundColor: AppColors.infoColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, text: LocaleKeys.recharge.tr(context: context), borderWidth: 0.w, fontWeight: FontWeight.w500, diff --git a/lib/presentation/home/widgets/large_service_card.dart b/lib/presentation/home/widgets/large_service_card.dart index e25ed0b3..7c86dd09 100644 --- a/lib/presentation/home/widgets/large_service_card.dart +++ b/lib/presentation/home/widgets/large_service_card.dart @@ -72,7 +72,7 @@ class LargeServiceCard extends StatelessWidget { height: 48.h, width: 48.h, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: serviceCardData.backgroundColor, + color: serviceCardData.backgroundColor!, borderRadius: 12.r, hasShadow: false, ), @@ -82,6 +82,7 @@ class LargeServiceCard extends StatelessWidget { icon: serviceCardData.icon, iconColor: serviceCardData.iconColor, fit: BoxFit.contain, + applyThemeColor: false, ), ), ), @@ -209,7 +210,7 @@ class FadedLargeServiceCard extends StatelessWidget { height: 32.h, width: 32.h, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: serviceCardData.backgroundColor, + color: serviceCardData.backgroundColor!, borderRadius: 30.r, hasShadow: false, ), @@ -219,6 +220,7 @@ class FadedLargeServiceCard extends StatelessWidget { icon: serviceCardData.icon, iconColor: serviceCardData.iconColor, fit: BoxFit.contain, + applyThemeColor: false ), ), ), diff --git a/lib/presentation/home/widgets/small_service_card.dart b/lib/presentation/home/widgets/small_service_card.dart index 84ef6fa2..0c2da8d6 100644 --- a/lib/presentation/home/widgets/small_service_card.dart +++ b/lib/presentation/home/widgets/small_service_card.dart @@ -32,9 +32,9 @@ class SmallServiceCard extends StatelessWidget { final String icon; final String title; final String subtitle; - final Color iconColor; - final Color textColor; - final Color backgroundColor; + final Color? iconColor; + final Color? textColor; + final Color? backgroundColor; final bool isBold; SmallServiceCard({ @@ -43,33 +43,36 @@ class SmallServiceCard extends StatelessWidget { this.serviceName = "", this.title = "", this.subtitle = "", - this.backgroundColor = AppColors.whiteColor, - this.iconColor = AppColors.blackColor, - this.textColor = AppColors.blackColor, + this.backgroundColor, + this.iconColor, + this.textColor, this.isBold = false, }); @override Widget build(BuildContext context) { + final resolvedBgColor = backgroundColor ?? AppColors.whiteColor; + final resolvedIconColor = iconColor ?? AppColors.blackColor; + final resolvedTextColor = textColor ?? AppColors.blackColor; return Container( height: 92.h, width: 92.h, padding: EdgeInsets.all(4.0), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: backgroundColor, + color: resolvedBgColor, borderRadius: 16, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: icon, iconColor: iconColor, width: 32.h, height: 32.h), + Utils.buildSvgWithAssets(icon: icon, iconColor: resolvedIconColor, width: 32.h, height: 32.h), SizedBox(height: 6.h), if (isFoldable || isTablet) ...[ - title.tr(context: context).toText10(color: textColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxlines: 1), - subtitle.tr(context: context).toText10(color: textColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxlines: 1), + title.tr(context: context).toText10(color: resolvedTextColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxlines: 1), + subtitle.tr(context: context).toText10(color: resolvedTextColor, weight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxlines: 1), ] else ...[ - title.tr(context: context).toText12(color: textColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 1), - subtitle.tr(context: context).toText12(color: textColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 1), + title.tr(context: context).toText12(color: resolvedTextColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 1), + subtitle.tr(context: context).toText12(color: resolvedTextColor, fontWeight: FontWeight.w500, isCenter: true, letterSpacing: -0.2, maxLine: 1), ] ], ), diff --git a/lib/presentation/home/widgets/welcome_widget.dart b/lib/presentation/home/widgets/welcome_widget.dart index 1bb17b5b..4ccc126a 100644 --- a/lib/presentation/home/widgets/welcome_widget.dart +++ b/lib/presentation/home/widgets/welcome_widget.dart @@ -40,7 +40,7 @@ class WelcomeWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Flexible(child: name.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1, height: 1)), - const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.black), + Icon(Icons.keyboard_arrow_down, size: 20, color: AppColors.greyTextColor), ], ), ], diff --git a/lib/presentation/home_health_care/hhc_procedures_page.dart b/lib/presentation/home_health_care/hhc_procedures_page.dart index ef5dce5c..c420742c 100644 --- a/lib/presentation/home_health_care/hhc_procedures_page.dart +++ b/lib/presentation/home_health_care/hhc_procedures_page.dart @@ -434,7 +434,7 @@ class _HhcProceduresPageState extends State { child: Container( height: 80.h, decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(10.r), ), ), diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index a6d81575..a074a8d8 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; @@ -122,35 +123,69 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ "Approval Details".toText16(isBold: true), - SizedBox(height: 16.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - LocaleKeys.procedureName.tr(context: context).toText14(isBold: true), - Expanded( - child: (insuranceApprovalResponseModel.apporvalDetails!.procedureName ?? "") - .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, textAlignment: TextAlign.end, maxLine: 2), - ), - ], - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "${LocaleKeys.status.tr(context: context)}: ".toText14(isBold: true), - insuranceApprovalResponseModel.apporvalDetails!.status!.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), - ], - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "${LocaleKeys.usageStatus.tr(context: context)}: ".toText14(isBold: true), - (insuranceApprovalResponseModel.apporvalDetails!.isInvoicedDesc ?? "").toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), - ], - ), - ], - ) : SizedBox.shrink(), + ListView.separated( + padding: EdgeInsets.only(top: 16.h), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: insuranceApprovalResponseModel.apporvalDetails!.length, + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.procedureName.tr(context: context).toText14(isBold: true), + Expanded( + child: (insuranceApprovalResponseModel.apporvalDetails![index]!.procedureName ?? "") + .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor, textAlignment: TextAlign.end, maxLine: 2), + ), + ], + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "${LocaleKeys.status.tr(context: context)}: ".toText14(isBold: true), + insuranceApprovalResponseModel.apporvalDetails![index]!.status!.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "${LocaleKeys.usageStatus.tr(context: context)}: ".toText14(isBold: true), + (insuranceApprovalResponseModel.apporvalDetails![index]!.isInvoicedDesc ?? "").toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + ], + ), + ], + ), + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => Column( + children: [ + SizedBox(height: 16.h), + Divider(height: 1, color: AppColors.greyColor), + SizedBox(height: 16.h), + ], + ), + ), + ], + ) + : SizedBox.shrink(), ), ) ], diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index 2e4a96b6..1b5db920 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -66,7 +66,7 @@ class LabOrderByTest extends StatelessWidget { backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, - fontSize: 14, + fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 4016aff9..15461659 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_toolbar.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingListView( title: LocaleKeys.labResults.tr(context: context), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: Consumer( builder: (context, model, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ CustomButton( text: LocaleKeys.byClinic.tr(context: context), onPressed: () { model.setIsSortByClinic(true); }, backgroundColor: model.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, borderColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12, fontWeight: FontWeight.w500, borderRadius: 10, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), SizedBox(width: 8.h), CustomButton( text: LocaleKeys.byHospital.tr(context: context), onPressed: () { model.setIsSortByClinic(false); }, backgroundColor: model.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, borderColor: model.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, textColor: model.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, fontSize: 12, fontWeight: FontWeight.w500, borderRadius: 10, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available model.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (model.patientLabOrdersViewList.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.patientLabOrdersViewList.length, itemBuilder: (context, index) { final group = model.patientLabOrdersViewList[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: InkWell( onTap: () { setState(() { expandedIndex = isExpanded ? null : index; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AppCustomChipWidget(labelText: "${group.length} ${LocaleKeys.results.tr(context: context)}"), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Text( model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown'), style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), ], ), ), AnimatedSwitcher( duration: Duration(milliseconds: 500), switchInCurve: Curves.easeIn, switchOutCurve: Curves.easeOut, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SizeTransition( sizeFactor: animation, axisAlignment: 0.0, child: child, ), ); }, child: isExpanded ? Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 0.h), child: ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group[index]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded(child: (order.doctorName ?? order.doctorNameEnglish ?? "").toString().toText14(weight: FontWeight.w500)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( labelText: ("${LocaleKeys.orderNo.tr(context: context)}: ${order.orderNo!}"), ), AppCustomChipWidget( labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), ), AppCustomChipWidget( labelText: model.isSortByClinic ? (order.projectName ?? "") : (order.clinicDescription ?? ""), ), ], ), // Row( // children: [ // CustomButton( // text: ("Order No: ".needTranslation + order.orderNo!), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // ], // ), // SizedBox(height: 8.h), // Row( // children: [ // CustomButton( // text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // ], // ), SizedBox(height: 12.h), Row( children: [ Expanded(flex: 2, child: SizedBox()), // Expanded( // flex: 1, // child: Container( // height: 40.h, // width: 40.w, // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( // color: AppColors.textColor, // borderRadius: 12, // ), // child: Padding( // padding: EdgeInsets.all(12.h), // child: Transform.flip( // flipX: _appState.isArabic(), // child: Utils.buildSvgWithAssets( // icon: AppAssets.forward_arrow_icon_small, // iconColor: AppColors.whiteColor, // fit: BoxFit.contain, // ), // ), // ), // ).onPress(() { // model.currentlySelectedPatientOrder = order; // labProvider.getPatientLabResultByHospital(order); // labProvider.getPatientSpecialResult(order); // Navigator.of(context).push( // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // ); // }), // ) Expanded( flex: 2, child: CustomButton( icon: AppAssets.view_report_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, text: LocaleKeys.viewResults.tr(context: context), onPressed: () { model.currentlySelectedPatientOrder = order; labProvider.getPatientLabResultByHospital(order); labProvider.getPatientSpecialResult(order); Navigator.of(context).push( CustomPageRoute(page: LabResultByClinic(labOrder: order)), ); }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ) ], ), // SizedBox(height: 12.h), // Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), // SizedBox(height: 12.h), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.length)) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (model.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: model.indexedCharacterForUniqueTest, details: model.uniqueTestsList, labViewModel: model, rangeViewModel: rangeViewModel, appState: _appState, ) ], )); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_toolbar.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingListView( title: LocaleKeys.labResults.tr(context: context), search: () async { if (labProvider.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: labProvider.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; labProvider.filterLabReports(value); } } }, child: Consumer( builder: (context, labViewModel, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ CustomButton( text: LocaleKeys.byClinic.tr(context: context), onPressed: () { labViewModel.setIsSortByClinic(true); }, backgroundColor: labViewModel.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, borderColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, fontSize: 12.f, fontWeight: FontWeight.w500, borderRadius: 10, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), SizedBox(width: 8.h), CustomButton( text: LocaleKeys.byHospital.tr(context: context), onPressed: () { labViewModel.setIsSortByClinic(false); }, backgroundColor: labViewModel.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, borderColor: labViewModel.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, textColor: labViewModel.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, fontSize: 12, fontWeight: FontWeight.w500, borderRadius: 10, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? Column( children: [ AppCustomChipWidget( labelText: selectedFilterText!, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIcon: AppAssets.close_bottom_sheet_icon, deleteIconColor: AppColors.whiteColor, deleteIconHasColor: true, onDeleteTap: () { selectedFilterText = ""; labProvider.filterLabReports(""); }, ), SizedBox(height: 8.h), ], ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available labViewModel.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (labViewModel.patientLabOrdersViewList.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: labViewModel.patientLabOrdersViewList.length, itemBuilder: (context, index) { final group = labViewModel.patientLabOrdersViewList[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: InkWell( onTap: () { setState(() { expandedIndex = isExpanded ? null : index; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AppCustomChipWidget(labelText: "${group.length} ${LocaleKeys.results.tr(context: context)}"), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Text( labViewModel.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown'), style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis, ), ], ), ), AnimatedSwitcher( duration: Duration(milliseconds: 500), switchInCurve: Curves.easeIn, switchOutCurve: Curves.easeOut, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SizeTransition( sizeFactor: animation, axisAlignment: 0.0, child: child, ), ); }, child: isExpanded ? Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 0.h), child: ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group[index]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded(child: (order.doctorName ?? order.doctorNameEnglish ?? "").toString().toText14(weight: FontWeight.w500)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( labelText: ("${LocaleKeys.orderNo.tr(context: context)}: ${order.orderNo!}"), ), AppCustomChipWidget( labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), ), AppCustomChipWidget( labelText: labViewModel.isSortByClinic ? (order.projectName ?? "") : (order.clinicDescription ?? ""), ), ], ), // Row( // children: [ // CustomButton( // text: ("Order No: ".needTranslation + order.orderNo!), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // ], // ), // SizedBox(height: 8.h), // Row( // children: [ // CustomButton( // text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), // onPressed: () {}, // backgroundColor: AppColors.greyColor, // borderColor: AppColors.greyColor, // textColor: AppColors.blackColor, // fontSize: 10, // fontWeight: FontWeight.w500, // borderRadius: 8, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 24.h, // ), // ], // ), SizedBox(height: 12.h), Row( children: [ Expanded(flex: 2, child: SizedBox()), // Expanded( // flex: 1, // child: Container( // height: 40.h, // width: 40.w, // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( // color: AppColors.textColor, // borderRadius: 12, // ), // child: Padding( // padding: EdgeInsets.all(12.h), // child: Transform.flip( // flipX: _appState.isArabic(), // child: Utils.buildSvgWithAssets( // icon: AppAssets.forward_arrow_icon_small, // iconColor: AppColors.whiteColor, // fit: BoxFit.contain, // ), // ), // ), // ).onPress(() { // model.currentlySelectedPatientOrder = order; // labProvider.getPatientLabResultByHospital(order); // labProvider.getPatientSpecialResult(order); // Navigator.of(context).push( // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // ); // }), // ) Expanded( flex: 2, child: CustomButton( icon: AppAssets.view_report_icon, iconColor: AppColors.primaryRedColor, iconSize: 16.h, text: LocaleKeys.viewResults.tr(context: context), onPressed: () { labViewModel.currentlySelectedPatientOrder = order; labProvider.getPatientLabResultByHospital(order); labProvider.getPatientSpecialResult(order); Navigator.of(context).push( CustomPageRoute(page: LabResultByClinic(labOrder: order)), ); }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ) ], ), // SizedBox(height: 12.h), // Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), // SizedBox(height: 12.h), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.length)) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (labViewModel.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: labViewModel.indexedCharacterForUniqueTest, details: labViewModel.uniqueTestsList, labViewModel: labViewModel, rangeViewModel: rangeViewModel, appState: _appState, ) ], )); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart index 8fb30900..af6c97f8 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart @@ -41,21 +41,54 @@ class LabResultByClinic extends StatelessWidget { Expanded( child: CollapsingListView( title: LocaleKeys.labResults.tr(), - aiOverview: () async { - final _dialogService = getIt.get(); - await _dialogService.showCommonBottomSheetWithoutH( - message: LocaleKeys.aiDisclaimer.tr(), - label: LocaleKeys.consent.tr(), - okLabel: LocaleKeys.acceptLbl.tr(), - cancelLabel: LocaleKeys.rejectView.tr(), - onOkPressed: () { - context.pop(); - labViewModel.getAiOverviewLabOrders(labOrder: labOrder, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); - }, - onCancelPressed: () { - context.pop(); - }); + downloadReport: () async { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.generatingReport.tr(context: context)); + await labViewModel + .getLabResultReportPDF( + labOrder: labOrder, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }) + .then((val) async { + LoaderBottomSheet.hideLoader(); + if (labViewModel.patientLabResultReportPDFBase64.isNotEmpty) { + String path = await Utils.createFileFromString(labViewModel.patientLabResultReportPDFBase64, "pdf"); + try { + OpenFilex.open(path); + } catch (ex) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Cannot open file"), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + } + }); }, + // aiOverview: () async { + // final _dialogService = getIt.get(); + // await _dialogService.showCommonBottomSheetWithoutH( + // message: LocaleKeys.aiDisclaimer.tr(), + // label: LocaleKeys.consent.tr(), + // okLabel: LocaleKeys.acceptLbl.tr(), + // cancelLabel: LocaleKeys.rejectView.tr(), + // onOkPressed: () { + // context.pop(); + // labViewModel.getAiOverviewLabOrders(labOrder: labOrder, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); + // }, + // onCancelPressed: () { + // context.pop(); + // }); + // }, child: SingleChildScrollView( child: Column( spacing: 8.h, @@ -109,53 +142,46 @@ class LabResultByClinic extends StatelessWidget { borderRadius: 24.h, hasShadow: true, ), - child: CustomButton( - text: LocaleKeys.downloadReport.tr(context: context), - onPressed: () async { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.generatingReport.tr(context: context)); - await labViewModel - .getLabResultReportPDF( - labOrder: labOrder, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }) - .then((val) async { - LoaderBottomSheet.hideLoader(); - if (labViewModel.patientLabResultReportPDFBase64.isNotEmpty) { - String path = await Utils.createFileFromString(labViewModel.patientLabResultReportPDFBase64, "pdf"); - try { - OpenFilex.open(path); - } catch (ex) { - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: "Cannot open file"), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - } - } - }); - }, - backgroundColor: AppColors.successColor, - borderColor: AppColors.successColor, - textColor: AppColors.whiteColor, - fontSize: 16, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 45.h, - icon: AppAssets.download, - iconColor: AppColors.whiteColor, - iconSize: 20.h, - ).paddingSymmetrical(24.h, 24.h), + child: Container( + height: 56.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [0.236, 1.0], // 53.6% and 100% + colors: [ + Color(0xFF8A38F5), // Transparent + Color(0xFFE20BBB), // Solid #F8F8F8 + ], + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(right: 4.w, left: 4.w), + child: Utils.buildSvgWithAssets(icon: AppAssets.aiOverView, width: 16.h, height: 16.h, iconColor: Colors.white), + ), + LocaleKeys.generateAiAnalysis.tr(context: context).toText16(isBold: true) + ], + ), + ).paddingSymmetrical(24.h, 24.h).onPress(() async { + final _dialogService = getIt.get(); + await _dialogService.showCommonBottomSheetWithoutH( + message: LocaleKeys.aiDisclaimer.tr(), + label: LocaleKeys.consent.tr(), + okLabel: LocaleKeys.acceptLbl.tr(), + cancelLabel: LocaleKeys.rejectView.tr(), + onOkPressed: () { + context.pop(); + labViewModel.getAiOverviewLabOrders(labOrder: labOrder, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); + }, + onCancelPressed: () { + context.pop(); + }); + }), ), ], ), diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 6caa4a59..07b9fb65 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -27,7 +27,7 @@ class LabResultList extends StatelessWidget { shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) { var labItem = list[index]; return LabOrderResultItem(onTap: () { - model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.packageShortDescription!, labItem.uOM ?? ""); + model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.testShortDescription!, labItem.uOM ?? ""); }, tests: labItem, index: index, diff --git a/lib/presentation/lab/lab_result_via_clinic/ai_analysis_widget.dart b/lib/presentation/lab/lab_result_via_clinic/ai_analysis_widget.dart index f0c9b26b..7a477761 100644 --- a/lib/presentation/lab/lab_result_via_clinic/ai_analysis_widget.dart +++ b/lib/presentation/lab/lab_result_via_clinic/ai_analysis_widget.dart @@ -20,7 +20,7 @@ class AiAnalysisWidget extends StatelessWidget { padding: EdgeInsets.all(1.5.r), // Border thickness child: Container( decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(22.r), ), padding: EdgeInsets.all(20.r), @@ -44,7 +44,7 @@ class AiAnalysisWidget extends StatelessWidget { SizedBox(height: 8.h), data.summary!.toText14(color: AppColors.textColorLight, height: 1.5, weight: FontWeight.w400), SizedBox(height: 16.h), - const Divider(color: AppColors.dividerColor, thickness: 1), + Divider(color: AppColors.dividerColor, thickness: 1), SizedBox(height: 16.h), ], if (data.aiPredictions != null && data.aiPredictions!.isNotEmpty) ...[ @@ -52,7 +52,7 @@ class AiAnalysisWidget extends StatelessWidget { SizedBox(height: 12.h), ...data.aiPredictions!.map((item) => _buildBulletItem(item)), SizedBox(height: 16.h), - const Divider(color: AppColors.dividerColor, thickness: 1), + Divider(color: AppColors.dividerColor, thickness: 1), SizedBox(height: 16.h), ], if (data.prevention != null && data.prevention!.isNotEmpty) ...[ @@ -88,7 +88,7 @@ class AiAnalysisWidget extends StatelessWidget { child: Container( width: 5.r, height: 5.r, - decoration: const BoxDecoration(color: AppColors.blackColor, shape: BoxShape.circle), + decoration: BoxDecoration(color: AppColors.blackColor, shape: BoxShape.circle), ), ), Expanded( diff --git a/lib/presentation/lab/lab_result_via_clinic/lab_ai_analysis_detailed_page.dart b/lib/presentation/lab/lab_result_via_clinic/lab_ai_analysis_detailed_page.dart index 14ff620b..2a4fd66d 100644 --- a/lib/presentation/lab/lab_result_via_clinic/lab_ai_analysis_detailed_page.dart +++ b/lib/presentation/lab/lab_result_via_clinic/lab_ai_analysis_detailed_page.dart @@ -133,7 +133,7 @@ class _LabAiAnalysisDetailedPageState extends State { ), if (!isLast) ...[ SizedBox(height: 12.h), - const Divider(color: AppColors.dividerColor, thickness: 1), + Divider(color: AppColors.dividerColor, thickness: 1), SizedBox(height: 12.h), ], ], diff --git a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart index ba97731f..cf6ce97b 100644 --- a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart @@ -22,7 +22,7 @@ class LabOrderResultItem extends StatelessWidget { final bool isExpanded; final Color iconColor; - const LabOrderResultItem({super.key, required this.onTap, this.tests, required this.index, this.isLoading = false, this.isExpanded = false,required this.iconColor, required this.severityText}); + const LabOrderResultItem({super.key, required this.onTap, this.tests, required this.index, this.isLoading = false, this.isExpanded = false, required this.iconColor, required this.severityText}); @override build(BuildContext context) { @@ -42,7 +42,10 @@ class LabOrderResultItem extends StatelessWidget { padding: EdgeInsets.only(bottom: 8.h), child: '${tests!.description}'.toText14(weight: FontWeight.w500), ), - (tests!.packageShortDescription ?? "").toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + // (tests!.packageShortDescription ?? "").toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), + + ((tests!.testShortDescription != null && tests!.testShortDescription!.isNotEmpty) ? tests!.testShortDescription : tests!.packageShortDescription)! + .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), SizedBox(height: 12.h), Directionality( textDirection: ui.TextDirection.ltr, @@ -65,28 +68,30 @@ class LabOrderResultItem extends StatelessWidget { ), ), ), - SizedBox(width: 4.h,), + SizedBox( + width: 4.h, + ), Visibility( // visible: tests?.checkIfGraphShouldBeDisplayed() == true, visible: true, child: Expanded( flex: 2, - child: Visibility( - visible: tests?.referanceRange != null, - child: Text( - "(${LocaleKeys.referenceRange.tr(context: context)}: ${tests?.referanceRange})", - style: TextStyle( - fontSize: 12.f, - fontWeight: FontWeight.w500, - fontFamily: 'Poppins', - color: AppColors.greyTextColor, + child: Visibility( + visible: tests?.referanceRange != null, + child: Text( + "(${LocaleKeys.referenceRange.tr(context: context)}: ${tests?.referanceRange})", + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w500, + fontFamily: 'Poppins', + color: AppColors.greyTextColor, + ), + // overflow: TextOverflow.ellipsis, + // maxLines: 2, + softWrap: true, ), - // overflow: TextOverflow.ellipsis, - // maxLines: 2, - softWrap: true, ), ), - ), ) ], ), @@ -102,14 +107,8 @@ class LabOrderResultItem extends StatelessWidget { spacing: 6.h, crossAxisAlignment: CrossAxisAlignment.start, children: [ - severityText.tr().toText10( - weight: FontWeight.w500, - color: AppColors.greyTextColor), - Utils.buildSvgWithAssets( - icon: AppAssets.lab_result_indicator, - width: 21, - height: 23, - iconColor: iconColor), + severityText.tr().toText10(weight: FontWeight.w500, color: AppColors.greyTextColor), + Utils.buildSvgWithAssets(icon: AppAssets.lab_result_indicator, width: 21, height: 23, iconColor: iconColor), ], ), ), @@ -119,7 +118,7 @@ class LabOrderResultItem extends StatelessWidget { iconSize: 16.h, text: LocaleKeys.viewReport.tr(context: context), onPressed: () { - onTap(); + onTap(); }, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index cab20aed..d2c1b524 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -34,41 +34,97 @@ class LabResultDetails extends StatelessWidget { Widget build(BuildContext context) { LabViewModel labViewModel = Provider.of(context, listen: false); final appState = getIt.get(); - return CollapsingListView( - title: LocaleKeys.labResultDetails.tr(context: context), - aiOverview: () async { - final _dialogService = getIt.get(); - await _dialogService.showCommonBottomSheetWithoutH( - message: LocaleKeys.aiDisclaimer.tr(), - label: LocaleKeys.consent.tr(), - okLabel: LocaleKeys.acceptLbl.tr(), - cancelLabel: LocaleKeys.rejectView.tr(), - onOkPressed: () { - context.pop(); - labViewModel.getAiOverviewSingleLabResult(langId: appState.getLanguageID().toString(), recentLabResult: recentLabResult, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); - }, - onCancelPressed: () { - context.pop(); - }); - }, - child: SingleChildScrollView( - child: Column( - spacing: 16.h, - children: [ - LabNameAndStatus(context), - getLabDescription(context), - LabGraph(context), - Selector( - selector: (_, model) => model.labOrderResponseByAi, - builder: (_, aiData, __) { - if (aiData != null) { - return AiAnalysisWidget(data: aiData).paddingOnly(bottom: 16.h); - } - return const SizedBox.shrink(); - }, + return Scaffold( + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.labResultDetails.tr(context: context), + // aiOverview: () async { + // final _dialogService = getIt.get(); + // await _dialogService.showCommonBottomSheetWithoutH( + // message: LocaleKeys.aiDisclaimer.tr(), + // label: LocaleKeys.consent.tr(), + // okLabel: LocaleKeys.acceptLbl.tr(), + // cancelLabel: LocaleKeys.rejectView.tr(), + // onOkPressed: () { + // context.pop(); + // labViewModel.getAiOverviewSingleLabResult(langId: appState.getLanguageID().toString(), recentLabResult: recentLabResult, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); + // }, + // onCancelPressed: () { + // context.pop(); + // }); + // }, + child: SingleChildScrollView( + child: Column( + spacing: 16.h, + children: [ + LabNameAndStatus(context), + getLabDescription(context), + LabGraph(context), + Selector( + selector: (_, model) => model.labOrderResponseByAi, + builder: (_, aiData, __) { + if (aiData != null) { + return AiAnalysisWidget(data: aiData).paddingOnly(bottom: 16.h); + } + return const SizedBox.shrink(); + }, + ), + ], + ).paddingAll(24.h), + ), ), - ], - ).paddingAll(24.h), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Container( + height: 56.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [0.236, 1.0], // 53.6% and 100% + colors: [ + Color(0xFF8A38F5), // Transparent + Color(0xFFE20BBB), // Solid #F8F8F8 + ], + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(right: 4.w, left: 4.w), + child: Utils.buildSvgWithAssets(icon: AppAssets.aiOverView, width: 16.h, height: 16.h, iconColor: Colors.white), + ), + LocaleKeys.generateAiAnalysis.tr(context: context).toText16(isBold: true) + ], + ), + ).paddingSymmetrical(24.h, 24.h).onPress(() async { + final _dialogService = getIt.get(); + await _dialogService.showCommonBottomSheetWithoutH( + message: LocaleKeys.aiDisclaimer.tr(), + label: LocaleKeys.consent.tr(), + okLabel: LocaleKeys.acceptLbl.tr(), + cancelLabel: LocaleKeys.rejectView.tr(), + onOkPressed: () { + context.pop(); + labViewModel.getAiOverviewSingleLabResult( + langId: appState.getLanguageID().toString(), recentLabResult: recentLabResult, loadingText: LocaleKeys.loadingAIAnalysis.tr(context: context)); + }, + onCancelPressed: () { + context.pop(); + }); + }), + ), + ], ), ); } @@ -166,7 +222,7 @@ class LabResultDetails extends StatelessWidget { : (labmodel.filteredGraphValues.length < 3) ? (labmodel.filteredGraphValues.length * 64) + 80.h : 260.h, - padding: EdgeInsets.all(16.h), + padding: EdgeInsets.all(14.h), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ @@ -177,7 +233,7 @@ class LabResultDetails extends StatelessWidget { Text( labmodel.isGraphVisible ? LocaleKeys.historyFlowchart.tr(context: context) : LocaleKeys.history.tr(context: context), style: TextStyle( - fontSize: 16, + fontSize: 16.f, fontFamily: 'Poppins', fontWeight: FontWeight.w600, color: AppColors.textColor, diff --git a/lib/presentation/lab/lab_results/lab_result_list_item.dart b/lib/presentation/lab/lab_results/lab_result_list_item.dart index 6ccf7509..f2dc90e4 100644 --- a/lib/presentation/lab/lab_results/lab_result_list_item.dart +++ b/lib/presentation/lab/lab_results/lab_result_list_item.dart @@ -42,7 +42,7 @@ class LabHistoryItem extends StatelessWidget{ ), Transform.flip( flipY: shouldRotateIcon, - child: Utils.buildSvgWithAssets(icon: assetUrl,height: 18, width: 18) + child: Utils.buildSvgWithAssets(icon: assetUrl, height: 18, width: 18) ), ], ); diff --git a/lib/presentation/lab/search_lab_report.dart b/lib/presentation/lab/search_lab_report.dart index bf27b911..d366f38f 100644 --- a/lib/presentation/lab/search_lab_report.dart +++ b/lib/presentation/lab/search_lab_report.dart @@ -59,7 +59,7 @@ class _SearchLabResultsContentState extends State { title: LocaleKeys.labResults.tr(), isClose: true, bottomChild: Container( - color: Colors.white, + color: AppColors.whiteColor, padding: EdgeInsets.all(ResponsiveExtension(20).h), child: CustomButton( text: LocaleKeys.search.tr(), @@ -144,7 +144,8 @@ class SuggestionChip extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: label.toText12( - color: isSelected ? Colors.white : Colors.black87, + // color: isSelected ? AppColors.textColor : Colors.black87, + color: AppColors.textColor, fontWeight: FontWeight.w500, ), ), diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 2e850996..f288af66 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -478,11 +478,12 @@ class _MedicalFilePageState extends State { // Using CustomExpandableList CustomExpandableList( expansionMode: ExpansionMode.exactlyOne, - dividerColor: Color(0xff2b353e1a), + dividerColor: AppColors.dividerColor, itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), items: [ ExpandableListItem( title: LocaleKeys.medicalServices.tr(context: context).toText18(weight: FontWeight.w600), + expandedBackgroundColor: Colors.transparent, children: [ SizedBox(height: 10.h), getSelectedTabData(0), @@ -518,53 +519,6 @@ class _MedicalFilePageState extends State { defaultTrailingIcon: Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 22.h, width: 22.w, iconColor: AppColors.textColor), ), ).paddingSymmetrical(16.w, 0.0), - - // ExpansionTileList( - // shrinkWrap: true, - // trailing: Icon(Icons.arrow_drop_down), - // expansionMode: ExpansionMode.exactlyOne, - // children: [ - // ExpansionTile( - // title: "Medical Services".toText20(weight: FontWeight.w600, color: AppColors.textColor), - // iconColor: Color(0xFF2E3039), - // trailing: Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 22.h, width: 22.w), - // children: [Text('Child 1')], - // ), - // ExpansionTile( - // title: "Medical Reports".toText20(weight: FontWeight.w600, color: AppColors.textColor), - // children: [Text('Child 2')], - // ), - // ExpansionTile( - // title: "Insurance & Payments".toText20(weight: FontWeight.w600, color: AppColors.textColor), - // children: [Text('Child 3')], - // ), - // ExpansionTile( - // title: "Tracker & Others".toText20(weight: FontWeight.w600, color: AppColors.textColor), - // children: [Text('Child 4')], - // ), - // ], - // ), - // Consumer(builder: (context, medicalFileVM, child) { - // return Column( - // children: [ - // CustomTabBar( - // activeTextColor: AppColors.primaryRedColor, - // activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), - // tabs: [ - // CustomTabBarModel(AppAssets.myFilesBottom, LocaleKeys.general.tr(context: context).needTranslation), - // CustomTabBarModel(AppAssets.insurance, LocaleKeys.insurance.tr(context: context)), - // CustomTabBarModel(AppAssets.requests, LocaleKeys.request.tr(context: context).needTranslation), - // CustomTabBarModel(AppAssets.more, "More".needTranslation), - // ], - // onTabChange: (index) { - // medicalFileVM.onTabChanged(index); - // }, - // ).paddingSymmetrical(24.w, 0.0), - // SizedBox(height: 24.h), - // getSelectedTabData(medicalFileVM.selectedTabIndex), - // ], - // ); - // }), ], ), ); @@ -669,9 +623,9 @@ class _MedicalFilePageState extends State { ), ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, @@ -792,7 +746,7 @@ class _MedicalFilePageState extends State { ? const CommonShimmerWidget().paddingSymmetrical(0.w, 0.h) : prescriptionVM.patientPrescriptionOrders.isNotEmpty ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 20.r, hasShadow: false), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: false), child: Padding( padding: EdgeInsets.all(16.w), child: Column( @@ -862,7 +816,7 @@ class _MedicalFilePageState extends State { separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ), SizedBox(height: 16.h), - const Divider(color: AppColors.dividerColor), + Divider(color: AppColors.dividerColor), SizedBox(height: 16.h), Row( children: [ @@ -892,7 +846,11 @@ class _MedicalFilePageState extends State { Expanded( child: CustomButton( text: LocaleKeys.allMedications.tr(context: context), - onPressed: () {}, + onPressed: () { Navigator.of(context).push( + CustomPageRoute( + page: ActiveMedicationPage(), + ), + );}, backgroundColor: AppColors.secondaryLightRedColor, borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, diff --git a/lib/presentation/medical_file/widgets/lab_rad_card.dart b/lib/presentation/medical_file/widgets/lab_rad_card.dart index f31c90b9..b107403d 100644 --- a/lib/presentation/medical_file/widgets/lab_rad_card.dart +++ b/lib/presentation/medical_file/widgets/lab_rad_card.dart @@ -36,6 +36,7 @@ class LabRadCard extends StatelessWidget { width: 40.w, height: 40.h, fit: BoxFit.cover, + applyThemeColor: false ).toShimmer2(isShow: false, radius: 12.r), SizedBox(width: 8.w), Flexible( diff --git a/lib/presentation/medical_file/widgets/medical_file_card.dart b/lib/presentation/medical_file/widgets/medical_file_card.dart index 77968c57..c2551d30 100644 --- a/lib/presentation/medical_file/widgets/medical_file_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_card.dart @@ -37,7 +37,7 @@ class MedicalFileCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Utils.buildSvgWithAssets(icon: svgIcon, width: iconS, height: iconS, fit: BoxFit.contain), + Utils.buildSvgWithAssets(icon: svgIcon, width: iconS, height: iconS, fit: BoxFit.contain, applyThemeColor: false), SizedBox(height: 8.h), isLargeText ? label.toText13(color: textColor, isBold: true, maxLine: 2) : label.toText11(color: textColor, isBold: true, maxLine: 2), ], diff --git a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart index 16c05d8b..a0c54e68 100644 --- a/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart +++ b/lib/presentation/medical_file/widgets/patient_sick_leave_card.dart @@ -182,15 +182,15 @@ class PatientSickLeaveCard extends StatelessWidget { } Color getStatusColor() { - Color statusColor = Colors.white; + Color statusColor = AppColors.whiteColor; if (patientSickLeavesResponseModel.status == 1) { - statusColor = Color(0xffCC9B14); // TODO change color as per In Queue design + statusColor = AppColors.statusPendingColor; } else if (patientSickLeavesResponseModel.status == 2) { statusColor = AppColors.successColor; } else if (patientSickLeavesResponseModel.status == 3) { statusColor = AppColors.primaryRedColor; } else { - statusColor = Colors.white; + statusColor = AppColors.whiteColor; } return statusColor; } diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index 87420fd5..fd3ba7f9 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -17,6 +17,7 @@ import 'package:hmg_patient_app_new/presentation/medical_report/widgets/patient_ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; @@ -251,46 +252,21 @@ class _MedicalReportsPageState extends State { ], ), SizedBox(height: 8.h), - Row( + Wrap( + alignment: WrapAlignment.start, + direction: Axis.horizontal, + spacing: 6.h, + runSpacing: 6.h, children: [ - CustomButton( - text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(report.appointmentDate), false), - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, + AppCustomChipWidget( + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(report.appointmentDate), false), ), - SizedBox(width: 8.h), - CustomButton( - text: medicalFileVM.isMedicalReportsSortByClinic ? report.projectName! : report.clinicDescription!, - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, - ), - SizedBox(width: 8.h), - CustomButton( - text: report.statusDesc!, - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, + AppCustomChipWidget( + labelText: medicalFileVM.isMedicalReportsSortByClinic ? report.projectName! : report.clinicDescription!, ), + report.statusDesc != null ? AppCustomChipWidget( + labelText: report.statusDesc ?? "test", + ) : SizedBox.shrink(), ], ), SizedBox(height: 8.h), @@ -394,13 +370,13 @@ class _MedicalReportsPageState extends State { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, fontSize: 16.f, fontWeight: FontWeight.w500, borderRadius: 12.r, height: 46.h, icon: AppAssets.requests, - iconColor: AppColors.whiteColor, + iconColor: Colors.white, iconSize: 20.h, ).paddingSymmetrical(24.h, 24.h), ), diff --git a/lib/presentation/monthly_report/monthly_report.dart b/lib/presentation/monthly_report/monthly_report.dart index 43c7fc1d..998d7f48 100644 --- a/lib/presentation/monthly_report/monthly_report.dart +++ b/lib/presentation/monthly_report/monthly_report.dart @@ -74,7 +74,7 @@ class MonthlyReport extends StatelessWidget { children: [ Row( children: [ - Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 8.h), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index 4a328c5e..6e0fa0e4 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -80,7 +80,7 @@ class InvoiceListCard extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h), + Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, applyThemeColor: false), SizedBox(height: 2.h), "${getInvoicesListResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), ], diff --git a/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart b/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart index 5a340346..d6a616ee 100644 --- a/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart +++ b/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart @@ -66,9 +66,7 @@ class PrescriptionDeliveryOrderSummaryPage extends StatelessWidget { Expanded( child: Padding( padding: EdgeInsets.all(8.h), - child: Center( - child: prescriptionsViewModel.prescriptionDetailsList[index].itemDescription!.trim().toText12(), - ), + child: prescriptionsViewModel.prescriptionDetailsList[index].itemDescription!.trim().toText14(weight: FontWeight.w500), ), ), ], diff --git a/lib/presentation/prescriptions/prescription_detail_page.dart b/lib/presentation/prescriptions/prescription_detail_page.dart index 5bb9cbd7..b9b5b789 100644 --- a/lib/presentation/prescriptions/prescription_detail_page.dart +++ b/lib/presentation/prescriptions/prescription_detail_page.dart @@ -240,7 +240,7 @@ class _PrescriptionDetailPageState extends State { backgroundColor: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? AppColors.successColor : AppColors.greyF7Color, borderColor: AppColors.successColor.withOpacity(0.01), textColor: - widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? AppColors.whiteColor : AppColors.textColor.withOpacity(0.35), + widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? Colors.white : AppColors.textColor.withOpacity(0.35), fontSize: 16, fontWeight: FontWeight.w500, borderRadius: 12, @@ -248,7 +248,7 @@ class _PrescriptionDetailPageState extends State { height: 50.h, icon: AppAssets.prescription_refill_icon, iconColor: - widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? AppColors.whiteColor : AppColors.textColor.withOpacity(0.35), + widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? Colors.white : AppColors.textColor.withOpacity(0.35), iconSize: 20.h, ).paddingSymmetrical(24.h, 24.h), ), diff --git a/lib/presentation/prescriptions/prescription_item_view.dart b/lib/presentation/prescriptions/prescription_item_view.dart index 03a343f7..38b96d37 100644 --- a/lib/presentation/prescriptions/prescription_item_view.dart +++ b/lib/presentation/prescriptions/prescription_item_view.dart @@ -84,76 +84,77 @@ class PrescriptionItemView extends StatelessWidget { SizedBox(height: 14.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), SizedBox(height: 14.h), - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.prescription_reminder_icon, width: 35.h, height: 35.h).toShimmer2(isShow: isLoading), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.setReminder.tr(context: context).toText13(isBold: true), - "Notify me before the consumption time".toText10(color: AppColors.textColorLight), - ], - ).toShimmer2(isShow: isLoading).expanded, - Switch( - activeColor: AppColors.successColor, - activeTrackColor: AppColors.successColor.withValues(alpha: .15), - value: isLoading ? false : prescriptionVM.prescriptionDetailsList[index].hasReminder!, - onChanged: (newValue) async { - CalenderUtilsNew calender = CalenderUtilsNew.instance; - - if (prescriptionVM.prescriptionDetailsList[index].hasReminder ?? false) { - LoaderBottomSheet.showLoader(loadingText: "Removing Reminders"); - bool resultValue = await calender.checkAndRemoveMultipleItems(id:prescriptionVM.prescriptionDetailsList[index].itemID.toString()); + Visibility( + visible: (!isLoading && prescriptionVM.prescriptionDetailsList[index].frequencyNumber != 0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.prescription_reminder_icon, width: 35.h, height: 35.h, applyThemeColor: false).toShimmer2(isShow: isLoading), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.setReminder.tr(context: context).toText13(isBold: true), + "Notify me before the consumption time".toText10(color: AppColors.textColorLight), + ], + ).toShimmer2(isShow: isLoading).expanded, + Switch( + activeColor: AppColors.successColor, + activeTrackColor: AppColors.successColor.withValues(alpha: .15), + value: isLoading ? false : prescriptionVM.prescriptionDetailsList[index].hasReminder!, + onChanged: (newValue) async { + CalenderUtilsNew calender = CalenderUtilsNew.instance; - prescriptionVM.setPrescriptionItemReminder(newValue, prescriptionVM.prescriptionDetailsList[index]); - LoaderBottomSheet.hideLoader(); - return; - } + if (prescriptionVM.prescriptionDetailsList[index].hasReminder ?? false) { + LoaderBottomSheet.showLoader(loadingText: "Removing Reminders"); + bool resultValue = await calender.checkAndRemoveMultipleItems(id: prescriptionVM.prescriptionDetailsList[index].itemID.toString()); - DateTime startDate = DateTime.now(); - DateTime endDate = DateTime(startDate.year, startDate.month, startDate.day + prescriptionVM.prescriptionDetailsList[index].days!.toInt()); - BottomSheetUtils().showReminderBottomSheet( - context, - endDate, - "", - prescriptionVM.prescriptionDetailsList[index].itemID.toString(), - "", - "", - title: "${prescriptionVM.prescriptionDetailsList[index].itemDescription} Prescription Reminder", - description: - "${prescriptionVM.prescriptionDetailsList[index].itemDescription} ${prescriptionVM.prescriptionDetailsList[index].frequency} ${prescriptionVM.prescriptionDetailsList[index].route} ", - onSuccess: () { + prescriptionVM.setPrescriptionItemReminder(newValue, prescriptionVM.prescriptionDetailsList[index]); + LoaderBottomSheet.hideLoader(); + return; + } - }, - isMultiAllowed: true, - onMultiDateSuccess: (int selectedIndex) async{ - bool isEventAdded = await calender.createMultipleEvents( + DateTime startDate = DateTime.now(); + DateTime endDate = DateTime(startDate.year, startDate.month, startDate.day + prescriptionVM.prescriptionDetailsList[index].days!.toInt()); + BottomSheetUtils().showReminderBottomSheet( + context, + endDate, + "", + prescriptionVM.prescriptionDetailsList[index].itemID.toString(), + "", + "", + title: "${prescriptionVM.prescriptionDetailsList[index].itemDescription} Prescription Reminder", + description: + "${prescriptionVM.prescriptionDetailsList[index].itemDescription} ${prescriptionVM.prescriptionDetailsList[index].frequency} ${prescriptionVM.prescriptionDetailsList[index].route} ", + onSuccess: () {}, + isMultiAllowed: true, + onMultiDateSuccess: (int selectedIndex) async { + bool isEventAdded = await calender.createMultipleEvents( reminderMinutes: selectedIndex, frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), - days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), + days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, route: prescriptionVM.prescriptionDetailsList[index].route!, - onFailure: (errorMessage)=> prescriptionVM.showError(errorMessage), - prescriptionNumber: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), - ); - prescriptionVM.setPrescriptionItemReminder(isEventAdded, prescriptionVM.prescriptionDetailsList[index]); - // setCalender(context, - // eventId: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), - // selectedMinutes: selectedIndex, - // frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), - // days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), - // orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, - // itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, - // route: prescriptionVM.prescriptionDetailsList[index].route!); - }, - ); - }, - ).toShimmer2(isShow: isLoading), - ], - ).paddingSymmetrical(16.h, 0.h), + onFailure: (errorMessage) => prescriptionVM.showError(errorMessage), + prescriptionNumber: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), + ); + prescriptionVM.setPrescriptionItemReminder(isEventAdded, prescriptionVM.prescriptionDetailsList[index]); + // setCalender(context, + // eventId: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), + // selectedMinutes: selectedIndex, + // frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), + // days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), + // orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, + // itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, + // route: prescriptionVM.prescriptionDetailsList[index].route!); + }, + ); + }, + ).toShimmer2(isShow: isLoading), + ], + ).paddingSymmetrical(16.h, 0.h), + ), SizedBox(height: 14.h), // Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), // Row( diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 82150ca7..2bd41f06 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -44,6 +45,9 @@ class ProfileSettings extends StatefulWidget { class ProfileSettingsState extends State { @override void initState() { + scheduleMicrotask(() { + insuranceViewModel.initInsuranceProvider(); + }); super.initState(); } @@ -81,9 +85,11 @@ class ProfileSettingsState extends State { int length = 3; final SwiperController _controller = SwiperController(); + late InsuranceViewModel insuranceViewModel; @override Widget build(BuildContext context) { + insuranceViewModel = Provider.of(context, listen: false); return CollapsingListView( title: LocaleKeys.profileAndSettings.tr(context: context), logout: () { @@ -152,7 +158,7 @@ class ProfileSettingsState extends State { spacing: 8.w, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h), + Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h, applyThemeColor: false), LocaleKeys.habibWallet.tr(context: context).toText16(weight: FontWeight.w600, maxlines: 2).expanded, Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), ], @@ -188,6 +194,38 @@ class ProfileSettingsState extends State { ], ), LocaleKeys.quickActions.tr(context: context).toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1).paddingOnly(left: 24.w, right: 24.w), + Container( + margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 24.h), + padding: EdgeInsets.only(top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + actionItem(AppAssets.language_change, LocaleKeys.language.tr(context: context), () { + showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.language.tr(context: context), child: AppLanguageChange(), callBackFunc: () {}, isFullScreen: false); + }, trailingLabel: Utils.appState.isArabic() ? "العربية" : "English"), + 1.divider, + actionItem(AppAssets.bell, LocaleKeys.notificationsSettings.tr(context: context), () { + openAppSettings(); + }), + 1.divider, + Consumer( + builder: (context, profileVm, _) { + return actionItem( + AppAssets.darkModeIcon, + LocaleKeys.darkMode.tr(context: context), + () { + profileVm.toggleDarkMode(!profileVm.isDarkMode); + }, + switchValue: profileVm.isDarkMode, + onSwitchChanged: (value) { + profileVm.toggleDarkMode(value); + }, + ); + }, + ), + ], + ), + ), Container( margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 24.h), padding: EdgeInsets.only(top: 4.h, bottom: 4.h), @@ -276,7 +314,7 @@ class ProfileSettingsState extends State { ); } - Widget actionItem(String icon, String label, VoidCallback onPress, {String trailingLabel = "", bool? switchValue, bool isExternalLink = false}) { + Widget actionItem(String icon, String label, VoidCallback onPress, {String trailingLabel = "", bool? switchValue, ValueChanged? onSwitchChanged, bool isExternalLink = false}) { return SizedBox( height: 56.h, child: Row( @@ -288,7 +326,7 @@ class ProfileSettingsState extends State { switchValue != null ? Switch( value: switchValue, - onChanged: (value) {}, + onChanged: onSwitchChanged ?? (value) {}, activeThumbColor: AppColors.successColor, activeTrackColor: AppColors.successColor.withValues(alpha: .15), ) @@ -457,8 +495,8 @@ class FamilyCardWidget extends StatelessWidget { onPressed: canSwitch ? () => onFamilySwitchPress(profile) : onAddFamilyMemberPress, backgroundColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, borderColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, - textColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor, - iconColor: canSwitch ? AppColors.primaryRedColor : AppColors.whiteColor, + textColor: canSwitch ? AppColors.primaryRedColor : Colors.white, + iconColor: canSwitch ? AppColors.primaryRedColor : Colors.white, height: isFoldable ? 50.h : 45.h, fontSize: 14.f, ).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h); diff --git a/lib/presentation/radiology/radiology_orders_page.dart b/lib/presentation/radiology/radiology_orders_page.dart index 2f424361..a07face1 100644 --- a/lib/presentation/radiology/radiology_orders_page.dart +++ b/lib/presentation/radiology/radiology_orders_page.dart @@ -86,19 +86,6 @@ class _RadiologyOrdersPageState extends State { child: Consumer( builder: (context, model, child) { // Build grouping lists if we have data and none constructed yet - if (!model.isRadiologyOrdersLoading && model.patientRadiologyOrders.isNotEmpty && model.patientRadiologyOrdersViewList.isEmpty) { - final clinicMap = >{}; - final hospitalMap = >{}; - for (var order in model.patientRadiologyOrders) { - final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); - clinicMap.putIfAbsent(clinicKey, () => []).add(order); - final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim(); - hospitalMap.putIfAbsent(hospitalKey, () => []).add(order); - } - model.patientRadiologyOrdersByClinic = clinicMap.values.toList(); - model.patientRadiologyOrdersByHospital = hospitalMap.values.toList(); - model.patientRadiologyOrdersViewList = model.isSortByClinic ? model.patientRadiologyOrdersByClinic : model.patientRadiologyOrdersByHospital; - } return Padding( padding: EdgeInsets.symmetric(horizontal: 24.h), @@ -140,13 +127,12 @@ class _RadiologyOrdersPageState extends State { ), ], ), - SizedBox(height: 8.h), selectedFilterText.isNotEmpty ? AppCustomChipWidget( padding: EdgeInsets.symmetric(horizontal: 5.h), labelText: selectedFilterText, - deleteIcon: 'assets/images/svg/cross_circle.svg', + deleteIcon: AppAssets.close_bottom_sheet_icon, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIconColor: AppColors.whiteColor, diff --git a/lib/presentation/radiology/radiology_result_page.dart b/lib/presentation/radiology/radiology_result_page.dart index 2328d0ea..7b3f11d4 100644 --- a/lib/presentation/radiology/radiology_result_page.dart +++ b/lib/presentation/radiology/radiology_result_page.dart @@ -85,14 +85,14 @@ class _RadiologyResultPageState extends State { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, icon: AppAssets.download, - iconColor: AppColors.whiteColor, + iconColor: Colors.white, iconSize: 20.h, ), SizedBox(height: 16.h), @@ -145,14 +145,14 @@ class _RadiologyResultPageState extends State { }, backgroundColor: AppColors.successColor, borderColor: AppColors.successColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 45.h, icon: AppAssets.download, - iconColor: AppColors.whiteColor, + iconColor: Colors.white, iconSize: 20.h, ).paddingSymmetrical(24.h, 24.h), ), diff --git a/lib/presentation/radiology/search_radiology.dart b/lib/presentation/radiology/search_radiology.dart index 8d63a59a..7c14fd1d 100644 --- a/lib/presentation/radiology/search_radiology.dart +++ b/lib/presentation/radiology/search_radiology.dart @@ -59,7 +59,7 @@ class _SearchRadiologyContentContentState extends State title: LocaleKeys.radiology.tr(), isClose: true, bottomChild: Container( - color: Colors.white, + color: AppColors.whiteColor, padding: EdgeInsets.all(ResponsiveExtension(20).h), child: CustomButton( text: LocaleKeys.search.tr(), @@ -144,7 +144,8 @@ class SuggestionChip extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: label.toText12( - color: isSelected ? Colors.white : Colors.black87, + // color: isSelected ? Colors.white : Colors.black87, + color: AppColors.textColor, fontWeight: FontWeight.w500, ), ), diff --git a/lib/presentation/smartwatches/huawei_health_example.dart b/lib/presentation/smartwatches/huawei_health_example.dart index 4163d8b0..bf569a6a 100644 --- a/lib/presentation/smartwatches/huawei_health_example.dart +++ b/lib/presentation/smartwatches/huawei_health_example.dart @@ -1,1563 +1,1563 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:huawei_health/huawei_health.dart'; - -const String packageName = 'com.ejada.hmg'; - -class HuaweiHealthExample extends StatefulWidget { - const HuaweiHealthExample({Key? key}) : super(key: key); - - @override - State createState() => _HuaweiHealthExampleState(); -} - -class _HuaweiHealthExampleState extends State { - /// Styles - static const TextStyle cardTitleTextStyle = TextStyle( - fontWeight: FontWeight.w500, - fontSize: 18, - ); - static const EdgeInsets componentPadding = EdgeInsets.all(8.0); - - /// Text Controllers for showing the logs of different modules - final TextEditingController _activityTextController = TextEditingController(); - final TextEditingController _dataTextController = TextEditingController(); - final TextEditingController _settingTextController = TextEditingController(); - final TextEditingController _autoRecorderTextController = TextEditingController(); - final TextEditingController _consentTextController = TextEditingController(); - final TextEditingController _healthTextController = TextEditingController(); - - /// Data controller reference to initialize at startup. - late DataController _dataController; - - String? accessToken = ''; - - @override - void initState() { - super.initState(); - if (!mounted) return; - // Initialize Event Callbacks - AutoRecorderController.autoRecorderStream.listen(_onAutoRecorderEvent); - // Initialize a DataController - initDataController(); - } - - /// Prints the specified text on both the console and the specified text controller. - void log( - String methodName, - TextEditingController controller, - LogOptions logOption, { - String? result = '', - String? error = '', - }) { - String log = ''; - switch (logOption) { - case LogOptions.call: - log = '$methodName called'; - break; - case LogOptions.success: - log = '$methodName [Success: $result] '; - break; - case LogOptions.error: - log = '$methodName [Error: $error] [Error Description: ${HiHealthStatusCodes.getStatusCodeMessage(error ?? '')}]'; - break; - case LogOptions.custom: - log = methodName; // Custom text - break; - } - debugPrint(log); - setState(() { - controller.text = '$log\n${controller.text}'; - }); - } - - /// Authorizes Huawei Health Kit for the user, with defined scopes. - void signIn() async { - // List of scopes to ask for authorization. - // - // Note: These scopes should also be authorized on the Huawei Developer Console. - final List scopes = [ - Scope.HEALTHKIT_ACTIVITY_READ, - Scope.HEALTHKIT_ACTIVITY_WRITE, - Scope.HEALTHKIT_BLOODGLUCOSE_READ, - Scope.HEALTHKIT_BLOODGLUCOSE_WRITE, - Scope.HEALTHKIT_CALORIES_READ, - Scope.HEALTHKIT_CALORIES_WRITE, - Scope.HEALTHKIT_DISTANCE_READ, - Scope.HEALTHKIT_DISTANCE_WRITE, - Scope.HEALTHKIT_HEARTRATE_READ, - Scope.HEALTHKIT_HEARTRATE_WRITE, - Scope.HEALTHKIT_HEIGHTWEIGHT_READ, - Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, - Scope.HEALTHKIT_LOCATION_READ, - Scope.HEALTHKIT_LOCATION_WRITE, - Scope.HEALTHKIT_PULMONARY_READ, - Scope.HEALTHKIT_PULMONARY_WRITE, - Scope.HEALTHKIT_SLEEP_READ, - Scope.HEALTHKIT_SLEEP_WRITE, - Scope.HEALTHKIT_SPEED_READ, - Scope.HEALTHKIT_SPEED_WRITE, - Scope.HEALTHKIT_STEP_READ, - Scope.HEALTHKIT_STEP_WRITE, - Scope.HEALTHKIT_STRENGTH_READ, - Scope.HEALTHKIT_STRENGTH_WRITE, - Scope.HEALTHKIT_BODYFAT_READ, - Scope.HEALTHKIT_BODYFAT_WRITE, - Scope.HEALTHKIT_NUTRITION_READ, - Scope.HEALTHKIT_NUTRITION_WRITE, - Scope.HEALTHKIT_BLOODPRESSURE_READ, - Scope.HEALTHKIT_BLOODPRESSURE_WRITE, - Scope.HEALTHKIT_BODYTEMPERATURE_READ, - Scope.HEALTHKIT_BODYTEMPERATURE_WRITE, - Scope.HEALTHKIT_OXYGENSTATURATION_READ, - Scope.HEALTHKIT_OXYGENSTATURATION_WRITE, - Scope.HEALTHKIT_REPRODUCTIVE_READ, - Scope.HEALTHKIT_REPRODUCTIVE_WRITE, - Scope.HEALTHKIT_ACTIVITY_RECORD_READ, - Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, - Scope.HEALTHKIT_HEARTRATE_REALTIME, - Scope.HEALTHKIT_STEP_REALTIME, - Scope.HEALTHKIT_HEARTHEALTH_WRITE, - Scope.HEALTHKIT_HEARTHEALTH_READ, - Scope.HEALTHKIT_STRESS_WRITE, - Scope.HEALTHKIT_STRESS_READ, - Scope.HEALTHKIT_OXYGEN_SATURATION_WRITE, - Scope.HEALTHKIT_OXYGEN_SATURATION_READ, - Scope.HEALTHKIT_HISTORYDATA_OPEN_WEEK, - Scope.HEALTHKIT_HISTORYDATA_OPEN_MONTH, - Scope.HEALTHKIT_HISTORYDATA_OPEN_YEAR, - ]; - try { - AuthHuaweiId? result = await HealthAuth.signIn(scopes); - debugPrint( - 'Granted Scopes for User(${result?.displayName}): ${result?.grantedScopes?.toString()}', - ); - showSnackBar( - 'Authorization Success.', - color: Colors.green, - ); - setState(() => accessToken = result?.accessToken); - } on PlatformException catch (e) { - debugPrint('Error on authorization, Error:${e.toString()}'); - showSnackBar( - 'Error on authorization, Error:${e.toString()}, Error Description: ' - '${HiHealthStatusCodes.getStatusCodeMessage(e.message ?? '')}', - ); - } - } - - // ActivityRecordsController - // - /// Adds an ActivityRecord with an ActivitySummary, time range is 2 hours from now. - Future addActivityRecord() async { - log( - 'addActivityRecord', - _activityTextController, - LogOptions.call, - ); - DateTime startTime = DateTime.now().subtract(const Duration(hours: 2)); - DateTime endTime = DateTime.now(); - // Build an ActivityRecord object - ActivityRecord activityRecord = ActivityRecord( - startTime: startTime, - endTime: endTime, - id: 'ActivityRecordId0', - name: 'AddActivityRecord', - activityTypeId: HiHealthActivities.running, - description: 'This is a test for ActivityRecord', - activitySummary: ActivitySummary( - paceSummary: PaceSummary( - avgPace: 247.27626, - bestPace: 212.0, - britishPaceMap: { - '102802480': 365.0, - }, - britishPartTimeMap: { - '1.0': 263.0, - }, - partTimeMap: { - '1.0': 456.0, - }, - paceMap: { - '1.0': 263.0, - }, - ), - dataSummary: [ - SamplePoint( - dataType: DataType.DT_CONTINUOUS_DISTANCE_TOTAL, - startTime: startTime.add(Duration(seconds: 1)), - endTime: endTime.subtract(Duration(seconds: 1)), - fieldValueOptions: FieldFloat(Field.FIELD_DISTANCE, 400), - timeUnit: TimeUnit.MILLISECONDS, - ), - SamplePoint( - dataType: DataType.POLYMERIZE_CONTINUOUS_SPEED_STATISTICS, - fieldValueOptions: FieldFloat(Field.FIELD_AVG, 60.0), - startTime: startTime.add(Duration(seconds: 1)), - endTime: endTime.subtract(Duration(seconds: 1)), - timeUnit: TimeUnit.MILLISECONDS, - ) - ..setFieldValue(Field.FIELD_MIN, 40.0) - ..setFieldValue(Field.FIELD_MAX, 80.0), - ]), - ); - - // Build the dataCollector object - DataCollector dataCollector = DataCollector( - dataGenerateType: DataGenerateType.DATA_TYPE_RAW, - dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, - name: 'AddActivityRecord1923', - ); - - // You can use sampleSets to add more sample points to the sampling dataset. - // Build a list of sampling point objects and add it to the sampling dataSet - List samplePoints = [ - SamplePoint( - dataCollector: dataCollector, - startTime: startTime.add(Duration(seconds: 1)), - endTime: endTime.subtract(Duration(seconds: 1)), - fieldValueOptions: FieldFloat(Field.FIELD_STEP_RATE, 10.0), - timeUnit: TimeUnit.MILLISECONDS, - ), - ]; - SampleSet sampleSet = SampleSet( - dataCollector, - samplePoints, - ); - - try { - await ActivityRecordsController.addActivityRecord( - ActivityRecordInsertOptions( - activityRecord: activityRecord, - sampleSets: [ - sampleSet, - ], - ), - ); - log( - 'addActivityRecord', - _activityTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'addActivityRecord', - _activityTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Obtains saved ActivityRecords between yesterday and now, - /// with the DT_CONTINUOUS_STEPS_DELTA data type - void getActivityRecord() async { - log( - 'getActivityRecord', - _activityTextController, - LogOptions.call, - ); - // Create start time that will be used to read activity record. - DateTime startTime = DateTime.now().subtract(const Duration(days: 1)); - - // Create end time that will be used to read activity record. - DateTime endTime = DateTime.now().add(const Duration(hours: 3)); - - ActivityRecordReadOptions activityRecordReadOptions = ActivityRecordReadOptions( - activityRecordId: "ActivityRecordId0", - activityRecordName: null, - startTime: startTime, - endTime: endTime, - timeUnit: TimeUnit.MILLISECONDS, - dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, - ); - try { - List result = await ActivityRecordsController.getActivityRecord( - activityRecordReadOptions, - ); - log( - 'getActivityRecord', - _activityTextController, - LogOptions.success, - result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', - ); - } on PlatformException catch (e) { - log( - 'getActivityRecord', - _activityTextController, - LogOptions.error, - result: e.message, - ); - } - } - - /// Starts the ActivityRecord with the id:`ActivityRecordRun1` - void beginActivityRecord() async { - try { - log( - 'beginActivityRecord', - _activityTextController, - LogOptions.call, - ); - // Build an ActivityRecord object - ActivityRecord activityRecord = ActivityRecord( - id: 'ActivityRecordRun0', - name: 'BeginActivityRecord', - description: 'This is ActivityRecord begin test!', - activityTypeId: HiHealthActivities.running, - startTime: DateTime.now().subtract(const Duration(hours: 1)), - ); - await ActivityRecordsController.beginActivityRecord( - activityRecord, - ); - log( - 'beginActivityRecord', - _activityTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'beginActivityRecord', - _activityTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Stops the ActivityRecord with the id:`ActivityRecordRun1` - void endActivityRecord() async { - try { - log( - 'endActivityRecord', - _activityTextController, - LogOptions.call, - ); - final List result = await ActivityRecordsController.endActivityRecord( - 'ActivityRecordRun0', - ); - // Return the list of activity records that have stopped - log( - 'endActivityRecord', - _activityTextController, - LogOptions.success, - result: result.toString(), - ); - } on PlatformException catch (e) { - log( - 'endActivityRecord', - _activityTextController, - LogOptions.error, - result: e.message, - ); - } - } - - /// Ends all the ongoing activity records. - /// - /// Result list will be null if there is no ongoing activity record. - void endAllActivityRecords() async { - try { - log( - 'endAllActivityRecords', - _activityTextController, - LogOptions.call, - ); - // Return the list of activity records that have stopped - List result = await ActivityRecordsController.endAllActivityRecords(); - log( - 'endAllActivityRecords', - _activityTextController, - LogOptions.success, - result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', - ); - } on PlatformException catch (e) { - log( - 'endAllActivityRecords', - _activityTextController, - LogOptions.error, - result: e.message, - ); - } - } - - // - // - // End of ActivityRecordsController Methods - - // DataController Methods - // - // - /// Initializes a DataController instance with a list of HiHealtOptions. - void initDataController() async { - if (!mounted) return; - log( - 'init', - _dataTextController, - LogOptions.call, - ); - try { - _dataController = await DataController.init(); - log( - 'init', - _dataTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'init', - _dataTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Clears all the data inserted by the app. - void clearAll() async { - log('clearAll', _dataTextController, LogOptions.call); - try { - await _dataController.clearAll(); - log('clearAll', _dataTextController, LogOptions.success); - } on PlatformException catch (e) { - log('clearAll', _dataTextController, LogOptions.error, error: e.message); - } - } - - /// Deletes DT_CONTINUOUS_STEPS_DELTA type data by the specified time range. - void delete() async { - log( - 'delete', - _dataTextController, - LogOptions.call, - ); - // Build the dataCollector object - DataCollector dataCollector = DataCollector( - dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, - dataGenerateType: DataGenerateType.DATA_TYPE_RAW, - dataStreamName: 'STEPS_DELTA', - ); - - // Build the time range for the deletion: start time and end time. - DeleteOptions deleteOptions = DeleteOptions( - dataCollectors: [dataCollector], - startTime: DateTime.parse('2020-10-10 08:00:00'), - endTime: DateTime.parse('2020-10-10 12:30:00'), - ); - - // Call the api with the constructed DeleteOptions instance. - try { - _dataController.delete(deleteOptions); - log( - 'delete', - _dataTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'delete', - _dataTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Inserts a sampling set with the DT_CONTINUOUS_STEPS_DELTA data type at the - /// specified start and end dates. - void insert() async { - log( - 'insert', - _dataTextController, - LogOptions.call, - ); - // Build the dataCollector object - DataCollector dataCollector = DataCollector( - dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, - dataStreamName: 'STEPS_DELTA', - dataGenerateType: DataGenerateType.DATA_TYPE_RAW, - ); - // You can use sampleSets to add more sampling points to the sampling dataset. - SampleSet sampleSet = SampleSet( - dataCollector, - [ - SamplePoint( - dataCollector: dataCollector, - startTime: DateTime.parse('2020-10-10 12:00:00'), - endTime: DateTime.parse('2020-10-10 12:12:00'), - fieldValueOptions: FieldInt( - Field.FIELD_STEPS_DELTA, - 100, - ), - ), - ], - ); - // Call the api with the constructed sample set. - try { - _dataController.insert(sampleSet); - log( - 'insert', - _dataTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'insert', - _dataTextController, - LogOptions.error, - error: e.message, - ); - } - } - - // Reads the user data between the specified start and end dates. - void read() async { - log( - 'read', - _dataTextController, - LogOptions.call, - ); - // Build the dataCollector object - DataCollector dataCollector = DataCollector( - dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, - dataGenerateType: DataGenerateType.DATA_TYPE_RAW, - dataStreamName: 'STEPS_DELTA', - ); - - // Build the time range for the query: start time and end time. - ReadOptions readOptions = ReadOptions( - dataCollectors: [ - dataCollector, - ], - startTime: DateTime.parse('2020-10-10 12:00:00'), - endTime: DateTime.parse('2020-10-10 12:12:00'), - )..groupByTime(10000); - - // Call the api with the constructed ReadOptions instance. - try { - ReadReply? readReply = await _dataController.read(readOptions); - log( - 'read', - _dataTextController, - LogOptions.success, - result: readReply.toString(), - ); - } on PlatformException catch (e) { - log( - 'read', - _dataTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Reads the daily summation between the dates: `2020.10.02` to `2020.12.15` for multiple data types. - /// Note that the time format is different for this method. - void readDailySummationList() async { - log( - 'readDailySummationList', - _dataTextController, - LogOptions.call, - ); - try { - List? sampleSets = await _dataController.readDailySummationList( - [DataType.DT_CONTINUOUS_STEPS_DELTA, DataType.DT_CONTINUOUS_CALORIES_BURNT], - 20201002, - 20201003, - ); - log( - 'readDailySummationList', - _dataTextController, - LogOptions.success, - result: sampleSets.toString(), - ); - } on PlatformException catch (e) { - log( - 'readDailySummationList', - _dataTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Reads the steps summation for today. - void readTodaySummation() async { - log( - 'readTodaySummation', - _dataTextController, - LogOptions.call, - ); - try { - SampleSet? sampleSet = await _dataController.readTodaySummation( - DataType.DT_CONTINUOUS_STEPS_DELTA, - ); - log( - 'readTodaySummation', - _dataTextController, - LogOptions.success, - result: sampleSet.toString(), - ); - } on PlatformException catch (e) { - log( - 'readTodaySummation', - _dataTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Updates DT_CONTINUOUS_STEPS_DELTA for the specified dates. - void update() async { - log( - 'update', - _dataTextController, - LogOptions.call, - ); - - // Build the dataCollector object - DataCollector dataCollector = DataCollector( - dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, - dataStreamName: 'STEPS_DELTA', - dataGenerateType: DataGenerateType.DATA_TYPE_RAW, - ); - - // You can use sampleSets to add more sampling points to the sampling dataset. - SampleSet sampleSet = SampleSet( - dataCollector, - [ - SamplePoint( - dataCollector: dataCollector, - startTime: DateTime.parse('2020-12-12 09:00:00'), - endTime: DateTime.parse('2020-12-12 09:05:00'), - fieldValueOptions: FieldInt( - Field.FIELD_STEPS_DELTA, - 120, - ), - ), - ], - ); - - // Build a parameter object for the update. - // Note: (1) The start time of the modified object updateOptions can not be greater than the minimum - // value of the start time of all sample data points in the modified data sample set - // (2) The end time of the modified object updateOptions can not be less than the maximum value of the - // end time of all sample data points in the modified data sample set - UpdateOptions updateOptions = UpdateOptions( - startTime: DateTime.parse('2020-12-12 08:00:00'), - endTime: DateTime.parse('2020-12-12 09:25:00'), - sampleSet: sampleSet, - ); - try { - await _dataController.update(updateOptions); - log( - 'update', - _dataTextController, - LogOptions.success, - result: sampleSet.toString(), - ); - } on PlatformException catch (e) { - log( - 'update', - _dataTextController, - LogOptions.error, - error: e.message, - ); - } - } - - // - // - // End of DataController Methods - - // SettingController Methods - // - /// Adds a custom DataType with the FIELD_ALTITUDE. - void addDataType() async { - log( - 'addDataType', - _settingTextController, - LogOptions.call, - ); - try { - // The name of the created data type must be prefixed with the package name - // of the app. Otherwise, the creation fails. If the same data type is tried to - // be added again an exception will be thrown. - DataTypeAddOptions options = DataTypeAddOptions( - '$packageName.myCustomDataType', - [ - const Field.newIntField('myIntField'), - Field.FIELD_ALTITUDE, - ], - ); - final DataType dataTypeResult = await SettingController.addDataType( - options, - ); - log( - 'addDataType', - _settingTextController, - LogOptions.success, - result: dataTypeResult.toString(), - ); - } on PlatformException catch (e) { - log( - 'addDataType', - _settingTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Reads the inserted data type on the [addDataType] method. - void readDataType() async { - log( - 'readDataType', - _settingTextController, - LogOptions.call, - ); - try { - final DataType dataTypeResult = await SettingController.readDataType( - '$packageName.myCustomDataType', - ); - log( - 'readDataType', - _settingTextController, - LogOptions.success, - result: dataTypeResult.toString(), - ); - } on PlatformException catch (e) { - log( - 'readDataType', - _settingTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Disables the Health Kit function, cancels user authorization, and cancels - /// all data records. (The task takes effect in 24 hours.) - void disableHiHealth() async { - log( - 'disableHiHealth', - _settingTextController, - LogOptions.call, - ); - try { - await SettingController.disableHiHealth(); - log( - 'disableHiHealth', - _settingTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'disableHiHealth', - _settingTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Checks the user privacy authorization to Health Kit. Redirects the user to - /// the Authorization screen if the permissions are not given. - void checkHealthAppAuthorization() async { - log( - 'checkHealthAppAuthorization', - _settingTextController, - LogOptions.call, - ); - try { - await SettingController.checkHealthAppAuthorization(); - log( - 'checkHealthAppAuthorization', - _settingTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'checkHealthAppAuthorization', - _settingTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Checks the user privacy authorization to Health Kit. If authorized `true` - /// value would be returned. - void getHealthAppAuthorization() async { - log( - 'getHealthAppAuthorization', - _settingTextController, - LogOptions.call, - ); - try { - final bool result = await SettingController.getHealthAppAuthorization(); - log( - 'getHealthAppAuthorization', - _settingTextController, - LogOptions.success, - result: result.toString(), - ); - } on PlatformException catch (e) { - log( - 'getHealthAppAuthorization', - _settingTextController, - LogOptions.error, - error: e.message, - ); - } - } - - void requestAuth() async { - final HealthKitAuthResult res = await SettingController.requestAuthorizationIntent( - [ - Scope.HEALTHKIT_STEP_READ, - Scope.HEALTHKIT_STEP_WRITE, - Scope.HEALTHKIT_HEIGHTWEIGHT_READ, - Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, - Scope.HEALTHKIT_HEARTRATE_READ, - Scope.HEALTHKIT_HEARTRATE_WRITE, - Scope.HEALTHKIT_ACTIVITY_RECORD_READ, - Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, - Scope.HEALTHKIT_HEARTHEALTH_READ, - Scope.HEALTHKIT_HEARTHEALTH_WRITE, - ], - true, - ); - debugPrint(res.authAccount?.accessToken); - } - - // - // - // End of SettingController Methods - - // AutoRecorderController Methods - // - // - // Callback function for AutoRecorderStream event. - void _onAutoRecorderEvent(SamplePoint? res) { - log( - '[AutoRecorderEvent] obtained, SamplePoint Field Value is ${res?.fieldValues?.toString()}', - _autoRecorderTextController, - LogOptions.custom, - ); - } - - /// Starts an Android Foreground Service to count the steps of the user. - /// The steps will be emitted to the AutoRecorderStream. - void startRecord() async { - log( - 'startRecord', - _autoRecorderTextController, - LogOptions.call, - ); - try { - await AutoRecorderController.startRecord( - DataType.DT_CONTINUOUS_STEPS_TOTAL, - NotificationProperties( - title: 'HMS Flutter Health Demo', - text: 'Counting steps', - subText: 'this is a subtext', - ticker: 'this is a ticker', - showChronometer: true, - ), - ); - log( - 'startRecord', - _autoRecorderTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'startRecord', - _autoRecorderTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Ends the Foreground service and stops the step count events. - void stopRecord() async { - log( - 'endRecord', - _autoRecorderTextController, - LogOptions.call, - ); - try { - await AutoRecorderController.stopRecord( - DataType.DT_CONTINUOUS_STEPS_TOTAL, - ); - log( - 'endRecord', - _autoRecorderTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'endRecord', - _autoRecorderTextController, - LogOptions.error, - error: e.message, - ); - } - } - - // - // - // End of AutoRecorderController Methods - - // ConsentController Methods - // - /// Obtains the application id from the agconnect-services.json file. - void getAppId() async { - log( - 'getAppId', - _consentTextController, - LogOptions.call, - ); - try { - final String appId = await ConsentsController.getAppId(); - log( - 'getAppId', - _consentTextController, - LogOptions.success, - result: appId, - ); - } on PlatformException catch (e) { - log( - 'getAppId', - _consentTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Gets the granted permission scopes for the app. - void getScopes() async { - log( - 'getScopes', - _consentTextController, - LogOptions.call, - ); - try { - final String appId = await ConsentsController.getAppId(); - final ScopeLangItem scopeLangItem = await ConsentsController.getScopes( - 'en-gb', - appId, - ); - log( - 'getScopes', - _consentTextController, - LogOptions.success, - result: scopeLangItem.toString(), - ); - } on PlatformException catch (e) { - log( - 'getScopes', - _consentTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Revokes all the permissions that authorized for this app. - void revoke() async { - log( - 'revoke', - _consentTextController, - LogOptions.call, - ); - try { - final String appId = await ConsentsController.getAppId(); - await ConsentsController.revoke(appId); - log( - 'revoke', - _consentTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'revoke', - _consentTextController, - LogOptions.error, - error: e.message, - ); - } - } - - /// Revokes the distance read/write permissions for the app. - void revokeWithScopes() async { - log( - 'revokeWithScopes', - _consentTextController, - LogOptions.call, - ); - try { - // Obtain the application id. - final String appId = await ConsentsController.getAppId(); - // Call the revokeWithScopes method with desired scopes. - await ConsentsController.revokeWithScopes( - appId, - [ - Scope.HEALTHKIT_DISTANCE_WRITE, - Scope.HEALTHKIT_DISTANCE_READ, - ], - ); - log( - 'revokeWithScopes', - _consentTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'revokeWithScopes', - _consentTextController, - LogOptions.error, - error: e.message, - ); - } - } - - // - // - // End of ConsentController Methods - - // HealthController Methods - // - void addHealthRecord() async { - log( - 'addHealthRecord', - _healthTextController, - LogOptions.call, - ); - try { - final DateTime startTime = DateTime(2023, 5, 11); - final DateTime endTime = DateTime(2023, 5, 13); - - DataCollector contDataCollector = DataCollector( - dataStreamName: 'contDataCollector', - packageName: packageName, - dataType: DataType.POLYMERIZE_CONTINUOUS_HEART_RATE_STATISTICS, - dataGenerateType: DataGenerateType.DATA_TYPE_RAW, - ); - - DataCollector instDataCollector = DataCollector( - dataStreamName: 'instDataCollector', - packageName: packageName, - dataType: DataType.DT_INSTANTANEOUS_HEART_RATE, - dataGenerateType: DataGenerateType.DATA_TYPE_RAW, - ); - - List subDataDetails = [ - SampleSet(instDataCollector, [ - SamplePoint( - dataCollector: instDataCollector, - ) - ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) - ..setFieldValue(Field.FIELD_BPM, 88.0) - ]) - ]; - - List subDataSummary = [ - SamplePoint( - dataCollector: contDataCollector, - ) - ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) - ..setFieldValue(Field.FIELD_AVG, 90.0) - ..setFieldValue(Field.FIELD_MAX, 100.0) - ..setFieldValue(Field.FIELD_MIN, 80.0) - ..setFieldValue(Field.LAST, 85.0) - ]; - - final HealthRecord healthRecord = HealthRecord( - startTime: startTime, - endTime: endTime, - metadata: 'Data', - dataCollector: DataCollector( - dataStreamName: 'such as step count', - packageName: packageName, - dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, - dataGenerateType: DataGenerateType.DATA_TYPE_RAW, - ), - ) - ..setSubDataSummary(subDataSummary) - ..setSubDataDetails(subDataDetails) - ..setFieldValue(HealthFields.FIELD_THRESHOLD, 42.0) - ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 48.0) - ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 42.0) - ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.0); - - final String? result = await HealthRecordController.addHealthRecord( - HealthRecordInsertOptions( - healthRecord: healthRecord, - ), - ); - log( - 'addHealthRecord', - _healthTextController, - LogOptions.success, - result: result.toString(), - ); - } on PlatformException catch (e) { - log( - 'addHealthRecord', - _healthTextController, - LogOptions.error, - error: e.message, - ); - } - } - - void getHealthRecord() async { - log( - 'getHealthRecord', - _healthTextController, - LogOptions.call, - ); - try { - final DateTime startTime = DateTime(2023, 5, 11); - final DateTime endTime = DateTime(2023, 5, 13); - - HealthRecordReply result = await HealthRecordController.getHealthRecord( - HealthRecordReadOptions( - packageName: packageName, - ) - ..setSubDataTypeList( - [ - DataType.DT_INSTANTANEOUS_HEART_RATE, - ], - ) - ..setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS, - ) - ..readByDataType( - HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, - ) - ..readHealthRecordsFromAllApps(), - ); - log( - 'getHealthRecord', - _healthTextController, - LogOptions.success, - result: result.healthRecords[0].toJson(), - ); - } on PlatformException catch (e) { - log( - 'getHealthRecord', - _healthTextController, - LogOptions.error, - error: e.message, - ); - } - } - - void updateHealthRecord() async { - log( - 'updateHealthRecord', - _healthTextController, - LogOptions.call, - ); - try { - final DateTime startTime = DateTime(2022, 10, 11); - final DateTime endTime = DateTime(2022, 10, 12); - final HealthRecord healthRecord = HealthRecord( - startTime: startTime, - endTime: endTime, - metadata: 'Data', - dataCollector: DataCollector( - dataStreamName: 'such as step count', - packageName: packageName, - dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, - dataGenerateType: DataGenerateType.DATA_TYPE_RAW, - ), - ) - ..setFieldValue(HealthFields.FIELD_THRESHOLD, 41.9) - ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 49.1) - ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 41.1) - ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.1); - await HealthRecordController.updateHealthRecord( - HealthRecordUpdateOptions( - healthRecord: healthRecord, - healthRecordId: '', - ), - ); - log( - 'updateHealthRecord', - _healthTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'updateHealthRecord', - _healthTextController, - LogOptions.error, - error: e.message, - ); - } - } - - void deleteHealthRecord() async { - log( - 'deleteHealthRecord', - _healthTextController, - LogOptions.call, - ); - try { - await HealthRecordController.deleteHealthRecord( - HealthRecordDeleteOptions( - startTime: DateTime.now().subtract(const Duration(days: 14)), - endTime: DateTime.now(), - )..setHealthRecordIds( - [ - '', - ], - ), - ); - log( - 'deleteHealthRecord', - _healthTextController, - LogOptions.success, - ); - } on PlatformException catch (e) { - log( - 'deleteHealthRecord', - _healthTextController, - LogOptions.error, - error: e.message, - ); - } - } - - // - // - // End of HealthController Methods - - // App's widgets. - // - // - Widget expansionCard({ - required String titleText, - required List children, - }) { - return Card( - margin: componentPadding, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - child: ExpansionTile( - title: Text( - titleText, - style: cardTitleTextStyle, - ), - children: children, - ), - ); - } - - Widget loggingArea( - TextEditingController moduleTextController, - ) { - return Column( - children: [ - Container( - margin: componentPadding, - padding: const EdgeInsets.all(8.0), - height: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5.0), - border: Border.all(color: Colors.black12), - ), - child: TextField( - readOnly: true, - maxLines: 15, - controller: moduleTextController, - decoration: const InputDecoration( - enabledBorder: InputBorder.none, - ), - ), - ), - TextButton( - child: const Text('Clear Log'), - onPressed: () => setState(() { - moduleTextController.text = ''; - }), - ) - ], - ); - } - - void showSnackBar( - String text, { - Color color = Colors.blue, - }) { - final SnackBar snackBar = SnackBar( - content: Text(text), - backgroundColor: color, - action: SnackBarAction( - label: 'Close', - textColor: Colors.white, - onPressed: () { - ScaffoldMessenger.of(context).removeCurrentSnackBar(); - }, - ), - ); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.white, - title: const Text( - 'Huawei Health Kit', - style: TextStyle( - color: Colors.blue, - fontWeight: FontWeight.bold, - ), - ), - centerTitle: true, - elevation: 0.0, - actions: [ - IconButton( - onPressed: requestAuth, - icon: const Icon(Icons.ac_unit), - ), - ], - ), - body: Builder( - builder: (BuildContext context) { - return ListView( - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - children: [ - // Sign In Widgets - Card( - margin: componentPadding, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Padding( - padding: componentPadding, - child: Text( - 'Tap to SignIn button to obtain the HMS Account to complete ' - 'login and authorization, and then use other buttons ' - 'to try the related API functions.', - textAlign: TextAlign.center, - ), - ), - const Padding( - padding: componentPadding, - child: Text( - 'Note: If the login page is not displayed, change the package ' - 'name, AppID, and configure the signature file by referring ' - 'to the developer guide on the official website.', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.blue, - ), - ), - ), - Container( - padding: componentPadding, - width: double.infinity, - child: OutlinedButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - Colors.blue, - ), - ), - child: const Text( - 'SignIn', - style: TextStyle( - color: Colors.white, - ), - ), - onPressed: () => signIn(), - ), - ), - ], - ), - ), - - // ActivityRecordsController - expansionCard( - titleText: 'ActivityRecords Controller', - children: [ - loggingArea(_activityTextController), - ListTile( - title: const Text('AddActivityRecord'), - onTap: () => addActivityRecord(), - ), - ListTile( - title: const Text('GetActivityRecord'), - onTap: () => getActivityRecord(), - ), - ListTile( - title: const Text('beginActivityRecord'), - onTap: () => beginActivityRecord(), - ), - ListTile( - title: const Text('endActivityRecord'), - onTap: () => endActivityRecord(), - ), - ListTile( - title: const Text('endAllActivityRecords'), - onTap: () => endAllActivityRecords(), - ), - ], - ), - // DataController Widgets - expansionCard( - titleText: 'DataController', - children: [ - loggingArea(_dataTextController), - ListTile( - title: const Text('readTodaySummation'), - onTap: () => readTodaySummation(), - ), - ListTile( - title: const Text('readDailySummationList'), - onTap: () => readDailySummationList(), - ), - ListTile( - title: const Text('insert'), - onTap: () => insert(), - ), - ListTile( - title: const Text('read'), - onTap: () => read(), - ), - ListTile( - title: const Text('update'), - onTap: () => update(), - ), - ListTile( - title: const Text('delete'), - onTap: () => delete(), - ), - ListTile( - title: const Text('clearAll'), - onTap: () => clearAll(), - ), - ], - ), - // SettingController Widgets. - expansionCard( - titleText: 'SettingController', - children: [ - loggingArea(_settingTextController), - ListTile( - title: const Text('addDataType'), - onTap: () => addDataType(), - ), - ListTile( - title: const Text('readDataType'), - onTap: () => readDataType(), - ), - ListTile( - title: const Text('disableHiHealth'), - onTap: () => disableHiHealth(), - ), - ListTile( - title: const Text('checkHealthAppAuthorization'), - onTap: () => checkHealthAppAuthorization(), - ), - ListTile( - title: const Text('getHealthAppAuthorization'), - onTap: () => getHealthAppAuthorization(), - ), - ], - ), - // AutoRecorderController Widgets - expansionCard( - titleText: 'AutoRecorderController', - children: [ - loggingArea(_autoRecorderTextController), - ListTile( - title: const Text('startRecord'), - onTap: () => startRecord(), - ), - ListTile( - title: const Text('stopRecord'), - onTap: () => stopRecord(), - ), - ], - ), - // Consent Controller Widgets - expansionCard( - titleText: 'ConsentController', - children: [ - loggingArea(_consentTextController), - ListTile( - title: const Text('getAppId'), - onTap: () => getAppId(), - ), - ListTile( - title: const Text('getScopes'), - onTap: () => getScopes(), - ), - ListTile( - title: const Text('revoke'), - onTap: () => revoke(), - ), - ListTile( - title: const Text('revokeWithScopes'), - onTap: () => revokeWithScopes(), - ), - ], - ), - - // Health Controller Widgets - expansionCard( - titleText: 'HealthController', - children: [ - loggingArea(_healthTextController), - ListTile( - title: const Text('addHealthRecord'), - onTap: () => addHealthRecord(), - ), - ListTile( - title: const Text('getHealthRecord'), - onTap: () => getHealthRecord(), - ), - ListTile( - title: const Text('updateHealthRecord'), - onTap: () => updateHealthRecord(), - ), - ListTile( - title: const Text('deleteHealthRecord'), - onTap: () => deleteHealthRecord(), - ), - ], - ), - ], - ); - }, - ), - ); - } -} - -/// Options for logging. -enum LogOptions { - call, - success, - error, - custom, -} +// import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +// import 'package:huawei_health/huawei_health.dart'; +// +// const String packageName = 'com.ejada.hmg'; +// +// class HuaweiHealthExample extends StatefulWidget { +// const HuaweiHealthExample({Key? key}) : super(key: key); +// +// @override +// State createState() => _HuaweiHealthExampleState(); +// } +// +// class _HuaweiHealthExampleState extends State { +// /// Styles +// static const TextStyle cardTitleTextStyle = TextStyle( +// fontWeight: FontWeight.w500, +// fontSize: 18, +// ); +// static const EdgeInsets componentPadding = EdgeInsets.all(8.0); +// +// /// Text Controllers for showing the logs of different modules +// final TextEditingController _activityTextController = TextEditingController(); +// final TextEditingController _dataTextController = TextEditingController(); +// final TextEditingController _settingTextController = TextEditingController(); +// final TextEditingController _autoRecorderTextController = TextEditingController(); +// final TextEditingController _consentTextController = TextEditingController(); +// final TextEditingController _healthTextController = TextEditingController(); +// +// /// Data controller reference to initialize at startup. +// late DataController _dataController; +// +// String? accessToken = ''; +// +// @override +// void initState() { +// super.initState(); +// if (!mounted) return; +// // Initialize Event Callbacks +// AutoRecorderController.autoRecorderStream.listen(_onAutoRecorderEvent); +// // Initialize a DataController +// initDataController(); +// } +// +// /// Prints the specified text on both the console and the specified text controller. +// void log( +// String methodName, +// TextEditingController controller, +// LogOptions logOption, { +// String? result = '', +// String? error = '', +// }) { +// String log = ''; +// switch (logOption) { +// case LogOptions.call: +// log = '$methodName called'; +// break; +// case LogOptions.success: +// log = '$methodName [Success: $result] '; +// break; +// case LogOptions.error: +// log = '$methodName [Error: $error] [Error Description: ${HiHealthStatusCodes.getStatusCodeMessage(error ?? '')}]'; +// break; +// case LogOptions.custom: +// log = methodName; // Custom text +// break; +// } +// debugPrint(log); +// setState(() { +// controller.text = '$log\n${controller.text}'; +// }); +// } +// +// /// Authorizes Huawei Health Kit for the user, with defined scopes. +// void signIn() async { +// // List of scopes to ask for authorization. +// // +// // Note: These scopes should also be authorized on the Huawei Developer Console. +// final List scopes = [ +// Scope.HEALTHKIT_ACTIVITY_READ, +// Scope.HEALTHKIT_ACTIVITY_WRITE, +// Scope.HEALTHKIT_BLOODGLUCOSE_READ, +// Scope.HEALTHKIT_BLOODGLUCOSE_WRITE, +// Scope.HEALTHKIT_CALORIES_READ, +// Scope.HEALTHKIT_CALORIES_WRITE, +// Scope.HEALTHKIT_DISTANCE_READ, +// Scope.HEALTHKIT_DISTANCE_WRITE, +// Scope.HEALTHKIT_HEARTRATE_READ, +// Scope.HEALTHKIT_HEARTRATE_WRITE, +// Scope.HEALTHKIT_HEIGHTWEIGHT_READ, +// Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, +// Scope.HEALTHKIT_LOCATION_READ, +// Scope.HEALTHKIT_LOCATION_WRITE, +// Scope.HEALTHKIT_PULMONARY_READ, +// Scope.HEALTHKIT_PULMONARY_WRITE, +// Scope.HEALTHKIT_SLEEP_READ, +// Scope.HEALTHKIT_SLEEP_WRITE, +// Scope.HEALTHKIT_SPEED_READ, +// Scope.HEALTHKIT_SPEED_WRITE, +// Scope.HEALTHKIT_STEP_READ, +// Scope.HEALTHKIT_STEP_WRITE, +// Scope.HEALTHKIT_STRENGTH_READ, +// Scope.HEALTHKIT_STRENGTH_WRITE, +// Scope.HEALTHKIT_BODYFAT_READ, +// Scope.HEALTHKIT_BODYFAT_WRITE, +// Scope.HEALTHKIT_NUTRITION_READ, +// Scope.HEALTHKIT_NUTRITION_WRITE, +// Scope.HEALTHKIT_BLOODPRESSURE_READ, +// Scope.HEALTHKIT_BLOODPRESSURE_WRITE, +// Scope.HEALTHKIT_BODYTEMPERATURE_READ, +// Scope.HEALTHKIT_BODYTEMPERATURE_WRITE, +// Scope.HEALTHKIT_OXYGENSTATURATION_READ, +// Scope.HEALTHKIT_OXYGENSTATURATION_WRITE, +// Scope.HEALTHKIT_REPRODUCTIVE_READ, +// Scope.HEALTHKIT_REPRODUCTIVE_WRITE, +// Scope.HEALTHKIT_ACTIVITY_RECORD_READ, +// Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, +// Scope.HEALTHKIT_HEARTRATE_REALTIME, +// Scope.HEALTHKIT_STEP_REALTIME, +// Scope.HEALTHKIT_HEARTHEALTH_WRITE, +// Scope.HEALTHKIT_HEARTHEALTH_READ, +// Scope.HEALTHKIT_STRESS_WRITE, +// Scope.HEALTHKIT_STRESS_READ, +// Scope.HEALTHKIT_OXYGEN_SATURATION_WRITE, +// Scope.HEALTHKIT_OXYGEN_SATURATION_READ, +// Scope.HEALTHKIT_HISTORYDATA_OPEN_WEEK, +// Scope.HEALTHKIT_HISTORYDATA_OPEN_MONTH, +// Scope.HEALTHKIT_HISTORYDATA_OPEN_YEAR, +// ]; +// try { +// AuthHuaweiId? result = await HealthAuth.signIn(scopes); +// debugPrint( +// 'Granted Scopes for User(${result?.displayName}): ${result?.grantedScopes?.toString()}', +// ); +// showSnackBar( +// 'Authorization Success.', +// color: Colors.green, +// ); +// setState(() => accessToken = result?.accessToken); +// } on PlatformException catch (e) { +// debugPrint('Error on authorization, Error:${e.toString()}'); +// showSnackBar( +// 'Error on authorization, Error:${e.toString()}, Error Description: ' +// '${HiHealthStatusCodes.getStatusCodeMessage(e.message ?? '')}', +// ); +// } +// } +// +// // ActivityRecordsController +// // +// /// Adds an ActivityRecord with an ActivitySummary, time range is 2 hours from now. +// Future addActivityRecord() async { +// log( +// 'addActivityRecord', +// _activityTextController, +// LogOptions.call, +// ); +// DateTime startTime = DateTime.now().subtract(const Duration(hours: 2)); +// DateTime endTime = DateTime.now(); +// // Build an ActivityRecord object +// ActivityRecord activityRecord = ActivityRecord( +// startTime: startTime, +// endTime: endTime, +// id: 'ActivityRecordId0', +// name: 'AddActivityRecord', +// activityTypeId: HiHealthActivities.running, +// description: 'This is a test for ActivityRecord', +// activitySummary: ActivitySummary( +// paceSummary: PaceSummary( +// avgPace: 247.27626, +// bestPace: 212.0, +// britishPaceMap: { +// '102802480': 365.0, +// }, +// britishPartTimeMap: { +// '1.0': 263.0, +// }, +// partTimeMap: { +// '1.0': 456.0, +// }, +// paceMap: { +// '1.0': 263.0, +// }, +// ), +// dataSummary: [ +// SamplePoint( +// dataType: DataType.DT_CONTINUOUS_DISTANCE_TOTAL, +// startTime: startTime.add(Duration(seconds: 1)), +// endTime: endTime.subtract(Duration(seconds: 1)), +// fieldValueOptions: FieldFloat(Field.FIELD_DISTANCE, 400), +// timeUnit: TimeUnit.MILLISECONDS, +// ), +// SamplePoint( +// dataType: DataType.POLYMERIZE_CONTINUOUS_SPEED_STATISTICS, +// fieldValueOptions: FieldFloat(Field.FIELD_AVG, 60.0), +// startTime: startTime.add(Duration(seconds: 1)), +// endTime: endTime.subtract(Duration(seconds: 1)), +// timeUnit: TimeUnit.MILLISECONDS, +// ) +// ..setFieldValue(Field.FIELD_MIN, 40.0) +// ..setFieldValue(Field.FIELD_MAX, 80.0), +// ]), +// ); +// +// // Build the dataCollector object +// DataCollector dataCollector = DataCollector( +// dataGenerateType: DataGenerateType.DATA_TYPE_RAW, +// dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, +// name: 'AddActivityRecord1923', +// ); +// +// // You can use sampleSets to add more sample points to the sampling dataset. +// // Build a list of sampling point objects and add it to the sampling dataSet +// List samplePoints = [ +// SamplePoint( +// dataCollector: dataCollector, +// startTime: startTime.add(Duration(seconds: 1)), +// endTime: endTime.subtract(Duration(seconds: 1)), +// fieldValueOptions: FieldFloat(Field.FIELD_STEP_RATE, 10.0), +// timeUnit: TimeUnit.MILLISECONDS, +// ), +// ]; +// SampleSet sampleSet = SampleSet( +// dataCollector, +// samplePoints, +// ); +// +// try { +// await ActivityRecordsController.addActivityRecord( +// ActivityRecordInsertOptions( +// activityRecord: activityRecord, +// sampleSets: [ +// sampleSet, +// ], +// ), +// ); +// log( +// 'addActivityRecord', +// _activityTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'addActivityRecord', +// _activityTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Obtains saved ActivityRecords between yesterday and now, +// /// with the DT_CONTINUOUS_STEPS_DELTA data type +// void getActivityRecord() async { +// log( +// 'getActivityRecord', +// _activityTextController, +// LogOptions.call, +// ); +// // Create start time that will be used to read activity record. +// DateTime startTime = DateTime.now().subtract(const Duration(days: 1)); +// +// // Create end time that will be used to read activity record. +// DateTime endTime = DateTime.now().add(const Duration(hours: 3)); +// +// ActivityRecordReadOptions activityRecordReadOptions = ActivityRecordReadOptions( +// activityRecordId: "ActivityRecordId0", +// activityRecordName: null, +// startTime: startTime, +// endTime: endTime, +// timeUnit: TimeUnit.MILLISECONDS, +// dataType: DataType.DT_INSTANTANEOUS_STEPS_RATE, +// ); +// try { +// List result = await ActivityRecordsController.getActivityRecord( +// activityRecordReadOptions, +// ); +// log( +// 'getActivityRecord', +// _activityTextController, +// LogOptions.success, +// result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', +// ); +// } on PlatformException catch (e) { +// log( +// 'getActivityRecord', +// _activityTextController, +// LogOptions.error, +// result: e.message, +// ); +// } +// } +// +// /// Starts the ActivityRecord with the id:`ActivityRecordRun1` +// void beginActivityRecord() async { +// try { +// log( +// 'beginActivityRecord', +// _activityTextController, +// LogOptions.call, +// ); +// // Build an ActivityRecord object +// ActivityRecord activityRecord = ActivityRecord( +// id: 'ActivityRecordRun0', +// name: 'BeginActivityRecord', +// description: 'This is ActivityRecord begin test!', +// activityTypeId: HiHealthActivities.running, +// startTime: DateTime.now().subtract(const Duration(hours: 1)), +// ); +// await ActivityRecordsController.beginActivityRecord( +// activityRecord, +// ); +// log( +// 'beginActivityRecord', +// _activityTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'beginActivityRecord', +// _activityTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Stops the ActivityRecord with the id:`ActivityRecordRun1` +// void endActivityRecord() async { +// try { +// log( +// 'endActivityRecord', +// _activityTextController, +// LogOptions.call, +// ); +// final List result = await ActivityRecordsController.endActivityRecord( +// 'ActivityRecordRun0', +// ); +// // Return the list of activity records that have stopped +// log( +// 'endActivityRecord', +// _activityTextController, +// LogOptions.success, +// result: result.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'endActivityRecord', +// _activityTextController, +// LogOptions.error, +// result: e.message, +// ); +// } +// } +// +// /// Ends all the ongoing activity records. +// /// +// /// Result list will be null if there is no ongoing activity record. +// void endAllActivityRecords() async { +// try { +// log( +// 'endAllActivityRecords', +// _activityTextController, +// LogOptions.call, +// ); +// // Return the list of activity records that have stopped +// List result = await ActivityRecordsController.endAllActivityRecords(); +// log( +// 'endAllActivityRecords', +// _activityTextController, +// LogOptions.success, +// result: '[IDs: ${result.map((ActivityRecord e) => e.id).toList()}]', +// ); +// } on PlatformException catch (e) { +// log( +// 'endAllActivityRecords', +// _activityTextController, +// LogOptions.error, +// result: e.message, +// ); +// } +// } +// +// // +// // +// // End of ActivityRecordsController Methods +// +// // DataController Methods +// // +// // +// /// Initializes a DataController instance with a list of HiHealtOptions. +// void initDataController() async { +// if (!mounted) return; +// log( +// 'init', +// _dataTextController, +// LogOptions.call, +// ); +// try { +// _dataController = await DataController.init(); +// log( +// 'init', +// _dataTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'init', +// _dataTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Clears all the data inserted by the app. +// void clearAll() async { +// log('clearAll', _dataTextController, LogOptions.call); +// try { +// await _dataController.clearAll(); +// log('clearAll', _dataTextController, LogOptions.success); +// } on PlatformException catch (e) { +// log('clearAll', _dataTextController, LogOptions.error, error: e.message); +// } +// } +// +// /// Deletes DT_CONTINUOUS_STEPS_DELTA type data by the specified time range. +// void delete() async { +// log( +// 'delete', +// _dataTextController, +// LogOptions.call, +// ); +// // Build the dataCollector object +// DataCollector dataCollector = DataCollector( +// dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, +// dataGenerateType: DataGenerateType.DATA_TYPE_RAW, +// dataStreamName: 'STEPS_DELTA', +// ); +// +// // Build the time range for the deletion: start time and end time. +// DeleteOptions deleteOptions = DeleteOptions( +// dataCollectors: [dataCollector], +// startTime: DateTime.parse('2020-10-10 08:00:00'), +// endTime: DateTime.parse('2020-10-10 12:30:00'), +// ); +// +// // Call the api with the constructed DeleteOptions instance. +// try { +// _dataController.delete(deleteOptions); +// log( +// 'delete', +// _dataTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'delete', +// _dataTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Inserts a sampling set with the DT_CONTINUOUS_STEPS_DELTA data type at the +// /// specified start and end dates. +// void insert() async { +// log( +// 'insert', +// _dataTextController, +// LogOptions.call, +// ); +// // Build the dataCollector object +// DataCollector dataCollector = DataCollector( +// dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, +// dataStreamName: 'STEPS_DELTA', +// dataGenerateType: DataGenerateType.DATA_TYPE_RAW, +// ); +// // You can use sampleSets to add more sampling points to the sampling dataset. +// SampleSet sampleSet = SampleSet( +// dataCollector, +// [ +// SamplePoint( +// dataCollector: dataCollector, +// startTime: DateTime.parse('2020-10-10 12:00:00'), +// endTime: DateTime.parse('2020-10-10 12:12:00'), +// fieldValueOptions: FieldInt( +// Field.FIELD_STEPS_DELTA, +// 100, +// ), +// ), +// ], +// ); +// // Call the api with the constructed sample set. +// try { +// _dataController.insert(sampleSet); +// log( +// 'insert', +// _dataTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'insert', +// _dataTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// // Reads the user data between the specified start and end dates. +// void read() async { +// log( +// 'read', +// _dataTextController, +// LogOptions.call, +// ); +// // Build the dataCollector object +// DataCollector dataCollector = DataCollector( +// dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, +// dataGenerateType: DataGenerateType.DATA_TYPE_RAW, +// dataStreamName: 'STEPS_DELTA', +// ); +// +// // Build the time range for the query: start time and end time. +// ReadOptions readOptions = ReadOptions( +// dataCollectors: [ +// dataCollector, +// ], +// startTime: DateTime.parse('2020-10-10 12:00:00'), +// endTime: DateTime.parse('2020-10-10 12:12:00'), +// )..groupByTime(10000); +// +// // Call the api with the constructed ReadOptions instance. +// try { +// ReadReply? readReply = await _dataController.read(readOptions); +// log( +// 'read', +// _dataTextController, +// LogOptions.success, +// result: readReply.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'read', +// _dataTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Reads the daily summation between the dates: `2020.10.02` to `2020.12.15` for multiple data types. +// /// Note that the time format is different for this method. +// void readDailySummationList() async { +// log( +// 'readDailySummationList', +// _dataTextController, +// LogOptions.call, +// ); +// try { +// List? sampleSets = await _dataController.readDailySummationList( +// [DataType.DT_CONTINUOUS_STEPS_DELTA, DataType.DT_CONTINUOUS_CALORIES_BURNT], +// 20201002, +// 20201003, +// ); +// log( +// 'readDailySummationList', +// _dataTextController, +// LogOptions.success, +// result: sampleSets.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'readDailySummationList', +// _dataTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Reads the steps summation for today. +// void readTodaySummation() async { +// log( +// 'readTodaySummation', +// _dataTextController, +// LogOptions.call, +// ); +// try { +// SampleSet? sampleSet = await _dataController.readTodaySummation( +// DataType.DT_CONTINUOUS_STEPS_DELTA, +// ); +// log( +// 'readTodaySummation', +// _dataTextController, +// LogOptions.success, +// result: sampleSet.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'readTodaySummation', +// _dataTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Updates DT_CONTINUOUS_STEPS_DELTA for the specified dates. +// void update() async { +// log( +// 'update', +// _dataTextController, +// LogOptions.call, +// ); +// +// // Build the dataCollector object +// DataCollector dataCollector = DataCollector( +// dataType: DataType.DT_CONTINUOUS_STEPS_DELTA, +// dataStreamName: 'STEPS_DELTA', +// dataGenerateType: DataGenerateType.DATA_TYPE_RAW, +// ); +// +// // You can use sampleSets to add more sampling points to the sampling dataset. +// SampleSet sampleSet = SampleSet( +// dataCollector, +// [ +// SamplePoint( +// dataCollector: dataCollector, +// startTime: DateTime.parse('2020-12-12 09:00:00'), +// endTime: DateTime.parse('2020-12-12 09:05:00'), +// fieldValueOptions: FieldInt( +// Field.FIELD_STEPS_DELTA, +// 120, +// ), +// ), +// ], +// ); +// +// // Build a parameter object for the update. +// // Note: (1) The start time of the modified object updateOptions can not be greater than the minimum +// // value of the start time of all sample data points in the modified data sample set +// // (2) The end time of the modified object updateOptions can not be less than the maximum value of the +// // end time of all sample data points in the modified data sample set +// UpdateOptions updateOptions = UpdateOptions( +// startTime: DateTime.parse('2020-12-12 08:00:00'), +// endTime: DateTime.parse('2020-12-12 09:25:00'), +// sampleSet: sampleSet, +// ); +// try { +// await _dataController.update(updateOptions); +// log( +// 'update', +// _dataTextController, +// LogOptions.success, +// result: sampleSet.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'update', +// _dataTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// // +// // +// // End of DataController Methods +// +// // SettingController Methods +// // +// /// Adds a custom DataType with the FIELD_ALTITUDE. +// void addDataType() async { +// log( +// 'addDataType', +// _settingTextController, +// LogOptions.call, +// ); +// try { +// // The name of the created data type must be prefixed with the package name +// // of the app. Otherwise, the creation fails. If the same data type is tried to +// // be added again an exception will be thrown. +// DataTypeAddOptions options = DataTypeAddOptions( +// '$packageName.myCustomDataType', +// [ +// const Field.newIntField('myIntField'), +// Field.FIELD_ALTITUDE, +// ], +// ); +// final DataType dataTypeResult = await SettingController.addDataType( +// options, +// ); +// log( +// 'addDataType', +// _settingTextController, +// LogOptions.success, +// result: dataTypeResult.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'addDataType', +// _settingTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Reads the inserted data type on the [addDataType] method. +// void readDataType() async { +// log( +// 'readDataType', +// _settingTextController, +// LogOptions.call, +// ); +// try { +// final DataType dataTypeResult = await SettingController.readDataType( +// '$packageName.myCustomDataType', +// ); +// log( +// 'readDataType', +// _settingTextController, +// LogOptions.success, +// result: dataTypeResult.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'readDataType', +// _settingTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Disables the Health Kit function, cancels user authorization, and cancels +// /// all data records. (The task takes effect in 24 hours.) +// void disableHiHealth() async { +// log( +// 'disableHiHealth', +// _settingTextController, +// LogOptions.call, +// ); +// try { +// await SettingController.disableHiHealth(); +// log( +// 'disableHiHealth', +// _settingTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'disableHiHealth', +// _settingTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Checks the user privacy authorization to Health Kit. Redirects the user to +// /// the Authorization screen if the permissions are not given. +// void checkHealthAppAuthorization() async { +// log( +// 'checkHealthAppAuthorization', +// _settingTextController, +// LogOptions.call, +// ); +// try { +// await SettingController.checkHealthAppAuthorization(); +// log( +// 'checkHealthAppAuthorization', +// _settingTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'checkHealthAppAuthorization', +// _settingTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Checks the user privacy authorization to Health Kit. If authorized `true` +// /// value would be returned. +// void getHealthAppAuthorization() async { +// log( +// 'getHealthAppAuthorization', +// _settingTextController, +// LogOptions.call, +// ); +// try { +// final bool result = await SettingController.getHealthAppAuthorization(); +// log( +// 'getHealthAppAuthorization', +// _settingTextController, +// LogOptions.success, +// result: result.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'getHealthAppAuthorization', +// _settingTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// void requestAuth() async { +// final HealthKitAuthResult res = await SettingController.requestAuthorizationIntent( +// [ +// Scope.HEALTHKIT_STEP_READ, +// Scope.HEALTHKIT_STEP_WRITE, +// Scope.HEALTHKIT_HEIGHTWEIGHT_READ, +// Scope.HEALTHKIT_HEIGHTWEIGHT_WRITE, +// Scope.HEALTHKIT_HEARTRATE_READ, +// Scope.HEALTHKIT_HEARTRATE_WRITE, +// Scope.HEALTHKIT_ACTIVITY_RECORD_READ, +// Scope.HEALTHKIT_ACTIVITY_RECORD_WRITE, +// Scope.HEALTHKIT_HEARTHEALTH_READ, +// Scope.HEALTHKIT_HEARTHEALTH_WRITE, +// ], +// true, +// ); +// debugPrint(res.authAccount?.accessToken); +// } +// +// // +// // +// // End of SettingController Methods +// +// // AutoRecorderController Methods +// // +// // +// // Callback function for AutoRecorderStream event. +// void _onAutoRecorderEvent(SamplePoint? res) { +// log( +// '[AutoRecorderEvent] obtained, SamplePoint Field Value is ${res?.fieldValues?.toString()}', +// _autoRecorderTextController, +// LogOptions.custom, +// ); +// } +// +// /// Starts an Android Foreground Service to count the steps of the user. +// /// The steps will be emitted to the AutoRecorderStream. +// void startRecord() async { +// log( +// 'startRecord', +// _autoRecorderTextController, +// LogOptions.call, +// ); +// try { +// await AutoRecorderController.startRecord( +// DataType.DT_CONTINUOUS_STEPS_TOTAL, +// NotificationProperties( +// title: 'HMS Flutter Health Demo', +// text: 'Counting steps', +// subText: 'this is a subtext', +// ticker: 'this is a ticker', +// showChronometer: true, +// ), +// ); +// log( +// 'startRecord', +// _autoRecorderTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'startRecord', +// _autoRecorderTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Ends the Foreground service and stops the step count events. +// void stopRecord() async { +// log( +// 'endRecord', +// _autoRecorderTextController, +// LogOptions.call, +// ); +// try { +// await AutoRecorderController.stopRecord( +// DataType.DT_CONTINUOUS_STEPS_TOTAL, +// ); +// log( +// 'endRecord', +// _autoRecorderTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'endRecord', +// _autoRecorderTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// // +// // +// // End of AutoRecorderController Methods +// +// // ConsentController Methods +// // +// /// Obtains the application id from the agconnect-services.json file. +// void getAppId() async { +// log( +// 'getAppId', +// _consentTextController, +// LogOptions.call, +// ); +// try { +// final String appId = await ConsentsController.getAppId(); +// log( +// 'getAppId', +// _consentTextController, +// LogOptions.success, +// result: appId, +// ); +// } on PlatformException catch (e) { +// log( +// 'getAppId', +// _consentTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Gets the granted permission scopes for the app. +// void getScopes() async { +// log( +// 'getScopes', +// _consentTextController, +// LogOptions.call, +// ); +// try { +// final String appId = await ConsentsController.getAppId(); +// final ScopeLangItem scopeLangItem = await ConsentsController.getScopes( +// 'en-gb', +// appId, +// ); +// log( +// 'getScopes', +// _consentTextController, +// LogOptions.success, +// result: scopeLangItem.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'getScopes', +// _consentTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Revokes all the permissions that authorized for this app. +// void revoke() async { +// log( +// 'revoke', +// _consentTextController, +// LogOptions.call, +// ); +// try { +// final String appId = await ConsentsController.getAppId(); +// await ConsentsController.revoke(appId); +// log( +// 'revoke', +// _consentTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'revoke', +// _consentTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// /// Revokes the distance read/write permissions for the app. +// void revokeWithScopes() async { +// log( +// 'revokeWithScopes', +// _consentTextController, +// LogOptions.call, +// ); +// try { +// // Obtain the application id. +// final String appId = await ConsentsController.getAppId(); +// // Call the revokeWithScopes method with desired scopes. +// await ConsentsController.revokeWithScopes( +// appId, +// [ +// Scope.HEALTHKIT_DISTANCE_WRITE, +// Scope.HEALTHKIT_DISTANCE_READ, +// ], +// ); +// log( +// 'revokeWithScopes', +// _consentTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'revokeWithScopes', +// _consentTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// // +// // +// // End of ConsentController Methods +// +// // HealthController Methods +// // +// void addHealthRecord() async { +// log( +// 'addHealthRecord', +// _healthTextController, +// LogOptions.call, +// ); +// try { +// final DateTime startTime = DateTime(2023, 5, 11); +// final DateTime endTime = DateTime(2023, 5, 13); +// +// DataCollector contDataCollector = DataCollector( +// dataStreamName: 'contDataCollector', +// packageName: packageName, +// dataType: DataType.POLYMERIZE_CONTINUOUS_HEART_RATE_STATISTICS, +// dataGenerateType: DataGenerateType.DATA_TYPE_RAW, +// ); +// +// DataCollector instDataCollector = DataCollector( +// dataStreamName: 'instDataCollector', +// packageName: packageName, +// dataType: DataType.DT_INSTANTANEOUS_HEART_RATE, +// dataGenerateType: DataGenerateType.DATA_TYPE_RAW, +// ); +// +// List subDataDetails = [ +// SampleSet(instDataCollector, [ +// SamplePoint( +// dataCollector: instDataCollector, +// ) +// ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) +// ..setFieldValue(Field.FIELD_BPM, 88.0) +// ]) +// ]; +// +// List subDataSummary = [ +// SamplePoint( +// dataCollector: contDataCollector, +// ) +// ..setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS) +// ..setFieldValue(Field.FIELD_AVG, 90.0) +// ..setFieldValue(Field.FIELD_MAX, 100.0) +// ..setFieldValue(Field.FIELD_MIN, 80.0) +// ..setFieldValue(Field.LAST, 85.0) +// ]; +// +// final HealthRecord healthRecord = HealthRecord( +// startTime: startTime, +// endTime: endTime, +// metadata: 'Data', +// dataCollector: DataCollector( +// dataStreamName: 'such as step count', +// packageName: packageName, +// dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, +// dataGenerateType: DataGenerateType.DATA_TYPE_RAW, +// ), +// ) +// ..setSubDataSummary(subDataSummary) +// ..setSubDataDetails(subDataDetails) +// ..setFieldValue(HealthFields.FIELD_THRESHOLD, 42.0) +// ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 48.0) +// ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 42.0) +// ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.0); +// +// final String? result = await HealthRecordController.addHealthRecord( +// HealthRecordInsertOptions( +// healthRecord: healthRecord, +// ), +// ); +// log( +// 'addHealthRecord', +// _healthTextController, +// LogOptions.success, +// result: result.toString(), +// ); +// } on PlatformException catch (e) { +// log( +// 'addHealthRecord', +// _healthTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// void getHealthRecord() async { +// log( +// 'getHealthRecord', +// _healthTextController, +// LogOptions.call, +// ); +// try { +// final DateTime startTime = DateTime(2023, 5, 11); +// final DateTime endTime = DateTime(2023, 5, 13); +// +// HealthRecordReply result = await HealthRecordController.getHealthRecord( +// HealthRecordReadOptions( +// packageName: packageName, +// ) +// ..setSubDataTypeList( +// [ +// DataType.DT_INSTANTANEOUS_HEART_RATE, +// ], +// ) +// ..setTimeInterval( +// startTime, +// endTime, +// TimeUnit.MILLISECONDS, +// ) +// ..readByDataType( +// HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, +// ) +// ..readHealthRecordsFromAllApps(), +// ); +// log( +// 'getHealthRecord', +// _healthTextController, +// LogOptions.success, +// result: result.healthRecords[0].toJson(), +// ); +// } on PlatformException catch (e) { +// log( +// 'getHealthRecord', +// _healthTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// void updateHealthRecord() async { +// log( +// 'updateHealthRecord', +// _healthTextController, +// LogOptions.call, +// ); +// try { +// final DateTime startTime = DateTime(2022, 10, 11); +// final DateTime endTime = DateTime(2022, 10, 12); +// final HealthRecord healthRecord = HealthRecord( +// startTime: startTime, +// endTime: endTime, +// metadata: 'Data', +// dataCollector: DataCollector( +// dataStreamName: 'such as step count', +// packageName: packageName, +// dataType: HealthDataTypes.DT_HEALTH_RECORD_BRADYCARDIA, +// dataGenerateType: DataGenerateType.DATA_TYPE_RAW, +// ), +// ) +// ..setFieldValue(HealthFields.FIELD_THRESHOLD, 41.9) +// ..setFieldValue(HealthFields.FIELD_MAX_HEART_RATE, 49.1) +// ..setFieldValue(HealthFields.FIELD_MIN_HEART_RATE, 41.1) +// ..setFieldValue(HealthFields.FIELD_AVG_HEART_RATE, 45.1); +// await HealthRecordController.updateHealthRecord( +// HealthRecordUpdateOptions( +// healthRecord: healthRecord, +// healthRecordId: '', +// ), +// ); +// log( +// 'updateHealthRecord', +// _healthTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'updateHealthRecord', +// _healthTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// void deleteHealthRecord() async { +// log( +// 'deleteHealthRecord', +// _healthTextController, +// LogOptions.call, +// ); +// try { +// await HealthRecordController.deleteHealthRecord( +// HealthRecordDeleteOptions( +// startTime: DateTime.now().subtract(const Duration(days: 14)), +// endTime: DateTime.now(), +// )..setHealthRecordIds( +// [ +// '', +// ], +// ), +// ); +// log( +// 'deleteHealthRecord', +// _healthTextController, +// LogOptions.success, +// ); +// } on PlatformException catch (e) { +// log( +// 'deleteHealthRecord', +// _healthTextController, +// LogOptions.error, +// error: e.message, +// ); +// } +// } +// +// // +// // +// // End of HealthController Methods +// +// // App's widgets. +// // +// // +// Widget expansionCard({ +// required String titleText, +// required List children, +// }) { +// return Card( +// margin: componentPadding, +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(10.0), +// ), +// child: ExpansionTile( +// title: Text( +// titleText, +// style: cardTitleTextStyle, +// ), +// children: children, +// ), +// ); +// } +// +// Widget loggingArea( +// TextEditingController moduleTextController, +// ) { +// return Column( +// children: [ +// Container( +// margin: componentPadding, +// padding: const EdgeInsets.all(8.0), +// height: 200, +// decoration: BoxDecoration( +// borderRadius: BorderRadius.circular(5.0), +// border: Border.all(color: Colors.black12), +// ), +// child: TextField( +// readOnly: true, +// maxLines: 15, +// controller: moduleTextController, +// decoration: const InputDecoration( +// enabledBorder: InputBorder.none, +// ), +// ), +// ), +// TextButton( +// child: const Text('Clear Log'), +// onPressed: () => setState(() { +// moduleTextController.text = ''; +// }), +// ) +// ], +// ); +// } +// +// void showSnackBar( +// String text, { +// Color color = Colors.blue, +// }) { +// final SnackBar snackBar = SnackBar( +// content: Text(text), +// backgroundColor: color, +// action: SnackBarAction( +// label: 'Close', +// textColor: Colors.white, +// onPressed: () { +// ScaffoldMessenger.of(context).removeCurrentSnackBar(); +// }, +// ), +// ); +// ScaffoldMessenger.of(context).showSnackBar(snackBar); +// } +// +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar( +// backgroundColor: Colors.white, +// title: const Text( +// 'Huawei Health Kit', +// style: TextStyle( +// color: Colors.blue, +// fontWeight: FontWeight.bold, +// ), +// ), +// centerTitle: true, +// elevation: 0.0, +// actions: [ +// IconButton( +// onPressed: requestAuth, +// icon: const Icon(Icons.ac_unit), +// ), +// ], +// ), +// body: Builder( +// builder: (BuildContext context) { +// return ListView( +// physics: const BouncingScrollPhysics( +// parent: AlwaysScrollableScrollPhysics(), +// ), +// children: [ +// // Sign In Widgets +// Card( +// margin: componentPadding, +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(10.0), +// ), +// child: Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// const Padding( +// padding: componentPadding, +// child: Text( +// 'Tap to SignIn button to obtain the HMS Account to complete ' +// 'login and authorization, and then use other buttons ' +// 'to try the related API functions.', +// textAlign: TextAlign.center, +// ), +// ), +// const Padding( +// padding: componentPadding, +// child: Text( +// 'Note: If the login page is not displayed, change the package ' +// 'name, AppID, and configure the signature file by referring ' +// 'to the developer guide on the official website.', +// textAlign: TextAlign.center, +// style: TextStyle( +// color: Colors.blue, +// ), +// ), +// ), +// Container( +// padding: componentPadding, +// width: double.infinity, +// child: OutlinedButton( +// style: ButtonStyle( +// backgroundColor: MaterialStateProperty.all( +// Colors.blue, +// ), +// ), +// child: const Text( +// 'SignIn', +// style: TextStyle( +// color: Colors.white, +// ), +// ), +// onPressed: () => signIn(), +// ), +// ), +// ], +// ), +// ), +// +// // ActivityRecordsController +// expansionCard( +// titleText: 'ActivityRecords Controller', +// children: [ +// loggingArea(_activityTextController), +// ListTile( +// title: const Text('AddActivityRecord'), +// onTap: () => addActivityRecord(), +// ), +// ListTile( +// title: const Text('GetActivityRecord'), +// onTap: () => getActivityRecord(), +// ), +// ListTile( +// title: const Text('beginActivityRecord'), +// onTap: () => beginActivityRecord(), +// ), +// ListTile( +// title: const Text('endActivityRecord'), +// onTap: () => endActivityRecord(), +// ), +// ListTile( +// title: const Text('endAllActivityRecords'), +// onTap: () => endAllActivityRecords(), +// ), +// ], +// ), +// // DataController Widgets +// expansionCard( +// titleText: 'DataController', +// children: [ +// loggingArea(_dataTextController), +// ListTile( +// title: const Text('readTodaySummation'), +// onTap: () => readTodaySummation(), +// ), +// ListTile( +// title: const Text('readDailySummationList'), +// onTap: () => readDailySummationList(), +// ), +// ListTile( +// title: const Text('insert'), +// onTap: () => insert(), +// ), +// ListTile( +// title: const Text('read'), +// onTap: () => read(), +// ), +// ListTile( +// title: const Text('update'), +// onTap: () => update(), +// ), +// ListTile( +// title: const Text('delete'), +// onTap: () => delete(), +// ), +// ListTile( +// title: const Text('clearAll'), +// onTap: () => clearAll(), +// ), +// ], +// ), +// // SettingController Widgets. +// expansionCard( +// titleText: 'SettingController', +// children: [ +// loggingArea(_settingTextController), +// ListTile( +// title: const Text('addDataType'), +// onTap: () => addDataType(), +// ), +// ListTile( +// title: const Text('readDataType'), +// onTap: () => readDataType(), +// ), +// ListTile( +// title: const Text('disableHiHealth'), +// onTap: () => disableHiHealth(), +// ), +// ListTile( +// title: const Text('checkHealthAppAuthorization'), +// onTap: () => checkHealthAppAuthorization(), +// ), +// ListTile( +// title: const Text('getHealthAppAuthorization'), +// onTap: () => getHealthAppAuthorization(), +// ), +// ], +// ), +// // AutoRecorderController Widgets +// expansionCard( +// titleText: 'AutoRecorderController', +// children: [ +// loggingArea(_autoRecorderTextController), +// ListTile( +// title: const Text('startRecord'), +// onTap: () => startRecord(), +// ), +// ListTile( +// title: const Text('stopRecord'), +// onTap: () => stopRecord(), +// ), +// ], +// ), +// // Consent Controller Widgets +// expansionCard( +// titleText: 'ConsentController', +// children: [ +// loggingArea(_consentTextController), +// ListTile( +// title: const Text('getAppId'), +// onTap: () => getAppId(), +// ), +// ListTile( +// title: const Text('getScopes'), +// onTap: () => getScopes(), +// ), +// ListTile( +// title: const Text('revoke'), +// onTap: () => revoke(), +// ), +// ListTile( +// title: const Text('revokeWithScopes'), +// onTap: () => revokeWithScopes(), +// ), +// ], +// ), +// +// // Health Controller Widgets +// expansionCard( +// titleText: 'HealthController', +// children: [ +// loggingArea(_healthTextController), +// ListTile( +// title: const Text('addHealthRecord'), +// onTap: () => addHealthRecord(), +// ), +// ListTile( +// title: const Text('getHealthRecord'), +// onTap: () => getHealthRecord(), +// ), +// ListTile( +// title: const Text('updateHealthRecord'), +// onTap: () => updateHealthRecord(), +// ), +// ListTile( +// title: const Text('deleteHealthRecord'), +// onTap: () => deleteHealthRecord(), +// ), +// ], +// ), +// ], +// ); +// }, +// ), +// ); +// } +// } +// +// /// Options for logging. +// enum LogOptions { +// call, +// success, +// error, +// custom, +// } diff --git a/lib/presentation/smartwatches/widgets/health_chart.dart b/lib/presentation/smartwatches/widgets/health_chart.dart index fe6ae78f..e3c368ba 100644 --- a/lib/presentation/smartwatches/widgets/health_chart.dart +++ b/lib/presentation/smartwatches/widgets/health_chart.dart @@ -1,5 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; class HealthChart extends StatelessWidget { final List spots; @@ -19,7 +20,7 @@ class HealthChart extends StatelessWidget { height: 200, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 80c70714..323a13c2 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -49,7 +49,7 @@ class PossibleConditionsPage extends StatelessWidget { child: Container( height: 80.h, decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(10.r), ), ), diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index 484ad2d7..462cffbd 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -184,7 +184,7 @@ class _RiskFactorsScreenState extends State { Container( height: 40.h, decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24.r), ), ).toShimmer2(isShow: true, radius: 24.r), diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index 8d32b7f4..cbfc15a1 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -160,7 +160,7 @@ class _SuggestionsScreenState extends State { Container( height: 40.h, decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24.r), ), ).toShimmer2(isShow: true, radius: 24.r), diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index cbbfc68e..d7fbf63c 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -222,7 +222,7 @@ class _SymptomsSelectorPageState extends State { Container( height: 40.h, decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24.r), ), ).toShimmer2(isShow: true, radius: 24.r), diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 0231ef19..b4ceff4c 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -275,7 +275,7 @@ class _TriagePageState extends State { Container( height: 40.h, decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24.r), ), ).toShimmer2(isShow: true, radius: 24.r), diff --git a/lib/presentation/symptoms_checker/widgets/organ_dot_widget.dart b/lib/presentation/symptoms_checker/widgets/organ_dot_widget.dart index 0783dfb7..2ef4497c 100644 --- a/lib/presentation/symptoms_checker/widgets/organ_dot_widget.dart +++ b/lib/presentation/symptoms_checker/widgets/organ_dot_widget.dart @@ -34,7 +34,7 @@ class OrganDot extends StatelessWidget { child: Container( width: size.w * 0.25, height: size.h * 0.25, - decoration: const BoxDecoration( + decoration: BoxDecoration( shape: BoxShape.circle, color: AppColors.whiteColor, ), diff --git a/lib/presentation/todo_section/ancillary_order_payment_page.dart b/lib/presentation/todo_section/ancillary_order_payment_page.dart index b34bf212..9d75c769 100644 --- a/lib/presentation/todo_section/ancillary_order_payment_page.dart +++ b/lib/presentation/todo_section/ancillary_order_payment_page.dart @@ -264,6 +264,7 @@ class _AncillaryOrderPaymentPageState extends State { width: 200.h, height: 80.h, fit: BoxFit.contain, + applyThemeColor: false, ).paddingSymmetrical(24.h, 0.h).onPress(() { if (!todoSectionViewModel.isProcessingPayment) { if (Utils.havePrivilege(103)) { diff --git a/lib/presentation/todo_section/ancillary_procedures_details_page.dart b/lib/presentation/todo_section/ancillary_procedures_details_page.dart index eba80cd4..38d4f978 100644 --- a/lib/presentation/todo_section/ancillary_procedures_details_page.dart +++ b/lib/presentation/todo_section/ancillary_procedures_details_page.dart @@ -673,7 +673,7 @@ class _AncillaryOrderDetailsListState extends State { ); }, isDisabled: !isButtonEnabled, - textColor: AppColors.whiteColor, + textColor: Colors.white, borderRadius: 12.r, borderColor: Colors.transparent, padding: EdgeInsets.symmetric(vertical: 16.h), diff --git a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart index 58ee6d46..6a41252e 100644 --- a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart +++ b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart @@ -118,7 +118,7 @@ class AncillaryOrderCard extends StatelessWidget { : Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "Ancillary Orders".toText14(isBold: true), + LocaleKeys.ancillaryOrdersList.tr(context: context).toText14(isBold: true), Row( children: [ LocaleKeys.viewAll.tr(context: context).toText12(color: AppColors.primaryRedColor), diff --git a/lib/presentation/water_monitor/water_consumption_page.dart b/lib/presentation/water_monitor/water_consumption_page.dart index 54dace58..7e6d1d5e 100644 --- a/lib/presentation/water_monitor/water_consumption_page.dart +++ b/lib/presentation/water_monitor/water_consumption_page.dart @@ -79,7 +79,7 @@ class _WaterConsumptionPageState extends State { subTitle.toText18(weight: FontWeight.w600), ], ), - Utils.buildSvgWithAssets(icon: AppAssets.greenTickIcon) + Utils.buildSvgWithAssets(icon: AppAssets.greenTickIcon, applyThemeColor: false) ], ).paddingSymmetrical(0, 8.h); } diff --git a/lib/presentation/water_monitor/water_monitor_settings_page.dart b/lib/presentation/water_monitor/water_monitor_settings_page.dart index a3c82c0c..81157c8a 100644 --- a/lib/presentation/water_monitor/water_monitor_settings_page.dart +++ b/lib/presentation/water_monitor/water_monitor_settings_page.dart @@ -75,7 +75,7 @@ class _WaterMonitorSettingsPageState extends State { message: "", child: Container( padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 4.h, bottom: 4.h), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.r)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(20.r)), child: ListView.separated( shrinkWrap: true, itemCount: items.length, diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 1267dbb2..f22c6805 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -118,7 +118,7 @@ class AppRoutes { userInfoSelection: (context) => UserInfoSelectionPage(), userInfoFlowManager: (context) => UserInfoFlowManager(), smartWatches: (context) => SmartwatchHomePage(), - huaweiHealthExample: (context) => HuaweiHealthExample(), + // huaweiHealthExample: (context) => HuaweiHealthExample(), covid19Test: (context) => Covid19LandingPage(), waterConsumptionPage: (context) => WaterConsumptionPage(), waterMonitorSettingsPage: (context) => WaterMonitorSettingsPage(), diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index 6347a389..c4dfb7c4 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -306,7 +306,7 @@ Widget showPhoneNumberPickerWidget({required BuildContext context, String? messa onPressed: onSMSPress, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedBorderColor, - textColor: AppColors.whiteColor, + textColor: Colors.white, icon: AppAssets.message, ), ), @@ -325,11 +325,12 @@ Widget showPhoneNumberPickerWidget({required BuildContext context, String? messa child: CustomButton( text: LocaleKeys.sendOTPWHATSAPP.tr(context: context), onPressed: onWhatsappPress, - backgroundColor: Colors.white, + backgroundColor: AppColors.whiteColor, borderColor: AppColors.borderOnlyColor, textColor: AppColors.textColor, icon: AppAssets.whatsapp, iconColor: null, + applyThemeColor: false, ), ), ], diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 326665e9..df20fded 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -101,6 +101,7 @@ class _SplashScreenState extends State { print('Call Canceled : ------->'); try { + FlutterCallkitIncoming.unsilenceEvents(); FlutterCallkitIncoming.onEvent.listen((event) async { switch (event!.event) { case Event.actionCallIncoming: diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index c09d537f..51d2cbc2 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; class AppTheme { static ThemeData getTheme(isArabic) => ThemeData( @@ -14,33 +15,13 @@ class AppTheme { }, ), hintColor: Colors.grey[400], - // colorScheme: ColorScheme.fromSwatch(accentColor: MyColors.backgroundColor), disabledColor: Colors.grey[300], - // errorColor: const Colors, - // scaffoldBackgroundColor: MyColors.backgroundColor, textSelectionTheme: const TextSelectionThemeData(cursorColor: Colors.grey, selectionColor: Color.fromRGBO(80, 100, 253, 0.5), selectionHandleColor: Colors.grey), canvasColor: Colors.white, - // backgroundColor: const Color.fromRGBO(255, 255, 255, 1), highlightColor: Colors.grey[100]!.withOpacity(0.4), splashColor: Colors.transparent, - // primaryColor: primaryColor, - // primaryColorDark: primaryColor, - // toggleableActiveColor: secondaryColor, - // indicatorColor: secondaryColor, - // bottomSheetTheme: const BottomSheetThemeData( - // backgroundColor: Color(0xFFE0E0E0), - // ), bottomSheetTheme: BottomSheetThemeData( backgroundColor: Colors.black.withOpacity(0)), - // primaryTextTheme: const TextTheme( - // bodyText2: TextStyle(color: Colors.white), - // ), - // iconTheme: const IconThemeData(color: MyColors.darkTextColor), - // textTheme: const TextTheme( - // bodyText1: TextStyle(color: Colors.black, letterSpacing: 0.6), - // headline1: TextStyle(color: Colors.white, letterSpacing: 0.6), - // headline2: TextStyle(color: Colors.white, letterSpacing: 0.6), - // ), floatingActionButtonTheme: const FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2), appBarTheme: AppBarTheme( color: const Color(0xff515A5D), @@ -52,6 +33,38 @@ class AppTheme { surfaceTintColor: Colors.transparent, ), ); + + static ThemeData getDarkTheme(isArabic) => ThemeData( + fontFamily: isArabic ? 'GESSTwo' : 'Poppins', + primarySwatch: Colors.red, + visualDensity: VisualDensity.adaptivePlatformDensity, + brightness: Brightness.dark, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder() + }, + ), + hintColor: Colors.grey[600], + disabledColor: Colors.grey[700], + textSelectionTheme: const TextSelectionThemeData(cursorColor: Colors.grey, selectionColor: Color.fromRGBO(80, 100, 253, 0.5), selectionHandleColor: Colors.grey), + canvasColor: AppColors.dark.scaffoldBgColor, + scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor, + highlightColor: Colors.grey[800]!.withOpacity(0.4), + splashColor: Colors.transparent, + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: Colors.black.withOpacity(0)), + floatingActionButtonTheme: const FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2), + appBarTheme: AppBarTheme( + color: AppColors.dark.scaffoldBgColor, + elevation: 0.0, + actionsIconTheme: const IconThemeData( + color: Colors.white, + ), + systemOverlayStyle: SystemUiOverlayStyle.light, + surfaceTintColor: Colors.transparent, + ), + ); } extension ExtendedRevoCheckTheme on TextTheme { diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index d7d97670..17e53ba7 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -1,119 +1,311 @@ import 'package:flutter/material.dart'; class AppColors { + /// Global dark-mode flag — toggled by ProfileSettingsViewModel. + /// When true, every getter below returns the dark palette value. + static bool isDarkMode = false; + static const transparent = Colors.transparent; - static const mainPurple = Color(0xFF7954F7); // #7954F7 - - static const scaffoldBgColor = Color(0xFFF8F8F8); // #F8F8F8 - static const bottomSheetBgColor = Color(0xFFF8F8FA); // #F8F8FA - static const lightGreyEFColor = Color(0xffeaeaff); // #EAEAFF - static const greyF7Color = Color(0xffF7F7F7); // #F7F7F7 - static const greyInfoTextColor = Color(0xff777777); // #777777 - static const lightGrayColor = Color(0xff808080); // #808080 - static const greyTextColorLight = Color(0xFFA2A2A2); // #A2A2A2 - -// New UI Colors - static const whiteColor = Color(0xFFffffff); // #FFFFFF - static const Color bgScaffoldColor = Color(0xffF8F8F8); // #F8F8F8 - static const Color primaryRedColor = Color(0xFFED1C2B); // #ED1C2B - static const Color primaryRedBorderColor = Color(0xFFED1C2B); // #ED1C2B - static const Color secondaryLightRedColor = Color(0xFFFEE9EA); // #FEE9EA - static const Color secondaryLightRedBorderColor = Color(0xFFFEE9EA); // #FEE9EA - static const Color bgRedLightColor = Color(0xFFFEE9EA); // #FEE9EA - static const Color bgGreenColor = Color(0xFF18C273); // #18C273 - static const Color textColor = Color(0xFF2E3039); // #2E3039 - static const Color borderGrayColor = Color(0x332E3039); // #2E3039 (20% opacity) - static const Color textColorLight = Color(0xFF5E5E5E); // #5E5E5E - static const Color borderOnlyColor = Color(0xFF2E3039); // #2E3039 - static const Color chipBorderColorOpacity20 = Color(0x332E3039); // #2E3039 (20% opacity) - static const Color dividerColor = Color(0x40D2D2D2); // #D2D2D2 (25% opacity) - static const Color warningColorYellow = Color(0xFFF4A308); // #F4A308 - static const Color blackBgColor = Color(0xFF2E3039); // #2E3039 - static const blackColor = textColor; // #2E3039 - static const Color inputLabelTextColor = Color(0xff898A8D); // #898A8D - static const Color greyTextColor = Color(0xFF8F9AA3); // #8F9AA3 - static const Color lightGrayBGColor = Color(0x142E3039); // #2E3039 (8% opacity) - static const Color checkBoxBorderColor = Color(0xffD2D2D2); // #D2D2D2 - - static const Color pharmacyBGColor = Color(0xFF359846); // #359846 - - static const lightGreenColor = Color(0xFF0ccedde); // #0CCEDDE - static const textGreenColor = Color(0xFF18C273); // #18C273 - static const Color ratingColorYellow = Color(0xFFFFAF15); // #FFAF15 - static const Color spacerLineColor = Color(0x2E30391A); // #2E3039 (10% opacity) - - //Chips - static const Color successColor = Color(0xff18C273); // #18C273 - static const Color successLightBgColor = Color(0xffDDF6EA); // #DDF6EA - static const Color errorColor = Color(0xFFED1C2B); // #ED1C2B - static const Color alertColor = Color(0xFFD48D05); // #D48D05 - static const Color infoColor = Color(0xFF0B85F7); // #0B85F7 - static const Color warningColor = Color(0xFFFFCC00); // #FFCC00 - static const Color greyColor = Color(0xFFEFEFF0); // #EFEFF0 - static const Color chipPrimaryRedBorderColor = Color(0xFFED1C2B); // #ED1C2B - static const Color chipSecondaryLightRedColor = Color(0xFFFEE9EA); // #FEE9EA - // static const Color chipSecondaryLightRedColor = Color(0xFFFF9E15); // #FEE9EA - - static const Color successLightColor = Color(0xFF18C273); // #18C273 - static const Color errorLightColor = Color(0xFFED1C2B); // #ED1C2B - static const Color alertLightColor = Color(0xFFD48D05); // #D48D05 - static const Color infoLightColor = Color(0xFF0B85F7); // #0B85F7 - static const Color warningLightColor = Color(0xFFFFCC00); // #FFCC00 - static const Color greyLightColor = Color(0xFFEFEFF0); // #EFEFF0 - static const Color thumbColor = Color(0xFF18C273); // #18C273 - static const Color switchBackgroundColor = Color(0x2618C273); // #18C273 (15% opacity) - - static const Color bottomNAVBorder = Color(0xFFEEEEEE); // #EEEEEE - - static const Color quickLoginColor = Color(0xFF666666); // #666666 - - static const Color tooltipTextColor = Color(0xFF414D55); // #414D55 - static const Color graphGridColor = Color(0x4D18C273); // #18C273 (30% opacity) - static const Color criticalLowAndHigh = Color(0xFFED1C2B); // #ED1C2B - static const Color highAndLow = Color(0xFFFFAF15); // #FFAF15 - static const Color labelTextColor = Color(0xFF838383); // #838383 - static const Color calenderTextColor = Color(0xFFD0D0D0); // #D0D0D0 - static const Color lightGreenButtonColor = Color(0x2618C273); // #18C273 (15% opacity) - - static const Color lightRedButtonColor = Color(0x1AED1C2B); // #ED1C2B (10% opacity) - - // Status Colors - static const Color statusPendingColor = Color(0xffCC9B14); // #CC9B14 - static const Color statusProcessingColor = Color(0xff2E303A); // #2E303A - static const Color statusCompletedColor = Color(0xff359846); // #359846 - static const Color statusRejectedColor = Color(0xffD02127); // #D02127 - - // Info Banner Colors - static const Color infoBannerBgColor = Color(0xFFFFF4E6); // #FFF4E6 - static const Color infoBannerBorderColor = Color(0xFFFFE5B4); // #FFE5B4 - static const Color infoBannerIconColor = Color(0xFFCC9B14); // #CC9B14 - static const Color infoBannerTextColor = Color(0xFF856404); // #856404 - - // SymptomsChecker - - static const Color chipColorSeekMedicalAdvice = Color(0xFFFFAF15); // #FFAF15 - static const Color chipTextColorSeekMedicalAdvice = Color(0xFFD48D05); // #AB7103 - static const Color chipColorMonitor = Color(0xFF18C273); // #18C273 - static const Color chipColorEmergency = Color(0xFFED1C2B); // #ED1C2B - - // Services Page Colors - static const Color eReferralCardColor = Color(0xFFFF8012); // #FF8012 - static const Color bloodDonationCardColor = Color(0xFFFF5662); // #FF5662 - static const Color myChildVaccineCardColor = Color(0xFFFF2D78); // #FF5662 - static const Color bookAppointment = Color(0xFF415364); // #415364 + + // ── Scaffold / Background ───────────────────────────────────────────────── + static Color get scaffoldBgColor => isDarkMode ? dark.scaffoldBgColor : const Color(0xFFF8F8F8); + static Color get bottomSheetBgColor => isDarkMode ? dark.bottomSheetBgColor : const Color(0xFFF8F8FA); + static Color get lightGreyEFColor => isDarkMode ? dark.lightGreyEFColor : const Color(0xffeaeaff); + static Color get greyF7Color => isDarkMode ? dark.greyF7Color : const Color(0xffF7F7F7); + static Color get greyInfoTextColor => isDarkMode ? dark.greyInfoTextColor : const Color(0xff777777); + static Color get lightGrayColor => isDarkMode ? dark.lightGrayColor : const Color(0xff808080); + static Color get greyTextColorLight => isDarkMode ? dark.greyTextColorLight : const Color(0xFFA2A2A2); + static Color get mainPurple => isDarkMode ? dark.mainPurple : const Color(0xFF7954F7); + + // ── Core Colors ─────────────────────────────────────────────────────────── + static Color get whiteColor => isDarkMode ? dark.whiteColor : const Color(0xFFffffff); + // static const Color primaryRedColor = Color(0xFFED1C2B); + static Color get primaryRedColor => isDarkMode ? dark.primaryRedColor : Color(0xFFED1C2B); + static Color get secondaryLightRedColor => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); + static const Color successColor = Color(0xFF18C273); + static Color get successLightBgColor => isDarkMode ? dark.successLightBgColor : const Color(0xffDDF6EA); + static Color get greyColor => isDarkMode ? dark.greyColor : const Color(0xFFEFEFF0); + static Color get chipBgColor => isDarkMode ? dark.chipBgColor : const Color(0xFFEFEFF0); + static Color get textColor => isDarkMode ? dark.textColor : const Color(0xFF2E3039); + static Color get iconColor => isDarkMode ? dark.iconColor : const Color(0xFF2E3039); + static Color get borderGrayColor => isDarkMode ? dark.borderGrayColor : const Color(0x332E3039); + static Color get textColorLight => isDarkMode ? dark.textColorLight : const Color(0xFF5E5E5E); + static Color get dividerColor => isDarkMode ? dark.dividerColor : const Color(0x40D2D2D2); + static Color get warningColorYellow => isDarkMode ? dark.warningColorYellow : const Color(0xFFF4A308); + static Color get inputLabelTextColor => isDarkMode ? dark.inputLabelTextColor : const Color(0xff898A8D); + static Color get greyTextColor => isDarkMode ? dark.greyTextColor : const Color(0xFF8F9AA3); + static Color get lightGrayBGColor => isDarkMode ? dark.lightGrayBGColor : const Color(0x142E3039); + static Color get checkBoxBorderColor => isDarkMode ? dark.checkBoxBorderColor : const Color(0xffD2D2D2); + static Color get pharmacyBGColor => isDarkMode ? dark.pharmacyBGColor : const Color(0xFF359846); + static Color get lightGreenColor => isDarkMode ? dark.lightGreenColor : const Color(0xFF0ccedde); + static Color get ratingColorYellow => isDarkMode ? dark.ratingColorYellow : const Color(0xFFFFAF15); + static Color get spacerLineColor => isDarkMode ? dark.spacerLineColor : const Color(0x2E30391A); + + // ── Semantic / Status Colors ────────────────────────────────────────────── + static Color get errorColor => isDarkMode ? dark.errorColor : const Color(0xFFED1C2B); + static Color get alertColor => isDarkMode ? dark.alertColor : const Color(0xFFD48D05); + static Color get infoColor => isDarkMode ? dark.infoColor : const Color(0xFF0B85F7); + static Color get warningColor => isDarkMode ? dark.warningColor : const Color(0xFFFFCC00); + static Color get switchBackgroundColor => isDarkMode ? dark.switchBackgroundColor : const Color(0x2618C273); + static Color get bottomNAVBorder => isDarkMode ? dark.bottomNAVBorder : const Color(0xFFEEEEEE); + static Color get quickLoginColor => isDarkMode ? dark.quickLoginColor : const Color(0xFF666666); + static Color get tooltipTextColor => isDarkMode ? dark.tooltipTextColor : const Color(0xFF414D55); + static Color get graphGridColor => isDarkMode ? dark.graphGridColor : const Color(0x4D18C273); + static Color get highAndLow => isDarkMode ? dark.highAndLow : const Color(0xFFFFAF15); + static Color get labelTextColor => isDarkMode ? dark.labelTextColor : const Color(0xFF838383); + static Color get calenderTextColor => isDarkMode ? dark.calenderTextColor : const Color(0xFFD0D0D0); + static Color get lightGreenButtonColor => isDarkMode ? dark.lightGreenButtonColor : const Color(0x2618C273); + static Color get lightRedButtonColor => isDarkMode ? dark.lightRedButtonColor : const Color(0x1AED1C2B); + + // ── Status Colors ───────────────────────────────────────────────────────── + static Color get statusPendingColor => isDarkMode ? dark.statusPendingColor : const Color(0xffCC9B14); + static Color get statusProcessingColor => isDarkMode ? dark.statusProcessingColor : const Color(0xff2E303A); + static Color get statusCompletedColor => isDarkMode ? dark.statusCompletedColor : const Color(0xff359846); + static Color get statusRejectedColor => isDarkMode ? dark.statusRejectedColor : const Color(0xffD02127); + + // ── Info Banner Colors ──────────────────────────────────────────────────── + static Color get infoBannerBgColor => isDarkMode ? dark.infoBannerBgColor : const Color(0xFFFFF4E6); + static Color get infoBannerBorderColor => isDarkMode ? dark.infoBannerBorderColor : const Color(0xFFFFE5B4); + static Color get infoBannerIconColor => isDarkMode ? dark.infoBannerIconColor : const Color(0xFFCC9B14); + static Color get infoBannerTextColor => isDarkMode ? dark.infoBannerTextColor : const Color(0xFF856404); + + // ── Symptoms Checker ─────────────────────────────────────────────��──────── + static Color get chipColorSeekMedicalAdvice => isDarkMode ? dark.chipColorSeekMedicalAdvice : const Color(0xFFFFAF15); + static Color get chipTextColorSeekMedicalAdvice => isDarkMode ? dark.chipTextColorSeekMedicalAdvice : const Color(0xFFD48D05); + + // ── Services Page Colors ────────────────────────────────────────────────── + static Color get eReferralCardColor => isDarkMode ? dark.eReferralCardColor : const Color(0xFFFF8012); + static Color get bloodDonationCardColor => isDarkMode ? dark.bloodDonationCardColor : const Color(0xFFFF5662); + static Color get myChildVaccineCardColor => isDarkMode ? dark.myChildVaccineCardColor : const Color(0xFFFF2D78); + static Color get bookAppointment => isDarkMode ? dark.bookAppointment : const Color(0xFF415364); + + // ── Water Monitor ───────────────────────────────────────────────────────── + static Color get blueColor => isDarkMode ? dark.blueColor : const Color(0xFF4EB5FF); + static Color get blueGradientColorOne => isDarkMode ? dark.blueGradientColorOne : const Color(0xFFF1F7FD); + static Color get blueGradientColorTwo => isDarkMode ? dark.blueGradientColorTwo : const Color(0xFFD9EFFF); + + // ── Shimmer ─────────────────────────────────────────────────────────────── + static Color get shimmerBaseColor => isDarkMode ? dark.shimmerBaseColor : const Color(0xFFE0E0E0); + static Color get shimmerHighlightColor => isDarkMode ? dark.shimmerHighlightColor : const Color(0xFFF5F5F5); + static Color get covid29Color => isDarkMode ? dark.covid29Color : const Color(0xff2563EB); + static Color get lightGreyTextColor => isDarkMode ? dark.lightGreyTextColor : const Color(0xFF959595); + static Color get labelColorYellow => isDarkMode ? dark.labelColorYellow : const Color(0xFFFBCB6E); + + static const LinearGradient aiLinearGradient = + LinearGradient(colors: [Color(0xFF8A38F5), Color(0xFFE20BBB)], begin: Alignment.topLeft, end: Alignment.bottomRight); + + // ── Aliases ─────────────────────────────────────────────────────────────── + static Color get bgScaffoldColor => scaffoldBgColor; + static Color get primaryRedBorderColor => primaryRedColor; + static Color get chipPrimaryRedBorderColor => primaryRedColor; + static Color get criticalLowAndHigh => primaryRedColor; + static Color get chipColorEmergency => primaryRedColor; + static Color get errorLightColor => primaryRedColor; + static Color get secondaryLightRedBorderColor => secondaryLightRedColor; + static Color get bgRedLightColor => secondaryLightRedColor; + static Color get chipSecondaryLightRedColor => secondaryLightRedColor; + static Color get blackBgColor => textColor; + static Color get blackColor => textColor; + static Color get borderOnlyColor => textColor; + static Color get chipBorderColorOpacity20 => borderGrayColor; + static Color get bgGreenColor => successColor; + static Color get textGreenColor => successColor; + static Color get successLightColor => successColor; + static Color get thumbColor => successColor; + static Color get chipColorMonitor => successColor; + static Color get greyLightColor => greyColor; + static Color get alertLightColor => alertColor; + static Color get infoLightColor => infoColor; + static Color get warningLightColor => warningColor; + + // ── Dark Mode Palette ───────────────────────────────────────────────────── + static const AppColorsDark dark = AppColorsDark(); +} + +// --------------------------------------------------------------------------- +// Dark palette — mirrors every color in AppColors with a dark-friendly value. +// --------------------------------------------------------------------------- +class AppColorsDark { + const AppColorsDark(); + + // Scaffold / Background + Color get scaffoldBgColor => const Color(0xFF191919); + Color get bottomSheetBgColor => const Color(0xFF1C1C1E); + Color get lightGreyEFColor => const Color(0xFF2A2A3A); + Color get greyF7Color => const Color(0xFF1E1E1E); + Color get greyInfoTextColor => const Color(0xFF9E9E9E); + Color get lightGrayColor => const Color(0xFF9E9E9E); + Color get greyTextColorLight => const Color(0xFF757575); + + // ⚠️ NEW — was missing from AppColorsDark + Color get mainPurple => const Color(0xFF9B7AFF); // brighter purple for dark surfaces + + // Core + Color get baseColor => const Color(0xFF1E1E1E); + // ⚠️ NEW — maps whiteColor → dark surface color + Color get whiteColor => const Color(0xFF1E1E1E); // surface replacement for white + Color get primaryRedColor => const Color(0xFFDE5C5D); + Color get secondaryLightRedColor => const Color(0xFF3A1015); + Color get successColor => const Color(0xFF18C273); + Color get successLightBgColor => const Color(0xFF0D2E1F); + Color get textColor => const Color(0xFFFFFFFF); + Color get iconColor => const Color(0xFFFFFFFF); + Color get borderGrayColor => const Color(0x55ECECEC); + Color get textColorLight => const Color(0xFFB0B0B0); + Color get dividerColor => const Color(0x33FFFFFF); + Color get warningColorYellow => const Color(0xFFF4A308); + Color get inputLabelTextColor => const Color(0xFF9E9E9E); + Color get greyTextColor => const Color(0xFF8F9AA3); + Color get lightGrayBGColor => const Color(0x22FFFFFF); + Color get checkBoxBorderColor => const Color(0xFF555555); + Color get pharmacyBGColor => const Color(0xFF359846); + // ⚠️ NEW — was missing from AppColorsDark + Color get lightGreenColor => const Color(0xFF0ccedde); // same teal accent — works on dark + Color get ratingColorYellow => const Color(0xFFFFAF15); + Color get spacerLineColor => const Color(0x22FFFFFF); + + // Semantic / Status + Color get errorColor => const Color(0xFFD63D48); + Color get alertColor => const Color(0xFFD48D05); + Color get infoColor => const Color(0xFF4DA6FF); + Color get warningColor => const Color(0xFFFFCC00); + Color get greyColor => const Color(0xFF2C2C2E); + Color get chipBgColor => const Color(0xFF3E3E3E); + Color get cardDarkBorderColor => const Color(0x0DEAEAEA); + Color get serviceCardButtonColor => const Color(0xFF3E3E3E); + Color get switchBackgroundColor => const Color(0x2618C273); + Color get bottomNAVBorder => const Color(0xFF2C2C2E); + Color get quickLoginColor => const Color(0xFF9E9E9E); + Color get tooltipTextColor => const Color(0xFFCFD8DC); + Color get graphGridColor => const Color(0x4D18C273); + Color get highAndLow => const Color(0xFFFFAF15); + Color get labelTextColor => const Color(0xFF9E9E9E); + Color get calenderTextColor => const Color(0xFF555555); + Color get lightGreenButtonColor => const Color(0x2618C273); + Color get lightRedButtonColor => const Color(0x1AED1C2B); + + // Status + Color get statusPendingColor => const Color(0xFFCC9B14); + Color get statusProcessingColor => const Color(0xFFECECEC); + Color get statusCompletedColor => const Color(0xFF359846); + Color get statusRejectedColor => const Color(0xFFD02127); + + // Info Banner + Color get infoBannerBgColor => const Color(0xFF2A1F0A); + Color get infoBannerBorderColor => const Color(0xFF5A3D0A); + Color get infoBannerIconColor => const Color(0xFFCC9B14); + Color get infoBannerTextColor => const Color(0xFFFFCB6B); + + // Symptoms Checker + Color get chipColorSeekMedicalAdvice => const Color(0xFFFFAF15); + Color get chipTextColorSeekMedicalAdvice => const Color(0xFFD48D05); + + // Services Page + Color get eReferralCardColor => const Color(0xFFFF8012); + Color get bloodDonationCardColor => const Color(0xFFFF5662); + Color get myChildVaccineCardColor => const Color(0xFFFF2D78); + Color get bookAppointment => const Color(0xFF415364); + + // Water Monitor + Color get blueColor => const Color(0xFF4EB5FF); + Color get blueGradientColorOne => const Color(0xFF101C2A); + Color get blueGradientColorTwo => const Color(0xFF0D2235); + + // Shimmer + Color get shimmerBaseColor => const Color(0xFF2C2C2C); + Color get shimmerHighlightColor => const Color(0xFF3D3D3D); + Color get covid29Color => const Color(0xFF2563EB); + Color get lightGreyTextColor => const Color(0xFF757575); + Color get labelColorYellow => const Color(0xFFFBCB6E); + + // Aliases + Color get bgScaffoldColor => scaffoldBgColor; + Color get primaryRedBorderColor => primaryRedColor; + Color get chipPrimaryRedBorderColor => primaryRedColor; + Color get criticalLowAndHigh => primaryRedColor; + Color get chipColorEmergency => primaryRedColor; + Color get errorLightColor => primaryRedColor; + Color get secondaryLightRedBorderColor => secondaryLightRedColor; + Color get bgRedLightColor => secondaryLightRedColor; + Color get chipSecondaryLightRedColor => secondaryLightRedColor; + Color get blackBgColor => textColor; + Color get blackColor => textColor; + Color get borderOnlyColor => textColor; + Color get chipBorderColorOpacity20 => borderGrayColor; + Color get bgGreenColor => successColor; + Color get textGreenColor => successColor; + Color get successLightColor => successColor; + Color get thumbColor => successColor; + Color get chipColorMonitor => successColor; + Color get greyLightColor => greyColor; + Color get alertLightColor => alertColor; + Color get infoLightColor => infoColor; + Color get warningLightColor => warningColor; +} + +// --------------------------------------------------------------------------- +// BuildContext extension — resolves the right palette based on theme brightness +// --------------------------------------------------------------------------- +extension AppColorsContext on BuildContext { + bool get _isDark => Theme.of(this).brightness == Brightness.dark; + + // Scaffold / Background + Color get scaffoldBgColor => _isDark ? AppColors.dark.scaffoldBgColor : const Color(0xFFF8F8F8); + Color get bottomSheetBgColor => _isDark ? AppColors.dark.bottomSheetBgColor : const Color(0xFFF8F8FA); + Color get greyF7Color => _isDark ? AppColors.dark.greyF7Color : const Color(0xffF7F7F7); + Color get greyInfoTextColor => _isDark ? AppColors.dark.greyInfoTextColor : const Color(0xff777777); + Color get lightGrayColor => _isDark ? AppColors.dark.lightGrayColor : const Color(0xff808080); + + // Core + Color get cardBaseColor => _isDark ? AppColors.dark.baseColor : const Color(0xFFFFFFFF); + Color get primaryRedColor => _isDark ? AppColors.dark.primaryRedColor : AppColors.primaryRedColor; + Color get secondaryLightRedColor => _isDark ? AppColors.dark.serviceCardButtonColor : const Color(0xFFFEE9EA); + Color get secondaryLightRedForegroundColor => _isDark ? const Color(0xFFFFFFFF) : AppColors.primaryRedColor; + Color get successColor => AppColors.successColor; + Color get successLightBgColor => _isDark ? AppColors.dark.successLightBgColor : const Color(0xffDDF6EA); + Color get textColor => _isDark ? AppColors.dark.textColor : const Color(0xFF2E3039); + Color get iconColor => _isDark ? AppColors.dark.iconColor : const Color(0xFF2E3039); + Color get borderGrayColor => _isDark ? AppColors.dark.borderGrayColor : const Color(0x332E3039); + Color get textColorLight => _isDark ? AppColors.dark.textColorLight : const Color(0xFF5E5E5E); + Color get dividerColor => _isDark ? AppColors.dark.dividerColor : const Color(0x40D2D2D2); + Color get inputLabelTextColor => _isDark ? AppColors.dark.inputLabelTextColor : const Color(0xff898A8D); + Color get greyTextColor => AppColors.greyTextColor; + Color get lightGrayBGColor => _isDark ? AppColors.dark.lightGrayBGColor : const Color(0x142E3039); + Color get checkBoxBorderColor => _isDark ? AppColors.dark.checkBoxBorderColor : const Color(0xffD2D2D2); + + // Status / Semantic + Color get errorColor => _isDark ? AppColors.dark.errorColor : const Color(0xFFED1C2B); + Color get alertColor => AppColors.alertColor; + Color get infoColor => _isDark ? AppColors.dark.infoColor : const Color(0xFF0B85F7); + Color get warningColor => AppColors.warningColor; + Color get greyColor => _isDark ? AppColors.dark.greyColor : const Color(0xFFEFEFF0); + Color get chipBgColor => _isDark ? AppColors.dark.chipBgColor : const Color(0xFFEFEFF0); + Color get cardDarkBorderColor => _isDark ? AppColors.dark.cardDarkBorderColor : Colors.transparent; + Color get serviceCardButtonColor => _isDark ? AppColors.dark.serviceCardButtonColor : const Color(0xFF2E3039); + Color get bottomNAVBorder => _isDark ? AppColors.dark.bottomNAVBorder : const Color(0xFFEEEEEE); + Color get labelTextColor => _isDark ? AppColors.dark.labelTextColor : const Color(0xFF838383); + Color get lightGreyTextColor => _isDark ? AppColors.dark.lightGreyTextColor : const Color(0xFF959595); + + // Info Banner + Color get infoBannerBgColor => _isDark ? AppColors.dark.infoBannerBgColor : const Color(0xFFFFF4E6); + Color get infoBannerBorderColor => _isDark ? AppColors.dark.infoBannerBorderColor : const Color(0xFFFFE5B4); + Color get infoBannerIconColor => AppColors.infoBannerIconColor; + Color get infoBannerTextColor => _isDark ? AppColors.dark.infoBannerTextColor : const Color(0xFF856404); // Water Monitor - static const Color blueColor = Color(0xFF4EB5FF); // #4EB5FF - static const Color blueGradientColorOne = Color(0xFFF1F7FD); // #F1F7FD - static const Color blueGradientColorTwo = Color(0xFFD9EFFF); // #D9EFFF + Color get blueColor => AppColors.blueColor; + Color get blueGradientColorOne => _isDark ? AppColors.dark.blueGradientColorOne : const Color(0xFFF1F7FD); + Color get blueGradientColorTwo => _isDark ? AppColors.dark.blueGradientColorTwo : const Color(0xFFD9EFFF); // Shimmer - static const Color shimmerBaseColor = Color(0xFFE0E0E0); // #E0E0E0 - static const Color shimmerHighlightColor = Color(0xFFF5F5F5); // #F5F5F5 - static const Color covid29Color = Color(0xff2563EB); // #2563EB - static const Color lightGreyTextColor = Color(0xFF959595); - static const Color labelColorYellow = Color(0xFFFBCB6E); + Color get shimmerBaseColor => _isDark ? AppColors.dark.shimmerBaseColor : const Color(0xFFE0E0E0); + Color get shimmerHighlightColor => _isDark ? AppColors.dark.shimmerHighlightColor : const Color(0xFFF5F5F5); static const Color tooltipColor = Color(0xFF1AACACAC); - static const LinearGradient aiLinearGradient = LinearGradient(colors: [Color(0xFF8A38F5), Color(0xFFE20BBB)], begin: Alignment.topLeft, end: Alignment.bottomRight); + // Aliases + Color get bgScaffoldColor => scaffoldBgColor; + Color get bgGreenColor => successColor; + Color get bgRedLightColor => secondaryLightRedColor; + Color get blackColor => textColor; + Color get blackBgColor => textColor; } diff --git a/lib/widgets/appbar/app_bar_widget.dart b/lib/widgets/appbar/app_bar_widget.dart index 942a93f2..05db7724 100644 --- a/lib/widgets/appbar/app_bar_widget.dart +++ b/lib/widgets/appbar/app_bar_widget.dart @@ -57,7 +57,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { ), // Logo - if (!hideLogoAndLang) Utils.buildSvgWithAssets(icon: AppAssets.habiblogo, height: 24.h), + if (!hideLogoAndLang) Utils.buildSvgWithAssets(icon: AppAssets.habiblogo, height: 24.h, applyThemeColor: false), if (!hideLogoAndLang) Expanded( diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index 25221962..0c32d87d 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -25,6 +25,7 @@ class CollapsingListView extends StatelessWidget { VoidCallback? requests; VoidCallback? sendEmail; VoidCallback? doctorResponse; + VoidCallback? downloadReport; Widget? bottomChild; Widget? trailing; bool isClose; @@ -47,6 +48,7 @@ class CollapsingListView extends StatelessWidget { this.requests, this.sendEmail, this.doctorResponse, + this.downloadReport, this.isLeading = true, this.trailing, this.leadingCallback, @@ -89,6 +91,7 @@ class CollapsingListView extends StatelessWidget { requests: requests, sendEmail: sendEmail, doctorResponse: doctorResponse, + downloadReport: downloadReport, bottomChild: bottomChild, trailing: trailing, aiOverview: aiOverview, @@ -200,6 +203,7 @@ class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget VoidCallback? requests; VoidCallback? sendEmail; VoidCallback? doctorResponse; + VoidCallback? downloadReport; Widget? bottomChild; Widget? trailing; @@ -217,6 +221,7 @@ class ScrollAnimatedTitle extends StatefulWidget implements PreferredSizeWidget this.requests, this.sendEmail, this.doctorResponse, + this.downloadReport, this.bottomChild, this.trailing, }); @@ -295,6 +300,7 @@ class _ScrollAnimatedTitleState extends State { if (widget.doctorResponse != null) actionButton(context, t, title: LocaleKeys.doctorResponses.tr(context: context), icon: AppAssets.doctorResponseIcon).onPress(widget.doctorResponse!), if (widget.search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(widget.search!), if (widget.aiOverview != null) actionButton(context, t, title: LocaleKeys.aiOverView.tr(context: context), icon: AppAssets.aiOverView, isAiButton: true).onPress(widget.aiOverview!), + if (widget.downloadReport != null) actionButton(context, t, title: LocaleKeys.downloadReport.tr(context: context), icon: AppAssets.download).onPress(widget.downloadReport!), if (widget.trailing != null) widget.trailing!, ] ], diff --git a/lib/widgets/appbar/collapsing_toolbar.dart b/lib/widgets/appbar/collapsing_toolbar.dart index 8bf1e4b4..9b1d4854 100644 --- a/lib/widgets/appbar/collapsing_toolbar.dart +++ b/lib/widgets/appbar/collapsing_toolbar.dart @@ -96,7 +96,7 @@ class _CollapsingToolbarState extends State { ? Transform.flip( flipX: appState.isArabic(), child: IconButton( - icon: Utils.buildSvgWithAssets(icon: widget.isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h), + icon: Utils.buildSvgWithAssets(icon: widget.isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h, applyThemeColor: false), padding: EdgeInsets.only(left: 12), onPressed: () => Navigator.pop(context), highlightColor: Colors.transparent, diff --git a/lib/widgets/arrow_back.dart b/lib/widgets/arrow_back.dart index cf3e52ef..4446ea9b 100644 --- a/lib/widgets/arrow_back.dart +++ b/lib/widgets/arrow_back.dart @@ -3,12 +3,13 @@ import 'package:flutter/material.dart'; class ArrowBack extends StatelessWidget { final Function? onTap; - final Color color; + final Color? color; - ArrowBack({Key? key, this.onTap, this.color = AppColors.whiteColor}) : super(key: key); + ArrowBack({Key? key, this.onTap, this.color}) : super(key: key); @override Widget build(BuildContext context) { + final resolvedColor = color ?? AppColors.whiteColor; // ProjectViewModel projectViewModel = Provider.of(context); return GestureDetector( behavior: HitTestBehavior.opaque, @@ -17,7 +18,7 @@ class ArrowBack extends StatelessWidget { }, context), child: Icon( Icons.arrow_back_ios, - color: color, + color: resolvedColor, ), ); } diff --git a/lib/widgets/bottom_navigation/bottom_navigation.dart b/lib/widgets/bottom_navigation/bottom_navigation.dart index 46067dfe..9d390388 100644 --- a/lib/widgets/bottom_navigation/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation/bottom_navigation.dart @@ -28,9 +28,7 @@ class BottomNavigation extends StatelessWidget { icon: AppAssets.bookAppoBottom, fillIcon: AppAssets.bookAppoBottom, label: LocaleKeys.appointment.tr(context: context), - iconSize: 32, - isSpecial: true, - ), + iconSize: 32, isSpecial: true), appState.isAuthenticated // ? BottomNavItem(icon: AppAssets.toDoBottom, label: LocaleKeys.todoList.tr(context: context)) ? BottomNavItem(icon: AppAssets.symptomCheckerBottomIcon, fillIcon: AppAssets.symptomCheckerBottomFillIcon, label: LocaleKeys.symptoms.tr(context: context)) @@ -39,8 +37,16 @@ class BottomNavigation extends StatelessWidget { ]; return Container( - decoration: _containerDecoration, - padding: _containerPadding, + decoration: BoxDecoration( + color: AppColors.whiteColor, + border: Border( + top: BorderSide( + color: AppColors.bottomNAVBorder, + width: 0.5, + ), + ), + ), + padding: const EdgeInsets.all(15), // height: 84.h, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -69,6 +75,7 @@ class BottomNavigation extends StatelessWidget { icon: isSelected ? item.fillIcon : item.icon, height: item.iconSize.h, width: item.iconSize.w, + applyThemeColor: !item.isSpecial // iconColor: isSelected ? Colors.black87 : Colors.black87, ), // const SizedBox(height: 10), @@ -100,14 +107,3 @@ class BottomNavItem { }); } -// Constants -const EdgeInsets _containerPadding = EdgeInsets.all(15); -const BoxDecoration _containerDecoration = BoxDecoration( - color: Colors.white, - border: Border( - top: BorderSide( - color: AppColors.bottomNAVBorder, - width: 0.5, - ), - ), -); diff --git a/lib/widgets/bottom_sheet.dart b/lib/widgets/bottom_sheet.dart index 4aef0efa..ee70782b 100644 --- a/lib/widgets/bottom_sheet.dart +++ b/lib/widgets/bottom_sheet.dart @@ -1,5 +1,6 @@ import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; void showMyBottomSheet(BuildContext context, {required Widget child, required VoidCallback callBackFunc, String? type}) { showModalBottomSheet( @@ -10,8 +11,8 @@ void showMyBottomSheet(BuildContext context, {required Widget child, required Vo return Container( constraints: BoxConstraints( maxHeight: type =='CONTINUE_ACTION' ? MediaQuery.of(context).size.height *.75 : double.infinity,), - decoration: const BoxDecoration( - color: Colors.white, + decoration: BoxDecoration( + color: AppColors.whiteColor, borderRadius: BorderRadius.only( topRight: Radius.circular(25), topLeft: Radius.circular(25), diff --git a/lib/widgets/bottomsheet/exception_bottom_sheet.dart b/lib/widgets/bottomsheet/exception_bottom_sheet.dart index 16531bdb..ea5f8d83 100644 --- a/lib/widgets/bottomsheet/exception_bottom_sheet.dart +++ b/lib/widgets/bottomsheet/exception_bottom_sheet.dart @@ -47,7 +47,7 @@ class _ExceptionBottomSheetState extends State { child: Container( padding: EdgeInsets.all(24.h), decoration: BoxDecoration( - color: Color(0xFFF8F8FA), + color: AppColors.whiteColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index 8500ce8e..de3b598a 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -25,6 +25,7 @@ class CustomButton extends StatelessWidget { final double? iconSize; final TextOverflow? textOverflow; final BorderSide? borderSide; + final bool applyThemeColor; const CustomButton({ super.key, @@ -47,6 +48,7 @@ class CustomButton extends StatelessWidget { this.iconSize, this.textOverflow, this.borderSide, + this.applyThemeColor = true, }); @override @@ -76,7 +78,7 @@ class CustomButton extends StatelessWidget { if (icon != null) Padding( padding: text.isNotEmpty ? EdgeInsets.only(right: 4.w, left: 4.w) : EdgeInsets.zero, - child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isDisabled, width: iconS, height: iconS), + child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isDisabled, width: iconS, height: iconS, applyThemeColor: applyThemeColor), ), if (text.isNotEmpty) Text( diff --git a/lib/widgets/buttons/default_button.dart b/lib/widgets/buttons/default_button.dart index d8d8cacd..f6a47a1c 100644 --- a/lib/widgets/buttons/default_button.dart +++ b/lib/widgets/buttons/default_button.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; extension WithContainer on Widget { Widget get insideContainer => Container( - color: Colors.white, + color: AppColors.whiteColor, padding: const EdgeInsets.only(top: 16, bottom: 16, right: 21, left: 21), child: this, ); diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index 8597b3ed..77345933 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -9,23 +9,24 @@ class AppCustomChipWidget extends StatelessWidget { const AppCustomChipWidget({ super.key, this.labelText, - this.textColor = AppColors.textColor, - this.backgroundColor = AppColors.greyColor, + this.textColor, + this.backgroundColor, this.iconSize, this.icon = "", - this.iconColor = AppColors.textColor, + this.iconColor, this.richText, this.iconHasColor = true, this.shape, this.deleteIcon, this.deleteIconSize, - this.deleteIconColor = AppColors.textColor, + this.deleteIconColor, this.deleteIconHasColor = false, this.padding = EdgeInsets.zero, this.isIconPNG = false, this.onChipTap, this.labelPadding, this.onDeleteTap, + this.applyThemeColor = true, }); final String? labelText; @@ -36,7 +37,7 @@ class AppCustomChipWidget extends StatelessWidget { final String icon; final String? deleteIcon; final Size? deleteIconSize; - final Color iconColor; + final Color? iconColor; final Color? deleteIconColor; final bool iconHasColor; final bool deleteIconHasColor; @@ -46,10 +47,15 @@ class AppCustomChipWidget extends StatelessWidget { final bool isIconPNG; final void Function()? onChipTap; final void Function()? onDeleteTap; + final bool applyThemeColor; @override Widget build(BuildContext context) { final iconS = iconSize ?? 12.w; + final resolvedTextColor = textColor ?? AppColors.textColor; + final resolvedBackgroundColor = backgroundColor ?? AppColors.greyColor; + final resolvedIconColor = iconColor ?? AppColors.textColor; + final resolvedDeleteIconColor = deleteIconColor ?? AppColors.textColor; return GestureDetector( onTap: onChipTap, child: SizedBox( @@ -74,15 +80,16 @@ class AppCustomChipWidget extends StatelessWidget { icon: icon, width: iconS, height: iconS, - iconColor: iconHasColor ? iconColor : null, + iconColor: iconHasColor ? resolvedIconColor : null, fit: BoxFit.contain, + applyThemeColor: applyThemeColor, ) : SizedBox.shrink(), - label: richText ?? (labelText?? "").toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor), + label: richText ?? (labelText?? "").toText10(weight: FontWeight.w500, letterSpacing: 0, color: resolvedTextColor), padding: padding, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, labelPadding: labelPadding ?? EdgeInsetsDirectional.only(end: deleteIcon?.isNotEmpty == true ? 2.w : 8.w), - backgroundColor: backgroundColor, + backgroundColor: resolvedBackgroundColor, shape: shape ?? SmoothRectangleBorder( borderRadius: BorderRadius.circular(8.r), @@ -96,7 +103,8 @@ class AppCustomChipWidget extends StatelessWidget { icon: deleteIcon!, width: iconS, height: iconS, - iconColor: deleteIconHasColor ? deleteIconColor : null, + iconColor: deleteIconHasColor ? resolvedDeleteIconColor : null, + applyThemeColor: applyThemeColor, ), ) : null, @@ -104,9 +112,9 @@ class AppCustomChipWidget extends StatelessWidget { ) : Chip( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: richText ?? (labelText?? "").toText10(weight: FontWeight.w500, letterSpacing: 0, color: textColor, isCenter: true), + label: richText ?? (labelText?? "").toText10(weight: FontWeight.w500, letterSpacing: 0, color: resolvedTextColor, isCenter: true), padding: EdgeInsets.zero, - backgroundColor: backgroundColor, + backgroundColor: resolvedBackgroundColor, shape: shape ?? SmoothRectangleBorder( borderRadius: BorderRadius.circular(8.r), @@ -118,7 +126,7 @@ class AppCustomChipWidget extends StatelessWidget { ? InkWell( onTap: onDeleteTap, child: Utils.buildSvgWithAssets( - icon: deleteIcon!, width: iconS, height: iconS, iconColor: deleteIconHasColor ? deleteIconColor : null)) + icon: deleteIcon!, width: iconS, height: iconS, iconColor: deleteIconHasColor ? resolvedDeleteIconColor : null, applyThemeColor: applyThemeColor)) : null, onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, ), diff --git a/lib/widgets/chip/custom_selectable_chip.dart b/lib/widgets/chip/custom_selectable_chip.dart index be42bfd9..06ccb95f 100644 --- a/lib/widgets/chip/custom_selectable_chip.dart +++ b/lib/widgets/chip/custom_selectable_chip.dart @@ -9,7 +9,7 @@ class CustomSelectableChip extends StatelessWidget { final Color activeColor; final Color activeTextColor; final Color inactiveBorderColor; - final Color inactiveTextColor; + final Color? inactiveTextColor; const CustomSelectableChip({ super.key, @@ -19,11 +19,12 @@ class CustomSelectableChip extends StatelessWidget { this.activeColor = const Color(0xFFD03C32), // red accent this.activeTextColor = Colors.white, this.inactiveBorderColor = const Color(0xFFE8E8E8), - this.inactiveTextColor = const Color(0xFF222222), + this.inactiveTextColor, }); @override Widget build(BuildContext context) { + final resolvedInactiveTextColor = inactiveTextColor ?? AppColors.textColor; final double radius = 8.0; return AnimatedContainer( duration: const Duration(milliseconds: 180), @@ -44,7 +45,7 @@ class CustomSelectableChip extends StatelessWidget { style: TextStyle( fontSize: 12.f, fontWeight: FontWeight.w500, - color: selected ? activeTextColor : inactiveTextColor, + color: selected ? activeTextColor : resolvedInactiveTextColor, letterSpacing: -0.02 * 18, height: 1.0, ), diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index 515d1f72..5269a8dc 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -134,7 +134,7 @@ class ButtonSheetContent extends StatelessWidget { padding: EdgeInsets.symmetric( horizontal: 16, ), - child: Utils.buildSvgWithAssets(icon: AppAssets.closeBottomNav, width: 32, height: 32).onPress(() { + child: Utils.buildSvgWithAssets(icon: AppAssets.closeBottomNav, width: 32, height: 32, applyThemeColor: false).onPress(() { Navigator.of(context).pop(); }), ) @@ -169,9 +169,10 @@ void showCommonBottomSheetWithoutHeight( bool useSafeArea = false, bool hasBottomPadding = true, EdgeInsets? padding, - Color backgroundColor = AppColors.bottomSheetBgColor, + Color? backgroundColor, VoidCallback? onCloseClicked, }) { + final resolvedBgColor = backgroundColor ?? AppColors.bottomSheetBgColor; showModalBottomSheet( sheetAnimationStyle: AnimationStyle( duration: Duration(milliseconds: 500), @@ -184,7 +185,7 @@ void showCommonBottomSheetWithoutHeight( showDragHandle: false, isDismissible: isDismissible, enableDrag: isDismissible, - backgroundColor: backgroundColor, + backgroundColor: resolvedBgColor, useSafeArea: useSafeArea, builder: (BuildContext context) { return SafeArea( @@ -228,7 +229,7 @@ void showCommonBottomSheetWithoutHeight( if (isCloseButtonVisible) ...[ Utils.buildSvgWithAssets( icon: AppAssets.close_bottom_sheet_icon, - iconColor: Color(0xff2B353E), + iconColor: AppColors.textColor, ).onPress(() { onCloseClicked?.call(); Navigator.of(context).pop(); diff --git a/lib/widgets/countdown_timer.dart b/lib/widgets/countdown_timer.dart index 1722ebfa..86249fc8 100644 --- a/lib/widgets/countdown_timer.dart +++ b/lib/widgets/countdown_timer.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; Widget buildTime(Duration duration, {bool isHomePage = false}) { String twoDigits(int n) => n.toString().padLeft(2, '0'); @@ -41,7 +42,7 @@ Widget buildDigit(String digit) { padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), // margin: const EdgeInsets.symmetric(horizontal: 2), decoration: BoxDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: BorderRadius.circular(8), ), child: ClipRect( @@ -100,13 +101,13 @@ Widget buildLabel(String label) { } Widget buildTimeSeparator() { - return const Padding( + return Padding( padding: EdgeInsets.symmetric(horizontal: 2.0), child: Text( ":", style: TextStyle( color: Colors.black, - fontSize: 20, + fontSize: 20.f, ), ), ); diff --git a/lib/widgets/custom_tab_bar.dart b/lib/widgets/custom_tab_bar.dart index 03f1b25a..71cbe820 100644 --- a/lib/widgets/custom_tab_bar.dart +++ b/lib/widgets/custom_tab_bar.dart @@ -16,10 +16,10 @@ class CustomTabBarModel { class CustomTabBar extends StatefulWidget { final int initialIndex; final List tabs; - final Color activeTextColor; - final Color activeBackgroundColor; - final Color inActiveTextColor; - final Color inActiveBackgroundColor; + final Color? activeTextColor; + final Color? activeBackgroundColor; + final Color? inActiveTextColor; + final Color? inActiveBackgroundColor; final Function(int)? onTabChange; final bool shouldTabExpanded; @@ -27,10 +27,10 @@ class CustomTabBar extends StatefulWidget { super.key, required this.tabs, this.initialIndex = 0, - this.activeTextColor = const Color(0xff2E3039), - this.inActiveTextColor = const Color(0xff898A8D), - this.activeBackgroundColor = const Color(0x142E3039), - this.inActiveBackgroundColor = Colors.white, + this.activeTextColor, + this.inActiveTextColor, + this.activeBackgroundColor, + this.inActiveBackgroundColor, this.onTabChange, this.shouldTabExpanded = false }); @@ -59,6 +59,10 @@ class CustomTabBarState extends State { @override Widget build(BuildContext context) { + final resolvedActiveTextColor = widget.activeTextColor ?? AppColors.textColor; + final resolvedInActiveTextColor = widget.inActiveTextColor ?? AppColors.inputLabelTextColor; + final resolvedActiveBgColor = widget.activeBackgroundColor ?? AppColors.lightGrayBGColor; + final resolvedInActiveBgColor = widget.inActiveBackgroundColor ?? AppColors.whiteColor; late Widget parentWidget; if(widget.shouldTabExpanded){ return Row( @@ -72,7 +76,7 @@ class CustomTabBarState extends State { scrollDirection: Axis.horizontal, padding: EdgeInsets.zero, physics: const BouncingScrollPhysics(), - itemBuilder: (cxt, index) => myTab(widget.tabs[index], index), + itemBuilder: (cxt, index) => myTab(widget.tabs[index], index, resolvedActiveTextColor, resolvedInActiveTextColor, resolvedActiveBgColor, resolvedInActiveBgColor), separatorBuilder: (cxt, index) => 4.width, itemCount: widget.tabs.length, ); @@ -80,7 +84,7 @@ class CustomTabBarState extends State { parentWidget = Row( mainAxisAlignment: MainAxisAlignment.center, spacing: 4.w, - children: [for (int i = 0; i < widget.tabs.length; i++) myTab(widget.tabs[i], i).expanded], + children: [for (int i = 0; i < widget.tabs.length; i++) myTab(widget.tabs[i], i, resolvedActiveTextColor, resolvedInActiveTextColor, resolvedActiveBgColor, resolvedInActiveBgColor).expanded], ); } @@ -94,14 +98,14 @@ class CustomTabBarState extends State { child: Center(child: parentWidget)); } - Widget myTab(CustomTabBarModel tabBar, int currentIndex) { + Widget myTab(CustomTabBarModel tabBar, int currentIndex, Color activeTextColor, Color inActiveTextColor, Color activeBgColor, Color inActiveBgColor) { bool isSelected = selectedIndex == currentIndex; return Container( height: (isTablet || isFoldable) ? 60.h : 54.h, padding: EdgeInsets.only(top: 4.h, bottom: 4.h, left: 14.w, right: 14.w), alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: isSelected ? widget.activeBackgroundColor : widget.inActiveBackgroundColor, + color: isSelected ? activeBgColor : inActiveBgColor, borderRadius: 10.r, ), child: Row( @@ -113,11 +117,11 @@ class CustomTabBarState extends State { icon: tabBar.image!, height: 18.h, width: 18.w, - iconColor: isSelected ? widget.activeTextColor : widget.inActiveTextColor, + iconColor: isSelected ? activeTextColor : inActiveTextColor, ), tabBar.title.toText13( weight: isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected ? widget.activeTextColor : widget.inActiveTextColor, + color: isSelected ? activeTextColor : inActiveTextColor, letterSpacing: isSelected ? -0.3 : -0.1), ], )).onPress(() { diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index 89725f32..d6e2eadf 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -74,7 +74,7 @@ class _DateRangeSelectorState extends State { LocaleKeys.to.tr(context: context), style: TextStyle( color: AppColors.calenderTextColor, - fontSize: 14.h, + fontSize: 14.f, fontWeight: FontWeight.w500, letterSpacing: -.2 ), @@ -87,15 +87,15 @@ class _DateRangeSelectorState extends State { thickness: 1, ).paddingOnly(bottom: 16.h, top: 16.h), Material( - color: Colors.white, + color: AppColors.whiteColor, child: SfDateRangePicker( controller: _calendarController, selectionMode: DateRangePickerSelectionMode.range, showNavigationArrow: true, headerHeight: 40.h, - backgroundColor: Colors.white, + backgroundColor: AppColors.whiteColor, headerStyle: DateRangePickerHeaderStyle( - backgroundColor: Colors.white, + backgroundColor: AppColors.whiteColor, textAlign: TextAlign.start, textStyle: TextStyle( fontSize: 18.f, @@ -107,7 +107,7 @@ class _DateRangeSelectorState extends State { ), monthViewSettings: DateRangePickerMonthViewSettings( viewHeaderStyle: DateRangePickerViewHeaderStyle( - backgroundColor: Colors.white, + backgroundColor: AppColors.whiteColor, textStyle: TextStyle( fontSize: 14.f, fontWeight: FontWeight.w600, @@ -247,7 +247,7 @@ class _DateRangeSelectorState extends State { label, style: TextStyle( color: AppColors.inputLabelTextColor, - fontSize: 12.h, + fontSize: 12.f, fontWeight: FontWeight.w500, ), ), @@ -255,7 +255,7 @@ class _DateRangeSelectorState extends State { date!, style: TextStyle( color: AppColors.textColor, - fontSize: 14.h, + fontSize: 14.f, fontWeight: FontWeight.w500, ), ) diff --git a/lib/widgets/datepicker_widget.dart b/lib/widgets/datepicker_widget.dart index 6ca9fe1f..f9d8ff73 100644 --- a/lib/widgets/datepicker_widget.dart +++ b/lib/widgets/datepicker_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; class DatePickerWidget extends StatelessWidget { final String labelText; @@ -33,7 +34,7 @@ class DatePickerWidget extends StatelessWidget { padding: padding, alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: isAllowRadius ? 15 : null, side: isBorderAllowed ? BorderSide(color: const Color(0xffefefef), width: 1) : null, ), @@ -52,7 +53,7 @@ class DatePickerWidget extends StatelessWidget { return Text( labelText, style: const TextStyle( - fontSize: 12, + fontSize: 12.f, fontWeight: FontWeight.w500, color: Color(0xff898A8D), letterSpacing: -0.2, @@ -98,7 +99,7 @@ class DatePickerWidget extends StatelessWidget { textAlign: TextAlign.left, textDirection: TextDirection.ltr, style: TextStyle( - fontSize: 14, + fontSize: 14.f, height: 21 / 14, fontWeight: FontWeight.w500, color: (selectedValue != null && selectedValue!.isNotEmpty) ? const Color(0xff2E3039) : const Color(0xffB0B0B0), diff --git a/lib/widgets/dialogs/confirm_dialog.dart b/lib/widgets/dialogs/confirm_dialog.dart index ce597c2d..5f7782fa 100644 --- a/lib/widgets/dialogs/confirm_dialog.dart +++ b/lib/widgets/dialogs/confirm_dialog.dart @@ -20,7 +20,7 @@ class ConfirmDialog extends StatelessWidget { @override Widget build(BuildContext context) { return Dialog( - backgroundColor: Colors.white, + backgroundColor: AppColors.whiteColor, shape: const RoundedRectangleBorder(), insetPadding: const EdgeInsets.only(left: 21, right: 21), child: Padding( @@ -35,7 +35,7 @@ class ConfirmDialog extends StatelessWidget { Expanded( child: Text( title ?? LocaleKeys.confirm.tr(), - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: AppColors.blackColor, height: 35 / 24, letterSpacing: -0.96), + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: AppColors.blackColor, height: 35 / 24, letterSpacing: -0.96), ).paddingOnly(top: 16), ), IconButton( diff --git a/lib/widgets/dropdown/country_dropdown_widget.dart b/lib/widgets/dropdown/country_dropdown_widget.dart index 1cd387f9..bc4b5edf 100644 --- a/lib/widgets/dropdown/country_dropdown_widget.dart +++ b/lib/widgets/dropdown/country_dropdown_widget.dart @@ -73,7 +73,7 @@ class CustomCountryDropdownState extends State { }, child: Row( children: [ - Utils.buildSvgWithAssets(icon: selectedCountry != null ? selectedCountry!.iconPath : AppAssets.ksa, width: 40.h, height: 40.h), + Utils.buildSvgWithAssets(icon: selectedCountry != null ? selectedCountry!.iconPath : AppAssets.ksa, width: 40.h, height: 40.h, applyThemeColor: false), SizedBox(width: 8.h), Utils.buildSvgWithAssets(icon: _isDropdownOpen ? AppAssets.dropdow_icon : AppAssets.dropdow_icon), ], @@ -189,7 +189,7 @@ class CustomCountryDropdownState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 16.h), child: Row( children: [ - Utils.buildSvgWithAssets(icon: country.iconPath, width: 38.h, height: 38.h), + Utils.buildSvgWithAssets(icon: country.iconPath, width: 38.h, height: 38.h, applyThemeColor: false), if (!widget.isFromBottomSheet) SizedBox(width: 12.h), if (!widget.isFromBottomSheet) Text(appState.getLanguageCode() == "ar" ? country.nameArabic : country.displayName, diff --git a/lib/widgets/dropdown/dropdown_widget.dart b/lib/widgets/dropdown/dropdown_widget.dart index 5ef5e580..a78b8fa5 100644 --- a/lib/widgets/dropdown/dropdown_widget.dart +++ b/lib/widgets/dropdown/dropdown_widget.dart @@ -55,8 +55,8 @@ class DropdownWidget extends StatelessWidget { padding: padding, alignment: Alignment.center, // This might need adjustment based on layout decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, - borderRadius: isAllowRadius ? 15.h : null, + color: AppColors.whiteColor, + borderRadius: isAllowRadius ? 15.h : null, side: isBorderAllowed ? BorderSide(color: hasError! ? Colors.red: const Color(0xffefefef), width: 1) : null, ), child: Row( @@ -92,7 +92,8 @@ class DropdownWidget extends StatelessWidget { margin: EdgeInsets.only(right: 10.h), padding: EdgeInsets.all(8.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 10.h, color: AppColors.greyColor), - child: Utils.buildSvgWithAssets(icon: leadingIcon!)); + child: Utils.buildSvgWithAssets(icon: leadingIcon!), + ); } Widget _buildLabelText(Color? labelColor) { @@ -142,7 +143,7 @@ class DropdownWidget extends StatelessWidget { borderRadius: BorderRadius.circular(12), ), - color: AppColors.scaffoldBgColor + color: Colors.black ); if (selected != null && onChange != null) { @@ -163,7 +164,8 @@ class DropdownWidget extends StatelessWidget { fontSize: 14.f, height: 21 / 14, fontWeight: FontWeight.w500, - color: (selectedValue != null && selectedValue!.isNotEmpty) ? const Color(0xff2E3039) : const Color(0xffB0B0B0), + // color: (selectedValue != null && selectedValue!.isNotEmpty) ? const Color(0xff2E3039) : const Color(0xffB0B0B0), + color: AppColors.textColor, letterSpacing: -0.2, ), ), diff --git a/lib/widgets/expandable_list_widget.dart b/lib/widgets/expandable_list_widget.dart index db4e3980..d6d2efdf 100644 --- a/lib/widgets/expandable_list_widget.dart +++ b/lib/widgets/expandable_list_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; // ==================== MAIN CUSTOM WIDGET ==================== @@ -251,12 +252,12 @@ class ExpandableListTheme { }); factory ExpandableListTheme.defaultTheme(BuildContext context) { - final textColor = Theme.of(context).textTheme.bodyLarge?.color ?? Colors.black; + final textColor = Theme.of(context).textTheme.bodyLarge?.color ?? AppColors.textColor; return ExpandableListTheme( backgroundColor: Colors.transparent, expandedBackgroundColor: Colors.transparent, - dividerColor: Colors.grey.shade300, + dividerColor: AppColors.dividerColor, defaultTrailingIcon: Icon( Icons.keyboard_arrow_down, color: textColor, @@ -278,10 +279,10 @@ class ExpandableListTheme { }) { return ExpandableListTheme( backgroundColor: backgroundColor ?? Colors.transparent, - expandedBackgroundColor: expandedBackgroundColor ?? Colors.grey.shade50, - dividerColor: dividerColor ?? Colors.grey.shade300, + expandedBackgroundColor: expandedBackgroundColor ?? AppColors.whiteColor, + dividerColor: dividerColor ?? AppColors.dividerColor, defaultTrailingIcon: defaultTrailingIcon ?? - Icon(Icons.keyboard_arrow_down, color: Colors.black, size: 24), + Icon(Icons.keyboard_arrow_down, color: AppColors.textColor, size: 24), itemPadding: itemPadding ?? EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), contentPadding: contentPadding ?? EdgeInsets.only(left: 16.w, right: 16.w, bottom: 16.h), leadingSpacing: leadingSpacing ?? 12.w, diff --git a/lib/widgets/family_files/family_file_add_widget.dart b/lib/widgets/family_files/family_file_add_widget.dart index 15751e93..d8b4d13e 100644 --- a/lib/widgets/family_files/family_file_add_widget.dart +++ b/lib/widgets/family_files/family_file_add_widget.dart @@ -39,7 +39,7 @@ class _FamilyFileAddWidgetState extends State { widget.message.toText16(color: AppColors.textColor, weight: FontWeight.w500), SizedBox(height: 20.h), Container( - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(24)), + decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(24)), padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), child: Column( children: [ diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index f1145212..d9a69a99 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -51,12 +51,12 @@ class CustomGraph extends StatelessWidget { final double? maxY; final double? maxX; final double? minX; - final Color spotColor; - final Color graphColor; + final Color? spotColor; + final Color? graphColor; final Color? secondaryGraphColor; // Color for secondary line - final Color graphShadowColor; - final Color graphGridColor; - final Color bottomLabelColor; + final Color? graphShadowColor; + final Color? graphGridColor; + final Color? bottomLabelColor; final double? bottomLabelSize; final FontWeight? bottomLabelFontWeight; final double? leftLabelInterval; @@ -92,12 +92,12 @@ class CustomGraph extends StatelessWidget { this.maxX, this.showBottomTitleDates = true, this.isFullScreeGraph = false, - this.spotColor = AppColors.bgGreenColor, - this.graphColor = AppColors.bgGreenColor, + this.spotColor, + this.graphColor, this.secondaryGraphColor, - this.graphShadowColor = AppColors.graphGridColor, - this.graphGridColor = AppColors.graphGridColor, - this.bottomLabelColor = AppColors.textColor, + this.graphShadowColor, + this.graphGridColor, + this.bottomLabelColor, this.bottomLabelFontWeight = FontWeight.w500, this.bottomLabelSize, this.leftLabelInterval, @@ -117,8 +117,13 @@ class CustomGraph extends StatelessWidget { @override Widget build(BuildContext context) { + final resolvedSpotColor = spotColor ?? AppColors.bgGreenColor; + final resolvedGraphColor = graphColor ?? AppColors.bgGreenColor; + final resolvedGraphShadowColor = graphShadowColor ?? AppColors.graphGridColor; + final resolvedGraphGridColor = graphGridColor ?? AppColors.graphGridColor; + return Material( - color: Colors.white, + color: AppColors.whiteColor, child: SizedBox( width: width, height: height, @@ -138,7 +143,7 @@ class CustomGraph extends StatelessWidget { // Show custom marker for touched spot with correct color per line return indicators.map((int index) { // Determine which line is being touched based on barData - Color dotColor = spotColor; + Color dotColor = resolvedSpotColor; if (secondaryDataPoints != null && barData.spots.length > 0) { // Check if this is the secondary line by comparing the first spot's color final gradient = barData.gradient; @@ -218,7 +223,7 @@ class CustomGraph extends StatelessWidget { top: BorderSide.none, ), ), - lineBarsData: _buildColoredLineSegments(dataPoints, showLinePoints), + lineBarsData: _buildColoredLineSegments(dataPoints, showLinePoints, resolvedGraphColor, resolvedGraphShadowColor), gridData: FlGridData( show: showGridLines ?? true, drawVerticalLine: false, @@ -228,7 +233,7 @@ class CustomGraph extends StatelessWidget { getDrawingHorizontalLine: getDrawingHorizontalLine ?? (value) { return FlLine( - color: graphGridColor, + color: resolvedGraphGridColor, strokeWidth: 1, dashArray: [5, 5], ); @@ -240,7 +245,7 @@ class CustomGraph extends StatelessWidget { ); } - List _buildColoredLineSegments(List dataPoints, bool showLinePoints) { + List _buildColoredLineSegments(List dataPoints, bool showLinePoints, Color resolvedGraphColor, Color resolvedGraphShadowColor) { final List allSpots = dataPoints.asMap().entries.map((entry) { double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value; return FlSpot(entry.key.toDouble(), value); @@ -254,7 +259,7 @@ class CustomGraph extends StatelessWidget { isStrokeJoinRound: true, barWidth: 2, gradient: LinearGradient( - colors: [graphColor, graphColor], + colors: [resolvedGraphColor, resolvedGraphColor], begin: Alignment.centerLeft, end: Alignment.centerRight, ), @@ -265,7 +270,7 @@ class CustomGraph extends StatelessWidget { cutOffY: cutOffY ?? 0, gradient: LinearGradient( colors: [ - graphShadowColor, + resolvedGraphShadowColor, Colors.white, ], begin: Alignment.topCenter, diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index e25b1176..eb3021f5 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -182,12 +182,13 @@ class _BottomSheetItem extends StatelessWidget { final Function onTap; final IconData icon; final String title; - final Color color; + final Color? color; - _BottomSheetItem({Key? key, required this.onTap, required this.title, required this.icon, this.color = AppColors.mainPurple}) : super(key: key); + _BottomSheetItem({Key? key, required this.onTap, required this.title, required this.icon, this.color}) : super(key: key); @override Widget build(BuildContext context) { + final resolvedColor = color ?? AppColors.mainPurple; return InkWell( onTap: () { if (onTap != null) { @@ -202,7 +203,7 @@ class _BottomSheetItem extends StatelessWidget { if (icon != null) Icon( icon, - color: color, + color: resolvedColor, size: 18.0, ), if (icon != null) const SizedBox(width: 24.0), diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index aae196cf..25dd4580 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -133,7 +133,7 @@ class TextInputWidget extends StatelessWidget { // height: isMultiline ? null : 64.h, alignment: Alignment.center, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: Colors.white, + color: AppColors.whiteColor, borderRadius: isAllowRadius ? (12.r) : null, side: isBorderAllowed ? BorderSide(color: hasError ? errorColor : const Color(0xffefefef), width: 1) : null, ), @@ -210,7 +210,7 @@ class TextInputWidget extends StatelessWidget { borderRadius: 12.r, color: AppColors.greyColor, ), - child: Utils.buildSvgWithAssets(icon: leadingIcon!)); + child: Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false)); } Widget _buildTrailingIcon(BuildContext context, {bool isArrowTrailing = false}) { diff --git a/lib/widgets/loader/bottomsheet_loader.dart b/lib/widgets/loader/bottomsheet_loader.dart index 71b83415..bff293d0 100644 --- a/lib/widgets/loader/bottomsheet_loader.dart +++ b/lib/widgets/loader/bottomsheet_loader.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; class LoaderBottomSheet { static final NavigationService _navService = GetIt.I(); @@ -27,8 +28,8 @@ class LoaderBottomSheet { builder: (_) { return Container( height: MediaQuery.of(context).size.height * 0.3, - decoration: const BoxDecoration( - color: Colors.white, + decoration: BoxDecoration( + color: AppColors.whiteColor, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Center( diff --git a/lib/widgets/map/map_utility_screen.dart b/lib/widgets/map/map_utility_screen.dart index cb3eb8ef..422809cf 100644 --- a/lib/widgets/map/map_utility_screen.dart +++ b/lib/widgets/map/map_utility_screen.dart @@ -80,7 +80,7 @@ class MapUtilityScreen extends StatelessWidget { ), Align( alignment: AlignmentDirectional.topStart, - child: Utils.buildSvgWithAssets(icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h).onPress(() { + child: Utils.buildSvgWithAssets(icon: AppAssets.closeBottomNav, width: 32.h, height: 32.h, applyThemeColor: false).onPress(() { onCrossClicked?.call(); // context // .read() diff --git a/lib/widgets/nfc/nfc_reader_sheet.dart b/lib/widgets/nfc/nfc_reader_sheet.dart index b76b36fc..956f758b 100644 --- a/lib/widgets/nfc/nfc_reader_sheet.dart +++ b/lib/widgets/nfc/nfc_reader_sheet.dart @@ -18,7 +18,7 @@ void showNfcReader(BuildContext context, {required Function onNcfScan, required shape: RoundedRectangleBorder( borderRadius: BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), ), - backgroundColor: Colors.white, + backgroundColor: AppColors.whiteColor, builder: (context) { return NfcLayout( onNcfScan: onNcfScan, @@ -87,7 +87,7 @@ class _NfcLayoutState extends State { "Ready To Scan", style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 24, + fontSize: 24.f, ), ), SizedBox( @@ -103,7 +103,7 @@ class _NfcLayoutState extends State { Text( "Approach an NFC Tag", style: TextStyle( - fontSize: 18, + fontSize: 18.f, ), ), SizedBox( @@ -125,7 +125,7 @@ class _NfcLayoutState extends State { backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, - fontSize: 14, + fontSize: 14.f, fontWeight: FontWeight.w500, borderRadius: 12.h, height: 40.h, diff --git a/lib/widgets/phone_number_input.dart b/lib/widgets/phone_number_input.dart index 83ddcf16..6ea07141 100644 --- a/lib/widgets/phone_number_input.dart +++ b/lib/widgets/phone_number_input.dart @@ -67,7 +67,7 @@ class _PhoneNumberInputState extends State { ) .toList(), onChanged: _onCountryCodeChanged, - icon: const Icon( + icon: Icon( Icons.keyboard_arrow_down_rounded, color: AppColors.mainPurple, ), @@ -100,7 +100,7 @@ class _PhoneNumberInputState extends State { ), ), style: const TextStyle( - fontSize: 18, + fontSize: 18.f, fontWeight: FontWeight.bold, letterSpacing: 2, color: Colors.black, diff --git a/package/device_calendar_plus/CHANGELOG.md b/package/device_calendar_plus/CHANGELOG.md new file mode 100644 index 00000000..466b9b0c --- /dev/null +++ b/package/device_calendar_plus/CHANGELOG.md @@ -0,0 +1,52 @@ +## 0.3.1 - 2025-11-07 + +### Fixed +- `showEventModal()` now properly awaits until the modal is dismissed (iOS and Android) + +## 0.3.0 - 2024-11-05 + +### Changed +- **BREAKING**: `deleteEvent()` now requires named parameter `eventId` and always deletes entire series for recurring events +- **BREAKING**: `updateEvent()` now uses named parameter `eventId` (renamed from `instanceId`) and always updates entire series for recurring events +- **BREAKING**: Removed `deleteAllInstances` and `updateAllInstances` parameters - operations on recurring events now always affect the entire series +- Renamed `getEvent()` and `showEventModal()` parameter from `instanceId` to `id` to clarify that both event IDs and instance IDs are accepted + +### Removed +- **BREAKING**: `NOT_SUPPORTED` error code (no longer needed) + +## 0.2.0 - 2024-11-05 + +### Added +- `openAppSettings()` method to guide users to system settings when permissions are denied +- Testing status documentation in README + +### Removed +- **BREAKING**: `getPlatformVersion()` method (unused boilerplate) + +### Changed +- Updated all platform packages to 0.2.0 + +## 0.1.1 - 2024-11-04 + +### Added +- Android: ProGuard/R8 rules for release build compatibility + +## 0.1.0 - 2024-11-04 + +Initial release. + +### Added +- Calendar permissions management (request/check) +- List device calendars with metadata (name, color, read-only status, primary flag) +- Query events by date range with optional calendar filtering +- Get single event by ID with support for recurring event instances +- Create events with full metadata support +- Update events including single-instance and all-instance updates for recurring events +- Delete events (single or all instances) +- Show native event modal +- All-day event support with floating date behavior +- Timezone handling for timed events +- Typed exception model with `DeviceCalendarException` and `DeviceCalendarError` enum +- Federated plugin architecture (Android + iOS) +- Support for Android API 24+ (target/compile 35) +- Support for iOS 13+ \ No newline at end of file diff --git a/package/device_calendar_plus/LICENSE b/package/device_calendar_plus/LICENSE new file mode 100644 index 00000000..0152eb2c --- /dev/null +++ b/package/device_calendar_plus/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 bullet.to + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/device_calendar_plus/README.md b/package/device_calendar_plus/README.md new file mode 100644 index 00000000..73c0751c --- /dev/null +++ b/package/device_calendar_plus/README.md @@ -0,0 +1,381 @@ +# device_calendar_plus + +A modern, maintained Flutter plugin for reading and writing device calendar events on **Android** and **iOS**. +Modern replacement for the unmaintained [`device_calendar`](https://pub.dev/packages/device_calendar) plugin — rebuilt for 2025 Flutter standards, working towards feature parity with a cleaner API, and no timezone package dependency. + +[![pub package](https://img.shields.io/pub/v/device_calendar_plus.svg)](https://pub.dev/packages/device_calendar_plus) +[![pub points](https://img.shields.io/pub/points/device_calendar_plus)](https://pub.dev/packages/device_calendar_plus/score) +[![platforms](https://img.shields.io/badge/platforms-android%20%7C%20ios-blue.svg)](#) +[![MIT license](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) + +## ✨ Overview + +`device_calendar_plus` lets Flutter apps read and write native calendar data using: + +- **Android** Calendar Provider +- **iOS** EventKit + +It provides a **clean Dart API**, proper **time-zone handling**, and an **actively maintained** federated structure. + +Created by [Bullet](https://bullet.to) — a personal task + notes + calendar app using this plugin in production. + +## ✅ Supported versions + +| Platform | Min OS / SDK | Target / Compile | +| ----------- | -------------- | ---------------------- | +| **Android** | **minSdk 24+** | **target/compile 35** | +| **iOS** | **iOS 13+** | Latest Xcode / iOS SDK | + +## 🚀 Features + +- **Permissions**: Request and check calendar permissions +- **Calendars**: Create, read, update, and delete calendars +- **Events**: Create, read, update, and delete events +- **Query**: Retrieve events by date range or specific event IDs +- **Native UI**: Open native event modal for viewing/editing in both android and iOS +- **All-Day Events**: Proper handling of floating calendar dates +- **Timezones**: Correct timezone behavior for timed events +- **Recurring Events**: Read recurring event instances; update/delete entire series + +## 🧩 Installation + +Add the dependency to your project: + +```yaml +dependencies: + device_calendar_plus: +``` + + + +### iOS + +Add usage descriptions to your app’s **Info.plist**: + +```xml + +NSCalendarsUsageDescription +Access your calendar to view and manage events. + + +NSCalendarsFullAccessUsageDescription +Full access to view and edit your calendar events. +NSCalendarsWriteOnlyAccessUsageDescription +Add events without reading existing events. +``` + +### Android + +Add calendar permissions to `android/app/src/main/AndroidManifest.xml`: + +```xml + + +``` + +**ProGuard / R8**: ProGuard rules are automatically applied by the plugin. No manual configuration needed. + +## ⏰ DateTime and Timezone Behavior + +**All DateTimes returned by this plugin are in local time.** + +### All-Day Events (Floating Dates) + +All-day events are treated as **floating calendar dates**, not specific instants in time. This means: + +- An all-day event for "January 15, 2024" will always display as January 15, regardless of what timezone your device is in +- The date components (year, month, day) are preserved across timezone changes +- **Do NOT convert all-day event DateTimes to UTC** — they represent calendar dates, not moments in time +- Example: A birthday on "January 15" should always show as January 15, whether you're in New York or Tokyo + +### Non-All-Day Events (Instants in Time) + +Regular timed events represent specific moments in time and can be converted to UTC as needed: + +- These events have specific start/end times in a timezone (e.g., "3:00 PM New York time") +- They represent absolute instants that correspond to different local times across timezones +- **You can freely convert these DateTimes to UTC** for storage, comparison, or API calls +- Example: A meeting at "3:00 PM EST" is the same instant as "12:00 PM PST" + +### Summary + +```dart +// All-day event - treat as a calendar date, NOT a UTC instant +final birthdayEvent = await plugin.getEvent(birthdayId); +if (birthdayEvent.isAllDay) { + // ✅ Use the date components directly + print('Birthday: ${birthdayEvent.startDate.year}-${birthdayEvent.startDate.month}-${birthdayEvent.startDate.day}'); + + // ❌ Don't convert to UTC - it's a calendar date, not a moment in time + // final utcDate = birthdayEvent.startDate.toUtc(); // DON'T DO THIS +} + +// Regular timed event - this IS an instant in time +final meetingEvent = await plugin.getEvent(meetingId); +if (!meetingEvent.isAllDay) { + // ✅ Convert to UTC for storage/comparison + final utcTime = meetingEvent.startDate.toUtc(); + + // ✅ Format in local time for display + print('Meeting at: ${meetingEvent.startDate}'); +} +``` + +## 🧱 Exception model + +Each `DeviceCalendarException` uses an enum code to describe the error type: + +```dart +enum DeviceCalendarError { + permissionDenied, + ... +} +``` + +This enum provides stable, descriptive error codes for all exceptions thrown by the plugin. + +> **Note on error codes:** +> `DeviceCalendarError` exists for developer ergonomics and clearer `switch` handling. +> We may introduce new enum values in future minor versions as new error cases appear. +We do not consider this a breaking change. + + +## 🛠️ Usage Examples + +### Request Permissions + +```dart +import 'package:device_calendar_plus/device_calendar_plus.dart'; + +// Get the singleton instance +final plugin = DeviceCalendar.instance; + +// Request calendar permissions +final status = await plugin.requestPermissions(); +if (status != CalendarPermissionStatus.granted) { + // Handle permission denied + return; +} +``` + +### Check Permissions + +Use `hasPermissions()` to check the current permission status without prompting the user: + +```dart +final plugin = DeviceCalendar.instance; + +// Check current permission status (doesn't prompt) +final status = await plugin.hasPermissions(); + +if (status == CalendarPermissionStatus.granted) { + // Permissions already granted + final calendars = await plugin.listCalendars(); +} else if (status == CalendarPermissionStatus.notDetermined) { + // User hasn't been asked yet - now we can prompt + final newStatus = await plugin.requestPermissions(); +} else { + // Denied or restricted - show appropriate UI + print('Permissions: $status'); +} +``` + +### List Calendars + +```dart +final plugin = DeviceCalendar.instance; + +// List all calendars +final calendars = await plugin.listCalendars(); +for (final calendar in calendars) { + print('${calendar.name} (${calendar.readOnly ? "read-only" : "writable"})'); + if (calendar.isPrimary) { + print(' ⭐ Primary calendar'); + } + if (calendar.colorHex != null) { + print(' Color: ${calendar.colorHex}'); + } +} + +// Find a writable calendar +final writableCalendar = calendars.firstWhere( + (cal) => !cal.readOnly, + orElse: () => calendars.first, +); +``` + +### Retrieve Events + +```dart +final plugin = DeviceCalendar.instance; + +// Get events for the next 30 days +final now = DateTime.now(); +final startDate = now; +final endDate = now.add(const Duration(days: 30)); + +// Get events from all calendars +final allEvents = await plugin.listEvents( + startDate, + endDate, +); +print('Found ${allEvents.length} events'); + +// Get events from specific calendars only +final calendarIds = ['calendar-id-1', 'calendar-id-2']; +final filteredEvents = await plugin.listEvents( + startDate, + endDate, + calendarIds: calendarIds, +); + +``` + +### Get Single Event + +```dart +final plugin = DeviceCalendar.instance; + +// Get a specific event by instanceId +final event = await plugin.getEvent(event.instanceId); +if (event != null) { + print('Event: ${event.title}'); +} + +// For recurring events, get a specific occurrence +final instance = await plugin.getEvent(event.instanceId); + +// For recurring events, get the master event definition +final masterEvent = await plugin.getEvent(event.eventId); +``` + +### Show Event in Modal + +```dart +final plugin = DeviceCalendar.instance; + +// Show a specific event in a modal dialog +await plugin.showEventModal(event.instanceId); + +// For recurring events, show a specific occurrence +await plugin.showEventModal(event.instanceId); + +// For recurring events, show the master event +await plugin.showEventModal(event.eventId); +``` + +### Create Event + +```dart +final plugin = DeviceCalendar.instance; + +// Create a basic event +final eventId = await plugin.createEvent( + calendarId: 'your-calendar-id', + title: 'Team Meeting', + startDate: DateTime(2024, 3, 20, 14, 0), + endDate: DateTime(2024, 3, 20, 15, 0), +); + +// Create an all-day event +final allDayEventId = await plugin.createEvent( + calendarId: 'your-calendar-id', + title: 'Conference', + startDate: DateTime(2024, 3, 20), + endDate: DateTime(2024, 3, 21), + isAllDay: true, +); + +// Create event with all optional parameters +final detailedEventId = await plugin.createEvent( + calendarId: 'your-calendar-id', + title: 'Project Kickoff', + startDate: DateTime(2024, 3, 20, 10, 0), + endDate: DateTime(2024, 3, 20, 12, 0), + description: 'Quarterly project kickoff meeting', + location: 'Conference Room A', + timeZone: 'America/New_York', + availability: EventAvailability.busy, +); +``` + +### Update Event + +```dart +final plugin = DeviceCalendar.instance; + +// Update event title +await plugin.updateEvent( + instanceId: event.instanceId, + title: 'Updated Meeting Title', +); + +// Update multiple fields +await plugin.updateEvent( + instanceId: event.instanceId, + title: 'Team Sync', + startDate: DateTime(2024, 3, 21, 15, 0), + endDate: DateTime(2024, 3, 21, 16, 0), + location: 'Conference Room B', + description: 'Updated description', +); + +// Change a timed event to all-day +await plugin.updateEvent( + instanceId: event.instanceId, + isAllDay: true, +); + +// Change an all-day event to timed +await plugin.updateEvent( + instanceId: event.instanceId, + isAllDay: false, + startDate: DateTime(2024, 3, 21, 10, 0), + endDate: DateTime(2024, 3, 21, 11, 0), +); + +// Update timezone (reinterprets local time) +// Note: "3 PM EST" becomes "3 PM PST" (different instant in time) +await plugin.updateEvent( + instanceId: event.instanceId, + timeZone: 'America/Los_Angeles', +); +``` + +**Note on Recurring Events**: For recurring events, `updateEvent` will always update the ENTIRE series (all past and future occurrences). Single-instance updates are not supported to maintain consistent behavior across platforms. + +### Delete Event + +```dart +final plugin = DeviceCalendar.instance; + +// Delete a single event +await plugin.deleteEvent(event.instanceId); + +// For recurring events, this deletes the ENTIRE series (all occurrences) +await plugin.deleteEvent(event.instanceId); +``` + +## 🤝 Contributing + +Contributions, PRs and issue reports welcome. +Open an issue first for larger features or breaking changes. + +- Code style: `dart format .` +- Run tests: `flutter test` +- Federated layout: platform code lives in + `/packages/device_calendar_plus_android` and `/packages/device_calendar_plus_ios`; + shared contracts in `/packages/device_calendar_plus_platform_interface`. + +## 🧪 Testing Status + +This plugin includes both **unit tests** and **integration tests** to ensure reliability. + +## 📄 License + +MIT © 2025 Bullet +See [LICENSE](LICENSE) for details. + +--- + +**Maintained by [Bullet](https://bullet.to)** — a cross-platform task + notes + calendar app built with Flutter. \ No newline at end of file diff --git a/package/device_calendar_plus/analysis_options.yaml b/package/device_calendar_plus/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/package/device_calendar_plus/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/package/device_calendar_plus/example/README.md b/package/device_calendar_plus/example/README.md new file mode 100644 index 00000000..2b3fce4c --- /dev/null +++ b/package/device_calendar_plus/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/package/device_calendar_plus/example/README_INTEGRATION_TESTS.md b/package/device_calendar_plus/example/README_INTEGRATION_TESTS.md new file mode 100644 index 00000000..be9af8ad --- /dev/null +++ b/package/device_calendar_plus/example/README_INTEGRATION_TESTS.md @@ -0,0 +1,43 @@ +# Integration Tests + +This directory contains integration tests for the Device Calendar Plus plugin. + +## Running Integration Tests + +### Quick Start + +Use the provided script to automatically handle permissions and run tests: + +```bash +./run_integration_tests.sh +``` + +**Note:** The script handles everything automatically - no manual permission granting needed! + +### Find Device IDs + +List available devices: +```bash +flutter devices +``` + +Example output: +``` +iPhone 16 (mobile) • F0A86A59-EB1B-4AA2-B487-8D3AA46664D8 • ios +sdk gphone64 arm64 (mobile) • emulator-5554 • android +``` + +### Examples + +```bash +# Run on iOS simulator +./run_integration_tests.sh F0A86A59-EB1B-4AA2-B487-8D3AA46664D8 + +# Run on Android emulator +./run_integration_tests.sh emulator-5554 + +# Run on booted iOS simulator +./run_integration_tests.sh booted +``` + +**Note:** The script is recommended as it handles platform detection and permission granting automatically. \ No newline at end of file diff --git a/package/device_calendar_plus/example/analysis_options.yaml b/package/device_calendar_plus/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/package/device_calendar_plus/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/package/device_calendar_plus/example/android/app/build.gradle.kts b/package/device_calendar_plus/example/android/app/build.gradle.kts new file mode 100644 index 00000000..83e256df --- /dev/null +++ b/package/device_calendar_plus/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "to.bullet.example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "to.bullet.example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/package/device_calendar_plus/example/android/app/src/debug/AndroidManifest.xml b/package/device_calendar_plus/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/package/device_calendar_plus/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/package/device_calendar_plus/example/android/app/src/main/AndroidManifest.xml b/package/device_calendar_plus/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..73ac6cb3 --- /dev/null +++ b/package/device_calendar_plus/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/device_calendar_plus/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/package/device_calendar_plus/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 00000000..59288ff6 --- /dev/null +++ b/package/device_calendar_plus/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,29 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + try { + flutterEngine.getPlugins().add(new to.bullet.device_calendar_plus_android.DeviceCalendarPlusAndroidPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin device_calendar_plus_android, to.bullet.device_calendar_plus_android.DeviceCalendarPlusAndroidPlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.flutter.plugins.integration_test.IntegrationTestPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin integration_test, dev.flutter.plugins.integration_test.IntegrationTestPlugin", e); + } + } +} diff --git a/package/device_calendar_plus/example/android/app/src/main/kotlin/to/bullet/example/MainActivity.kt b/package/device_calendar_plus/example/android/app/src/main/kotlin/to/bullet/example/MainActivity.kt new file mode 100644 index 00000000..49bba550 --- /dev/null +++ b/package/device_calendar_plus/example/android/app/src/main/kotlin/to/bullet/example/MainActivity.kt @@ -0,0 +1,5 @@ +package to.bullet.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/package/device_calendar_plus/example/android/app/src/main/res/drawable-v21/launch_background.xml b/package/device_calendar_plus/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/package/device_calendar_plus/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/package/device_calendar_plus/example/android/app/src/main/res/drawable/launch_background.xml b/package/device_calendar_plus/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/package/device_calendar_plus/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/package/device_calendar_plus/example/android/app/src/main/res/values-night/styles.xml b/package/device_calendar_plus/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/package/device_calendar_plus/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/package/device_calendar_plus/example/android/app/src/main/res/values/styles.xml b/package/device_calendar_plus/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/package/device_calendar_plus/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/package/device_calendar_plus/example/android/app/src/profile/AndroidManifest.xml b/package/device_calendar_plus/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/package/device_calendar_plus/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/package/device_calendar_plus/example/android/build.gradle.kts b/package/device_calendar_plus/example/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/package/device_calendar_plus/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/package/device_calendar_plus/example/android/gradle.properties b/package/device_calendar_plus/example/android/gradle.properties new file mode 100644 index 00000000..f018a618 --- /dev/null +++ b/package/device_calendar_plus/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/package/device_calendar_plus/example/android/gradle/wrapper/gradle-wrapper.properties b/package/device_calendar_plus/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ac3b4792 --- /dev/null +++ b/package/device_calendar_plus/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/package/device_calendar_plus/example/android/local.properties b/package/device_calendar_plus/example/android/local.properties new file mode 100644 index 00000000..a4725303 --- /dev/null +++ b/package/device_calendar_plus/example/android/local.properties @@ -0,0 +1,2 @@ +sdk.dir=/Users/cloud/Library/Android/sdk +flutter.sdk=/Users/cloud/sdk/flutter \ No newline at end of file diff --git a/package/device_calendar_plus/example/android/settings.gradle.kts b/package/device_calendar_plus/example/android/settings.gradle.kts new file mode 100644 index 00000000..fb605bc8 --- /dev/null +++ b/package/device_calendar_plus/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/package/device_calendar_plus/example/integration_test/device_calendar_test.dart b/package/device_calendar_plus/example/integration_test/device_calendar_test.dart new file mode 100644 index 00000000..6da9f023 --- /dev/null +++ b/package/device_calendar_plus/example/integration_test/device_calendar_test.dart @@ -0,0 +1,862 @@ +// import 'package:device_calendar_plus/device_calendar_plus.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:integration_test/integration_test.dart'; +// +// void main() { +// IntegrationTestWidgetsFlutterBinding.ensureInitialized(); +// +// group('Device Calendar Integration Tests', () { +// late DeviceCalendar plugin; +// final List createdCalendarIds = []; +// +// setUpAll(() { +// plugin = DeviceCalendar.instance; +// }); +// +// tearDownAll(() async { +// // Clean up all created calendars +// if (createdCalendarIds.isNotEmpty) { +// for (final id in createdCalendarIds) { +// await plugin.deleteCalendar(id); +// } +// } +// }); +// +// test('1. Request Permissions', () async { +// final status = await plugin.requestPermissions(); +// +// // The test will continue regardless of permission status, but warn if denied +// if (status != CalendarPermissionStatus.granted) {} +// +// expect( +// status, +// isIn([ +// CalendarPermissionStatus.granted, +// CalendarPermissionStatus.denied, +// CalendarPermissionStatus.restricted, +// ])); +// }); +// +// test('1b. Check Permissions Status', () async { +// final status = await plugin.hasPermissions(); +// +// // After auto-granting permissions via run_integration_tests.sh, +// // the status should be granted +// expect(status, CalendarPermissionStatus.granted); +// }); +// +// test('2. Create and Delete Calendar', () async { +// // This test creates and immediately deletes a calendar to verify delete works +// // If delete fails, only one calendar needs manual cleanup +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarName = 'Create-Delete Test $timestamp'; +// +// // Create calendar +// final calendarId = await plugin.createCalendar(name: calendarName); +// expect(calendarId, isNotEmpty); +// expect(calendarId, isA()); +// +// // Delete calendar +// await plugin.deleteCalendar(calendarId); +// +// // Verify it's gone by listing calendars +// final calendars = await plugin.listCalendars(); +// final deletedCalendar = +// calendars.where((cal) => cal.id == calendarId).toList(); +// expect(deletedCalendar, isEmpty, +// reason: 'Calendar should be deleted and not in list'); +// }); +// +// test('3. Verify Calendar in List', () async { +// // Create a new calendar for this test +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarName = 'Verify Test Calendar $timestamp'; +// +// final calendarId = await plugin.createCalendar(name: calendarName); +// createdCalendarIds.add(calendarId); +// +// // List all calendars +// final calendars = await plugin.listCalendars(); +// +// expect(calendars, isNotEmpty); +// +// // Find our newly created calendar +// final createdCalendar = calendars.firstWhere( +// (cal) => cal.id == calendarId, +// orElse: () => throw Exception('Created calendar not found in list'), +// ); +// +// expect(createdCalendar.name, equals(calendarName)); +// expect(createdCalendar.id, equals(calendarId)); +// }); +// +// test('4. Create Calendar with Color', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarName = 'Colored Calendar $timestamp'; +// final colorHex = '#FF5733'; +// +// final calendarId = await plugin.createCalendar( +// name: calendarName, +// colorHex: colorHex, +// ); +// +// expect(calendarId, isNotEmpty); +// createdCalendarIds.add(calendarId); +// +// // List calendars and find the one we just created +// final calendars = await plugin.listCalendars(); +// final coloredCalendar = +// calendars.firstWhere((cal) => cal.id == calendarId); +// +// expect(coloredCalendar.colorHex, isNotNull); +// +// // Note: iOS may convert the color to a different color space, +// // so we can't do an exact match. Just verify it has a color. +// +// // On Android, the color should match exactly +// // On iOS, color may be slightly different due to color space conversion +// if (coloredCalendar.colorHex != null) { +// expect(coloredCalendar.colorHex!.length, equals(7)); // #RRGGBB format +// expect(coloredCalendar.colorHex!.startsWith('#'), isTrue); +// } +// }); +// +// test('5. Create Multiple Calendars', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarNames = [ +// 'Multi Test Calendar 1 $timestamp', +// 'Multi Test Calendar 2 $timestamp', +// 'Multi Test Calendar 3 $timestamp', +// ]; +// +// final createdIds = []; +// +// // Create 3 calendars +// for (final name in calendarNames) { +// final calendarId = await plugin.createCalendar(name: name); +// expect(calendarId, isNotEmpty); +// createdIds.add(calendarId); +// createdCalendarIds.add(calendarId); +// } +// +// expect(createdIds.length, equals(3)); +// expect(createdIds.toSet().length, equals(3)); // All unique IDs +// +// // Verify all 3 appear in the list +// final calendars = await plugin.listCalendars(); +// +// for (var i = 0; i < calendarNames.length; i++) { +// final calendar = calendars.firstWhere( +// (cal) => cal.id == createdIds[i], +// orElse: () => +// throw Exception('Calendar ${calendarNames[i]} not found'), +// ); +// +// expect(calendar.name, equals(calendarNames[i])); +// } +// }); +// +// test('6. Cross-Platform Consistency', () async { +// // Create a calendar and verify the data structure is consistent +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarName = 'Consistency Test $timestamp'; +// final colorHex = '#3498DB'; +// +// final calendarId = await plugin.createCalendar( +// name: calendarName, +// colorHex: colorHex, +// ); +// createdCalendarIds.add(calendarId); +// +// final calendars = await plugin.listCalendars(); +// final calendar = calendars.firstWhere((cal) => cal.id == calendarId); +// +// // Verify all expected fields are present and of correct types +// expect(calendar.id, isA()); +// expect(calendar.id, isNotEmpty); +// expect(calendar.name, isA()); +// expect(calendar.name, equals(calendarName)); +// expect(calendar.readOnly, isA()); +// expect(calendar.isPrimary, isA()); +// expect(calendar.hidden, isA()); +// +// // Optional fields +// if (calendar.colorHex != null) { +// expect(calendar.colorHex, isA()); +// } +// if (calendar.accountName != null) { +// expect(calendar.accountName, isA()); +// } +// if (calendar.accountType != null) { +// expect(calendar.accountType, isA()); +// } +// }); +// +// test('7. Update Calendar - Name Only', () async { +// // Create a calendar and update just its name +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final originalName = 'Update Name Test $timestamp'; +// final newName = 'Updated Name $timestamp'; +// +// final calendarId = await plugin.createCalendar(name: originalName); +// createdCalendarIds.add(calendarId); +// +// // Update just the name +// await plugin.updateCalendar(calendarId, name: newName); +// +// // Verify the update +// final calendars = await plugin.listCalendars(); +// final updatedCalendar = +// calendars.firstWhere((cal) => cal.id == calendarId); +// expect(updatedCalendar.name, equals(newName)); +// }); +// +// test('8. Update Calendar - Color Only', () async { +// // Create a calendar and update just its color +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarName = 'Update Color Test $timestamp'; +// final newColor = '#00FF00'; // Green +// +// final calendarId = await plugin.createCalendar( +// name: calendarName, +// colorHex: '#FF0000', // Red +// ); +// createdCalendarIds.add(calendarId); +// +// // Update just the color +// await plugin.updateCalendar(calendarId, colorHex: newColor); +// +// // Verify the update +// final calendars = await plugin.listCalendars(); +// final updatedCalendar = +// calendars.firstWhere((cal) => cal.id == calendarId); +// expect(updatedCalendar.colorHex?.toUpperCase(), +// equals(newColor.toUpperCase())); +// }); +// +// test('9. Update Calendar - Name and Color', () async { +// // Create a calendar and update both name and color +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final originalName = 'Update Both Test $timestamp'; +// final newName = 'Updated Both $timestamp'; +// final newColor = '#0000FF'; // Blue +// +// final calendarId = await plugin.createCalendar( +// name: originalName, +// colorHex: '#FF0000', // Red +// ); +// createdCalendarIds.add(calendarId); +// +// // Update both name and color +// await plugin.updateCalendar(calendarId, +// name: newName, colorHex: newColor); +// +// // Verify the updates +// final calendars = await plugin.listCalendars(); +// final updatedCalendar = +// calendars.firstWhere((cal) => cal.id == calendarId); +// expect(updatedCalendar.name, equals(newName)); +// expect(updatedCalendar.colorHex?.toUpperCase(), +// equals(newColor.toUpperCase())); +// }); +// +// test('10. Error Handling - Update with No Parameters', () async { +// // Create a calendar first +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = +// await plugin.createCalendar(name: 'Error Test $timestamp'); +// createdCalendarIds.add(calendarId); +// +// // Try to update without providing any parameters +// try { +// await plugin.updateCalendar(calendarId); +// fail('Should have thrown an error when no parameters provided'); +// } on ArgumentError catch (e) { +// expect(e.message, contains('At least one')); +// } +// }); +// +// test('11. Error Handling - Update with Empty Name', () async { +// // Create a calendar first +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = +// await plugin.createCalendar(name: 'Empty Name Test $timestamp'); +// createdCalendarIds.add(calendarId); +// +// // Try to update with an empty name +// try { +// await plugin.updateCalendar(calendarId, name: ''); +// fail('Should have thrown an error for empty name'); +// } on ArgumentError catch (e) { +// expect(e.message, contains('cannot be empty')); +// } +// }); +// +// test('12. Error Handling - Create with Empty Name', () async { +// // Attempting to create a calendar with an empty name should fail +// try { +// await plugin.createCalendar(name: ''); +// fail('Should have thrown an error for empty calendar name'); +// } on ArgumentError catch (e) { +// // Expected - test passes +// expect(e.message, contains('cannot be empty')); +// } +// }); +// +// test('13. Error Handling - Create with Whitespace-only Name', () async { +// // Whitespace-only names should also fail +// try { +// await plugin.createCalendar(name: ' '); +// fail('Should have thrown an error for whitespace-only calendar name'); +// } on ArgumentError catch (e) { +// expect(e.message, contains('cannot be empty')); +// } +// }); +// +// test('14. Color Format Variations', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// +// // Test different valid color formats +// final colorVariations = [ +// '#FF0000', // Red +// '#00FF00', // Green +// '#0000FF', // Blue +// '#FFFFFF', // White +// '#000000', // Black +// ]; +// +// for (var i = 0; i < colorVariations.length; i++) { +// final color = colorVariations[i]; +// final calendarId = await plugin.createCalendar( +// name: 'Color Test $i $timestamp', +// colorHex: color, +// ); +// +// expect(calendarId, isNotEmpty); +// createdCalendarIds.add(calendarId); +// } +// }); +// +// test('11. Create Event', () async { +// // Create a test calendar first +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'Event Test Calendar $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// final now = DateTime.now(); +// final startDate = DateTime(now.year, now.month, now.day, 14, 0); +// final endDate = DateTime(now.year, now.month, now.day, 15, 0); +// +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'Test Event', +// startDate: startDate, +// endDate: endDate, +// description: 'This is a test event', +// location: 'Test Location', +// availability: EventAvailability.busy, +// ); +// +// expect(eventId, isNotEmpty); +// +// // Verify event was created by retrieving it +// final events = await plugin.listEvents( +// startDate.subtract(Duration(hours: 1)), +// endDate.add(Duration(hours: 1)), +// calendarIds: [calendarId], +// ); +// +// expect(events, isNotEmpty); +// final createdEvent = events.firstWhere((e) => e.eventId == eventId); +// expect(createdEvent.title, 'Test Event'); +// expect(createdEvent.description, 'This is a test event'); +// expect(createdEvent.location, 'Test Location'); +// }); +// +// test('12. Create All-Day Event', () async { +// // Create a test calendar +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'All-Day Event Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// final today = DateTime.now(); +// final tomorrow = today.add(Duration(days: 1)); +// +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'All-Day Test Event', +// startDate: DateTime(today.year, today.month, today.day), +// endDate: DateTime(tomorrow.year, tomorrow.month, tomorrow.day), +// isAllDay: true, +// availability: EventAvailability.free, +// ); +// +// expect(eventId, isNotEmpty); +// +// // Verify the event is all-day +// final events = await plugin.listEvents( +// DateTime(today.year, today.month, today.day), +// DateTime(tomorrow.year, tomorrow.month, tomorrow.day) +// .add(Duration(days: 1)), +// calendarIds: [calendarId], +// ); +// +// expect(events, isNotEmpty); +// final allDayEvent = events.firstWhere((e) => e.eventId == eventId); +// expect(allDayEvent.isAllDay, true); +// }); +// +// test('12b. All-Day Event Date Normalization', () async { +// // Test that all-day events strip time components +// // Pass DateTime with time components, verify event is still all-day +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'Date Normalization Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// final today = DateTime.now(); +// final tomorrow = today.add(Duration(days: 1)); +// +// // Pass dates WITH time components +// final startWithTime = +// DateTime(today.year, today.month, today.day, 14, 30, 45); +// final endWithTime = +// DateTime(tomorrow.year, tomorrow.month, tomorrow.day, 18, 15, 30); +// +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'All-Day with Time Components', +// startDate: startWithTime, +// endDate: endWithTime, +// isAllDay: true, +// ); +// +// expect(eventId, isNotEmpty); +// +// // Retrieve and verify the event is still all-day +// final events = await plugin.listEvents( +// DateTime(today.year, today.month, today.day), +// DateTime(tomorrow.year, tomorrow.month, tomorrow.day) +// .add(Duration(days: 1)), +// calendarIds: [calendarId], +// ); +// +// expect(events, isNotEmpty); +// final normalizedEvent = events.firstWhere((e) => e.eventId == eventId); +// expect(normalizedEvent.isAllDay, true); +// +// // Verify the date is preserved correctly (floating date behavior) +// // All-day events should maintain the same calendar date regardless of timezone +// // The date components (year/month/day) must match what we passed in +// expect(normalizedEvent.startDate.year, today.year, +// reason: 'Year should be preserved for all-day events'); +// expect(normalizedEvent.startDate.month, today.month, +// reason: 'Month should be preserved for all-day events'); +// expect(normalizedEvent.startDate.day, today.day, +// reason: 'Day should be preserved for all-day events'); +// +// // Time should be midnight (00:00:00) +// expect(normalizedEvent.startDate.hour, 0); +// expect(normalizedEvent.startDate.minute, 0); +// expect(normalizedEvent.startDate.second, 0); +// }); +// +// test('13. Delete Event', () async { +// // Create a test calendar and event +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'Delete Event Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// final now = DateTime.now(); +// final startDate = DateTime(now.year, now.month, now.day, 16, 0); +// final endDate = DateTime(now.year, now.month, now.day, 17, 0); +// +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'Event To Delete', +// startDate: startDate, +// endDate: endDate, +// ); +// +// // Verify event exists +// final eventsBefore = await plugin.listEvents( +// startDate.subtract(Duration(hours: 1)), +// endDate.add(Duration(hours: 1)), +// calendarIds: [calendarId], +// ); +// expect(eventsBefore, isNotEmpty); +// +// // Delete the event +// await plugin.deleteEvent(eventId: eventId); +// +// // Verify event no longer exists +// final eventsAfter = await plugin.listEvents( +// startDate.subtract(Duration(hours: 1)), +// endDate.add(Duration(hours: 1)), +// calendarIds: [calendarId], +// ); +// +// final deletedEvent = +// eventsAfter.where((e) => e.eventId == eventId).toList(); +// expect(deletedEvent, isEmpty); +// }); +// +// test('14. Create Event with Different Availabilities', () async { +// // Create a test calendar +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'Availability Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// final now = DateTime.now(); +// final availabilities = [ +// EventAvailability.busy, +// EventAvailability.free, +// EventAvailability.tentative, +// ]; +// +// for (var i = 0; i < availabilities.length; i++) { +// final availability = availabilities[i]; +// final startDate = DateTime(now.year, now.month, now.day, 9 + i, 0); +// final endDate = DateTime(now.year, now.month, now.day, 10 + i, 0); +// +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'Event ${availability.name}', +// startDate: startDate, +// endDate: endDate, +// availability: availability, +// ); +// +// expect(eventId, isNotEmpty); +// } +// }); +// +// test( +// '15. Delete All Instances of Recurring Event', +// () async { +// // This test requires a recurring event to exist, which must be created +// // manually in the iOS Calendar or Android Calendar app since we don't +// // support creating recurring events yet. +// // +// // To test manually: +// // 1. Create a recurring event in your device's calendar app +// // 2. Get the instanceId (format: "eventId@timestamp") +// // 3. Uncomment and update the code below with the actual instanceId +// // 4. Run this test +// // +// // Example: +// // const recurringInstanceId = 'YOUR-EVENT-ID@1234567890000'; +// // await plugin.deleteEvent(recurringInstanceId); +// // +// // Expected: Entire series (all instances) of the recurring event should be deleted +// +// fail( +// 'This test requires manual setup. Create a recurring event in your ' +// 'device calendar app, then update this test with the instanceId.'); +// }, +// skip: 'Requires manual creation of recurring event. ' +// 'Will be automated when recurrence rule support is added.', +// ); +// +// test('16. Update Event Title', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'Update Title Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// // Create event +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'Original Title', +// startDate: DateTime.now().add(Duration(hours: 1)), +// endDate: DateTime.now().add(Duration(hours: 2)), +// ); +// +// // Update title +// await plugin.updateEvent( +// eventId: eventId, +// title: 'Updated Title', +// ); +// +// // Verify update +// final event = await plugin.getEvent(eventId); +// expect(event, isNotNull); +// expect(event!.title, 'Updated Title'); +// }); +// +// test('17. Update Event Dates', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'Update Dates Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// final originalStart = DateTime.now().add(Duration(hours: 1)); +// final originalEnd = DateTime.now().add(Duration(hours: 2)); +// +// // Create event +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'Date Update Test', +// startDate: originalStart, +// endDate: originalEnd, +// ); +// +// // Update dates +// final newStart = DateTime.now().add(Duration(days: 1, hours: 3)); +// final newEnd = DateTime.now().add(Duration(days: 1, hours: 4)); +// +// await plugin.updateEvent( +// eventId: eventId, +// startDate: newStart, +// endDate: newEnd, +// ); +// +// // Verify update +// final event = await plugin.getEvent(eventId); +// expect(event, isNotNull); +// // Allow small time differences (within 1 minute) +// expect(event!.startDate.difference(newStart).abs(), +// lessThan(Duration(minutes: 1))); +// expect(event.endDate.difference(newEnd).abs(), +// lessThan(Duration(minutes: 1))); +// }); +// +// test('18. Update Event Description and Location', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'Update Multi-field Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// // Create event +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'Multi-field Update Test', +// startDate: DateTime.now().add(Duration(hours: 1)), +// endDate: DateTime.now().add(Duration(hours: 2)), +// description: 'Original description', +// location: 'Original location', +// ); +// +// // Update multiple fields +// await plugin.updateEvent( +// eventId: eventId, +// description: 'Updated description', +// location: 'Updated location', +// ); +// +// // Verify update +// final event = await plugin.getEvent(eventId); +// expect(event, isNotNull); +// expect(event!.description, 'Updated description'); +// expect(event.location, 'Updated location'); +// }); +// +// test('19. Change Timed Event to All-Day', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'Timed to All-Day Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// final today = DateTime.now(); +// +// // Create timed event +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'Timed to All-Day', +// startDate: DateTime(today.year, today.month, today.day, 14, 0), +// endDate: DateTime(today.year, today.month, today.day, 15, 0), +// isAllDay: false, +// ); +// +// // Update to all-day +// await plugin.updateEvent( +// eventId: eventId, +// isAllDay: true, +// ); +// +// // Verify update +// final event = await plugin.getEvent(eventId); +// expect(event, isNotNull); +// expect(event!.isAllDay, true); +// // Time should be stripped to midnight +// expect(event.startDate.hour, 0); +// expect(event.startDate.minute, 0); +// expect(event.startDate.second, 0); +// }); +// +// test('20. Change All-Day Event to Timed', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'All-Day to Timed Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// final today = DateTime.now(); +// +// // Create all-day event +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'All-Day to Timed', +// startDate: DateTime(today.year, today.month, today.day), +// endDate: DateTime(today.year, today.month, today.day + 1), +// isAllDay: true, +// ); +// +// // Update to timed with specific hours +// final newStart = DateTime(today.year, today.month, today.day, 10, 0); +// final newEnd = DateTime(today.year, today.month, today.day, 11, 0); +// +// await plugin.updateEvent( +// eventId: eventId, +// isAllDay: false, +// startDate: newStart, +// endDate: newEnd, +// ); +// +// // Verify update +// final event = await plugin.getEvent(eventId); +// expect(event, isNotNull); +// expect(event!.isAllDay, false); +// // Should have specific time now (allowing small differences) +// expect(event.startDate.difference(newStart).abs(), +// lessThan(Duration(minutes: 1))); +// }); +// +// test('21. Update Event TimeZone', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'Update Timezone Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// final startDate = DateTime.now().add(Duration(hours: 1)); +// final endDate = DateTime.now().add(Duration(hours: 2)); +// +// // Create event with New York timezone +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'Timezone Update Test', +// startDate: startDate, +// endDate: endDate, +// timeZone: 'America/New_York', +// ); +// +// // Update to Los Angeles timezone +// // Note: This reinterprets the local time, not preserving the instant +// await plugin.updateEvent( +// eventId: eventId, +// timeZone: 'America/Los_Angeles', +// ); +// +// // Verify event is updated (note: the exact behavior may vary by platform) +// final event = await plugin.getEvent(eventId); +// expect(event, isNotNull); +// }); +// +// test('22. Update Event with No Fields Throws Error', () async { +// final timestamp = DateTime.now().millisecondsSinceEpoch; +// final calendarId = await plugin.createCalendar( +// name: 'No Fields Test $timestamp', +// ); +// createdCalendarIds.add(calendarId); +// +// // Create event +// final eventId = await plugin.createEvent( +// calendarId: calendarId, +// title: 'No Fields Test', +// startDate: DateTime.now().add(Duration(hours: 1)), +// endDate: DateTime.now().add(Duration(hours: 2)), +// ); +// +// // Attempt to update with no fields - should throw +// expect( +// () async => await plugin.updateEvent(eventId: eventId), +// throwsA(isA()), +// ); +// }); +// +// test( +// '24. Update All Instances of Recurring Event', +// () async { +// // This test requires a recurring event to exist, which must be created +// // manually in the iOS Calendar or Android Calendar app since we don't +// // support creating recurring events yet. +// // +// // To test manually: +// // 1. Create a recurring event in your device's calendar app +// // 2. Get the instanceId (format: "eventId" for series update) +// // 3. Uncomment and update the code below with the actual instanceId +// // 4. Run this test +// // +// // Example: +// // const recurringEventId = 'YOUR-EVENT-ID'; +// // await plugin.updateEvent( +// // instanceId: recurringEventId, +// // updateAllInstances: true, +// // title: 'Updated Recurring Event', +// // ); +// // +// // Expected: All instances of the recurring event should be updated +// +// fail( +// 'This test requires manual setup. Create a recurring event in your ' +// 'device calendar app, then update this test with the eventId.'); +// }, +// skip: 'Requires manual creation of recurring event. ' +// 'Will be automated when recurrence rule support is added.', +// ); +// +// test( +// '25. Show Event Modal Awaits Until Closed', +// () async { +// // This test requires manual verification because it involves system UI: +// // - iOS: EKEventViewController (requires XCUITest for automation) +// // - Android: External calendar app (requires Espresso inter-app testing) +// // +// // To test manually: +// // 1. Create an event in a test calendar +// // 2. Call showEventModal with the event's instanceId +// // 3. Verify the modal opens +// // 4. Add logging or UI updates after the await +// // 5. Dismiss the modal (tap Done/Back) +// // 6. Verify the Future completes ONLY after modal is dismissed +// // +// // Example: +// // final timestamp = DateTime.now().millisecondsSinceEpoch; +// // final calendarId = await plugin.createCalendar( +// // name: 'Modal Test $timestamp', +// // ); +// // final eventId = await plugin.createEvent( +// // calendarId: calendarId, +// // title: 'Modal Test Event', +// // startDate: DateTime.now().add(Duration(hours: 1)), +// // endDate: DateTime.now().add(Duration(hours: 2)), +// // ); +// // print('Opening modal...'); +// // await plugin.showEventModal(eventId); +// // print('Modal closed - Future completed!'); +// // +// // Expected: Second print statement appears ONLY after modal is dismissed +// +// fail( +// 'This test requires manual verification. Automated testing of system ' +// 'modal UI requires XCUITest (iOS) or Espresso (Android) setup.'); +// }, +// skip: 'Requires manual verification. System modal UI cannot be easily ' +// 'automated with integration_test package.', +// ); +// }); +// } diff --git a/package/device_calendar_plus/example/integration_test/integration_test_driver.dart b/package/device_calendar_plus/example/integration_test/integration_test_driver.dart new file mode 100644 index 00000000..d1ba1697 --- /dev/null +++ b/package/device_calendar_plus/example/integration_test/integration_test_driver.dart @@ -0,0 +1,51 @@ +// // ignore_for_file: avoid_print +// +// import 'dart:io'; +// +// import 'package:integration_test/integration_test_driver.dart'; +// +// Future main() async { +// const packageName = 'to.bullet.example'; +// +// // Grant Android permissions before tests run (via adb on host machine) +// print('📱 Granting calendar permissions via adb...'); +// for (final permission in [ +// 'android.permission.READ_CALENDAR', +// 'android.permission.WRITE_CALENDAR', +// ]) { +// try { +// final result = Process.runSync( +// 'adb', +// ['shell', 'pm', 'grant', packageName, permission], +// ); +// if (result.exitCode == 0) { +// print(' ✓ Granted $permission'); +// } else { +// print(' ⚠ Failed to grant $permission: ${result.stderr}'); +// } +// } catch (e) { +// print(' ⚠ Error granting $permission: $e'); +// } +// } +// print(''); +// +// // Run the integration tests +// await integrationDriver(); +// +// // Revoke permissions after tests (cleanup) +// print(''); +// print('🧹 Revoking calendar permissions...'); +// for (final permission in [ +// 'android.permission.READ_CALENDAR', +// 'android.permission.WRITE_CALENDAR', +// ]) { +// try { +// Process.runSync( +// 'adb', +// ['shell', 'pm', 'revoke', packageName, permission], +// ); +// } catch (e) { +// // Ignore errors during cleanup +// } +// } +// } diff --git a/package/device_calendar_plus/example/ios/Flutter/AppFrameworkInfo.plist b/package/device_calendar_plus/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..1dc6cf76 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/package/device_calendar_plus/example/ios/Flutter/Debug.xcconfig b/package/device_calendar_plus/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/package/device_calendar_plus/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/package/device_calendar_plus/example/ios/Flutter/Generated.xcconfig b/package/device_calendar_plus/example/ios/Flutter/Generated.xcconfig new file mode 100644 index 00000000..c29f15c0 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Flutter/Generated.xcconfig @@ -0,0 +1,15 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/cloud/sdk/flutter +FLUTTER_APPLICATION_PATH=/Users/cloud/Downloads/device_calender_plus/device_calendar_plus-0.3.1/example +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_TARGET=lib/main.dart +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NUMBER=1 +FLUTTER_CLI_BUILD_MODE=debug +EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 +EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/package/device_calendar_plus/example/ios/Flutter/Release.xcconfig b/package/device_calendar_plus/example/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/package/device_calendar_plus/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/package/device_calendar_plus/example/ios/Flutter/ephemeral/flutter_lldb_helper.py b/package/device_calendar_plus/example/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/package/device_calendar_plus/example/ios/Flutter/ephemeral/flutter_lldbinit b/package/device_calendar_plus/example/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/package/device_calendar_plus/example/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/package/device_calendar_plus/example/ios/Flutter/flutter_export_environment.sh b/package/device_calendar_plus/example/ios/Flutter/flutter_export_environment.sh new file mode 100755 index 00000000..100ac08d --- /dev/null +++ b/package/device_calendar_plus/example/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/cloud/sdk/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/cloud/Downloads/device_calender_plus/device_calendar_plus-0.3.1/example" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "FLUTTER_CLI_BUILD_MODE=debug" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/package/device_calendar_plus/example/ios/Podfile b/package/device_calendar_plus/example/ios/Podfile new file mode 100644 index 00000000..620e46eb --- /dev/null +++ b/package/device_calendar_plus/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/package/device_calendar_plus/example/ios/Podfile.lock b/package/device_calendar_plus/example/ios/Podfile.lock new file mode 100644 index 00000000..c0bba85f --- /dev/null +++ b/package/device_calendar_plus/example/ios/Podfile.lock @@ -0,0 +1,28 @@ +PODS: + - device_calendar_plus_ios (0.0.1): + - Flutter + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + +DEPENDENCIES: + - device_calendar_plus_ios (from `.symlinks/plugins/device_calendar_plus_ios/ios`) + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + +EXTERNAL SOURCES: + device_calendar_plus_ios: + :path: ".symlinks/plugins/device_calendar_plus_ios/ios" + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + +SPEC CHECKSUMS: + device_calendar_plus_ios: 34dca387f44fad29b840096a840f94edc55af858 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.pbxproj b/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..42bed359 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 90635437DFB068E040820C11 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF31540B1A7F63F9CCFABD70 /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + C31B88CF42DB7DF2C6CFC737 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2117130F85BBAB2E93535752 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 03BADFE508B8EA0BB14DC18C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1D3752562D0E8854792A7D51 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 2117130F85BBAB2E93535752 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 23F7FA9D412243D6B596B9BB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 75AD1B0E07FDA4B644D3FE93 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C842EA1CFFB3E90B842E497B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E823B4929251B419D87BC8C3 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EF31540B1A7F63F9CCFABD70 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 90635437DFB068E040820C11 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DD86C1416E89EFA0F12646D2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C31B88CF42DB7DF2C6CFC737 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 345967FC13CFEA0EADC4DDFB /* Frameworks */ = { + isa = PBXGroup; + children = ( + EF31540B1A7F63F9CCFABD70 /* Pods_Runner.framework */, + 2117130F85BBAB2E93535752 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4A9096EFAB9F6C52EC683391 /* Pods */ = { + isa = PBXGroup; + children = ( + C842EA1CFFB3E90B842E497B /* Pods-Runner.debug.xcconfig */, + 23F7FA9D412243D6B596B9BB /* Pods-Runner.release.xcconfig */, + 03BADFE508B8EA0BB14DC18C /* Pods-Runner.profile.xcconfig */, + E823B4929251B419D87BC8C3 /* Pods-RunnerTests.debug.xcconfig */, + 75AD1B0E07FDA4B644D3FE93 /* Pods-RunnerTests.release.xcconfig */, + 1D3752562D0E8854792A7D51 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 4A9096EFAB9F6C52EC683391 /* Pods */, + 345967FC13CFEA0EADC4DDFB /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 959C422494847625B2E119C8 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + DD86C1416E89EFA0F12646D2 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + A6A9218B978606CCE099305F /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E356C884A5DAF4676B7F4EF3 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 959C422494847625B2E119C8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + A6A9218B978606CCE099305F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E356C884A5DAF4676B7F4EF3 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UDCW28VB9U; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E823B4929251B419D87BC8C3 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 75AD1B0E07FDA4B644D3FE93 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1D3752562D0E8854792A7D51 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UDCW28VB9U; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = UDCW28VB9U; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/package/device_calendar_plus/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/package/device_calendar_plus/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..e3773d42 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/device_calendar_plus/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/package/device_calendar_plus/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/package/device_calendar_plus/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/package/device_calendar_plus/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/package/device_calendar_plus/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/package/device_calendar_plus/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/package/device_calendar_plus/example/ios/Runner/AppDelegate.swift b/package/device_calendar_plus/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..62666446 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/package/device_calendar_plus/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/package/device_calendar_plus/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/device_calendar_plus/example/ios/Runner/Base.lproj/Main.storyboard b/package/device_calendar_plus/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/device_calendar_plus/example/ios/Runner/GeneratedPluginRegistrant.h b/package/device_calendar_plus/example/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 00000000..7a890927 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +NS_ASSUME_NONNULL_END +#endif /* GeneratedPluginRegistrant_h */ diff --git a/package/device_calendar_plus/example/ios/Runner/GeneratedPluginRegistrant.m b/package/device_calendar_plus/example/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 00000000..5e513dd3 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,28 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#import "GeneratedPluginRegistrant.h" + +#if __has_include() +#import +#else +@import device_calendar_plus_ios; +#endif + +#if __has_include() +#import +#else +@import integration_test; +#endif + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { + [DeviceCalendarPlusIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"DeviceCalendarPlusIosPlugin"]]; + [IntegrationTestPlugin registerWithRegistrar:[registry registrarForPlugin:@"IntegrationTestPlugin"]]; +} + +@end diff --git a/package/device_calendar_plus/example/ios/Runner/Info.plist b/package/device_calendar_plus/example/ios/Runner/Info.plist new file mode 100644 index 00000000..57f088db --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSCalendarsUsageDescription + This app needs access to your calendar to demonstrate calendar functionality. + NSCalendarsWriteOnlyAccessUsageDescription + This app needs write access to your calendar to add events. + + diff --git a/package/device_calendar_plus/example/ios/Runner/Runner-Bridging-Header.h b/package/device_calendar_plus/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/package/device_calendar_plus/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/package/device_calendar_plus/example/ios/RunnerTests/RunnerTests.swift b/package/device_calendar_plus/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/package/device_calendar_plus/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/package/device_calendar_plus/example/lib/main.dart b/package/device_calendar_plus/example/lib/main.dart new file mode 100644 index 00000000..dbe66532 --- /dev/null +++ b/package/device_calendar_plus/example/lib/main.dart @@ -0,0 +1,912 @@ +import 'package:device_calendar_plus/device_calendar_plus.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // TRY THIS: Try running your application with "flutter run". You'll see + // the application has a purple toolbar. Then, without quitting the app, + // try changing the seedColor in the colorScheme below to Colors.green + // and then invoke "hot reload" (save your changes or press the "hot + // reload" button in a Flutter-supported IDE, or press "r" if you used + // the command line to start the app). + // + // Notice that the counter didn't reset back to zero; the application + // state is not lost during the reload. To reset the state, use hot + // restart instead. + // + // This works for code too, not just values: Most code changes can be + // tested with just a hot reload. + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + ), + home: const MyHomePage(title: 'Device Calendar Plus Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List _calendars = []; + bool _isLoadingCalendars = false; + final Set _selectedCalendarIds = {}; + List _events = []; + bool _isLoadingEvents = false; + + @override + void initState() { + super.initState(); + } + + Future _requestPermissions() async { + try { + final status = await DeviceCalendar.instance.requestPermissions(); + + if (!mounted) return; + + String message; + switch (status) { + case CalendarPermissionStatus.granted: + message = 'Permission granted! Full read/write access to calendars.'; + break; + case CalendarPermissionStatus.writeOnly: + message = + 'Write-only permission granted (iOS 17+). Can add events but not read existing ones.'; + break; + case CalendarPermissionStatus.denied: + message = + 'Permission denied. Please enable calendar access in Settings.'; + break; + case CalendarPermissionStatus.restricted: + message = + 'Calendar access is restricted by device policies (MDM/parental controls).'; + break; + case CalendarPermissionStatus.notDetermined: + message = 'Permission not yet determined.'; + break; + } + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Calendar Permission Status'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } on DeviceCalendarException catch (e) { + // Developer configuration error (missing manifest permissions) + if (!mounted) return; + + final title = e.errorCode == DeviceCalendarError.permissionsNotDeclared + ? 'Configuration Error' + : 'Calendar Error'; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: SingleChildScrollView( + child: Text( + e.message, + style: const TextStyle(fontFamily: 'monospace', fontSize: 12), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } catch (e) { + // Other errors + if (!mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: Text('Failed to request permissions: $e'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + } + + Future _loadCalendars() async { + setState(() { + _isLoadingCalendars = true; + }); + + try { + final calendars = await DeviceCalendar.instance.listCalendars(); + + setState(() { + _calendars = calendars; + _isLoadingCalendars = false; + }); + } on DeviceCalendarException catch (e) { + setState(() { + _isLoadingCalendars = false; + }); + + if (!mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Calendar Error'), + content: Text(e.message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } catch (e) { + setState(() { + _isLoadingCalendars = false; + }); + + if (!mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: Text('Failed to load calendars: $e'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + } + + Color _parseColor(String? colorHex) { + if (colorHex == null || colorHex.isEmpty) { + return Colors.grey; + } + try { + final hexColor = colorHex.replaceAll('#', ''); + return Color(int.parse('FF$hexColor', radix: 16)); + } catch (e) { + return Colors.grey; + } + } + + Future _loadEvents() async { + setState(() { + _isLoadingEvents = true; + }); + + try { + final now = DateTime.now(); + final startDate = DateTime(now.year, now.month - 3, now.day); + final endDate = DateTime(now.year, now.month + 3, now.day); + + final events = await DeviceCalendar.instance.listEvents( + startDate, + endDate, + calendarIds: + _selectedCalendarIds.isEmpty ? null : _selectedCalendarIds.toList(), + ); + + setState(() { + _events = events; + _isLoadingEvents = false; + }); + } on DeviceCalendarException catch (e) { + setState(() { + _isLoadingEvents = false; + }); + + if (!mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Calendar Error'), + content: Text(e.message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } catch (e) { + setState(() { + _isLoadingEvents = false; + }); + + if (!mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: Text('Failed to load events: $e'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + } + + Future _updateEvent(Event event) async { + try { + // Add an exclamation mark to the title + final newTitle = '${event.title}!'; + + await DeviceCalendar.instance.updateEvent( + eventId: event.instanceId, + title: newTitle, + ); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Updated: $newTitle'), + duration: const Duration(seconds: 2), + ), + ); + + // Reload events to show the change + await _loadEvents(); + } on DeviceCalendarException catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: ${e.message}')), + ); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update event: $e')), + ); + } + } + + Future _deleteEvent(Event event) async { + try { + await DeviceCalendar.instance.deleteEvent(eventId: event.instanceId); + + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Deleted: ${event.title}'), + duration: const Duration(seconds: 2), + ), + ); + + // Reload events to show the change + await _loadEvents(); + } on DeviceCalendarException catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: ${e.message}')), + ); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to delete event: $e')), + ); + } + } + + Future _showEventDetails(Event event) async { + try { + // Fetch the specific event instance using instanceId + // For recurring events, instanceId includes the timestamp + final fetchedEvent = await DeviceCalendar.instance.getEvent( + event.instanceId, + ); + + if (fetchedEvent == null) { + if (!mounted) return; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: const Text('Event not found'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + return; + } + + if (!mounted) return; + + final calendar = _calendars.firstWhere( + (c) => c.id == fetchedEvent.calendarId, + orElse: () => _calendars.first, + ); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(fetchedEvent.title), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildDetailRow( + Icons.calendar_today, + 'Calendar', + calendar.name, + ), + const SizedBox(height: 12), + _buildDetailRow( + Icons.access_time, + 'Time', + fetchedEvent.isAllDay + ? 'All Day' + : '${_formatEventDate(fetchedEvent.startDate)} • ${_formatEventTime(fetchedEvent)}', + ), + if (fetchedEvent.location != null) ...[ + const SizedBox(height: 12), + _buildDetailRow( + Icons.location_on, + 'Location', + fetchedEvent.location!, + ), + ], + if (fetchedEvent.description != null) ...[ + const SizedBox(height: 12), + _buildDetailRow( + Icons.notes, + 'Description', + fetchedEvent.description!, + ), + ], + if (fetchedEvent.timeZone != null) ...[ + const SizedBox(height: 12), + _buildDetailRow( + Icons.public, + 'Timezone', + fetchedEvent.timeZone!, + ), + ], + if (fetchedEvent.isRecurring) ...[ + const SizedBox(height: 12), + _buildDetailRow( + Icons.repeat, + 'Recurring', + 'Yes', + ), + ], + const SizedBox(height: 12), + _buildDetailRow( + Icons.info_outline, + 'Status', + '${fetchedEvent.status.name} • ${fetchedEvent.availability.name}', + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () async { + try { + print('opened'); + await DeviceCalendar.instance + .showEventModal(fetchedEvent.instanceId); + print('closed'); + } on DeviceCalendarException catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: ${e.message}')), + ); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to show event: $e')), + ); + } + }, + child: const Text('Show in Modal'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ), + ); + } on DeviceCalendarException catch (e) { + if (!mounted) return; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Calendar Error'), + content: Text(e.message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } catch (e) { + if (!mounted) return; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: Text('Failed to load event details: $e'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } + } + + Widget _buildDetailRow(IconData icon, String label, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 20, color: Colors.grey[600]), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + value, + style: const TextStyle(fontSize: 14), + ), + ], + ), + ), + ], + ); + } + + String _formatEventTime(Event event) { + if (event.isAllDay) { + return 'All Day'; + } + final startTime = + '${event.startDate.hour.toString().padLeft(2, '0')}:${event.startDate.minute.toString().padLeft(2, '0')}'; + final endTime = + '${event.endDate.hour.toString().padLeft(2, '0')}:${event.endDate.minute.toString().padLeft(2, '0')}'; + return '$startTime - $endTime'; + } + + String _formatEventDate(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final eventDay = DateTime(date.year, date.month, date.day); + + if (eventDay == today) { + return 'Today'; + } else if (eventDay == today.add(const Duration(days: 1))) { + return 'Tomorrow'; + } else if (eventDay == today.subtract(const Duration(days: 1))) { + return 'Yesterday'; + } else { + final months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' + ]; + return '${months[date.month - 1]} ${date.day}, ${date.year}'; + } + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // TRY THIS: Try changing the color here to a specific color (to + // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar + // change color while the other colors stay the same. + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const Text('Permissions'), + const SizedBox(height: 8), + ElevatedButton( + onPressed: _requestPermissions, + child: const Text('Request Calendar Permissions'), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Calendars', + style: Theme.of(context).textTheme.titleLarge, + ), + if (_calendars.isNotEmpty) + Text( + '${_calendars.length} found', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _isLoadingCalendars ? null : _loadCalendars, + icon: _isLoadingCalendars + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + label: Text(_isLoadingCalendars + ? 'Loading...' + : 'Load Calendars'), + ), + if (_calendars.isNotEmpty) ...[ + const SizedBox(height: 16), + Text( + 'Select calendars to fetch events:', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: _calendars.map((calendar) { + final color = _parseColor(calendar.colorHex); + final luminance = color.computeLuminance(); + final textColor = + luminance > 0.5 ? Colors.black : Colors.white; + final isSelected = + _selectedCalendarIds.contains(calendar.id); + + return FilterChip( + selected: isSelected, + backgroundColor: color.withValues(alpha: 0.3), + selectedColor: color, + checkmarkColor: textColor, + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + calendar.name, + style: TextStyle( + color: isSelected ? textColor : null, + fontWeight: calendar.isPrimary + ? FontWeight.bold + : FontWeight.normal, + ), + ), + if (calendar.isPrimary) ...[ + const SizedBox(width: 4), + Icon( + Icons.star, + size: 14, + color: isSelected ? textColor : null, + ), + ], + if (calendar.readOnly) ...[ + const SizedBox(width: 4), + Icon( + Icons.lock, + size: 14, + color: isSelected ? textColor : null, + ), + ], + ], + ), + avatar: calendar.accountName != null + ? CircleAvatar( + backgroundColor: isSelected + ? color.withValues(alpha: 0.3) + : color.withValues(alpha: 0.2), + child: Text( + calendar.accountName![0].toUpperCase(), + style: TextStyle( + color: isSelected ? textColor : null, + fontSize: 12, + ), + ), + ) + : null, + onSelected: (selected) { + setState(() { + if (selected) { + _selectedCalendarIds.add(calendar.id); + } else { + _selectedCalendarIds.remove(calendar.id); + } + }); + }, + ); + }).toList(), + ), + ], + ], + ), + ), + ), + if (_calendars.isNotEmpty) ...[ + const SizedBox(height: 16), + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Events', + style: Theme.of(context).textTheme.titleLarge, + ), + if (_events.isNotEmpty) + Text( + '${_events.length} found', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + const SizedBox(height: 8), + Text( + 'From 3 months ago to 3 months ahead', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _isLoadingEvents ? null : _loadEvents, + icon: _isLoadingEvents + ? const SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.event), + label: Text(_isLoadingEvents + ? 'Loading...' + : _selectedCalendarIds.isEmpty + ? 'Fetch Events (All calendars)' + : 'Fetch Events (${_selectedCalendarIds.length} selected)'), + ), + if (_events.isNotEmpty) ...[ + const SizedBox(height: 16), + SizedBox( + height: 400, + child: ListView.separated( + itemCount: _events.length, + separatorBuilder: (context, index) => + const Divider(), + itemBuilder: (context, index) { + final event = _events[index]; + final calendar = _calendars.firstWhere( + (c) => c.id == event.calendarId, + orElse: () => _calendars.first, + ); + final color = _parseColor(calendar.colorHex); + + return ListTile( + onTap: () => _showEventDetails(event), + leading: Container( + width: 4, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2), + ), + ), + title: Text( + event.title, + style: const TextStyle( + fontWeight: FontWeight.w500), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + '${_formatEventDate(event.startDate)} • ${_formatEventTime(event)}', + style: + Theme.of(context).textTheme.bodySmall, + ), + if (event.timeZone != null) ...[ + const SizedBox(height: 2), + Row( + children: [ + const Icon(Icons.access_time, + size: 12), + const SizedBox(width: 4), + Text( + event.timeZone!, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ], + if (event.location != null) ...[ + const SizedBox(height: 2), + Row( + children: [ + const Icon(Icons.location_on, + size: 12), + const SizedBox(width: 4), + Expanded( + child: Text( + event.location!, + style: Theme.of(context) + .textTheme + .bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + const SizedBox(height: 2), + Text( + calendar.name, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: color, + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (event.isAllDay) + const Icon(Icons.all_inclusive, size: 16), + if (event.status == EventStatus.tentative) + const Icon(Icons.help_outline, size: 16), + if (event.status == EventStatus.canceled) + const Icon(Icons.cancel_outlined, + size: 16), + const SizedBox(width: 4), + IconButton( + icon: const Icon(Icons.edit, size: 18), + onPressed: () => _updateEvent(event), + tooltip: 'Update', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + IconButton( + icon: const Icon(Icons.delete, size: 18), + onPressed: () => _deleteEvent(event), + tooltip: 'Delete', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ); + }, + ), + ), + ], + ], + ), + ), + ), + ], + ], + ), + ), + // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} diff --git a/package/device_calendar_plus/example/linux/CMakeLists.txt b/package/device_calendar_plus/example/linux/CMakeLists.txt new file mode 100644 index 00000000..e337802e --- /dev/null +++ b/package/device_calendar_plus/example/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "to.bullet.example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/package/device_calendar_plus/example/linux/flutter/CMakeLists.txt b/package/device_calendar_plus/example/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/package/device_calendar_plus/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/package/device_calendar_plus/example/linux/flutter/generated_plugin_registrant.cc b/package/device_calendar_plus/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..e71a16d2 --- /dev/null +++ b/package/device_calendar_plus/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/package/device_calendar_plus/example/linux/flutter/generated_plugin_registrant.h b/package/device_calendar_plus/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/package/device_calendar_plus/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/package/device_calendar_plus/example/linux/flutter/generated_plugins.cmake b/package/device_calendar_plus/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..2e1de87a --- /dev/null +++ b/package/device_calendar_plus/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/package/device_calendar_plus/example/linux/runner/CMakeLists.txt b/package/device_calendar_plus/example/linux/runner/CMakeLists.txt new file mode 100644 index 00000000..e97dabc7 --- /dev/null +++ b/package/device_calendar_plus/example/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/package/device_calendar_plus/example/linux/runner/main.cc b/package/device_calendar_plus/example/linux/runner/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/package/device_calendar_plus/example/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/package/device_calendar_plus/example/linux/runner/my_application.cc b/package/device_calendar_plus/example/linux/runner/my_application.cc new file mode 100644 index 00000000..f6904ba3 --- /dev/null +++ b/package/device_calendar_plus/example/linux/runner/my_application.cc @@ -0,0 +1,144 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView *view) +{ + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/package/device_calendar_plus/example/linux/runner/my_application.h b/package/device_calendar_plus/example/linux/runner/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/package/device_calendar_plus/example/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/package/device_calendar_plus/example/macos/Flutter/Flutter-Debug.xcconfig b/package/device_calendar_plus/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/package/device_calendar_plus/example/macos/Flutter/Flutter-Release.xcconfig b/package/device_calendar_plus/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/package/device_calendar_plus/example/macos/Flutter/GeneratedPluginRegistrant.swift b/package/device_calendar_plus/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..cccf817a --- /dev/null +++ b/package/device_calendar_plus/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/package/device_calendar_plus/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/package/device_calendar_plus/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig new file mode 100644 index 00000000..295eb2b3 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -0,0 +1,12 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/cloud/sdk/flutter +FLUTTER_APPLICATION_PATH=/Users/cloud/Downloads/device_calender_plus/device_calendar_plus-0.3.1/example +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NUMBER=1 +FLUTTER_CLI_BUILD_MODE=debug +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/package/device_calendar_plus/example/macos/Flutter/ephemeral/flutter_export_environment.sh b/package/device_calendar_plus/example/macos/Flutter/ephemeral/flutter_export_environment.sh new file mode 100755 index 00000000..6c9803e6 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/cloud/sdk/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/cloud/Downloads/device_calender_plus/device_calendar_plus-0.3.1/example" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "FLUTTER_CLI_BUILD_MODE=debug" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/package/device_calendar_plus/example/macos/Podfile b/package/device_calendar_plus/example/macos/Podfile new file mode 100644 index 00000000..ff5ddb3b --- /dev/null +++ b/package/device_calendar_plus/example/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/package/device_calendar_plus/example/macos/Runner.xcodeproj/project.pbxproj b/package/device_calendar_plus/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..b914e7af --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/package/device_calendar_plus/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/package/device_calendar_plus/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/package/device_calendar_plus/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/package/device_calendar_plus/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..ac78810c --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/device_calendar_plus/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/package/device_calendar_plus/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/package/device_calendar_plus/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/package/device_calendar_plus/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/package/device_calendar_plus/example/macos/Runner/AppDelegate.swift b/package/device_calendar_plus/example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/package/device_calendar_plus/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/package/device_calendar_plus/example/macos/Runner/Base.lproj/MainMenu.xib b/package/device_calendar_plus/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package/device_calendar_plus/example/macos/Runner/Configs/AppInfo.xcconfig b/package/device_calendar_plus/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..a95b9293 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = to.bullet.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 to.bullet. All rights reserved. diff --git a/package/device_calendar_plus/example/macos/Runner/Configs/Debug.xcconfig b/package/device_calendar_plus/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/package/device_calendar_plus/example/macos/Runner/Configs/Release.xcconfig b/package/device_calendar_plus/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/package/device_calendar_plus/example/macos/Runner/Configs/Warnings.xcconfig b/package/device_calendar_plus/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/package/device_calendar_plus/example/macos/Runner/DebugProfile.entitlements b/package/device_calendar_plus/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..dddb8a30 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/package/device_calendar_plus/example/macos/Runner/Info.plist b/package/device_calendar_plus/example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/package/device_calendar_plus/example/macos/Runner/MainFlutterWindow.swift b/package/device_calendar_plus/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/package/device_calendar_plus/example/macos/Runner/Release.entitlements b/package/device_calendar_plus/example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/package/device_calendar_plus/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/package/device_calendar_plus/example/macos/RunnerTests/RunnerTests.swift b/package/device_calendar_plus/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/package/device_calendar_plus/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/package/device_calendar_plus/example/pubspec.lock b/package/device_calendar_plus/example/pubspec.lock new file mode 100644 index 00000000..66c5de7d --- /dev/null +++ b/package/device_calendar_plus/example/pubspec.lock @@ -0,0 +1,307 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + device_calendar_plus: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.3.1" + device_calendar_plus_android: + dependency: transitive + description: + name: device_calendar_plus_android + sha256: f66f363273e0b174fdb3f98030a8c67ae9e9dfcb6122e4ca842881f976313db0 + url: "https://pub.dev" + source: hosted + version: "0.3.1" + device_calendar_plus_ios: + dependency: transitive + description: + name: device_calendar_plus_ios + sha256: c9b234091d3edc78871ed524077c262ce66217b960b7942ef071032e47b60fa8 + url: "https://pub.dev" + source: hosted + version: "0.3.1" + device_calendar_plus_platform_interface: + dependency: transitive + description: + name: device_calendar_plus_platform_interface + sha256: fb4d893d04b10ab00bc175be8a374aad08a7c4ad7d763c7ff48036d76be7604b + url: "https://pub.dev" + source: hosted + version: "0.3.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/package/device_calendar_plus/example/pubspec.yaml b/package/device_calendar_plus/example/pubspec.yaml new file mode 100644 index 00000000..1e6e8972 --- /dev/null +++ b/package/device_calendar_plus/example/pubspec.yaml @@ -0,0 +1,95 @@ +name: example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + + + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# 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 +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ">=3.5.0 <4.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + device_calendar_plus: + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/package/device_calendar_plus/example/run_integration_tests.sh b/package/device_calendar_plus/example/run_integration_tests.sh new file mode 100755 index 00000000..895f6ee4 --- /dev/null +++ b/package/device_calendar_plus/example/run_integration_tests.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +# Integration Test Runner for Device Calendar Plus +# This script automatically grants calendar permissions and runs integration tests +# on iOS simulators or Android emulators. + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Check if device ID is provided +if [ -z "$1" ]; then + echo -e "${RED}❌ Error: Device ID required${NC}" + echo "" + echo "Usage: $0 " + echo "" + echo "Find device IDs with: flutter devices" + echo "" + echo "Examples:" + echo " $0 F0A86A59-EB1B-4AA2-B487-8D3AA46664D8 # iOS simulator" + echo " $0 emulator-5554 # Android emulator" + echo " $0 booted # Currently booted iOS simulator" + exit 1 +fi + +DEVICE_ID="$1" + +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BLUE} Device Calendar Plus - Integration Tests${NC}" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" + +# Detect platform +if [[ "$DEVICE_ID" == *"emulator"* ]] || flutter devices | grep "$DEVICE_ID" | grep -q "android"; then + PLATFORM="android" +elif flutter devices | grep "$DEVICE_ID" | grep -q "ios"; then + PLATFORM="ios" +else + echo -e "${RED}❌ Could not detect platform for device: $DEVICE_ID${NC}" + echo "" + echo "Run 'flutter devices' to see available devices" + exit 1 +fi + +echo -e "${GREEN}✓${NC} Device ID: ${YELLOW}$DEVICE_ID${NC}" +echo -e "${GREEN}✓${NC} Platform: ${YELLOW}$PLATFORM${NC}" +echo "" + +# Grant permissions based on platform +if [ "$PLATFORM" == "ios" ]; then + echo "🍎 iOS detected" + echo "📱 Granting calendar permissions via xcrun..." + + xcrun simctl privacy "$DEVICE_ID" grant calendar to.bullet.example + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓${NC} Calendar permissions granted" + else + echo -e "${YELLOW}⚠️ Warning: Could not grant permissions${NC}" + echo " The simulator may need to be booted first" + echo " Tests may prompt for permissions on first run" + fi + echo "" + +elif [ "$PLATFORM" == "android" ]; then + echo "🤖 Android detected" + echo " (Permissions will be granted automatically by test driver)" + echo "" +fi + +# Run the integration tests +echo "🚀 Running integration tests on $DEVICE_ID..." +echo "" + +cd "$(dirname "$0")" + +# Build test command based on platform +if [ "$PLATFORM" == "android" ]; then + # Use custom driver that grants permissions via adb + if flutter drive \ + --driver=integration_test/integration_test_driver.dart \ + --target=integration_test/device_calendar_test.dart \ + -d "$DEVICE_ID"; then + EXIT_CODE=0 + else + EXIT_CODE=1 + fi +else + # iOS: Use regular flutter test + if flutter test integration_test/device_calendar_test.dart -d "$DEVICE_ID"; then + EXIT_CODE=0 + else + EXIT_CODE=1 + fi +fi + +echo "" +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + +if [ $EXIT_CODE -eq 0 ]; then + echo -e "${GREEN}✅ All integration tests passed!${NC}" +else + echo -e "${RED}❌ Some tests failed${NC}" + + if [ "$PLATFORM" == "ios" ]; then + echo "" + echo "If tests failed due to permissions:" + echo " 1. Ensure the simulator is booted before running the script" + echo " 2. Try: xcrun simctl privacy $DEVICE_ID reset calendar" + echo " 3. Then run the script again" + fi +fi + +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + +exit $EXIT_CODE + diff --git a/package/device_calendar_plus/example/web/favicon.png b/package/device_calendar_plus/example/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/package/device_calendar_plus/example/web/favicon.png differ diff --git a/package/device_calendar_plus/example/web/icons/Icon-192.png b/package/device_calendar_plus/example/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/package/device_calendar_plus/example/web/icons/Icon-192.png differ diff --git a/package/device_calendar_plus/example/web/icons/Icon-512.png b/package/device_calendar_plus/example/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/package/device_calendar_plus/example/web/icons/Icon-512.png differ diff --git a/package/device_calendar_plus/example/web/icons/Icon-maskable-192.png b/package/device_calendar_plus/example/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/package/device_calendar_plus/example/web/icons/Icon-maskable-192.png differ diff --git a/package/device_calendar_plus/example/web/icons/Icon-maskable-512.png b/package/device_calendar_plus/example/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/package/device_calendar_plus/example/web/icons/Icon-maskable-512.png differ diff --git a/package/device_calendar_plus/example/web/index.html b/package/device_calendar_plus/example/web/index.html new file mode 100644 index 00000000..29b58086 --- /dev/null +++ b/package/device_calendar_plus/example/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + diff --git a/package/device_calendar_plus/example/web/manifest.json b/package/device_calendar_plus/example/web/manifest.json new file mode 100644 index 00000000..096edf8f --- /dev/null +++ b/package/device_calendar_plus/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/package/device_calendar_plus/example/windows/CMakeLists.txt b/package/device_calendar_plus/example/windows/CMakeLists.txt new file mode 100644 index 00000000..d960948a --- /dev/null +++ b/package/device_calendar_plus/example/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/package/device_calendar_plus/example/windows/flutter/CMakeLists.txt b/package/device_calendar_plus/example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..903f4899 --- /dev/null +++ b/package/device_calendar_plus/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/package/device_calendar_plus/example/windows/flutter/generated_plugin_registrant.cc b/package/device_calendar_plus/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..8b6d4680 --- /dev/null +++ b/package/device_calendar_plus/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/package/device_calendar_plus/example/windows/flutter/generated_plugin_registrant.h b/package/device_calendar_plus/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/package/device_calendar_plus/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/package/device_calendar_plus/example/windows/flutter/generated_plugins.cmake b/package/device_calendar_plus/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..b93c4c30 --- /dev/null +++ b/package/device_calendar_plus/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/package/device_calendar_plus/example/windows/runner/CMakeLists.txt b/package/device_calendar_plus/example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/package/device_calendar_plus/example/windows/runner/Runner.rc b/package/device_calendar_plus/example/windows/runner/Runner.rc new file mode 100644 index 00000000..c9b7af44 --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "to.bullet" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 to.bullet. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/package/device_calendar_plus/example/windows/runner/flutter_window.cpp b/package/device_calendar_plus/example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..955ee303 --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/package/device_calendar_plus/example/windows/runner/flutter_window.h b/package/device_calendar_plus/example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/package/device_calendar_plus/example/windows/runner/main.cpp b/package/device_calendar_plus/example/windows/runner/main.cpp new file mode 100644 index 00000000..a61bf80d --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/package/device_calendar_plus/example/windows/runner/resource.h b/package/device_calendar_plus/example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/package/device_calendar_plus/example/windows/runner/resources/app_icon.ico b/package/device_calendar_plus/example/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/package/device_calendar_plus/example/windows/runner/resources/app_icon.ico differ diff --git a/package/device_calendar_plus/example/windows/runner/runner.exe.manifest b/package/device_calendar_plus/example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..153653e8 --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/package/device_calendar_plus/example/windows/runner/utils.cpp b/package/device_calendar_plus/example/windows/runner/utils.cpp new file mode 100644 index 00000000..3a0b4651 --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/package/device_calendar_plus/example/windows/runner/utils.h b/package/device_calendar_plus/example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/package/device_calendar_plus/example/windows/runner/win32_window.cpp b/package/device_calendar_plus/example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/package/device_calendar_plus/example/windows/runner/win32_window.h b/package/device_calendar_plus/example/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/package/device_calendar_plus/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/package/device_calendar_plus/lib/device_calendar_plus.dart b/package/device_calendar_plus/lib/device_calendar_plus.dart new file mode 100644 index 00000000..4c5bfc87 --- /dev/null +++ b/package/device_calendar_plus/lib/device_calendar_plus.dart @@ -0,0 +1,767 @@ +import 'package:device_calendar_plus_platform_interface/device_calendar_plus_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'src/calendar.dart'; +import 'src/calendar_permission_status.dart'; +import 'src/event.dart'; +import 'src/event_availability.dart'; +import 'src/platform_exception_converter.dart'; + +export 'src/calendar.dart'; +export 'src/calendar_permission_status.dart'; +export 'src/device_calendar_error.dart'; +export 'src/event.dart'; +export 'src/event_availability.dart'; +export 'src/event_status.dart'; +export 'src/platform_exception_codes.dart'; + +/// Main API for accessing device calendar functionality. +class DeviceCalendar { + DeviceCalendar._internal(); + + static final DeviceCalendar instance = DeviceCalendar._internal(); + + factory DeviceCalendar() => instance; + + /// Requests calendar permissions from the user. + /// + /// On first call, this will show the system permission dialog. + /// On subsequent calls, it returns the current permission status. + /// + /// Returns a [CalendarPermissionStatus] indicating the result + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// final status = await plugin.requestPermissions(); + /// if (status == CalendarPermissionStatus.granted) { + /// // Access calendars + /// } else if (status == CalendarPermissionStatus.denied) { + /// // Show "Enable in Settings" message + /// } else if (status == CalendarPermissionStatus.restricted) { + /// // Show "Contact administrator" message + /// } + /// ``` + Future requestPermissions() async { + return _handlePermissionRequest( + () => DeviceCalendarPlusPlatform.instance.requestPermissions(), + ); + } + + /// Checks the current calendar permission status WITHOUT requesting permissions. + /// + /// Unlike [requestPermissions], this method will NOT prompt the user for + /// permissions if they haven't been granted yet. It only checks the current status. + /// + /// Use this method if you want to check permissions before deciding whether + /// to call [requestPermissions], or when you want to verify permissions without + /// triggering the system permission dialog. + /// + /// Returns the current [CalendarPermissionStatus]. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// final status = await plugin.hasPermissions(); + /// if (status == CalendarPermissionStatus.granted) { + /// // Permissions already granted + /// final calendars = await plugin.listCalendars(); + /// } else if (status == CalendarPermissionStatus.notDetermined) { + /// // User hasn't been asked yet + /// final newStatus = await plugin.requestPermissions(); + /// } + /// ``` + Future hasPermissions() async { + return _handlePermissionRequest( + () => DeviceCalendarPlusPlatform.instance.hasPermissions(), + ); + } + + /// Opens the app's settings page in the system settings. + /// + /// This is useful when permissions have been denied and you want to guide + /// the user to manually enable calendar permissions in the system settings. + /// + /// On iOS, this opens the app's specific settings page directly. + /// On Android, this opens the app info page where users can navigate to permissions. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// final status = await plugin.hasPermissions(); + /// if (status == CalendarPermissionStatus.denied) { + /// // Show dialog explaining why permission is needed + /// showDialog( + /// context: context, + /// builder: (context) => AlertDialog( + /// title: Text('Calendar Permission Required'), + /// content: Text('Please enable calendar access in settings.'), + /// actions: [ + /// TextButton( + /// onPressed: () { + /// Navigator.pop(context); + /// plugin.openAppSettings(); + /// }, + /// child: Text('Open Settings'), + /// ), + /// ], + /// ), + /// ); + /// } + /// ``` + Future openAppSettings() async { + try { + await DeviceCalendarPlusPlatform.instance.openAppSettings(); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Helper method to handle permission requests and convert status values + Future _handlePermissionRequest( + Future Function() permissionCall, + ) async { + try { + final String? statusValue = await permissionCall(); + return _convertStatusValue(statusValue); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Converts a status value string to CalendarPermissionStatus + CalendarPermissionStatus _convertStatusValue(String? statusValue) { + // Default to denied if status is null or unrecognized + if (statusValue == null) { + return CalendarPermissionStatus.denied; + } + + // Parse the enum value by name + try { + return CalendarPermissionStatus.values.firstWhere( + (e) => e.name == statusValue, + orElse: () => CalendarPermissionStatus.denied, + ); + } catch (_) { + return CalendarPermissionStatus.denied; + } + } + + /// Lists all calendars available on the device. + /// + /// Returns a list of [Calendar] objects representing each calendar. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// final calendars = await plugin.listCalendars(); + /// for (final calendar in calendars) { + /// print('${calendar.name} (${calendar.id})'); + /// print(' Read-only: ${calendar.readOnly}'); + /// print(' Primary: ${calendar.isPrimary}'); + /// print(' Color: ${calendar.colorHex}'); + /// } + /// ``` + Future> listCalendars() async { + try { + final List> rawCalendars = + await DeviceCalendarPlusPlatform.instance.listCalendars(); + return rawCalendars.map((map) => Calendar.fromMap(map)).toList(); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Creates a new calendar on the device. + /// + /// [name] is the display name for the calendar (required). + /// [colorHex] is an optional color in #RRGGBB format (e.g., "#FF5733"). + /// + /// Returns the ID of the newly created calendar. + /// + /// The calendar is created in the device's local storage. + /// Requires calendar write permissions - call [requestPermissions] first. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// + /// // Create a calendar with just a name + /// final calendarId = await plugin.createCalendar(name: 'My Calendar'); + /// + /// // Create a calendar with a name and color + /// final coloredCalendarId = await plugin.createCalendar( + /// name: 'Work Calendar', + /// colorHex: '#FF5733', + /// ); + /// ``` + Future createCalendar({ + required String name, + String? colorHex, + }) async { + if (name.trim().isEmpty) { + throw ArgumentError.value( + name, + 'name', + 'Calendar name cannot be empty', + ); + } + + try { + final String calendarId = await DeviceCalendarPlusPlatform.instance + .createCalendar(name, colorHex); + return calendarId; + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Updates an existing calendar on the device. + /// + /// [calendarId] is the ID of the calendar to update. + /// [name] is the new display name for the calendar (optional). + /// [colorHex] is the new color in #RRGGBB format (optional, e.g., "#FF5733"). + /// + /// At least one of [name] or [colorHex] must be provided. + /// Requires calendar write permissions - call [requestPermissions] first. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// + /// // Update just the name + /// await plugin.updateCalendar(calendarId, name: 'New Name'); + /// + /// // Update just the color + /// await plugin.updateCalendar(calendarId, colorHex: '#FF5733'); + /// + /// // Update both name and color + /// await plugin.updateCalendar( + /// calendarId, + /// name: 'New Name', + /// colorHex: '#FF5733', + /// ); + /// ``` + Future updateCalendar( + String calendarId, { + String? name, + String? colorHex, + }) async { + // Validate that at least one parameter is provided + if (name == null && colorHex == null) { + throw ArgumentError( + 'At least one of name or colorHex must be provided', + ); + } + + // Validate name if provided + if (name != null && name.trim().isEmpty) { + throw ArgumentError.value( + name, + 'name', + 'Calendar name cannot be empty', + ); + } + + try { + await DeviceCalendarPlusPlatform.instance + .updateCalendar(calendarId, name, colorHex); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Deletes a calendar from the device. + /// + /// [calendarId] is the ID of the calendar to delete. + /// + /// This will also delete all events within the calendar. + /// Requires calendar write permissions - call [requestPermissions] first. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// + /// // Delete a calendar by ID + /// await plugin.deleteCalendar(calendarId); + /// ``` + Future deleteCalendar(String calendarId) async { + try { + await DeviceCalendarPlusPlatform.instance.deleteCalendar(calendarId); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Lists events within the specified date range. + /// + /// [startDate] and [endDate] are required parameters that define the time + /// window for fetching events. + /// + /// **Important iOS Limitation**: iOS automatically limits event queries to a + /// maximum span of 4 years. If you specify a range exceeding 4 years, iOS + /// will truncate it to the first 4 years automatically. + /// + /// [calendarIds] is an optional parameter to filter events to specific + /// calendars. If null or empty, events from all calendars are returned. + /// + /// Recurring events are automatically expanded into individual instances + /// within the date range. Each instance has: + /// - The same [Event.eventId] + /// - Different [Event.startDate] and [Event.endDate] + /// + /// This combination uniquely identifies each occurrence of a recurring event. + /// + /// Returns a list of [Event] objects sorted by start date. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// final now = DateTime.now(); + /// final nextMonth = now.add(Duration(days: 30)); + /// + /// // Get all events in the next month + /// final events = await plugin.listEvents( + /// now, + /// nextMonth, + /// ); + /// + /// // Get events from specific calendars only + /// final workEvents = await plugin.listEvents( + /// now, + /// nextMonth, + /// calendarIds: ['work-calendar-id', 'project-calendar-id'], + /// ); + /// + /// for (final event in events) { + /// print('${event.title} at ${event.startDate}'); + /// } + /// ``` + Future> listEvents( + DateTime startDate, + DateTime endDate, { + List? calendarIds, + }) async { + try { + final List> rawEvents = + await DeviceCalendarPlusPlatform.instance.listEvents( + startDate, + endDate, + calendarIds, + ); + return rawEvents.map((map) => Event.fromMap(map)).toList(); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Retrieves a single event by ID. + /// + /// The [id] can be either an event ID or an instance ID: + /// - **Event ID**: Returns the master event definition (for recurring events) + /// - **Instance ID**: Returns a specific occurrence (for recurring events) + /// + /// + /// Returns null if no matching event is found. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// // Get specific instance of a recurring event + /// final instance = await plugin.getEvent(event.instanceId); + /// + /// // Get master event definition for a recurring event + /// final masterEvent = await plugin.getEvent(event.eventId); + /// ``` + Future getEvent(String id) async { + try { + final Map? rawEvent = + await DeviceCalendarPlusPlatform.instance.getEvent(id); + + if (rawEvent == null) { + return null; + } + + return Event.fromMap(rawEvent); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Shows a calendar event in a modal dialog. + /// + /// The [id] can be either an event ID or an instance ID: + /// - **Event ID**: Shows the master event definition (for recurring events) + /// - **Instance ID**: Shows a specific occurrence (for recurring events) + /// + /// + /// **Platform Differences:** + /// - **iOS**: Presents the event in a native modal using EventKit's + /// `EKEventViewController`. The user can view and edit the event without + /// leaving your app. Requires your app to be in the foreground. + /// - **Android**: Opens the event using an Intent with `ACTION_VIEW`. + /// The system handles the presentation based on device and app configuration. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// // Show specific instance of a recurring event + /// await plugin.showEventModal(event.instanceId); + /// + /// // Show master event definition + /// await plugin.showEventModal(event.eventId); + /// ``` + Future showEventModal(String id) async { + try { + await DeviceCalendarPlusPlatform.instance.showEventModal(id); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Creates a new event in the specified calendar. + /// + /// [calendarId] is the ID of the calendar to create the event in (required). + /// [title] is the event title (required). + /// [startDate] is the start date/time (required). + /// [endDate] is the end date/time (required). + /// [isAllDay] indicates if this is an all-day event (default: false). + /// [description] is optional event notes/description. + /// [location] is optional event location. + /// [timeZone] is optional timezone identifier (null for all-day events). + /// The platform will validate the timezone string. + /// [url] is optional event URL (supported on both platforms). + /// [availability] is the availability status (default: EventAvailability.busy). + /// + /// Returns the system-generated event ID. + /// Requires calendar write permissions - call [requestPermissions] first. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// + /// // Create a basic event + /// final eventId = await plugin.createEvent( + /// calendarId: 'cal-123', + /// title: 'Team Meeting', + /// startDate: DateTime.now(), + /// endDate: DateTime.now().add(Duration(hours: 1)), + /// ); + /// + /// // Create an event with all options + /// final detailedEventId = await plugin.createEvent( + /// calendarId: 'cal-123', + /// title: 'Project Review', + /// startDate: DateTime(2024, 3, 15, 14, 0), + /// endDate: DateTime(2024, 3, 15, 15, 0), + /// description: 'Q1 project review meeting', + /// location: 'Conference Room A', + /// timeZone: 'America/New_York', + /// availability: EventAvailability.busy, + /// ); + /// ``` + Future createEvent({ + required String calendarId, + required String title, + required DateTime startDate, + required DateTime endDate, + int? reminderMinutes, + bool isAllDay = false, + String? description, + String? location, + String? timeZone, + EventAvailability availability = EventAvailability.busy, + }) async { + print("createEvent: the reminder minutes are $reminderMinutes"); + + // Validate required fields + if (calendarId.trim().isEmpty) { + throw ArgumentError.value( + calendarId, + 'calendarId', + 'Calendar ID cannot be empty', + ); + } + + if (title.trim().isEmpty) { + throw ArgumentError.value( + title, + 'title', + 'Event title cannot be empty', + ); + } + + if (endDate.isBefore(startDate)) { + throw ArgumentError( + 'End date must be after start date', + ); + } + + // Normalize dates for all-day events + // All-day events should use midnight (00:00:00) and ignore time components + DateTime normalizedStartDate = startDate; + DateTime normalizedEndDate = endDate; + + if (isAllDay) { + normalizedStartDate = DateTime( + startDate.year, + startDate.month, + startDate.day, + ); + normalizedEndDate = DateTime( + endDate.year, + endDate.month, + endDate.day, + ); + } + + try { + final String eventId = + await DeviceCalendarPlusPlatform.instance.createEvent( + calendarId, + title, + normalizedStartDate, + normalizedEndDate, + isAllDay, + reminderMinutes, + description, + location, + timeZone, + availability.name, + ); + return eventId; + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Deletes an event from the device. + /// + /// [eventId] identifies the event to delete. You can pass either: + /// - An event ID (e.g., from `event.eventId`) + /// - An instance ID (e.g., from `event.instanceId`) - the event ID will be extracted by the platform + /// + /// **For recurring events**: This will delete the ENTIRE series (all past + /// and future occurrences). Single-instance deletion is not supported to + /// maintain consistent behavior across platforms. + /// + /// For non-recurring events, this deletes the single event. + /// Requires calendar write permissions - call [requestPermissions] first. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// + /// // Delete using event ID + /// await plugin.deleteEvent(eventId: event.eventId); + /// + /// // Delete using instance ID (event ID will be extracted by platform) + /// await plugin.deleteEvent(eventId: event.instanceId); + /// ``` + Future deleteEvent({required String eventId}) async { + if (eventId.trim().isEmpty) { + throw ArgumentError.value( + eventId, + 'eventId', + 'Event ID cannot be empty', + ); + } + + try { + await DeviceCalendarPlusPlatform.instance.deleteEvent(eventId); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } + + /// Updates an existing event on the device. + /// + /// [eventId] identifies the event to update (required). You can pass either: + /// - An event ID (e.g., from `event.eventId`) + /// - An instance ID (e.g., from `event.instanceId`) - the event ID will be extracted by the platform + /// + /// **For recurring events**: This will update the ENTIRE series (all past + /// and future occurrences). Single-instance updates are not supported to + /// maintain consistent behavior across platforms. + /// + /// All field parameters are optional - only provided fields will be updated: + /// - [title] - new event title + /// - [startDate] - new start date/time + /// - [endDate] - new end date/time + /// - [description] - new event description + /// - [location] - new event location + /// - [isAllDay] - change between all-day and timed event + /// - Changing timed → all-day: Time components are stripped to midnight + /// - Changing all-day → timed: Midnight time is used + /// - [timeZone] - new timezone identifier + /// - Note: This reinterprets the local time, not preserving the instant + /// - Example: "3:00 PM EST" → "3:00 PM PST" (different instant in time) + /// + /// At least one field must be provided. + /// Requires calendar write permissions - call [requestPermissions] first. + /// + /// Example: + /// ```dart + /// final plugin = DeviceCalendar.instance; + /// + /// // Update event title using event ID (entire series for recurring events) + /// await plugin.updateEvent( + /// eventId: event.eventId, + /// title: 'Updated Meeting Title', + /// ); + /// + /// // Update using instance ID (event ID will be extracted by platform) + /// await plugin.updateEvent( + /// eventId: event.instanceId, + /// isAllDay: true, + /// ); + /// + /// // Update multiple fields + /// await plugin.updateEvent( + /// eventId: event.eventId, + /// title: 'Team Sync', + /// startDate: DateTime(2024, 3, 20, 10, 0), + /// endDate: DateTime(2024, 3, 20, 11, 0), + /// location: 'Conference Room B', + /// ); + /// ``` + Future updateEvent({ + required String eventId, + String? title, + DateTime? startDate, + DateTime? endDate, + String? description, + String? location, + bool? isAllDay, + String? timeZone, + }) async { + // Validate eventId + if (eventId.trim().isEmpty) { + throw ArgumentError.value( + eventId, + 'eventId', + 'Event ID cannot be empty', + ); + } + + // Validate at least one field is provided + if (title == null && + startDate == null && + endDate == null && + description == null && + location == null && + isAllDay == null && + timeZone == null) { + throw ArgumentError( + 'At least one field must be provided to update', + ); + } + + // Validate dates if both are provided + if (startDate != null && endDate != null && endDate.isBefore(startDate)) { + throw ArgumentError( + 'End date must be after start date', + ); + } + + // Normalize dates for all-day events + // We need to check if the event is becoming all-day or if we're updating dates on an existing all-day event + // Since we don't have access to the existing event here, we'll let the platform handle it + // But if isAllDay is being set to true, we should normalize the dates + DateTime? normalizedStartDate = startDate; + DateTime? normalizedEndDate = endDate; + + if (isAllDay == true) { + // Event is becoming all-day, normalize dates to midnight + if (startDate != null) { + normalizedStartDate = DateTime( + startDate.year, + startDate.month, + startDate.day, + ); + } + if (endDate != null) { + normalizedEndDate = DateTime( + endDate.year, + endDate.month, + endDate.day, + ); + } + } + + try { + await DeviceCalendarPlusPlatform.instance.updateEvent( + eventId, + title: title, + startDate: normalizedStartDate, + endDate: normalizedEndDate, + description: description, + location: location, + isAllDay: isAllDay, + timeZone: timeZone, + ); + } on PlatformException catch (e, stackTrace) { + final convertedException = + PlatformExceptionConverter.convertPlatformException(e); + if (convertedException != null) { + Error.throwWithStackTrace(convertedException, stackTrace); + } + rethrow; + } + } +} diff --git a/package/device_calendar_plus/lib/src/calendar.dart b/package/device_calendar_plus/lib/src/calendar.dart new file mode 100644 index 00000000..064bc6a2 --- /dev/null +++ b/package/device_calendar_plus/lib/src/calendar.dart @@ -0,0 +1,127 @@ +/// Represents a user's calendar +class Calendar { + /// Identifier returned by the platform (Android `Calendars._ID`, iOS `EKCalendar.calendarIdentifier`). + final String id; + + /// User-facing label shown in native calendar pickers. + final String name; + + /// Calendar color as a hex string in `#RRGGBB` format, if provided by the OS. + final String? colorHex; + + /// Whether edits are disallowed (subscribed/shared calendars, server-managed feeds, etc.). + final bool readOnly; + + /// Account name or email that owns the calendar, when exposed by the platform. + final String? accountName; + + /// Platform-specific account type (for example `com.google`, `CalDAV`, or `local` on Android). + final String? accountType; + + /// Indicates that the calendar is the default destination for new events on that account/device. + /// + /// Android maps this to `Calendars.IS_PRIMARY`; iOS matches `eventStore.defaultCalendarForNewEvents`. + final bool isPrimary; + + /// Marks calendars hidden in the Android Calendar UI. iOS always reports `false`. + final bool hidden; + + /// Creates an immutable calendar description. + const Calendar({ + required this.id, + required this.name, + this.colorHex, + required this.readOnly, + this.accountName, + this.accountType, + this.isPrimary = false, + this.hidden = false, + }); + + /// Builds a calendar object from a platform channel payload. + factory Calendar.fromMap(Map map) { + return Calendar( + id: map['id'] as String, + name: map['name'] as String, + colorHex: map['colorHex'] as String?, + readOnly: map['readOnly'] as bool? ?? false, + accountName: map['accountName'] as String?, + accountType: map['accountType'] as String?, + isPrimary: map['isPrimary'] as bool? ?? false, + hidden: map['hidden'] as bool? ?? false, + ); + } + + /// Serializes the calendar back into a map for platform channel use. + Map toMap() { + return { + 'id': id, + 'name': name, + 'colorHex': colorHex, + 'readOnly': readOnly, + 'accountName': accountName, + 'accountType': accountType, + 'isPrimary': isPrimary, + 'hidden': hidden, + }; + } + + /// Returns a copy with selectively overridden fields. + Calendar copyWith({ + String? id, + String? name, + String? colorHex, + bool? readOnly, + String? accountName, + String? accountType, + bool? isPrimary, + bool? hidden, + }) { + return Calendar( + id: id ?? this.id, + name: name ?? this.name, + colorHex: colorHex ?? this.colorHex, + readOnly: readOnly ?? this.readOnly, + accountName: accountName ?? this.accountName, + accountType: accountType ?? this.accountType, + isPrimary: isPrimary ?? this.isPrimary, + hidden: hidden ?? this.hidden, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Calendar && + other.id == id && + other.name == name && + other.colorHex == colorHex && + other.readOnly == readOnly && + other.accountName == accountName && + other.accountType == accountType && + other.isPrimary == isPrimary && + other.hidden == hidden; + } + + @override + int get hashCode { + return Object.hash( + id, + name, + colorHex, + readOnly, + accountName, + accountType, + isPrimary, + hidden, + ); + } + + @override + String toString() { + return 'DeviceCalendar(id: $id, name: $name, colorHex: $colorHex, ' + 'readOnly: $readOnly, accountName: $accountName, accountType: $accountType, ' + 'isPrimary: $isPrimary, hidden: $hidden)'; + } +} diff --git a/package/device_calendar_plus/lib/src/calendar_permission_status.dart b/package/device_calendar_plus/lib/src/calendar_permission_status.dart new file mode 100644 index 00000000..b0f4733d --- /dev/null +++ b/package/device_calendar_plus/lib/src/calendar_permission_status.dart @@ -0,0 +1,32 @@ +/// Represents the current status of calendar permissions. +enum CalendarPermissionStatus { + /// Full read and write access to calendars. + granted, + + /// Permission has been denied by the user. + denied, + + /// Write-only access to calendars (iOS 17+ only). + /// + /// On iOS 17 and later, apps can request write-only access to add events + /// without being able to read existing calendar data. + /// + /// This status is never returned on Android. + writeOnly, + + /// Access is restricted by device policies (iOS only). + /// + /// This typically occurs when parental controls, Mobile Device Management (MDM), + /// or Screen Time restrictions prevent calendar access. The user cannot grant + /// permission even if they want to. + /// + /// This status is never returned on Android. + restricted, + + /// Permission has not been requested yet (iOS only). + /// + /// This is the initial state before the app has requested calendar permissions. + /// + /// This status is never returned on Android. + notDetermined, +} diff --git a/package/device_calendar_plus/lib/src/device_calendar_error.dart b/package/device_calendar_plus/lib/src/device_calendar_error.dart new file mode 100644 index 00000000..2951d5e5 --- /dev/null +++ b/package/device_calendar_plus/lib/src/device_calendar_error.dart @@ -0,0 +1,62 @@ +/// Error codes for device calendar operations. +enum DeviceCalendarError { + // Permission-related errors + + /// Calendar permissions are not declared in the app's manifest. + /// + /// On Android: Missing READ_CALENDAR or WRITE_CALENDAR in AndroidManifest.xml + /// On iOS: Missing NSCalendarsUsageDescription in Info.plist + permissionsNotDeclared, + + /// Calendar permission was denied by the user. + permissionDenied, + + // Input validation errors + + /// Invalid arguments were passed to a method. + invalidArguments, + + // Resource errors + + /// Requested calendar or event not found. + notFound, + + /// Calendar is read-only and cannot be modified. + readOnly, + + // Operation errors + + /// Calendar operation failed. + operationFailed, + + // System/availability errors + + /// Calendar system is not available. + calendarUnavailable, + + // Generic errors + + /// An unknown error occurred. + unknown, +} + +/// Exception thrown by device calendar operations. +class DeviceCalendarException implements Exception { + /// The error code describing what went wrong. + final DeviceCalendarError errorCode; + + /// A human-readable error message. + final String message; + + /// Optional additional details about the error. + final dynamic details; + + const DeviceCalendarException({ + required this.errorCode, + required this.message, + this.details, + }); + + @override + String toString() => 'DeviceCalendarException($errorCode): $message'; +} diff --git a/package/device_calendar_plus/lib/src/event.dart b/package/device_calendar_plus/lib/src/event.dart new file mode 100644 index 00000000..3ad4a77f --- /dev/null +++ b/package/device_calendar_plus/lib/src/event.dart @@ -0,0 +1,182 @@ +import 'event_availability.dart'; +import 'event_status.dart'; + +/// Represents a calendar event. +class Event { + /// Unique system identifier for this event. + /// For recurring events, all instances share the same eventId. + final String eventId; + + /// Instance identifier that uniquely identifies this specific event instance. + /// + /// **UNSTABLE ID:** This is a plugin-generated identifier, not a system ID. + /// It is derived from the [eventId] and the event's start date. + /// + /// Use this with [DeviceCalendar.instance.getEvent] and [DeviceCalendar.instance.showEventModal] + /// to fetch or display this specific event occurrence. + /// + /// For non-recurring events, this equals [eventId]. + /// For recurring events, this is a unique identifier for each occurrence. + /// + /// **Important:** This ID becomes invalid when the event's start date changes. + /// You are responsible for keeping instanceId up to date by re-fetching events. + /// + /// Example scenario where instanceId becomes invalid: + /// ```dart + /// // 1. You fetch some events + /// final events = await plugin.retrieveEvents(calendarId, ...); + /// + /// // 2. User opens native modal from one of the events and changes the start date + /// await plugin.showEventModal(event.instanceId); + /// // User changes date from Nov 5 to Nov 6 and saves + /// + /// // 3. Your stored instanceId is now invalid! + /// // The savedInstanceId no longer points to any event + /// + /// // 4. You must re-fetch to get the updated instanceId + /// final events = await plugin.retrieveEvents(calendarId, ...); + /// ``` + final String instanceId; + + /// ID of the calendar this event belongs to. + final String calendarId; + + /// Title of the event. + final String title; + + /// Description of the event. + final String? description; + + /// Location of the event. + final String? location; + + /// Start date and time of the event. + /// + /// For all-day events, treat this as a floating date (timezone-independent). + final DateTime startDate; + + /// End date and time of the event. + /// + /// For all-day events, treat this as a floating date (timezone-independent). + /// Uses half-open interval [start, end). (i.e. the event is up to, but not including, the end date.) + final DateTime endDate; + + /// Whether this is an all-day event. + final bool isAllDay; + + /// Availability status of the event. + final EventAvailability availability; + + /// Status of the event. + final EventStatus status; + + /// Timezone identifier for the event (e.g., "America/New_York"). + /// Null for all-day events (floating dates). + final String? timeZone; + + /// Whether this is a recurring event. + /// True for recurring events, false for one-time events. + final bool isRecurring; + + Event({ + required this.eventId, + required this.instanceId, + required this.calendarId, + required this.title, + this.description, + this.location, + required this.startDate, + required this.endDate, + required this.isAllDay, + required this.availability, + required this.status, + this.timeZone, + required this.isRecurring, + }); + + /// Creates an Event from a map returned by the platform. + factory Event.fromMap(Map map) { + return Event( + eventId: map['eventId'] as String, + instanceId: map['instanceId'] as String, + calendarId: map['calendarId'] as String, + title: map['title'] as String, + description: map['description'] as String?, + location: map['location'] as String?, + startDate: DateTime.fromMillisecondsSinceEpoch(map['startDate'] as int), + endDate: DateTime.fromMillisecondsSinceEpoch(map['endDate'] as int), + isAllDay: map['isAllDay'] as bool, + availability: EventAvailability.fromName(map['availability'] as String), + status: EventStatus.fromName(map['status'] as String), + timeZone: map['timeZone'] as String?, + isRecurring: map['isRecurring'] as bool? ?? false, + ); + } + + /// Converts this Event to a map for platform communication. + Map toMap() { + final map = { + 'eventId': eventId, + 'instanceId': instanceId, + 'calendarId': calendarId, + 'title': title, + 'startDate': startDate.millisecondsSinceEpoch, + 'endDate': endDate.millisecondsSinceEpoch, + 'isAllDay': isAllDay, + 'availability': availability.name, + 'status': status.name, + 'isRecurring': isRecurring, + }; + + if (description != null) map['description'] = description; + if (location != null) map['location'] = location; + if (timeZone != null) map['timeZone'] = timeZone; + + return map; + } + + @override + String toString() { + return 'Event(eventId: $eventId, instanceId: $instanceId, calendarId: $calendarId, title: $title, ' + 'startDate: $startDate, endDate: $endDate, isAllDay: $isAllDay)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Event && + other.eventId == eventId && + other.instanceId == instanceId && + other.calendarId == calendarId && + other.title == title && + other.description == description && + other.location == location && + other.startDate == startDate && + other.endDate == endDate && + other.isAllDay == isAllDay && + other.availability == availability && + other.status == status && + other.timeZone == timeZone && + other.isRecurring == isRecurring; + } + + @override + int get hashCode { + return Object.hash( + eventId, + instanceId, + calendarId, + title, + description, + location, + startDate, + endDate, + isAllDay, + availability, + status, + timeZone, + isRecurring, + ); + } +} diff --git a/package/device_calendar_plus/lib/src/event_availability.dart b/package/device_calendar_plus/lib/src/event_availability.dart new file mode 100644 index 00000000..3843773b --- /dev/null +++ b/package/device_calendar_plus/lib/src/event_availability.dart @@ -0,0 +1,36 @@ +/// Availability status of a calendar event. +enum EventAvailability { + /// Availability is busy (default for most events). + /// + /// Available on: Android, iOS + busy, + + /// Availability is free (time is available despite event). + /// + /// Available on: Android, iOS + free, + + /// Availability is tentative (event is not confirmed). + /// + /// Available on: Android, iOS + tentative, + + /// Availability is unavailable (out of office, etc.). + /// + /// Available on: iOS only + unavailable, + + /// Availability status is not supported or unknown. + /// + /// Available on: iOS only (when calendar doesn't support availability) + notSupported; + + /// Safely parses a string to an EventAvailability enum. + /// Returns [notSupported] if the value doesn't match any known case. + static EventAvailability fromName(String name) { + return EventAvailability.values.firstWhere( + (e) => e.name == name, + orElse: () => EventAvailability.notSupported, + ); + } +} diff --git a/package/device_calendar_plus/lib/src/event_status.dart b/package/device_calendar_plus/lib/src/event_status.dart new file mode 100644 index 00000000..83d7f27f --- /dev/null +++ b/package/device_calendar_plus/lib/src/event_status.dart @@ -0,0 +1,31 @@ +/// Status of a calendar event. +enum EventStatus { + /// Event has no status or status is not set. + /// + /// Available on: Android, iOS + none, + + /// Event is confirmed. + /// + /// Available on: Android, iOS + confirmed, + + /// Event is tentative (not yet confirmed). + /// + /// Available on: Android, iOS + tentative, + + /// Event has been canceled. + /// + /// Available on: Android, iOS + canceled; + + /// Safely parses a string to an EventStatus enum. + /// Returns [none] if the value doesn't match any known case. + static EventStatus fromName(String name) { + return EventStatus.values.firstWhere( + (e) => e.name == name, + orElse: () => EventStatus.none, + ); + } +} diff --git a/package/device_calendar_plus/lib/src/platform_exception_codes.dart b/package/device_calendar_plus/lib/src/platform_exception_codes.dart new file mode 100644 index 00000000..5e4f552b --- /dev/null +++ b/package/device_calendar_plus/lib/src/platform_exception_codes.dart @@ -0,0 +1,65 @@ +/// Platform exception codes used for communication between native and Dart. +/// +/// These constants ensure consistency between native platform code +/// (Kotlin/Swift) and Dart error handling. +class PlatformExceptionCodes { + PlatformExceptionCodes._(); + + // Permission-related errors + + /// Calendar permissions are not declared in the app's manifest. + /// + /// Android: Missing READ_CALENDAR or WRITE_CALENDAR in AndroidManifest.xml + /// iOS: Missing NSCalendarsUsageDescription in Info.plist + static const String permissionsNotDeclared = 'PERMISSIONS_NOT_DECLARED'; + + /// Calendar permission denied by user. + /// + /// User has explicitly denied calendar access, or security exception occurred. + static const String permissionDenied = 'PERMISSION_DENIED'; + + // Input validation errors + + /// Invalid arguments passed to a method. + /// + /// Parameters are missing, of wrong type, or contain invalid values. + static const String invalidArguments = 'INVALID_ARGUMENTS'; + + // Resource errors + + /// Requested calendar or event not found. + /// + /// The calendar ID or event instance ID doesn't exist. + static const String notFound = 'NOT_FOUND'; + + /// Calendar is read-only and cannot be modified. + /// + /// Attempting to update or delete a calendar that doesn't allow modifications. + static const String readOnly = 'READ_ONLY'; + + // Operation errors + + /// Calendar operation failed. + /// + /// Save, update, or delete operation failed for reasons other than permissions. + /// Check error message for details. + static const String operationFailed = 'OPERATION_FAILED'; + + // System/availability errors + + /// Calendar system is not available. + /// + /// Examples: + /// - Calendar app not installed (Android) + /// - Local calendar source not found (iOS) + /// - Event store unavailable + static const String calendarUnavailable = 'CALENDAR_UNAVAILABLE'; + + // Generic errors + + /// An unknown or unexpected error occurred. + /// + /// Used for unexpected exceptions that don't fit other categories. + /// Check error message for details. + static const String unknownError = 'UNKNOWN_ERROR'; +} diff --git a/package/device_calendar_plus/lib/src/platform_exception_converter.dart b/package/device_calendar_plus/lib/src/platform_exception_converter.dart new file mode 100644 index 00000000..b22c03bc --- /dev/null +++ b/package/device_calendar_plus/lib/src/platform_exception_converter.dart @@ -0,0 +1,53 @@ +import 'package:flutter/services.dart'; + +import 'device_calendar_error.dart'; +import 'platform_exception_codes.dart'; + +/// Helper class for converting platform exceptions to DeviceCalendarExceptions. +class PlatformExceptionConverter { + PlatformExceptionConverter._(); // Prevent instantiation + + /// Converts a platform exception code string to a DeviceCalendarError enum. + /// + /// Returns null for unrecognized error codes. + static DeviceCalendarError? errorCodeFromString(String code) { + switch (code) { + case PlatformExceptionCodes.permissionsNotDeclared: + return DeviceCalendarError.permissionsNotDeclared; + case PlatformExceptionCodes.permissionDenied: + return DeviceCalendarError.permissionDenied; + case PlatformExceptionCodes.invalidArguments: + return DeviceCalendarError.invalidArguments; + case PlatformExceptionCodes.notFound: + return DeviceCalendarError.notFound; + case PlatformExceptionCodes.readOnly: + return DeviceCalendarError.readOnly; + case PlatformExceptionCodes.operationFailed: + return DeviceCalendarError.operationFailed; + case PlatformExceptionCodes.calendarUnavailable: + return DeviceCalendarError.calendarUnavailable; + case PlatformExceptionCodes.unknownError: + return DeviceCalendarError.unknown; + default: + return null; + } + } + + /// Converts a PlatformException to a DeviceCalendarException if it matches known codes. + /// Returns null if the exception should be rethrown as-is. + static DeviceCalendarException? convertPlatformException( + PlatformException e) { + final errorCode = errorCodeFromString(e.code); + + // Only convert recognized error codes + if (errorCode != null) { + return DeviceCalendarException( + errorCode: errorCode, + message: e.message ?? 'An error occurred', + details: e.details, + ); + } + + return null; + } +} diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/CHANGELOG.md b/package/device_calendar_plus/package/device_calendar_plus_android/CHANGELOG.md new file mode 100644 index 00000000..48b7a342 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/CHANGELOG.md @@ -0,0 +1,32 @@ +## 0.3.1 - 2025-11-07 + +### Fixed +- `showEvent()` now uses `startActivityForResult()` to properly await until the calendar activity is dismissed + +## 0.3.0 - 2024-11-05 + +### Changed +- **BREAKING**: `deleteEvent()` now always deletes entire series for recurring events (removed `deleteAllInstances` parameter) +- **BREAKING**: `updateEvent()` now always updates entire series for recurring events (removed `updateAllInstances` parameter) +- Native code now extracts event ID from instance ID format automatically + +### Removed +- **BREAKING**: `NOT_SUPPORTED` error code (no longer needed as single-instance operations are not attempted) + +## 0.2.0 - 2024-11-05 + +### Added +- `openAppSettings()` implementation to open Android app settings via Intent + +### Removed +- **BREAKING**: `getPlatformVersion()` implementation (unused boilerplate) + +## 0.1.1 - 2024-11-04 + +### Added +- ProGuard/R8 rules to prevent code stripping in release builds +- Automatic consumer ProGuard rules configuration + +## 0.1.0 - 2024-11-04 + +Initial release. \ No newline at end of file diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/LICENSE b/package/device_calendar_plus/package/device_calendar_plus_android/LICENSE new file mode 100644 index 00000000..0152eb2c --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 bullet.to + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/README.md b/package/device_calendar_plus/package/device_calendar_plus_android/README.md new file mode 100644 index 00000000..b8c6f7a2 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/README.md @@ -0,0 +1,20 @@ +# device_calendar_plus_android + +Android implementation of the `device_calendar_plus` plugin. + +This package implements calendar functionality using the Android Calendar Provider API. It is automatically included when you add `device_calendar_plus` to your Android app. + +## For App Developers + +You don't need to add this package directly. Just use the main [`device_calendar_plus`](https://pub.dev/packages/device_calendar_plus) package, and this Android implementation will be automatically included. + +## Implementation Details + +- **Platform**: Android API 24+ (target/compile 35) +- **Language**: Kotlin +- **APIs Used**: Android Calendar Provider, ContentResolver +- **Permissions**: READ_CALENDAR, WRITE_CALENDAR + +## License + +MIT © 2025 Bullet diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/analysis_options.yaml b/package/device_calendar_plus/package/device_calendar_plus_android/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/build.gradle b/package/device_calendar_plus/package/device_calendar_plus_android/android/build.gradle new file mode 100644 index 00000000..119050ee --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/build.gradle @@ -0,0 +1,72 @@ +group = "to.bullet.device_calendar_plus_android" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.1.0" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.9.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "to.bullet.device_calendar_plus_android" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 24 + } + + buildTypes { + release { + consumerProguardFiles("proguard-rules.pro") + } + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/gradle/wrapper/gradle-wrapper.jar b/package/device_calendar_plus/package/device_calendar_plus_android/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..980502d1 Binary files /dev/null and b/package/device_calendar_plus/package/device_calendar_plus_android/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/gradle/wrapper/gradle-wrapper.properties b/package/device_calendar_plus/package/device_calendar_plus_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..128196a7 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/gradlew b/package/device_calendar_plus/package/device_calendar_plus_android/android/gradlew new file mode 100755 index 00000000..faf93008 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/gradlew.bat b/package/device_calendar_plus/package/device_calendar_plus_android/android/gradlew.bat new file mode 100644 index 00000000..9d21a218 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/proguard-rules.pro b/package/device_calendar_plus/package/device_calendar_plus_android/android/proguard-rules.pro new file mode 100644 index 00000000..303a3fcd --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/proguard-rules.pro @@ -0,0 +1,5 @@ +# Keep all classes in the plugin package for Flutter method channel access +# R8/ProGuard can't detect that Flutter calls into these classes via method channels, +# so we need to explicitly keep them to prevent stripping in release builds +-keep class to.bullet.device_calendar_plus_android.** { *; } + diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/settings.gradle b/package/device_calendar_plus/package/device_calendar_plus_android/android/settings.gradle new file mode 100644 index 00000000..80511a30 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'device_calendar_plus_android' diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/AndroidManifest.xml b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..674d5b7b --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/CalendarService.kt b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/CalendarService.kt new file mode 100644 index 00000000..a24a0640 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/CalendarService.kt @@ -0,0 +1,281 @@ +package to.bullet.device_calendar_plus_android + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.provider.CalendarContract +import androidx.core.content.ContextCompat + +class CalendarService(private val activity: Activity) { + + fun listCalendars(): Result>> { + val calendars = mutableListOf>() + + val projection = arrayOf( + CalendarContract.Calendars._ID, + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, + CalendarContract.Calendars.CALENDAR_COLOR, + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, + CalendarContract.Calendars.ACCOUNT_NAME, + CalendarContract.Calendars.ACCOUNT_TYPE, + CalendarContract.Calendars.IS_PRIMARY, + CalendarContract.Calendars.VISIBLE + ) + + try { + activity.contentResolver.query( + CalendarContract.Calendars.CONTENT_URI, + projection, + null, + null, + null + )?.use { cursor -> + val idIndex = cursor.getColumnIndex(CalendarContract.Calendars._ID) + val nameIndex = cursor.getColumnIndex(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME) + val colorIndex = cursor.getColumnIndex(CalendarContract.Calendars.CALENDAR_COLOR) + val accessLevelIndex = cursor.getColumnIndex(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL) + val accountNameIndex = cursor.getColumnIndex(CalendarContract.Calendars.ACCOUNT_NAME) + val accountTypeIndex = cursor.getColumnIndex(CalendarContract.Calendars.ACCOUNT_TYPE) + val isPrimaryIndex = cursor.getColumnIndex(CalendarContract.Calendars.IS_PRIMARY) + val visibleIndex = cursor.getColumnIndex(CalendarContract.Calendars.VISIBLE) + + while (cursor.moveToNext()) { + val id = cursor.getString(idIndex) + val name = cursor.getString(nameIndex) + val color = if (!cursor.isNull(colorIndex)) cursor.getInt(colorIndex) else null + val accessLevel = cursor.getInt(accessLevelIndex) + val accountName = if (!cursor.isNull(accountNameIndex)) cursor.getString(accountNameIndex) else null + val accountType = if (!cursor.isNull(accountTypeIndex)) cursor.getString(accountTypeIndex) else null + val isPrimary = if (!cursor.isNull(isPrimaryIndex)) cursor.getInt(isPrimaryIndex) == 1 else false + val visible = if (!cursor.isNull(visibleIndex)) cursor.getInt(visibleIndex) == 1 else true + + // Determine if read-only based on access level + val readOnly = accessLevel < CalendarContract.Calendars.CAL_ACCESS_CONTRIBUTOR + + // Convert color to hex string + val colorHex = color?.let { ColorHelper.colorToHex(it) } + + val calendarMap = mutableMapOf( + "id" to id, + "name" to name, + "readOnly" to readOnly, + "isPrimary" to isPrimary, + "hidden" to !visible // Invert visible to hidden + ) + + colorHex?.let { calendarMap["colorHex"] = it } + accountName?.let { calendarMap["accountName"] = it } + accountType?.let { calendarMap["accountType"] = it } + + calendars.add(calendarMap) + } + } + } catch (e: SecurityException) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.UNKNOWN_ERROR, + "Failed to query calendars: ${e.message}" + ) + ) + } + + return Result.success(calendars) + } + + fun createCalendar(name: String, colorHex: String?): Result { + // Check for write calendar permission + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CALENDAR) + != PackageManager.PERMISSION_GRANTED) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied. Call requestPermissions() first." + ) + ) + } + + val accountName = "local" + val accountType = CalendarContract.ACCOUNT_TYPE_LOCAL + + // Android automatically creates the account when inserting the first calendar + try { + val values = android.content.ContentValues().apply { + put(CalendarContract.Calendars.ACCOUNT_NAME, accountName) + put(CalendarContract.Calendars.ACCOUNT_TYPE, accountType) + put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, name) + put(CalendarContract.Calendars.NAME, name) + put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER) + put(CalendarContract.Calendars.OWNER_ACCOUNT, accountName) + put(CalendarContract.Calendars.VISIBLE, 1) + put(CalendarContract.Calendars.SYNC_EVENTS, 1) + + // Set color if provided + if (colorHex != null) { + val color = ColorHelper.hexToColor(colorHex) + put(CalendarContract.Calendars.CALENDAR_COLOR, color) + } + } + + val uri = activity.contentResolver.insert( + CalendarContract.Calendars.CONTENT_URI + .buildUpon() + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, accountName) + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, accountType) + .build(), + values + ) + + if (uri != null) { + val calendarId = uri.lastPathSegment + if (calendarId != null) { + return Result.success(calendarId) + } + } + + return Result.failure( + CalendarException( + PlatformExceptionCodes.OPERATION_FAILED, + "Failed to create calendar: No calendar ID returned" + ) + ) + } catch (e: SecurityException) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.OPERATION_FAILED, + "Failed to create calendar: ${e.message}" + ) + ) + } + } + + fun updateCalendar(calendarId: String, name: String?, colorHex: String?): Result { + // Check for write calendar permission + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CALENDAR) + != PackageManager.PERMISSION_GRANTED) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied. Call requestPermissions() first." + ) + ) + } + + try { + // Prepare values to update + val values = android.content.ContentValues() + + // Update name if provided + if (name != null) { + values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, name) + values.put(CalendarContract.Calendars.NAME, name) + } + + // Update color if provided + if (colorHex != null) { + val color = ColorHelper.hexToColor(colorHex) + values.put(CalendarContract.Calendars.CALENDAR_COLOR, color) + } + + // Update the calendar + val updatedRows = activity.contentResolver.update( + CalendarContract.Calendars.CONTENT_URI, + values, + "${CalendarContract.Calendars._ID} = ?", + arrayOf(calendarId) + ) + + if (updatedRows == 0) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.NOT_FOUND, + "Calendar with ID $calendarId not found" + ) + ) + } + + return Result.success(Unit) + } catch (e: SecurityException) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.OPERATION_FAILED, + "Failed to update calendar: ${e.message}" + ) + ) + } + } + + fun deleteCalendar(calendarId: String): Result { + // Check for write calendar permission + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CALENDAR) + != PackageManager.PERMISSION_GRANTED) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied. Call requestPermissions() first." + ) + ) + } + + try { + val deletedRows = activity.contentResolver.delete( + CalendarContract.Calendars.CONTENT_URI, + "${CalendarContract.Calendars._ID} = ?", + arrayOf(calendarId) + ) + + if (deletedRows == 0) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.NOT_FOUND, + "Calendar with ID $calendarId not found" + ) + ) + } + + return Result.success(Unit) + } catch (e: SecurityException) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.OPERATION_FAILED, + "Failed to delete calendar: ${e.message}" + ) + ) + } + } + +} + +data class CalendarException( + val code: String, + override val message: String +) : Exception(message) + diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/ColorHelper.kt b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/ColorHelper.kt new file mode 100644 index 00000000..8663d262 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/ColorHelper.kt @@ -0,0 +1,23 @@ +package to.bullet.device_calendar_plus_android + +import android.graphics.Color + +object ColorHelper { + fun hexToColor(hex: String): Int { + val hexSanitized = hex.trim().removePrefix("#") + + // Parse RGB hex string to integer + return try { + Color.parseColor("#$hexSanitized") + } catch (e: Exception) { + // Default to black if parsing fails + Color.BLACK + } + } + + fun colorToHex(color: Int): String { + // Android color is ARGB, we want RGB hex string + return String.format("#%06X", 0xFFFFFF and color) + } +} + diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/DeviceCalendarPlusAndroidPlugin.kt b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/DeviceCalendarPlusAndroidPlugin.kt new file mode 100644 index 00000000..3d83ff81 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/DeviceCalendarPlusAndroidPlugin.kt @@ -0,0 +1,503 @@ +package to.bullet.device_calendar_plus_android + +import android.app.Activity +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry + +/** DeviceCalendarPlusAndroidPlugin */ +class DeviceCalendarPlusAndroidPlugin : + FlutterPlugin, + MethodCallHandler, + ActivityAware, + PluginRegistry.RequestPermissionsResultListener, + PluginRegistry.ActivityResultListener { + + private lateinit var channel: MethodChannel + private var activity: Activity? = null + private var permissionService: PermissionService? = null + private var calendarService: CalendarService? = null + private var eventsService: EventsService? = null + private var showEventModalResult: Result? = null + + companion object { + private const val SHOW_EVENT_REQUEST_CODE = 1001 + } + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "device_calendar_plus_android") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "requestPermissions" -> handleRequestPermissions(result) + "hasPermissions" -> handleHasPermissions(result) + "openAppSettings" -> handleOpenAppSettings(result) + "listCalendars" -> handleListCalendars(result) + "createCalendar" -> handleCreateCalendar(call, result) + "updateCalendar" -> handleUpdateCalendar(call, result) + "deleteCalendar" -> handleDeleteCalendar(call, result) + "listEvents" -> handleListEvents(call, result) + "getEvent" -> handleGetEvent(call, result) + "showEventModal" -> handleShowEventModal(call, result) + "createEvent" -> handleCreateEvent(call, result) + "deleteEvent" -> handleDeleteEvent(call, result) + "updateEvent" -> handleUpdateEvent(call, result) + else -> result.notImplemented() + } + } + + private fun handleRequestPermissions(result: Result) { + val service = permissionService!! + + service.requestPermissions { serviceResult -> + serviceResult.fold( + onSuccess = { status -> result.success(status) }, + onFailure = { error -> + if (error is PermissionException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + } + + private fun handleHasPermissions(result: Result) { + val service = permissionService!! + + val serviceResult = service.hasPermissions() + serviceResult.fold( + onSuccess = { status -> result.success(status) }, + onFailure = { error -> + if (error is PermissionException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleOpenAppSettings(result: Result) { + val currentActivity = activity + if (currentActivity == null) { + result.error( + PlatformExceptionCodes.UNKNOWN_ERROR, + "Activity not available", + null + ) + return + } + + try { + val intent = android.content.Intent( + android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + android.net.Uri.parse("package:${currentActivity.packageName}") + ) + intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK) + currentActivity.startActivity(intent) + result.success(null) + } catch (e: Exception) { + result.error( + PlatformExceptionCodes.UNKNOWN_ERROR, + "Failed to open app settings: ${e.message}", + null + ) + } + } + + private fun handleListCalendars(result: Result) { + val service = calendarService!! + + val serviceResult = service.listCalendars() + serviceResult.fold( + onSuccess = { calendars -> result.success(calendars) }, + onFailure = { error -> + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleCreateCalendar(call: MethodCall, result: Result) { + val service = calendarService ?: error("CalendarService not initialized - plugin lifecycle error") + + // Parse arguments + val name = call.argument("name") + val colorHex = call.argument("colorHex") + + if (name == null) { + result.error( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Missing or invalid name", + null + ) + return + } + + val serviceResult = service.createCalendar(name, colorHex) + serviceResult.fold( + onSuccess = { calendarId -> result.success(calendarId) }, + onFailure = { error -> + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleUpdateCalendar(call: MethodCall, result: Result) { + val service = calendarService ?: error("CalendarService not initialized - plugin lifecycle error") + + // Parse arguments + val calendarId = call.argument("calendarId") + val name = call.argument("name") + val colorHex = call.argument("colorHex") + + if (calendarId == null) { + result.error( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Missing or invalid calendarId", + null + ) + return + } + + val serviceResult = service.updateCalendar(calendarId, name, colorHex) + serviceResult.fold( + onSuccess = { result.success(null) }, + onFailure = { error -> + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleDeleteCalendar(call: MethodCall, result: Result) { + val service = calendarService ?: error("CalendarService not initialized - plugin lifecycle error") + + // Parse arguments + val calendarId = call.argument("calendarId") + + if (calendarId == null) { + result.error( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Missing or invalid calendarId", + null + ) + return + } + + val serviceResult = service.deleteCalendar(calendarId) + serviceResult.fold( + onSuccess = { result.success(null) }, + onFailure = { error -> + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleListEvents(call: MethodCall, result: Result) { + val service = eventsService ?: error("EventsService not initialized - plugin lifecycle error") + + // Parse arguments + val startDateMillis = call.argument("startDate") + val endDateMillis = call.argument("endDate") + val calendarIds = call.argument>("calendarIds") + + if (startDateMillis == null || endDateMillis == null) { + result.error( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Missing or invalid startDate or endDate", + null + ) + return + } + + val startDate = java.util.Date(startDateMillis) + val endDate = java.util.Date(endDateMillis) + + val serviceResult = service.retrieveEvents(startDate, endDate, calendarIds) + serviceResult.fold( + onSuccess = { events -> result.success(events) }, + onFailure = { error -> + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleGetEvent(call: MethodCall, result: Result) { + val service = eventsService ?: error("EventsService not initialized - plugin lifecycle error") + + // Parse arguments + val instanceId = call.argument("instanceId") + + if (instanceId == null) { + result.error( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Missing or invalid instanceId", + null + ) + return + } + + val serviceResult = service.getEvent(instanceId) + serviceResult.fold( + onSuccess = { event -> result.success(event) }, + onFailure = { error -> + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleShowEventModal(call: MethodCall, result: Result) { + val service = eventsService ?: error("EventsService not initialized - plugin lifecycle error") + val currentActivity = activity ?: error("Activity not initialized - plugin lifecycle error") + + // Parse arguments + val instanceId = call.argument("instanceId") + + if (instanceId == null) { + result.error( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Missing or invalid instanceId", + null + ) + return + } + + // Store the result callback to call when activity returns + showEventModalResult = result + + val serviceResult = service.showEvent(currentActivity, instanceId, SHOW_EVENT_REQUEST_CODE) + serviceResult.fold( + onSuccess = { /* Result will be sent in onActivityResult */ }, + onFailure = { error -> + // Clear stored result on error + showEventModalResult = null + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleCreateEvent(call: MethodCall, result: Result) { + val service = eventsService ?: error("EventsService not initialized - plugin lifecycle error") + + // Parse arguments + val calendarId = call.argument("calendarId") + val title = call.argument("title") + val startDateMillis = call.argument("startDate") + val endDateMillis = call.argument("endDate") + val isAllDay = call.argument("isAllDay") + val description = call.argument("description") + val location = call.argument("location") + val timeZone = call.argument("timeZone") + val availability = call.argument("availability") + val reminderMinutes = call.argument("reminderMinutes") + + // Validate required arguments + if (calendarId == null || title == null || startDateMillis == null || + endDateMillis == null || isAllDay == null || availability == null) { + result.error( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Missing required arguments for createEvent", + null + ) + return + } + + val startDate = java.util.Date(startDateMillis) + val endDate = java.util.Date(endDateMillis) + + val serviceResult = service.createEvent( + calendarId, + title, + startDate, + endDate, + isAllDay, + description, + location, + timeZone, + reminderMinutes, + availability + ) + + serviceResult.fold( + onSuccess = { eventId -> result.success(eventId) }, + onFailure = { error -> + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleDeleteEvent(call: MethodCall, result: Result) { + val service = eventsService ?: error("EventsService not initialized - plugin lifecycle error") + + // Parse arguments + val instanceId = call.argument("instanceId") + + if (instanceId == null) { + result.error( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Missing required arguments for deleteEvent", + null + ) + return + } + + val serviceResult = service.deleteEvent(instanceId) + serviceResult.fold( + onSuccess = { result.success(null) }, + onFailure = { error -> + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + private fun handleUpdateEvent(call: MethodCall, result: Result) { + val service = eventsService ?: error("EventsService not initialized - plugin lifecycle error") + + // Parse required arguments + val instanceId = call.argument("instanceId") + + if (instanceId == null) { + result.error( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Missing required arguments for updateEvent", + null + ) + return + } + + // Parse optional arguments (all can be null) + val title = call.argument("title") + val startDateMillis = call.argument("startDate") + val endDateMillis = call.argument("endDate") + val description = call.argument("description") + val location = call.argument("location") + val isAllDay = call.argument("isAllDay") + val timeZone = call.argument("timeZone") + + // Convert dates if provided + val startDate = startDateMillis?.let { java.util.Date(it) } + val endDate = endDateMillis?.let { java.util.Date(it) } + + val serviceResult = service.updateEvent( + instanceId, + title, + startDate, + endDate, + description, + location, + isAllDay, + timeZone + ) + + serviceResult.fold( + onSuccess = { result.success(null) }, + onFailure = { error -> + if (error is CalendarException) { + result.error(error.code, error.message, null) + } else { + result.error(PlatformExceptionCodes.UNKNOWN_ERROR, error.message, null) + } + } + ) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + return permissionService?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: android.content.Intent?): Boolean { + if (requestCode == SHOW_EVENT_REQUEST_CODE) { + // Calendar activity closed, complete the future + showEventModalResult?.success(null) + showEventModalResult = null + return true + } + return false + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + permissionService = PermissionService(binding.activity) + calendarService = CalendarService(binding.activity) + eventsService = EventsService(binding.activity) + binding.addRequestPermissionsResultListener(this) + binding.addActivityResultListener(this) + } + + override fun onDetachedFromActivityForConfigChanges() { + activity = null + permissionService = null + calendarService = null + eventsService = null + showEventModalResult = null + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity + permissionService = PermissionService(binding.activity) + calendarService = CalendarService(binding.activity) + eventsService = EventsService(binding.activity) + binding.addRequestPermissionsResultListener(this) + binding.addActivityResultListener(this) + } + + override fun onDetachedFromActivity() { + activity = null + permissionService = null + calendarService = null + eventsService = null + showEventModalResult = null + } +} diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/EventsService.kt b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/EventsService.kt new file mode 100644 index 00000000..72149502 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/EventsService.kt @@ -0,0 +1,764 @@ +package to.bullet.device_calendar_plus_android + +import android.app.Activity +import android.content.ContentUris +import android.content.ContentValues +import android.content.Intent +import android.provider.CalendarContract +import java.util.Date + +class EventsService(private val activity: Activity) { + + fun retrieveEvents( + startDate: Date, + endDate: Date, + calendarIds: List?, + eventId: String? = null + ): Result>> { + val events = mutableListOf>() + + // Convert dates to milliseconds + val startMillis = startDate.time + val endMillis = endDate.time + + // Build URI with date range for Instances API + val uri = CalendarContract.Instances.CONTENT_URI.buildUpon() + .appendPath(startMillis.toString()) + .appendPath(endMillis.toString()) + .build() + + val projection = arrayOf( + CalendarContract.Instances.EVENT_ID, + CalendarContract.Instances.CALENDAR_ID, + CalendarContract.Instances.TITLE, + CalendarContract.Instances.DESCRIPTION, + CalendarContract.Instances.EVENT_LOCATION, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END, + CalendarContract.Instances.ALL_DAY, + CalendarContract.Instances.AVAILABILITY, + CalendarContract.Instances.STATUS, + CalendarContract.Instances.EVENT_TIMEZONE, + CalendarContract.Instances.RRULE + ) + + // Build selection clause for calendar and event filtering + val selections = mutableListOf() + val args = mutableListOf() + + if (calendarIds != null && calendarIds.isNotEmpty()) { + val placeholders = calendarIds.joinToString(",") { "?" } + selections.add("${CalendarContract.Instances.CALENDAR_ID} IN ($placeholders)") + args.addAll(calendarIds) + } + + if (eventId != null) { + selections.add("${CalendarContract.Instances.EVENT_ID} = ?") + args.add(eventId) + } + + val selection = if (selections.isNotEmpty()) selections.joinToString(" AND ") else null + val selectionArgs = if (args.isNotEmpty()) args.toTypedArray() else null + + try { + activity.contentResolver.query( + uri, + projection, + selection, + selectionArgs, + "${CalendarContract.Instances.BEGIN} ASC" + )?.use { cursor -> + while (cursor.moveToNext()) { + val eventMap = buildEventMapFromCursor( + cursor, + CalendarContract.Instances.EVENT_ID, + CalendarContract.Instances.CALENDAR_ID, + CalendarContract.Instances.TITLE, + CalendarContract.Instances.DESCRIPTION, + CalendarContract.Instances.EVENT_LOCATION, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END, + CalendarContract.Instances.ALL_DAY, + CalendarContract.Instances.AVAILABILITY, + CalendarContract.Instances.STATUS, + CalendarContract.Instances.EVENT_TIMEZONE, + CalendarContract.Instances.RRULE + ) + events.add(eventMap) + } + } + } catch (e: SecurityException) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.UNKNOWN_ERROR, + "Failed to query events: ${e.message}" + ) + ) + } + + return Result.success(events) + } + + private fun availabilityToString(availability: Int): String { + return when (availability) { + CalendarContract.Events.AVAILABILITY_BUSY -> "busy" + CalendarContract.Events.AVAILABILITY_FREE -> "free" + CalendarContract.Events.AVAILABILITY_TENTATIVE -> "tentative" + else -> "busy" + } + } + + private fun statusToString(status: Int): String { + return when (status) { + CalendarContract.Events.STATUS_CONFIRMED -> "confirmed" + CalendarContract.Events.STATUS_TENTATIVE -> "tentative" + CalendarContract.Events.STATUS_CANCELED -> "canceled" + else -> "none" + } + } + + private fun buildEventMapFromCursor( + cursor: android.database.Cursor, + eventIdColumn: String, + calendarIdColumn: String, + titleColumn: String, + descriptionColumn: String, + locationColumn: String, + startColumn: String, + endColumn: String, + allDayColumn: String, + availabilityColumn: String, + statusColumn: String, + timeZoneColumn: String, + recurrenceRuleColumn: String, + createdColumn: String? = null, + lastModifiedColumn: String? = null + ): Map { + val eventIdIndex = cursor.getColumnIndex(eventIdColumn) + val calendarIdIndex = cursor.getColumnIndex(calendarIdColumn) + val titleIndex = cursor.getColumnIndex(titleColumn) + val descriptionIndex = cursor.getColumnIndex(descriptionColumn) + val locationIndex = cursor.getColumnIndex(locationColumn) + val startIndex = cursor.getColumnIndex(startColumn) + val endIndex = cursor.getColumnIndex(endColumn) + val allDayIndex = cursor.getColumnIndex(allDayColumn) + val availabilityIndex = cursor.getColumnIndex(availabilityColumn) + val statusIndex = cursor.getColumnIndex(statusColumn) + val timeZoneIndex = cursor.getColumnIndex(timeZoneColumn) + val recurrenceRuleIndex = cursor.getColumnIndex(recurrenceRuleColumn) + val createdIndex = if (createdColumn != null) cursor.getColumnIndex(createdColumn) else -1 + val lastModifiedIndex = if (lastModifiedColumn != null) cursor.getColumnIndex(lastModifiedColumn) else -1 + + val eventId = cursor.getString(eventIdIndex) + val calendarId = cursor.getString(calendarIdIndex) + val title = if (!cursor.isNull(titleIndex)) cursor.getString(titleIndex) else "" + val description = if (!cursor.isNull(descriptionIndex)) cursor.getString(descriptionIndex) else null + val location = if (!cursor.isNull(locationIndex)) cursor.getString(locationIndex) else null + val rawStart = cursor.getLong(startIndex) + val rawEnd = if (!cursor.isNull(endIndex)) cursor.getLong(endIndex) else rawStart + val allDay = if (!cursor.isNull(allDayIndex)) cursor.getInt(allDayIndex) == 1 else false + val availability = if (!cursor.isNull(availabilityIndex)) cursor.getInt(availabilityIndex) else 0 + val status = if (!cursor.isNull(statusIndex)) cursor.getInt(statusIndex) else 0 + val timeZone = if (!cursor.isNull(timeZoneIndex)) cursor.getString(timeZoneIndex) else null + val recurrenceRule = if (!cursor.isNull(recurrenceRuleIndex)) cursor.getString(recurrenceRuleIndex) else null + val createdDate = if (createdIndex >= 0 && !cursor.isNull(createdIndex)) cursor.getLong(createdIndex) else null + val lastModifiedDate = if (lastModifiedIndex >= 0 && !cursor.isNull(lastModifiedIndex)) cursor.getLong(lastModifiedIndex) else null + + // Generate instanceId using RAW timestamps before any modifications + val instanceId: String = if (recurrenceRule != null) { + "$eventId@$rawStart" + } else { + eventId + } + + // For all-day events, Android stores and returns UTC timestamps + // We need to convert them to local time while preserving the calendar date + val start: Long + val end: Long + + if (allDay) { + // Extract date components from UTC timestamp + val utcCal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")) + utcCal.timeInMillis = rawStart + val startYear = utcCal.get(java.util.Calendar.YEAR) + val startMonth = utcCal.get(java.util.Calendar.MONTH) + val startDay = utcCal.get(java.util.Calendar.DAY_OF_MONTH) + + utcCal.timeInMillis = rawEnd + val endYear = utcCal.get(java.util.Calendar.YEAR) + val endMonth = utcCal.get(java.util.Calendar.MONTH) + val endDay = utcCal.get(java.util.Calendar.DAY_OF_MONTH) + + // Create local timestamps with those date components + val localCal = java.util.Calendar.getInstance() + localCal.set(startYear, startMonth, startDay, 0, 0, 0) + localCal.set(java.util.Calendar.MILLISECOND, 0) + start = localCal.timeInMillis + + localCal.set(endYear, endMonth, endDay, 0, 0, 0) + localCal.set(java.util.Calendar.MILLISECOND, 0) + end = localCal.timeInMillis + } else { + start = rawStart + end = rawEnd + } + + val eventMap = mutableMapOf( + "eventId" to eventId, + "instanceId" to instanceId, + "calendarId" to calendarId, + "title" to title, + "startDate" to start, + "endDate" to end, + "isAllDay" to allDay, + "availability" to availabilityToString(availability), + "status" to statusToString(status) + ) + + description?.let { eventMap["description"] = it } + location?.let { eventMap["location"] = it } + + // Add timezone for timed events only + if (!allDay && timeZone != null) { + eventMap["timeZone"] = timeZone + } + + // Set isRecurring flag + eventMap["isRecurring"] = (recurrenceRule != null) + + // Add creation and modification dates if available + if (createdDate != null) { + eventMap["createdDate"] = createdDate + } + if (lastModifiedDate != null) { + eventMap["updatedDate"] = lastModifiedDate + } + + return eventMap + } + + fun getEvent(instanceId: String): Result?> { + // Parse instanceId: "eventId" or "eventId@timestamp" + val parts = instanceId.split("@", limit = 2) + val eventId = parts[0] + + if (parts.size == 2) { + // Recurring event with timestamp + val occurrenceMillis = parts[1].toLongOrNull() ?: return Result.failure( + CalendarException( + PlatformExceptionCodes.INVALID_ARGUMENTS, + "Invalid instanceId format: $instanceId" + ) + ) + + // Query ±1 second around the exact occurrence time + // We use a small window since we have the precise timestamp + val startMillis = occurrenceMillis - 1000 + val endMillis = occurrenceMillis + 1000 + + val startDate = Date(startMillis) + val endDate = Date(endMillis) + + // Use retrieveEvents with event ID filter + val eventsResult = retrieveEvents(startDate, endDate, null, eventId) + + return eventsResult.mapCatching { events -> + // Find closest match to the occurrence time + events.minByOrNull { event -> + val eventStart = event["startDate"] as? Long ?: return@minByOrNull Long.MAX_VALUE + kotlin.math.abs(eventStart - occurrenceMillis) + } + } + } else { + // Non-recurring event or master event + val projection = arrayOf( + CalendarContract.Events._ID, + CalendarContract.Events.CALENDAR_ID, + CalendarContract.Events.TITLE, + CalendarContract.Events.DESCRIPTION, + CalendarContract.Events.EVENT_LOCATION, + CalendarContract.Events.DTSTART, + CalendarContract.Events.DTEND, + CalendarContract.Events.ALL_DAY, + CalendarContract.Events.AVAILABILITY, + CalendarContract.Events.STATUS, + CalendarContract.Events.EVENT_TIMEZONE, + CalendarContract.Events.RRULE + ) + + val selection = "${CalendarContract.Events._ID} = ?" + val selectionArgs = arrayOf(eventId) + + try { + activity.contentResolver.query( + CalendarContract.Events.CONTENT_URI, + projection, + selection, + selectionArgs, + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + val eventMap = buildEventMapFromCursor( + cursor, + CalendarContract.Events._ID, + CalendarContract.Events.CALENDAR_ID, + CalendarContract.Events.TITLE, + CalendarContract.Events.DESCRIPTION, + CalendarContract.Events.EVENT_LOCATION, + CalendarContract.Events.DTSTART, + CalendarContract.Events.DTEND, + CalendarContract.Events.ALL_DAY, + CalendarContract.Events.AVAILABILITY, + CalendarContract.Events.STATUS, + CalendarContract.Events.EVENT_TIMEZONE, + CalendarContract.Events.RRULE + ) + return Result.success(eventMap) + } else { + return Result.success(null) + } + } + + return Result.success(null) + } catch (e: SecurityException) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.UNKNOWN_ERROR, + "Failed to query event: ${e.message}" + ) + ) + } + } + } + + /** + * Shows a calendar event in a modal dialog. + */ + fun showEvent(activityContext: Activity, instanceId: String, requestCode: Int): Result { + return try { + // Validate permissions + if (android.content.pm.PackageManager.PERMISSION_GRANTED != + activity.checkSelfPermission(android.Manifest.permission.READ_CALENDAR)) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied. Call requestPermissions() first." + ) + ) + } + + // Parse instanceId: "eventId" or "eventId@timestamp" + val parts = instanceId.split("@", limit = 2) + val eventId = parts[0] + + val intent = Intent(Intent.ACTION_VIEW) + + // Build event URI + val eventUri = android.content.ContentUris.withAppendedId( + CalendarContract.Events.CONTENT_URI, + eventId.toLong() + ) + intent.data = eventUri + + // Add begin time for specific recurring event instances + if (parts.size == 2) { + val occurrenceMillis = parts[1].toLongOrNull() + if (occurrenceMillis != null) { + intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, occurrenceMillis) + } + } + + // Use startActivityForResult to get a callback when the activity closes + activityContext.startActivityForResult(intent, requestCode) + Result.success(Unit) + } catch (e: android.content.ActivityNotFoundException) { + Result.failure( + CalendarException( + PlatformExceptionCodes.CALENDAR_UNAVAILABLE, + "Calendar app not found" + ) + ) + } catch (e: SecurityException) { + Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + Result.failure( + CalendarException( + PlatformExceptionCodes.UNKNOWN_ERROR, + "Failed to open event: ${e.message}" + ) + ) + } + } + + fun createEvent( + calendarId: String, + title: String, + startDate: java.util.Date, + endDate: java.util.Date, + isAllDay: Boolean, + description: String?, + location: String?, + timeZone: String?, + reminderMinutes: Int?, + availability: String + ): Result { + // Check for write calendar permission + if (android.content.pm.PackageManager.PERMISSION_GRANTED != + activity.checkSelfPermission(android.Manifest.permission.WRITE_CALENDAR)) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied. Call requestPermissions() first." + ) + ) + } + + try { + // For all-day events, Android interprets timestamps as UTC to determine the calendar date + // We need to convert local date components to UTC midnight to preserve the calendar date + val startMillis: Long + val endMillis: Long + + if (isAllDay) { + // Extract date components from local time + val localCal = java.util.Calendar.getInstance() + localCal.time = startDate + val startYear = localCal.get(java.util.Calendar.YEAR) + val startMonth = localCal.get(java.util.Calendar.MONTH) + val startDay = localCal.get(java.util.Calendar.DAY_OF_MONTH) + + localCal.time = endDate + val endYear = localCal.get(java.util.Calendar.YEAR) + val endMonth = localCal.get(java.util.Calendar.MONTH) + val endDay = localCal.get(java.util.Calendar.DAY_OF_MONTH) + + // Create UTC timestamps with those date components + val utcCal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")) + utcCal.set(startYear, startMonth, startDay, 0, 0, 0) + utcCal.set(java.util.Calendar.MILLISECOND, 0) + startMillis = utcCal.timeInMillis + + utcCal.set(endYear, endMonth, endDay, 0, 0, 0) + utcCal.set(java.util.Calendar.MILLISECOND, 0) + endMillis = utcCal.timeInMillis + } else { + startMillis = startDate.time + endMillis = endDate.time + } + + val values = android.content.ContentValues().apply { + put(CalendarContract.Events.CALENDAR_ID, calendarId.toLong()) + put(CalendarContract.Events.TITLE, title) + put(CalendarContract.Events.DTSTART, startMillis) + put(CalendarContract.Events.DTEND, endMillis) + put(CalendarContract.Events.ALL_DAY, if (isAllDay) 1 else 0) + + // Set description if provided + if (description != null) { + put(CalendarContract.Events.DESCRIPTION, description) + } + + // Set location if provided + if (location != null) { + put(CalendarContract.Events.EVENT_LOCATION, location) + } + + // Set timezone + // For all-day events, use device timezone to make them "floating" + // This ensures the date components (year/month/day) stay the same + // regardless of timezone changes + if (isAllDay) { + put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id) + } else { + // For non-all-day events, use provided timezone or default to device timezone + val tz = timeZone ?: java.util.TimeZone.getDefault().id + put(CalendarContract.Events.EVENT_TIMEZONE, tz) + } + + // Map availability string to Android constant + val availabilityValue = when (availability) { + "free" -> CalendarContract.Events.AVAILABILITY_FREE + "tentative" -> CalendarContract.Events.AVAILABILITY_TENTATIVE + "unavailable" -> CalendarContract.Events.AVAILABILITY_BUSY + else -> CalendarContract.Events.AVAILABILITY_BUSY // "busy" or default + } + put(CalendarContract.Events.AVAILABILITY, availabilityValue) + + // Set status to confirmed + put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_CONFIRMED) + } + + val uri = activity.contentResolver.insert( + CalendarContract.Events.CONTENT_URI, + values + ) + + if (uri != null) { + val eventId = uri.lastPathSegment + + reminderMinutes?.let{ + val remindersContentValues = ContentValues().apply { + put(CalendarContract.Reminders.EVENT_ID, eventId) + put(CalendarContract.Reminders.MINUTES, reminderMinutes) + put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) + } + + // bulkInsert requires an Array of ContentValues + activity.contentResolver.bulkInsert( + CalendarContract.Reminders.CONTENT_URI, + arrayOf(remindersContentValues) // Wrap in an array + ) + } + + if (eventId != null) { + return Result.success(eventId) + } + } + + return Result.failure( + CalendarException( + PlatformExceptionCodes.OPERATION_FAILED, + "Failed to create event: No event ID returned" + ) + ) + } catch (e: SecurityException) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.OPERATION_FAILED, + "Failed to create event: ${e.message}" + ) + ) + } + } + + fun deleteEvent(instanceId: String): Result { + // Check for write calendar permission + if (android.content.pm.PackageManager.PERMISSION_GRANTED != + activity.checkSelfPermission(android.Manifest.permission.WRITE_CALENDAR)) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied. Call requestPermissions() first." + ) + ) + } + + try { + // Parse instanceId: "eventId" or "eventId@timestamp" + // For recurring events, we always delete the entire series + val parts = instanceId.split("@", limit = 2) + val eventId = parts[0] + + // Delete the event (entire series for recurring events) + val deletedRows = activity.contentResolver.delete( + CalendarContract.Events.CONTENT_URI, + "${CalendarContract.Events._ID} = ?", + arrayOf(eventId) + ) + + if (deletedRows == 0) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.NOT_FOUND, + "Event with ID $eventId not found" + ) + ) + } + + return Result.success(Unit) + } catch (e: SecurityException) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.OPERATION_FAILED, + "Failed to delete event: ${e.message}" + ) + ) + } + } + + fun updateEvent( + instanceId: String, + title: String?, + startDate: java.util.Date?, + endDate: java.util.Date?, + description: String?, + location: String?, + isAllDay: Boolean?, + timeZone: String? + ): Result { + // Check for write calendar permission + if (android.content.pm.PackageManager.PERMISSION_GRANTED != + activity.checkSelfPermission(android.Manifest.permission.WRITE_CALENDAR)) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied. Call requestPermissions() first." + ) + ) + } + + try { + // Parse instanceId: "eventId" or "eventId@timestamp" + // For recurring events, we always update the entire series + val parts = instanceId.split("@", limit = 2) + val eventId = parts[0] + + // Need to fetch existing event to determine if it's all-day + // This is required for proper date normalization + val existingEventResult = getEvent(instanceId) + val existingEvent = existingEventResult.getOrNull() + val wasAllDay = existingEvent?.get("isAllDay") as? Boolean ?: false + + // Build ContentValues with only provided fields + val values = android.content.ContentValues() + + // Update title if provided + if (title != null) { + values.put(CalendarContract.Events.TITLE, title) + } + + // Update description if provided + if (description != null) { + values.put(CalendarContract.Events.DESCRIPTION, description) + } + + // Update location if provided + if (location != null) { + values.put(CalendarContract.Events.EVENT_LOCATION, location) + } + + // Update isAllDay if provided + val effectiveIsAllDay = isAllDay ?: wasAllDay + if (isAllDay != null) { + values.put(CalendarContract.Events.ALL_DAY, if (isAllDay) 1 else 0) + } + + // Update dates if provided + // If event is/becomes all-day, need to normalize to UTC midnight + if (startDate != null || endDate != null) { + val startMillis: Long? + val endMillis: Long? + + if (effectiveIsAllDay) { + // For all-day events, convert date components to UTC midnight + val localCal = java.util.Calendar.getInstance() + + if (startDate != null) { + localCal.time = startDate + val startYear = localCal.get(java.util.Calendar.YEAR) + val startMonth = localCal.get(java.util.Calendar.MONTH) + val startDay = localCal.get(java.util.Calendar.DAY_OF_MONTH) + + val utcCal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")) + utcCal.set(startYear, startMonth, startDay, 0, 0, 0) + utcCal.set(java.util.Calendar.MILLISECOND, 0) + startMillis = utcCal.timeInMillis + } else { + startMillis = null + } + + if (endDate != null) { + localCal.time = endDate + val endYear = localCal.get(java.util.Calendar.YEAR) + val endMonth = localCal.get(java.util.Calendar.MONTH) + val endDay = localCal.get(java.util.Calendar.DAY_OF_MONTH) + + val utcCal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")) + utcCal.set(endYear, endMonth, endDay, 0, 0, 0) + utcCal.set(java.util.Calendar.MILLISECOND, 0) + endMillis = utcCal.timeInMillis + } else { + endMillis = null + } + } else { + // For timed events, use timestamps directly + startMillis = startDate?.time + endMillis = endDate?.time + } + + if (startMillis != null) { + values.put(CalendarContract.Events.DTSTART, startMillis) + } + if (endMillis != null) { + values.put(CalendarContract.Events.DTEND, endMillis) + } + } + + // Update timezone if provided + // Note: For all-day events, timezone should be set but is less relevant + if (timeZone != null) { + values.put(CalendarContract.Events.EVENT_TIMEZONE, timeZone) + } else if (isAllDay == true) { + // If changing to all-day, set device timezone + values.put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id) + } + + // Perform the update + val updatedRows = activity.contentResolver.update( + CalendarContract.Events.CONTENT_URI, + values, + "${CalendarContract.Events._ID} = ?", + arrayOf(eventId) + ) + + if (updatedRows == 0) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.NOT_FOUND, + "Event with ID $eventId not found" + ) + ) + } + + return Result.success(Unit) + } catch (e: SecurityException) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.PERMISSION_DENIED, + "Calendar permission denied: ${e.message}" + ) + ) + } catch (e: Exception) { + return Result.failure( + CalendarException( + PlatformExceptionCodes.OPERATION_FAILED, + "Failed to update event: ${e.message}" + ) + ) + } + } +} + diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/PermissionService.kt b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/PermissionService.kt new file mode 100644 index 00000000..29bbe4f8 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/PermissionService.kt @@ -0,0 +1,121 @@ +package to.bullet.device_calendar_plus_android + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat + +class PermissionService(private val activity: Activity) { + + companion object { + const val CALENDAR_PERMISSION_REQUEST_CODE = 2024 + + // Permission status values matching CalendarPermissionStatus enum + const val STATUS_GRANTED = "granted" + const val STATUS_DENIED = "denied" + } + + private var pendingCallback: ((Result) -> Unit)? = null + + private fun checkPermissionsDeclared(): PermissionException? { + val readPermission = Manifest.permission.READ_CALENDAR + val writePermission = Manifest.permission.WRITE_CALENDAR + + val packageInfo = activity.packageManager.getPackageInfo( + activity.packageName, + PackageManager.GET_PERMISSIONS + ) + + val declaredPermissions = packageInfo.requestedPermissions?.toList() ?: emptyList() + + if (!declaredPermissions.contains(readPermission) || !declaredPermissions.contains(writePermission)) { + val errorMessage = "Calendar permissions must be declared in AndroidManifest.xml.\n\n" + + "Add the following to android/app/src/main/AndroidManifest.xml:\n" + + "\n" + + "" + + return PermissionException(PlatformExceptionCodes.PERMISSIONS_NOT_DECLARED, errorMessage) + } + + return null + } + + private fun getCurrentPermissionStatus(): String { + val readPermission = Manifest.permission.READ_CALENDAR + val writePermission = Manifest.permission.WRITE_CALENDAR + + val readGranted = ContextCompat.checkSelfPermission( + activity, + readPermission + ) == PackageManager.PERMISSION_GRANTED + + val writeGranted = ContextCompat.checkSelfPermission( + activity, + writePermission + ) == PackageManager.PERMISSION_GRANTED + + return if (readGranted && writeGranted) STATUS_GRANTED else STATUS_DENIED + } + + fun hasPermissions(): Result { + val error = checkPermissionsDeclared() + if (error != null) { + return Result.failure(error) + } + + return Result.success(getCurrentPermissionStatus()) + } + + fun requestPermissions(callback: (Result) -> Unit) { + val error = checkPermissionsDeclared() + if (error != null) { + callback(Result.failure(error)) + return + } + + val currentStatus = getCurrentPermissionStatus() + if (currentStatus == STATUS_GRANTED) { + callback(Result.success(STATUS_GRANTED)) + return + } + + // Store the callback to be completed when permission result is received + pendingCallback = callback + + // Request both permissions + val readPermission = Manifest.permission.READ_CALENDAR + val writePermission = Manifest.permission.WRITE_CALENDAR + ActivityCompat.requestPermissions( + activity, + arrayOf(readPermission, writePermission), + CALENDAR_PERMISSION_REQUEST_CODE + ) + } + + fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + if (requestCode != CALENDAR_PERMISSION_REQUEST_CODE) { + return false + } + + val callback = pendingCallback ?: return false + pendingCallback = null + + // Check if both permissions were granted + val allGranted = grantResults.isNotEmpty() && + grantResults.all { it == PackageManager.PERMISSION_GRANTED } + + callback(Result.success(if (allGranted) STATUS_GRANTED else STATUS_DENIED)) + return true + } +} + +data class PermissionException( + val code: String, + override val message: String +) : Exception(message) + diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/PlatformExceptionCodes.kt b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/PlatformExceptionCodes.kt new file mode 100644 index 00000000..af9cdd23 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/main/kotlin/to/bullet/device_calendar_plus_android/PlatformExceptionCodes.kt @@ -0,0 +1,82 @@ +package to.bullet.device_calendar_plus_android + +/** + * Platform exception codes matching PlatformExceptionCodes in Dart. + * + * These codes are sent via method channel errors and caught/transformed + * by the Dart layer into DeviceCalendarException. + */ +object PlatformExceptionCodes { + // Permission-related errors + + /** + * Calendar permissions not declared in AndroidManifest.xml. + * + * Missing READ_CALENDAR or WRITE_CALENDAR in AndroidManifest.xml + */ + const val PERMISSIONS_NOT_DECLARED = "PERMISSIONS_NOT_DECLARED" + + /** + * Calendar permission denied by user. + * + * User has explicitly denied calendar access, or security exception occurred. + */ + const val PERMISSION_DENIED = "PERMISSION_DENIED" + + // Input validation errors + + /** + * Invalid arguments passed to a method. + * + * Parameters are missing, of wrong type, or contain invalid values. + */ + const val INVALID_ARGUMENTS = "INVALID_ARGUMENTS" + + // Resource errors + + /** + * Requested calendar or event not found. + * + * The calendar ID or event instance ID doesn't exist. + */ + const val NOT_FOUND = "NOT_FOUND" + + /** + * Calendar is read-only and cannot be modified. + * + * Attempting to update or delete a calendar that doesn't allow modifications. + */ + const val READ_ONLY = "READ_ONLY" + + // Operation errors + + /** + * Calendar operation failed. + * + * Save, update, or delete operation failed for reasons other than permissions. + * Check error message for details. + */ + const val OPERATION_FAILED = "OPERATION_FAILED" + + // System/availability errors + + /** + * Calendar system is not available. + * + * Examples: + * - Calendar app not installed + * - Event store unavailable + */ + const val CALENDAR_UNAVAILABLE = "CALENDAR_UNAVAILABLE" + + // Generic errors + + /** + * An unknown or unexpected error occurred. + * + * Used for unexpected exceptions that don't fit other categories. + * Check error message for details. + */ + const val UNKNOWN_ERROR = "UNKNOWN_ERROR" +} + diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/android/src/test/kotlin/to/bullet/device_calendar_plus_android/DeviceCalendarPlusAndroidPluginTest.kt b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/test/kotlin/to/bullet/device_calendar_plus_android/DeviceCalendarPlusAndroidPluginTest.kt new file mode 100644 index 00000000..c99d0013 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/android/src/test/kotlin/to/bullet/device_calendar_plus_android/DeviceCalendarPlusAndroidPluginTest.kt @@ -0,0 +1,27 @@ +package to.bullet.device_calendar_plus_android + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.mockito.Mockito +import kotlin.test.Test + +/* + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +internal class DeviceCalendarPlusAndroidPluginTest { + @Test + fun onMethodCall_getPlatformVersion_returnsExpectedValue() { + val plugin = DeviceCalendarPlusAndroidPlugin() + + val call = MethodCall("getPlatformVersion", null) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) + } +} diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/lib/device_calendar_plus_android.dart b/package/device_calendar_plus/package/device_calendar_plus_android/lib/device_calendar_plus_android.dart new file mode 100644 index 00000000..ed6b178e --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/lib/device_calendar_plus_android.dart @@ -0,0 +1,175 @@ +import 'package:device_calendar_plus_platform_interface/device_calendar_plus_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// The Android implementation of [DeviceCalendarPlusPlatform]. +class DeviceCalendarPlusAndroid extends DeviceCalendarPlusPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('device_calendar_plus_android'); + + /// Registers this class as the default instance of [DeviceCalendarPlusPlatform]. + static void registerWith() { + DeviceCalendarPlusPlatform.instance = DeviceCalendarPlusAndroid(); + } + + @override + Future requestPermissions() async { + return await methodChannel.invokeMethod('requestPermissions'); + } + + @override + Future hasPermissions() async { + return await methodChannel.invokeMethod('hasPermissions'); + } + + @override + Future openAppSettings() async { + await methodChannel.invokeMethod('openAppSettings'); + } + + @override + Future>> listCalendars() async { + final result = + await methodChannel.invokeMethod>('listCalendars'); + return result?.map((e) => Map.from(e as Map)).toList() ?? + []; + } + + @override + Future createCalendar(String name, String? colorHex) async { + final result = await methodChannel.invokeMethod( + 'createCalendar', + { + 'name': name, + 'colorHex': colorHex, + }, + ); + return result!; + } + + @override + Future updateCalendar( + String calendarId, String? name, String? colorHex) async { + await methodChannel.invokeMethod( + 'updateCalendar', + { + 'calendarId': calendarId, + 'name': name, + 'colorHex': colorHex, + }, + ); + } + + @override + Future deleteCalendar(String calendarId) async { + await methodChannel.invokeMethod( + 'deleteCalendar', + {'calendarId': calendarId}, + ); + } + + @override + Future>> listEvents( + DateTime startDate, + DateTime endDate, + List? calendarIds, + ) async { + final result = await methodChannel.invokeMethod>( + 'listEvents', + { + 'startDate': startDate.millisecondsSinceEpoch, + 'endDate': endDate.millisecondsSinceEpoch, + 'calendarIds': calendarIds, + }, + ); + return result?.map((e) => Map.from(e as Map)).toList() ?? + []; + } + + @override + Future?> getEvent(String instanceId) async { + final result = await methodChannel.invokeMethod>( + 'getEvent', + { + 'instanceId': instanceId, + }, + ); + return result != null ? Map.from(result) : null; + } + + @override + Future showEventModal(String instanceId) async { + await methodChannel.invokeMethod( + 'showEventModal', + {'instanceId': instanceId}, + ); + } + + @override + Future createEvent( + String calendarId, + String title, + DateTime startDate, + DateTime endDate, + bool isAllDay, + int? reminderMinutes, + String? description, + String? location, + String? timeZone, + String availability, + ) async { + final result = await methodChannel.invokeMethod( + 'createEvent', + { + 'calendarId': calendarId, + 'title': title, + 'startDate': startDate.millisecondsSinceEpoch, + 'endDate': endDate.millisecondsSinceEpoch, + 'isAllDay': isAllDay, + 'description': description, + 'location': location, + 'timeZone': timeZone, + 'availability': availability, + 'reminderMinutes': reminderMinutes, + }, + ); + return result!; + } + + @override + Future deleteEvent(String instanceId) async { + await methodChannel.invokeMethod( + 'deleteEvent', + { + 'instanceId': instanceId, + }, + ); + } + + @override + Future updateEvent( + String instanceId, { + String? title, + DateTime? startDate, + DateTime? endDate, + String? description, + String? location, + bool? isAllDay, + String? timeZone, + }) async { + await methodChannel.invokeMethod( + 'updateEvent', + { + 'instanceId': instanceId, + 'title': title, + 'startDate': startDate?.millisecondsSinceEpoch, + 'endDate': endDate?.millisecondsSinceEpoch, + 'description': description, + 'location': location, + 'isAllDay': isAllDay, + 'timeZone': timeZone, + }, + ); + } +} diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/pubspec.lock b/package/device_calendar_plus/package/device_calendar_plus_android/pubspec.lock new file mode 100644 index 00000000..1b3b4593 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/pubspec.lock @@ -0,0 +1,220 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + device_calendar_plus_platform_interface: + dependency: "direct main" + description: + path: "../device_calendar_plus_platform_interface" + relative: true + source: path + version: "0.3.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" +sdks: + dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/pubspec.yaml b/package/device_calendar_plus/package/device_calendar_plus_android/pubspec.yaml new file mode 100644 index 00000000..0dda027f --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/pubspec.yaml @@ -0,0 +1,64 @@ +name: device_calendar_plus_android +description: Android implementation of the device_calendar_plus plugin. +version: 0.3.1 +repository: https://github.com/bullet-to/device_calendar_plus + + +environment: + sdk: ">=3.5.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + device_calendar_plus_platform_interface: + path: ../device_calendar_plus_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + plugin: + implements: device_calendar_plus + platforms: + android: + package: to.bullet.device_calendar_plus_android + pluginClass: DeviceCalendarPlusAndroidPlugin + dartPluginClass: DeviceCalendarPlusAndroid + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/package/device_calendar_plus/package/device_calendar_plus_android/test/device_calendar_plus_android_test.dart b/package/device_calendar_plus/package/device_calendar_plus_android/test/device_calendar_plus_android_test.dart new file mode 100644 index 00000000..c60f2297 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_android/test/device_calendar_plus_android_test.dart @@ -0,0 +1,313 @@ +// import 'package:device_calendar_plus_android/device_calendar_plus_android.dart'; +// import 'package:device_calendar_plus_platform_interface/device_calendar_plus_platform_interface.dart'; +// import 'package:flutter/services.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// +// void main() { +// TestWidgetsFlutterBinding.ensureInitialized(); +// +// group('DeviceCalendarPlusAndroid', () { +// late DeviceCalendarPlusAndroid plugin; +// late List log; +// +// setUp(() async { +// plugin = DeviceCalendarPlusAndroid(); +// +// log = []; +// TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger +// .setMockMethodCallHandler(plugin.methodChannel, (methodCall) async { +// log.add(methodCall); +// switch (methodCall.method) { +// case 'requestPermissions': +// return 'granted'; // CalendarPermissionStatus.granted +// case 'hasPermissions': +// return 'granted'; // CalendarPermissionStatus.granted +// case 'openAppSettings': +// return null; +// case 'listCalendars': +// return [ +// { +// 'id': '1', +// 'name': 'Work', +// 'readOnly': false, +// 'isPrimary': true, +// 'hidden': false, +// } +// ]; +// case 'createCalendar': +// return 'android-calendar-id-456'; +// case 'updateCalendar': +// return null; +// case 'deleteCalendar': +// return null; +// case 'listEvents': +// return [ +// { +// 'eventId': 'event1', +// 'calendarId': 'cal1', +// 'title': 'Test Event', +// 'startDate': DateTime.now().millisecondsSinceEpoch, +// 'endDate': DateTime.now().millisecondsSinceEpoch, +// 'isAllDay': false, +// 'availability': 'busy', +// 'status': 'confirmed', +// } +// ]; +// case 'createEvent': +// return 'android-event-id-789'; +// case 'deleteEvent': +// return null; +// case 'updateEvent': +// return null; +// default: +// return null; +// } +// }); +// }); +// +// test('can be registered', () { +// DeviceCalendarPlusAndroid.registerWith(); +// expect(DeviceCalendarPlusPlatform.instance, +// isA()); +// }); +// +// test('requestPermissions returns granted status', () async { +// final status = await plugin.requestPermissions(); +// expect( +// log, +// [isMethodCall('requestPermissions', arguments: null)], +// ); +// expect(status, equals('granted')); // CalendarPermissionStatus.granted +// }); +// +// test('hasPermissions returns granted status', () async { +// final status = await plugin.hasPermissions(); +// expect( +// log, +// [isMethodCall('hasPermissions', arguments: null)], +// ); +// expect(status, equals('granted')); // CalendarPermissionStatus.granted +// }); +// +// test('openAppSettings calls method', () async { +// await plugin.openAppSettings(); +// expect( +// log, +// [isMethodCall('openAppSettings', arguments: null)], +// ); +// }); +// +// test('listCalendars returns list of calendars', () async { +// final calendars = await plugin.listCalendars(); +// expect( +// log, +// [isMethodCall('listCalendars', arguments: null)], +// ); +// expect(calendars, hasLength(1)); +// expect(calendars[0]['id'], equals('1')); +// expect(calendars[0]['name'], equals('Work')); +// }); +// +// test('createCalendar with name only', () async { +// final calendarId = await plugin.createCalendar('My Calendar', null); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('createCalendar')); +// expect(log[0].arguments['name'], equals('My Calendar')); +// expect(log[0].arguments['colorHex'], isNull); +// expect(calendarId, equals('android-calendar-id-456')); +// }); +// +// test('createCalendar with name and color', () async { +// final calendarId = +// await plugin.createCalendar('Work Calendar', '#FF5733'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('createCalendar')); +// expect(log[0].arguments['name'], equals('Work Calendar')); +// expect(log[0].arguments['colorHex'], equals('#FF5733')); +// expect(calendarId, equals('android-calendar-id-456')); +// }); +// +// test('updateCalendar with name only', () async { +// await plugin.updateCalendar('cal-456', 'Updated Name', null); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateCalendar')); +// expect(log[0].arguments['calendarId'], equals('cal-456')); +// expect(log[0].arguments['name'], equals('Updated Name')); +// expect(log[0].arguments['colorHex'], isNull); +// }); +// +// test('updateCalendar with name and color', () async { +// await plugin.updateCalendar('cal-456', 'Updated Name', '#00FF00'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateCalendar')); +// expect(log[0].arguments['calendarId'], equals('cal-456')); +// expect(log[0].arguments['name'], equals('Updated Name')); +// expect(log[0].arguments['colorHex'], equals('#00FF00')); +// }); +// +// test('deleteCalendar calls method with correct arguments', () async { +// await plugin.deleteCalendar('cal-456'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('deleteCalendar')); +// expect(log[0].arguments['calendarId'], equals('cal-456')); +// }); +// +// test('listEvents returns list of events', () async { +// final now = DateTime.now(); +// final later = now.add(Duration(days: 7)); +// +// final events = await plugin.listEvents(now, later, ['cal1']); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('listEvents')); +// expect(log[0].arguments['startDate'], equals(now.millisecondsSinceEpoch)); +// expect(log[0].arguments['endDate'], equals(later.millisecondsSinceEpoch)); +// expect(log[0].arguments['calendarIds'], equals(['cal1'])); +// +// expect(events, hasLength(1)); +// expect(events[0]['eventId'], equals('event1')); +// expect(events[0]['title'], equals('Test Event')); +// }); +// +// test('createEvent with all parameters', () async { +// final startDate = DateTime(2024, 3, 15, 14, 0); +// final endDate = DateTime(2024, 3, 15, 15, 0); +// +// final eventId = await plugin.createEvent( +// 'cal-123', +// 'Team Meeting', +// startDate, +// endDate, +// false, +// 'Weekly sync', +// 'Conference Room A', +// 'America/New_York', +// 'busy', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('createEvent')); +// expect(log[0].arguments['calendarId'], equals('cal-123')); +// expect(log[0].arguments['title'], equals('Team Meeting')); +// expect(log[0].arguments['startDate'], +// equals(startDate.millisecondsSinceEpoch)); +// expect( +// log[0].arguments['endDate'], equals(endDate.millisecondsSinceEpoch)); +// expect(log[0].arguments['isAllDay'], equals(false)); +// expect(log[0].arguments['description'], equals('Weekly sync')); +// expect(log[0].arguments['location'], equals('Conference Room A')); +// expect(log[0].arguments['timeZone'], equals('America/New_York')); +// expect(log[0].arguments['availability'], equals('busy')); +// expect(eventId, equals('android-event-id-789')); +// }); +// +// test('createEvent with minimal parameters', () async { +// final startDate = DateTime(2024, 3, 15, 14, 0); +// final endDate = DateTime(2024, 3, 15, 15, 0); +// +// final eventId = await plugin.createEvent( +// 'cal-123', +// 'Quick Event', +// startDate, +// endDate, +// true, +// null, +// null, +// null, +// 'free', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('createEvent')); +// expect(log[0].arguments['calendarId'], equals('cal-123')); +// expect(log[0].arguments['title'], equals('Quick Event')); +// expect(log[0].arguments['isAllDay'], equals(true)); +// expect(log[0].arguments['description'], isNull); +// expect(log[0].arguments['location'], isNull); +// expect(log[0].arguments['timeZone'], isNull); +// expect(log[0].arguments['availability'], equals('free')); +// expect(eventId, equals('android-event-id-789')); +// }); +// +// test('deleteEvent calls method with correct arguments', () async { +// await plugin.deleteEvent('event-123'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('deleteEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123')); +// }); +// +// test('deleteEvent for recurring event deletes entire series', () async { +// await plugin.deleteEvent('event-123@123456789'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('deleteEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123@123456789')); +// }); +// +// test('updateEvent with all parameters', () async { +// final startDate = DateTime(2024, 3, 20, 10, 0); +// final endDate = DateTime(2024, 3, 20, 11, 0); +// +// await plugin.updateEvent( +// 'event-123', +// title: 'Updated Title', +// startDate: startDate, +// endDate: endDate, +// description: 'Updated description', +// location: 'Updated location', +// isAllDay: false, +// timeZone: 'America/New_York', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123')); +// expect(log[0].arguments['title'], equals('Updated Title')); +// expect(log[0].arguments['startDate'], +// equals(startDate.millisecondsSinceEpoch)); +// expect( +// log[0].arguments['endDate'], equals(endDate.millisecondsSinceEpoch)); +// expect(log[0].arguments['description'], equals('Updated description')); +// expect(log[0].arguments['location'], equals('Updated location')); +// expect(log[0].arguments['isAllDay'], equals(false)); +// expect(log[0].arguments['timeZone'], equals('America/New_York')); +// }); +// +// test('updateEvent with minimal parameters', () async { +// await plugin.updateEvent( +// 'event-123', +// title: 'New Title', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123')); +// expect(log[0].arguments['title'], equals('New Title')); +// expect(log[0].arguments['startDate'], isNull); +// expect(log[0].arguments['endDate'], isNull); +// expect(log[0].arguments['description'], isNull); +// expect(log[0].arguments['location'], isNull); +// expect(log[0].arguments['isAllDay'], isNull); +// expect(log[0].arguments['timeZone'], isNull); +// expect(log[0].arguments['availability'], isNull); +// }); +// +// test('updateEvent for recurring event updates entire series', () async { +// await plugin.updateEvent( +// 'event-123', +// title: 'Updated Series', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123')); +// expect(log[0].arguments['title'], equals('Updated Series')); +// }); +// }); +// } diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/CHANGELOG.md b/package/device_calendar_plus/package/device_calendar_plus_ios/CHANGELOG.md new file mode 100644 index 00000000..c8cd783c --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/CHANGELOG.md @@ -0,0 +1,30 @@ +## 0.3.1 - 2025-11-07 + +### Fixed +- `showEvent()` now properly stores result callback and calls it in `eventViewController(_:didCompleteWith:)` delegate method after modal is dismissed + +## 0.3.0 - 2024-11-05 + +### Changed +- **BREAKING**: `deleteEvent()` now always deletes entire series for recurring events using `EKSpan.futureEvents` on master event (removed `deleteAllInstances` parameter) +- **BREAKING**: `updateEvent()` now always updates entire series for recurring events using `EKSpan.futureEvents` on master event (removed `updateAllInstances` parameter) +- Native code now extracts event ID from instance ID format automatically and fetches master event + +### Removed +- **BREAKING**: `NOT_SUPPORTED` error code (no longer needed) + +## 0.2.0 - 2024-11-05 + +### Added +- `openAppSettings()` implementation using UIApplication.openSettingsURLString + +### Removed +- **BREAKING**: `getPlatformVersion()` implementation (unused boilerplate) + +## 0.1.1 - 2024-11-04 + +Version sync with other packages. No functional changes. + +## 0.1.0 - 2024-11-04 + +Initial release. \ No newline at end of file diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/LICENSE b/package/device_calendar_plus/package/device_calendar_plus_ios/LICENSE new file mode 100644 index 00000000..0152eb2c --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 bullet.to + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/README.md b/package/device_calendar_plus/package/device_calendar_plus_ios/README.md new file mode 100644 index 00000000..c89d5462 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/README.md @@ -0,0 +1,20 @@ +# device_calendar_plus_ios + +iOS implementation of the `device_calendar_plus` plugin. + +This package implements calendar functionality using Apple's EventKit framework. It is automatically included when you add `device_calendar_plus` to your iOS app. + +## For App Developers + +You don't need to add this package directly. Just use the main [`device_calendar_plus`](https://pub.dev/packages/device_calendar_plus) package, and this iOS implementation will be automatically included. + +## Implementation Details + +- **Platform**: iOS 13+ +- **Language**: Swift +- **APIs Used**: EventKit, EventKitUI +- **Permissions**: Supports iOS 17+ write-only calendar access + +## License + +MIT © 2025 Bullet diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/analysis_options.yaml b/package/device_calendar_plus/package/device_calendar_plus_ios/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/CalendarService.swift b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/CalendarService.swift new file mode 100644 index 00000000..502bfa8c --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/CalendarService.swift @@ -0,0 +1,204 @@ +import EventKit + +class CalendarService { + private let eventStore: EKEventStore + private let permissionService: PermissionService + + init(eventStore: EKEventStore, permissionService: PermissionService) { + self.eventStore = eventStore + self.permissionService = permissionService + } + + func listCalendars(completion: @escaping (Result<[[String: Any]], CalendarError>) -> Void) { + // Check current permission status - listing calendars requires full access (reading) + guard permissionService.hasPermission(for: .full) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Get all event calendars + let calendars = eventStore.calendars(for: .event) + let defaultCalendar = eventStore.defaultCalendarForNewEvents + + var calendarMaps: [[String: Any]] = [] + + for calendar in calendars { + var calendarMap: [String: Any] = [ + "id": calendar.calendarIdentifier, + "name": calendar.title, + "readOnly": !calendar.allowsContentModifications, + "isPrimary": calendar == defaultCalendar, + "hidden": false // iOS doesn't expose hidden calendars + ] + + // Add color if available + if let cgColor = calendar.cgColor { + calendarMap["colorHex"] = ColorHelper.colorToHex(cgColor: cgColor) + } + + // Add account name from source + if let sourceTitle = calendar.source?.title { + calendarMap["accountName"] = sourceTitle + } + + // Add account type from source + if let sourceType = calendar.source?.sourceType { + calendarMap["accountType"] = sourceTypeToString(sourceType: sourceType) + } + + calendarMaps.append(calendarMap) + } + + completion(.success(calendarMaps)) + } + + func createCalendar(name: String, colorHex: String?, completion: @escaping (Result) -> Void) { + // Check current permission status - creating calendars requires full access (writing) + guard permissionService.hasPermission(for: .full) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Find the local source - this is the only writable source for local calendars + guard let localSource = eventStore.sources.first(where: { $0.sourceType == .local }) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.calendarUnavailable, + message: "Could not find local calendar source" + ))) + return + } + + // Create a new calendar + let calendar = EKCalendar(for: .event, eventStore: eventStore) + calendar.source = localSource + calendar.title = name + + // Set color if provided + if let colorHex = colorHex { + calendar.cgColor = ColorHelper.hexToColor(hex: colorHex) + } + + // Save the calendar + do { + try eventStore.saveCalendar(calendar, commit: true) + completion(.success(calendar.calendarIdentifier)) + } catch { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.operationFailed, + message: "Failed to save calendar: \(error.localizedDescription)" + ))) + } + } + + func updateCalendar(calendarId: String, name: String?, colorHex: String?, completion: @escaping (Result) -> Void) { + // Check current permission status - updating calendars requires full access (writing) + guard permissionService.hasPermission(for: .full) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Find the calendar by ID + guard let calendar = eventStore.calendar(withIdentifier: calendarId) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.notFound, + message: "Calendar with ID \(calendarId) not found" + ))) + return + } + + // Check if calendar is modifiable + guard calendar.allowsContentModifications else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.readOnly, + message: "Calendar is read-only and cannot be modified" + ))) + return + } + + // Update name if provided + if let name = name { + calendar.title = name + } + + // Update color if provided + if let colorHex = colorHex { + calendar.cgColor = ColorHelper.hexToColor(hex: colorHex) + } + + // Save the calendar + do { + try eventStore.saveCalendar(calendar, commit: true) + completion(.success(())) + } catch { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.operationFailed, + message: "Failed to update calendar: \(error.localizedDescription)" + ))) + } + } + + func deleteCalendar(calendarId: String, completion: @escaping (Result) -> Void) { + // Check current permission status - deleting calendars requires full access (writing) + guard permissionService.hasPermission(for: .full) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Find the calendar by ID + guard let calendar = eventStore.calendar(withIdentifier: calendarId) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.notFound, + message: "Calendar with ID \(calendarId) not found" + ))) + return + } + + // Delete the calendar + do { + try eventStore.removeCalendar(calendar, commit: true) + completion(.success(())) + } catch { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.operationFailed, + message: "Failed to delete calendar: \(error.localizedDescription)" + ))) + } + } + + private func sourceTypeToString(sourceType: EKSourceType) -> String { + switch sourceType { + case .local: + return "local" + case .exchange: + return "exchange" + case .calDAV: + return "caldav" + case .mobileMe: + return "mobileme" + case .subscribed: + return "subscribed" + case .birthdays: + return "birthdays" + @unknown default: + return "unknown" + } + } +} + +struct CalendarError: Error { + let code: String + let message: String +} + diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/ColorHelper.swift b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/ColorHelper.swift new file mode 100644 index 00000000..7ccc4eb4 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/ColorHelper.swift @@ -0,0 +1,31 @@ +import Foundation +import CoreGraphics + +class ColorHelper { + static func hexToColor(hex: String) -> CGColor { + var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) + hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") + + var rgb: UInt64 = 0 + Scanner(string: hexSanitized).scanHexInt64(&rgb) + + let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 + let g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 + let b = CGFloat(rgb & 0x0000FF) / 255.0 + + return CGColor(red: r, green: g, blue: b, alpha: 1.0) + } + + static func colorToHex(cgColor: CGColor) -> String { + guard let components = cgColor.components, components.count >= 3 else { + return "#000000" + } + + let r = Int(components[0] * 255.0) + let g = Int(components[1] * 255.0) + let b = Int(components[2] * 255.0) + + return String(format: "#%02X%02X%02X", r, g, b) + } +} + diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/DeviceCalendarPlusIosPlugin.swift b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/DeviceCalendarPlusIosPlugin.swift new file mode 100644 index 00000000..a42a830b --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/DeviceCalendarPlusIosPlugin.swift @@ -0,0 +1,542 @@ +import Flutter +import UIKit +import EventKit +import EventKitUI + +public class DeviceCalendarPlusIosPlugin: NSObject, FlutterPlugin, EKEventViewDelegate { + private let eventStore = EKEventStore() + private lazy var permissionService = PermissionService(eventStore: eventStore) + private lazy var calendarService = CalendarService(eventStore: eventStore, permissionService: permissionService) + private lazy var eventsService = EventsService(eventStore: eventStore, permissionService: permissionService) + private var eventModalResult: FlutterResult? + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "device_calendar_plus_ios", binaryMessenger: registrar.messenger()) + let instance = DeviceCalendarPlusIosPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "requestPermissions": + handleRequestPermissions(result: result) + case "hasPermissions": + handleHasPermissions(result: result) + case "openAppSettings": + handleOpenAppSettings(result: result) + case "listCalendars": + handleListCalendars(result: result) + case "createCalendar": + handleCreateCalendar(call: call, result: result) + case "updateCalendar": + handleUpdateCalendar(call: call, result: result) + case "deleteCalendar": + handleDeleteCalendar(call: call, result: result) + case "listEvents": + handleListEvents(call: call, result: result) + case "getEvent": + handleGetEvent(call: call, result: result) + case "showEventModal": + handleShowEventModal(call: call, result: result) + case "createEvent": + handleCreateEvent(call: call, result: result) + case "deleteEvent": + handleDeleteEvent(call: call, result: result) + case "updateEvent": + handleUpdateEvent(call: call, result: result) + default: + result(FlutterMethodNotImplemented) + } + } + + private func handleRequestPermissions(result: @escaping FlutterResult) { + permissionService.requestPermissions { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success(let status): + result(status) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleHasPermissions(result: @escaping FlutterResult) { + let serviceResult = permissionService.hasPermissions() + switch serviceResult { + case .success(let status): + result(status) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + + private func handleOpenAppSettings(result: @escaping FlutterResult) { + guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { + result(FlutterError( + code: PlatformExceptionCodes.unknownError, + message: "Failed to create settings URL", + details: nil + )) + return + } + + if UIApplication.shared.canOpenURL(settingsUrl) { + UIApplication.shared.open(settingsUrl, options: [:]) { success in + if success { + result(nil) + } else { + result(FlutterError( + code: PlatformExceptionCodes.unknownError, + message: "Failed to open app settings", + details: nil + )) + } + } + } else { + result(FlutterError( + code: PlatformExceptionCodes.unknownError, + message: "Cannot open settings URL", + details: nil + )) + } + } + + private func handleListCalendars(result: @escaping FlutterResult) { + calendarService.listCalendars { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success(let calendars): + result(calendars) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleCreateCalendar(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Invalid arguments for createCalendar", + details: nil + )) + return + } + + // Parse name (required) + guard let name = args["name"] as? String else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing or invalid name", + details: nil + )) + return + } + + // Parse colorHex (optional) + let colorHex = args["colorHex"] as? String + + calendarService.createCalendar(name: name, colorHex: colorHex) { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success(let calendarId): + result(calendarId) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleUpdateCalendar(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Invalid arguments for updateCalendar", + details: nil + )) + return + } + + // Parse calendar ID (required) + guard let calendarId = args["calendarId"] as? String else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing or invalid calendarId", + details: nil + )) + return + } + + // Parse name (optional) + let name = args["name"] as? String + + // Parse colorHex (optional) + let colorHex = args["colorHex"] as? String + + calendarService.updateCalendar(calendarId: calendarId, name: name, colorHex: colorHex) { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success: + result(nil) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleDeleteCalendar(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Invalid arguments for deleteCalendar", + details: nil + )) + return + } + + // Parse calendar ID (required) + guard let calendarId = args["calendarId"] as? String else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing or invalid calendarId", + details: nil + )) + return + } + + calendarService.deleteCalendar(calendarId: calendarId) { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success: + result(nil) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleListEvents(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Invalid arguments for listEvents", + details: nil + )) + return + } + + // Parse start date + guard let startDateMillis = args["startDate"] as? Int64 else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing or invalid startDate", + details: nil + )) + return + } + + // Parse end date + guard let endDateMillis = args["endDate"] as? Int64 else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing or invalid endDate", + details: nil + )) + return + } + + // Convert milliseconds to Date + let startDate = Date(timeIntervalSince1970: TimeInterval(startDateMillis) / 1000.0) + let endDate = Date(timeIntervalSince1970: TimeInterval(endDateMillis) / 1000.0) + + // Parse calendar IDs (optional) + let calendarIds = args["calendarIds"] as? [String] + + eventsService.retrieveEvents( + startDate: startDate, + endDate: endDate, + calendarIds: calendarIds + ) { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success(let events): + result(events) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleGetEvent(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Invalid arguments for getEvent", + details: nil + )) + return + } + + // Parse instance ID + guard let instanceId = args["instanceId"] as? String else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing or invalid instanceId", + details: nil + )) + return + } + + eventsService.getEvent(instanceId: instanceId) { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success(let event): + result(event) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleShowEventModal(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Invalid arguments for showEventModal", + details: nil + )) + return + } + + // Parse instance ID + guard let instanceId = args["instanceId"] as? String else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing or invalid instanceId", + details: nil + )) + return + } + + eventsService.showEvent(instanceId: instanceId) { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success(let viewController): + // If we have a view controller (modal mode), present it + if let viewController = viewController { + // Get the root view controller + guard let rootViewController = self.getRootViewController() else { + fatalError("Failed to get root view controller - plugin lifecycle error") + } + + // Set the delegate + viewController.delegate = self + + // Store the result callback to call it when modal is dismissed + self.eventModalResult = result + + // Wrap in navigation controller for proper dismissal + let navigationController = UINavigationController(rootViewController: viewController) + navigationController.modalPresentationStyle = .pageSheet + + rootViewController.present(navigationController, animated: true, completion: nil) + } else { + // Calendar app was opened + result(nil) + } + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleCreateEvent(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Invalid arguments for createEvent", + details: nil + )) + return + } + + // Parse required parameters + guard let calendarId = args["calendarId"] as? String, + let title = args["title"] as? String, + let startDateMillis = args["startDate"] as? Int64, + let endDateMillis = args["endDate"] as? Int64, + let isAllDay = args["isAllDay"] as? Bool, + let availability = args["availability"] as? String else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing required arguments for createEvent", + details: nil + )) + return + } + + // Parse optional parameters + let description = args["description"] as? String + let location = args["location"] as? String + let timeZone = args["timeZone"] as? String + let reminderMinutes = args["reminderMinutes"] as? Int + print("Reminder set for \(reminderMinutes) minutes before the event") + // Convert dates + let startDate = Date(timeIntervalSince1970: TimeInterval(startDateMillis) / 1000.0) + let endDate = Date(timeIntervalSince1970: TimeInterval(endDateMillis) / 1000.0) + + eventsService.createEvent( + calendarId: calendarId, + title: title, + startDate: startDate, + endDate: endDate, + isAllDay: isAllDay, + description: description, + location: location, + timeZone: timeZone, + availability: availability, + reminderMinutes: reminderMinutes + ) { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success(let eventId): + result(eventId) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleDeleteEvent(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Invalid arguments for deleteEvent", + details: nil + )) + return + } + + // Parse parameters + guard let instanceId = args["instanceId"] as? String else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing required arguments for deleteEvent", + details: nil + )) + return + } + + eventsService.deleteEvent( + instanceId: instanceId + ) { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success: + result(nil) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + private func handleUpdateEvent(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Invalid arguments for updateEvent", + details: nil + )) + return + } + + // Parse required parameters + guard let instanceId = args["instanceId"] as? String else { + result(FlutterError( + code: PlatformExceptionCodes.invalidArguments, + message: "Missing required arguments for updateEvent", + details: nil + )) + return + } + + // Parse optional parameters + let title = args["title"] as? String + let description = args["description"] as? String + let location = args["location"] as? String + let isAllDay = args["isAllDay"] as? Bool + let timeZone = args["timeZone"] as? String + + // Parse dates if provided + let startDate: Date? + if let startDateMillis = args["startDate"] as? Int64 { + startDate = Date(timeIntervalSince1970: TimeInterval(startDateMillis) / 1000.0) + } else { + startDate = nil + } + + let endDate: Date? + if let endDateMillis = args["endDate"] as? Int64 { + endDate = Date(timeIntervalSince1970: TimeInterval(endDateMillis) / 1000.0) + } else { + endDate = nil + } + + eventsService.updateEvent( + instanceId: instanceId, + title: title, + startDate: startDate, + endDate: endDate, + description: description, + location: location, + isAllDay: isAllDay, + timeZone: timeZone + ) { serviceResult in + DispatchQueue.main.async { + switch serviceResult { + case .success: + result(nil) + case .failure(let error): + result(FlutterError(code: error.code, message: error.message, details: nil)) + } + } + } + } + + // MARK: - EKEventViewControllerDelegate + + public func eventViewController(_ controller: EKEventViewController, didCompleteWith action: EKEventViewAction) { + // Dismiss the modal + controller.navigationController?.dismiss(animated: true) { + // Call the stored result callback after modal is dismissed + self.eventModalResult?(nil) + self.eventModalResult = nil + } + } + + // MARK: - Helper Methods + + private func getRootViewController() -> UIViewController? { + // Get the key window + if #available(iOS 13.0, *) { + // Use window scene for iOS 13+ + let scenes = UIApplication.shared.connectedScenes + let windowScene = scenes.first as? UIWindowScene + return windowScene?.windows.first(where: { $0.isKeyWindow })?.rootViewController + } else { + // Use deprecated keyWindow for older iOS versions + return UIApplication.shared.keyWindow?.rootViewController + } + } +} diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/EventsService.swift b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/EventsService.swift new file mode 100644 index 00000000..857ca79d --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/EventsService.swift @@ -0,0 +1,520 @@ +import EventKit +import EventKitUI + +extension EKEventAvailability { + var stringValue: String { + switch self { + case .notSupported: + return "notSupported" + case .busy: + return "busy" + case .free: + return "free" + case .tentative: + return "tentative" + case .unavailable: + return "unavailable" + @unknown default: + return "notSupported" + } + } +} + +extension EKEventStatus { + var stringValue: String { + switch self { + case .none: + return "none" + case .confirmed: + return "confirmed" + case .tentative: + return "tentative" + case .canceled: + return "canceled" + @unknown default: + return "none" + } + } +} + +class EventsService { + private let eventStore: EKEventStore + private let permissionService: PermissionService + + init(eventStore: EKEventStore, permissionService: PermissionService) { + self.eventStore = eventStore + self.permissionService = permissionService + } + + func retrieveEvents( + startDate: Date, + endDate: Date, + calendarIds: [String]?, + completion: @escaping (Result<[[String: Any]], CalendarError>) -> Void + ) { + // Check permission + guard permissionService.hasPermission(for: .full) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Filter calendars if IDs provided + var calendars: [EKCalendar]? + if let calendarIds = calendarIds, !calendarIds.isEmpty { + calendars = calendarIds.compactMap { calendarId in + eventStore.calendar(withIdentifier: calendarId) + } + + // If no valid calendars found, return empty list + if calendars?.isEmpty ?? true { + completion(.success([])) + return + } + } + + // Create predicate for events + // Note: iOS automatically limits to 4-year spans + let predicate = eventStore.predicateForEvents( + withStart: startDate, + end: endDate, + calendars: calendars + ) + + // Fetch events + let events = eventStore.events(matching: predicate) + + // Convert to maps + let eventMaps = events.map { event in + eventToMap(event: event) + } + + completion(.success(eventMaps)) + } + + private func eventToMap(event: EKEvent) -> [String: Any] { + // Generate instanceId + let startMillis = Int64(event.startDate.timeIntervalSince1970 * 1000) + let eventId = event.eventIdentifier ?? "" + let instanceId: String + if event.hasRecurrenceRules { + instanceId = "\(eventId)@\(startMillis)" + } else { + instanceId = eventId + } + + var eventMap: [String: Any] = [ + "eventId": eventId, + "instanceId": instanceId, + "calendarId": event.calendar.calendarIdentifier, + "title": event.title ?? "", + "isAllDay": event.isAllDay + ] + + // Add optional fields + if let notes = event.notes { + eventMap["description"] = notes + } + + if let location = event.location { + eventMap["location"] = location + } + + // Convert dates to milliseconds since epoch + var startDate = event.startDate! + var endDate = event.endDate! + + // For all-day events, iOS returns dates in UTC representing "floating" dates + // We need to convert them to the device's local timezone to preserve the calendar date + // Example: "Jan 1, 2022" in UTC should become "Jan 1, 2022 00:00" in local time + if event.isAllDay { + // For end date: iOS sets end time to 23:59:59, so add 1 second to get midnight (open interval) + endDate = endDate.addingTimeInterval(1) + + // Extract date components from UTC dates + let utcCalendar = Calendar(identifier: .gregorian) + let startComponents = utcCalendar.dateComponents([.year, .month, .day], from: startDate) + let endComponents = utcCalendar.dateComponents([.year, .month, .day], from: endDate) + + // Create dates in local timezone with same calendar date components + var localCalendar = Calendar.current + localCalendar.timeZone = TimeZone.current + if let localStartDate = localCalendar.date(from: startComponents) { + startDate = localStartDate + } + if let localEndDate = localCalendar.date(from: endComponents) { + endDate = localEndDate + } + } + + eventMap["startDate"] = Int64(startDate.timeIntervalSince1970 * 1000) + eventMap["endDate"] = Int64(endDate.timeIntervalSince1970 * 1000) + + // Map availability and status to strings + eventMap["availability"] = event.availability.stringValue + eventMap["status"] = event.status.stringValue + + // Add timezone for timed events (null for all-day events) + if !event.isAllDay, let timeZone = event.timeZone { + eventMap["timeZone"] = timeZone.identifier + } + + // Set isRecurring flag + eventMap["isRecurring"] = event.hasRecurrenceRules + + return eventMap + } + + func getEvent( + instanceId: String, + completion: @escaping (Result<[String: Any]?, CalendarError>) -> Void + ) { + // Check permission + guard permissionService.hasPermission(for: .full) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Parse instanceId: "eventId" or "eventId@timestamp" + let parts = instanceId.split(separator: "@", maxSplits: 1) + let eventId = String(parts[0]) + + if parts.count == 2, let timestampMillis = Int64(parts[1]) { + // Recurring event with timestamp + let occurrenceDate = Date(timeIntervalSince1970: TimeInterval(timestampMillis) / 1000.0) + + // Query ±1 second around the exact occurrence time + // We use a small window since we have the precise timestamp + let startDate = occurrenceDate.addingTimeInterval(-1) + let endDate = occurrenceDate.addingTimeInterval(1) + + let predicate = eventStore.predicateForEvents( + withStart: startDate, + end: endDate, + calendars: nil + ) + + let events = eventStore.events(matching: predicate) + + // Find the closest matching instance + let matchingEvents = events.filter { $0.eventIdentifier == eventId } + let closestEvent = matchingEvents.min(by: { + abs($0.startDate.timeIntervalSince(occurrenceDate)) < abs($1.startDate.timeIntervalSince(occurrenceDate)) + }) + + if let closestEvent = closestEvent { + completion(.success(eventToMap(event: closestEvent))) + } else { + completion(.success(nil)) + } + } else { + // Non-recurring event or master event + if let event = eventStore.event(withIdentifier: eventId) { + completion(.success(eventToMap(event: event))) + } else { + completion(.success(nil)) + } + } + } + + func showEvent( + instanceId: String, + completion: @escaping (Result) -> Void + ) { + // Check permission + guard permissionService.hasPermission(for: .full) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Parse instanceId: "eventId" or "eventId@timestamp" + let parts = instanceId.split(separator: "@", maxSplits: 1) + let eventId = String(parts[0]) + let occurrenceDate: Date? + + if parts.count == 2, let timestampMillis = Int64(parts[1]) { + occurrenceDate = Date(timeIntervalSince1970: TimeInterval(timestampMillis) / 1000.0) + } else { + occurrenceDate = nil + } + + // Fetch the event for modal presentation + let event: EKEvent? + + if let occurrenceDate = occurrenceDate { + // Query ±1 second around the exact occurrence time + // We use a small window since we have the precise timestamp + let startDate = occurrenceDate.addingTimeInterval(-1) + let endDate = occurrenceDate.addingTimeInterval(1) + + let predicate = eventStore.predicateForEvents( + withStart: startDate, + end: endDate, + calendars: nil + ) + + let events = eventStore.events(matching: predicate) + let matchingEvents = events.filter { $0.eventIdentifier == eventId } + + // Find the closest match to the occurrence date + event = matchingEvents.min(by: { abs($0.startDate.timeIntervalSince(occurrenceDate)) < abs($1.startDate.timeIntervalSince(occurrenceDate)) }) + } else { + // Get master event directly + event = eventStore.event(withIdentifier: eventId) + } + + // Check if event was found + guard let foundEvent = event else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.notFound, + message: "Event not found with instance ID: \(instanceId)" + ))) + return + } + + // Create event view controller + let eventViewController = EKEventViewController() + eventViewController.event = foundEvent + eventViewController.allowsEditing = true + eventViewController.allowsCalendarPreview = true + + completion(.success(eventViewController)) + } + private func createReminders(_ reminderMinutes: Int?) -> [EKAlarm]?{ + + guard let minutes = reminderMinutes as? Int else { + return nil + } + print("Reminder set for \(minutes) minutes before the event") + + var reminders = [EKAlarm]() + reminders.append(EKAlarm.init(relativeOffset: 60 * Double(-minutes))) + + return reminders + } + + func createEvent( + calendarId: String, + title: String, + startDate: Date, + endDate: Date, + isAllDay: Bool, + description: String?, + location: String?, + timeZone: String?, + availability: String, + reminderMinutes: Int?, + completion: @escaping (Result) -> Void + ) { + print("Reminder set for \(reminderMinutes) minutes before the event") + + // Check permission - creating events only requires write access + guard permissionService.hasPermission(for: .write) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Get the calendar + guard let calendar = eventStore.calendar(withIdentifier: calendarId) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.notFound, + message: "Calendar with ID \(calendarId) not found" + ))) + return + } + + // Create the event + let event = EKEvent(eventStore: eventStore) + event.calendar = calendar + event.title = title + event.startDate = startDate + event.endDate = endDate + event.isAllDay = isAllDay + event.alarms = createReminders(reminderMinutes) + + // Set optional properties + if let description = description { + event.notes = description + } + + if let location = location { + event.location = location + } + + // Set timezone (nil for all-day events) + if !isAllDay, let timeZoneIdentifier = timeZone { + event.timeZone = TimeZone(identifier: timeZoneIdentifier) + } + + // Map availability string to EKEventAvailability + switch availability { + case "free": + event.availability = .free + case "tentative": + event.availability = .tentative + case "unavailable": + event.availability = .unavailable + default: // "busy" or default + event.availability = .busy + } + + // Save the event + do { + try eventStore.save(event, span: .thisEvent) + + // Return the event ID + if let eventId = event.eventIdentifier { + completion(.success(eventId)) + } else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.operationFailed, + message: "Failed to get event ID after creation" + ))) + } + } catch { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.operationFailed, + message: "Failed to save event: \(error.localizedDescription)" + ))) + } + } + + func deleteEvent( + instanceId: String, + completion: @escaping (Result) -> Void + ) { + // Check permission + guard permissionService.hasPermission(for: .full) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Parse instanceId: "eventId" or "eventId@timestamp" + // For recurring events, we always delete the entire series, so extract just the eventId + let parts = instanceId.split(separator: "@", maxSplits: 1) + let eventId = String(parts[0]) + + // Fetch the master event by eventId + guard let event = eventStore.event(withIdentifier: eventId) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.notFound, + message: "Event not found with instance ID: \(instanceId)" + ))) + return + } + + // Delete the event + // For recurring events, .futureEvents on the master event deletes the entire series + // For non-recurring events, .futureEvents behaves the same as .thisEvent + do { + try eventStore.remove(event, span: .futureEvents) + completion(.success(())) + } catch { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.operationFailed, + message: "Failed to delete event: \(error.localizedDescription)" + ))) + } + } + + func updateEvent( + instanceId: String, + title: String?, + startDate: Date?, + endDate: Date?, + description: String?, + location: String?, + isAllDay: Bool?, + timeZone: String?, + completion: @escaping (Result) -> Void + ) { + // Check permission + guard permissionService.hasPermission(for: .full) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.permissionDenied, + message: "Calendar permission denied. Call requestPermissions() first." + ))) + return + } + + // Parse instanceId: "eventId" or "eventId@timestamp" + // For recurring events, we always update the entire series, so extract just the eventId + let parts = instanceId.split(separator: "@", maxSplits: 1) + let eventId = String(parts[0]) + + // Fetch the master event by eventId + guard let foundEvent = eventStore.event(withIdentifier: eventId) else { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.notFound, + message: "Event not found with instance ID: \(instanceId)" + ))) + return + } + + // Update only provided fields + if let title = title { + foundEvent.title = title + } + + if let description = description { + foundEvent.notes = description + } + + if let location = location { + foundEvent.location = location + } + + // Determine if event is/will be all-day + let effectiveIsAllDay = isAllDay ?? foundEvent.isAllDay + + // Update isAllDay if provided + if let isAllDay = isAllDay { + foundEvent.isAllDay = isAllDay + } + + // Update dates if provided + if let startDate = startDate { + foundEvent.startDate = startDate + } + if let endDate = endDate { + foundEvent.endDate = endDate + } + + // Update timezone + // For all-day events, timezone should be nil + // For timed events, set the timezone if provided + if effectiveIsAllDay { + foundEvent.timeZone = nil + } else if let timeZoneIdentifier = timeZone { + foundEvent.timeZone = TimeZone(identifier: timeZoneIdentifier) + } + + // Save the event + // For recurring events, .futureEvents on the master event updates the entire series + // For non-recurring events, .futureEvents behaves the same as .thisEvent + do { + try eventStore.save(foundEvent, span: .futureEvents) + completion(.success(())) + } catch { + completion(.failure(CalendarError( + code: PlatformExceptionCodes.operationFailed, + message: "Failed to update event: \(error.localizedDescription)" + ))) + } + } +} + diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/PermissionService.swift b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/PermissionService.swift new file mode 100644 index 00000000..393aaf85 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/PermissionService.swift @@ -0,0 +1,160 @@ +import EventKit + +enum CalendarPermissionType { + case write // Need to write events (iOS 17+ writeOnly or fullAccess is fine) + case full // Need to read calendars/events (requires fullAccess) +} + +class PermissionService { + private let eventStore: EKEventStore + + // Permission status values matching CalendarPermissionStatus enum + static let statusGranted = "granted" + static let statusWriteOnly = "writeOnly" + static let statusDenied = "denied" + static let statusRestricted = "restricted" + static let statusNotDetermined = "notDetermined" + + init(eventStore: EKEventStore) { + self.eventStore = eventStore + } + + /// Checks if calendar permissions are granted for the specified access level. + /// - Parameter type: The type of access required (.write or .full) + /// - Returns: true if the required permission level is granted + func hasPermission(for type: CalendarPermissionType = .full) -> Bool { + if #available(iOS 17.0, *) { + let status = EKEventStore.authorizationStatus(for: .event) + + switch type { + case .full: + // For full access (reading), need fullAccess only + switch status { + case .fullAccess: + return true + case .writeOnly, .denied, .restricted, .notDetermined: + return false + @unknown default: + return false + } + + case .write: + // For write-only operations, writeOnly or fullAccess is fine + switch status { + case .fullAccess, .writeOnly: + return true + case .denied, .restricted, .notDetermined: + return false + @unknown default: + return false + } + } + } else { + // iOS 16 and below only has .authorized (which is full access) + let status = EKEventStore.authorizationStatus(for: .event) + switch status { + case .authorized: + return true + case .denied, .restricted, .notDetermined: + return false + @unknown default: + return false + } + } + } + + private func checkUsageDescriptionDeclared() -> PermissionError? { + let usageDescription = Bundle.main.object(forInfoDictionaryKey: "NSCalendarsUsageDescription") as? String + + if usageDescription == nil || usageDescription?.isEmpty == true { + var errorMessage = "Calendar usage description not declared in Info.plist.\n\n" + errorMessage += "Add the following to ios/Runner/Info.plist:\n" + errorMessage += "NSCalendarsUsageDescription\n" + errorMessage += "Access your calendar to view and manage events.\n" + errorMessage += "NSCalendarsWriteOnlyAccessUsageDescription\n" + errorMessage += "Add events without reading existing events." + + return PermissionError(code: PlatformExceptionCodes.permissionsNotDeclared, message: errorMessage) + } + + return nil + } + + private func getCurrentPermissionStatus() -> String { + if #available(iOS 17.0, *) { + let currentStatus = EKEventStore.authorizationStatus(for: .event) + + switch currentStatus { + case .fullAccess: + return PermissionService.statusGranted + case .writeOnly: + return PermissionService.statusWriteOnly + case .denied: + return PermissionService.statusDenied + case .restricted: + return PermissionService.statusRestricted + case .notDetermined: + return PermissionService.statusNotDetermined + @unknown default: + return PermissionService.statusDenied + } + } else { + let currentStatus = EKEventStore.authorizationStatus(for: .event) + + switch currentStatus { + case .authorized: + return PermissionService.statusGranted + case .denied: + return PermissionService.statusDenied + case .restricted: + return PermissionService.statusRestricted + case .notDetermined: + return PermissionService.statusNotDetermined + @unknown default: + return PermissionService.statusDenied + } + } + } + + func hasPermissions() -> Result { + if let error = checkUsageDescriptionDeclared() { + return .failure(error) + } + + return .success(getCurrentPermissionStatus()) + } + + func requestPermissions(completion: @escaping (Result) -> Void) { + if let error = checkUsageDescriptionDeclared() { + completion(.failure(error)) + return + } + + let currentStatus = getCurrentPermissionStatus() + + // If already determined (granted, denied, restricted, or writeOnly), return immediately + if currentStatus != PermissionService.statusNotDetermined { + completion(.success(currentStatus)) + return + } + + // Request permissions if not determined + if #available(iOS 17.0, *) { + eventStore.requestFullAccessToEvents { granted, error in + let status = granted ? PermissionService.statusGranted : PermissionService.statusDenied + completion(.success(status)) + } + } else { + eventStore.requestAccess(to: .event) { granted, error in + let status = granted ? PermissionService.statusGranted : PermissionService.statusDenied + completion(.success(status)) + } + } + } +} + +struct PermissionError: Error { + let code: String + let message: String +} + diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/PlatformExceptionCodes.swift b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/PlatformExceptionCodes.swift new file mode 100644 index 00000000..c71a9bd0 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Classes/PlatformExceptionCodes.swift @@ -0,0 +1,62 @@ +/// Platform exception codes matching PlatformExceptionCodes in Dart. +/// +/// These codes are sent via method channel errors and caught/transformed +/// by the Dart layer into DeviceCalendarException. +enum PlatformExceptionCodes { + // Permission-related errors + + /// Calendar usage description not declared in Info.plist. + /// + /// Missing NSCalendarsUsageDescription in Info.plist + static let permissionsNotDeclared = "PERMISSIONS_NOT_DECLARED" + + /// Calendar permission denied by user. + /// + /// User has explicitly denied calendar access, or security exception occurred. + static let permissionDenied = "PERMISSION_DENIED" + + // Input validation errors + + /// Invalid arguments passed to a method. + /// + /// Parameters are missing, of wrong type, or contain invalid values. + static let invalidArguments = "INVALID_ARGUMENTS" + + // Resource errors + + /// Requested calendar or event not found. + /// + /// The calendar ID or event instance ID doesn't exist. + static let notFound = "NOT_FOUND" + + /// Calendar is read-only and cannot be modified. + /// + /// Attempting to update or delete a calendar that doesn't allow modifications. + static let readOnly = "READ_ONLY" + + // Operation errors + + /// Calendar operation failed. + /// + /// Save, update, or delete operation failed for reasons other than permissions. + /// Check error message for details. + static let operationFailed = "OPERATION_FAILED" + + // System/availability errors + + /// Calendar system is not available. + /// + /// Examples: + /// - Local calendar source not found + /// - Event store unavailable + static let calendarUnavailable = "CALENDAR_UNAVAILABLE" + + // Generic errors + + /// An unknown or unexpected error occurred. + /// + /// Used for unexpected exceptions that don't fit other categories. + /// Check error message for details. + static let unknownError = "UNKNOWN_ERROR" +} + diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Resources/PrivacyInfo.xcprivacy b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..a34b7e2e --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/ios/device_calendar_plus_ios.podspec b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/device_calendar_plus_ios.podspec new file mode 100644 index 00000000..4f0c9e08 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/ios/device_calendar_plus_ios.podspec @@ -0,0 +1,29 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint device_calendar_plus_ios.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'device_calendar_plus_ios' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '13.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + # If your plugin requires a privacy manifest, for example if it uses any + # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your + # plugin's privacy impact, and then uncomment this line. For more information, + # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + # s.resource_bundles = {'device_calendar_plus_ios_privacy' => ['Resources/PrivacyInfo.xcprivacy']} +end diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/lib/device_calendar_plus_ios.dart b/package/device_calendar_plus/package/device_calendar_plus_ios/lib/device_calendar_plus_ios.dart new file mode 100644 index 00000000..ed1fa78a --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/lib/device_calendar_plus_ios.dart @@ -0,0 +1,177 @@ +import 'package:device_calendar_plus_platform_interface/device_calendar_plus_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// The iOS implementation of [DeviceCalendarPlusPlatform]. +class DeviceCalendarPlusIos extends DeviceCalendarPlusPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('device_calendar_plus_ios'); + + /// Registers this class as the default instance of [DeviceCalendarPlusPlatform]. + static void registerWith() { + DeviceCalendarPlusPlatform.instance = DeviceCalendarPlusIos(); + } + + @override + Future requestPermissions() async { + return await methodChannel.invokeMethod('requestPermissions'); + } + + @override + Future hasPermissions() async { + return await methodChannel.invokeMethod('hasPermissions'); + } + + @override + Future openAppSettings() async { + await methodChannel.invokeMethod('openAppSettings'); + } + + @override + Future>> listCalendars() async { + final result = + await methodChannel.invokeMethod>('listCalendars'); + return result?.map((e) => Map.from(e as Map)).toList() ?? + []; + } + + @override + Future createCalendar(String name, String? colorHex) async { + final result = await methodChannel.invokeMethod( + 'createCalendar', + { + 'name': name, + 'colorHex': colorHex, + }, + ); + return result!; + } + + @override + Future updateCalendar( + String calendarId, String? name, String? colorHex) async { + await methodChannel.invokeMethod( + 'updateCalendar', + { + 'calendarId': calendarId, + 'name': name, + 'colorHex': colorHex, + }, + ); + } + + @override + Future deleteCalendar(String calendarId) async { + await methodChannel.invokeMethod( + 'deleteCalendar', + {'calendarId': calendarId}, + ); + } + + @override + Future>> listEvents( + DateTime startDate, + DateTime endDate, + List? calendarIds, + ) async { + final result = await methodChannel.invokeMethod>( + 'listEvents', + { + 'startDate': startDate.millisecondsSinceEpoch, + 'endDate': endDate.millisecondsSinceEpoch, + 'calendarIds': calendarIds, + }, + ); + return result?.map((e) => Map.from(e as Map)).toList() ?? + []; + } + + @override + Future?> getEvent(String instanceId) async { + final result = await methodChannel.invokeMethod>( + 'getEvent', + { + 'instanceId': instanceId, + }, + ); + return result != null ? Map.from(result) : null; + } + + @override + Future showEventModal(String instanceId) async { + await methodChannel.invokeMethod( + 'showEventModal', + {'instanceId': instanceId}, + ); + } + + @override + Future createEvent( + String calendarId, + String title, + DateTime startDate, + DateTime endDate, + bool isAllDay, + int? reminderMinutes, + String? description, + String? location, + String? timeZone, + String availability, + ) async { + print("createEvent2ios: the reminder minutes are $reminderMinutes"); + + final result = await methodChannel.invokeMethod( + 'createEvent', + { + 'calendarId': calendarId, + 'title': title, + 'startDate': startDate.millisecondsSinceEpoch, + 'endDate': endDate.millisecondsSinceEpoch, + 'isAllDay': isAllDay, + 'description': description, + 'location': location, + 'timeZone': timeZone, + 'availability': availability, + 'reminderMinutes': reminderMinutes, + }, + ); + return result!; + } + + @override + Future deleteEvent(String instanceId) async { + await methodChannel.invokeMethod( + 'deleteEvent', + { + 'instanceId': instanceId, + }, + ); + } + + @override + Future updateEvent( + String instanceId, { + String? title, + DateTime? startDate, + DateTime? endDate, + String? description, + String? location, + bool? isAllDay, + String? timeZone, + }) async { + await methodChannel.invokeMethod( + 'updateEvent', + { + 'instanceId': instanceId, + 'title': title, + 'startDate': startDate?.millisecondsSinceEpoch, + 'endDate': endDate?.millisecondsSinceEpoch, + 'description': description, + 'location': location, + 'isAllDay': isAllDay, + 'timeZone': timeZone, + }, + ); + } +} diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/pubspec.yaml b/package/device_calendar_plus/package/device_calendar_plus_ios/pubspec.yaml new file mode 100644 index 00000000..f4f21f92 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/pubspec.yaml @@ -0,0 +1,63 @@ +name: device_calendar_plus_ios +description: iOS implementation of the device_calendar_plus plugin. +version: 0.3.1 +repository: https://github.com/bullet-to/device_calendar_plus + + +environment: + sdk: ">=3.5.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + device_calendar_plus_platform_interface: + path: ../device_calendar_plus_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + plugin: + implements: device_calendar_plus + platforms: + ios: + pluginClass: DeviceCalendarPlusIosPlugin + dartPluginClass: DeviceCalendarPlusIos + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/package/device_calendar_plus/package/device_calendar_plus_ios/test/device_calendar_plus_ios_test.dart b/package/device_calendar_plus/package/device_calendar_plus_ios/test/device_calendar_plus_ios_test.dart new file mode 100644 index 00000000..9ed3f9ed --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_ios/test/device_calendar_plus_ios_test.dart @@ -0,0 +1,312 @@ +// import 'package:device_calendar_plus_ios/device_calendar_plus_ios.dart'; +// import 'package:device_calendar_plus_platform_interface/device_calendar_plus_platform_interface.dart'; +// import 'package:flutter/services.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// +// void main() { +// TestWidgetsFlutterBinding.ensureInitialized(); +// +// group('DeviceCalendarPlusIos', () { +// late DeviceCalendarPlusIos plugin; +// late List log; +// +// setUp(() async { +// plugin = DeviceCalendarPlusIos(); +// +// log = []; +// TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger +// .setMockMethodCallHandler(plugin.methodChannel, (methodCall) async { +// log.add(methodCall); +// switch (methodCall.method) { +// case 'requestPermissions': +// return 'granted'; // CalendarPermissionStatus.granted +// case 'hasPermissions': +// return 'granted'; // CalendarPermissionStatus.granted +// case 'openAppSettings': +// return null; +// case 'listCalendars': +// return [ +// { +// 'id': '1', +// 'name': 'Work', +// 'readOnly': false, +// 'isPrimary': true, +// 'hidden': false, +// } +// ]; +// case 'createCalendar': +// return 'test-calendar-id-123'; +// case 'updateCalendar': +// return null; +// case 'deleteCalendar': +// return null; +// case 'listEvents': +// return [ +// { +// 'eventId': 'event1', +// 'calendarId': 'cal1', +// 'title': 'Test Event', +// 'startDate': DateTime.now().millisecondsSinceEpoch, +// 'endDate': DateTime.now().millisecondsSinceEpoch, +// 'isAllDay': false, +// 'availability': 'busy', +// 'status': 'confirmed', +// } +// ]; +// case 'createEvent': +// return 'ios-event-id-456'; +// case 'deleteEvent': +// return null; +// case 'updateEvent': +// return null; +// default: +// return null; +// } +// }); +// }); +// +// test('can be registered', () { +// DeviceCalendarPlusIos.registerWith(); +// expect(DeviceCalendarPlusPlatform.instance, isA()); +// }); +// +// test('requestPermissions returns granted status', () async { +// final status = await plugin.requestPermissions(); +// expect( +// log, +// [isMethodCall('requestPermissions', arguments: null)], +// ); +// expect(status, equals('granted')); // CalendarPermissionStatus.granted +// }); +// +// test('hasPermissions returns granted status', () async { +// final status = await plugin.hasPermissions(); +// expect( +// log, +// [isMethodCall('hasPermissions', arguments: null)], +// ); +// expect(status, equals('granted')); // CalendarPermissionStatus.granted +// }); +// +// test('openAppSettings calls method', () async { +// await plugin.openAppSettings(); +// expect( +// log, +// [isMethodCall('openAppSettings', arguments: null)], +// ); +// }); +// +// test('listCalendars returns list of calendars', () async { +// final calendars = await plugin.listCalendars(); +// expect( +// log, +// [isMethodCall('listCalendars', arguments: null)], +// ); +// expect(calendars, hasLength(1)); +// expect(calendars[0]['id'], equals('1')); +// expect(calendars[0]['name'], equals('Work')); +// }); +// +// test('createCalendar with name only', () async { +// final calendarId = await plugin.createCalendar('My Calendar', null); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('createCalendar')); +// expect(log[0].arguments['name'], equals('My Calendar')); +// expect(log[0].arguments['colorHex'], isNull); +// expect(calendarId, equals('test-calendar-id-123')); +// }); +// +// test('createCalendar with name and color', () async { +// final calendarId = +// await plugin.createCalendar('Work Calendar', '#FF5733'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('createCalendar')); +// expect(log[0].arguments['name'], equals('Work Calendar')); +// expect(log[0].arguments['colorHex'], equals('#FF5733')); +// expect(calendarId, equals('test-calendar-id-123')); +// }); +// +// test('updateCalendar with name only', () async { +// await plugin.updateCalendar('cal-123', 'Updated Name', null); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateCalendar')); +// expect(log[0].arguments['calendarId'], equals('cal-123')); +// expect(log[0].arguments['name'], equals('Updated Name')); +// expect(log[0].arguments['colorHex'], isNull); +// }); +// +// test('updateCalendar with name and color', () async { +// await plugin.updateCalendar('cal-123', 'Updated Name', '#00FF00'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateCalendar')); +// expect(log[0].arguments['calendarId'], equals('cal-123')); +// expect(log[0].arguments['name'], equals('Updated Name')); +// expect(log[0].arguments['colorHex'], equals('#00FF00')); +// }); +// +// test('deleteCalendar calls method with correct arguments', () async { +// await plugin.deleteCalendar('cal-123'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('deleteCalendar')); +// expect(log[0].arguments['calendarId'], equals('cal-123')); +// }); +// +// test('listEvents returns list of events', () async { +// final now = DateTime.now(); +// final later = now.add(Duration(days: 7)); +// +// final events = await plugin.listEvents(now, later, ['cal1']); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('listEvents')); +// expect(log[0].arguments['startDate'], equals(now.millisecondsSinceEpoch)); +// expect(log[0].arguments['endDate'], equals(later.millisecondsSinceEpoch)); +// expect(log[0].arguments['calendarIds'], equals(['cal1'])); +// +// expect(events, hasLength(1)); +// expect(events[0]['eventId'], equals('event1')); +// expect(events[0]['title'], equals('Test Event')); +// }); +// +// test('createEvent with all parameters', () async { +// final startDate = DateTime(2024, 3, 15, 14, 0); +// final endDate = DateTime(2024, 3, 15, 15, 0); +// +// final eventId = await plugin.createEvent( +// 'cal-123', +// 'Team Meeting', +// startDate, +// endDate, +// false, +// 'Weekly sync', +// 'Conference Room A', +// 'America/New_York', +// 'busy', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('createEvent')); +// expect(log[0].arguments['calendarId'], equals('cal-123')); +// expect(log[0].arguments['title'], equals('Team Meeting')); +// expect(log[0].arguments['startDate'], +// equals(startDate.millisecondsSinceEpoch)); +// expect( +// log[0].arguments['endDate'], equals(endDate.millisecondsSinceEpoch)); +// expect(log[0].arguments['isAllDay'], equals(false)); +// expect(log[0].arguments['description'], equals('Weekly sync')); +// expect(log[0].arguments['location'], equals('Conference Room A')); +// expect(log[0].arguments['timeZone'], equals('America/New_York')); +// expect(log[0].arguments['availability'], equals('busy')); +// expect(eventId, equals('ios-event-id-456')); +// }); +// +// test('createEvent with minimal parameters', () async { +// final startDate = DateTime(2024, 3, 15, 14, 0); +// final endDate = DateTime(2024, 3, 15, 15, 0); +// +// final eventId = await plugin.createEvent( +// 'cal-123', +// 'Quick Event', +// startDate, +// endDate, +// true, +// null, +// null, +// null, +// 'free', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('createEvent')); +// expect(log[0].arguments['calendarId'], equals('cal-123')); +// expect(log[0].arguments['title'], equals('Quick Event')); +// expect(log[0].arguments['isAllDay'], equals(true)); +// expect(log[0].arguments['description'], isNull); +// expect(log[0].arguments['location'], isNull); +// expect(log[0].arguments['timeZone'], isNull); +// expect(log[0].arguments['availability'], equals('free')); +// expect(eventId, equals('ios-event-id-456')); +// }); +// +// test('deleteEvent calls method with correct arguments', () async { +// await plugin.deleteEvent('event-123'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('deleteEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123')); +// }); +// +// test('deleteEvent for recurring event deletes entire series', () async { +// await plugin.deleteEvent('event-123@123456789'); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('deleteEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123@123456789')); +// }); +// +// test('updateEvent with all parameters', () async { +// final startDate = DateTime(2024, 3, 20, 10, 0); +// final endDate = DateTime(2024, 3, 20, 11, 0); +// +// await plugin.updateEvent( +// 'event-123', +// title: 'Updated Title', +// startDate: startDate, +// endDate: endDate, +// description: 'Updated description', +// location: 'Updated location', +// isAllDay: false, +// timeZone: 'America/New_York', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123')); +// expect(log[0].arguments['title'], equals('Updated Title')); +// expect(log[0].arguments['startDate'], +// equals(startDate.millisecondsSinceEpoch)); +// expect( +// log[0].arguments['endDate'], equals(endDate.millisecondsSinceEpoch)); +// expect(log[0].arguments['description'], equals('Updated description')); +// expect(log[0].arguments['location'], equals('Updated location')); +// expect(log[0].arguments['isAllDay'], equals(false)); +// expect(log[0].arguments['timeZone'], equals('America/New_York')); +// }); +// +// test('updateEvent with minimal parameters', () async { +// await plugin.updateEvent( +// 'event-123', +// title: 'New Title', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123')); +// expect(log[0].arguments['title'], equals('New Title')); +// expect(log[0].arguments['startDate'], isNull); +// expect(log[0].arguments['endDate'], isNull); +// expect(log[0].arguments['description'], isNull); +// expect(log[0].arguments['location'], isNull); +// expect(log[0].arguments['isAllDay'], isNull); +// expect(log[0].arguments['timeZone'], isNull); +// expect(log[0].arguments['availability'], isNull); +// }); +// +// test('updateEvent for recurring event updates entire series', () async { +// await plugin.updateEvent( +// 'event-123', +// title: 'Updated Series', +// ); +// +// expect(log.length, equals(1)); +// expect(log[0].method, equals('updateEvent')); +// expect(log[0].arguments['instanceId'], equals('event-123')); +// expect(log[0].arguments['title'], equals('Updated Series')); +// }); +// }); +// } diff --git a/package/device_calendar_plus/package/device_calendar_plus_platform_interface/CHANGELOG.md b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/CHANGELOG.md new file mode 100644 index 00000000..ef23deb7 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/CHANGELOG.md @@ -0,0 +1,28 @@ +## 0.3.1 - 2025-11-07 + +Version sync with other packages. No functional changes. + +## 0.3.0 - 2024-11-05 + +### Changed +- **BREAKING**: `deleteEvent()` signature changed - removed `deleteAllInstances` parameter, operations on recurring events now always delete entire series +- **BREAKING**: `updateEvent()` signature changed - removed `updateAllInstances` parameter, operations on recurring events now always update entire series + +### Removed +- **BREAKING**: `NOT_SUPPORTED` platform exception code (no longer needed) + +## 0.2.0 - 2024-11-05 + +### Added +- `openAppSettings()` method to direct users to app settings for permission management + +### Removed +- **BREAKING**: `getPlatformVersion()` method (unused boilerplate) + +## 0.1.1 - 2024-11-04 + +Version sync with other packages. No functional changes. + +## 0.1.0 - 2024-11-04 + +Initial release. \ No newline at end of file diff --git a/package/device_calendar_plus/package/device_calendar_plus_platform_interface/LICENSE b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/LICENSE new file mode 100644 index 00000000..0152eb2c --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 bullet.to + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/device_calendar_plus/package/device_calendar_plus_platform_interface/README.md b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/README.md new file mode 100644 index 00000000..861d0ab2 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/README.md @@ -0,0 +1,25 @@ +# device_calendar_plus_platform_interface + +Platform interface for the `device_calendar_plus` plugin. + +This package defines the interface that platform implementations must implement. It is not intended to be used directly by application developers. + +## For App Developers + +If you're building a Flutter app, use the main [`device_calendar_plus`](https://pub.dev/packages/device_calendar_plus) package instead. + +## For Plugin Developers + +This package contains: +- Method channel constants and method names +- Platform interface abstract class +- Data serialization contracts for calendars and events +- Permission status and error code definitions + +Platform implementations: +- [`device_calendar_plus_android`](https://pub.dev/packages/device_calendar_plus_android) - Android implementation +- [`device_calendar_plus_ios`](https://pub.dev/packages/device_calendar_plus_ios) - iOS implementation + +## License + +MIT © 2025 Bullet diff --git a/package/device_calendar_plus/package/device_calendar_plus_platform_interface/analysis_options.yaml b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/package/device_calendar_plus/package/device_calendar_plus_platform_interface/lib/device_calendar_plus_platform_interface.dart b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/lib/device_calendar_plus_platform_interface.dart new file mode 100644 index 00000000..8c82f78d --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/lib/device_calendar_plus_platform_interface.dart @@ -0,0 +1,201 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// The interface that implementations of device_calendar_plus must implement. +/// +/// Platform implementations should extend this class rather than implement it +/// as `DeviceCalendar`. Extending this class (using `extends`) ensures that +/// the subclass will get the default implementation, while platform +/// implementations that `implements` this interface will be broken by newly +/// added [DeviceCalendarPlusPlatform] methods. +abstract class DeviceCalendarPlusPlatform extends PlatformInterface { + DeviceCalendarPlusPlatform() : super(token: _token); + + static final Object _token = Object(); + + static DeviceCalendarPlusPlatform? _instance; + + /// The default instance of [DeviceCalendarPlusPlatform] to use. + /// + /// Platform-specific implementations (Android/iOS) set this automatically. + static DeviceCalendarPlusPlatform get instance { + if (_instance == null) { + throw StateError( + 'DeviceCalendarPlusPlatform.instance has not been initialized. ' + 'This should never happen in production as platform-specific ' + 'implementations register themselves automatically.', + ); + } + return _instance!; + } + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [DeviceCalendarPlusPlatform] when they register themselves. + static set instance(DeviceCalendarPlusPlatform instance) { + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// Requests calendar permissions from the user. + /// + /// On first call, this will show the system permission dialog. + /// On subsequent calls, it returns the current permission status. + /// + /// Returns the raw string status value from the platform. + /// The main API layer converts this to [CalendarPermissionStatus]. + Future requestPermissions(); + + /// Checks the current calendar permission status WITHOUT requesting permissions. + /// + /// Unlike [requestPermissions], this method will NOT prompt the user for + /// permissions if they haven't been granted yet. It only checks the current status. + /// + /// Returns the raw string status value from the platform. + /// The main API layer converts this to [CalendarPermissionStatus]. + Future hasPermissions(); + + /// Opens the app's settings page in the system settings. + /// + /// This is useful when permissions have been denied and you want to guide + /// the user to manually enable calendar permissions in the system settings. + /// + /// On iOS, opens the app's specific settings page. + /// On Android, opens the app info page where users can navigate to permissions. + Future openAppSettings(); + + /// Lists all calendars available on the device. + /// + /// Returns a list of calendar data as maps. The main API layer + /// converts these to [DeviceCalendar] objects. + Future>> listCalendars(); + + /// Creates a new calendar on the device. + /// + /// [name] is the display name for the calendar (required). + /// [colorHex] is an optional color in #RRGGBB format. + /// + /// Returns the ID of the newly created calendar. + /// + /// The calendar is created in the device's local storage. + /// Requires calendar write permissions. + Future createCalendar(String name, String? colorHex); + + /// Updates an existing calendar on the device. + /// + /// [calendarId] is the ID of the calendar to update. + /// [name] is the new display name for the calendar (optional). + /// [colorHex] is the new color in #RRGGBB format (optional). + /// + /// At least one of [name] or [colorHex] must be provided. + /// Requires calendar write permissions. + Future updateCalendar( + String calendarId, String? name, String? colorHex); + + /// Deletes a calendar from the device. + /// + /// [calendarId] is the ID of the calendar to delete. + /// + /// This will also delete all events within the calendar. + /// Requires calendar write permissions. + Future deleteCalendar(String calendarId); + + /// Lists events within the specified date range. + /// + /// Returns a list of event data as maps. The main API layer + /// converts these to [Event] objects. + Future>> listEvents( + DateTime startDate, + DateTime endDate, + List? calendarIds, + ); + + /// Retrieves a single event by instance ID. + /// + /// [instanceId] uniquely identifies the event instance: + /// - For non-recurring events: Just the eventId + /// - For recurring events: "eventId@rawTimestampMillis" format + /// + /// Returns event data as a map (including instanceId field), or null if not found. + Future?> getEvent(String instanceId); + + /// Shows a calendar event in a modal dialog. + /// + /// [instanceId] uniquely identifies the event instance to show: + /// - For non-recurring events: Just the eventId + /// - For recurring events: "eventId@rawTimestampMillis" format + /// + /// On iOS, presents the event in a modal using EKEventViewController. + /// On Android, opens the event using an Intent with ACTION_VIEW. + Future showEventModal(String instanceId); + + /// Creates a new event in the specified calendar. + /// + /// [calendarId] is the ID of the calendar to create the event in. + /// [title] is the event title. + /// [startDate] is the start date/time. + /// [endDate] is the end date/time. + /// [isAllDay] indicates if this is an all-day event. + /// [description] is optional event notes/description. + /// [location] is optional event location. + /// [timeZone] is optional timezone identifier (null for all-day events). + /// [availability] is the availability status (busy, free, tentative, unavailable). + /// + /// Returns the ID of the newly created event (system-generated). + /// Requires calendar write permissions. + Future createEvent( + String calendarId, + String title, + DateTime startDate, + DateTime endDate, + bool isAllDay, + int? reminderMinutes, + String? description, + String? location, + String? timeZone, + String availability, + ); + + /// Deletes an event from the device. + /// + /// [instanceId] uniquely identifies the event instance to delete: + /// - For non-recurring events: Just the eventId + /// - For recurring events: "eventId@rawTimestampMillis" format + /// + /// **For recurring events**: This will delete the ENTIRE series (all past and + /// future occurrences). Single-instance deletion is not supported to maintain + /// consistent behavior across platforms. + /// + /// Requires calendar write permissions. + Future deleteEvent(String instanceId); + + /// Updates an existing event on the device. + /// + /// [instanceId] uniquely identifies the event instance to update: + /// - For non-recurring events: Just the eventId + /// - For recurring events: "eventId@rawTimestampMillis" format + /// + /// **For recurring events**: This will update the ENTIRE series (all past and + /// future occurrences). Single-instance updates are not supported to maintain + /// consistent behavior across platforms. + /// + /// All field parameters are optional - only provided fields will be updated: + /// - [title] - new event title + /// - [startDate] - new start date/time + /// - [endDate] - new end date/time + /// - [description] - new event description + /// - [location] - new event location + /// - [isAllDay] - change between all-day and timed event + /// - [timeZone] - new timezone identifier + /// + /// At least one field must be provided. + /// Requires calendar write permissions. + Future updateEvent( + String instanceId, { + String? title, + DateTime? startDate, + DateTime? endDate, + String? description, + String? location, + bool? isAllDay, + String? timeZone, + }); +} diff --git a/package/device_calendar_plus/package/device_calendar_plus_platform_interface/pubspec.yaml b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/pubspec.yaml new file mode 100644 index 00000000..ca8808f5 --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/pubspec.yaml @@ -0,0 +1,56 @@ +name: device_calendar_plus_platform_interface +description: Platform interface for device_calendar_plus plugin. +version: 0.3.1 +repository: https://github.com/bullet-to/device_calendar_plus + + +environment: + sdk: ">=3.5.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/package/device_calendar_plus/package/device_calendar_plus_platform_interface/test/device_calendar_plus_platform_interface_test.dart b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/test/device_calendar_plus_platform_interface_test.dart new file mode 100644 index 00000000..bc1f9acf --- /dev/null +++ b/package/device_calendar_plus/package/device_calendar_plus_platform_interface/test/device_calendar_plus_platform_interface_test.dart @@ -0,0 +1,225 @@ +// import 'package:device_calendar_plus_platform_interface/device_calendar_plus_platform_interface.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +// +// class MockDeviceCalendarPlusPlatform extends DeviceCalendarPlusPlatform +// with MockPlatformInterfaceMixin { +// @override +// Future requestPermissions() async => "granted"; +// +// @override +// Future hasPermissions() async => "granted"; +// +// @override +// Future openAppSettings() async {} +// +// @override +// Future>> listCalendars() async => []; +// +// @override +// Future createCalendar(String name, String? colorHex) async => +// 'mock-calendar-id'; +// +// @override +// Future updateCalendar( +// String calendarId, String? name, String? colorHex) async {} +// +// @override +// Future deleteCalendar(String calendarId) async {} +// +// @override +// Future>> listEvents( +// DateTime startDate, +// DateTime endDate, +// List? calendarIds, +// ) async => +// []; +// +// @override +// Future?> getEvent(String instanceId) async => null; +// +// @override +// Future showEventModal(String instanceId) async {} +// +// @override +// Future createEvent( +// String calendarId, +// String title, +// DateTime startDate, +// DateTime endDate, +// bool isAllDay, +// String? description, +// String? location, +// String? timeZone, +// String availability, +// ) async => +// 'mock-event-id'; +// +// @override +// Future deleteEvent(String instanceId) async {} +// +// @override +// Future updateEvent( +// String instanceId, { +// String? title, +// DateTime? startDate, +// DateTime? endDate, +// String? description, +// String? location, +// bool? isAllDay, +// String? timeZone, +// String? availability, +// }) async {} +// } +// +// void main() { +// test('can set and get custom instance', () { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// expect(DeviceCalendarPlusPlatform.instance, mock); +// }); +// +// test('requestPermissions returns expected value', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// expect(await DeviceCalendarPlusPlatform.instance.requestPermissions(), +// 'granted'); +// }); +// +// test('hasPermissions returns expected value', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// expect( +// await DeviceCalendarPlusPlatform.instance.hasPermissions(), 'granted'); +// }); +// +// test('openAppSettings completes successfully', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// await DeviceCalendarPlusPlatform.instance.openAppSettings(); +// // Should complete without error +// }); +// +// test('listCalendars returns expected value', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// expect(await DeviceCalendarPlusPlatform.instance.listCalendars(), []); +// }); +// +// test('createCalendar returns expected value', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// final calendarId = await DeviceCalendarPlusPlatform.instance +// .createCalendar('Test Calendar', '#FF5733'); +// expect(calendarId, equals('mock-calendar-id')); +// }); +// +// test('updateCalendar completes', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// await DeviceCalendarPlusPlatform.instance +// .updateCalendar('calendar-123', 'New Name', '#00FF00'); +// // Should complete without error +// }); +// +// test('deleteCalendar completes', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// await DeviceCalendarPlusPlatform.instance.deleteCalendar('calendar-123'); +// // Should complete without error +// }); +// +// test('listEvents returns expected value', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// final result = await DeviceCalendarPlusPlatform.instance.listEvents( +// DateTime.now(), +// DateTime.now().add(Duration(days: 7)), +// null, +// ); +// expect(result, []); +// }); +// +// test('getEvent returns expected value', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// final result = +// await DeviceCalendarPlusPlatform.instance.getEvent('event-123'); +// expect(result, null); +// }); +// +// test('showEventModal completes', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// await DeviceCalendarPlusPlatform.instance.showEventModal('event-123'); +// // Should complete without error +// }); +// +// test('createEvent returns expected value', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// final eventId = await DeviceCalendarPlusPlatform.instance.createEvent( +// 'calendar-123', +// 'Team Meeting', +// DateTime(2024, 3, 15, 14, 0), +// DateTime(2024, 3, 15, 15, 0), +// false, +// 'Weekly team sync', +// 'Conference Room A', +// 'America/New_York', +// 'busy', +// ); +// expect(eventId, equals('mock-event-id')); +// }); +// +// test('deleteEvent completes', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// await DeviceCalendarPlusPlatform.instance.deleteEvent('event-123'); +// // Should complete without error +// }); +// +// test('deleteEvent for recurring event deletes entire series', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// await DeviceCalendarPlusPlatform.instance +// .deleteEvent('event-123@123456789'); +// // Should complete without error +// }); +// +// test('updateEvent with all parameters completes', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// await DeviceCalendarPlusPlatform.instance.updateEvent( +// 'event-123', +// title: 'Updated Title', +// startDate: DateTime(2024, 3, 20, 10, 0), +// endDate: DateTime(2024, 3, 20, 11, 0), +// description: 'Updated description', +// location: 'Updated location', +// isAllDay: false, +// timeZone: 'America/New_York', +// ); +// // Should complete without error +// }); +// +// test('updateEvent with minimal parameters completes', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// await DeviceCalendarPlusPlatform.instance.updateEvent( +// 'event-123', +// title: 'New Title', +// ); +// // Should complete without error +// }); +// +// test('updateEvent for recurring event updates entire series', () async { +// final mock = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mock; +// await DeviceCalendarPlusPlatform.instance.updateEvent( +// 'event-123', +// title: 'Updated Series', +// ); +// // Should complete without error +// }); +// } diff --git a/package/device_calendar_plus/pubspec.lock b/package/device_calendar_plus/pubspec.lock new file mode 100644 index 00000000..a5f5289d --- /dev/null +++ b/package/device_calendar_plus/pubspec.lock @@ -0,0 +1,237 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + device_calendar_plus_android: + dependency: "direct main" + description: + name: device_calendar_plus_android + sha256: f66f363273e0b174fdb3f98030a8c67ae9e9dfcb6122e4ca842881f976313db0 + url: "https://pub.dev" + source: hosted + version: "0.3.1" + device_calendar_plus_ios: + dependency: "direct main" + description: + name: device_calendar_plus_ios + sha256: c9b234091d3edc78871ed524077c262ce66217b960b7942ef071032e47b60fa8 + url: "https://pub.dev" + source: hosted + version: "0.3.1" + device_calendar_plus_platform_interface: + dependency: "direct main" + description: + name: device_calendar_plus_platform_interface + sha256: fb4d893d04b10ab00bc175be8a374aad08a7c4ad7d763c7ff48036d76be7604b + url: "https://pub.dev" + source: hosted + version: "0.3.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: "direct dev" + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" +sdks: + dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/package/device_calendar_plus/pubspec.yaml b/package/device_calendar_plus/pubspec.yaml new file mode 100644 index 00000000..cf5779bf --- /dev/null +++ b/package/device_calendar_plus/pubspec.yaml @@ -0,0 +1,77 @@ +name: device_calendar_plus +description: A modern, maintained Flutter plugin for reading and writing device calendar events on Android and iOS. +version: 0.3.1 +repository: https://github.com/bullet-to/device_calendar_plus +issue_tracker: https://github.com/bullet-to/device_calendar_plus/issues + + +topics: + - calendar + - events + - eventkit + - calendar-provider + - federated + +environment: + sdk: ">=3.5.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + device_calendar_plus_platform_interface: + path: ./package/device_calendar_plus_platform_interface + device_calendar_plus_android: + path: ./package/device_calendar_plus_android + device_calendar_plus_ios: + path: ./package/device_calendar_plus_ios + + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + plugin_platform_interface: ^2.1.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + plugin: + platforms: + android: + default_package: device_calendar_plus_android + ios: + default_package: device_calendar_plus_ios + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/package/device_calendar_plus/test/device_calendar_plus_test.dart b/package/device_calendar_plus/test/device_calendar_plus_test.dart new file mode 100644 index 00000000..8a687e31 --- /dev/null +++ b/package/device_calendar_plus/test/device_calendar_plus_test.dart @@ -0,0 +1,1348 @@ +// import 'package:device_calendar_plus/device_calendar_plus.dart'; +// import 'package:device_calendar_plus_platform_interface/device_calendar_plus_platform_interface.dart'; +// import 'package:flutter/services.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +// +// class MockDeviceCalendarPlusPlatform extends DeviceCalendarPlusPlatform +// with MockPlatformInterfaceMixin { +// String? _permissionStatusCode = +// "notDetermined"; // CalendarPermissionStatus.notDetermined.name +// List> _calendars = []; +// List> _events = []; +// Map? _event; +// PlatformException? _exceptionToThrow; +// +// // Callback to capture createEvent arguments +// Future Function( +// String calendarId, +// String title, +// DateTime startDate, +// DateTime endDate, +// bool isAllDay, +// String? description, +// String? location, +// String? timeZone, +// String availability, +// )? _createEventCallback; +// +// // Callback to capture updateEvent arguments +// Future Function( +// String instanceId, { +// String? title, +// DateTime? startDate, +// DateTime? endDate, +// String? description, +// String? location, +// bool? isAllDay, +// String? timeZone, +// String? availability, +// })? _updateEventCallback; +// +// void setPermissionStatus(CalendarPermissionStatus status) { +// _permissionStatusCode = status.name; +// } +// +// void setCalendars(List> calendars) { +// _calendars = calendars; +// } +// +// void setEvents(List> events) { +// _events = events; +// } +// +// void setEvent(Map? event) { +// _event = event; +// } +// +// void throwException(PlatformException exception) { +// _exceptionToThrow = exception; +// } +// +// void clearException() { +// _exceptionToThrow = null; +// } +// +// void setCreateEventCallback( +// Future Function( +// String calendarId, +// String title, +// DateTime startDate, +// DateTime endDate, +// bool isAllDay, +// String? description, +// String? location, +// String? timeZone, +// String availability, +// ) callback, +// ) { +// _createEventCallback = callback; +// } +// +// void setUpdateEventCallback( +// Future Function( +// String instanceId, { +// String? title, +// DateTime? startDate, +// DateTime? endDate, +// String? description, +// String? location, +// bool? isAllDay, +// String? timeZone, +// String? availability, +// }) callback, +// ) { +// _updateEventCallback = callback; +// } +// +// @override +// Future requestPermissions() async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// return _permissionStatusCode; +// } +// +// @override +// Future hasPermissions() async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// return _permissionStatusCode; +// } +// +// @override +// Future openAppSettings() async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// // Mock implementation - just returns successfully +// } +// +// @override +// Future>> listCalendars() async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// return _calendars; +// } +// +// @override +// Future createCalendar(String name, String? colorHex) async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// return 'mock-calendar-id-${DateTime.now().millisecondsSinceEpoch}'; +// } +// +// @override +// Future updateCalendar( +// String calendarId, String? name, String? colorHex) async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// } +// +// @override +// Future deleteCalendar(String calendarId) async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// } +// +// @override +// Future>> listEvents( +// DateTime startDate, +// DateTime endDate, +// List? calendarIds, +// ) async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// return _events; +// } +// +// @override +// Future?> getEvent(String instanceId) async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// return _event; +// } +// +// @override +// Future showEventModal(String instanceId) async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// // Mock implementation does nothing +// } +// +// @override +// Future createEvent( +// String calendarId, +// String title, +// DateTime startDate, +// DateTime endDate, +// bool isAllDay, +// String? description, +// String? location, +// String? timeZone, +// String availability, +// ) async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// +// // Use callback if set, otherwise return default +// if (_createEventCallback != null) { +// return _createEventCallback!( +// calendarId, +// title, +// startDate, +// endDate, +// isAllDay, +// description, +// location, +// timeZone, +// availability, +// ); +// } +// +// return 'mock-event-id-${DateTime.now().millisecondsSinceEpoch}'; +// } +// +// @override +// Future deleteEvent(String instanceId) async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// } +// +// @override +// Future updateEvent( +// String instanceId, { +// String? title, +// DateTime? startDate, +// DateTime? endDate, +// String? description, +// String? location, +// bool? isAllDay, +// String? timeZone, +// }) async { +// if (_exceptionToThrow != null) { +// throw _exceptionToThrow!; +// } +// if (_updateEventCallback != null) { +// return _updateEventCallback!( +// instanceId, +// title: title, +// startDate: startDate, +// endDate: endDate, +// description: description, +// location: location, +// isAllDay: isAllDay, +// timeZone: timeZone, +// ); +// } +// } +// } +// +// void main() { +// late MockDeviceCalendarPlusPlatform mockPlatform; +// +// setUp(() { +// mockPlatform = MockDeviceCalendarPlusPlatform(); +// DeviceCalendarPlusPlatform.instance = mockPlatform; +// }); +// +// group('DeviceCalendar', () { +// group('requestPermissions', () { +// group('status conversion', () { +// test('converts status code to CalendarPermissionStatus', () async { +// mockPlatform.setPermissionStatus(CalendarPermissionStatus.granted); +// final result = await DeviceCalendar.instance.requestPermissions(); +// expect(result, CalendarPermissionStatus.granted); +// }); +// }); +// +// group('edge case handling', () { +// test('defaults to denied when status is null', () async { +// mockPlatform._permissionStatusCode = null; +// final result = await DeviceCalendar.instance.requestPermissions(); +// expect(result, CalendarPermissionStatus.denied); +// }); +// }); +// +// group('error handling', () { +// test('throws DeviceCalendarException when permissions not declared', +// () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSIONS_NOT_DECLARED', +// message: 'Calendar permissions must be declared', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.requestPermissions(), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionsNotDeclared, +// ), +// ), +// ); +// }); +// +// test('rethrows other PlatformExceptions unchanged', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'SOME_OTHER_ERROR', +// message: 'Something went wrong', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.requestPermissions(), +// throwsA( +// isA().having( +// (e) => e.code, +// 'code', +// 'SOME_OTHER_ERROR', +// ), +// ), +// ); +// }); +// }); +// }); +// +// group('hasPermissions', () { +// group('status conversion', () { +// test('converts status code to CalendarPermissionStatus', () async { +// mockPlatform.setPermissionStatus(CalendarPermissionStatus.granted); +// final result = await DeviceCalendar.instance.hasPermissions(); +// expect(result, CalendarPermissionStatus.granted); +// }); +// }); +// +// group('edge case handling', () { +// test('defaults to denied when status is null', () async { +// mockPlatform._permissionStatusCode = null; +// final result = await DeviceCalendar.instance.hasPermissions(); +// expect(result, CalendarPermissionStatus.denied); +// }); +// }); +// +// group('error handling', () { +// test('throws DeviceCalendarException when permissions not declared', +// () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSIONS_NOT_DECLARED', +// message: 'Calendar permissions must be declared', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.hasPermissions(), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionsNotDeclared, +// ), +// ), +// ); +// }); +// +// test('rethrows other PlatformExceptions unchanged', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'SOME_OTHER_ERROR', +// message: 'Something went wrong', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.hasPermissions(), +// throwsA( +// isA().having( +// (e) => e.code, +// 'code', +// 'SOME_OTHER_ERROR', +// ), +// ), +// ); +// }); +// }); +// }); +// +// group('openAppSettings', () { +// test('completes successfully', () async { +// mockPlatform.clearException(); +// await DeviceCalendar.instance.openAppSettings(); +// // Should complete without error +// }); +// +// group('error handling', () { +// test('throws DeviceCalendarException when permissions not declared', +// () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSIONS_NOT_DECLARED', +// message: 'Calendar permissions must be declared', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.openAppSettings(), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionsNotDeclared, +// ), +// ), +// ); +// }); +// +// test('rethrows other PlatformExceptions unchanged', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'UNABLE_TO_OPEN_SETTINGS', +// message: 'Failed to open settings', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.openAppSettings(), +// throwsA( +// isA().having( +// (e) => e.code, +// 'code', +// 'UNABLE_TO_OPEN_SETTINGS', +// ), +// ), +// ); +// }); +// }); +// }); +// +// group('listCalendars', () { +// test('returns list of Calendar objects', () async { +// mockPlatform.setCalendars([ +// { +// 'id': '1', +// 'name': 'Work', +// 'colorHex': '#FF0000', +// 'readOnly': false, +// 'accountName': 'work@example.com', +// 'accountType': 'com.google', +// 'isPrimary': true, +// 'hidden': false, +// }, +// { +// 'id': '2', +// 'name': 'Personal', +// 'readOnly': true, +// 'isPrimary': false, +// 'hidden': false, +// }, +// ]); +// +// final calendars = await DeviceCalendar.instance.listCalendars(); +// +// expect(calendars, hasLength(2)); +// expect(calendars[0].id, '1'); +// expect(calendars[0].name, 'Work'); +// expect(calendars[0].colorHex, '#FF0000'); +// expect(calendars[0].readOnly, false); +// expect(calendars[0].isPrimary, true); +// expect(calendars[0].hidden, false); +// +// expect(calendars[1].id, '2'); +// expect(calendars[1].name, 'Personal'); +// expect(calendars[1].readOnly, true); +// expect(calendars[1].isPrimary, false); +// }); +// +// test('throws DeviceCalendarException when permission denied', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSION_DENIED', +// message: 'Calendar permission denied', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.listCalendars(), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionDenied, +// ), +// ), +// ); +// }); +// +// test('returns empty list when no calendars', () async { +// mockPlatform.setCalendars([]); +// final calendars = await DeviceCalendar.instance.listCalendars(); +// expect(calendars, isEmpty); +// }); +// }); +// +// group('createCalendar', () { +// test('returns calendar ID when created successfully', () async { +// final calendarId = +// await DeviceCalendar.instance.createCalendar(name: 'Test Calendar'); +// +// expect(calendarId, isNotEmpty); +// expect(calendarId, isA()); +// expect(calendarId, startsWith('mock-calendar-id')); +// }); +// +// test('creates calendar with name only', () async { +// final calendarId = +// await DeviceCalendar.instance.createCalendar(name: 'Work Calendar'); +// +// expect(calendarId, isNotEmpty); +// }); +// +// test('creates calendar with name and color', () async { +// final calendarId = await DeviceCalendar.instance.createCalendar( +// name: 'Personal Calendar', +// colorHex: '#FF5733', +// ); +// +// expect(calendarId, isNotEmpty); +// }); +// +// test('throws DeviceCalendarException when permission denied', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSION_DENIED', +// message: 'Calendar permission denied', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.createCalendar(name: 'Test Calendar'), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionDenied, +// ), +// ), +// ); +// }); +// +// test('rethrows other PlatformExceptions unchanged', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'SOME_OTHER_ERROR', +// message: 'Something went wrong', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.createCalendar(name: 'Test Calendar'), +// throwsA( +// isA().having( +// (e) => e.code, +// 'code', +// 'SOME_OTHER_ERROR', +// ), +// ), +// ); +// }); +// +// test('throws ArgumentError when name is empty', () async { +// expect( +// () => DeviceCalendar.instance.createCalendar(name: ''), +// throwsA( +// isA().having( +// (e) => e.message, +// 'message', +// contains('cannot be empty'), +// ), +// ), +// ); +// }); +// +// test('throws ArgumentError when name is whitespace only', () async { +// expect( +// () => DeviceCalendar.instance.createCalendar(name: ' '), +// throwsA( +// isA().having( +// (e) => e.message, +// 'message', +// contains('cannot be empty'), +// ), +// ), +// ); +// }); +// }); +// +// group('updateCalendar', () { +// test('updates calendar successfully', () async { +// await DeviceCalendar.instance.updateCalendar( +// 'calendar-123', +// name: 'Updated Name', +// colorHex: '#00FF00', +// ); +// // Should complete without error +// }); +// +// test('updates calendar with name only', () async { +// await DeviceCalendar.instance.updateCalendar( +// 'calendar-123', +// name: 'New Name', +// ); +// // Should complete without error +// }); +// +// test('updates calendar with color only', () async { +// await DeviceCalendar.instance.updateCalendar( +// 'calendar-123', +// colorHex: '#FF5733', +// ); +// // Should complete without error +// }); +// +// test('throws ArgumentError when no parameters provided', () async { +// expect( +// () => DeviceCalendar.instance.updateCalendar('calendar-123'), +// throwsA( +// isA().having( +// (e) => e.message, +// 'message', +// contains('At least one'), +// ), +// ), +// ); +// }); +// +// test('throws ArgumentError when name is empty', () async { +// expect( +// () => +// DeviceCalendar.instance.updateCalendar('calendar-123', name: ''), +// throwsA( +// isA().having( +// (e) => e.message, +// 'message', +// contains('cannot be empty'), +// ), +// ), +// ); +// }); +// +// test('throws ArgumentError when name is whitespace only', () async { +// expect( +// () => DeviceCalendar.instance +// .updateCalendar('calendar-123', name: ' '), +// throwsA( +// isA().having( +// (e) => e.message, +// 'message', +// contains('cannot be empty'), +// ), +// ), +// ); +// }); +// +// test('converts permissionDenied PlatformException', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSION_DENIED', message: 'Permission denied'), +// ); +// +// expect( +// () => DeviceCalendar.instance +// .updateCalendar('calendar-123', name: 'New Name'), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionDenied, +// ), +// ), +// ); +// +// mockPlatform.clearException(); +// }); +// +// test('rethrows unknown PlatformException', () async { +// mockPlatform.throwException( +// PlatformException(code: 'someOtherError', message: 'Some error'), +// ); +// +// expect( +// () => DeviceCalendar.instance +// .updateCalendar('calendar-123', name: 'New Name'), +// throwsA( +// isA().having( +// (e) => e.code, +// 'code', +// 'someOtherError', +// ), +// ), +// ); +// +// mockPlatform.clearException(); +// }); +// }); +// +// group('listEvents', () { +// test('returns list of Event objects', () async { +// final now = DateTime.now(); +// final later = now.add(Duration(hours: 2)); +// +// mockPlatform.setEvents([ +// { +// 'eventId': 'event1', +// 'instanceId': 'event1', +// 'calendarId': 'cal1', +// 'title': 'Team Meeting', +// 'description': 'Weekly sync', +// 'location': 'Conference Room A', +// 'startDate': now.millisecondsSinceEpoch, +// 'endDate': later.millisecondsSinceEpoch, +// 'isAllDay': false, +// 'availability': 'busy', +// 'status': 'confirmed', +// 'isRecurring': false, +// }, +// { +// 'eventId': 'event2', +// 'instanceId': 'event2', +// 'calendarId': 'cal1', +// 'title': 'All Day Event', +// 'startDate': now.millisecondsSinceEpoch, +// 'endDate': later.millisecondsSinceEpoch, +// 'isAllDay': true, +// 'availability': 'free', +// 'status': 'tentative', +// 'isRecurring': false, +// }, +// ]); +// +// final events = await DeviceCalendar.instance.listEvents( +// now, +// now.add(Duration(days: 7)), +// ); +// +// expect(events, hasLength(2)); +// expect(events[0].eventId, 'event1'); +// expect(events[0].title, 'Team Meeting'); +// expect(events[0].description, 'Weekly sync'); +// expect(events[0].location, 'Conference Room A'); +// expect(events[0].isAllDay, false); +// expect(events[0].availability, EventAvailability.busy); +// expect(events[0].status, EventStatus.confirmed); +// +// expect(events[1].eventId, 'event2'); +// expect(events[1].title, 'All Day Event'); +// expect(events[1].isAllDay, true); +// expect(events[1].availability, EventAvailability.free); +// expect(events[1].status, EventStatus.tentative); +// }); +// +// test('handles unknown availability and status gracefully', () async { +// final now = DateTime.now(); +// +// mockPlatform.setEvents([ +// { +// 'eventId': 'event1', +// 'instanceId': 'event1', +// 'calendarId': 'cal1', +// 'title': 'Test Event', +// 'startDate': now.millisecondsSinceEpoch, +// 'endDate': now.millisecondsSinceEpoch, +// 'isAllDay': false, +// 'availability': 'unknownValue', +// 'status': 'unknownStatus', +// 'isRecurring': false, +// }, +// ]); +// +// final events = await DeviceCalendar.instance.listEvents( +// now, +// now.add(Duration(days: 1)), +// ); +// +// expect(events, hasLength(1)); +// expect(events[0].availability, EventAvailability.notSupported); +// expect(events[0].status, EventStatus.none); +// }); +// +// test('returns empty list when no events', () async { +// mockPlatform.setEvents([]); +// final events = await DeviceCalendar.instance.listEvents( +// DateTime.now(), +// DateTime.now().add(Duration(days: 7)), +// ); +// expect(events, isEmpty); +// }); +// +// test('throws DeviceCalendarException when permission denied', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSION_DENIED', +// message: 'Calendar permission denied', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.listEvents( +// DateTime.now(), +// DateTime.now().add(Duration(days: 7)), +// ), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionDenied, +// ), +// ), +// ); +// }); +// }); +// +// group('getEvent', () { +// test('returns non-recurring event when found by instanceId', () async { +// final now = DateTime.now(); +// +// mockPlatform.setEvent({ +// 'eventId': 'event1', +// 'instanceId': 'event1', +// 'calendarId': 'cal1', +// 'title': 'Team Meeting', +// 'description': 'Weekly sync', +// 'startDate': now.millisecondsSinceEpoch, +// 'endDate': now.add(Duration(hours: 1)).millisecondsSinceEpoch, +// 'isAllDay': false, +// 'availability': 'busy', +// 'status': 'confirmed', +// 'isRecurring': false, +// }); +// +// final event = await DeviceCalendar.instance.getEvent('event1'); +// +// expect(event, isNotNull); +// expect(event!.eventId, 'event1'); +// expect( +// event.instanceId, 'event1'); // Non-recurring: instanceId == eventId +// expect(event.title, 'Team Meeting'); +// expect(event.description, 'Weekly sync'); +// }); +// +// test('returns recurring event instance by instanceId', () async { +// final eventStart = DateTime(2025, 11, 15, 14, 0); +// final instanceId = 'recurring1@${eventStart.millisecondsSinceEpoch}'; +// +// mockPlatform.setEvent({ +// 'eventId': 'recurring1', +// 'instanceId': instanceId, +// 'calendarId': 'cal1', +// 'title': 'Daily Standup', +// 'startDate': eventStart.millisecondsSinceEpoch, +// 'endDate': +// eventStart.add(Duration(minutes: 30)).millisecondsSinceEpoch, +// 'isAllDay': false, +// 'availability': 'busy', +// 'status': 'confirmed', +// 'isRecurring': true, +// }); +// +// final event = await DeviceCalendar.instance.getEvent(instanceId); +// +// expect(event, isNotNull); +// expect(event!.eventId, 'recurring1'); +// expect(event.instanceId, instanceId); +// expect(event.title, 'Daily Standup'); +// expect(event.startDate, eventStart); +// }); +// +// test('returns null when event not found', () async { +// mockPlatform.setEvent(null); +// +// final event = await DeviceCalendar.instance.getEvent('nonexistent'); +// +// expect(event, isNull); +// }); +// +// test('parses instanceId correctly for recurring events', () async { +// final eventStart = DateTime(2025, 11, 15, 14, 0); +// final instanceId = 'event123@${eventStart.millisecondsSinceEpoch}'; +// +// mockPlatform.setEvent({ +// 'eventId': 'event123', +// 'instanceId': instanceId, +// 'calendarId': 'cal1', +// 'title': 'Recurring Event', +// 'startDate': eventStart.millisecondsSinceEpoch, +// 'endDate': eventStart.add(Duration(hours: 1)).millisecondsSinceEpoch, +// 'isAllDay': false, +// 'availability': 'busy', +// 'status': 'confirmed', +// 'isRecurring': true, +// }); +// +// final event = await DeviceCalendar.instance.getEvent(instanceId); +// +// expect(event, isNotNull); +// expect(event!.eventId, 'event123'); +// expect(event.startDate, eventStart); +// }); +// +// test('throws DeviceCalendarException when permission denied', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSION_DENIED', +// message: 'Calendar permission denied', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.getEvent('event1'), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionDenied, +// ), +// ), +// ); +// }); +// }); +// +// group('createEvent', () { +// test('creates event with all parameters', () async { +// final calendarId = 'cal-123'; +// final title = 'Team Meeting'; +// final startDate = DateTime(2024, 3, 15, 14, 0); +// final endDate = DateTime(2024, 3, 15, 15, 0); +// +// final eventId = await DeviceCalendar.instance.createEvent( +// calendarId: calendarId, +// title: title, +// startDate: startDate, +// endDate: endDate, +// description: 'Weekly sync', +// location: 'Conference Room A', +// timeZone: 'America/New_York', +// ); +// +// expect(eventId, isNotEmpty); +// expect(eventId, startsWith('mock-event-id-')); +// }); +// +// test('creates all-day event', () async { +// final eventId = await DeviceCalendar.instance.createEvent( +// calendarId: 'cal-123', +// title: 'All Day Event', +// startDate: DateTime(2024, 3, 15), +// endDate: DateTime(2024, 3, 16), +// isAllDay: true, +// ); +// +// expect(eventId, isNotEmpty); +// }); +// +// test('normalizes dates for all-day events (strips time components)', +// () async { +// // Create an all-day event with time components +// final startWithTime = DateTime(2024, 3, 15, 14, 30, 45); +// final endWithTime = DateTime(2024, 3, 16, 18, 15, 30); +// +// // Mock to capture what was actually passed to the platform +// DateTime? capturedStart; +// DateTime? capturedEnd; +// +// final mock = MockDeviceCalendarPlusPlatform(); +// mock.setCreateEventCallback(( +// calendarId, +// title, +// startDate, +// endDate, +// isAllDay, +// description, +// location, +// timeZone, +// availability, +// ) { +// capturedStart = startDate; +// capturedEnd = endDate; +// return Future.value('event-id'); +// }); +// +// DeviceCalendarPlusPlatform.instance = mock; +// +// await DeviceCalendar.instance.createEvent( +// calendarId: 'cal-123', +// title: 'All Day Event', +// startDate: startWithTime, +// endDate: endWithTime, +// isAllDay: true, +// ); +// +// // Verify dates were normalized to midnight +// expect(capturedStart, isNotNull); +// expect(capturedEnd, isNotNull); +// expect(capturedStart!.hour, 0); +// expect(capturedStart!.minute, 0); +// expect(capturedStart!.second, 0); +// expect(capturedStart!.millisecond, 0); +// expect(capturedEnd!.hour, 0); +// expect(capturedEnd!.minute, 0); +// expect(capturedEnd!.second, 0); +// expect(capturedEnd!.millisecond, 0); +// +// // Verify dates preserved the day +// expect(capturedStart!.year, 2024); +// expect(capturedStart!.month, 3); +// expect(capturedStart!.day, 15); +// expect(capturedEnd!.year, 2024); +// expect(capturedEnd!.month, 3); +// expect(capturedEnd!.day, 16); +// }); +// +// test('preserves exact time for non-all-day events', () async { +// final startWithTime = DateTime(2024, 3, 15, 14, 30, 45); +// final endWithTime = DateTime(2024, 3, 15, 18, 15, 30); +// +// DateTime? capturedStart; +// DateTime? capturedEnd; +// +// final mock = MockDeviceCalendarPlusPlatform(); +// mock.setCreateEventCallback(( +// calendarId, +// title, +// startDate, +// endDate, +// isAllDay, +// description, +// location, +// timeZone, +// availability, +// ) { +// capturedStart = startDate; +// capturedEnd = endDate; +// return Future.value('event-id'); +// }); +// +// DeviceCalendarPlusPlatform.instance = mock; +// +// await DeviceCalendar.instance.createEvent( +// calendarId: 'cal-123', +// title: 'Meeting', +// startDate: startWithTime, +// endDate: endWithTime, +// isAllDay: false, +// ); +// +// // Verify exact times were preserved +// expect(capturedStart, equals(startWithTime)); +// expect(capturedEnd, equals(endWithTime)); +// }); +// +// test('creates event with minimal parameters', () async { +// final eventId = await DeviceCalendar.instance.createEvent( +// calendarId: 'cal-123', +// title: 'Quick Meeting', +// startDate: DateTime.now(), +// endDate: DateTime.now().add(Duration(hours: 1)), +// ); +// +// expect(eventId, isNotEmpty); +// }); +// +// test('throws ArgumentError when calendar ID is empty', () async { +// expect( +// () => DeviceCalendar.instance.createEvent( +// calendarId: '', +// title: 'Meeting', +// startDate: DateTime.now(), +// endDate: DateTime.now().add(Duration(hours: 1)), +// ), +// throwsArgumentError, +// ); +// }); +// +// test('throws ArgumentError when title is empty', () async { +// expect( +// () => DeviceCalendar.instance.createEvent( +// calendarId: 'cal-123', +// title: '', +// startDate: DateTime.now(), +// endDate: DateTime.now().add(Duration(hours: 1)), +// ), +// throwsArgumentError, +// ); +// }); +// +// test('throws ArgumentError when end date is before start date', () async { +// final now = DateTime.now(); +// expect( +// () => DeviceCalendar.instance.createEvent( +// calendarId: 'cal-123', +// title: 'Invalid Event', +// startDate: now, +// endDate: now.subtract(Duration(hours: 1)), +// ), +// throwsArgumentError, +// ); +// }); +// +// test('converts PlatformException to DeviceCalendarException', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSION_DENIED', +// message: 'Calendar permission denied', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.createEvent( +// calendarId: 'cal-123', +// title: 'Meeting', +// startDate: DateTime.now(), +// endDate: DateTime.now().add(Duration(hours: 1)), +// ), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionDenied, +// ), +// ), +// ); +// }); +// }); +// +// group('deleteEvent', () { +// test('deletes single event', () async { +// await DeviceCalendar.instance.deleteEvent(eventId: 'event-123'); +// // Should complete without error +// }); +// +// test('deletes all instances of recurring event', () async { +// await DeviceCalendar.instance.deleteEvent( +// eventId: 'event-123@123456789', +// ); +// // Should complete without error +// }); +// +// test('deletes single instance of recurring event', () async { +// await DeviceCalendar.instance.deleteEvent( +// eventId: 'event-123@123456789', +// ); +// // Should complete without error +// }); +// +// test('throws ArgumentError when instance ID is empty', () async { +// expect( +// () => DeviceCalendar.instance.deleteEvent(eventId: ''), +// throwsArgumentError, +// ); +// }); +// +// test('converts PlatformException to DeviceCalendarException', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSION_DENIED', +// message: 'Calendar permission denied', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.deleteEvent(eventId: 'event-123'), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionDenied, +// ), +// ), +// ); +// }); +// }); +// +// group('updateEvent', () { +// test('updates event with all parameters', () async { +// await DeviceCalendar.instance.updateEvent( +// eventId: 'event-123', +// title: 'Updated Title', +// startDate: DateTime(2024, 3, 20, 10, 0), +// endDate: DateTime(2024, 3, 20, 11, 0), +// description: 'Updated description', +// location: 'Updated location', +// isAllDay: false, +// timeZone: 'America/New_York', +// ); +// // Should complete without error +// }); +// +// test('updates event with single field', () async { +// await DeviceCalendar.instance.updateEvent( +// eventId: 'event-123', +// title: 'New Title', +// ); +// // Should complete without error +// }); +// +// test('updates entire series for recurring event', () async { +// await DeviceCalendar.instance.updateEvent( +// eventId: 'event-123', +// title: 'Updated Series', +// ); +// // Should complete without error (updates entire series automatically) +// }); +// +// test('normalizes dates when isAllDay is true', () async { +// final startWithTime = DateTime(2024, 3, 15, 14, 30, 45); +// final endWithTime = DateTime(2024, 3, 16, 18, 15, 30); +// +// DateTime? capturedStart; +// DateTime? capturedEnd; +// +// final mock = MockDeviceCalendarPlusPlatform(); +// mock.setUpdateEventCallback(( +// instanceId, { +// title, +// startDate, +// endDate, +// description, +// location, +// isAllDay, +// timeZone, +// availability, +// }) { +// capturedStart = startDate; +// capturedEnd = endDate; +// return Future.value(); +// }); +// DeviceCalendarPlusPlatform.instance = mock; +// +// await DeviceCalendar.instance.updateEvent( +// eventId: 'event-123', +// startDate: startWithTime, +// endDate: endWithTime, +// isAllDay: true, +// ); +// +// expect(capturedStart, isNotNull); +// expect(capturedEnd, isNotNull); +// expect(capturedStart!.hour, 0); +// expect(capturedStart!.minute, 0); +// expect(capturedStart!.second, 0); +// expect(capturedStart!.millisecond, 0); +// expect(capturedEnd!.hour, 0); +// expect(capturedEnd!.minute, 0); +// expect(capturedEnd!.second, 0); +// expect(capturedEnd!.millisecond, 0); +// +// expect(capturedStart!.year, 2024); +// expect(capturedStart!.month, 3); +// expect(capturedStart!.day, 15); +// expect(capturedEnd!.year, 2024); +// expect(capturedEnd!.month, 3); +// expect(capturedEnd!.day, 16); +// }); +// +// test('preserves exact time when isAllDay is false', () async { +// final startWithTime = DateTime(2024, 3, 15, 14, 30, 45); +// final endWithTime = DateTime(2024, 3, 15, 18, 15, 30); +// +// DateTime? capturedStart; +// DateTime? capturedEnd; +// +// final mock = MockDeviceCalendarPlusPlatform(); +// mock.setUpdateEventCallback(( +// instanceId, { +// title, +// startDate, +// endDate, +// description, +// location, +// isAllDay, +// timeZone, +// availability, +// }) { +// capturedStart = startDate; +// capturedEnd = endDate; +// return Future.value(); +// }); +// DeviceCalendarPlusPlatform.instance = mock; +// +// await DeviceCalendar.instance.updateEvent( +// eventId: 'event-123', +// startDate: startWithTime, +// endDate: endWithTime, +// isAllDay: false, +// ); +// +// expect(capturedStart, equals(startWithTime)); +// expect(capturedEnd, equals(endWithTime)); +// }); +// +// test('throws ArgumentError when eventId is empty', () async { +// expect( +// () => DeviceCalendar.instance.updateEvent( +// eventId: '', +// title: 'New Title', +// ), +// throwsArgumentError, +// ); +// }); +// +// test('throws ArgumentError when no fields provided', () async { +// expect( +// () => DeviceCalendar.instance.updateEvent( +// eventId: 'event-123', +// ), +// throwsArgumentError, +// ); +// }); +// +// test('throws ArgumentError when endDate is before startDate', () async { +// expect( +// () => DeviceCalendar.instance.updateEvent( +// eventId: 'event-123', +// startDate: DateTime(2024, 3, 20, 11, 0), +// endDate: DateTime(2024, 3, 20, 10, 0), +// ), +// throwsArgumentError, +// ); +// }); +// +// test('converts PlatformException to DeviceCalendarException', () async { +// mockPlatform.throwException( +// PlatformException( +// code: 'PERMISSION_DENIED', +// message: 'Calendar permission denied', +// ), +// ); +// +// expect( +// () => DeviceCalendar.instance.updateEvent( +// eventId: 'event-123', +// title: 'New Title', +// ), +// throwsA( +// isA().having( +// (e) => e.errorCode, +// 'errorCode', +// DeviceCalendarError.permissionDenied, +// ), +// ), +// ); +// }); +// }); +// }); +// } diff --git a/pubspec.yaml b/pubspec.yaml index 45ea9442..d561347c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: hmg_patient_app_new description: "New HMG Patient App" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 0.0.1+3 +version: 0.0.11+8 environment: sdk: ">=3.6.0 <4.0.0" @@ -46,7 +46,8 @@ dependencies: file_picker: 10.3.8 local_auth: ^2.3.0 share_plus: ^11.1.0 - device_calendar_plus: ^0.3.1 + device_calendar_plus: + path: ./package/device_calendar_plus device_calendar: git: https://github.com/bardram/device_calendar manage_calendar_events: ^2.0.3 @@ -93,10 +94,7 @@ dependencies: huawei_health: ^6.15.0+300 intl: ^0.20.2 flutter_widget_from_html: ^0.17.1 - huawei_map: - git: - url: https://github.com/fleoparra/hms-flutter-plugin.git - path: flutter-hms-map + huawei_map: ^6.12.0+301 scrollable_positioned_list: ^0.3.8