Chat module

This commit is contained in:
2025-07-09 18:32:34 +08:00
parent 3f1f9c2489
commit bd8a7ad5ef
37 changed files with 1992 additions and 990 deletions

View File

@@ -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());
}
}

View File

@@ -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,
),
);

View File

@@ -9,6 +9,7 @@ class ChatContactModel {
final String? statusMessage;
final String? lastMessage;
final DateTime? lastMessageTime;
final bool isGroup;
ChatContactModel({
required this.id,
@@ -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: '文件传输助手',

View File

@@ -0,0 +1,65 @@
import 'package:caller/app/models/contactModels/contactModels.dart';
class GroupDetails {
final String groupId;
final List<ChatContactModel> 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'),
),
];

View File

@@ -63,7 +63,24 @@ Future<void> 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.');
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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<ChatHomeScreen> {
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<ChatHomeScreen> {
),
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<ChatHomeScreen> {
_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,
),
],
),

View File

@@ -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<ChatContactModel> 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("")
],
),
),
);
}
},
),
),
);
}
}

View File

@@ -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<ChatSearchController>();
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}',
),
);
}),
),
],
),
),
),
);
}
}

View File

@@ -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<ChatInfoPage> createState() => _ChatInfoPageState();
}
class _ChatInfoPageState extends State<ChatInfoPage> {
final ChatInfoController controller = Get.find<ChatInfoController>();
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)),
),
],
),
);
},
),
),
],
),
);
}
}

View File

@@ -6,18 +6,42 @@ 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),
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),
@@ -46,6 +70,13 @@ class ChatListPage extends StatelessWidget {
],
),
onTap: () => _openChat(context, contact),
),
Divider(
thickness: 0.3,
height: 0.1,
color: Theme.of(context).dividerColor,
),
],
);
},
),

View File

@@ -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,6 +20,9 @@ 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;
ChatMessageScreen({Key? key, required this.contact}) : super(key: key);
@@ -27,7 +32,8 @@ class ChatMessageScreen extends StatefulWidget {
State<ChatMessageScreen> createState() => _ChatMessageScreenState();
}
class _ChatMessageScreenState extends State<ChatMessageScreen> with TickerProviderStateMixin {
class _ChatMessageScreenState extends State<ChatMessageScreen>
with TickerProviderStateMixin {
final FocusNode _focusNode = FocusNode();
late AnimationController _emojiController;
late Animation<Offset> _emojiOffset;
@@ -96,7 +102,6 @@ void _updateRecordingDialog(String text, Color color) {
(recordingDialogContext as Element).markNeedsBuild();
}
@override
void initState() {
super.initState();
@@ -124,7 +129,23 @@ 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(() {});
}
}
@@ -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,16 +194,54 @@ 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<ChatMessageModel> _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();
widget.chatController.isEmojiPickerVisible.value = false;
_emojiController.reverse();
widget.chatController.isAttachmentPanelVisible.value = false;
},
child: Scaffold(
backgroundColor: Colors.black,
@@ -177,23 +254,56 @@ void _updateRecordingDialog(String text, Color color) {
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.more_vert,color: Colors.white,),
icon: const Icon(Icons.more_vert, color: Colors.white),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context)=> ContactInfoPage(contact: widget.contact),));
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),
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: 8),
Text(widget.contact.name,style: TextStyle(color: Colors.white),),
SizedBox(width: 16),
Text(widget.contact.name, style: TextStyle(color: Colors.white)),
],
),
),
body: SafeArea(
child: Column(
@@ -521,12 +631,18 @@ void _updateRecordingDialog(String text, Color color) {
if (dragOffset < -50) {
if (!isCancelled) {
isCancelled = true;
_updateRecordingDialog("Release to cancel", Colors.redAccent);
_updateRecordingDialog(
"Release to cancel",
Colors.redAccent,
);
}
} else {
if (isCancelled) {
isCancelled = false;
_updateRecordingDialog("Slide up to cancel", Colors.white70);
_updateRecordingDialog(
"Slide up to cancel",
Colors.white70,
);
}
}
},
@@ -545,14 +661,18 @@ void _updateRecordingDialog(String text, Color color) {
color: Colors.grey.withOpacity(0.25),
borderRadius: BorderRadius.circular(5),
),
child: Obx(() => Text(
child: Obx(
() => Text(
"Hold to talk",
style: TextStyle(
color: widget.chatController.isRecording.value ? Colors.red : Colors.white,
color: widget.chatController.isRecording.value
? Colors.red
: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
)),
),
),
),
);
} else {
@@ -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 =
@@ -657,7 +802,11 @@ Widget _buildAttachmentPanel() {
children: [
_buildAttachmentButton(Icons.camera_alt, 'Camera', _openCamera),
_buildAttachmentButton(Icons.image, 'Gallery', _pickFromGallery),
_buildAttachmentButton(Icons.insert_drive_file, 'Document', _pickDocument),
_buildAttachmentButton(
Icons.insert_drive_file,
'Document',
_pickDocument,
),
_buildAttachmentButton(Icons.video_call, 'Call', _showCallOptions),
],
),
@@ -701,7 +850,10 @@ void _showCallOptions() {
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text('Make a call', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
child: Text(
'Make a call',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
ListTile(
leading: const Icon(Icons.call, color: Colors.green),
@@ -727,16 +879,13 @@ void _showCallOptions() {
}
void _startAudioCall() {
// TODO: your audio call logic
print("Starting audio call...");
}
void _startVideoCall() {
// TODO: your video call logic
print("Starting video call...");
}
Widget _buildEmojiPickerContainer() {
return Obx(() {
if (!widget.chatController.isEmojiPickerVisible.value) {

View File

@@ -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,
),
);
}
}

View File

@@ -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
),
),
],
);
}

