From bd8a7ad5ef6371a335d9131939ee92a38c16f3a4 Mon Sep 17 00:00:00 2001 From: Bilal Khan Date: Wed, 9 Jul 2025 18:32:34 +0800 Subject: [PATCH] Chat module --- lib/app/bindings/controllersBindings.dart | 18 + lib/app/bindings/themeController.dart | 77 +++ .../models/contactModels/contactModels.dart | 18 +- .../contactModels/groupContactsModel.dart | 65 ++ .../controllers/chatController.dart | 19 +- .../controllers/chatInfoController.dart | 11 + .../controllers/chatSearchController.dart | 13 + .../views/messageChatViews/ChatHomePage.dart | 20 +- .../messageChatViews/allGroupMembersInfo.dart | 70 +++ .../messageChatViews/chatHistoryPage.dart | 130 ++++ .../views/messageChatViews/chatInfoPage.dart | 350 +++++++++++ .../views/messageChatViews/chatListPage.dart | 95 ++- .../messageChatViews/chatMessagePage.dart | 591 +++++++++++------- .../widgets/dotted_box_widget.dart | 102 +++ .../contactViews/ContactPage/contactInfo.dart | 40 +- .../contactViews/ContactPage/contactPage.dart | 188 ++---- .../contactViews/homePage/chatInfo.dart | 308 --------- .../controllers/contactController.dart | 40 +- .../controller/discover_controller.dart | 17 + .../discoverModule/views/discoverPage.dart | 66 +- .../widgets/listTileViewWidget.dart | 26 +- .../mineModule/controller/mineController.dart | 16 +- .../views/homePage/mineHomePage.dart | 231 +++++++ .../chat/mineModule/views/minePage.dart | 160 ----- .../views/settingsPage/settingPage.dart | 17 + .../selfMedia/video/views/videoFeed.dart | 64 +- lib/main.dart | 52 +- .../controller/language_controller.dart | 18 + lib/translations/en_Us.dart | 59 ++ lib/translations/zh_CN.dart | 56 ++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 29 + pubspec.yaml | 5 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 37 files changed, 1992 insertions(+), 990 deletions(-) create mode 100644 lib/app/bindings/controllersBindings.dart create mode 100644 lib/app/bindings/themeController.dart create mode 100644 lib/app/models/contactModels/groupContactsModel.dart create mode 100644 lib/app/modules/chat/MessageModule/controllers/chatInfoController.dart create mode 100644 lib/app/modules/chat/MessageModule/controllers/chatSearchController.dart create mode 100644 lib/app/modules/chat/MessageModule/views/messageChatViews/allGroupMembersInfo.dart create mode 100644 lib/app/modules/chat/MessageModule/views/messageChatViews/chatHistoryPage.dart create mode 100644 lib/app/modules/chat/MessageModule/views/messageChatViews/chatInfoPage.dart create mode 100644 lib/app/modules/chat/MessageModule/widgets/dotted_box_widget.dart delete mode 100644 lib/app/modules/chat/contactModule/contactViews/homePage/chatInfo.dart create mode 100644 lib/app/modules/chat/discoverModule/controller/discover_controller.dart create mode 100644 lib/app/modules/chat/mineModule/views/homePage/mineHomePage.dart delete mode 100644 lib/app/modules/chat/mineModule/views/minePage.dart create mode 100644 lib/app/modules/chat/mineModule/views/settingsPage/settingPage.dart create mode 100644 lib/translations/controller/language_controller.dart create mode 100644 lib/translations/en_Us.dart create mode 100644 lib/translations/zh_CN.dart diff --git a/lib/app/bindings/controllersBindings.dart b/lib/app/bindings/controllersBindings.dart new file mode 100644 index 0000000..18982c3 --- /dev/null +++ b/lib/app/bindings/controllersBindings.dart @@ -0,0 +1,18 @@ +import 'package:caller/app/modules/chat/MessageModule/controllers/chatInfoController.dart'; +import 'package:caller/app/modules/chat/MessageModule/controllers/chatSearchController.dart'; +import 'package:caller/app/modules/chat/contactModule/controllers/contactController.dart'; +import 'package:caller/app/modules/chat/discoverModule/controller/discover_controller.dart'; +import 'package:caller/translations/controller/language_controller.dart'; +import 'package:get/get.dart'; + +class AppBindings extends Bindings { + @override + void dependencies() { + + Get.lazyPut(() => LanguageController()); + Get.lazyPut(() => DiscoverController()); + Get.lazyPut(() => ContactController()); + Get.lazyPut(()=>ChatInfoController()); + Get.lazyPut(()=>ChatSearchController()); + } +} diff --git a/lib/app/bindings/themeController.dart b/lib/app/bindings/themeController.dart new file mode 100644 index 0000000..aa98848 --- /dev/null +++ b/lib/app/bindings/themeController.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class ThemeController extends GetxController { + // Observable theme mode, default to system. + var themeMode = ThemeMode.system.obs; + + void toggleTheme(bool isDark) { + themeMode.value = isDark ? ThemeMode.dark : ThemeMode.light; + } + + void setSystemTheme() { + themeMode.value = ThemeMode.system; + } + + bool get isDarkMode { + if (themeMode.value == ThemeMode.system) { + final brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; + return brightness == Brightness.dark; + } + return themeMode.value == ThemeMode.dark; + } +} +// light theme +final ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + scaffoldBackgroundColor: Colors.white, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Colors.white, + selectedItemColor: Colors.green, + unselectedItemColor: Colors.grey, + elevation: 8, + ), + iconTheme: const IconThemeData(color: Colors.black87), + dividerColor: Colors.grey.shade300, + cardColor: Colors.white, + textTheme: const TextTheme( + bodyLarge: TextStyle(color: Colors.black), + bodyMedium: TextStyle(color: Colors.black87), + ), + primaryColor: Colors.green, + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), +); +//dark theme +final ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + scaffoldBackgroundColor: Colors.black, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: Colors.black, + selectedItemColor: Colors.teal, + unselectedItemColor: Colors.grey, + elevation: 8, + ), + iconTheme: const IconThemeData(color: Colors.white70), + dividerColor: Colors.grey.shade700, + cardColor: Colors.grey.shade900, + textTheme: const TextTheme( + bodyLarge: TextStyle(color: Colors.white), + bodyMedium: TextStyle(color: Colors.white70), + ), + primaryColor: Colors.teal, + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: Colors.teal, + foregroundColor: Colors.white, + ), +); diff --git a/lib/app/models/contactModels/contactModels.dart b/lib/app/models/contactModels/contactModels.dart index b9fc7b9..d2b2e3d 100644 --- a/lib/app/models/contactModels/contactModels.dart +++ b/lib/app/models/contactModels/contactModels.dart @@ -9,11 +9,12 @@ class ChatContactModel { final String? statusMessage; final String? lastMessage; final DateTime? lastMessageTime; + final bool isGroup; ChatContactModel({ required this.id, required this.name, - this.nameIndex, + this.nameIndex, required this.avatarUrl, required this.isOnline, required this.phoneNumber, @@ -21,10 +22,10 @@ class ChatContactModel { this.statusMessage, this.lastMessage, this.lastMessageTime, + this.isGroup = false, }); } - class ContactItem { final String avatar; final String title; @@ -34,6 +35,19 @@ class ContactItem { final dummyContacts = [ + +ChatContactModel( + id: 'family_group', + name: '家人群', + nameIndex: 'J', + avatarUrl: 'https://picsum.photos/id/18/200/200', + isOnline: false, + phoneNumber: 'N/A', + region: '家庭群组', + lastMessage: '妈妈: 记得回家吃饭', + lastMessageTime: DateTime.now().subtract(Duration(hours: 12)), + isGroup: true, // Mark as group +), ChatContactModel( id: 'wx_helper', name: '文件传输助手', diff --git a/lib/app/models/contactModels/groupContactsModel.dart b/lib/app/models/contactModels/groupContactsModel.dart new file mode 100644 index 0000000..3cee3ea --- /dev/null +++ b/lib/app/models/contactModels/groupContactsModel.dart @@ -0,0 +1,65 @@ +import 'package:caller/app/models/contactModels/contactModels.dart'; + +class GroupDetails { + final String groupId; + final List members; + final ChatContactModel? admin; + final DateTime? createdAt; + + GroupDetails({ + required this.groupId, + required this.members, + this.admin, + this.createdAt, + }); +} + +final dummyGroupDetails = [ + GroupDetails( + groupId: 'family_group', + members: [ + dummyContacts.firstWhere((c) => c.id == 'zhang_san'), + dummyContacts.firstWhere((c) => c.id == 'li_si'), + dummyContacts.firstWhere((c) => c.id == 'wang_wu'), + dummyContacts.firstWhere((c) => c.id == 'alex_wang'), + dummyContacts.firstWhere((c) => c.id == 'emma_smith'), + dummyContacts.firstWhere((c) => c.id == 'david_zhang'), + dummyContacts.firstWhere((c) => c.id == 'liu_fang'), + dummyContacts.firstWhere((c) => c.id == 'mike_johnson'), + dummyContacts.firstWhere((c) => c.id == 'wei_xin'), + dummyContacts.firstWhere((c) => c.id == 'zhou_jie'), + dummyContacts.firstWhere((c) => c.id == 'sarah_li'), + dummyContacts.firstWhere((c) => c.id == 'chen_qi'), + dummyContacts.firstWhere((c) => c.id == 'zhao_liu'), + dummyContacts.firstWhere((c) => c.id == 'tencent_service'), + dummyContacts.firstWhere((c) => c.id == 'wechat_team'), + + dummyContacts.firstWhere((c) => c.id == 'zhang_san'), + dummyContacts.firstWhere((c) => c.id == 'li_si'), + dummyContacts.firstWhere((c) => c.id == 'wang_wu'), + dummyContacts.firstWhere((c) => c.id == 'alex_wang'), + dummyContacts.firstWhere((c) => c.id == 'emma_smith'), + dummyContacts.firstWhere((c) => c.id == 'david_zhang'), + dummyContacts.firstWhere((c) => c.id == 'liu_fang'), + dummyContacts.firstWhere((c) => c.id == 'mike_johnson'), + dummyContacts.firstWhere((c) => c.id == 'wei_xin'), + dummyContacts.firstWhere((c) => c.id == 'zhou_jie'), + dummyContacts.firstWhere((c) => c.id == 'sarah_li'), + dummyContacts.firstWhere((c) => c.id == 'chen_qi'), + dummyContacts.firstWhere((c) => c.id == 'zhao_liu'), + dummyContacts.firstWhere((c) => c.id == 'tencent_service'), + dummyContacts.firstWhere((c) => c.id == 'wechat_team'), + + ], + admin: dummyContacts.firstWhere((c) => c.id == 'zhang_san'), + ), + GroupDetails( + groupId: 'project_group', + members: [ + dummyContacts.firstWhere((c) => c.id == 'alex_wang'), + dummyContacts.firstWhere((c) => c.id == 'emma_smith'), + dummyContacts.firstWhere((c) => c.id == 'david_zhang'), + ], + admin: dummyContacts.firstWhere((c) => c.id == 'alex_wang'), + ), +]; \ No newline at end of file diff --git a/lib/app/modules/chat/MessageModule/controllers/chatController.dart b/lib/app/modules/chat/MessageModule/controllers/chatController.dart index 950d85c..7dc68ea 100644 --- a/lib/app/modules/chat/MessageModule/controllers/chatController.dart +++ b/lib/app/modules/chat/MessageModule/controllers/chatController.dart @@ -63,7 +63,24 @@ Future stopRecordingAndSend() async { if (path != null) { final file = File(path); - sendFileMessage(file); + +//livekit backend + // sendFileMessage(file); + + // Temporarily add the recording file to the UI as a message + final tempMsg = ChatMessageModel( + id: DateTime.now().millisecondsSinceEpoch.toString(), + text: '[Sent audio file: ${p.basename(file.path)}]', + fileName: p.basename(file.path), + mimeType: 'audio/m4a', + fileUrl: file.path, + participantIdentity: 'Khan', + timestamp: DateTime.now(), + isMe: true, + isPrivate: false, + ); + + messages.add(tempMsg); } else { Get.snackbar('Error', 'Recording failed or was cancelled.'); } diff --git a/lib/app/modules/chat/MessageModule/controllers/chatInfoController.dart b/lib/app/modules/chat/MessageModule/controllers/chatInfoController.dart new file mode 100644 index 0000000..0a3313f --- /dev/null +++ b/lib/app/modules/chat/MessageModule/controllers/chatInfoController.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; + +class ChatInfoController extends GetxController { + final RxBool muteNotifications = false.obs; + final RxBool stickyOnTop = false.obs; + final RxBool alert = false.obs; + + void toggleMuteNotifications() => muteNotifications.toggle(); + void toggleStickyOnTop() => stickyOnTop.toggle(); + void toggleAlert() => alert.toggle(); +} \ No newline at end of file diff --git a/lib/app/modules/chat/MessageModule/controllers/chatSearchController.dart b/lib/app/modules/chat/MessageModule/controllers/chatSearchController.dart new file mode 100644 index 0000000..84e1d9e --- /dev/null +++ b/lib/app/modules/chat/MessageModule/controllers/chatSearchController.dart @@ -0,0 +1,13 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; + +class ChatSearchController extends GetxController { + final RxString searchQuery = ''.obs; + final TextEditingController textController = TextEditingController(); + + // Method to update the search query + void updateSearchQuery(String query) { + searchQuery.value = query; + + } +} \ No newline at end of file diff --git a/lib/app/modules/chat/MessageModule/views/messageChatViews/ChatHomePage.dart b/lib/app/modules/chat/MessageModule/views/messageChatViews/ChatHomePage.dart index 5db4f48..ac588cd 100644 --- a/lib/app/modules/chat/MessageModule/views/messageChatViews/ChatHomePage.dart +++ b/lib/app/modules/chat/MessageModule/views/messageChatViews/ChatHomePage.dart @@ -2,9 +2,10 @@ import 'package:caller/app/constants/colors/colors.dart'; import 'package:caller/app/modules/chat/MessageModule/views/messageChatViews/chatListPage.dart'; import 'package:caller/app/modules/chat/contactModule/contactViews/ContactPage/contactPage.dart'; import 'package:caller/app/modules/chat/discoverModule/views/discoverPage.dart'; -import 'package:caller/app/modules/chat/mineModule/views/minePage.dart'; +import 'package:caller/app/modules/chat/mineModule/views/homePage/mineHomePage.dart'; import 'package:flutter/material.dart'; import 'package:caller/app/modules/selfMedia/video/views/videoFeed.dart'; +import 'package:get/get.dart'; class ChatHomeScreen extends StatefulWidget { const ChatHomeScreen({Key? key}) : super(key: key); @@ -28,13 +29,14 @@ class _ChatHomeScreenState extends State { return Scaffold( appBar: AppBar( centerTitle: true, - title: const Text('消息'), + title: Text('app_bar_title'.tr), backgroundColor: AppColors.primary, + leading: null, elevation: 0, actions: [ IconButton( icon: const Icon(Icons.switch_account), - onPressed: () => Navigator.pushReplacement( + onPressed: () => Navigator.push( context, MaterialPageRoute(builder: (_) => VideoFeedScreen()) ), @@ -43,7 +45,7 @@ class _ChatHomeScreenState extends State { ), body: _pages[_currentIndex], bottomNavigationBar: BottomNavigationBar( - backgroundColor: Colors.white, + backgroundColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor, type: BottomNavigationBarType.fixed, currentIndex: _currentIndex, selectedItemColor: AppColors.primary, @@ -53,22 +55,22 @@ class _ChatHomeScreenState extends State { _currentIndex = index; }); }, - items: const [ + items: [ BottomNavigationBarItem( icon: Icon(Icons.chat), - label: '消息', + label: 'message'.tr, ), BottomNavigationBarItem( icon: Icon(Icons.contacts), - label: '联系人', + label: 'contacts'.tr, ), BottomNavigationBarItem( icon: Icon(Icons.photo_camera), - label: '发现', + label: 'discover'.tr, ), BottomNavigationBarItem( icon: Icon(Icons.person), - label: '我的', + label: 'mine'.tr, ), ], ), diff --git a/lib/app/modules/chat/MessageModule/views/messageChatViews/allGroupMembersInfo.dart b/lib/app/modules/chat/MessageModule/views/messageChatViews/allGroupMembersInfo.dart new file mode 100644 index 0000000..1f47ee4 --- /dev/null +++ b/lib/app/modules/chat/MessageModule/views/messageChatViews/allGroupMembersInfo.dart @@ -0,0 +1,70 @@ +import 'package:caller/app/modules/chat/MessageModule/widgets/dotted_box_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:caller/app/models/contactModels/contactModels.dart'; // Import the models + +class GroupMembersPage extends StatelessWidget { + final List members; + + const GroupMembersPage({Key? key, required this.members}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('All Group Members'), centerTitle: true), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: GridView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + crossAxisSpacing: 8, + mainAxisSpacing: 10, + ), + itemCount: members.length + 1, + itemBuilder: (context, index) { + if (index < members.length) { + final member = members[index]; + return Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: member.avatarUrl, + width: 50, + height: 50, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 5), + Text(member.name, style: TextStyle(fontSize: 10)), + ], + ); + } else { + return GestureDetector( + onTap: () {}, + child: Center( + child: Column( + children: [ + FittedBox( + child: SizedBox( + width: 50, + height: 50, + child: DottedBorderBox( + child: Center(child: Icon(Icons.add, size: 16)), + ), + ), + ), + Text("") + ], + ), + ), + ); + } + }, + ), + ), + ); + } +} diff --git a/lib/app/modules/chat/MessageModule/views/messageChatViews/chatHistoryPage.dart b/lib/app/modules/chat/MessageModule/views/messageChatViews/chatHistoryPage.dart new file mode 100644 index 0000000..a0eff3a --- /dev/null +++ b/lib/app/modules/chat/MessageModule/views/messageChatViews/chatHistoryPage.dart @@ -0,0 +1,130 @@ +import 'package:caller/app/modules/chat/MessageModule/controllers/chatSearchController.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class ChatSearchScreen extends StatelessWidget { + final ChatSearchController controller = Get.find(); + + ChatSearchScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 40), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 300, + child: TextField( + minLines: 1, + maxLines: 4, + controller: controller.textController, + cursorColor: Theme.of(context).primaryColor, + onTap: () {}, + style: TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color), + decoration: InputDecoration( + prefixIcon: Icon(Icons.search), + hintText: 'search'.tr, + hintStyle: Theme.of(context).textTheme.bodyLarge, + filled: true, + fillColor: Colors.grey.withOpacity(0.25), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide.none, + ), + ), + onSubmitted: (text) {}, + ), + ), + TextButton( + onPressed: () { + Get.back(); + }, + child: Text( + 'cancel'.tr, + style: TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color), + ), + ) + ], + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'filter_by'.tr, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + FilterChip( + label: Text('date'.tr), + onSelected: (value) { + }, + ), + FilterChip( + label: Text('photos_and_videos'.tr), + onSelected: (value) { + }, + ), + FilterChip( + label: Text('files'.tr), + onSelected: (value) { + }, + ), + FilterChip( + label: Text('links'.tr), + onSelected: (value) { + }, + ), + FilterChip( + label: Text('music_and_audio'.tr), + onSelected: (value) { + }, + ), + FilterChip( + label: Text('transactions'.tr), + onSelected: (value) { + }, + ), + FilterChip( + label: Text('mini'.tr), + onSelected: (value) { + }, + ), + FilterChip( + label: Text('channels'.tr), + onSelected: (value) { + }, + ), + ], + ), + Expanded( + child: Obx(() { + return Center( + child: Text( + '${'search_results_for'.tr}: ${controller.searchQuery.value}', + ), + ); + }), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/chat/MessageModule/views/messageChatViews/chatInfoPage.dart b/lib/app/modules/chat/MessageModule/views/messageChatViews/chatInfoPage.dart new file mode 100644 index 0000000..7049d64 --- /dev/null +++ b/lib/app/modules/chat/MessageModule/views/messageChatViews/chatInfoPage.dart @@ -0,0 +1,350 @@ +import 'package:caller/app/models/contactModels/groupContactsModel.dart'; +import 'package:caller/app/modules/chat/MessageModule/controllers/chatInfoController.dart'; +import 'package:caller/app/modules/chat/MessageModule/views/messageChatViews/allGroupMembersInfo.dart'; +import 'package:caller/app/modules/chat/MessageModule/widgets/dotted_box_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:caller/app/models/contactModels/contactModels.dart'; // Import your models +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:get/get.dart'; + +class ChatInfoPage extends StatefulWidget { + final ChatContactModel contact; + final GroupDetails? groupDetails; + const ChatInfoPage({Key? key, required this.contact, this.groupDetails}) + : super(key: key); + + @override + State createState() => _ChatInfoPageState(); +} + +class _ChatInfoPageState extends State { + final ChatInfoController controller = Get.find(); + + GroupDetails? get group => + widget.groupDetails ?? + dummyGroupDetails.firstWhere( + (g) => g.groupId == widget.contact.id, + orElse: () => GroupDetails(groupId: widget.contact.id, members: []), + ); + +@override +Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon( + Icons.arrow_back_ios_new, + color: Theme.of(context).iconTheme.color, + ), + ), + centerTitle: true, + title: Text('chat_info'.tr), + ), + body: ListView( + children: [ +//profile for individual contact + if (!widget.contact.isGroup) + Container( + color: Theme.of(context).cardColor, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: CachedNetworkImage( + imageUrl: widget.contact.avatarUrl, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: 10), + Text( + '${widget.contact.name.substring(0, 3)}...', + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 5), + + ], + ), + const SizedBox(width: 15), + SizedBox( + width: 55, + height: 55, + child: DottedBorderBox( + child: Center(child: Icon(Icons.add)), + ), + ), + ], + ), + ), + + const SizedBox(height: 10), + + // Group members info +if (widget.contact.isGroup) ...[ + Container( + color: Theme.of(context).cardColor, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 10), + child: Column( + children: [ + Wrap( + spacing: 15, + runSpacing: 15, + children: [ + ...group!.members.take(19).map((member) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: member.avatarUrl, + width: 55, + height: 55, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 5), + SizedBox( + width: 55, + child: Text( + member.name, + style: TextStyle(fontSize: 11), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ], + )), + // Dotted container + SizedBox( + width: 55, + height: 55, + child: DottedBorderBox( + child: Center(child: Icon(Icons.add)), + ), + ), + ], + ), + SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => GroupMembersPage(members: group!.members), + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'View group members', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).textTheme.bodyLarge!.color, + ), + ), + Icon(Icons.chevron_right, size: 16, color: Theme.of(context).iconTheme.color), + ], + ), + ), + ), + ], + ), + ), + ), + +], +SizedBox(height: 10,), +if (widget.contact.isGroup)...[ + Container( + color: Theme.of(context).cardColor, + child: Column( + children: [ + Container( + color: Theme.of(context).cardColor, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('group_name'.tr, + style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color,fontSize: 16),), + Spacer(), + Text(widget.contact.name), + SizedBox(width: 5,), + Icon(Icons.chevron_right), + ], + ), + ), + ), + Divider( + thickness: 0.3, + height: 0.1, + color: Theme.of(context).dividerColor, + ), + Container( + color: Theme.of(context).cardColor, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('group_qr_code'.tr, + style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color,fontSize: 16),), + Spacer(), + Icon(Icons.qr_code_outlined), + SizedBox(width: 5,), + Icon(Icons.chevron_right), + ], + ), + ), + ), + ], + ), + ), + +], +SizedBox(height: 10,), + + // Common Options + + Container( + color: Theme.of(context).cardColor, + child: ListTile( + title: Text('search_chat'.tr), + trailing: Icon(Icons.chevron_right), + onTap: () {}, + ), + ), + const SizedBox(height: 10), + + Container( + color: Theme.of(context).cardColor, + child: Column( + children: [ + Obx( + () => ListTile( + title: Text('mute_notification'.tr), + trailing: Switch( + activeColor: Theme.of(context).cardColor, + inactiveThumbColor: Theme.of( + context, + ).textTheme.bodyLarge!.color, + activeTrackColor: Theme.of( + context, + ).textTheme.bodyLarge!.color, + inactiveTrackColor: Theme.of(context).cardColor, + + value: controller.muteNotifications.value, + onChanged: (value) => + controller.toggleMuteNotifications(), + ), + ), + ), + Divider( + thickness: 0.3, + height: 0.1, + color: Theme.of(context).dividerColor, + ), + Obx( + () => ListTile( + title: Text('sticky_top'.tr), + trailing: Switch( + activeColor: Theme.of(context).cardColor, + inactiveThumbColor: Theme.of( + context, + ).textTheme.bodyLarge!.color, + activeTrackColor: Theme.of( + context, + ).textTheme.bodyLarge!.color, + inactiveTrackColor: Theme.of(context).cardColor, + value: controller.stickyOnTop.value, + onChanged: (value) => controller.toggleStickyOnTop(), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 10), + + Container( + color: Theme.of(context).cardColor, + child: ListTile( + title: Text('background'.tr), + trailing: Icon(Icons.chevron_right), + onTap: () {}, + ), + ), + const SizedBox(height: 10), + + Container( + color: Theme.of(context).cardColor, + child: ListTile( + title: Text('clear_chat_history'.tr), + trailing: Icon(Icons.chevron_right), + onTap: () {}, + ), + ), + const SizedBox(height: 10), + + Container( + color: Theme.of(context).cardColor, + child: ListTile( + title: Text('report'.tr), + trailing: Icon(Icons.chevron_right), + onTap: () {}, + ), + ), + const SizedBox(height: 10), + + if (widget.contact.isGroup) + Container( + color: Theme.of(context).cardColor, + child: ListTile( + title: Center( + child: Text( + 'leave_group'.tr, + style: TextStyle(color: Colors.red), + ), + ), + onTap: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('leave_group'.tr), + content: Text('are_you_sure_leave_group'.tr), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('cancel'.tr), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + child: Text('leave'.tr, style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); +} +} \ No newline at end of file diff --git a/lib/app/modules/chat/MessageModule/views/messageChatViews/chatListPage.dart b/lib/app/modules/chat/MessageModule/views/messageChatViews/chatListPage.dart index 3f005e7..2f9ebe8 100644 --- a/lib/app/modules/chat/MessageModule/views/messageChatViews/chatListPage.dart +++ b/lib/app/modules/chat/MessageModule/views/messageChatViews/chatListPage.dart @@ -6,46 +6,77 @@ import 'package:caller/app/modules/chat/contactModule/widgets/contactPageWidgets import 'package:cached_network_image/cached_network_image.dart'; class ChatListPage extends StatelessWidget { - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor:Colors.white, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: ListView.builder( itemCount: dummyContacts.length, itemBuilder: (context, index) { final contact = dummyContacts[index]; - return ListTile( - leading: CircleAvatar( - backgroundImage: CachedNetworkImageProvider(contact.avatarUrl), - radius: 24, - ), - title: Text(contact.name), - subtitle: Text( - contact.lastMessage ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - _formatTime(contact.lastMessageTime), - style: TextStyle(color: Colors.grey, fontSize: 12), + return Column( + children: [ + ListTile( + leading: contact.isGroup + ? ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + height: 50, + width: 50, + child: Stack( + children: [ + CachedNetworkImage( + imageUrl: contact.avatarUrl, + width: 50, + height: 50, + fit: BoxFit.cover, + ), + Align( + alignment: Alignment.bottomRight, + child: Icon(Icons.group,size: 18,)), + ], + ), + ), + ) + : CircleAvatar( + backgroundImage: CachedNetworkImageProvider( + contact.avatarUrl, + ), + radius: 24, + ), + title: Text(contact.name), + subtitle: Text( + contact.lastMessage ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - if (contact.isOnline) - Container( - margin: EdgeInsets.only(top: 4), - width: 10, - height: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.green, + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _formatTime(contact.lastMessageTime), + style: TextStyle(color: Colors.grey, fontSize: 12), ), - ), - ], - ), - onTap: () => _openChat(context, contact), + if (contact.isOnline) + Container( + margin: EdgeInsets.only(top: 4), + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.green, + ), + ), + ], + ), + onTap: () => _openChat(context, contact), + ), + Divider( + thickness: 0.3, + height: 0.1, + color: Theme.of(context).dividerColor, + ), + ], ); }, ), @@ -72,4 +103,4 @@ class ChatListPage extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/app/modules/chat/MessageModule/views/messageChatViews/chatMessagePage.dart b/lib/app/modules/chat/MessageModule/views/messageChatViews/chatMessagePage.dart index cb784c9..423c8b2 100644 --- a/lib/app/modules/chat/MessageModule/views/messageChatViews/chatMessagePage.dart +++ b/lib/app/modules/chat/MessageModule/views/messageChatViews/chatMessagePage.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'dart:io'; import 'package:caller/app/models/chatModels/chatMeesageModel.dart'; import 'package:caller/app/models/contactModels/contactModels.dart'; +import 'package:caller/app/models/contactModels/groupContactsModel.dart'; import 'package:caller/app/modules/chat/MessageModule/controllers/chatController.dart'; +import 'package:caller/app/modules/chat/MessageModule/views/messageChatViews/chatInfoPage.dart'; import 'package:caller/app/modules/chat/contactModule/contactViews/ContactPage/contactInfo.dart'; import 'package:caller/app/modules/chat/MessageModule/views/messageChatViews/fullscreenChatPlay.dart'; import 'package:caller/app/modules/chat/MessageModule/widgets/animated_wave.dart'; @@ -18,8 +20,11 @@ import 'package:open_filex/open_filex.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:mime/mime.dart'; +import 'package:path/path.dart' as p; + class ChatMessageScreen extends StatefulWidget { - final ChatContactModel contact; + final ChatContactModel contact; ChatMessageScreen({Key? key, required this.contact}) : super(key: key); final ChatController chatController = Get.put(ChatController()); @@ -27,75 +32,75 @@ class ChatMessageScreen extends StatefulWidget { State createState() => _ChatMessageScreenState(); } -class _ChatMessageScreenState extends State with TickerProviderStateMixin { +class _ChatMessageScreenState extends State + with TickerProviderStateMixin { final FocusNode _focusNode = FocusNode(); late AnimationController _emojiController; late Animation _emojiOffset; -double dragOffset = 0; -bool isCancelled = false; -String recordingDialogText = "Slide up to cancel"; -Color recordingDialogColor = Colors.white70; -BuildContext? recordingDialogContext; -void _startRecording() { - isCancelled = false; - recordingDialogText = "Slide up to cancel"; - recordingDialogColor = Colors.white70; + double dragOffset = 0; + bool isCancelled = false; + String recordingDialogText = "Slide up to cancel"; + Color recordingDialogColor = Colors.white70; + BuildContext? recordingDialogContext; + void _startRecording() { + isCancelled = false; + recordingDialogText = "Slide up to cancel"; + recordingDialogColor = Colors.white70; - widget.chatController.startRecording(); + widget.chatController.startRecording(); - showDialog( - context: context, - barrierDismissible: false, - barrierColor: Colors.black.withOpacity(0.5), - builder: (ctx) { - recordingDialogContext = ctx; - return Dialog( - backgroundColor: Colors.transparent, - elevation: 0, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(16), + showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.black.withOpacity(0.5), + builder: (ctx) { + recordingDialogContext = ctx; + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + const Text( + "Recording...", + style: TextStyle(color: Colors.white, fontSize: 18), + ), + const SizedBox(height: 20), + const SizedBox( + width: 60, + height: 60, + child: AnimatedWave(), + ), + const SizedBox(height: 20), + Text( + recordingDialogText, + style: TextStyle(color: recordingDialogColor), + ), + ], + ), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 10), - const Text( - "Recording...", - style: TextStyle(color: Colors.white, fontSize: 18), - ), - const SizedBox(height: 20), - const SizedBox( - width: 60, - height: 60, - child: AnimatedWave(), - ), - const SizedBox(height: 20), - Text( - recordingDialogText, - style: TextStyle(color: recordingDialogColor), - ), - ], - ), - ), - ], - ), - ); - }, - ); -} - -void _updateRecordingDialog(String text, Color color) { - recordingDialogText = text; - recordingDialogColor = color; - (recordingDialogContext as Element).markNeedsBuild(); -} + ], + ), + ); + }, + ); + } + void _updateRecordingDialog(String text, Color color) { + recordingDialogText = text; + recordingDialogColor = color; + (recordingDialogContext as Element).markNeedsBuild(); + } @override void initState() { @@ -124,14 +129,30 @@ void _updateRecordingDialog(String text, Color color) { if (photo != null) { final file = File(photo.path); - widget.chatController.sendFileMessage(file); + + //backend live-kit + // widget.chatController.sendFileMessage(file); + + final tempMsg = ChatMessageModel( + id: DateTime.now().millisecondsSinceEpoch.toString(), + text: '[Picked file: ${p.basename(file.path)}]', + fileName: p.basename(file.path), + mimeType: lookupMimeType(file.path) ?? 'application/octet-stream', + fileUrl: file.path, + participantIdentity: 'Khan', + timestamp: DateTime.now(), + isMe: true, + isPrivate: false, + ); + widget.chatController.messages.add(tempMsg); // Add file to UI immediately + setState(() {}); } } Future _pickFromGallery() async { final List? assets = await AssetPicker.pickAssets( context, - pickerConfig: AssetPickerConfig( + pickerConfig: AssetPickerConfig( requestType: RequestType.common, maxAssets: 1, ), @@ -140,7 +161,25 @@ void _updateRecordingDialog(String text, Color color) { if (assets != null && assets.isNotEmpty) { final file = await assets.first.file; if (file != null) { - widget.chatController.sendFileMessage(file); + //backend live-kit + // widget.chatController.sendFileMessage(file); + + // Temporarily show the file in the UI by directly adding it + final tempMsg = ChatMessageModel( + id: DateTime.now().millisecondsSinceEpoch.toString(), + text: '[Picked file: ${p.basename(file.path)}]', + fileName: p.basename(file.path), + mimeType: lookupMimeType(file.path) ?? 'application/octet-stream', + fileUrl: file.path, + participantIdentity: 'Khan', + timestamp: DateTime.now(), + isMe: true, + isPrivate: false, + ); + widget.chatController.messages.add( + tempMsg, + ); // Add file to UI immediately + setState(() {}); } } } @@ -155,17 +194,55 @@ void _updateRecordingDialog(String text, Color color) { final picked = result.files.first; if (picked.path != null) { final file = File(picked.path!); - widget.chatController.sendFileMessage(file); + + //backend live-kit + // widget.chatController.sendFileMessage(file); + + // Temporarily show the file in the UI by directly adding it + final tempMsg = ChatMessageModel( + id: DateTime.now().millisecondsSinceEpoch.toString(), + text: '[Picked file: ${p.basename(file.path)}]', + fileName: p.basename(file.path), + mimeType: lookupMimeType(file.path) ?? 'application/octet-stream', + fileUrl: file.path, + participantIdentity: 'Khan', + timestamp: DateTime.now(), + isMe: true, + isPrivate: false, + ); + widget.chatController.messages.add( + tempMsg, + ); // Add file to UI immediately + setState(() {}); } } } + //filter query + String searchQuery = ""; + + // Function to filter the messages + List _getFilteredMessages() { + if (searchQuery.isEmpty) { + return widget.chatController.messages; + } else { + return widget.chatController.messages + .where( + (msg) => msg.text.toLowerCase().contains(searchQuery.toLowerCase()), + ) + .toList(); + } + } + @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - FocusScope.of(context).unfocus(); - }, + onTap: () { + FocusScope.of(context).unfocus(); + widget.chatController.isEmojiPickerVisible.value = false; + _emojiController.reverse(); + widget.chatController.isAttachmentPanelVisible.value = false; + }, child: Scaffold( backgroundColor: Colors.black, appBar: AppBar( @@ -174,26 +251,59 @@ void _updateRecordingDialog(String text, Color color) { onTap: () => Navigator.pop(context), child: const Icon(Icons.arrow_back_ios, color: Colors.white), ), - centerTitle: true, - actions: [ - IconButton( - icon: const Icon(Icons.more_vert,color: Colors.white,), - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (context)=> ContactInfoPage(contact: widget.contact),)); - }, - ), - ], + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.more_vert, color: Colors.white), + onPressed: () { + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatInfoPage( + contact: widget.contact, + + groupDetails: widget.contact.isGroup + ? dummyGroupDetails.firstWhere( + (g) => g.groupId == widget.contact.id, + orElse: () => GroupDetails( + groupId: widget.contact.id, + members: [], + ), + ) + : null, + ), + ), + ); + }, + ), + ], title: Row( children: [ - CircleAvatar( - backgroundImage: CachedNetworkImageProvider(widget.contact.avatarUrl), - radius: 24, - ), - SizedBox(width: 8), - Text(widget.contact.name,style: TextStyle(color: Colors.white),), + widget.contact.isGroup + ? ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + height: 50, + width: 50, + child: CachedNetworkImage( + imageUrl: widget.contact.avatarUrl, + width: 50, + height: 50, + fit: BoxFit.cover, + ), + ), + ) + : CircleAvatar( + backgroundImage: CachedNetworkImageProvider( + widget.contact.avatarUrl, + ), + radius: 24, + ), + SizedBox(width: 16), + Text(widget.contact.name, style: TextStyle(color: Colors.white)), ], ), - ), body: SafeArea( child: Column( @@ -510,51 +620,61 @@ void _updateRecordingDialog(String text, Color color) { child: Obx(() { if (widget.chatController.isVoiceMode.value) { // Show hold to talk button - return Listener( - onPointerDown: (details) { - dragOffset = 0; - isCancelled = false; - _startRecording(); - }, - onPointerMove: (details) { - dragOffset += details.delta.dy; - if (dragOffset < -50) { - if (!isCancelled) { - isCancelled = true; - _updateRecordingDialog("Release to cancel", Colors.redAccent); - } - } else { - if (isCancelled) { - isCancelled = false; - _updateRecordingDialog("Slide up to cancel", Colors.white70); - } - } - }, - onPointerUp: (details) { - Navigator.of(recordingDialogContext!).pop(); - if (isCancelled) { - widget.chatController.cancelRecording(); - } else { - widget.chatController.stopRecordingAndSend(); - } - }, - child: Container( - height: 48, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.25), - borderRadius: BorderRadius.circular(5), - ), - child: Obx(() => Text( - "Hold to talk", - style: TextStyle( - color: widget.chatController.isRecording.value ? Colors.red : Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - )), - ), - ); + return Listener( + onPointerDown: (details) { + dragOffset = 0; + isCancelled = false; + _startRecording(); + }, + onPointerMove: (details) { + dragOffset += details.delta.dy; + if (dragOffset < -50) { + if (!isCancelled) { + isCancelled = true; + _updateRecordingDialog( + "Release to cancel", + Colors.redAccent, + ); + } + } else { + if (isCancelled) { + isCancelled = false; + _updateRecordingDialog( + "Slide up to cancel", + Colors.white70, + ); + } + } + }, + onPointerUp: (details) { + Navigator.of(recordingDialogContext!).pop(); + if (isCancelled) { + widget.chatController.cancelRecording(); + } else { + widget.chatController.stopRecordingAndSend(); + } + }, + child: Container( + height: 48, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.25), + borderRadius: BorderRadius.circular(5), + ), + child: Obx( + () => Text( + "Hold to talk", + style: TextStyle( + color: widget.chatController.isRecording.value + ? Colors.red + : Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); } else { // Normal text input return TextField( @@ -586,8 +706,20 @@ void _updateRecordingDialog(String text, Color color) { borderSide: BorderSide.none, ), ), - onSubmitted: (text) => - widget.chatController.sendMessage(text), + onSubmitted: (text) { + final newMessage = ChatMessageModel( + text: text.trim(), + isMe: true, + timestamp: DateTime.now(), + id: DateTime.now().millisecondsSinceEpoch.toString(), + participantIdentity: 'Khan', + ); + + widget.chatController.messages.add(newMessage); + setState(() {}); + + // widget.chatController.sendMessage(text); + }, ); } }), @@ -620,9 +752,22 @@ void _updateRecordingDialog(String text, Color color) { color: Colors.white, ), onPressed: hasText - ? () => widget.chatController.sendMessage( - widget.chatController.textController.text, - ) + ? () { + final newMessage = ChatMessageModel( + text: widget.chatController.textController.text + .trim(), + isMe: true, + timestamp: DateTime.now(), + id: DateTime.now().millisecondsSinceEpoch + .toString(), + participantIdentity: 'Khan', + ); + widget.chatController.messages.add(newMessage); + + // widget.chatController.sendMessage( + // widget.chatController.textController.text, + // ); + } : () { FocusScope.of(context).unfocus(); widget.chatController.isEmojiPickerVisible.value = @@ -642,100 +787,104 @@ void _updateRecordingDialog(String text, Color color) { ); } -Widget _buildAttachmentPanel() { - return Obx(() { - if (!widget.chatController.isAttachmentPanelVisible.value) { - return const SizedBox.shrink(); - } + Widget _buildAttachmentPanel() { + return Obx(() { + if (!widget.chatController.isAttachmentPanelVisible.value) { + return const SizedBox.shrink(); + } - return Container( - height: 130, - padding: const EdgeInsets.all(12), - color: Colors.black12, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildAttachmentButton(Icons.camera_alt, 'Camera', _openCamera), - _buildAttachmentButton(Icons.image, 'Gallery', _pickFromGallery), - _buildAttachmentButton(Icons.insert_drive_file, 'Document', _pickDocument), - _buildAttachmentButton(Icons.video_call, 'Call', _showCallOptions), - ], - ), - ); - }); -} - -Widget _buildAttachmentButton( - IconData icon, - String label, - VoidCallback onTap, -) { - return Column( - children: [ - InkWell( - onTap: onTap, - child: CircleAvatar( - radius: 24, - backgroundColor: Colors.green, - child: Icon(icon, color: Colors.white), - ), - ), - const SizedBox(height: 4), - Text(label, style: const TextStyle(color: Colors.white70)), - ], - ); -} - -/// Opens a bottom sheet with audio and video call options -void _showCallOptions() { - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - backgroundColor: Colors.white, - builder: (_) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, + return Container( + height: 130, + padding: const EdgeInsets.all(12), + color: Colors.black12, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const Padding( - padding: EdgeInsets.all(16), - child: Text('Make a call', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - ), - ListTile( - leading: const Icon(Icons.call, color: Colors.green), - title: const Text('Audio Call'), - onTap: () { - Navigator.pop(context); - _startAudioCall(); - }, - ), - ListTile( - leading: const Icon(Icons.videocam, color: Colors.green), - title: const Text('Video Call'), - onTap: () { - Navigator.pop(context); - _startVideoCall(); - }, + _buildAttachmentButton(Icons.camera_alt, 'Camera', _openCamera), + _buildAttachmentButton(Icons.image, 'Gallery', _pickFromGallery), + _buildAttachmentButton( + Icons.insert_drive_file, + 'Document', + _pickDocument, ), + _buildAttachmentButton(Icons.video_call, 'Call', _showCallOptions), ], ), ); - }, - ); -} + }); + } -void _startAudioCall() { - // TODO: your audio call logic - print("Starting audio call..."); -} + Widget _buildAttachmentButton( + IconData icon, + String label, + VoidCallback onTap, + ) { + return Column( + children: [ + InkWell( + onTap: onTap, + child: CircleAvatar( + radius: 24, + backgroundColor: Colors.green, + child: Icon(icon, color: Colors.white), + ), + ), + const SizedBox(height: 4), + Text(label, style: const TextStyle(color: Colors.white70)), + ], + ); + } -void _startVideoCall() { - // TODO: your video call logic - print("Starting video call..."); -} + /// Opens a bottom sheet with audio and video call options + void _showCallOptions() { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + backgroundColor: Colors.white, + builder: (_) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.all(16), + child: Text( + 'Make a call', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ListTile( + leading: const Icon(Icons.call, color: Colors.green), + title: const Text('Audio Call'), + onTap: () { + Navigator.pop(context); + _startAudioCall(); + }, + ), + ListTile( + leading: const Icon(Icons.videocam, color: Colors.green), + title: const Text('Video Call'), + onTap: () { + Navigator.pop(context); + _startVideoCall(); + }, + ), + ], + ), + ); + }, + ); + } + void _startAudioCall() { + print("Starting audio call..."); + } + + void _startVideoCall() { + print("Starting video call..."); + } Widget _buildEmojiPickerContainer() { return Obx(() { diff --git a/lib/app/modules/chat/MessageModule/widgets/dotted_box_widget.dart b/lib/app/modules/chat/MessageModule/widgets/dotted_box_widget.dart new file mode 100644 index 0000000..43a4525 --- /dev/null +++ b/lib/app/modules/chat/MessageModule/widgets/dotted_box_widget.dart @@ -0,0 +1,102 @@ +// import 'package:flutter/material.dart'; + +// class DottedBorderBox extends StatelessWidget { +// final Widget child; + +// DottedBorderBox({required this.child}); + +// @override +// Widget build(BuildContext context) { +// return CustomPaint( +// painter: _DottedBorderPainter(), +// child: child, +// ); +// } +// } + +// class _DottedBorderPainter extends CustomPainter { +// @override +// void paint(Canvas canvas, Size size) { +// final paint = Paint() +// ..color = Colors.white ..strokeWidth = 2 +// ..style = PaintingStyle.stroke; + +// final dashWidth = 4.0; +// final gapWidth = 4.0; + +// // Draw top border +// double dashCount = (size.width / (dashWidth + gapWidth)).floorToDouble(); +// for (double i = 0; i < dashCount; i++) { +// final startX = i * (dashWidth + gapWidth); +// final endX = startX + dashWidth; +// canvas.drawLine( +// Offset(startX, 0), +// Offset(endX, 0), +// paint, +// ); +// } + +// // Draw bottom border +// for (double i = 0; i < dashCount; i++) { +// final startX = i * (dashWidth + gapWidth); +// final endX = startX + dashWidth; +// canvas.drawLine( +// Offset(startX, size.height), +// Offset(endX, size.height), +// paint, +// ); +// } + +// // Draw left border +// double dashCountVertical = (size.height / (dashWidth + gapWidth)).floorToDouble(); +// for (double i = 0; i < dashCountVertical; i++) { +// final startY = i * (dashWidth + gapWidth); +// final endY = startY + dashWidth; +// canvas.drawLine( +// Offset(0, startY), +// Offset(0, endY), +// paint, +// ); +// } + +// // Draw right border +// for (double i = 0; i < dashCountVertical; i++) { +// final startY = i * (dashWidth + gapWidth); +// final endY = startY + dashWidth; +// canvas.drawLine( +// Offset(size.width, startY), +// Offset(size.width, endY), +// paint, +// ); +// } +// } + +// @override +// bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +// } + + +import 'package:flutter/material.dart'; +import 'package:dotted_decoration/dotted_decoration.dart'; + +class DottedBorderBox extends StatelessWidget { + final Widget child; + + const DottedBorderBox({required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: DottedDecoration( + color: Theme.of(context).textTheme.bodyLarge!.color!, + strokeWidth: 1, + dash: [4, 4], + shape: Shape.box, + borderRadius: BorderRadius.circular(10), + ), + child: SizedBox.expand( + child: child, + ), + ); + } +} \ No newline at end of file diff --git a/lib/app/modules/chat/contactModule/contactViews/ContactPage/contactInfo.dart b/lib/app/modules/chat/contactModule/contactViews/ContactPage/contactInfo.dart index b314cc0..56c0edb 100644 --- a/lib/app/modules/chat/contactModule/contactViews/ContactPage/contactInfo.dart +++ b/lib/app/modules/chat/contactModule/contactViews/ContactPage/contactInfo.dart @@ -28,6 +28,7 @@ class ContactInfoPage extends StatelessWidget { FocusScope.of(context).unfocus(); }, child: Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( centerTitle: true, title: Text('联系方式'), @@ -39,12 +40,12 @@ class ContactInfoPage extends StatelessWidget { body: SingleChildScrollView( child: Column( children: [ - _buildProfileHeader(), + _buildProfileHeader(context), Divider(height: 1, thickness: 0.5), _buildInfoSection(context), Divider(height: 1, thickness: 0.5), SizedBox(height: 20,), - _buildActionButtons(), + _buildActionButtons(context), ], ), ), @@ -54,7 +55,7 @@ class ContactInfoPage extends StatelessWidget { } // Profile header including avatar, name, online status and status message - Widget _buildProfileHeader() { + Widget _buildProfileHeader(BuildContext context) { return Padding( padding: EdgeInsets.all(20), child: Row( @@ -75,18 +76,25 @@ class ContactInfoPage extends StatelessWidget { children: [ Text( contact.name, - style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, + + color: Theme.of(context).textTheme.bodyLarge!.color + ), overflow: TextOverflow.ellipsis, ), SizedBox(height: 4), Text( "Weixin ID:${contact.id}", - style: TextStyle(fontSize: 14), + style: TextStyle(fontSize: 14, + color: Theme.of(context).textTheme.bodyLarge!.color + ), overflow: TextOverflow.ellipsis, ), Text( "Region:${contact.region}", - style: TextStyle(fontSize: 14), + style: TextStyle(fontSize: 14, + color: Theme.of(context).textTheme.bodyLarge!.color + ), overflow: TextOverflow.ellipsis, ), ], @@ -152,7 +160,7 @@ class ContactInfoPage extends StatelessWidget { Text("11", style: TextStyle( fontWeight: FontWeight.w600, - color: Colors.black, + color: Theme.of(context).textTheme.bodyLarge!.color ), ) ],), @@ -168,7 +176,7 @@ class ContactInfoPage extends StatelessWidget { Text("Scan QR Code", style: TextStyle( fontWeight: FontWeight.w600, - color: Colors.black, + color: Theme.of(context).textTheme.bodyLarge!.color ), ) @@ -180,29 +188,33 @@ class ContactInfoPage extends StatelessWidget { } // Action buttons for messages, voice call, video call, etc. - Widget _buildActionButtons() { + Widget _buildActionButtons(BuildContext context) { return Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildActionButton(Icons.message, 'Messages'), - _buildActionButton(Icons.phone, 'Voice Call'), - _buildActionButton(Icons.video_call, 'Video Call'), + _buildActionButton(Icons.message, 'Messages',context), + _buildActionButton(Icons.phone, 'Voice Call',context), + _buildActionButton(Icons.video_call, 'Video Call',context), ], ), ); } // Helper widget for action buttons - Widget _buildActionButton(IconData icon, String label) { + Widget _buildActionButton(IconData icon, String label,BuildContext context) { return Column( children: [ IconButton( icon: Icon(icon, color: Colors.green), onPressed: () {}, ), - Text(label), + Text(label, + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color + ), + ), ], ); } diff --git a/lib/app/modules/chat/contactModule/contactViews/ContactPage/contactPage.dart b/lib/app/modules/chat/contactModule/contactViews/ContactPage/contactPage.dart index 2eb3fd7..57af137 100644 --- a/lib/app/modules/chat/contactModule/contactViews/ContactPage/contactPage.dart +++ b/lib/app/modules/chat/contactModule/contactViews/ContactPage/contactPage.dart @@ -1,115 +1,39 @@ -import 'dart:async'; -import 'package:caller/app/constants/constants.dart'; +import 'package:caller/app/models/contactModels/contactModels.dart'; import 'package:caller/app/modules/chat/contactModule/contactViews/ContactPage/contactInfo.dart'; +import 'package:caller/app/modules/chat/contactModule/controllers/contactController.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:caller/app/models/contactModels/contactModels.dart'; - -// === VIEWMODEL === -class ContactsViewModel extends GetxController { - final RxList contacts = [].obs; - final RxList functionButtons = [].obs; - final RxString currentLetter = ''.obs; - - Timer? _letterTimer; - - @override - void onInit() { - super.onInit(); - _initializeMockData(); - } - - void _initializeMockData() { - functionButtons.value = [ - ContactItem(avatar: '${contactAssets}ic_new_friend.webp', title: '新的朋友'), - ContactItem(avatar: '${contactAssets}ic_group.webp', title: '群聊'), - ContactItem(avatar: '${contactAssets}ic_tag.webp', title: '标签'), - ContactItem(avatar: '${contactAssets}ic_no_public.webp', title: '公众号'), - ]; - - contacts.value = dummyContacts; // your dummy contacts list - contacts.sort((a, b) => a.nameIndex!.compareTo(b.nameIndex!)); - } - - /// Jump scroll to letter header inside contacts list. - /// [keys] maps letters to GlobalKeys of header widgets. - void jumpToIndex( - String letter, - ScrollController controller, - Map keys, - ) { - if (!keys.containsKey(letter)) return; - - final keyContext = keys[letter]!.currentContext; - if (keyContext != null) { - final box = keyContext.findRenderObject() as RenderBox; - final viewport = - controller.position.context.storageContext.findRenderObject() - as RenderBox; - - // Offset of header relative to the viewport top - final offset = box.localToGlobal(Offset.zero, ancestor: viewport).dy; - - // Calculate absolute scroll offset + height of functionButtons area (to compensate) - // We calculate functionButtons height assuming each ListTile is 56 pixels (default) - final functionButtonsHeight = functionButtons.length * 56.0; - - final targetScrollOffset = - controller.offset + offset - functionButtonsHeight; - - // Animate scroll to position - controller.animateTo( - targetScrollOffset.clamp(0, controller.position.maxScrollExtent), - duration: Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - currentLetter.value = letter; - _resetLetterTimer(); - } - - void _resetLetterTimer() { - _letterTimer?.cancel(); - _letterTimer = Timer(Duration(milliseconds: 800), () { - currentLetter.value = ''; - }); - } -} - -// === PAGE === class ContactsPage extends StatelessWidget { final ScrollController sC = ScrollController(); final Map letterKeys = {}; @override Widget build(BuildContext context) { - final viewModel = Get.put(ContactsViewModel()); + // Access ContactController + final contactController = Get.find(); + return Scaffold( - backgroundColor: Colors.grey.shade100, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Stack( children: [ Obx(() { - // total items = function buttons + contacts - final totalCount = - viewModel.functionButtons.length + viewModel.contacts.length; + final totalCount = contactController.functionButtons.length + contactController.contacts.length; return ListView.builder( controller: sC, itemCount: totalCount, itemBuilder: (context, index) { - if (index < viewModel.functionButtons.length) { + if (index < contactController.functionButtons.length) { // Show function buttons first - final item = viewModel.functionButtons[index]; - return _buildFunctionButton(item); + final item = contactController.functionButtons[index]; + return _buildFunctionButton(item,context); } else { // Show contacts after function buttons - final contactIndex = index - viewModel.functionButtons.length; - final contact = viewModel.contacts[contactIndex]; + final contactIndex = index - contactController.functionButtons.length; + final contact = contactController.contacts[contactIndex]; - final showHeader = - contactIndex == 0 || - contact.nameIndex != - viewModel.contacts[contactIndex - 1].nameIndex; + final showHeader = contactIndex == 0 || + contact.nameIndex != contactController.contacts[contactIndex - 1].nameIndex; final letterKey = showHeader ? letterKeys.putIfAbsent( @@ -138,13 +62,11 @@ class ContactsPage extends StatelessWidget { ), ), Container( - color: Colors.white, + color: Theme.of(context).cardColor, child: ListTile( leading: CircleAvatar( radius: 24, - backgroundImage: CachedNetworkImageProvider( - contact.avatarUrl, - ), + backgroundImage: CachedNetworkImageProvider(contact.avatarUrl), ), title: Text( contact.name, @@ -153,22 +75,14 @@ class ContactsPage extends StatelessWidget { subtitle: contact.statusMessage != null ? Text(contact.statusMessage!) : null, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 2, - ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - ContactInfoPage(contact: contact), - ), - ); + Navigator.push(context, MaterialPageRoute(builder: (_)=>ContactInfoPage(contact: contact,))); }, ), ), - Divider(height: 1), + Divider(thickness: 0.3, + height: 0.1, color: Theme.of(context).dividerColor), ], ); } @@ -180,59 +94,62 @@ class ContactsPage extends StatelessWidget { top: 100, bottom: 100, width: 24, - child: _buildIndexBar(context, viewModel), + child: _buildIndexBar(context, contactController), ), - Obx(() => _buildCurrentLetterIndicator(viewModel)), + Obx(() => _buildCurrentLetterIndicator(contactController)), ], ), ); } - Widget _buildFunctionButton(ContactItem item) { + // Build function button UI + Widget _buildFunctionButton(ContactItem item,BuildContext context) { return Container( - color: Colors.white, + color: Theme.of(context).cardColor, child: ListTile( leading: Image.asset(item.avatar, width: 36, height: 36), - title: Text(item.title), + title: Text(item.title.tr), onTap: () {}, ), ); } - Widget _buildIndexBar(BuildContext context, ContactsViewModel viewModel) { + // Index bar on the right side + Widget _buildIndexBar(BuildContext context, ContactController contactController) { return GestureDetector( onVerticalDragUpdate: (details) => - _handleDrag(details.localPosition.dy, context, viewModel), + _handleDrag(details.localPosition.dy, context, contactController), onVerticalDragStart: (details) => - _handleDrag(details.localPosition.dy, context, viewModel), + _handleDrag(details.localPosition.dy, context, contactController), onTapDown: (details) => - _handleDrag(details.localPosition.dy, context, viewModel), + _handleDrag(details.localPosition.dy, context, contactController), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: INDEX_BAR_WORDS .map( - (e) => - Text(e, style: TextStyle(fontSize: 12, color: Colors.grey)), + (e) => Text(e, style: TextStyle(fontSize: 12, color: Colors.grey)), ) .toList(), ), ); } + // Handle drag events when the user drags on the index bar void _handleDrag( double localDy, BuildContext context, - ContactsViewModel viewModel, + ContactController contactController, ) { final box = context.findRenderObject() as RenderBox; final tileHeight = box.size.height / INDEX_BAR_WORDS.length; final index = (localDy ~/ tileHeight).clamp(0, INDEX_BAR_WORDS.length - 1); final letter = INDEX_BAR_WORDS[index]; - viewModel.jumpToIndex(letter, sC, letterKeys); + contactController.jumpToIndex(letter, sC, letterKeys); } - Widget _buildCurrentLetterIndicator(ContactsViewModel viewModel) { - if (viewModel.currentLetter.value.isEmpty) return SizedBox.shrink(); + // Display current letter indicator when dragging + Widget _buildCurrentLetterIndicator(ContactController contactController) { + if (contactController.currentLetter.value.isEmpty) return SizedBox.shrink(); return Center( child: Container( width: 80, @@ -243,7 +160,7 @@ class ContactsPage extends StatelessWidget { ), alignment: Alignment.center, child: Text( - viewModel.currentLetter.value, + contactController.currentLetter.value, style: TextStyle( fontSize: 32, color: Colors.white, @@ -257,30 +174,5 @@ class ContactsPage extends StatelessWidget { // === MOCK INDEX BAR WORDS === const List INDEX_BAR_WORDS = [ - 'A', - 'B', - 'C', - 'D', - 'E', - 'F', - 'G', - 'H', - 'I', - 'J', - 'K', - 'L', - 'M', - 'N', - 'O', - 'P', - 'Q', - 'R', - 'S', - 'T', - 'U', - 'V', - 'W', - 'X', - 'Y', - 'Z', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ]; diff --git a/lib/app/modules/chat/contactModule/contactViews/homePage/chatInfo.dart b/lib/app/modules/chat/contactModule/contactViews/homePage/chatInfo.dart deleted file mode 100644 index 0b2c6df..0000000 --- a/lib/app/modules/chat/contactModule/contactViews/homePage/chatInfo.dart +++ /dev/null @@ -1,308 +0,0 @@ -// import 'package:caller/app/constants/constants.dart'; -// import 'package:flutter/cupertino.dart'; -// import 'package:flutter/material.dart'; -// import 'package:get/get.dart'; - -// class ChatInfoPage extends StatefulWidget { -// // final String id; - -// // ChatInfoPage(this.id); - -// @override -// _ChatInfoPageState createState() => _ChatInfoPageState(); -// } - -// class _ChatInfoPageState extends State { - -// bool isRemind = false; -// bool isTop = false; -// bool isDoNotDisturb = true; - -// Widget buildSwitch(item) { -// return new LabelRow( -// label: item['label'] as String, -// margin: item['label'] == '消息免打扰' ? EdgeInsets.only(top: 10.0) : null, -// isLine: item['label'] != '强提醒', -// isRight: false, -// rightW: new SizedBox( -// height: 25.0, -// child: new CupertinoSwitch( -// value: item['value'] as bool, -// onChanged: (v) {}, -// ), -// ), -// onPressed: () {}, -// ); -// } - -// List body() { -// List switchItems = [ -// {"label": '消息免打扰', 'value': isDoNotDisturb}, -// {"label": '置顶聊天', 'value': isTop}, -// {"label": '强提醒', 'value': isRemind}, -// ]; - -// return [ -// // new ChatMamBer(model: model), -// new LabelRow( -// label: '查找聊天记录', -// margin: EdgeInsets.only(top: 10.0), -// // onPressed: () => Get.to(new SearchPage()), -// ), -// new Column( -// children: switchItems.map(buildSwitch).toList(), -// ), -// new LabelRow( -// label: '设置当前聊天背景', -// margin: EdgeInsets.only(top: 10.0), -// // onPressed: () => Get.to(new ChatBackgroundPage()), -// ), -// new LabelRow( -// label: '清空聊天记录', -// margin: EdgeInsets.only(top: 10.0), -// // onPressed: () { -// // // confirmAlert( -// // // context, -// // // (isOK) { -// // // if (isOK) showToast('敬请期待'); -// // }, -// // tips: '确定删除群的聊天记录吗?', -// // okBtn: '清空', -// // ); -// // }, -// ), -// new LabelRow( -// label: '投诉', -// margin: EdgeInsets.only(top: 10.0), -// // onPressed: () => -// // Get.to(new WebViewPage(url: helpUrl, title: '投诉')), -// ), -// ]; -// } - -// @override -// void initState() { -// super.initState(); -// getInfo(); -// } - -// Future getInfo() async { -// // final List infoList = await getUsersProfile([widget.id]); -// // if (infoList.isEmpty) { -// // // showToast('获取用户信息错误'); -// // return; -// // } -// setState(() { -// // model = infoList[0]; -// }); -// } - -// @override -// Widget build(BuildContext context) { -// return new Scaffold( -// backgroundColor: chatBg, -// appBar: new ComMomBar(title: '聊天信息'), -// body: new SingleChildScrollView( -// child: new Column(children: body()), -// ), -// ); -// } -// } - - - - - - - - -// class LabelRow extends StatelessWidget { -// final String? label; -// final VoidCallback? onPressed; -// final double? labelWidth; -// final bool isRight; -// final bool isLine; -// final String? value; -// final String? rValue; -// final Widget? rightW; -// final EdgeInsetsGeometry? margin; -// final EdgeInsetsGeometry padding; -// final Widget? headW; -// final double lineWidth; - -// LabelRow({ -// this.label, -// this.onPressed, -// this.value, -// this.labelWidth, -// this.isRight = true, -// this.isLine = false, -// this.rightW, -// this.rValue, -// this.margin, -// this.padding = const EdgeInsets.only(top: 15.0, bottom: 15.0, right: 5.0), -// this.headW, -// this.lineWidth = mainLineWidth, -// }); - -// @override -// Widget build(BuildContext context) { -// return Container( -// margin: margin, -// child: TextButton( -// style: TextButton.styleFrom( -// backgroundColor: Colors.white, -// padding: EdgeInsets.all(0), -// ), -// onPressed: onPressed ?? () {}, -// child: Container( -// padding: padding, -// margin: EdgeInsets.only(left: 20.0), -// decoration: BoxDecoration( -// border: isLine -// ? Border(bottom: BorderSide(color: lineColor, width: lineWidth)) -// : null, -// ), -// child: Row( -// children: [ -// if (headW != null) headW!, -// SizedBox( -// width: labelWidth, -// child: Text( -// label ?? '', -// style: TextStyle(fontSize: 17.0), -// ), -// ), -// if (value != null) -// Text( -// value!, -// style: TextStyle( -// color: mainTextColor.withOpacity(0.7), -// ), -// ), -// Spacer(), -// if (rValue != null) -// Text( -// rValue!, -// style: TextStyle( -// color: mainTextColor.withOpacity(0.7), -// fontWeight: FontWeight.w400, -// ), -// ), -// if (rightW != null) rightW!, -// if (isRight) -// Icon( -// CupertinoIcons.right_chevron, -// color: mainTextColor.withOpacity(0.5), -// ) -// else -// Container(width: 10.0), -// ], -// ), -// ), -// ), -// ); -// } -// } - - - - - - -// class ComMomBar extends StatelessWidget implements PreferredSizeWidget { -// const ComMomBar({ -// Key? key, -// this.title = '', -// this.showShadow = false, -// this.rightDMActions, -// this.backgroundColor = appBarColor, -// this.mainColor = Colors.black, -// this.titleW, -// this.bottom, -// this.leadingImg = '', -// this.leadingW, -// }) : super(key: key); - -// final String title; -// final bool showShadow; -// final List? rightDMActions; -// final Color backgroundColor; -// final Color mainColor; -// final Widget? titleW; -// final Widget? leadingW; -// final PreferredSizeWidget? bottom; -// final String leadingImg; - -// @override -// Size get preferredSize => const Size.fromHeight(50.0); - -// Widget? leading(BuildContext context) { -// final bool isShow = Navigator.canPop(context); -// if (isShow) { -// return InkWell( -// child: Container( -// width: 15, -// height: 28, -// child: leadingImg.isNotEmpty -// ? Image.asset(leadingImg) -// : Icon(CupertinoIcons.back, color: mainColor), -// ), -// onTap: () { -// if (Navigator.canPop(context)) { -// FocusScope.of(context).requestFocus(FocusNode()); -// Navigator.pop(context); -// } -// }, -// ); -// } else { -// return null; -// } -// } - -// @override -// Widget build(BuildContext context) { -// return showShadow -// ? Container( -// decoration: BoxDecoration( -// border: Border( -// bottom: BorderSide(color: Colors.grey, width: showShadow ? 0.5 : 0.0), -// ), -// ), -// child: AppBar( -// title: titleW ?? Text( -// title, -// style: TextStyle( -// color: mainColor, -// fontSize: 17.0, -// fontWeight: FontWeight.w600, -// ), -// ), -// backgroundColor: mainColor, -// elevation: 0.0, -// // brightness: Brightness.light, -// leading: leadingW ?? leading(context), -// centerTitle: true, -// actions: rightDMActions ?? [Center()], -// bottom: bottom, -// ), -// ) -// : AppBar( -// title: titleW ?? Text( -// title, -// style: TextStyle( -// color: mainColor, -// fontSize: 17.0, -// fontWeight: FontWeight.w600, -// ), -// ), -// backgroundColor: backgroundColor, -// elevation: 0.0, -// // brightness: Brightness.light, -// leading: leadingW ?? leading(context), -// centerTitle: false, -// bottom: bottom, -// actions: rightDMActions ?? [Center()], -// ); -// } -// } \ No newline at end of file diff --git a/lib/app/modules/chat/contactModule/controllers/contactController.dart b/lib/app/modules/chat/contactModule/controllers/contactController.dart index 165b6f4..81979ec 100644 --- a/lib/app/modules/chat/contactModule/controllers/contactController.dart +++ b/lib/app/modules/chat/contactModule/controllers/contactController.dart @@ -1,14 +1,12 @@ import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; +import 'package:caller/app/constants/constants.dart'; import 'package:get/get.dart'; import 'package:caller/app/models/contactModels/contactModels.dart'; -import 'package:cached_network_image/cached_network_image.dart'; - -// === VIEWMODEL === -class ContactsViewModel extends GetxController { +import 'package:flutter/material.dart'; +class ContactController extends GetxController { final RxList contacts = [].obs; - final RxString currentLetter = ''.obs; + final RxList functionButtons = [].obs; + final RxString currentLetter = ''.obs; Timer? _letterTimer; @@ -19,12 +17,23 @@ class ContactsViewModel extends GetxController { } void _initializeMockData() { - contacts.value = dummyContacts; // your dummy contacts list - contacts.sort((a, b) => a.nameIndex!.compareTo(b.nameIndex!)); + functionButtons.value = [ + ContactItem(avatar: '${contactAssets}ic_new_friend.webp', title: 'friends_circle'), + ContactItem(avatar: '${contactAssets}ic_group.webp', title: 'scan'), + ContactItem(avatar: '${contactAssets}ic_tag.webp', title: 'shake'), + ContactItem(avatar: '${contactAssets}ic_no_public.webp', title: 'search'), + ]; + + contacts.value = dummyContacts; + contacts.sort((a, b) => a.nameIndex!.compareTo(b.nameIndex!)); } + // Jump scroll to the letter header inside contacts list void jumpToIndex( - String letter, ScrollController controller, Map keys) { + String letter, + ScrollController controller, + Map keys, + ) { if (!keys.containsKey(letter)) return; final keyContext = keys[letter]!.currentContext; @@ -36,12 +45,14 @@ class ContactsViewModel extends GetxController { // Offset of header relative to the viewport top final offset = box.localToGlobal(Offset.zero, ancestor: viewport).dy; - // Calculate absolute scroll offset - final targetScrollOffset = controller.offset + offset; + // Calculate absolute scroll offset + height of functionButtons area (to compensate) + final functionButtonsHeight = functionButtons.length * 56.0; - // Animate scroll to position + final targetScrollOffset = controller.offset + offset - functionButtonsHeight; + + // Animate scroll to the calculated position controller.animateTo( - targetScrollOffset, + targetScrollOffset.clamp(0, controller.position.maxScrollExtent), duration: Duration(milliseconds: 300), curve: Curves.easeOut, ); @@ -50,6 +61,7 @@ class ContactsViewModel extends GetxController { _resetLetterTimer(); } + // Reset current letter after 800ms of inactivity void _resetLetterTimer() { _letterTimer?.cancel(); _letterTimer = Timer(Duration(milliseconds: 800), () { diff --git a/lib/app/modules/chat/discoverModule/controller/discover_controller.dart b/lib/app/modules/chat/discoverModule/controller/discover_controller.dart new file mode 100644 index 0000000..a203633 --- /dev/null +++ b/lib/app/modules/chat/discoverModule/controller/discover_controller.dart @@ -0,0 +1,17 @@ +import 'package:get/get.dart'; + +class DiscoverController extends GetxController { + // List of menu items with dynamic labels + final List> menuItems = [ + {'label': 'friends_circle', 'icon': 'assets/images/discover/ff_Icon_album.webp'}, + {'label': 'scan', 'icon': 'assets/images/discover/ff_Icon_qr_code.webp'}, + {'label': 'shake', 'icon': 'assets/images/discover/ff_Icon_shake.webp'}, + {'label': 'search', 'icon': 'assets/images/discover/ff_Icon_browse.webp'}, + {'label': 'nearby_people', 'icon': 'assets/images/discover/ff_Icon_search.webp'}, + {'label': 'drift_bottle', 'icon': 'assets/images/discover/ff_Icon_nearby.webp'}, + {'label': 'nearby_restaurants', 'icon': 'assets/images/discover/ff_Icon_qr_code.webp'}, + {'label': 'shopping', 'icon': 'assets/images/discover/ff_Icon_qr_code.webp'}, + {'label': 'game', 'icon': 'assets/images/discover/game_center_h5.webp'}, + {'label': 'mini_program', 'icon': 'assets/images/discover/mini_program.webp'}, + ]; +} diff --git a/lib/app/modules/chat/discoverModule/views/discoverPage.dart b/lib/app/modules/chat/discoverModule/views/discoverPage.dart index 1a51a80..b63fccc 100644 --- a/lib/app/modules/chat/discoverModule/views/discoverPage.dart +++ b/lib/app/modules/chat/discoverModule/views/discoverPage.dart @@ -1,24 +1,21 @@ import 'package:caller/app/constants/constants.dart'; +import 'package:caller/app/modules/chat/discoverModule/controller/discover_controller.dart'; import 'package:caller/app/modules/chat/discoverModule/widgets/listTileViewWidget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -class DiscoverPage extends StatefulWidget { +class DiscoverPage extends StatelessWidget { const DiscoverPage({super.key}); - @override - State createState() => _DiscoverPageState(); -} - -class _DiscoverPageState extends State { - Widget buildContent(Map item) { + Widget buildContent(Map item,BuildContext context) { bool isShow() { - if (item['name'] == '朋友圈' || - item['name'] == '摇一摇' || - item['name'] == '搜一搜' || - item['name'] == '附近的餐厅' || - item['name'] == '游戏' || - item['name'] == '小程序') { + // Check the item name and return true/false for whether it should show + if (item['label'] == 'friends_circle' || + item['label'] == 'shake' || + item['label'] == 'search' || + item['label'] == 'nearby_restaurants' || + item['label'] == 'game' || + item['label'] == 'mini_program') { return true; } else { return false; @@ -26,23 +23,19 @@ class _DiscoverPageState extends State { } return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.zero, -), - + color: Theme.of(context).cardColor, child: ListTileView( - title: item['name']!, + title: item['label']!.tr, // Use .tr to get the translated label titleStyle: const TextStyle(fontSize: 15.0), isLabel: false, padding: const EdgeInsets.symmetric(vertical: 16.0), icon: item['icon']!, margin: EdgeInsets.only(bottom: isShow() ? 10.0 : 0.0), onPressed: () { - if (item['name'] == '朋友圈') { - // Get.to(WeChatFriendsCircle()); + if (item['label'] == 'friends_circle') { + // Handle navigation or action } else { - // Get.to(LanguagePage()); + // Handle other items } }, ), @@ -51,30 +44,23 @@ class _DiscoverPageState extends State { @override Widget build(BuildContext context) { - final List> data = [ - {'icon': 'assets/images/discover/ff_Icon_album.webp', 'name': '朋友圈'}, - {'icon': 'assets/images/discover/ff_Icon_qr_code.webp', 'name': '扫一扫'}, - {'icon': 'assets/images/discover/ff_Icon_shake.webp', 'name': '摇一摇'}, - {'icon': 'assets/images/discover/ff_Icon_browse.webp', 'name': '看一看'}, - {'icon': 'assets/images/discover/ff_Icon_search.webp', 'name': '搜一搜'}, - {'icon': 'assets/images/discover/ff_Icon_nearby.webp', 'name': '附近的人'}, - {'icon': 'assets/images/discover/ff_Icon_bottle.webp', 'name': '漂流瓶'}, - {'icon': 'assets/images/discover/ff_Icon_qr_code.webp', 'name': '附近的餐厅'}, - {'icon': 'assets/images/discover/ff_Icon_qr_code.webp', 'name': '购物'}, - {'icon': 'assets/images/discover/game_center_h5.webp', 'name': '游戏'}, - {'icon': 'assets/images/discover/mini_program.webp', 'name': '小程序'}, - ]; + final DiscoverController discoverController = Get.find(); return Scaffold( - backgroundColor: appBarColor, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: ScrollConfiguration( - behavior: MyBehavior(), + behavior: ScrollBehavior(), child: SingleChildScrollView( - child: Column(children: data.map(buildContent).toList()), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: discoverController.menuItems + .map((item) => buildContent(item,context)) + .toList(), + ), + ), ), ), ); } } - -class MyBehavior extends ScrollBehavior {} diff --git a/lib/app/modules/chat/discoverModule/widgets/listTileViewWidget.dart b/lib/app/modules/chat/discoverModule/widgets/listTileViewWidget.dart index f89b34d..57f7b8f 100644 --- a/lib/app/modules/chat/discoverModule/widgets/listTileViewWidget.dart +++ b/lib/app/modules/chat/discoverModule/widgets/listTileViewWidget.dart @@ -42,19 +42,18 @@ class ListTileView extends StatelessWidget { if (label != null) Text( label!, - style: TextStyle(color: mainTextColor, fontSize: 12), ), ], ); var view = [ - isLabel ? text : Text(title, style: titleStyle), + isLabel ? text : Text(title, style: TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color,)), Spacer(), Container( width: 7.0, child: Image.asset( 'assets/images/ic_right_arrow_grey.webp', - color: mainTextColor.withOpacity(0.5), + color: Theme.of(context).iconTheme.color, fit: BoxFit.cover, ), ), @@ -78,14 +77,21 @@ class ListTileView extends StatelessWidget { ); return Container( + color: Theme.of(context).cardColor, margin: margin, - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.white, - padding: EdgeInsets.all(0), - ), - onPressed: onPressed ?? () {}, - child: row, + child: Column( + children: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).cardColor, + padding: EdgeInsets.all(0), + ), + onPressed: onPressed ?? () {}, + child: row, + ), + Divider(thickness: 0.3, + height: 0.1, color: Theme.of(context).dividerColor), + ], ), ); } diff --git a/lib/app/modules/chat/mineModule/controller/mineController.dart b/lib/app/modules/chat/mineModule/controller/mineController.dart index 7d74315..3a67aee 100644 --- a/lib/app/modules/chat/mineModule/controller/mineController.dart +++ b/lib/app/modules/chat/mineModule/controller/mineController.dart @@ -7,13 +7,13 @@ class MineViewModel extends GetxController { final RxString nickName = '张三'.obs; final RxString account = 'wxid_123456'.obs; - // Mock data for menu items + // Menu items with dynamic labels final List> menuItems = [ - {'label': '支付', 'icon': 'assets/images/mine/ic_pay.png'}, - {'label': '收藏', 'icon': 'assets/images/favorite.webp'}, - {'label': '相册', 'icon': 'assets/images/mine/ic_card_package.png'}, - {'label': '卡片', 'icon': 'assets/images/mine/ic_card_package.png'}, - {'label': '表情', 'icon': 'assets/images/mine/ic_emoji.png'}, - {'label': '设置', 'icon': 'assets/images/mine/ic_setting.png'}, + {'label': 'pay', 'icon': 'assets/images/mine/ic_pay.png'}, + {'label': 'favorites', 'icon': 'assets/images/favorite.webp'}, + {'label': 'album', 'icon': 'assets/images/mine/ic_card_package.png'}, + {'label': 'cards', 'icon': 'assets/images/mine/ic_card_package.png'}, + {'label': 'emojis', 'icon': 'assets/images/mine/ic_emoji.png'}, + {'label': 'settings', 'icon': 'assets/images/mine/ic_setting.png'}, ]; -} \ No newline at end of file +} diff --git a/lib/app/modules/chat/mineModule/views/homePage/mineHomePage.dart b/lib/app/modules/chat/mineModule/views/homePage/mineHomePage.dart new file mode 100644 index 0000000..b02177d --- /dev/null +++ b/lib/app/modules/chat/mineModule/views/homePage/mineHomePage.dart @@ -0,0 +1,231 @@ +import 'package:caller/app/bindings/themeController.dart'; +import 'package:caller/app/constants/constants.dart' as AppColors; +import 'package:caller/app/modules/chat/discoverModule/widgets/ImageWidget.dart'; +import 'package:caller/app/modules/chat/discoverModule/widgets/listTileViewWidget.dart'; +import 'package:caller/app/modules/chat/mineModule/controller/mineController.dart'; +import 'package:caller/app/modules/chat/mineModule/views/settingsPage/settingPage.dart'; +import 'package:caller/translations/controller/language_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +// Main View +class MinePage extends StatelessWidget { + MinePage({super.key}); + + final MineViewModel viewModel = Get.put(MineViewModel()); + final LanguageController languageController = Get.put(LanguageController()); + final ThemeController themeController = Get.find(); + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: SingleChildScrollView(child: _buildBody(context)), + ); + } + + Widget _buildBody(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Column( + children: [ + _buildUserInfoSection(context), + SizedBox(height: 16.0), + _buildMenuList(context), + Container( + color: Theme.of(context).cardColor, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon(Icons.language), + SizedBox(width: 10), + Text( + "switch_language".tr, + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + ), + ), + Spacer(), + + // Obx(() { + // return Text( + // languageController.isEnglish.value ? 'English' : 'Chinese', + // style: TextStyle(fontSize: 18), + // ); + // }), + SizedBox(width: 20), + + Padding( + padding: const EdgeInsets.all(8.0), + child: Switch( + activeColor: Theme.of(context).cardColor, + inactiveThumbColor: Theme.of( + context, + ).textTheme.bodyLarge!.color, + activeTrackColor: Theme.of( + context, + ).textTheme.bodyLarge!.color, + inactiveTrackColor: Theme.of(context).cardColor, + value: languageController.isEnglish.value, + onChanged: (value) { + languageController.changeLanguage( + value ? 'en' : 'zh', + ); + }, + ), + ), + ], + ), + ), + Obx(() { + final isDark = themeController.isDarkMode; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.change_circle), + SizedBox(width: 10), + Text( + "theme".tr, + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Switch( + activeColor: Theme.of(context).cardColor, + inactiveThumbColor: Theme.of( + context, + ).textTheme.bodyLarge!.color, + activeTrackColor: Theme.of( + context, + ).textTheme.bodyLarge!.color, + inactiveTrackColor: Theme.of(context).cardColor, + value: isDark, + onChanged: (value) { + themeController.toggleTheme(value); + }, + ), + ), + ], + ), + ); + }), + ], + ), + ), + ], + ), + ); + } + + // User info section (avatar, name, account) + Widget _buildUserInfoSection(BuildContext context) { + return Obx( + () => InkWell( + onTap: () {}, + child: Container( + color: Theme.of(context).cardColor, + child: ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: CachedNetworkImage( + imageUrl: viewModel.avatar.value.isNotEmpty + ? viewModel.avatar.value + : "https://picsum.photos/id/1/200/200", + width: 80, + height: 80, + fit: BoxFit.cover, + ), + ), + title: Text('文件传输助手'), + subtitle: Text('微信号:wx_000001'), + trailing: IconButton( + icon: ImageIcon( + AssetImage("assets/images/mine/addfriend_icon_myqr.png"), + size: 24.0, + ), + onPressed: () {}, + ), + ), + ), + ), + ); + } + + // Menu list (支付、收藏、相册等) + Widget _buildMenuList(BuildContext context) { + return Column( + children: viewModel.menuItems + .map((item) => _buildMenuItem(item, context)) + .toList(), + ); + } + + // Individual menu item + Widget _buildMenuItem(Map item, BuildContext context) { + String translatedLabel = item['label']!.tr; + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.zero, + color: Theme.of(context).cardColor, + ), + child: ListTileView( + border: _shouldShowBorder(item['label']) + ? Border(bottom: BorderSide(color: AppColors.lineColor, width: 0.2)) + : null, + title: translatedLabel, + titleStyle: const TextStyle(fontSize: 15.0), + isLabel: false, + padding: const EdgeInsets.symmetric(vertical: 16.0), + icon: item['icon']!, + margin: EdgeInsets.symmetric( + vertical: _shouldAddVerticalMargin(item['label']) ? 10.0 : 0.0, + ), + onPressed: () => _handleMenuItemTap(item['label'],context), + width: 25.0, + fit: BoxFit.cover, + horizontal: 15.0, + ), + ); + } + + // Helper: Check if border should be shown + bool _shouldShowBorder(String? label) { + return label != '支付' && label != '设置' && label != '表情'; + } + + // Helper: Check if vertical margin should be added + bool _shouldAddVerticalMargin(String? label) { + return label == '支付' || label == '设置'; + } + +void _handleMenuItemTap(String? label, BuildContext context) { + print('Tapped label: "$label"'); + if (label == 'pay') { + + } else if (label == 'favorites') { + } else if (label == 'album') { + } else if (label == 'cards') { + } else if (label == 'emojis') { + } else if (label == 'settings') { + print('settings'); + Get.to(() => SettingPage()); + } else { + print('No action for $label'); + } +} + + + // Calculate top bar height + double topBarHeight() { + return MediaQuery.of(Get.context!).padding.top + kToolbarHeight; + } +} diff --git a/lib/app/modules/chat/mineModule/views/minePage.dart b/lib/app/modules/chat/mineModule/views/minePage.dart deleted file mode 100644 index 0133bf6..0000000 --- a/lib/app/modules/chat/mineModule/views/minePage.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:caller/app/constants/constants.dart' as AppColors; -import 'package:caller/app/modules/chat/discoverModule/widgets/ImageWidget.dart'; -import 'package:caller/app/modules/chat/discoverModule/widgets/listTileViewWidget.dart'; -import 'package:caller/app/modules/chat/mineModule/controller/mineController.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:cached_network_image/cached_network_image.dart'; - - -// Main View -class MinePage extends StatelessWidget { - MinePage({super.key}); - - final MineViewModel viewModel = Get.put(MineViewModel()); - - @override - Widget build(BuildContext context) { - return Container( - color: AppColors.appBarColor, - child: Scaffold( - backgroundColor: AppColors.appBarColor, - body: SingleChildScrollView( - child: _buildBody(), - ), - ), - ); - } - - Widget _buildBody() { - return Column( - children: [ - _buildUserInfoSection(), - SizedBox(height: 16.0), - _buildMenuList(), - ], - ); - } - - // User info section (avatar, name, account) - Widget _buildUserInfoSection() { - return Obx(() => InkWell( - onTap: () {}, - child: Container( - decoration: BoxDecoration( - color: Colors.white -), - child: ListTile( - leading: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: CachedNetworkImage( - imageUrl: viewModel.avatar.value.isNotEmpty - ? viewModel.avatar.value - : "https://picsum.photos/id/1/200/200", - width: 80, - height: 80, - fit: BoxFit.cover, - ), - ), - title: Text('文件传输助手'), - subtitle: Text('微信号:wx_000001'), - trailing: IconButton( - icon: ImageIcon( - AssetImage("assets/images/mine/addfriend_icon_myqr.png"), - size: 24.0, - ), - onPressed: () {}, - ), - ), - ), - )); -} - // Avatar widget (handles mock/default avatar) - Widget _buildAvatar() { - if (viewModel.avatar.value.isNotEmpty) { - return ImageView( - img: viewModel.avatar.value, - width: 60, - height: 60, - fit: BoxFit.fill, - ); - } else { - return Icon(Icons.person ); - } - } - - // Menu list (支付、收藏、相册等) - Widget _buildMenuList() { - return Column( - children: viewModel.menuItems - .map((item) => _buildMenuItem(item)) - .toList(), - ); - } - - // Individual menu item - Widget _buildMenuItem(Map item) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.zero, -), - child: ListTileView( - border: _shouldShowBorder(item['label']) - ? Border( - bottom: BorderSide( - color: AppColors.lineColor, - width: 0.2, - ), - ) - : null, - title: item['label']!, - titleStyle: const TextStyle(fontSize: 15.0), - isLabel: false, - padding: const EdgeInsets.symmetric(vertical: 16.0), - icon: item['icon']!, - margin: EdgeInsets.symmetric( - vertical: _shouldAddVerticalMargin(item['label']) ? 10.0 : 0.0, - ), - onPressed: () => _handleMenuItemTap(item['label']), - width: 25.0, - fit: BoxFit.cover, - horizontal: 15.0, - ), - ); - } - - // Helper: Check if border should be shown - bool _shouldShowBorder(String? label) { - return label != '支付' && label != '设置' && label != '表情'; - } - - // Helper: Check if vertical margin should be added - bool _shouldAddVerticalMargin(String? label) { - return label == '支付' || label == '设置'; - } - - // Handle menu item taps (replace with your navigation logic) - void _handleMenuItemTap(String? label) { - switch (label) { - case '支付': - // Get.to(() => const PayHomePage()); // Replace with your page - break; - case '设置': - // Add your settings navigation - break; - // Add other cases as needed - } - } - - // Navigate to personal info page (replace with your page) - void _navigateToPersonalInfo() { - // Get.to(() => const PersonalInfoPage()); - } - - // Calculate top bar height - double topBarHeight() { - return MediaQuery.of(Get.context!).padding.top + kToolbarHeight; - } -} - diff --git a/lib/app/modules/chat/mineModule/views/settingsPage/settingPage.dart b/lib/app/modules/chat/mineModule/views/settingsPage/settingPage.dart new file mode 100644 index 0000000..b8ce311 --- /dev/null +++ b/lib/app/modules/chat/mineModule/views/settingsPage/settingPage.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class SettingPage extends StatelessWidget { + const SettingPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center(child: Text("under construction")) + ],), + ); + } +} \ No newline at end of file diff --git a/lib/app/modules/selfMedia/video/views/videoFeed.dart b/lib/app/modules/selfMedia/video/views/videoFeed.dart index 8707bbd..4667aa7 100644 --- a/lib/app/modules/selfMedia/video/views/videoFeed.dart +++ b/lib/app/modules/selfMedia/video/views/videoFeed.dart @@ -235,8 +235,6 @@ // } // } - - // import 'package:flutter/material.dart'; // import 'package:cached_video_player_plus/cached_video_player_plus.dart'; @@ -438,10 +436,13 @@ import 'package:caller/app/modules/chat/MessageModule/views/messageChatViews/ChatHomePage.dart'; import 'package:flutter/material.dart'; import 'package:cached_video_player_plus/cached_video_player_plus.dart'; +import 'package:flutter_vlc_player/flutter_vlc_player.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../../../constants/colors/colors.dart'; import '../services/videoPreloader.dart'; import '../widgets/videoWidget.dart'; +import 'package:get/get.dart'; class VideoFeedScreen extends StatefulWidget { const VideoFeedScreen({Key? key}) : super(key: key); @@ -450,7 +451,8 @@ class VideoFeedScreen extends StatefulWidget { _VideoFeedScreenState createState() => _VideoFeedScreenState(); } -class _VideoFeedScreenState extends State { +class _VideoFeedScreenState extends State + with WidgetsBindingObserver { final PageController _pageController = PageController(); final VideoPreloader _preloader = VideoPreloader(); @@ -464,11 +466,14 @@ class _VideoFeedScreenState extends State { ]; int currentIndex = 0; + final Map _controllers = {}; + final Map _rtmpControllers = {}; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _initializePreloader(); _loadSavedIndex(); } @@ -532,6 +537,7 @@ class _VideoFeedScreenState extends State { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _controllers.forEach((key, controller) { controller.dispose(); }); @@ -539,17 +545,51 @@ class _VideoFeedScreenState extends State { super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused || + state == AppLifecycleState.inactive || + state == AppLifecycleState.detached) { + for (var controller in _controllers.values) { + if (controller.value.isInitialized) { + controller.setVolume(0); + controller.pause(); + } + } + } + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( - backgroundColor: Colors.black, - title: const Text('Video Feed'), + backgroundColor: AppColors.primary, + centerTitle: true, + leading: null, + title: Text( + 'self_media_title'.tr, + style: TextStyle(color: Colors.white), + ), + actions: [ - IconButton( - icon: const Icon(Icons.switch_account), - onPressed: ()=>Navigator.pushReplacement(context, MaterialPageRoute(builder: (_)=>ChatHomeScreen())), + IconButton( + icon: Icon(Icons.switch_account,), + onPressed: () async { + for (var controller in _controllers.values) { + if (controller.value.isInitialized) { + controller.setVolume(0); + await controller.pause(); + } + } + await Future.delayed(Duration(milliseconds: 100)); + if (mounted) { + await Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => ChatHomeScreen()), + ); + } + }, ), // IconButton( // icon: const Icon(Icons.info), @@ -610,7 +650,9 @@ class _VideoFeedScreenState extends State { children: [ Text('Cache Size: $cacheSize'), Text('Cached Videos: $cachedCount'), - Text('Max Cache Size: ${(_preloader.maxCacheSize / (1024 * 1024)).toStringAsFixed(0)}MB'), + Text( + 'Max Cache Size: ${(_preloader.maxCacheSize / (1024 * 1024)).toStringAsFixed(0)}MB', + ), Text('Preload Count: ${_preloader.maxPreloadCount}'), ], ), @@ -629,7 +671,9 @@ class _VideoFeedScreenState extends State { context: context, builder: (context) => AlertDialog( title: const Text('Clear Cache'), - content: const Text('Are you sure you want to clear all cached videos?'), + content: const Text( + 'Are you sure you want to clear all cached videos?', + ), actions: [ TextButton( onPressed: () => Navigator.pop(context), diff --git a/lib/main.dart b/lib/main.dart index deea86f..c63bab2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ +import 'package:caller/app/bindings/controllersBindings.dart'; +import 'package:caller/app/bindings/themeController.dart'; import 'package:caller/app/modules/chat/MessageModule/controllers/audioPlayerController.dart'; import 'package:caller/app/modules/chat/MessageModule/views/messageChatViews/ChatHomePage.dart'; -import 'package:caller/app/modules/chat/contactModule/contactViews/homePage/chatInfo.dart'; import 'package:caller/app/modules/chat/MessageModule/views/messageChatViews/chatLayout.dart'; import 'package:caller/app/modules/chat/MessageModule/views/createroomPage/views/roomCheckPage.dart'; import 'package:caller/app/modules/chat/MessageModule/views/room/views/connect.dart'; @@ -8,12 +9,15 @@ import 'package:caller/app/modules/chat/MessageModule/views/room/views/liveKitRo import 'package:caller/app/modules/chat/MessageModule/views/room/views/room.dart'; import 'package:caller/app/modules/chat/MessageModule/views/room/views/viewerPage.dart'; import 'package:caller/app/constants/services/theme.dart'; +import 'package:caller/translations/controller/language_controller.dart'; +import 'package:caller/translations/en_Us.dart'; +import 'package:caller/translations/zh_CN.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:intl/intl.dart'; import 'package:get/get.dart'; -import 'package:permission_handler/permission_handler.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'app/modules/selfMedia/video/views/videoFeed.dart'; @@ -27,15 +31,18 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.dark, - )); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + ), + ); - await _requestPermissions(); + await _requestPermissions(); runApp(const LiveKitExampleApp()); } + // Function to request permissions Future _requestPermissions() async { Map statuses = await [ @@ -54,16 +61,35 @@ Future _requestPermissions() async { } }); } + class LiveKitExampleApp extends StatelessWidget { // - const LiveKitExampleApp({ - super.key, - }); + const LiveKitExampleApp({super.key}); @override - Widget build(BuildContext context) => GetMaterialApp( + Widget build(BuildContext context) { + final ThemeController themeController = Get.put(ThemeController()); + return Obx(() { + return GetMaterialApp( + theme: lightTheme, + darkTheme: darkTheme, + themeMode: themeController.themeMode.value, + initialBinding: AppBindings(), + locale: Locale('en', 'US'), + translations: MyTranslations(), + fallbackLocale: Locale('en', 'US'), title: 'LiveKit', // theme: LiveKitTheme().buildThemeData(context), - home: ChatHomeScreen(), + home: ChatHomeScreen(), ); + }); + } +} + +class MyTranslations extends Translations { + @override + Map> get keys => { + 'en_US': EnUs.values, + 'zh_CN': ZhCn.values, + }; } diff --git a/lib/translations/controller/language_controller.dart b/lib/translations/controller/language_controller.dart new file mode 100644 index 0000000..b201d0c --- /dev/null +++ b/lib/translations/controller/language_controller.dart @@ -0,0 +1,18 @@ +// lib/controllers/language_controller.dart +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class LanguageController extends GetxController { + var isEnglish = true.obs; + // Method to change the language + void changeLanguage(String languageCode) { + print("Changing language to: $languageCode"); + if (languageCode == 'en') { + Get.updateLocale(Locale('en', 'US')); + isEnglish.value = true; + } else if (languageCode == 'zh') { + Get.updateLocale(Locale('zh', 'CN')); + isEnglish.value = false; + } + } +} \ No newline at end of file diff --git a/lib/translations/en_Us.dart b/lib/translations/en_Us.dart new file mode 100644 index 0000000..ae8884a --- /dev/null +++ b/lib/translations/en_Us.dart @@ -0,0 +1,59 @@ +class EnUs { + static const Map values = { + //mine screen + 'pay': 'Pay', + 'favorites': 'Favorites', + 'album': 'Album', + 'cards': 'Cards', + 'emojis': 'Emojis', + 'settings': 'Settings', + 'switch_language': 'Switch Language', + 'theme':'Switch Theme', + //discover screen + 'friends_circle': 'Friends Circle', + 'scan': 'Scan', + 'shake': 'Shake', + 'search': 'Search', + 'nearby_people': 'Nearby People', + 'drift_bottle': 'Drift Bottle', + 'nearby_restaurants': 'Nearby Restaurants', + 'shopping': 'Shopping', + 'game': 'Game', + 'mini_program': 'Mini Program', + //navbar + 'app_bar_title': 'Chat Module', + 'self_media_title':'Self Media', + 'message':'Message', + 'contacts':'Contacts', + 'discover':'Discover', + 'mine':'Mine', + //chat info screen + 'search_chat':'Search Chat History', + 'mute_notification':'Mute Notifications', + 'sticky_top':'Sticky on Top', + 'alert':'Alert', + 'background':'Background', + 'clear_chat_history':'Clear Chat History', + 'report':'Report', + 'chat_info':'Chat Info', + // Search Screen + 'filter_by':'Filter by', + 'date':'Date', + 'photos_and_videos':'Photos & Videos', + 'files':'Files', + 'links':'Links', + 'music_and_audio':'Music & Audio', + 'transactions':'Transactions', + 'mini':'Mini Program', + 'channels':'Channels', + 'cancel':'Cancel', + 'search_results_for':'Search results for: ', + 'leave_group':'Leave Group', + 'group_name':'Group Name', + 'group_qr_code':'Group QR Code', + 'group_notice':'Group Notice' + + + + }; +} diff --git a/lib/translations/zh_CN.dart b/lib/translations/zh_CN.dart new file mode 100644 index 0000000..9c1f36a --- /dev/null +++ b/lib/translations/zh_CN.dart @@ -0,0 +1,56 @@ +class ZhCn { + static const Map values = { + //mine screen + 'pay': '支付', + 'favorites': '收藏', + 'album': '相册', + 'cards': '卡片', + 'emojis': '表情', + 'settings': '设置', + 'switch_language': '切换语言', + 'theme': '切换主题', + //discover screen + 'friends_circle': '朋友圈', + 'scan': '扫一扫', + 'shake': '摇一摇', + 'search': '搜一搜', + 'nearby_people': '附近的人', + 'drift_bottle': '漂流瓶', + 'nearby_restaurants': '附近的餐厅', + 'shopping': '购物', + 'game': '游戏', + 'mini_program': '小程序', + //navbar + 'app_bar_title': '聊天模块', + 'self_media_title': '我的媒体', + 'message': '消息', + 'contacts': '联系人', + 'discover': '发现', + 'mine': '我的', + //chat info screen + 'search_chat': '搜索聊天记录', + 'mute_notification': '静音通知', + 'sticky_top': '置顶聊天', + 'alert': '举报', + 'background': '背景', + 'clear_chat_history': '清空聊天记录', + 'report': '举报', + 'chat_info': '聊天信息', + // Search Screen + 'filter_by': '筛选', + 'date': '日期', + 'photos_and_videos': '图片和视频', + 'files': '文件', + 'links': '链接', + 'music_and_audio': '音乐和音频', + 'transactions': '交易', + 'mini': '小程序', + 'channels': '频道', + 'search_results_for': '搜索结果:', + 'cancel': '取消', + 'leave_group': '离开群组', + 'group_name': '群组名称', + 'group_qr_code': '群组二维码', + 'group_notice': '群组公告' + }; +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a93d36d..f6453be 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_localization_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin"); + flutter_localization_plugin_register_with_registrar(flutter_localization_registrar); g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 394ed7b..4559aaf 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST emoji_picker_flutter file_selector_linux + flutter_localization flutter_webrtc record_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 51a0a25..7d70c65 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import device_info_plus import emoji_picker_flutter import file_picker import file_selector_macos +import flutter_localization import flutter_webrtc import just_audio import livekit_client @@ -30,6 +31,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 4f1a126..887f8f1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.3" + dotted_decoration: + dependency: "direct main" + description: + name: dotted_decoration + sha256: a5c5771367690b4f64ebfa7911954ab472b9675f025c373f514e32ac4bb81d5e + url: "https://pub.dev" + source: hosted + version: "2.0.0" dropdown_button2: dependency: "direct main" description: @@ -334,6 +342,19 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localization: + dependency: "direct main" + description: + name: flutter_localization + sha256: "578a73455a0deffc4169ef9372ba0562a3e2cff563e5c524ea87bc96daa519c0" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1365,6 +1386,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" youtube_explode_dart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b664f2a..9c35578 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,9 @@ environment: dependencies: flutter: sdk: flutter + flutter_localization: ^0.3.3 + + # ffmpeg_kit_flutter_new: ^2.0.0 visibility_detector: ^0.4.0+2 flutter_vlc_player: ^7.4.3 @@ -57,6 +60,7 @@ dependencies: crypto: ^3.0.6 flutter_background: ^1.1.0 intl: ^0.20.2 + dotted_decoration: ^2.0.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -84,6 +88,7 @@ dev_dependencies: + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ec6cf8d..424c309 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterLocalizationPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); LiveKitPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 070ea08..b8b15bf 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus emoji_picker_flutter file_selector_windows + flutter_localization flutter_webrtc livekit_client permission_handler_windows