From 234dce65e58b139a6d8a7951696922eb3dc8deff Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Tue, 30 Dec 2025 16:43:56 +0300 Subject: [PATCH 1/8] chat bot & Speech to Text --- android/app/src/main/AndroidManifest.xml | 11 + assets/icons/chatbot.svg | 46 +++ assets/icons/microphone.svg | 12 + assets/icons/send.svg | 3 + ios/Runner/Info.plist | 4 +- lib/api/dashboard_api_client.dart | 28 ++ lib/classes/colors.dart | 1 + lib/classes/consts.dart | 4 +- lib/config/routes.dart | 10 +- lib/models/generic_response_model.dart | 153 +++++++- lib/ui/chatbot/chatbot_screen.dart | 334 +++++++++++++++++ lib/ui/landing/dashboard_screen.dart | 456 ++++++++++++----------- pubspec.yaml | 2 + 13 files changed, 830 insertions(+), 234 deletions(-) create mode 100644 assets/icons/chatbot.svg create mode 100644 assets/icons/microphone.svg create mode 100644 assets/icons/send.svg create mode 100644 lib/ui/chatbot/chatbot_screen.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6d2f83f6..1cf76f65 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,17 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/microphone.svg b/assets/icons/microphone.svg new file mode 100644 index 00000000..0cbd4aaf --- /dev/null +++ b/assets/icons/microphone.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/send.svg b/assets/icons/send.svg new file mode 100644 index 00000000..37f97e2b --- /dev/null +++ b/assets/icons/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 01197de4..5288a289 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -56,7 +56,9 @@ NSLocationWhenInUseUsageDescription This App requires access to your location to mark your attendance. NSMicrophoneUsageDescription - This app requires microphone access to for call. + This app requires microphone access for calls and speech-to-text input. + NSSpeechRecognitionUsageDescription + This app requires speech recognition to convert your voice to text. NSPhotoLibraryUsageDescription This app requires photo library access to select image as document & upload it. UIApplicationSupportsIndirectInputEvents diff --git a/lib/api/dashboard_api_client.dart b/lib/api/dashboard_api_client.dart index 2bddb42e..fa4d00a2 100644 --- a/lib/api/dashboard_api_client.dart +++ b/lib/api/dashboard_api_client.dart @@ -414,4 +414,32 @@ class DashboardApiClient { // postParams, // ); // } + + Future getChatBotSession() async { + String url = "${ApiConsts.erpRest}Atlas_InitiateChat"; + Map postParams = {}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + return responseData; + }, + url, + postParams, + ); + } + + Future sendChatBotMessage({required AtlasInitiateChatResponse atlasChatTokenResponse, required String atlasChatText}) async { + String url = "${ApiConsts.erpRest}Atlas_ContinueChat"; + Map postParams = {"Atlas_AccessToken": atlasChatTokenResponse.atlasAccessToken, "Atlas_ChatSessionID": atlasChatTokenResponse.atlasChatSessionId, "Atlas_ChatText": atlasChatText}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + return responseData; + }, + url, + postParams, + ); + } } diff --git a/lib/classes/colors.dart b/lib/classes/colors.dart index 1b0dd069..4c88da98 100644 --- a/lib/classes/colors.dart +++ b/lib/classes/colors.dart @@ -67,4 +67,5 @@ class MyColors { static const Color lightGreyIconColor = Color(0xff919191); static const Color selectedBorderColor = Color(0xff37A4BE); static const Color mazayaRedColor = Color(0xffED1C2B); + static const Color hintTextColor = Color(0xff9AA4B2); } diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index dbec87d9..29d22d73 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -9,9 +9,9 @@ class ApiConsts { // static String baseUrl = "https://webservices.hmg.com"; // PreProd // static String baseUrl = "https://hmgwebservices.com"; // Live server - static String baseUrl = "https://mohemm.hmg.com"; // New Live server + // static String baseUrl = "https://mohemm.hmg.com"; // New Live server // - // static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver + static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver // static String baseUrl = "http://10.20.200.111:1010/"; // static String baseUrl = "https://webservices.hmg.com"; // PreProd diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 46538801..49913b5d 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -10,6 +10,7 @@ import 'package:mohem_flutter_app/ui/chat/favorite_users_screen.dart'; import 'package:mohem_flutter_app/ui/chat/group_chat_detaied_screen.dart'; import 'package:mohem_flutter_app/ui/chat/group_members.dart'; import 'package:mohem_flutter_app/ui/chat/manage_group.dart'; +import 'package:mohem_flutter_app/ui/chatbot/chatbot_screen.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; import 'package:mohem_flutter_app/ui/landing/itg/change_itg_ad_password_screen.dart'; import 'package:mohem_flutter_app/ui/landing/itg/its_add_screen_video_image.dart'; @@ -32,6 +33,7 @@ import 'package:mohem_flutter_app/ui/misc/request_submit_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_listview_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/services_menu_list_screen.dart'; + // import 'package:mohem_flutter_app/ui/my_attendance/my_attendance_screen.dart'; import 'package:mohem_flutter_app/ui/my_team/create_request.dart'; import 'package:mohem_flutter_app/ui/my_team/employee_details.dart'; @@ -54,6 +56,7 @@ import 'package:mohem_flutter_app/ui/profile/profile_screen.dart'; import 'package:mohem_flutter_app/ui/screens/announcements/announcement_details.dart'; import 'package:mohem_flutter_app/ui/screens/announcements/announcements.dart'; import 'package:mohem_flutter_app/ui/screens/child_education/child_education_assistance.dart'; + // import 'package:mohem_flutter_app/ui/my_attendance/work_from_home_screen.dart'; import 'package:mohem_flutter_app/ui/screens/eit/add_eit.dart'; import 'package:mohem_flutter_app/ui/screens/event_activity/event_activity.dart'; @@ -191,6 +194,7 @@ class AppRoutes { static const String chat = "/chat"; static const String chatDetailed = "/chatDetailed"; static const String chatFavoriteUsers = "/chatFavoriteUsers"; + static const String chatBotHome = "/chatBotHome"; //Group Chat static const String manageGroup = "/manageGroup"; @@ -223,8 +227,8 @@ class AppRoutes { newPassword: (BuildContext context) => NewPasswordScreen(), forgotPassword: (BuildContext context) => ForgotPasswordScreen(), todayAttendance: (BuildContext context) => TodayAttendanceScreen2(), - //eit + //eit addEitScreen: (BuildContext context) => AddEITScreen(), //Work List @@ -235,7 +239,6 @@ class AppRoutes { worklistSettings: (BuildContext context) => WorklistSettings(), // Leave Balance - leaveBalance: (BuildContext context) => LeaveBalance(), addLeaveBalance: (BuildContext context) => AddLeaveBalanceScreen(), @@ -312,6 +315,7 @@ class AppRoutes { chat: (BuildContext context) => ChatHome(), chatDetailed: (BuildContext context) => ChatDetailScreen(), chatFavoriteUsers: (BuildContext context) => ChatFavoriteUsersScreen(), + chatBotHome: (BuildContext context) => ChatBotScreen(), //Group Chat manageGroup: (BuildContext context) => ManageGroupScreen(), @@ -328,6 +332,6 @@ class AppRoutes { unsafeDeviceScreen: (BuildContext context) => const UnsafeDeviceScreen(), appUpdateScreen: (BuildContext context) => const AppUpdateScreen(), childEducation: (BuildContext context) => ChildEducationAssistance(), - activityScreen:(BuildContext context) => const EventActivityScreen() + activityScreen: (BuildContext context) => const EventActivityScreen(), }; } diff --git a/lib/models/generic_response_model.dart b/lib/models/generic_response_model.dart index df94c8b7..337572cf 100644 --- a/lib/models/generic_response_model.dart +++ b/lib/models/generic_response_model.dart @@ -398,6 +398,9 @@ class GenericResponseModel { bool? isActiveCode; bool? isSMSSent; PortalDirectionData? portalDirectionData; + AtlasInitiateChatResponse? atlasInitiateChatResponse; + AtlasContinueChatResponseDetails? atlasContinueChatResponseDetails; + GenericResponseModel({ this.date, @@ -669,7 +672,10 @@ class GenericResponseModel { this.ePharmacyGetItemOnHandList, this.isActiveCode, this.isSMSSent, - this.getFADisposalNtfDetails + this.getFADisposalNtfDetails, + this.atlasInitiateChatResponse, + this.atlasContinueChatResponseDetails, + }); GenericResponseModel.fromJson(Map json) { @@ -943,7 +949,6 @@ class GenericResponseModel { getFADisposalNtfDetails = json['GetFADisposalNtfDetails'] != null ? GetFaDisposalNtfDetails.fromJson(json['GetFADisposalNtfDetails']) : null; - if (json['GetEarningsList'] != null) { getEarningsList = []; json['GetEarningsList'].forEach((v) { @@ -1464,6 +1469,10 @@ class GenericResponseModel { ePharmacyGetItemOnHandList = json['ePharmacy_GetItemOnHandList']; isActiveCode = json['isActiveCode']; isSMSSent = json['isSMSSent']; + atlasInitiateChatResponse = json["Atlas_InitiateChat_Response"] == null ? null : AtlasInitiateChatResponse.fromJson(json["Atlas_InitiateChat_Response"]); + atlasContinueChatResponseDetails = json["Atlas_ContinueChat_ResponseDetails"] == null ? null : AtlasContinueChatResponseDetails.fromJson(json["Atlas_ContinueChat_ResponseDetails"]); + + } Map toJson() { @@ -1561,7 +1570,7 @@ class GenericResponseModel { if (this.getActionHistoryList != null) { data['GetActionHistoryList'] = this.getActionHistoryList!.map((v) => v.toJson()).toList(); } - data['GetFADisposalNtfDetails'] =this.getFADisposalNtfDetails; + data['GetFADisposalNtfDetails'] = this.getFADisposalNtfDetails; data['GetAddressDffStructureList'] = this.getAddressDffStructureList; data['GetAddressNotificationBodyList'] = this.getAddressNotificationBodyList; @@ -1949,6 +1958,9 @@ class GenericResponseModel { data['ePharmacy_GetItemOnHandList'] = this.ePharmacyGetItemOnHandList; data['isActiveCode'] = this.isActiveCode; data['isSMSSent'] = this.isSMSSent; + data["Atlas_InitiateChat_Response"] = atlasInitiateChatResponse?.toJson(); + data["Atlas_ContinueChat_ResponseDetails"] = atlasContinueChatResponseDetails?.toJson(); + return data; } } @@ -1974,3 +1986,138 @@ class TicketBookingResult { TicketBookingResult(this.success, this.clientId); } + +class AtlasInitiateChatResponse { + String? atlasAccessToken; + String? atlasChatSessionId; + String? atlasTokenType; + + AtlasInitiateChatResponse({this.atlasAccessToken, this.atlasChatSessionId, this.atlasTokenType}); + + factory AtlasInitiateChatResponse.fromRawJson(String str) => AtlasInitiateChatResponse.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory AtlasInitiateChatResponse.fromJson(Map json) => + AtlasInitiateChatResponse(atlasAccessToken: json["Atlas_AccessToken"], atlasChatSessionId: json["Atlas_ChatSessionID"], atlasTokenType: json["Atlas_TokenType"]); + + Map toJson() => {"Atlas_AccessToken": atlasAccessToken, "Atlas_ChatSessionID": atlasChatSessionId, "Atlas_TokenType": atlasTokenType}; +} + + + +class AtlasContinueChatResponseDetails { + AiResponse? aiResponse; + QueryClassification? queryClassification; + String? sessionId; + AiResponse? userMessage; + + AtlasContinueChatResponseDetails({ + this.aiResponse, + this.queryClassification, + this.sessionId, + this.userMessage, + }); + + factory AtlasContinueChatResponseDetails.fromRawJson(String str) => AtlasContinueChatResponseDetails.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory AtlasContinueChatResponseDetails.fromJson(Map json) => AtlasContinueChatResponseDetails( + aiResponse: json["ai_response"] == null ? null : AiResponse.fromJson(json["ai_response"]), + queryClassification: json["query_classification"] == null ? null : QueryClassification.fromJson(json["query_classification"]), + sessionId: json["session_id"], + userMessage: json["user_message"] == null ? null : AiResponse.fromJson(json["user_message"]), + ); + + Map toJson() => { + "ai_response": aiResponse?.toJson(), + "query_classification": queryClassification?.toJson(), + "session_id": sessionId, + "user_message": userMessage?.toJson(), + }; +} + +class AiResponse { + DateTime? createdAt; + String? id; + StructuredData? structuredData; + String? text; + + AiResponse({ + this.createdAt, + this.id, + this.structuredData, + this.text, + }); + + factory AiResponse.fromRawJson(String str) => AiResponse.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory AiResponse.fromJson(Map json) => AiResponse( + createdAt: json["created_at"] == null ? null : DateTime.parse(json["created_at"]), + id: json["id"], + structuredData: json["structured_data"] == null ? null : StructuredData.fromJson(json["structured_data"]), + text: json["text"], + ); + + Map toJson() => { + "created_at": createdAt?.toIso8601String(), + "id": id, + "structured_data": structuredData?.toJson(), + "text": text, + }; +} + +class StructuredData { + String? answer; + List? sources; + String? type; + + StructuredData({ + this.answer, + this.sources, + this.type, + }); + + factory StructuredData.fromRawJson(String str) => StructuredData.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory StructuredData.fromJson(Map json) => StructuredData( + answer: json["answer"], + sources: json["sources"] == null ? [] : List.from(json["sources"]!.map((x) => x)), + type: json["type"], + ); + + Map toJson() => { + "answer": answer, + "sources": sources == null ? [] : List.from(sources!.map((x) => x)), + "type": type, + }; +} + +class QueryClassification { + int? confidence; + String? type; + + QueryClassification({ + this.confidence, + this.type, + }); + + factory QueryClassification.fromRawJson(String str) => QueryClassification.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory QueryClassification.fromJson(Map json) => QueryClassification( + confidence: json["confidence"], + type: json["type"], + ); + + Map toJson() => { + "confidence": confidence, + "type": type, + }; +} \ No newline at end of file diff --git a/lib/ui/chatbot/chatbot_screen.dart b/lib/ui/chatbot/chatbot_screen.dart new file mode 100644 index 00000000..5b5989df --- /dev/null +++ b/lib/ui/chatbot/chatbot_screen.dart @@ -0,0 +1,334 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:mohem_flutter_app/api/dashboard_api_client.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; +import 'package:speech_to_text/speech_recognition_result.dart'; +import 'package:speech_to_text/speech_to_text.dart'; + +enum ChatMode { home, chatActive } + +class ChatMessage { + final String text; + final bool isUser; + final DateTime timestamp; + + ChatMessage({required this.text, required this.isUser, required this.timestamp}); +} + +class ChatBotScreen extends StatefulWidget { + const ChatBotScreen({Key? key}) : super(key: key); + + @override + State createState() => _ChatBotScreenState(); +} + +class _ChatBotScreenState extends State { + ChatMode _currentMode = ChatMode.home; + final List _messages = []; + final TextEditingController _messageController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + AtlasInitiateChatResponse? atlasChatTokenResponse; + + // Speech to text + final SpeechToText _speechToText = SpeechToText(); + bool _speechEnabled = false; + + @override + void initState() { + // TODO: implement initState + super.initState(); + print("Chat Bot Launched"); + getChatSession(); + _initSpeech(); + } + + /// Initialize speech to text + void _initSpeech() async { + _speechEnabled = await _speechToText.initialize(); + setState(() {}); + } + + /// Start listening for speech + void _startListening() async { + await _speechToText.listen(onResult: _onSpeechResult); + setState(() {}); + } + + /// Stop listening for speech + void _stopListening() async { + await _speechToText.stop(); + setState(() {}); + } + + /// Callback when speech is recognized + void _onSpeechResult(SpeechRecognitionResult result) { + setState(() { + _messageController.text = result.recognizedWords; + // Move cursor to end of text + _messageController.selection = TextSelection.fromPosition(TextPosition(offset: _messageController.text.length)); + }); + } + + void getChatSession() async { + try { + GenericResponseModel? res = await DashboardApiClient().getChatBotSession(); + if (res != null) { + atlasChatTokenResponse = res.atlasInitiateChatResponse; + } + } catch (ex) { + Utils.handleException(ex, context, null); + } + } + + @override + void dispose() { + atlasChatTokenResponse = null; + + _messageController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _sendMessage() async { + String text = _messageController.text.trim(); + if (text.isEmpty || atlasChatTokenResponse == null) return; + + // setState(() { + // _messages.add(ChatMessage(text: text, isUser: true, timestamp: DateTime.now())); + // _currentMode = ChatMode.chatActive; + // _messageController.clear(); + // }); + // + // // Simulate bot response + // Future.delayed(const Duration(milliseconds: 500), () { + // if (mounted) { + // setState(() { + // _messages.add(ChatMessage(text: "Hello there! How may I assist you today?", isUser: false, timestamp: DateTime.now())); + // }); + // _scrollToBottom(); + // } + // }); + + try { + setState(() { + _messages.add(ChatMessage(text: text, isUser: true, timestamp: DateTime.now())); + _currentMode = ChatMode.chatActive; + _messageController.clear(); + }); + GenericResponseModel? res = await DashboardApiClient().sendChatBotMessage(atlasChatTokenResponse: atlasChatTokenResponse!, atlasChatText: text); + if (res != null) { + res.atlasContinueChatResponseDetails; + if (mounted) { + setState(() { + _messages.add( + ChatMessage( + text: res.atlasContinueChatResponseDetails!.aiResponse!.structuredData!.answer ?? "", + isUser: false, + timestamp: res.atlasContinueChatResponseDetails!.aiResponse!.createdAt ?? DateTime.now(), + ), + ); + }); + } + _scrollToBottom(); + } + } catch (ex) { + Utils.handleException(ex, context, null); + } + + _scrollToBottom(); + } + + void _scrollToBottom() { + Future.delayed(const Duration(milliseconds: 100), () { + if (_scrollController.hasClients) { + _scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); + } + }); + } + + void _sendSuggestion(String text) { + _messageController.text = text; + _sendMessage(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: MyColors.backgroundColor, + appBar: AppBar( + title: const Text("ChatBot", style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2B353E))), + backgroundColor: MyColors.backgroundColor, + centerTitle: false, + ), + body: Column(children: [Expanded(child: _currentMode == ChatMode.home ? _buildHomeMode() : _buildChatMode()), _buildBottomInputBar()]), + ); + } + + Widget _buildHomeMode() { + return Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Hello, ask me", textAlign: TextAlign.center, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))), + const Text("anything...", textAlign: TextAlign.center, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))), + const SizedBox(height: 50), + Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 12, + children: [ + _buildSuggestionChip("Nostalgia Perfume", () => _sendSuggestion("Nostalgia Perfume")), + _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")), + _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")), + _buildSuggestionChip("Al Jadi", () => _sendSuggestion("Al Jadi")), + ], + ), + ], + ), + ), + ); + } + + Widget _buildChatMode() { + return ListView.separated( + controller: _scrollController, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + itemCount: _messages.length, + itemBuilder: (BuildContext context, int index) { + ChatMessage message = _messages[index]; + return _buildMessageBubble(message); + }, + separatorBuilder: (BuildContext context, int index) { + return const SizedBox(height: 15); + }, + ); + } + + Widget _buildMessageBubble(ChatMessage message) { + bool isUser = message.isUser; + return Row( + mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isUser) const Spacer(flex: 2), + Flexible( + flex: 7, + child: Column( + crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isUser ? Colors.white : null, + gradient: isUser ? null : const LinearGradient(colors: [Color(0xFF32D892), Color(0xFF259CB8)], begin: Alignment.centerLeft, end: Alignment.centerRight), + borderRadius: BorderRadius.circular(15), + boxShadow: isUser ? null : [BoxShadow(color: const Color(0xFF000000).withValues(alpha: 0.0784), offset: const Offset(0, 0), blurRadius: 13, spreadRadius: 0)], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(message.text, softWrap: true, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: isUser ? const Color(0xFF2B353E) : MyColors.white)), + Align( + alignment: Alignment.centerRight, + child: Text( + _formatTimestamp(message.timestamp), + style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: -0.4, color: isUser ? const Color(0xFF2B353E) : MyColors.whiteColor), + ), + ), + ], + ), + ), + ], + ), + ), + if (!isUser) const Spacer(flex: 2), + ], + ); + } + + String _formatTimestamp(DateTime timestamp) { + String hour = timestamp.hour.toString().padLeft(2, '0'); + String minute = timestamp.minute.toString().padLeft(2, '0'); + String month = timestamp.month.toString().padLeft(2, '0'); + String day = timestamp.day.toString().padLeft(2, '0'); + int year = timestamp.year; + return "$month/$day/$year $hour:$minute AM"; + } + + Widget _buildSuggestionChip(String label, VoidCallback onTap) { + return Material( + color: Colors.white, + elevation: 3, + shadowColor: const Color(0xFF000015).withValues(alpha: 0.0784), + borderRadius: BorderRadius.circular(10), + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: 0.2)), + ), + ), + ); + } + + Widget _buildBottomInputBar() { + return Container( + decoration: const BoxDecoration(color: MyColors.backgroundColor), + child: SafeArea( + top: false, + child: Row( + children: [ + // Plus button + // IconButton(padding: EdgeInsets.zero, icon: const Icon(Icons.add, color: MyColors.darkTextColor, size: 34), onPressed: () {}), + // const SizedBox(width: 12), + // Text field + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + height: 48, + decoration: BoxDecoration(color: Colors.transparent, borderRadius: BorderRadius.circular(10), border: BoxBorder.all(width: 1, color: const Color(0xFFE5E5E5))), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _messageController, + onSubmitted: (_) => _sendMessage(), + maxLines: 1, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + hintText: 'Type a message..', + hintStyle: TextStyle(color: MyColors.hintTextColor, fontSize: 14, fontWeight: FontWeight.normal), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 10), + ), + ), + ), + GestureDetector( + onTap: _speechEnabled ? (_speechToText.isNotListening ? _startListening : _stopListening) : null, + child: SvgPicture.asset("assets/icons/microphone.svg", colorFilter: ColorFilter.mode(_speechToText.isListening ? Colors.red : MyColors.darkTextColor, BlendMode.srcIn)), + ), + ], + ), + ), + ), + const SizedBox(width: 6), + // Send button + GestureDetector( + onTap: _sendMessage, + child: Container(width: 48, height: 48, decoration: const BoxDecoration(color: MyColors.white, shape: BoxShape.circle), child: Center(child: SvgPicture.asset("assets/icons/send.svg"))), + ), + ], + ), + ), + ).paddingOnly(left: 16, right: 16, bottom: 16); + } +} diff --git a/lib/ui/landing/dashboard_screen.dart b/lib/ui/landing/dashboard_screen.dart index 3122d17d..e87bafea 100644 --- a/lib/ui/landing/dashboard_screen.dart +++ b/lib/ui/landing/dashboard_screen.dart @@ -315,103 +315,103 @@ class _DashboardScreenState extends State with WidgetsBindingOb child: Consumer( builder: (BuildContext context, DashboardProviderModel model, Widget? child) { return (model.isAttendanceTrackingLoading - ? GetAttendanceTrackingShimmer() - : Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - gradient: const LinearGradient( - transform: GradientRotation(.46), - begin: Alignment.topRight, - end: Alignment.bottomLeft, - colors: [MyColors.gradiantEndColor, MyColors.gradiantStartColor], - ), - ), - child: Stack( - alignment: Alignment.center, - children: [ - if (model.isTimeRemainingInSeconds == 0) SvgPicture.asset("assets/images/thumb.svg"), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.markAttendance.tr().toText14(color: Colors.white, isBold: true), - if (model.isTimeRemainingInSeconds == 0) DateTime.now().toString().split(" ")[0].toText12(color: Colors.white), - if (model.isTimeRemainingInSeconds != 0) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + ? GetAttendanceTrackingShimmer() + : Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: const LinearGradient( + transform: GradientRotation(.46), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [MyColors.gradiantEndColor, MyColors.gradiantStartColor], + ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + if (model.isTimeRemainingInSeconds == 0) SvgPicture.asset("assets/images/thumb.svg"), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.markAttendance.tr().toText14(color: Colors.white, isBold: true), + if (model.isTimeRemainingInSeconds == 0) DateTime.now().toString().split(" ")[0].toText12(color: Colors.white), + if (model.isTimeRemainingInSeconds != 0) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 9.height, + Directionality( + textDirection: ui.TextDirection.ltr, + child: CountdownTimer( + endTime: model.endTime, + onEnd: null, + endWidget: "00:00:00".toText14(color: Colors.white, isBold: true), + textStyle: const TextStyle(color: Colors.white, fontSize: 14, letterSpacing: -0.48, fontWeight: FontWeight.bold), + ), + ), + LocaleKeys.timeLeftToday.tr().toText12(color: Colors.white), + 9.height, + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(20)), + child: LinearProgressIndicator( + value: model.progress, + minHeight: 8, + valueColor: const AlwaysStoppedAnimation(Colors.white), + backgroundColor: const Color(0xff196D73), + ), + ), + ], + ), + ], + ).paddingOnly(top: 12, right: 15, left: 12), + ), + Row( children: [ - 9.height, - Directionality( - textDirection: ui.TextDirection.ltr, - child: CountdownTimer( - endTime: model.endTime, - onEnd: null, - endWidget: "00:00:00".toText14(color: Colors.white, isBold: true), - textStyle: const TextStyle(color: Colors.white, fontSize: 14, letterSpacing: -0.48, fontWeight: FontWeight.bold), - ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.checkIn.tr().toText12(color: Colors.white), + (model.attendanceTracking!.pSwipeIn == null ? "--:--" : model.attendanceTracking!.pSwipeIn).toString().toText14( + color: Colors.white, + isBold: true, + ), + 4.height, + ], + ).paddingOnly(left: 12, right: 12), ), - LocaleKeys.timeLeftToday.tr().toText12(color: Colors.white), - 9.height, - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: LinearProgressIndicator( - value: model.progress, - minHeight: 8, - valueColor: const AlwaysStoppedAnimation(Colors.white), - backgroundColor: const Color(0xff196D73), + Container( + margin: EdgeInsets.only(top: AppState().isArabic(context) ? 6 : 0), + width: 45, + height: 45, + padding: const EdgeInsets.only(left: 10, right: 10), + decoration: BoxDecoration( + color: const Color(0xff259EA4), + borderRadius: BorderRadius.only( + bottomRight: AppState().isArabic(context) ? const Radius.circular(0) : const Radius.circular(15), + bottomLeft: AppState().isArabic(context) ? const Radius.circular(15) : const Radius.circular(0), + ), ), - ), + child: SvgPicture.asset(model.isTimeRemainingInSeconds == 0 ? "assets/images/biometrics.svg" : "assets/images/biometrics.svg"), + ).onPress(() { + showMyBottomSheet(context, callBackFunc: () {}, child: MarkAttendanceWidget(model, isFromDashboard: true)); + }), ], ), - ], - ).paddingOnly(top: 12, right: 15, left: 12), - ), - Row( - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.checkIn.tr().toText12(color: Colors.white), - (model.attendanceTracking!.pSwipeIn == null ? "--:--" : model.attendanceTracking!.pSwipeIn).toString().toText14( - color: Colors.white, - isBold: true, - ), - 4.height, - ], - ).paddingOnly(left: 12, right: 12), + ], ), - Container( - margin: EdgeInsets.only(top: AppState().isArabic(context) ? 6 : 0), - width: 45, - height: 45, - padding: const EdgeInsets.only(left: 10, right: 10), - decoration: BoxDecoration( - color: const Color(0xff259EA4), - borderRadius: BorderRadius.only( - bottomRight: AppState().isArabic(context) ? const Radius.circular(0) : const Radius.circular(15), - bottomLeft: AppState().isArabic(context) ? const Radius.circular(15) : const Radius.circular(0), - ), - ), - child: SvgPicture.asset(model.isTimeRemainingInSeconds == 0 ? "assets/images/biometrics.svg" : "assets/images/biometrics.svg"), - ).onPress(() { - showMyBottomSheet(context, callBackFunc: () {}, child: MarkAttendanceWidget(model, isFromDashboard: true)); - }), ], ), - ], - ), - ], - ), - ).onPress(() { - Navigator.pushNamed(context, AppRoutes.todayAttendance); - })) + ).onPress(() { + Navigator.pushNamed(context, AppRoutes.todayAttendance); + })) .animatedSwither(); }, ), @@ -460,70 +460,70 @@ class _DashboardScreenState extends State with WidgetsBindingOb flex: 2, child: RichText( text: - AppState().isArabic(context) - ? TextSpan( - children: [ - TextSpan( - text: 'اطلع على مميزات', - style: TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - TextSpan( - text: ' مزايا', - style: TextStyle( - fontSize: 16, - fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', - fontWeight: FontWeight.w700, - letterSpacing: -0.2, - height: 24 / 16, - color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed - ), - ), - ], - ) - : TextSpan( - children: [ - TextSpan( - text: LocaleKeys.explore.tr() + ' ', - style: const TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - TextSpan( - text: LocaleKeys.mazaya.tr(), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - fontFamily: 'Poppins', - letterSpacing: -0.2, - height: 24 / 16, - color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed - ), - ), - TextSpan( - text: ' ' + LocaleKeys.benefits.tr(), - style: const TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - ], - ), + AppState().isArabic(context) + ? TextSpan( + children: [ + TextSpan( + text: 'اطلع على مميزات', + style: TextStyle( + fontSize: 16, + letterSpacing: -0.2, + fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', + fontWeight: FontWeight.w700, + height: 24 / 16, + color: Color(0xFF5D5E5E), + ), + ), + TextSpan( + text: ' مزايا', + style: TextStyle( + fontSize: 16, + fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', + fontWeight: FontWeight.w700, + letterSpacing: -0.2, + height: 24 / 16, + color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed + ), + ), + ], + ) + : TextSpan( + children: [ + TextSpan( + text: LocaleKeys.explore.tr() + ' ', + style: const TextStyle( + fontSize: 16, + letterSpacing: -0.2, + fontFamily: 'Poppins', + fontWeight: FontWeight.w700, + height: 24 / 16, + color: Color(0xFF5D5E5E), + ), + ), + TextSpan( + text: LocaleKeys.mazaya.tr(), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + fontFamily: 'Poppins', + letterSpacing: -0.2, + height: 24 / 16, + color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed + ), + ), + TextSpan( + text: ' ' + LocaleKeys.benefits.tr(), + style: const TextStyle( + fontSize: 16, + letterSpacing: -0.2, + fontFamily: 'Poppins', + fontWeight: FontWeight.w700, + height: 24 / 16, + color: Color(0xFF5D5E5E), + ), + ), + ], + ), ), ), const Expanded(flex: 1, child: SizedBox()), @@ -576,12 +576,10 @@ class _DashboardScreenState extends State with WidgetsBindingOb LocaleKeys.discounts.tr().toText24(isBold: true), 6.width, Container( - padding: const EdgeInsets.only(left: 8, right: 8), - decoration: BoxDecoration( - color: MyColors.yellowColor, - borderRadius: BorderRadius.circular(10), - ), - child: LocaleKeys.newString.tr().toText10(isBold: true)), + padding: const EdgeInsets.only(left: 8, right: 8), + decoration: BoxDecoration(color: MyColors.yellowColor, borderRadius: BorderRadius.circular(10)), + child: LocaleKeys.newString.tr().toText10(isBold: true), + ), ], ), ], @@ -589,7 +587,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb ), LocaleKeys.viewAllOffers.tr().toText12(isUnderLine: true).onPress(() { Navigator.pushNamed(context, AppRoutes.offersAndDiscounts); - }) + }), ], ).paddingOnly(left: 21, right: 21), Consumer( @@ -597,59 +595,54 @@ class _DashboardScreenState extends State with WidgetsBindingOb return SizedBox( height: 103 + 33, child: ListView.separated( - shrinkWrap: true, - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.only(left: 21, right: 21, top: 13), - scrollDirection: Axis.horizontal, - itemBuilder: (BuildContext cxt, int index) { - return model.isOffersLoading - ? const OffersShimmerWidget() - : InkWell( - onTap: () { - navigateToDetails(data.getOffersList[index]); - }, - child: SizedBox( - width: 73, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 73, - height: 73, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all( - Radius.circular(100), - ), - border: Border.all(color: MyColors.lightGreyE3Color, width: 1), - ), - child: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(50), + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 21, right: 21, top: 13), + scrollDirection: Axis.horizontal, + itemBuilder: (BuildContext cxt, int index) { + return model.isOffersLoading + ? const OffersShimmerWidget() + : InkWell( + onTap: () { + navigateToDetails(data.getOffersList[index]); + }, + child: SizedBox( + width: 73, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 73, + height: 73, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(100)), + border: Border.all(color: MyColors.lightGreyE3Color, width: 1), ), - child: Hero( - tag: "ItemImage" + data.getOffersList[index].offersDiscountId.toString()!, - transitionOnUserGestures: true, - child: Image.network( - data.getOffersList[index].logo ?? "", - fit: BoxFit.contain, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(50)), + child: Hero( + tag: "ItemImage" + data.getOffersList[index].offersDiscountId.toString()!, + transitionOnUserGestures: true, + child: Image.network(data.getOffersList[index].logo ?? "", fit: BoxFit.contain), ), ), ), - ), - 4.height, - Expanded( - child: AppState().isArabic(context) - ? data.getOffersList[index].titleAr!.toText12(isCenter: true, maxLine: 1) - : data.getOffersList[index].titleEn!.toText12(isCenter: true, maxLine: 1), - ), - ], + 4.height, + Expanded( + child: + AppState().isArabic(context) + ? data.getOffersList[index].titleAr!.toText12(isCenter: true, maxLine: 1) + : data.getOffersList[index].titleEn!.toText12(isCenter: true, maxLine: 1), + ), + ], + ), ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => 8.width, - itemCount: 9), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => 8.width, + itemCount: 9, + ), ); }, ), @@ -772,28 +765,28 @@ class _DashboardScreenState extends State with WidgetsBindingOb SvgPicture.asset( "assets/icons/chat/chat.svg", color: - !checkIfPrivilegedForChat() - ? MyColors.lightGreyE3Color - : currentIndex == 4 - ? MyColors.grey3AColor - : cProvider.disbaleChatForThisUser - ? MyColors.lightGreyE3Color - : MyColors.grey98Color, + !checkIfPrivilegedForChat() + ? MyColors.lightGreyE3Color + : currentIndex == 4 + ? MyColors.grey3AColor + : cProvider.disbaleChatForThisUser + ? MyColors.lightGreyE3Color + : MyColors.grey98Color, ).paddingAll(4), Consumer( builder: (BuildContext cxt, ChatProviderModel data, Widget? child) { return !checkIfPrivilegedForChat() ? const SizedBox() : Positioned( - right: 0, - top: 0, - child: Container( - padding: const EdgeInsets.only(left: 4, right: 4), - alignment: Alignment.center, - decoration: BoxDecoration(color: cProvider.disbaleChatForThisUser ? MyColors.pinkDarkColor : MyColors.redColor, borderRadius: BorderRadius.circular(17)), - child: data.chatUConvCounter.toString().toText10(color: Colors.white), - ), - ); + right: 0, + top: 0, + child: Container( + padding: const EdgeInsets.only(left: 4, right: 4), + alignment: Alignment.center, + decoration: BoxDecoration(color: cProvider.disbaleChatForThisUser ? MyColors.pinkDarkColor : MyColors.redColor, borderRadius: BorderRadius.circular(17)), + child: data.chatUConvCounter.toString().toText10(color: Colors.white), + ), + ); }, ), ], @@ -824,6 +817,19 @@ class _DashboardScreenState extends State with WidgetsBindingOb }, ), ), + floatingActionButton: FloatingActionButton.large( + onPressed: () { + Navigator.pushNamed(context, AppRoutes.chatBotHome); + }, + backgroundColor: Colors.transparent, + elevation: 0, + focusElevation: 0, + hoverElevation: 0, + highlightElevation: 0, + + shape: const CircleBorder(), + child: SvgPicture.asset("assets/icons/chatbot.svg", width: 100, height: 100), + ), ), ); } @@ -864,4 +870,4 @@ class _DashboardScreenState extends State with WidgetsBindingOb } return false; } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 6abe6fc0..12343528 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -142,6 +142,8 @@ dependencies: webview_flutter: ^4.13.0 nfc_manager: ^3.2.0 + speech_to_text: ^7.3.0 + # saf: ^1.0.3+4 From f253a5e99e9097546a228bbf4f0cb56ef5920b06 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Wed, 31 Dec 2025 16:42:40 +0300 Subject: [PATCH 2/8] Chat Bot Completed --- lib/ui/chatbot/chatbot_screen.dart | 184 +++++++++++++++++++++++------ 1 file changed, 150 insertions(+), 34 deletions(-) diff --git a/lib/ui/chatbot/chatbot_screen.dart b/lib/ui/chatbot/chatbot_screen.dart index 5b5989df..dca43faa 100644 --- a/lib/ui/chatbot/chatbot_screen.dart +++ b/lib/ui/chatbot/chatbot_screen.dart @@ -31,6 +31,7 @@ class _ChatBotScreenState extends State { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); AtlasInitiateChatResponse? atlasChatTokenResponse; + bool _isThinking = false; // Speech to text final SpeechToText _speechToText = SpeechToText(); @@ -40,7 +41,6 @@ class _ChatBotScreenState extends State { void initState() { // TODO: implement initState super.initState(); - print("Chat Bot Launched"); getChatSession(); _initSpeech(); } @@ -76,7 +76,9 @@ class _ChatBotScreenState extends State { try { GenericResponseModel? res = await DashboardApiClient().getChatBotSession(); if (res != null) { - atlasChatTokenResponse = res.atlasInitiateChatResponse; + setState(() { + atlasChatTokenResponse = res.atlasInitiateChatResponse; + }); } } catch (ex) { Utils.handleException(ex, context, null); @@ -92,37 +94,23 @@ class _ChatBotScreenState extends State { super.dispose(); } - void _sendMessage() async { - String text = _messageController.text.trim(); + void _sendMessage({String? suggestionText}) async { + String text = suggestionText ?? _messageController.text.trim(); if (text.isEmpty || atlasChatTokenResponse == null) return; - - // setState(() { - // _messages.add(ChatMessage(text: text, isUser: true, timestamp: DateTime.now())); - // _currentMode = ChatMode.chatActive; - // _messageController.clear(); - // }); - // - // // Simulate bot response - // Future.delayed(const Duration(milliseconds: 500), () { - // if (mounted) { - // setState(() { - // _messages.add(ChatMessage(text: "Hello there! How may I assist you today?", isUser: false, timestamp: DateTime.now())); - // }); - // _scrollToBottom(); - // } - // }); - try { setState(() { _messages.add(ChatMessage(text: text, isUser: true, timestamp: DateTime.now())); _currentMode = ChatMode.chatActive; _messageController.clear(); + _isThinking = true; }); + _scrollToBottom(); GenericResponseModel? res = await DashboardApiClient().sendChatBotMessage(atlasChatTokenResponse: atlasChatTokenResponse!, atlasChatText: text); if (res != null) { res.atlasContinueChatResponseDetails; if (mounted) { setState(() { + _isThinking = false; _messages.add( ChatMessage( text: res.atlasContinueChatResponseDetails!.aiResponse!.structuredData!.answer ?? "", @@ -135,6 +123,9 @@ class _ChatBotScreenState extends State { _scrollToBottom(); } } catch (ex) { + setState(() { + _isThinking = false; + }); Utils.handleException(ex, context, null); } @@ -150,8 +141,11 @@ class _ChatBotScreenState extends State { } void _sendSuggestion(String text) { - _messageController.text = text; - _sendMessage(); + if (atlasChatTokenResponse == null) { + Utils.showToast("Please wait, initializing chat..."); + return; + } + _sendMessage(suggestionText: text); } @override @@ -199,8 +193,11 @@ class _ChatBotScreenState extends State { return ListView.separated( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - itemCount: _messages.length, + itemCount: _messages.length + (_isThinking ? 1 : 0), itemBuilder: (BuildContext context, int index) { + if (index == _messages.length && _isThinking) { + return _buildThinkingIndicator(); + } ChatMessage message = _messages[index]; return _buildMessageBubble(message); }, @@ -210,9 +207,28 @@ class _ChatBotScreenState extends State { ); } + Widget _buildThinkingIndicator() { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + gradient: const LinearGradient(colors: [Color(0xFF32D892), Color(0xFF259CB8)], begin: Alignment.centerLeft, end: Alignment.centerRight), + borderRadius: BorderRadius.circular(15), + boxShadow: [BoxShadow(color: const Color(0xFF000000).withValues(alpha: 0.0784), offset: const Offset(0, 0), blurRadius: 13, spreadRadius: 0)], + ), + child: const ThinkingDotsAnimation(), + ), + const Spacer(flex: 2), + ], + ); + } + Widget _buildMessageBubble(ChatMessage message) { bool isUser = message.isUser; - return Row( + Widget bubbleContent = Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -251,6 +267,11 @@ class _ChatBotScreenState extends State { if (!isUser) const Spacer(flex: 2), ], ); + + if (!isUser) { + return AnimatedMessageBubble(child: bubbleContent); + } + return bubbleContent; } String _formatTimestamp(DateTime timestamp) { @@ -300,21 +321,25 @@ class _ChatBotScreenState extends State { Expanded( child: TextField( controller: _messageController, - onSubmitted: (_) => _sendMessage(), + onSubmitted: _isThinking ? null : (_) => _sendMessage(), + enabled: !_isThinking, maxLines: 1, textAlignVertical: TextAlignVertical.center, - decoration: const InputDecoration( - hintText: 'Type a message..', - hintStyle: TextStyle(color: MyColors.hintTextColor, fontSize: 14, fontWeight: FontWeight.normal), + decoration: InputDecoration( + hintText: _isThinking ? 'Waiting for response...' : 'Type a message..', + hintStyle: const TextStyle(color: MyColors.hintTextColor, fontSize: 14, fontWeight: FontWeight.normal), border: InputBorder.none, isDense: true, - contentPadding: EdgeInsets.symmetric(vertical: 10), + contentPadding: const EdgeInsets.symmetric(vertical: 10), ), ), ), GestureDetector( - onTap: _speechEnabled ? (_speechToText.isNotListening ? _startListening : _stopListening) : null, - child: SvgPicture.asset("assets/icons/microphone.svg", colorFilter: ColorFilter.mode(_speechToText.isListening ? Colors.red : MyColors.darkTextColor, BlendMode.srcIn)), + onTap: _isThinking ? null : (_speechEnabled ? (_speechToText.isNotListening ? _startListening : _stopListening) : null), + child: SvgPicture.asset( + "assets/icons/microphone.svg", + colorFilter: ColorFilter.mode(_isThinking ? MyColors.hintTextColor : (_speechToText.isListening ? Colors.red : MyColors.darkTextColor), BlendMode.srcIn), + ), ), ], ), @@ -323,8 +348,13 @@ class _ChatBotScreenState extends State { const SizedBox(width: 6), // Send button GestureDetector( - onTap: _sendMessage, - child: Container(width: 48, height: 48, decoration: const BoxDecoration(color: MyColors.white, shape: BoxShape.circle), child: Center(child: SvgPicture.asset("assets/icons/send.svg"))), + onTap: _isThinking ? null : _sendMessage, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration(color: _isThinking ? MyColors.hintTextColor.withValues(alpha: 0.3) : MyColors.white, shape: BoxShape.circle), + child: Center(child: SvgPicture.asset("assets/icons/send.svg", colorFilter: _isThinking ? const ColorFilter.mode(MyColors.hintTextColor, BlendMode.srcIn) : null)), + ), ), ], ), @@ -332,3 +362,89 @@ class _ChatBotScreenState extends State { ).paddingOnly(left: 16, right: 16, bottom: 16); } } + +class ThinkingDotsAnimation extends StatefulWidget { + const ThinkingDotsAnimation({Key? key}) : super(key: key); + + @override + State createState() => _ThinkingDotsAnimationState(); +} + +class _ThinkingDotsAnimationState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Thinking", style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white)), + const SizedBox(width: 2), + AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget? child) { + int dotCount = (_controller.value * 4).floor() % 4; + String dots = '.' * dotCount; + return SizedBox(width: 18, child: Text(dots, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white))); + }, + ), + ], + ); + } +} + +class AnimatedMessageBubble extends StatefulWidget { + final Widget child; + + const AnimatedMessageBubble({Key? key, required this.child}) : super(key: key); + + @override + State createState() => _AnimatedMessageBubbleState(); +} + +class _AnimatedMessageBubbleState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnimation; + late Animation _slideAnimation; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400)); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + + _slideAnimation = Tween(begin: const Offset(-0.3, 0.0), end: Offset.zero).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); + + _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack)); + + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SlideTransition( + position: _slideAnimation, + child: FadeTransition(opacity: _fadeAnimation, child: ScaleTransition(scale: _scaleAnimation, alignment: Alignment.centerLeft, child: widget.child)), + ); + } +} From 4f4db082faff7c507c4dc7a4ef2aa061ec07a912 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Wed, 31 Dec 2025 16:44:13 +0300 Subject: [PATCH 3/8] Chat Bot Completed --- android/app/src/main/AndroidManifest.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1cf76f65..a8196e05 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,9 +30,7 @@ - - - + From 0175c958ef990742c48c37a9a1e44bc271a82072 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Mon, 28 Sep 2026 11:42:28 +0300 Subject: [PATCH 4/8] translation added. --- assets/langs/ar-SA.json | 11 +++- assets/langs/en-US.json | 11 +++- lib/generated/codegen_loader.g.dart | 22 +++++++- lib/generated/locale_keys.g.dart | 9 ++++ .../get_mobile_login_info_list_model.dart | 2 +- lib/ui/chatbot/chatbot_screen.dart | 50 ++++++++++++------- 6 files changed, 83 insertions(+), 22 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 916e8fb8..93160f54 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -642,5 +642,14 @@ "yearsUsed": "سنوات الاستخدام", "faRequest": "طلب التخلص من FA", "showMore": "عرض المزيد", - "buyerDetails": "تفاصيل المشتري" + "buyerDetails": "تفاصيل المشتري", + "chatBot": "المساعد الذكي", + "helloAskMe": "مرحباً، اسألني", + "anything": "عن أي شيء...", + "thinking": "جاري التفكير", + "waitingForResponse": "في انتظار الرد...", + "typeAMessage": "اكتب رسالة...", + "initializingChatPleaseWait": "يرجى الانتظار، جاري تهيئة المحادثة...", + "am": "ص", + "pm": "م" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 5fcb35cd..7637798c 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -640,5 +640,14 @@ "yearsUsed": "Years Used", "faRequest":"FA Disposal Request", "showMore": "Show More", - "buyerDetails": "Buyer Details" + "buyerDetails": "Buyer Details", + "chatBot": "ChatBot", + "helloAskMe": "Hello, ask me", + "anything": "anything...", + "thinking": "Thinking", + "waitingForResponse": "Waiting for response...", + "typeAMessage": "Type a message..", + "initializingChatPleaseWait": "Please wait, initializing chat...", + "am": "AM", + "pm": "PM" } \ No newline at end of file diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 6feb4387..0d41dfad 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -655,7 +655,16 @@ class CodegenLoader extends AssetLoader{ "yearsUsed": "سنوات الاستخدام", "faRequest": "طلب التخلص من FA", "showMore": "عرض المزيد", - "buyerDetails": "تفاصيل المشتري" + "buyerDetails": "تفاصيل المشتري", + "chatBot": "المساعد الذكي", + "helloAskMe": "مرحباً، اسألني", + "anything": "عن أي شيء...", + "thinking": "جاري التفكير", + "waitingForResponse": "في انتظار الرد...", + "typeAMessage": "اكتب رسالة...", + "initializingChatPleaseWait": "يرجى الانتظار، جاري تهيئة المحادثة...", + "am": "ص", + "pm": "م" }; static const Map _en_US = { "mohemm": "Mohemm", @@ -1298,7 +1307,16 @@ static const Map _en_US = { "yearsUsed": "Years Used", "faRequest": "FA Disposal Request", "showMore": "Show More", - "buyerDetails": "Buyer Details" + "buyerDetails": "Buyer Details", + "chatBot": "ChatBot", + "helloAskMe": "Hello, ask me", + "anything": "anything...", + "thinking": "Thinking", + "waitingForResponse": "Waiting for response...", + "typeAMessage": "Type a message..", + "initializingChatPleaseWait": "Please wait, initializing chat...", + "am": "AM", + "pm": "PM" }; static const Map> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 1a1cf956..9e9feace 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -628,5 +628,14 @@ abstract class LocaleKeys { static const faRequest = 'faRequest'; static const showMore = 'showMore'; static const buyerDetails = 'buyerDetails'; + static const chatBot = 'chatBot'; + static const helloAskMe = 'helloAskMe'; + static const anything = 'anything'; + static const thinking = 'thinking'; + static const waitingForResponse = 'waitingForResponse'; + static const typeAMessage = 'typeAMessage'; + static const initializingChatPleaseWait = 'initializingChatPleaseWait'; + static const am = 'am'; + static const pm = 'pm'; } diff --git a/lib/models/get_mobile_login_info_list_model.dart b/lib/models/get_mobile_login_info_list_model.dart index a2f7799b..d544ef58 100644 --- a/lib/models/get_mobile_login_info_list_model.dart +++ b/lib/models/get_mobile_login_info_list_model.dart @@ -1,6 +1,6 @@ class GetMobileLoginInfoListModel { int? iD; - int? employeeID; + String? employeeID; int? channelID; int? companyID; String? deviceType; diff --git a/lib/ui/chatbot/chatbot_screen.dart b/lib/ui/chatbot/chatbot_screen.dart index dca43faa..a41b3d24 100644 --- a/lib/ui/chatbot/chatbot_screen.dart +++ b/lib/ui/chatbot/chatbot_screen.dart @@ -1,9 +1,12 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:mohem_flutter_app/api/dashboard_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:speech_to_text/speech_recognition_result.dart'; import 'package:speech_to_text/speech_to_text.dart'; @@ -53,7 +56,10 @@ class _ChatBotScreenState extends State { /// Start listening for speech void _startListening() async { - await _speechToText.listen(onResult: _onSpeechResult); + await _speechToText.listen( + onResult: _onSpeechResult, + localeId: AppState().isArabic(context) ? 'ar_SA' : 'en_US', + ); setState(() {}); } @@ -133,7 +139,7 @@ class _ChatBotScreenState extends State { } void _scrollToBottom() { - Future.delayed(const Duration(milliseconds: 100), () { + Future.delayed(const Duration(milliseconds: 100), () { if (_scrollController.hasClients) { _scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); } @@ -142,7 +148,7 @@ class _ChatBotScreenState extends State { void _sendSuggestion(String text) { if (atlasChatTokenResponse == null) { - Utils.showToast("Please wait, initializing chat..."); + Utils.showToast(LocaleKeys.initializingChatPleaseWait.tr()); return; } _sendMessage(suggestionText: text); @@ -153,7 +159,7 @@ class _ChatBotScreenState extends State { return Scaffold( backgroundColor: MyColors.backgroundColor, appBar: AppBar( - title: const Text("ChatBot", style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2B353E))), + title: Text(LocaleKeys.chatBot.tr(), style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2B353E))), backgroundColor: MyColors.backgroundColor, centerTitle: false, ), @@ -169,18 +175,18 @@ class _ChatBotScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Text("Hello, ask me", textAlign: TextAlign.center, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))), - const Text("anything...", textAlign: TextAlign.center, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))), + Text(LocaleKeys.helloAskMe.tr(), textAlign: TextAlign.center, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))), + Text(LocaleKeys.anything.tr(), textAlign: TextAlign.center, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))), const SizedBox(height: 50), - Wrap( + const Wrap( alignment: WrapAlignment.center, spacing: 8, runSpacing: 12, children: [ - _buildSuggestionChip("Nostalgia Perfume", () => _sendSuggestion("Nostalgia Perfume")), - _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")), - _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")), - _buildSuggestionChip("Al Jadi", () => _sendSuggestion("Al Jadi")), + // _buildSuggestionChip("Nostalgia Perfume", () => _sendSuggestion("Nostalgia Perfume")), + // _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")), + // _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")), + // _buildSuggestionChip("Al Jadi", () => _sendSuggestion("Al Jadi")), ], ), ], @@ -275,12 +281,16 @@ class _ChatBotScreenState extends State { } String _formatTimestamp(DateTime timestamp) { - String hour = timestamp.hour.toString().padLeft(2, '0'); - String minute = timestamp.minute.toString().padLeft(2, '0'); + int hourInt = timestamp.hour; + int minute = timestamp.minute; + String period = hourInt >= 12 ? LocaleKeys.pm.tr() : LocaleKeys.am.tr(); + int formattedHour = hourInt % 12 == 0 ? 12 : hourInt % 12; + String hour = formattedHour.toString().padLeft(2, '0'); + String min = minute.toString().padLeft(2, '0'); String month = timestamp.month.toString().padLeft(2, '0'); String day = timestamp.day.toString().padLeft(2, '0'); int year = timestamp.year; - return "$month/$day/$year $hour:$minute AM"; + return "$month/$day/$year $hour:$min $period"; } Widget _buildSuggestionChip(String label, VoidCallback onTap) { @@ -326,7 +336,7 @@ class _ChatBotScreenState extends State { maxLines: 1, textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( - hintText: _isThinking ? 'Waiting for response...' : 'Type a message..', + hintText: _isThinking ? LocaleKeys.waitingForResponse.tr() : LocaleKeys.typeAMessage.tr(), hintStyle: const TextStyle(color: MyColors.hintTextColor, fontSize: 14, fontWeight: FontWeight.normal), border: InputBorder.none, isDense: true, @@ -353,7 +363,13 @@ class _ChatBotScreenState extends State { width: 48, height: 48, decoration: BoxDecoration(color: _isThinking ? MyColors.hintTextColor.withValues(alpha: 0.3) : MyColors.white, shape: BoxShape.circle), - child: Center(child: SvgPicture.asset("assets/icons/send.svg", colorFilter: _isThinking ? const ColorFilter.mode(MyColors.hintTextColor, BlendMode.srcIn) : null)), + child: Center( + child: SvgPicture.asset( + "assets/icons/send.svg", + matchTextDirection: true, + colorFilter: _isThinking ? const ColorFilter.mode(MyColors.hintTextColor, BlendMode.srcIn) : null, + ), + ), ), ), ], @@ -390,7 +406,7 @@ class _ThinkingDotsAnimationState extends State with Sing return Row( mainAxisSize: MainAxisSize.min, children: [ - const Text("Thinking", style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white)), + Text(LocaleKeys.thinking.tr(), style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white)), const SizedBox(width: 2), AnimatedBuilder( animation: _controller, From 9284d0e1003dd3c8b076430028e155d539452b45 Mon Sep 17 00:00:00 2001 From: taha Date: Tue, 29 Sep 2026 16:37:37 +0300 Subject: [PATCH 5/8] chat history maintained --- lib/ui/chatbot/chatbot_screen.dart | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/ui/chatbot/chatbot_screen.dart b/lib/ui/chatbot/chatbot_screen.dart index a41b3d24..964eaa29 100644 --- a/lib/ui/chatbot/chatbot_screen.dart +++ b/lib/ui/chatbot/chatbot_screen.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_svg/svg.dart'; import 'package:mohem_flutter_app/api/dashboard_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; @@ -11,6 +12,11 @@ import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:speech_to_text/speech_recognition_result.dart'; import 'package:speech_to_text/speech_to_text.dart'; + +class MessagesHolder{ + static List messages = []; +} + enum ChatMode { home, chatActive } class ChatMessage { @@ -51,6 +57,13 @@ class _ChatBotScreenState extends State { /// Initialize speech to text void _initSpeech() async { _speechEnabled = await _speechToText.initialize(); + + if(MessagesHolder.messages.isNotEmpty){ + setState(() { + _messages.addAll(MessagesHolder.messages); + _currentMode = ChatMode.chatActive; + }); + } setState(() {}); } @@ -159,9 +172,26 @@ class _ChatBotScreenState extends State { return Scaffold( backgroundColor: MyColors.backgroundColor, appBar: AppBar( + automaticallyImplyLeading: false, title: Text(LocaleKeys.chatBot.tr(), style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2B353E))), backgroundColor: MyColors.backgroundColor, centerTitle: false, + actions: [ + IconButton( + icon: const Icon(Icons.minimize, color: Color(0xFF2B353E)), + onPressed: () { + MessagesHolder.messages = _messages; + Navigator.of(context).pop(); + }, + ), + IconButton( + icon: const Icon(Icons.close, color: Color(0xFF2B353E)), + onPressed: () { + MessagesHolder.messages.clear(); + Navigator.of(context).pop(); + }, + ), + ], ), body: Column(children: [Expanded(child: _currentMode == ChatMode.home ? _buildHomeMode() : _buildChatMode()), _buildBottomInputBar()]), ); @@ -464,3 +494,6 @@ class _AnimatedMessageBubbleState extends State with Sing ); } } + + + From a6be594b3b681fbcc9db3aa0c0fc9b8bd7a477c3 Mon Sep 17 00:00:00 2001 From: taha Date: Thu, 1 Oct 2026 14:30:54 +0300 Subject: [PATCH 6/8] Update dependencies and improve code structure --- analysis_options.yaml | 5 + android/app/build.gradle | 8 +- android/app/src/main/AndroidManifest.xml | 1 - android/build.gradle | 9 + android/gradle.properties | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 6 +- check.sh | 699 ++++++++++++++++++ ios/Flutter/AppFrameworkInfo.plist | 2 - ios/Runner.xcodeproj/project.pbxproj | 68 +- .../xcshareddata/xcschemes/Runner.xcscheme | 18 + ios/Runner/AppDelegate.swift | 9 +- ios/Runner/Info.plist | 25 +- lib/api/dashboard_api_client.dart | 5 +- lib/classes/date_uitl.dart | 5 +- lib/classes/utils.dart | 37 +- lib/generated_plugin_registrant.dart | 2 - lib/main.dart | 51 +- lib/provider/chat_provider_model.dart | 14 +- lib/theme/app_theme.dart | 1 + lib/ui/login/verify_last_login_screen.dart | 10 +- lib/ui/login/verify_login_screen.dart | 4 +- lib/ui/marathon/widgets/question_card.dart | 11 +- lib/ui/my_team/subordinate_leave.dart | 8 +- .../announcements/announcement_details.dart | 7 +- .../mowadhafhi/mowadhafhi_hr_request.dart | 4 +- .../my_document_detail_screen.dart | 17 +- lib/ui/screens/my_requests/my_requests.dart | 6 +- .../offers_and_discounts_details.dart | 14 +- lib/ui/work_list/item_history_screen.dart | 6 +- lib/ui/work_list/work_list_screen.dart | 2 +- .../worklist_fragments/info_fragments.dart | 2 +- lib/widgets/image_picker.dart | 6 +- lib/widgets/nfc/nfc_reader_sheet.dart | 25 +- pubspec.yaml | 93 +-- 35 files changed, 1010 insertions(+), 178 deletions(-) create mode 100755 check.sh diff --git a/analysis_options.yaml b/analysis_options.yaml index 674be66a..8716f747 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -19,6 +19,11 @@ analyzer: unnecessary_null_checks: error unnecessary_brace_in_string_interps: error unnecessary_final: error + exclude: + - build/** + - android/** + - ios/** + - web/** linter: diff --git a/android/app/build.gradle b/android/app/build.gradle index 1901f3ac..20d77ebb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -18,7 +18,7 @@ if (keystorePropertiesFile.exists()) { android { namespace 'hmg.cloudSolutions.mohem' - compileSdk 36 + compileSdk 37 ndkVersion '28.2.13676358' @@ -34,7 +34,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "hmg.cloudSolutions.mohem" minSdkVersion 30 - targetSdk 35 + targetSdk 37 targetSdk = flutter.targetSdkVersion versionCode flutter.versionCode versionName flutter.versionName @@ -60,12 +60,14 @@ android { buildTypes { debug { signingConfig signingConfigs.debug + minifyEnabled false + shrinkResources false } release { signingConfig signingConfigs.release minifyEnabled true shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles 'proguard-rules.pro' } } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a8196e05..6477dfd5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,7 +43,6 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="MoheM" - android:extractNativeLibs="true" android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round"> + if (project.name == "huawei_push") { + project.android { + compileSdk 36 + } + } + } +} rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" diff --git a/android/gradle.properties b/android/gradle.properties index 8df3fe98..acecd2f0 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -4,4 +4,8 @@ android.useAndroidX=true android.enableJetifier=true apmsInstrumentationEnabled=false -#org.gradle.java.home=/Users/amirs/Library/Java/JavaVirtualMachines/jbr-17.0.12/Contents/Home/ \ No newline at end of file +#org.gradle.java.home=/Users/amirs/Library/Java/JavaVirtualMachines/jbr-17.0.12/Contents/Home/ +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 941ceb11..54ce42ec 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jul 08 11:08:34 AST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/settings.gradle b/android/settings.gradle index 10a4a8da..1f8397e2 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -30,8 +30,10 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.11.0' apply false - id("org.jetbrains.kotlin.android") version "2.2.0" apply false +// id "com.android.application" version '8.11.0' apply false +// id("org.jetbrains.kotlin.android") version "2.2.0" apply false + id "com.android.application" version '9.0.1' apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false id("com.google.gms.google-services") version "4.4.3" apply false id("com.google.firebase.crashlytics") version "3.0.4" apply false id('org.gradle.toolchains.foojay-resolver-convention') version '0.9.0' apply false diff --git a/check.sh b/check.sh new file mode 100755 index 00000000..d8db1291 --- /dev/null +++ b/check.sh @@ -0,0 +1,699 @@ +#!/bin/bash + +# Enhanced ELF Alignment Checker for Android 16KB Page Size Compatibility +# This script checks if your app's native libraries are compatible with 16KB page size devices +# as required by Google Play starting November 1st, 2025 for apps targeting Android 15+ + +progname="${0##*/}" +progname="${progname%.sh}" + +# Color codes and formatting - detect if colors are supported +if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && tput colors >/dev/null 2>&1 && [[ $(tput colors) -ge 8 ]]; then + RED="\033[31m" + GREEN="\033[32m" + YELLOW="\033[33m" + BLUE="\033[34m" + CYAN="\033[36m" + PURPLE="\033[35m" + BOLD="\033[1m" + DIM="\033[2m" + ENDCOLOR="\033[0m" + USE_COLORS=true +else + # No color support - use plain text + RED="" + GREEN="" + YELLOW="" + BLUE="" + CYAN="" + PURPLE="" + BOLD="" + DIM="" + ENDCOLOR="" + USE_COLORS=false +fi + +# Unicode symbols with fallbacks +if [[ $USE_COLORS == true ]]; then + CHECK_MARK="✓" + CROSS_MARK="✗" + WARNING_MARK="⚠" + INFO_MARK="ℹ" + ROCKET="🚀" + GEAR="⚙" + PACKAGE="📦" + DOCUMENT="📄" + ALERT="🚨" + PARTY="🎉" + TOOLS="🔧" + BOOKS="📚" + ARROW="▶" +else + CHECK_MARK="[OK]" + CROSS_MARK="[X]" + WARNING_MARK="[!]" + INFO_MARK="[i]" + ROCKET="[>]" + GEAR="[*]" + PACKAGE="[P]" + DOCUMENT="[D]" + ALERT="[!]" + PARTY="[*]" + TOOLS="[T]" + BOOKS="[B]" + ARROW=">" +fi + +# Enhanced formatting functions +print_banner() { + local title="$1" + local subtitle="$2" + local width=88 + + echo + printf "%*s\n" $width | tr ' ' '═' + + # Main title + local title_len=${#title} + local title_padding=$(( (width - title_len - 4) / 2 )) + printf "║${BOLD}${BLUE}%*s %s %*s${ENDCOLOR}║\n" \ + $title_padding "" "$title" $title_padding "" + + # Subtitle if provided + if [[ -n "$subtitle" ]]; then + local subtitle_len=${#subtitle} + local subtitle_padding=$(( (width - subtitle_len - 4) / 2 )) + printf "║${DIM}%*s %s %*s${ENDCOLOR}║\n" \ + $subtitle_padding "" "$subtitle" $subtitle_padding "" + fi + + printf "%*s\n" $width | tr ' ' '═' + echo +} + +print_section() { + local icon="$1" + local title="$2" + local description="$3" + + echo + echo -e "${BOLD}${BLUE}$icon $title${ENDCOLOR}" + if [[ -n "$description" ]]; then + echo -e "${DIM}$description${ENDCOLOR}" + fi + printf "${BLUE}%*s${ENDCOLOR}\n" 80 | tr ' ' '─' +} + +print_status() { + local status="$1" + local message="$2" + local detail="$3" + + case $status in + "success") + printf " ${GREEN}${CHECK_MARK}${ENDCOLOR} %s" "$message" + ;; + "error") + printf " ${RED}${CROSS_MARK}${ENDCOLOR} %s" "$message" + ;; + "warning") + printf " ${YELLOW}${WARNING_MARK}${ENDCOLOR} %s" "$message" + ;; + "info") + printf " ${CYAN}${INFO_MARK}${ENDCOLOR} %s" "$message" + ;; + "processing") + printf " ${PURPLE}${GEAR}${ENDCOLOR} %s" "$message" + ;; + esac + + if [[ -n "$detail" ]]; then + printf "\n ${DIM}%s${ENDCOLOR}" "$detail" + fi + echo +} + +print_subsection() { + local title="$1" + echo + echo -e " ${BOLD}${PURPLE}${ARROW} $title${ENDCOLOR}" + printf " ${PURPLE}%*s${ENDCOLOR}\n" 50 | tr ' ' '┈' +} + +print_table_header() { + echo + local line1="┌$(printf '─%.0s' {1..29})┬$(printf '─%.0s' {1..18})┬$(printf '─%.0s' {1..15})┬$(printf '─%.0s' {1..13})┐" + local line2="│ $(printf '%-27s' 'Library') │ $(printf '%-16s' 'Architecture') │ $(printf '%-13s' 'Alignment') │ $(printf '%-11s' 'Status') │" + local line3="├$(printf '─%.0s' {1..29})┼$(printf '─%.0s' {1..18})┼$(printf '─%.0s' {1..15})┼$(printf '─%.0s' {1..13})┤" + + echo -e "${CYAN}$line1${ENDCOLOR}" + echo -e "${BOLD}${CYAN}$line2${ENDCOLOR}" + echo -e "${CYAN}$line3${ENDCOLOR}" +} + +print_table_row() { + local lib="$1" + local arch="$2" + local alignment="$3" + local status="$4" + local is_critical="$5" + + local status_symbol + local status_color + local status_text + + if [[ $status == "ALIGNED" ]]; then + status_symbol="$CHECK_MARK" + status_color="$GREEN" + status_text="PASS" + else + status_symbol="$CROSS_MARK" + if [[ $is_critical == "true" ]]; then + status_color="$RED" + status_text="FAIL" + else + status_color="$YELLOW" + status_text="WARN" + fi + fi + + # Truncate library name if too long + local display_lib="$lib" + if [[ ${#lib} -gt 27 ]]; then + display_lib="${lib:0:24}..." + fi + + printf "${CYAN}│${ENDCOLOR} %-27s ${CYAN}│${ENDCOLOR} %-16s ${CYAN}│${ENDCOLOR} %-13s ${CYAN}│${ENDCOLOR} ${status_color}%s %-7s${ENDCOLOR} ${CYAN}│${ENDCOLOR}\n" \ + "$display_lib" "$arch" "$alignment" "$status_symbol" "$status_text" + + if [[ $is_critical == "true" && $status == "UNALIGNED" ]]; then + printf "${CYAN}│${ENDCOLOR} ${RED}%-75s${ENDCOLOR} ${CYAN}│${ENDCOLOR}\n" " ${ALERT} CRITICAL: Required for Google Play compliance!" + fi +} + +print_table_footer() { + local line="└$(printf '─%.0s' {1..29})┴$(printf '─%.0s' {1..18})┴$(printf '─%.0s' {1..15})┴$(printf '─%.0s' {1..13})┘" + echo -e "${CYAN}$line${ENDCOLOR}" + echo +} + +print_summary_box() { + local title="$1" + local status="$2" # success, warning, error + shift 2 + local lines=("$@") + + local box_color + local title_color + case $status in + "success") box_color="$GREEN"; title_color="$GREEN" ;; + "warning") box_color="$YELLOW"; title_color="$YELLOW" ;; + "error") box_color="$RED"; title_color="$RED" ;; + *) box_color="$BLUE"; title_color="$BLUE" ;; + esac + + local width=78 + echo + printf "${box_color}┌%*s┐${ENDCOLOR}\n" $((width-2)) | tr ' ' '─' + + # Title + local title_len=${#title} + local title_padding=$(( (width - title_len - 4) / 2 )) + printf "${box_color}│${ENDCOLOR}${BOLD}${title_color}%*s %s %*s${ENDCOLOR}${box_color}│${ENDCOLOR}\n" \ + $title_padding "" "$title" $title_padding "" + + printf "${box_color}├%*s┤${ENDCOLOR}\n" $((width-2)) | tr ' ' '─' + + # Content lines + for line in "${lines[@]}"; do + printf "${box_color}│${ENDCOLOR} %-*s ${box_color}│${ENDCOLOR}\n" $((width-4)) "$line" + done + + printf "${box_color}└%*s┘${ENDCOLOR}\n" $((width-2)) | tr ' ' '─' + echo +} + +cleanup_trap() { + if [ -n "${tmp}" -a -d "${tmp}" ]; then + print_status "processing" "Cleaning up temporary files..." + rm -rf "${tmp}" + fi + exit $1 +} + +usage() { + print_banner "Android 16KB Page Size Compatibility Checker" "Google Play Compliance Tool" + + echo -e "${BOLD}DESCRIPTION:${ENDCOLOR}" + echo " This tool verifies that your Android app's native libraries are compatible" + echo " with 16KB page size devices, as required by Google Play starting November 1st, 2025." + echo + + echo -e "${BOLD}USAGE:${ENDCOLOR}" + echo -e " ${GREEN}$progname${ENDCOLOR} ${CYAN}[input-path|input-APK|input-APEX]${ENDCOLOR}" + echo + + echo -e "${BOLD}EXAMPLES:${ENDCOLOR}" + echo -e " ${DIM}# Check an APK file${ENDCOLOR}" + echo -e " $progname ${CYAN}app/build/outputs/apk/release/app-release.apk${ENDCOLOR}" + echo + echo -e " ${DIM}# Check a directory of native libraries${ENDCOLOR}" + echo -e " $progname ${CYAN}/path/to/native/libs/${ENDCOLOR}" + echo + + echo -e "${BOLD}WHAT THIS TOOL CHECKS:${ENDCOLOR}" + echo -e " ${CHECK_MARK} APK zip-alignment for 16KB boundaries (requires build-tools 35.0.0+)" + echo -e " ${CHECK_MARK} ELF segment alignment in native libraries (arm64-v8a and x86_64)" + echo -e " ${CHECK_MARK} Compliance with Android 16KB page size requirements" + echo + + echo -e "${BOLD}RESULT MEANINGS:${ENDCOLOR}" + echo -e " ${GREEN}${CHECK_MARK} PASS${ENDCOLOR} - Library is compatible with 16KB page sizes (2**14 or higher)" + echo -e " ${RED}${CROSS_MARK} FAIL${ENDCOLOR} - Library needs recompilation with 16KB ELF alignment" + echo -e " ${YELLOW}${WARNING_MARK} WARN${ENDCOLOR} - Non-critical architecture but should be fixed" + echo +} + +# Enhanced dependency checking +check_dependencies() { + print_section "$GEAR" "Dependency Check" "Verifying required tools are available" + + local missing_tools=() + local available_tools=() + + # Check each tool + local tools=("objdump" "unzip" "file") + for tool in "${tools[@]}"; do + if command -v "$tool" >/dev/null 2>&1; then + available_tools+=("$tool") + print_status "success" "$tool" "$(which "$tool")" + else + missing_tools+=("$tool") + print_status "error" "$tool not found" + fi + done + + if [ ${#missing_tools[@]} -gt 0 ]; then + echo + print_subsection "Installation Instructions" + for tool in "${missing_tools[@]}"; do + case $tool in + objdump) + print_status "info" "Install objdump:" "macOS: Xcode Command Line Tools or Android NDK's llvm-objdump" + print_status "info" "" "Linux: sudo apt-get install binutils (Ubuntu/Debian)" + ;; + unzip) + print_status "info" "Install unzip:" "Usually pre-installed on most systems" + print_status "info" "" "Linux: sudo apt-get install unzip" + ;; + file) + print_status "info" "Install file:" "Usually pre-installed on most systems" + print_status "info" "" "Linux: sudo apt-get install file" + ;; + esac + echo + done + exit 1 + fi + + print_status "success" "All dependencies satisfied" "${#available_tools[@]} tools available" +} + +# Validate input arguments +if [ ${#} -ne 1 ]; then + usage + exit 1 +fi + +case ${1} in + --help | -h | -\?) + usage + exit 0 + ;; + *) + dir="${1}" + ;; +esac + +# Validate input file/directory +if ! [ -f "${dir}" -o -d "${dir}" ]; then + print_status "error" "Invalid input: ${dir}" + echo " Please provide a valid APK file, APEX file, or directory containing native libraries." + exit 1 +fi + +# Check dependencies before proceeding +check_dependencies + +print_banner "ANALYSIS IN PROGRESS" "Checking 16KB Page Size Compatibility" + +print_status "processing" "Target: $(basename "${dir}")" +print_status "info" "Compliance Deadline: November 1st, 2025" +print_status "info" "Requirement: Apps targeting Android 15+ must support 16KB pages" + +# APK Processing +if [[ "${dir}" == *.apk ]]; then + trap 'cleanup_trap' EXIT + + print_section "$PACKAGE" "APK Analysis" "Processing Android Package file" + + # Enhanced zipalign check + if command -v zipalign >/dev/null 2>&1; then + if { zipalign --help 2>&1 | grep -q "\-P "; }; then + print_status "processing" "Checking APK zip-alignment for 16KB boundaries..." + + zip_result=$(zipalign -v -c -P 16 4 "${dir}" 2>&1) + zip_exit_code=$? + + if [ $zip_exit_code -eq 0 ]; then + print_status "success" "APK zip-alignment verification passed" + else + print_status "error" "APK zip-alignment verification failed" + echo " ${DIM}Details:${ENDCOLOR}" + echo "$zip_result" | grep -E 'lib/arm64-v8a|lib/x86_64|Verification|would be' | sed 's/^/ /' || echo " $zip_result" + fi + else + print_status "warning" "zipalign version doesn't support 16KB alignment checks" + print_status "info" "Solution: Update to Android SDK build-tools 35.0.0+" + echo " ${DIM}Update via Android Studio → SDK Manager → SDK Tools${ENDCOLOR}" + echo " ${DIM}Or run: sdkmanager \"build-tools;35.0.0\"${ENDCOLOR}" + fi + else + print_status "warning" "zipalign not found in PATH" + print_status "info" "Ensure Android SDK build-tools are installed and in PATH" + fi + + # Extract APK + dir_filename=$(basename "${dir}") + tmp=$(mktemp -d -t "${dir_filename%.apk}_out_XXXXX") + + if [ ! -d "${tmp}" ]; then + print_status "error" "Failed to create temporary directory" + exit 1 + fi + + print_status "processing" "Extracting native libraries from APK..." + if ! unzip -q "${dir}" "lib/*" -d "${tmp}" 2>/dev/null; then + print_summary_box "NO NATIVE LIBRARIES FOUND" "success" \ + "${PARTY} Your app contains only Java/Kotlin code!" \ + "" \ + "${CHECK_MARK} Apps without native libraries automatically support 16KB devices" \ + "${CHECK_MARK} No additional changes required for Google Play compliance" \ + "${CHECK_MARK} You're all set for the November 1st, 2025 deadline!" + cleanup_trap 0 + fi + + dir="${tmp}" +fi + +# APEX Processing +if [[ "${dir}" == *.apex ]]; then + trap 'cleanup_trap' EXIT + + print_section "$DOCUMENT" "APEX Analysis" "Processing Android Pony EXpress file" + + if ! command -v deapexer >/dev/null 2>&1; then + print_status "error" "deapexer tool not found" + echo " Please ensure Android SDK tools are properly installed and in your PATH." + exit 1 + fi + + dir_filename=$(basename "${dir}") + tmp=$(mktemp -d -t "${dir_filename%.apex}_out_XXXXX") + + if [ ! -d "${tmp}" ]; then + print_status "error" "Failed to create temporary directory" + exit 1 + fi + + print_status "processing" "Extracting APEX contents..." + if ! deapexer extract "${dir}" "${tmp}"; then + print_status "error" "Failed to extract APEX file" + cleanup_trap 1 + fi + + dir="${tmp}" +fi + +# Track libraries for enhanced summary +unaligned_libs=() +aligned_libs=() +critical_unaligned_libs=() +non_critical_unaligned_libs=() +total_libs=0 +critical_libs=0 + +print_section "$GEAR" "ELF Segment Analysis" "Scanning native libraries for 16KB alignment compliance" + +# Find all native libraries +matches="$(find "${dir}" -type f -name '*.so' 2>/dev/null)" +if [ -z "$matches" ]; then + # Also check for ELF files without .so extension + matches="$(find "${dir}" -type f -exec file {} \; 2>/dev/null | grep 'ELF' | cut -d: -f1)" +fi + +if [ -z "$matches" ]; then + print_summary_box "NO NATIVE LIBRARIES DETECTED" "success" \ + "${PARTY} Your app uses only Java/Kotlin code!" \ + "" \ + "${CHECK_MARK} No native library alignment issues to worry about" \ + "${CHECK_MARK} Already compatible with 16KB page size devices" \ + "${CHECK_MARK} Ready for Google Play's November 1st, 2025 requirement!" + cleanup_trap 0 +fi + +print_status "info" "Found native libraries - analyzing ELF segment alignment..." + +print_table_header + +IFS=$'\n' +for match in $matches; do + # Skip non-ELF files and nested packages + if [[ "${match}" == *".apk" ]]; then + continue + fi + if [[ "${match}" == *".apex" ]]; then + continue + fi + + # Verify it's actually an ELF file + if ! [[ $(file "${match}" 2>/dev/null) == *"ELF"* ]]; then + continue + fi + + total_libs=$((total_libs + 1)) + + # Determine architecture and criticality + arch="unknown" + is_critical="false" + if [[ "${match}" == *"arm64-v8a"* ]]; then + arch="arm64-v8a" + is_critical="true" + critical_libs=$((critical_libs + 1)) + elif [[ "${match}" == *"x86_64"* ]]; then + arch="x86_64" + is_critical="true" + critical_libs=$((critical_libs + 1)) + elif [[ "${match}" == *"armeabi-v7a"* ]]; then + arch="armeabi-v7a" + elif [[ "${match}" == *"x86"* ]]; then + arch="x86" + fi + + # Check ELF segment alignment + res="$(objdump -p "${match}" 2>/dev/null | grep LOAD | awk '{ print $NF }' | head -1)" + + if [ -z "$res" ]; then + print_table_row "$(basename "${match}")" "$arch" "UNKNOWN" "FAILED" "$is_critical" + continue + fi + + # Check if alignment meets 16KB requirement (2**14 or higher) + if [[ $res =~ 2\*\*(1[4-9]|[2-9][0-9]|[1-9][0-9]{2,}) ]]; then + print_table_row "$(basename "${match}")" "$arch" "$res" "ALIGNED" "$is_critical" + aligned_libs+=("${match}") + else + print_table_row "$(basename "${match}")" "$arch" "$res" "UNALIGNED" "$is_critical" + unaligned_libs+=("${match}") + + if [[ "$is_critical" == "true" ]]; then + critical_unaligned_libs+=("${match}") + else + non_critical_unaligned_libs+=("${match}") + fi + fi +done + +print_table_footer + +# Enhanced Summary Section +if [ ${#unaligned_libs[@]} -gt 0 ]; then + # Failure case - unaligned libraries found + summary_lines=( + "${ALERT} ACTION REQUIRED: Unaligned native libraries detected!" + "" + "Analysis Results:" + " • Total libraries scanned: $total_libs" + " • Critical architectures (64-bit): $critical_libs" + " • Libraries aligned: ${#aligned_libs[@]}" + " • Libraries UNALIGNED: ${#unaligned_libs[@]}" + ) + + if [ ${#critical_unaligned_libs[@]} -gt 0 ]; then + summary_lines+=(" • Critical failures: ${#critical_unaligned_libs[@]} (MUST FIX)") + fi + + if [ ${#non_critical_unaligned_libs[@]} -gt 0 ]; then + summary_lines+=(" • Non-critical warnings: ${#non_critical_unaligned_libs[@]} (SHOULD FIX)") + fi + + summary_lines+=("") + summary_lines+=("Google Play Compliance: FAILED") + summary_lines+=("Deadline: November 1st, 2025") + + print_summary_box "COMPATIBILITY CHECK FAILED" "error" "${summary_lines[@]}" + + # Detailed library breakdown + if [ ${#critical_unaligned_libs[@]} -gt 0 ]; then + print_subsection "Critical Libraries Requiring Immediate Attention" + for lib in "${critical_unaligned_libs[@]}"; do + arch_type="64-bit" + [[ "${lib}" == *"arm64-v8a"* ]] && arch_type="ARM64" + [[ "${lib}" == *"x86_64"* ]] && arch_type="x86_64" + print_status "error" "$(basename "$lib")" "$arch_type architecture - Required for Google Play" + done + fi + + if [ ${#non_critical_unaligned_libs[@]} -gt 0 ]; then + print_subsection "Non-Critical Libraries (Recommended to Fix)" + for lib in "${non_critical_unaligned_libs[@]}"; do + arch_type="32-bit" + [[ "${lib}" == *"armeabi-v7a"* ]] && arch_type="ARMv7" + [[ "${lib}" == *"x86"* ]] && arch_type="x86" + print_status "warning" "$(basename "$lib")" "$arch_type architecture" + done + fi + + # Comprehensive fix instructions + print_section "$TOOLS" "How to Fix Unaligned Libraries" "Step-by-step guide to achieve 16KB compatibility" + + print_subsection "Step 1: Update Your Build Environment" + print_status "info" "Android Gradle Plugin (AGP)" "Upgrade to version 8.5.1 or higher" + print_status "info" "Android NDK" "Update to NDK r27 or higher (r28+ recommended)" + print_status "info" "Build Tools" "Ensure Android SDK build-tools 35.0.0+ for zipalign" + + print_subsection "Step 2: Configure 16KB ELF Alignment" + echo + echo -e "${BOLD}For NDK r28 and newer:${ENDCOLOR}" + print_status "success" "Automatic Support" "16KB alignment enabled by default - no changes needed!" + + echo + echo -e "${BOLD}For NDK r27:${ENDCOLOR}" + print_status "info" "Gradle Configuration" "Add to your app/build.gradle:" + echo -e "${DIM} android {" + echo -e " defaultConfig {" + echo -e " externalNativeBuild {" + echo -e " cmake {" + echo -e " arguments '-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON'" + echo -e " }" + echo -e " }" + echo -e " }" + echo -e " }${ENDCOLOR}" + + print_status "info" "NDK-Build Configuration" "Add to Application.mk:" + echo -e "${DIM} APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true${ENDCOLOR}" + + echo + echo -e "${BOLD}For NDK r26 and older:${ENDCOLOR}" + print_status "info" "Manual Linker Flags" "Add to your native build configuration:" + echo -e "${DIM} # For Android.mk:" + echo -e " LOCAL_LDFLAGS += \"-Wl,-z,max-page-size=16384\"" + echo + echo -e " # For CMakeLists.txt:" + echo -e " target_link_options(\${CMAKE_PROJECT_NAME} PRIVATE \"-Wl,-z,max-page-size=16384\")${ENDCOLOR}" + + print_subsection "Step 3: Update Library Packaging" + print_status "info" "AGP 8.5.1+" "Uncompressed libraries used by default (no changes needed)" + print_status "info" "Older AGP versions" "Add to app/build.gradle:" + echo -e "${DIM} android {" + echo -e " packagingOptions {" + echo -e " jniLibs {" + echo -e " useLegacyPackaging = true" + echo -e " }" + echo -e " }" + echo -e " }${ENDCOLOR}" + + print_subsection "Step 4: Build and Verify" + print_status "processing" "Clean Build" "Run ./gradlew clean" + print_status "processing" "Rebuild Project" "Run ./gradlew assembleRelease" + print_status "processing" "Re-run This Script" "Verify all libraries are now aligned" + print_status "processing" "Test on Device" "Use Android 15 emulator with 16KB system image" + + print_section "$BOOKS" "Additional Resources" "Learn more about 16KB page size support" + print_status "info" "Official Guide" "https://developer.android.com/guide/practices/page-sizes" + print_status "info" "NDK Documentation" "https://developer.android.com/ndk/guides/" + print_status "info" "Testing Guide" "Use 'adb shell getconf PAGE_SIZE' (should return 16384)" + print_status "info" "Emulator Setup" "Android 15 system images with 16KB page size" + + final_message="" + if [ ${#critical_unaligned_libs[@]} -gt 0 ]; then + final_message="CRITICAL: Fix required for Google Play compliance by November 1st, 2025!" + else + final_message="Warning: Non-critical issues found - recommended to fix for complete compatibility" + fi + + print_summary_box "NEXT STEPS" "error" \ + "$final_message" \ + "" \ + "1. Update your build tools and NDK version" \ + "2. Apply the configuration changes above" \ + "3. Clean and rebuild your project" \ + "4. Run this script again to verify fixes" \ + "5. Test thoroughly on 16KB devices/emulator" + + cleanup_trap 1 +else + # Success case - all libraries aligned + summary_lines=( + "${PARTY} All native libraries are properly aligned!" + "" + "Analysis Results:" + " • Total libraries scanned: $total_libs" + " • Critical architectures: $critical_libs" + " • All libraries: ${#aligned_libs[@]}/${total_libs} ALIGNED" + "" + "Google Play Compliance: PASSED ${CHECK_MARK}" + "Ready for November 1st, 2025 deadline!" + ) + + print_summary_box "COMPATIBILITY CHECK PASSED" "success" "${summary_lines[@]}" + + print_section "$ROCKET" "Next Steps" "Ensure complete 16KB compatibility" + + print_status "success" "Library Alignment" "All ELF segments properly aligned for 16KB pages" + print_status "info" "Runtime Testing" "Test on Android 15 emulator with 16KB system image" + print_status "info" "Code Review" "Check for hardcoded PAGE_SIZE dependencies in your code" + print_status "info" "Third-party SDKs" "Verify all dependencies are 16KB compatible" + print_status "info" "Zipalign Verification" "Run: zipalign -c -P 16 -v 4 your-app.apk" + + print_subsection "Testing Commands" + echo -e "${DIM} # Verify page size on device/emulator${ENDCOLOR}" + echo -e "${DIM} adb shell getconf PAGE_SIZE # Should return: 16384${ENDCOLOR}" + echo + echo -e "${DIM} # Verify APK alignment${ENDCOLOR}" + echo -e "${DIM} zipalign -c -P 16 -v 4 app-release.apk${ENDCOLOR}" + + print_summary_box "CONGRATULATIONS!" "success" \ + "${PARTY} Your app is 16KB page size compatible!" \ + "" \ + "${CHECK_MARK} All native libraries meet Google Play requirements" \ + "${CHECK_MARK} Ready for devices with 16KB page sizes" \ + "${CHECK_MARK} Compliant with November 1st, 2025 deadline" \ + "" \ + "Recommendation: Test thoroughly on 16KB environment to ensure" \ + "no runtime issues exist in your application code." +fi + +echo +print_status "info" "Analysis completed at $(date)" +echo diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c569640..391a902b 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6aac74dd..fe5b6ee4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -12,6 +12,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 76AE113C28E078EA00513112 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 76AE113B28E078EA00513112 /* GoogleService-Info.plist */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 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 */; }; @@ -39,6 +40,7 @@ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7627313328E45A2400F354F7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 76AE113B28E078EA00513112 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; 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 = ""; }; @@ -57,6 +59,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 1CCF895CFB3C95DEBB84A05F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -75,6 +78,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -144,13 +148,16 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, C4CFBC4C5CAC00182015ACD5 /* [CP] Embed Pods Frameworks */, - EF29CE25BB57F5AA48CE1136 /* [CP] Copy Pods Resources */, + D9FFC0F21D4146458615A992 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -179,6 +186,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -242,23 +252,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 6C6E76E4FB19FAF11C24005D /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -291,6 +284,23 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + D9FFC0F21D4146458615A992 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -367,7 +377,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -454,7 +464,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -504,7 +514,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -600,6 +610,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9c12df59..5db441f5 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -57,10 +78,10 @@ This App requires access to your location to mark your attendance. NSMicrophoneUsageDescription This app requires microphone access for calls and speech-to-text input. - NSSpeechRecognitionUsageDescription - This app requires speech recognition to convert your voice to text. NSPhotoLibraryUsageDescription This app requires photo library access to select image as document & upload it. + NSSpeechRecognitionUsageDescription + This app requires speech recognition to convert your voice to text. UIApplicationSupportsIndirectInputEvents UIBackgroundModes diff --git a/lib/api/dashboard_api_client.dart b/lib/api/dashboard_api_client.dart index fa4d00a2..39be599f 100644 --- a/lib/api/dashboard_api_client.dart +++ b/lib/api/dashboard_api_client.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:http/http.dart' as http; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:mobile_device_identifier/mobile_device_identifier.dart'; import 'package:mohem_flutter_app/api/api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; @@ -254,7 +253,7 @@ class DashboardApiClient { String QRValue = "", String payrollCode = "", }) async { - var _mobileDeviceIdentifierPlugin = MobileDeviceIdentifier(); + Codec stringToBase64 = utf8.fuse(base64); String url = "${ApiConsts.swpRest}AuthenticateAndSwipeUserSupportNFC"; var uuid = Uuid(); @@ -262,7 +261,7 @@ class DashboardApiClient { // Generate a v4 (random) id if (Platform.isAndroid) { AndroidDeviceInfo androidInfo = await DeviceInfoPlugin().androidInfo; - deviceId = await _mobileDeviceIdentifierPlugin.getDeviceId() ?? androidInfo.id; + deviceId = await androidInfo.id; deviceId = stringToBase64.encode(deviceId); deviceId = deviceId.length >= 50 ? deviceId.substring(0, 48) : deviceId; } else if (Platform.isIOS) { diff --git a/lib/classes/date_uitl.dart b/lib/classes/date_uitl.dart index 8714b402..55f371b6 100644 --- a/lib/classes/date_uitl.dart +++ b/lib/classes/date_uitl.dart @@ -22,7 +22,7 @@ class DateUtil { return DateTime.now(); } - static DateTime convertStringToDate(String date) { + static DateTime convertStringToDate(String? date) { // /Date(1585774800000+0300)/ if (date != null) { const start = "/Date("; @@ -34,8 +34,9 @@ class DateUtil { date.substring(startIndex + start.length, endIndex), ), ); - } else + } else { return DateTime.now(); + } } static DateTime convertSimpleStringDateToDate(String date, {bool isITG = false}) { diff --git a/lib/classes/utils.dart b/lib/classes/utils.dart index 6a9b16bc..8843414d 100644 --- a/lib/classes/utils.dart +++ b/lib/classes/utils.dart @@ -20,7 +20,9 @@ import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; import 'package:mohem_flutter_app/widgets/loading_dialog.dart'; import 'package:nfc_manager/nfc_manager.dart'; -import 'package:nfc_manager/platform_tags.dart'; +import 'package:nfc_manager/nfc_manager_android.dart'; +import 'package:nfc_manager/nfc_manager_ios.dart'; +// import 'package:nfc_manager/platform_tags.dart'; import 'package:shared_preferences/shared_preferences.dart'; // ignore_for_file: avoid_annotating_with_dynamic @@ -349,15 +351,34 @@ class Utils { NfcManager.instance .startSession( onDiscovered: (NfcTag tag) async { - MifareUltralight f; + // MifareUltralight f; + // if (Platform.isAndroid) { + // print(tag); + // f = MifareUltralight(tag: tag, identifier: tag.data["nfca"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22); + // } else { + // f = MifareUltralight(tag: tag, identifier: tag.data["mifare"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22); + // } + + Uint8List? id; + if (Platform.isAndroid) { - print(tag); - f = MifareUltralight(tag: tag, identifier: tag.data["nfca"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22); - } else { - f = MifareUltralight(tag: tag, identifier: tag.data["mifare"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22); + id = NfcTagAndroid.from(tag)?.id; + } else if (Platform.isIOS) { + id = MiFareIos.from(tag)?.identifier; + } + + if (id == null) { + await NfcManager.instance.stopSession(); + return; } - String identifier = f.identifier.map((e) => e.toRadixString(16).padLeft(2, '0')).join(''); - NfcManager.instance.stopSession(); + + String identifier = + id.map((e) => e.toRadixString(16).padLeft(2, '0')).join(''); + print(identifier); + + await NfcManager.instance.stopSession(); + // String identifier = f.identifier.map((e) => e.toRadixString(16).padLeft(2, '0')).join(''); + // NfcManager.instance.stopSession(); onRead(identifier); }, pollingOptions: {NfcPollingOption.iso14443}, diff --git a/lib/generated_plugin_registrant.dart b/lib/generated_plugin_registrant.dart index 213926c6..6a0ba5bd 100644 --- a/lib/generated_plugin_registrant.dart +++ b/lib/generated_plugin_registrant.dart @@ -8,7 +8,6 @@ import 'package:audio_session/audio_session_web.dart'; import 'package:camera_web/camera_web.dart'; -import 'package:file_picker/_internal/file_picker_web.dart'; import 'package:firebase_core_web/firebase_core_web.dart'; import 'package:firebase_messaging_web/firebase_messaging_web.dart'; import 'package:fluttertoast/fluttertoast_web.dart'; @@ -27,7 +26,6 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart'; void registerPlugins(Registrar registrar) { AudioSessionWeb.registerWith(registrar); CameraPlugin.registerWith(registrar); - FilePickerWeb.registerWith(registrar); FirebaseCoreWeb.registerWith(registrar); FirebaseMessagingWeb.registerWith(registrar); FluttertoastWebPlugin.registerWith(registrar); diff --git a/lib/main.dart b/lib/main.dart index 263ed8ec..8cd7d66c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,8 @@ import 'package:month_year_picker/month_year_picker.dart'; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'package:sizer/sizer.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb; // test uat account // username 199067 @@ -75,32 +77,33 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Sizer( - builder: (BuildContext context, Orientation orientation, DeviceType deviceType) { + builder: (BuildContext context, Orientation orientation, ScreenType deviceType){ PostParamsModel? obj = AppState().postParamsObject; - obj?.languageID = EasyLocalization.of(context)?.locale.languageCode == "ar" ? 1 : 2; - AppState().setPostParamsModel(obj!); - List> delegates = context.localizationDelegates; - // delegates.add(GlobalMaterialLocalizations.delegate); - delegates.add(MonthYearPickerLocalizations.delegate); - return SafeArea( - top: false, // Set to true if you want to avoid the notch area as well - bottom: Platform.isIOS ? false : true, - child: MaterialApp( - navigatorKey: AppRoutes.navigatorKey, - builder: (BuildContext context, Widget? child) { - return MediaQuery(data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), child: child!); - }, - theme: AppTheme.getTheme(EasyLocalization.of(context)?.locale.languageCode == "ar"), - debugShowCheckedModeBanner: false, - localizationsDelegates: delegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - initialRoute: AppRoutes.initialRoute, - routes: AppRoutes.routes, - ), - ); - }, + obj?.languageID = EasyLocalization.of(context)?.locale.languageCode == "ar" ? 1 : 2; + AppState().setPostParamsModel(obj!); + List> delegates = context.localizationDelegates; + // delegates.add(GlobalMaterialLocalizations.delegate); + delegates.add(MonthYearPickerLocalizations.delegate); + return SafeArea( + top: false, // Set to true if you want to avoid the notch area as well + bottom: Platform.isIOS ? false : true, + child: MaterialApp( + navigatorKey: AppRoutes.navigatorKey, + builder: (BuildContext context, Widget? child) { + return MediaQuery(data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), child: child!); + }, + theme: AppTheme.getTheme(EasyLocalization.of(context)?.locale.languageCode == "ar"), + debugShowCheckedModeBanner: false, + localizationsDelegates: delegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + initialRoute: AppRoutes.initialRoute, + routes: AppRoutes.routes, + ), + ); + } ); + } } diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 4c19817b..232df220 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -1609,13 +1609,13 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { await File('$dirPath/.nomedia').create(); } path = "$dirPath/${AppState().chatDetails!.response!.id}-$receiverID-${DateTime.now().microsecondsSinceEpoch}.aac"; - recorderController = RecorderController() - ..androidEncoder = AndroidEncoder.aac - ..androidOutputFormat = AndroidOutputFormat.mpeg4 - ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC - ..sampleRate = 6000 - ..updateFrequency = const Duration(milliseconds: 100) - ..bitRate = 18000; + recorderController = RecorderController(); + // ..androidEncoder = AndroidEncoder.aac + // ..androidOutputFormat = AndroidOutputFormat.mpeg4 + // ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC + // ..sampleRate = 6000 + // ..updateFrequency = const Duration(milliseconds: 100) + // ..bitRate = 18000; playerController = PlayerController(); } diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index f2286006..d2d9fab7 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; diff --git a/lib/ui/login/verify_last_login_screen.dart b/lib/ui/login/verify_last_login_screen.dart index 5c760925..36425aa2 100644 --- a/lib/ui/login/verify_last_login_screen.dart +++ b/lib/ui/login/verify_last_login_screen.dart @@ -112,7 +112,7 @@ class _VerifyLastLoginScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, - children: [LocaleKeys.lastLoginDetails.tr().toText16(), DateUtil.formatDateToDate(DateUtil.convertStringToDate(mobileLoginInfoListModel!.editedOn ?? DateTime.now().toString()), false).toText12()], + children: [LocaleKeys.lastLoginDetails.tr().toText16(), DateUtil.formatDateToDate(DateUtil.convertStringToDate(mobileLoginInfoListModel?.editedOn ?? null), false).toText12()], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -121,7 +121,7 @@ class _VerifyLastLoginScreenState extends State { LocaleKeys.verificationType.tr().toText10(color: MyColors.grey57Color), getVerificationType(mobileLoginInfoListModel!.loginType!).toText12(), const Expanded(child: SizedBox()), - DateUtil.formatDateToTime(DateUtil.convertStringToDate(mobileLoginInfoListModel!.editedOn!)).toText12(), + DateUtil.formatDateToTime(DateUtil.convertStringToDate(mobileLoginInfoListModel?.editedOn)).toText12(), ], ), ], @@ -208,7 +208,7 @@ class _VerifyLastLoginScreenState extends State { Future loginWithFaceIDAndBiometrics() async { - IOSAuthMessages iosStrings = const IOSAuthMessages(cancelButton: 'cancel', goToSettingsButton: 'settings', goToSettingsDescription: 'Please set up your Touch ID.', lockOut: 'Please reenable your Touch ID'); + IOSAuthMessages iosStrings = const IOSAuthMessages(cancelButton: 'cancel'); // IOSAuthMessages iosStrings = const IOSAuthMessages( // cancelButton: 'cancel', @@ -219,9 +219,7 @@ class _VerifyLastLoginScreenState extends State { bool authenticated = false; try { - authenticated = await auth.authenticate(localizedReason: 'Scan your fingerprint to authenticate', options: const AuthenticationOptions( - useErrorDialogs: true, stickyAuth: true, biometricOnly: true, - ), + authenticated = await auth.authenticate(localizedReason: 'Scan your fingerprint to authenticate', authMessages: [ iosStrings, const AndroidAuthMessages(), diff --git a/lib/ui/login/verify_login_screen.dart b/lib/ui/login/verify_login_screen.dart index e0015e0e..625db60f 100644 --- a/lib/ui/login/verify_login_screen.dart +++ b/lib/ui/login/verify_login_screen.dart @@ -515,11 +515,11 @@ class _VerifyLoginScreenState extends State { Future loginWithFaceIDAndBiometrics() async { IOSAuthMessages iosStrings = - const IOSAuthMessages(cancelButton: 'cancel', goToSettingsButton: 'settings', goToSettingsDescription: 'Please set up your Touch ID.', lockOut: 'Please reenable your Touch ID'); + const IOSAuthMessages(cancelButton: 'cancel',); bool authenticated = false; try { authenticated = await auth.authenticate(localizedReason: 'Scan your fingerprint to authenticate', - options: const AuthenticationOptions(useErrorDialogs: true, stickyAuth: true, biometricOnly: true,), authMessages: [ + authMessages: [ iosStrings, const AndroidAuthMessages(), ],); diff --git a/lib/ui/marathon/widgets/question_card.dart b/lib/ui/marathon/widgets/question_card.dart index 0965c9d5..28395d11 100644 --- a/lib/ui/marathon/widgets/question_card.dart +++ b/lib/ui/marathon/widgets/question_card.dart @@ -30,18 +30,21 @@ class QuestionCard extends StatelessWidget { width: double.infinity, child: AppinioSwiper( duration: const Duration(milliseconds: 400), - padding: EdgeInsets.zero, + swipeOptions: const SwipeOptions.all(), isDisabled: true, controller: provider.swiperController, - unswipe: (int index, AppinioSwiperDirection direction) {}, - onSwipe: (int index, AppinioSwiperDirection direction) {}, - cards: provider.cardContentList, + cardCount: provider.cardContentList.length, + cardBuilder: (BuildContext context, int index) { + return provider.cardContentList[index]; + }, ), ), ); } } + + class CardContent extends StatelessWidget { const CardContent({Key? key}) : super(key: key); diff --git a/lib/ui/my_team/subordinate_leave.dart b/lib/ui/my_team/subordinate_leave.dart index e2fb9995..85e85c0e 100644 --- a/lib/ui/my_team/subordinate_leave.dart +++ b/lib/ui/my_team/subordinate_leave.dart @@ -97,8 +97,8 @@ class _SubordinateLeaveState extends State { separatorBuilder: (BuildContext cxt, int index) => 12.height, itemCount: getSubordinatesLeavesTotalList.length, itemBuilder: (BuildContext context, int index) { - var diffDays = DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATEEND!) - .difference(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATESTART!)) + var diffDays = DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATEEND) + .difference(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATESTART)) .inDays + 1; return Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -109,8 +109,8 @@ class _SubordinateLeaveState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ getSubordinatesLeavesTotalList[index].eMPLOYEENAME!.toText16(), - ItemDetailView(LocaleKeys.from.tr(), DateUtil.getFormattedDate(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATESTART!), "MMM dd yyyy")), - ItemDetailView(LocaleKeys.to.tr(), DateUtil.getFormattedDate(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATEEND!), "MMM dd yyyy")), + ItemDetailView(LocaleKeys.from.tr(), DateUtil.getFormattedDate(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATESTART), "MMM dd yyyy")), + ItemDetailView(LocaleKeys.to.tr(), DateUtil.getFormattedDate(DateUtil.convertStringToDate(getSubordinatesLeavesTotalList[index].dATEEND), "MMM dd yyyy")), ItemDetailView(LocaleKeys.numberDays.tr(), diffDays.toString()), ], ).expanded diff --git a/lib/ui/screens/announcements/announcement_details.dart b/lib/ui/screens/announcements/announcement_details.dart index 210a8f7c..a8cbcac6 100644 --- a/lib/ui/screens/announcements/announcement_details.dart +++ b/lib/ui/screens/announcements/announcement_details.dart @@ -2,7 +2,8 @@ import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +// import 'package:flutter_html/flutter_html.dart'; import 'package:mohem_flutter_app/api/pending_transactions_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; @@ -59,8 +60,8 @@ class _AnnouncementDetailsState extends State { ), ), ), - Html( - data: AppState().isArabic(context) ? getAnnouncementDetailsObj?.bodyAR : getAnnouncementDetailsObj?.bodyEN, + HtmlWidget( + AppState().isArabic(context) ? getAnnouncementDetailsObj?.bodyAR??"" : getAnnouncementDetailsObj?.bodyEN ??"", ).paddingOnly(top: 12), ], ).objectContainerView().paddingAll(21), diff --git a/lib/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart b/lib/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart index 44b3833d..f1f8cda3 100644 --- a/lib/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart +++ b/lib/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart @@ -190,9 +190,9 @@ class _MowadhafhiHRRequestState extends State { title.toText16().expanded, 6.width, SimpleButton(LocaleKeys.add.tr(), () async { - FilePickerResult? result = await FilePicker.platform.pickFiles(allowMultiple: true); + List? result = await FilePicker.pickFiles(); if (result != null) { - attachmentFiles = attachmentFiles + result.paths.map((path) => File(path!)).toList(); + attachmentFiles = attachmentFiles + result.map((path) => File(path.uri.toFilePath())).toList(); attachmentFiles = attachmentFiles.toSet().toList(); setState(() {}); } diff --git a/lib/ui/screens/my_documents/my_document_detail_screen.dart b/lib/ui/screens/my_documents/my_document_detail_screen.dart index ec975862..86ebd1ca 100644 --- a/lib/ui/screens/my_documents/my_document_detail_screen.dart +++ b/lib/ui/screens/my_documents/my_document_detail_screen.dart @@ -92,17 +92,22 @@ class _MyDocumentDetailScreenState extends State { ), 12.height, DottedBorder( - borderType: BorderType.RRect, - radius: const Radius.circular(10), - padding: const EdgeInsets.all(12), - dashPattern: const [2, 1], - color: MyColors.selectedBorderColor, + options: RoundedRectDottedBorderOptions( + radius: const Radius.circular(10), + padding: const EdgeInsets.all(12), + dashPattern: const [2, 1], + color: MyColors.selectedBorderColor, + ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Transform.rotate( angle: 45, - child: const Icon(Icons.attach_file_rounded, size: 16, color: MyColors.selectedBorderColor), + child: const Icon( + Icons.attach_file_rounded, + size: 16, + color: MyColors.selectedBorderColor, + ), ), 4.width, "Attach Image".toText14(color: MyColors.selectedBorderColor), diff --git a/lib/ui/screens/my_requests/my_requests.dart b/lib/ui/screens/my_requests/my_requests.dart index 4d16219a..12410ae7 100644 --- a/lib/ui/screens/my_requests/my_requests.dart +++ b/lib/ui/screens/my_requests/my_requests.dart @@ -67,10 +67,10 @@ class _MyRequestsState extends State { isReadOnly: false, ).paddingOnly(bottom: 12), itemBuilder: (_) => >[ - for (int i = 0; i < getConcurrentProgramsList!.length; i++) PopupMenuItem(child: Text(getConcurrentProgramsList![i].uSERCONCURRENTPROGRAMNAME!), value: i), + for (int i = 0; i < getConcurrentProgramsList.length; i++) PopupMenuItem(child: Text(getConcurrentProgramsList[i].uSERCONCURRENTPROGRAMNAME!), value: i), ], onSelected: (int popupIndex) { - selectedConcurrentProgramList = getConcurrentProgramsList![popupIndex]; + selectedConcurrentProgramList = getConcurrentProgramsList[popupIndex]; getCCPTransactions(selectedConcurrentProgramList?.cONCURRENTPROGRAMNAME); setState(() {}); }), @@ -105,7 +105,7 @@ class _MyRequestsState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ("Request ID: " + getCCPTransactionsList[index].rEQUESTID.toString()).toText12(color: MyColors.grey57Color), - DateUtil.formatDateToDate(DateUtil.convertStringToDate(getCCPTransactionsList[index].rEQUESTDATE!), false).toText12(color: MyColors.grey70Color), + DateUtil.formatDateToDate(DateUtil.convertStringToDate(getCCPTransactionsList[index].rEQUESTDATE), false).toText12(color: MyColors.grey70Color), ], ), Container( diff --git a/lib/ui/screens/offers_and_discounts/offers_and_discounts_details.dart b/lib/ui/screens/offers_and_discounts/offers_and_discounts_details.dart index be0582ca..890d1a46 100644 --- a/lib/ui/screens/offers_and_discounts/offers_and_discounts_details.dart +++ b/lib/ui/screens/offers_and_discounts/offers_and_discounts_details.dart @@ -6,8 +6,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; @@ -16,7 +16,7 @@ import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; +// import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; class OffersAndDiscountsDetails extends StatefulWidget { @@ -68,10 +68,12 @@ class _OffersAndDiscountsDetailsState extends State { AppState().isArabic(context) ? getOffersList[0].titleAr!.toText22(isBold: true, color: const Color(0xff2B353E)).center : getOffersList[0].titleEn!.toText22(isBold: true, color: const Color(0xff2B353E)).center, - Html( - data: AppState().isArabic(context) ? getOffersList[0].descriptionAr! : getOffersList[0].descriptionEn ?? "", - onLinkTap: (String? url, Map attributes, _) { + HtmlWidget( + AppState().isArabic(context) ? getOffersList[0].descriptionAr! : getOffersList[0].descriptionEn ?? "", + onTapUrl: (String? url) { launchUrl(Uri.parse(url!)); + + return true; }, ), checkDate(getOffersList[0].endDate!).paddingOnly(left: 8), @@ -122,7 +124,7 @@ class _OffersAndDiscountsDetailsState extends State { Directory tempDir = await getTemporaryDirectory(); File file = await File('${tempDir.path}/${DateTime.now().toString()}.png').create(); await file.writeAsBytes(pngBytes); - await SharePlus.instance.share(ShareParams(files: [(XFile(file.path))], text: AppState().isArabic(context) ? getOffersList[0].titleAr : getOffersList[0].titleEn)); + // await SharePlus.instance.share(ShareParams(files: [(XFile(file.path))], text: AppState().isArabic(context) ? getOffersList[0].titleAr : getOffersList[0].titleEn)); } catch (ex) { debugPrint(ex.toString()); } diff --git a/lib/ui/work_list/item_history_screen.dart b/lib/ui/work_list/item_history_screen.dart index 4bc65798..a8c3a756 100644 --- a/lib/ui/work_list/item_history_screen.dart +++ b/lib/ui/work_list/item_history_screen.dart @@ -211,7 +211,7 @@ class _ItemHistoryScreenState extends State { interval: 1, getTitlesWidget: (double value, TitleMeta meta) { return SideTitleWidget( - axisSide: meta.axisSide, + meta: meta, child: Text(reversedList[int.parse(meta.formattedValue)].cREATIONDATE!, style: TextStyle(fontSize: 10)), ); })), @@ -222,7 +222,7 @@ class _ItemHistoryScreenState extends State { interval: 1, getTitlesWidget: (double value, TitleMeta meta) { return SideTitleWidget( - axisSide: meta.axisSide, + meta: meta, child: Text(meta.formattedValue, style: TextStyle(fontSize: 10)), ); })), @@ -233,7 +233,7 @@ class _ItemHistoryScreenState extends State { reservedSize: 15, getTitlesWidget: (double value, TitleMeta meta) { return SideTitleWidget( - axisSide: meta.axisSide, + meta: meta, child: Text("", style: TextStyle(fontSize: 10)), ); })), diff --git a/lib/ui/work_list/work_list_screen.dart b/lib/ui/work_list/work_list_screen.dart index d659ea63..4f75edcf 100644 --- a/lib/ui/work_list/work_list_screen.dart +++ b/lib/ui/work_list/work_list_screen.dart @@ -472,7 +472,7 @@ class _WorkListScreenState extends State { 10.height, Row( children: [ - DateUtil.formatDateToDate(DateUtil.convertStringToDate(requestDetails.modifiedDate!), false).toText10(color: MyColors.lightTextColor).expanded, + DateUtil.formatDateToDate(DateUtil.convertStringToDate(requestDetails.modifiedDate), false).toText10(color: MyColors.lightTextColor).expanded, RotatedBox(quarterTurns: AppState().isArabic(context) ? 2 : 4, child: SvgPicture.asset("assets/images/arrow_next.svg", color: MyColors.darkIconColor)), ], ), diff --git a/lib/ui/work_list/worklist_fragments/info_fragments.dart b/lib/ui/work_list/worklist_fragments/info_fragments.dart index 541d3470..d6016d8c 100644 --- a/lib/ui/work_list/worklist_fragments/info_fragments.dart +++ b/lib/ui/work_list/worklist_fragments/info_fragments.dart @@ -750,7 +750,7 @@ class InfoFragment extends StatelessWidget { ItemDetailGrid( ItemDetailViewCol( LocaleKeys.requestDate.tr(), DateUtil.formatDateToDate( - DateUtil.convertStringToDate(data.paymentDetailsList![0].requestDate!), false) ?? ""), + DateUtil.convertStringToDate(data.paymentDetailsList![0].requestDate), false) ?? ""), ItemDetailViewCol( LocaleKeys.requesterEmpName.tr(), data.paymentDetailsList![0].requesterEmployeeName ?? ""), ), diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index a5d346f8..b85e80af 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -47,7 +47,7 @@ class ImageOptions { } }, onFilesTap: () async { - FilePickerResult? result = await FilePicker.platform.pickFiles( + List? result = await FilePicker.pickFiles( type: FileType.custom, allowedExtensions: [ 'jpg', @@ -63,8 +63,8 @@ class ImageOptions { 'zip', ], ); - List files = result!.paths.map((path) => File(path!)).toList(); - image(result.files.first.path.toString(), files.first); + List files = result!.map((path) => File(path.uri.toFilePath())).toList(); + image(result.first.path.toString(), files.first); }, ), ); diff --git a/lib/widgets/nfc/nfc_reader_sheet.dart b/lib/widgets/nfc/nfc_reader_sheet.dart index 5e0b62d3..136c504b 100644 --- a/lib/widgets/nfc/nfc_reader_sheet.dart +++ b/lib/widgets/nfc/nfc_reader_sheet.dart @@ -3,7 +3,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:nfc_manager/nfc_manager.dart'; -import 'package:nfc_manager/platform_tags.dart'; +import 'package:nfc_manager/nfc_manager_android.dart'; +import 'package:nfc_manager/nfc_manager_ios.dart'; +import 'dart:typed_data'; void showNfcReader(BuildContext context, {required Function(String? nfcId) onNcfScan}) { showModalBottomSheet( @@ -40,14 +42,23 @@ class _NfcLayoutState extends State { void initState() { super.initState(); - NfcManager.instance.startSession(onDiscovered: (NfcTag tag) async { - var f; + NfcManager.instance.startSession( pollingOptions: {NfcPollingOption.iso14443},onDiscovered: (NfcTag tag) async { + Uint8List? id; + if (Platform.isAndroid) { - f = MifareUltralight(tag: tag, identifier: tag.data["nfca"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22); - } else { - f = MifareUltralight(tag: tag, identifier: tag.data["mifare"]["identifier"], type: 2, maxTransceiveLength: 252, timeout: 22); + id = NfcTagAndroid.from(tag)?.id; + } else if (Platform.isIOS) { + id = MiFareIos.from(tag)?.identifier; + } + + if (id == null) { + await NfcManager.instance.stopSession(); + return; } - String identifier = f.identifier.map((e) => e.toRadixString(16).padLeft(2, '0')).join(''); + + String identifier = + id.map((e) => e.toRadixString(16).padLeft(2, '0')).join(''); + print(identifier); nfcId = identifier; setState(() { diff --git a/pubspec.yaml b/pubspec.yaml index 12343528..88f1f6ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,93 +38,98 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.4 path_provider: ^2.0.8 - injector: ^2.0.0 + injector: ^4.0.0 provider: ^6.0.1 easy_localization: ^3.0.0 http: ^1.4.0 - permission_handler: ^12.0.1 + permission_handler: ^13.0.2 flutter_svg: any - sizer: ^2.0.15 - local_auth: ^2.3.0 - fluttertoast: ^8.0.8 - syncfusion_flutter_calendar: ^29.1.38 + sizer: ^3.1.3 + local_auth: ^3.0.2 + fluttertoast: ^10.0.0 + syncfusion_flutter_calendar: ^35.1.37 # flutter_calendar_carousel: ^2.1.0 pie_chart: ^5.1.0 shared_preferences: ^2.0.12 - firebase_messaging: ^15.2.9 - shimmer: ^2.0.0 - logger: ^1.1.0 + firebase_messaging: ^16.7.0 + shimmer: ^4.0.0 + logger: ^2.8.0 flutter_countdown_timer: ^4.1.0 # platform_device_id_plus: ^1.0.7 - mobile_device_identifier: ^0.0.3 - device_info_plus: ^11.5.0 +# mobile_device_identifier: ^0.0.3 + device_info_plus: ^13.2.0 image_picker: ^1.1.2 - file_picker: ^8.3.1 - geolocator: ^9.0.2 + file_picker: ^13.1.0 + geolocator: ^14.1.1 month_year_picker: ^0.5.0+1 month_picker_dialog: ^6.0.3 # open_file: ^3.2.1 open_filex: ^4.6.0 - wifi_iot: ^0.3.19+1 - flutter_html: ^3.0.0-alpha.6 - mobile_scanner: ^6.0.10 + wifi_iot: ^0.4.0 +# flutter_html: ^3.0.0-alpha.6 + flutter_widget_from_html: ^0.17.4 + just_audio: ^0.10.6 + + +# mobile@1234 + mobile_scanner: ^7.4.2 # flutter_barcode_scanner: ^2.0.0 # qr_code_scanner: ^1.0.1 # qr_flutter: ^4.0.0 url_launcher: ^6.0.15 - share_plus: ^11.0.0 +# share_plus: ^13.3.0 flutter_rating_bar: ^4.0.1 auto_size_text: ^3.0.0 pull_to_refresh: ^2.0.0 - fl_chart: ^0.66.0 + fl_chart: ^1.2.0 # lottie json animations lottie: any # Marathon Card Swipe - appinio_swiper: ^1.1.1 + appinio_swiper: ^2.1.1 expandable: ^5.0.1 safe_device: ^1.1.9 # networkImage - cached_network_image: ^3.2.2 + cached_network_image: ^4.0.3 #Chat signalr_netcore: ^1.3.3 logging: ^1.0.1 swipe_to: ^1.0.2 - flutter_webrtc: ^0.12.11 + flutter_webrtc: ^1.6.2+hotfix.3 # camera: ^0.10.3 - camera: ^0.11.2 - flutter_local_notifications: ^18.0.1 + camera: ^0.12.1 + flutter_local_notifications: ^22.3.1 #firebase_analytics: any #Chat Voice Message Recoding & Play - audio_waveforms: ^1.3.0 - rxdart: ^0.27.7 + audio_waveforms: ^2.0.2 + rxdart: ^0.28.0 #Encryption - flutter_des: ^2.1.0 + flutter_des: ^3.0.0 - video_player: 2.7.0 - just_audio: ^0.9.30 + video_player: ^2.14.0 +# just_audio: ^0.9.30 # safe_device: ^1.1.2 flutter_layout_grid: ^2.0.1 #Huawei Dependencies # huawei_hmsavailability: ^6.6.0+300 # huawei_location: 6.0.0+302 - huawei_location: ^6.14.2+301 + huawei_location: ^6.17.0+300 # huawei_push: ^6.7.0+300 - huawei_map: - git: - url: https://github.com/fleoparra/hms-flutter-plugin.git - path: flutter-hms-map - huawei_push: - git: - url: https://github.com/crasowas/hms-flutter-plugin.git - path: flutter-hms-push - firebase_crashlytics: ^4.3.9 +# huawei_map: +# git: +# url: https://github.com/fleoparra/hms-flutter-plugin.git +# path: flutter-hms-map + huawei_push: ^6.15.0+300 + # git: +# url: https://github.com/crasowas/hms-flutter-plugin.git +# path: flutter-hms-push + firebase_crashlytics: ^5.4.0 #Items for sale Image Carousel Slider carousel_slider: ^5.1.1 @@ -132,15 +137,15 @@ dependencies: #Huawei Specified # store_checker: ^1.1.0 google_api_availability: ^5.0.1 - google_maps_flutter_web: ^0.5.4 - in_app_update: 4.1.0 + google_maps_flutter_web: ^0.6.4+1 + in_app_update: 5.0.0 flutter_inappwebview: ^6.1.5 - flutter_blurhash: ^0.8.0 + flutter_blurhash: ^0.9.1 #todo its for temporary purpose, later will remove this. - dotted_border: ^2.0.0+3 + dotted_border: ^3.1.0 webview_flutter: ^4.13.0 - nfc_manager: ^3.2.0 + nfc_manager: ^4.2.1 speech_to_text: ^7.3.0 @@ -161,7 +166,7 @@ dev_dependencies: # 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: ^1.0.4 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From 9dcbb0d855e56d0d225739405e2265f46a96aa3d Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 5 Oct 2026 13:53:17 +0300 Subject: [PATCH 7/8] changes for Latest AGP --- android/app/build.gradle | 2 +- android/gradle.properties | 9 +++++++-- android/gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 4 ++-- lib/classes/consts.dart | 5 +++-- lib/theme/app_theme.dart | 1 + pubspec.yaml | 12 ++++++------ 7 files changed, 21 insertions(+), 14 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1901f3ac..97746d6a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,7 +33,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "hmg.cloudSolutions.mohem" - minSdkVersion 30 + minSdkVersion 28 targetSdk 35 targetSdk = flutter.targetSdkVersion versionCode flutter.versionCode diff --git a/android/gradle.properties b/android/gradle.properties index 8df3fe98..05625607 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,7 +1,12 @@ -org.gradle.jvmargs=-Xmx4096m +#org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:MaxMetaspaceSize=1024m +org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Xmx4096m -Dfile.encoding=UTF-8 kotlin.daemon.jvmargs=-Xmx4096m android.useAndroidX=true android.enableJetifier=true apmsInstrumentationEnabled=false -#org.gradle.java.home=/Users/amirs/Library/Java/JavaVirtualMachines/jbr-17.0.12/Contents/Home/ \ No newline at end of file +#org.gradle.java.home=/Users/amirs/Library/Java/JavaVirtualMachines/jbr-17.0.12/Contents/Home/ +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 941ceb11..5700695c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jul 08 11:08:34 AST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/settings.gradle b/android/settings.gradle index 10a4a8da..3fc0ddba 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -30,8 +30,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.11.0' apply false - id("org.jetbrains.kotlin.android") version "2.2.0" apply false + id "com.android.application" version '8.11.1' apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false id("com.google.gms.google-services") version "4.4.3" apply false id("com.google.firebase.crashlytics") version "3.0.4" apply false id('org.gradle.toolchains.foojay-resolver-convention') version '0.9.0' apply false diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 5e980a90..d665dbd4 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -9,12 +9,13 @@ class ApiConsts { // static String baseUrl = "https://webservices.hmg.com"; // PreProd // static String baseUrl = "https://hmgwebservices.com"; // Live server - // static String baseUrl = "https://mohemm.hmg.com"; // New Live server + static String baseUrl = "https://mohemm.hmg.com"; // New Live server static String ssoBaseUrl = "https://sso.hmg.com"; // New Live server // // static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver + // static String baseUrl = "http://10.201.214.101:2027/"; // static String baseUrl = "http://10.20.200.111:1010/"; - static String baseUrl = "https://mohemm.hmg.com"; // New Live server + // static String baseUrl = "https://mohemm.hmg.com"; // New Live server //static String eLearningBaseUrl = "https://elearning.hmg.com/moodle_dev/api/"; // Elearning server UAT static String eLearningBaseUrl = "https://elearning.hmg.com/moodle/api/"; // Elearning server live static String enSATLiveURL = "https://ensat.hmg.com/auth/autologin"; // ENSAT server live diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index f2286006..d2d9fab7 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index a4eda02b..e1dc8827 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,8 +17,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.7.2+300082 -#version: 3.9.3+1 +version: 3.7.7+300087 +#version: 3.9.8+1 environment: sdk: ^3.7.0 @@ -65,8 +65,8 @@ dependencies: month_year_picker: ^0.5.0+1 month_picker_dialog: ^6.0.3 # open_file: ^3.2.1 - open_filex: ^4.6.0 - wifi_iot: ^0.3.19+1 + open_filex: ^4.7.0 + wifi_iot: ^0.4.0 flutter_html: ^3.0.0-alpha.6 mobile_scanner: ^6.0.10 # flutter_barcode_scanner: ^2.0.0 @@ -94,7 +94,7 @@ dependencies: swipe_to: ^1.0.2 flutter_webrtc: ^0.12.11 # camera: ^0.10.3 - camera: ^0.11.2 + camera: ^0.12.1 flutter_local_notifications: ^18.0.1 #firebase_analytics: any @@ -137,7 +137,7 @@ dependencies: # store_checker: ^1.1.0 google_api_availability: ^5.0.1 google_maps_flutter_web: ^0.5.4 - in_app_update: 4.1.0 + in_app_update: ^5.0.0 flutter_inappwebview: ^6.1.5 flutter_blurhash: ^0.8.0 From 4baf5f9389465aea2962a670401c9f7e1f5e9d19 Mon Sep 17 00:00:00 2001 From: Haroon Amjad <> Date: Mon, 5 Oct 2026 15:30:46 +0300 Subject: [PATCH 8/8] Update dependencies and remove unused keys from localization --- lib/generated/codegen_loader.g.dart | 2 -- lib/models/generic_response_model.dart | 1 - pubspec.yaml | 6 +++--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index a35a0b12..4c96539f 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -669,7 +669,6 @@ class CodegenLoader extends AssetLoader{ "noCoursesAvailable": "لا توجد دورات متاحة", "pullToRefresh": "اسحب للتحديث", "courseUrlNotAvailable": "رابط الدورة غير متوفر", - "buyerDetails": "تفاصيل المشتري", "etqanOVR": "الإبلاغ عن حادثة", "etqanRequest": "طلبات الحوادث", "selectProject": "حدد المشروع", @@ -1357,7 +1356,6 @@ static const Map _en_US = { "noCoursesAvailable": "No Courses Available", "pullToRefresh": "Pull to refresh", "courseUrlNotAvailable": "Course URL not available", - "buyerDetails": "Buyer Details", "etqanOVR": "Report An Incident", "etqanRequest": "Incident Requests", "selectProject": "Select Project", diff --git a/lib/models/generic_response_model.dart b/lib/models/generic_response_model.dart index ff89a138..1c1662b5 100644 --- a/lib/models/generic_response_model.dart +++ b/lib/models/generic_response_model.dart @@ -675,7 +675,6 @@ class GenericResponseModel { this.isSMSSent, this.getFADisposalNtfDetails, this.Mohemm_ENSAT_GetItgToken_Response, - this.getFADisposalNtfDetails, this.atlasInitiateChatResponse, this.atlasContinueChatResponseDetails, diff --git a/pubspec.yaml b/pubspec.yaml index c6d6e94e..db54c04c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,7 +50,7 @@ dependencies: syncfusion_flutter_calendar: ^35.1.37 # flutter_calendar_carousel: ^2.1.0 pie_chart: ^5.1.0 - shared_preferences: ^2.0.12 + shared_preferences: ^2.5.5 firebase_messaging: ^16.7.0 shimmer: ^4.0.0 logger: ^2.8.0 @@ -77,7 +77,7 @@ dependencies: # flutter_barcode_scanner: ^2.0.0 # qr_code_scanner: ^1.0.1 # qr_flutter: ^4.0.0 - url_launcher: ^6.0.15 + url_launcher: ^6.3.3 # share_plus: ^13.3.0 flutter_rating_bar: ^4.0.1 auto_size_text: ^3.0.0 @@ -88,7 +88,7 @@ dependencies: # Marathon Card Swipe appinio_swiper: ^2.1.1 expandable: ^5.0.1 - safe_device: ^1.1.9 + safe_device: ^1.4.1 # networkImage cached_network_image: ^4.0.3