View File

@@ -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<ChatContactModel> contacts = <ChatContactModel>[].obs;
final RxList<ContactItem> functionButtons = <ContactItem>[].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<String, GlobalKey> 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<String, GlobalKey> letterKeys = {};
@override
Widget build(BuildContext context) {
final viewModel = Get.put(ContactsViewModel());
// Access ContactController
final contactController = Get.find<ContactController>();
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<String> 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',
];

View File

@@ -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<ChatInfoPage> {
// 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<Widget> 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<void>(new SearchPage()),
// ),
// new Column(
// children: switchItems.map(buildSwitch).toList(),
// ),
// new LabelRow(
// label: '设置当前聊天背景',
// margin: EdgeInsets.only(top: 10.0),
// // onPressed: () => Get.to<void>(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<void>(new WebViewPage(url: helpUrl, title: '投诉')),
// ),
// ];
// }
// @override
// void initState() {
// super.initState();
// getInfo();
// }
// Future<void> getInfo() async {
// // final List<V2TimUserFullInfo> 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: <Widget>[
// 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<Widget>? 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()],
// );
// }
// }

View File

@@ -1,13 +1,11 @@
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<ChatContactModel> contacts = <ChatContactModel>[].obs;
final RxList<ContactItem> functionButtons = <ContactItem>[].obs;
final RxString currentLetter = ''.obs;
Timer? _letterTimer;
@@ -19,12 +17,23 @@ class ContactsViewModel extends GetxController {
}
void _initializeMockData() {
contacts.value = dummyContacts; // your dummy contacts list
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<String, GlobalKey> keys) {
String letter,
ScrollController controller,
Map<String, GlobalKey> 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), () {

View File

@@ -0,0 +1,17 @@
import 'package:get/get.dart';
class DiscoverController extends GetxController {
// List of menu items with dynamic labels
final List<Map<String, String>> 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'},
];
}

View File

@@ -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<DiscoverPage> createState() => _DiscoverPageState();
}
class _DiscoverPageState extends State<DiscoverPage> {
Widget buildContent(Map<String, String> item) {
Widget buildContent(Map<String, String> 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<DiscoverPage> {
}
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<void>(WeChatFriendsCircle());
if (item['label'] == 'friends_circle') {
// Handle navigation or action
} else {
// Get.to<void>(LanguagePage());
// Handle other items
}
},
),
@@ -51,30 +44,23 @@ class _DiscoverPageState extends State<DiscoverPage> {
@override
Widget build(BuildContext context) {
final List<Map<String, String>> 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<DiscoverController>();
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 {}

View File

@@ -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,15 +77,22 @@ class ListTileView extends StatelessWidget {
);
return Container(
color: Theme.of(context).cardColor,
margin: margin,
child: TextButton(
child: Column(
children: [
TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.white,
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),
],
),
);
}
}

View File

@@ -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<Map<String, String>> 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'},
];
}

View File

@@ -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<String, String> 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;
}
}

View File

@@ -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<String, String> 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;
}
}

View File

@@ -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"))
],),
);
}
}

View File

@@ -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<VideoFeedScreen> {
class _VideoFeedScreenState extends State<VideoFeedScreen>
with WidgetsBindingObserver {
final PageController _pageController = PageController();
final VideoPreloader _preloader = VideoPreloader();
@@ -464,11 +466,14 @@ class _VideoFeedScreenState extends State<VideoFeedScreen> {
];
int currentIndex = 0;
final Map<int, CachedVideoPlayerPlusController> _controllers = {};
final Map<int, VlcPlayerController> _rtmpControllers = {};
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initializePreloader();
_loadSavedIndex();
}
@@ -532,6 +537,7 @@ class _VideoFeedScreenState extends State<VideoFeedScreen> {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_controllers.forEach((key, controller) {
controller.dispose();
});
@@ -539,17 +545,51 @@ class _VideoFeedScreenState extends State<VideoFeedScreen> {
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())),
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<VideoFeedScreen> {
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<VideoFeedScreen> {
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),

View File

@@ -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,6 +9,9 @@ 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';
@@ -28,14 +32,17 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
));
),
);
await _requestPermissions();
runApp(const LiveKitExampleApp());
}
// Function to request permissions
Future<void> _requestPermissions() async {
Map<Permission, PermissionStatus> statuses = await [
@@ -54,16 +61,35 @@ Future<void> _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(),
);
});
}
}
class MyTranslations extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'en_US': EnUs.values,
'zh_CN': ZhCn.values,
};
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,59 @@
class EnUs {
static const Map<String, String> 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'
};
}

View File

@@ -0,0 +1,56 @@
class ZhCn {
static const Map<String, String> 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': '群组公告'
};
}

View File

@@ -8,6 +8,7 @@
#include <emoji_picker_flutter/emoji_picker_flutter_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_localization/flutter_localization_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <record_linux/record_linux_plugin.h>
@@ -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);

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
emoji_picker_flutter
file_selector_linux
flutter_localization
flutter_webrtc
record_linux
)

View File

@@ -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"))

View File

@@ -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:

View File

@@ -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

View File

@@ -9,6 +9,7 @@
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <emoji_picker_flutter/emoji_picker_flutter_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_localization/flutter_localization_plugin_c_api.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <livekit_client/live_kit_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
@@ -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(

View File

@@ -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