More Codes

- Search Page (ugly)
This commit is contained in:
Linloir 2022-10-13 23:30:38 +08:00
parent 43cb79f6ab
commit f93a95d141
No known key found for this signature in database
GPG Key ID: 58EEB209A0F2C366
28 changed files with 920 additions and 172 deletions

View File

@ -0,0 +1,69 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 21:49:53
* @LastEditTime : 2022-10-13 22:17:17
* @Description :
*/
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tcp_client/common/avatar/cubit/avatar_cubit.dart';
import 'package:tcp_client/common/avatar/cubit/avatar_state.dart';
import 'package:tcp_client/repositories/user_repository/user_repository.dart';
class UserAvatar extends StatelessWidget {
const UserAvatar({required this.userid, super.key});
final int userid;
@override
Widget build(BuildContext context) {
return BlocBuilder<AvatarCubit, AvatarState>(
bloc: AvatarCubit(
userid: userid,
userRepository: context.read<UserRepository>()
),
builder: (context, state) {
if(state.userInfo.avatarEncoded == null) {
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(5.0),
boxShadow: [BoxShadow(blurRadius: 10.0, color: Colors.grey[850]!.withOpacity(0.15))]
),
child: Text(
state.userInfo.userName[0],
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w300,
color: Colors.white,
shadows: [Shadow(blurRadius: 5.0, color: Colors.white.withOpacity(0.15))]
),
),
);
}
else {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
boxShadow: [BoxShadow(blurRadius: 10.0, color: Colors.grey[850]!.withOpacity(0.15))]
),
child: ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.fitWidth,
child: Image.memory(base64.decode(state.userInfo.avatarEncoded!)),
),
)
),
);
}
},
);
}
}

View File

@ -0,0 +1,28 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 21:50:14
* @LastEditTime : 2022-10-13 22:03:01
* @Description :
*/
import 'package:bloc/bloc.dart';
import 'package:tcp_client/common/avatar/cubit/avatar_state.dart';
import 'package:tcp_client/repositories/common_models/userinfo.dart';
import 'package:tcp_client/repositories/user_repository/user_repository.dart';
class AvatarCubit extends Cubit<AvatarState> {
AvatarCubit({
required int userid,
required this.userRepository
}): super(AvatarState(userInfo: userRepository.getUserInfo(userid: userid))) {
userRepository.userInfoStreamBroadcast.listen(onFetchedUserInfo);
}
final UserRepository userRepository;
void onFetchedUserInfo(UserInfo userInfo) {
if(userInfo.userID == state.userInfo.userID) {
emit(AvatarState(userInfo: userInfo));
}
}
}

View File

@ -0,0 +1,20 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 21:50:07
* @LastEditTime : 2022-10-13 22:02:11
* @Description :
*/
import 'package:equatable/equatable.dart';
import 'package:tcp_client/repositories/common_models/userinfo.dart';
class AvatarState extends Equatable {
const AvatarState({
required this.userInfo,
});
final UserInfo userInfo;
@override
List<Object?> get props => [userInfo.userID, userInfo.avatarEncoded];
}

View File

@ -0,0 +1,28 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 21:50:14
* @LastEditTime : 2022-10-13 22:05:52
* @Description :
*/
import 'package:bloc/bloc.dart';
import 'package:tcp_client/common/username/cubit/username_state.dart';
import 'package:tcp_client/repositories/common_models/userinfo.dart';
import 'package:tcp_client/repositories/user_repository/user_repository.dart';
class UsernameCubit extends Cubit<UsernameState> {
UsernameCubit({
required int userid,
required this.userRepository
}): super(UsernameState(userInfo: userRepository.getUserInfo(userid: userid))) {
userRepository.userInfoStreamBroadcast.listen(onFetchedUserInfo);
}
final UserRepository userRepository;
void onFetchedUserInfo(UserInfo userInfo) {
if(userInfo.userID == state.userInfo.userID) {
emit(UsernameState(userInfo: userInfo));
}
}
}

View File

@ -0,0 +1,20 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 21:50:07
* @LastEditTime : 2022-10-13 22:05:43
* @Description :
*/
import 'package:equatable/equatable.dart';
import 'package:tcp_client/repositories/common_models/userinfo.dart';
class UsernameState extends Equatable {
const UsernameState({
required this.userInfo,
});
final UserInfo userInfo;
@override
List<Object?> get props => [userInfo.userID, userInfo.avatarEncoded];
}

View File

@ -0,0 +1,42 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 22:05:12
* @LastEditTime : 2022-10-13 22:21:42
* @Description :
*/
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tcp_client/common/username/cubit/username_cubit.dart';
import 'package:tcp_client/common/username/cubit/username_state.dart';
import 'package:tcp_client/repositories/user_repository/user_repository.dart';
class UserNameText extends StatelessWidget {
const UserNameText({
required this.userid,
this.fontWeight = FontWeight.normal,
super.key
});
final int userid;
final FontWeight fontWeight;
@override
Widget build(BuildContext context) {
return BlocBuilder<UsernameCubit, UsernameState>(
bloc: UsernameCubit(
userid: userid,
userRepository: context.read<UserRepository>()
),
builder: (context, state) {
return Text(
state.userInfo.userName,
style: TextStyle(
fontSize: 18,
fontWeight: fontWeight
),
);
}
);
}
}

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 14:02:28
* @LastEditTime : 2022-10-13 16:43:49
* @LastEditTime : 2022-10-13 23:02:04
* @Description :
*/

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-11 11:05:08
* @LastEditTime : 2022-10-13 16:55:48
* @LastEditTime : 2022-10-13 23:02:55
* @Description :
*/
@ -15,9 +15,11 @@ import 'package:tcp_client/home/view/message_page/cubit/msg_list_cubit.dart';
import 'package:tcp_client/home/view/message_page/mesage_page.dart';
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
import 'package:tcp_client/repositories/user_repository/user_repository.dart';
import 'package:tcp_client/search/search_page.dart';
class HomePage extends StatelessWidget {
HomePage({
const HomePage({
required this.localServiceRepository,
required this.tcpRepository,
super.key
@ -26,8 +28,6 @@ class HomePage extends StatelessWidget {
final LocalServiceRepository localServiceRepository;
final TCPRepository tcpRepository;
final PageController _controller = PageController();
static Route<void> route({
required LocalServiceRepository localServiceRepository,
required TCPRepository tcpRepository
@ -38,47 +38,98 @@ class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
return MultiRepositoryProvider(
providers: [
BlocProvider<MessageListCubit>(
create: (context) => MessageListCubit(
RepositoryProvider<UserRepository>(
create: (context) => UserRepository(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
),
),
BlocProvider<ContactCubit>(
create: (context) => ContactCubit(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
),
),
BlocProvider<HomeCubit>(
create: (context) => HomeCubit(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
),
)
RepositoryProvider<LocalServiceRepository>.value(value: localServiceRepository),
RepositoryProvider<TCPRepository>.value(value: tcpRepository),
],
child: BlocListener<HomeCubit, HomeState>(
listenWhen:(previous, current) => current.page != previous.page,
listener: (context, state) {
_controller.animateToPage(
state.page.value,
duration: const Duration(milliseconds: 375),
curve: Curves.easeInOutCubicEmphasized
);
},
child: Scaffold(
body: PageView(
controller: _controller,
children: const [
MessagePage(),
ContactPage()
],
child: MultiBlocProvider(
providers: [
BlocProvider<MessageListCubit>(
create: (context) => MessageListCubit(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
),
),
),
)
BlocProvider<ContactCubit>(
create: (context) => ContactCubit(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
),
),
BlocProvider<HomeCubit>(
create: (context) => HomeCubit(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
),
)
],
child: HomePageView(),
),
);
}
}
class HomePageView extends StatelessWidget {
HomePageView({super.key});
final PageController _controller = PageController();
@override
Widget build(BuildContext context) {
return BlocListener<HomeCubit, HomeState>(
listenWhen:(previous, current) => current.page != previous.page,
listener: (context, state) {
_controller.animateToPage(
state.page.value,
duration: const Duration(milliseconds: 375),
curve: Curves.easeInOutCubicEmphasized
);
},
child: Scaffold(
appBar: AppBar(
title: BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
return Text(
state.page.literal,
style: const TextStyle(
fontWeight: FontWeight.bold
),
);
},
),
actions: [
IconButton(
icon: const Icon(Icons.search_rounded),
onPressed: () {
Navigator.of(context).push(SearchPage.route(
localServiceRepository: context.read<LocalServiceRepository>(),
tcpRepository: context.read<TCPRepository>(),
userRepository: context.read<UserRepository>()
));
},
)
],
),
body: Center(
child: BlocBuilder<HomeCubit, HomeState>(
builder:(context, state) => PageView(
controller: _controller,
onPageChanged: (value) => context.read<HomeCubit>().switchPage(HomePagePosition.fromValue(value)),
children: const [
MessagePage(),
ContactPage()
],
),
),
),
),
);
}
}

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 23:36:07
* @LastEditTime : 2022-10-13 16:10:57
* @LastEditTime : 2022-10-13 22:59:25
* @Description :
*/
@ -18,36 +18,39 @@ class ContactPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<ContactCubit, ContactState>(
builder: (context, state) {
return AzListView(
data: state.indexedData,
itemCount: state.contacts.length,
itemBuilder: (context, index) {
return ContactTile(
userInfo: state.contacts[index],
);
},
physics: const BouncingScrollPhysics(),
susItemBuilder: (context, index) {
return Container(
height: 40,
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.only(left: 16.0),
color: Colors.grey[200],
alignment: Alignment.centerLeft,
child: Text(
ContactModel(userInfo: state.contacts[index]).getSuspensionTag(),
softWrap: false,
style: TextStyle(
fontSize: 14.0,
color: Colors.grey[700],
return Container(
color: Colors.green,
child: BlocBuilder<ContactCubit, ContactState>(
builder: (context, state) {
return AzListView(
data: state.indexedData,
itemCount: state.contacts.length,
itemBuilder: (context, index) {
return ContactTile(
userInfo: state.contacts[index],
);
},
physics: const BouncingScrollPhysics(),
susItemBuilder: (context, index) {
return Container(
height: 40,
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.only(left: 16.0),
color: Colors.grey[200],
alignment: Alignment.centerLeft,
child: Text(
ContactModel(userInfo: state.contacts[index]).getSuspensionTag(),
softWrap: false,
style: TextStyle(
fontSize: 14.0,
color: Colors.grey[700],
),
),
),
);
},
);
},
);
},
);
},
),
);
}
}

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 14:02:00
* @LastEditTime : 2022-10-13 16:44:03
* @LastEditTime : 2022-10-13 22:26:07
* @Description :
*/
@ -32,7 +32,7 @@ class ContactTile extends StatelessWidget {
InkWell(
onTap: () {
Navigator.of(context).push(ChatPage.route(userInfo: userInfo));
context.read<MessageListCubit>().addEmptyMessageOf(user: userInfo);
context.read<MessageListCubit>().addEmptyMessageOf(targetUser: userInfo.userID);
context.read<HomeCubit>().switchPage(HomePagePosition.message);
},
),

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 23:38:31
* @LastEditTime : 2022-10-13 16:12:53
* @LastEditTime : 2022-10-13 22:27:29
* @Description :
*/
@ -25,11 +25,11 @@ class MessageListCubit extends Cubit<MessageListState> {
final LocalServiceRepository localServiceRepository;
final TCPRepository tcpRepository;
void addEmptyMessageOf({required UserInfo user}) {
if(state.messageList.any((element) => element.userInfo.userID == user.userID)) {
void addEmptyMessageOf({required int targetUser}) {
if(state.messageList.any((element) => element.targetUser == targetUser)) {
return;
}
var newList = [MessageInfo(userInfo: user)];
var newList = [MessageInfo(targetUser: targetUser)];
emit(MessageListState(messageList: newList..addAll(state.messageList)));
}
@ -50,12 +50,9 @@ class MessageListCubit extends Cubit<MessageListState> {
//therefore only insert to map if the target user does not exist
if(!addedUserSet.contains(targetUser)) {
addedUserSet.add(targetUser);
var targetUserInfo = await localServiceRepository.fetchUserInfoViaID(userid: targetUser);
//TODO: Maybe need to add API in tcp repository to fetch user info via id
targetUserInfo ??= UserInfo(userid: targetUser, username: targetUser.toString());
//Create message info
latestMessages.add(MessageInfo(
userInfo: targetUserInfo,
targetUser: targetUser,
message: message
));
}
@ -74,12 +71,9 @@ class MessageListCubit extends Cubit<MessageListState> {
var targetUser = response.message.senderID == curUser ?
response.message.recieverID :
response.message.senderID;
var targetUserInfo = await localServiceRepository.fetchUserInfoViaID(userid: targetUser);
//TODO: Maybe need to add API in tcp repository to fetch user info via id
targetUserInfo ??= UserInfo(userid: targetUser, username: targetUser.toString());
emit(state.updateWithSingle(
messageInfo: MessageInfo(
userInfo: targetUserInfo,
targetUser: targetUser,
message: response.message
)
));

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 23:37:49
* @LastEditTime : 2022-10-13 11:09:54
* @LastEditTime : 2022-10-13 22:24:22
* @Description :
*/
@ -22,7 +22,7 @@ class MessageListState extends Equatable {
}) {
var newList = <MessageInfo>[messageInfo];
for(var msgInfo in messageList) {
if(msgInfo.userInfo.userID == messageInfo.userInfo.userID) {
if(msgInfo.targetUser == messageInfo.targetUser) {
continue;
}
newList.add(msgInfo);
@ -41,11 +41,11 @@ class MessageListState extends Equatable {
insertListIndex < orderedNewMessages.length &&
origListIndex < messageList.length
) {
if(addedUsers.contains(orderedNewMessages[insertListIndex].userInfo.userID)) {
if(addedUsers.contains(orderedNewMessages[insertListIndex].targetUser)) {
insertListIndex += 1;
continue;
}
if(addedUsers.contains(messageList[origListIndex].userInfo.userID)) {
if(addedUsers.contains(messageList[origListIndex].targetUser)) {
origListIndex += 1;
continue;
}
@ -54,35 +54,35 @@ class MessageListState extends Equatable {
(orderedNewMessages[insertListIndex].message?.timeStamp ?? 0)
) {
newList.add(messageList[origListIndex]);
addedUsers.add(messageList[origListIndex].userInfo.userID);
addedUsers.add(messageList[origListIndex].targetUser);
origListIndex += 1;
continue;
}
else {
newList.add(orderedNewMessages[insertListIndex]);
addedUsers.add(orderedNewMessages[insertListIndex].userInfo.userID);
addedUsers.add(orderedNewMessages[insertListIndex].targetUser);
insertListIndex += 1;
continue;
}
}
//Add the messages left
while(origListIndex < messageList.length) {
if(addedUsers.contains(messageList[origListIndex].userInfo.userID)) {
if(addedUsers.contains(messageList[origListIndex].targetUser)) {
origListIndex += 1;
continue;
}
newList.add(messageList[origListIndex]);
addedUsers.add(messageList[origListIndex].userInfo.userID);
addedUsers.add(messageList[origListIndex].targetUser);
origListIndex += 1;
continue;
}
while(insertListIndex < orderedNewMessages.length) {
if(addedUsers.contains(orderedNewMessages[insertListIndex].userInfo.userID)) {
if(addedUsers.contains(orderedNewMessages[insertListIndex].targetUser)) {
origListIndex += 1;
continue;
}
newList.add(orderedNewMessages[insertListIndex]);
addedUsers.add(orderedNewMessages[insertListIndex].userInfo.userID);
addedUsers.add(orderedNewMessages[insertListIndex].targetUser);
origListIndex += 1;
continue;
}

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-11 11:05:18
* @LastEditTime : 2022-10-13 16:11:24
* @LastEditTime : 2022-10-13 22:59:10
* @Description :
*/
@ -18,19 +18,22 @@ class MessagePage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<MessageListCubit, MessageListState>(
builder: (context, state) {
return ListView.separated(
itemBuilder: (context, index) {
return MessageTile(
userInfo: state.messageList[index].userInfo,
message: state.messageList[index].message,
);
},
separatorBuilder: (context, index) {
return const Divider(
height: 0.5,
);
},
itemCount: state.messageList.length
return Container(
color: Colors.blue,
child: ListView.separated(
itemBuilder: (context, index) {
return MessageTile(
userID: state.messageList[index].targetUser,
message: state.messageList[index].message,
);
},
separatorBuilder: (context, index) {
return const Divider(
height: 0.5,
);
},
itemCount: state.messageList.length
),
);
}
);

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 23:48:54
* @LastEditTime : 2022-10-12 23:50:17
* @LastEditTime : 2022-10-13 22:24:01
* @Description :
*/
@ -11,13 +11,13 @@ import 'package:tcp_client/repositories/common_models/userinfo.dart';
class MessageInfo extends Equatable {
final Message? message;
final UserInfo userInfo;
final int targetUser;
const MessageInfo({
this.message,
required this.userInfo
required this.targetUser
});
@override
List<Object?> get props => [message?.contentmd5, userInfo];
List<Object?> get props => [message?.contentmd5, targetUser];
}

View File

@ -1,24 +1,26 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 13:17:52
* @LastEditTime : 2022-10-13 14:57:14
* @LastEditTime : 2022-10-13 22:23:31
* @Description :
*/
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:tcp_client/common/avatar/avatar.dart';
import 'package:tcp_client/common/username/username.dart';
import 'package:tcp_client/repositories/common_models/message.dart';
import 'package:tcp_client/repositories/common_models/userinfo.dart';
class MessageTile extends StatelessWidget {
const MessageTile({
required this.userInfo,
required this.userID,
this.message,
super.key
});
final UserInfo userInfo;
final int userID;
final Message? message;
@override
@ -33,37 +35,38 @@ class MessageTile extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if(userInfo.avatarEncoded != null && userInfo.avatarEncoded!.isEmpty)
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: Colors.grey[700]!,
width: 1.0
)
),
child: ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.fitWidth,
child: Image.memory(base64Decode(userInfo.avatarEncoded!)),
),
)
),
),
if(userInfo.avatarEncoded == null || userInfo.avatarEncoded!.isEmpty)
Container(
color: Colors.grey,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: Colors.grey[700]!,
width: 1.0
)
),
),
UserAvatar(userid: userID),
// if(userInfo.avatarEncoded != null && userInfo.avatarEncoded!.isEmpty)
// Container(
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(5.0),
// border: Border.all(
// color: Colors.grey[700]!,
// width: 1.0
// )
// ),
// child: ClipRRect(
// borderRadius: BorderRadius.circular(5.0),
// child: OverflowBox(
// alignment: Alignment.center,
// child: FittedBox(
// fit: BoxFit.fitWidth,
// child: Image.memory(base64Decode(userInfo.avatarEncoded!)),
// ),
// )
// ),
// ),
// if(userInfo.avatarEncoded == null || userInfo.avatarEncoded!.isEmpty)
// Container(
// color: Colors.grey,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(5.0),
// border: Border.all(
// color: Colors.grey[700]!,
// width: 1.0
// )
// ),
// ),
const SizedBox(width: 12,),
Expanded(
child: Column(
@ -73,13 +76,7 @@ class MessageTile extends StatelessWidget {
vertical: 8.0,
horizontal: 0
),
child: Text(
userInfo.userName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
child: UserNameText(userid: userID, fontWeight: FontWeight.bold,)
),
const Spacer(),
Padding(

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-11 09:44:03
* @LastEditTime : 2022-10-12 13:56:56
* @LastEditTime : 2022-10-13 23:04:49
* @Description : Abstract TCP request class
*/
@ -108,10 +108,19 @@ class LogoutRequest extends TCPRequest {
}
class GetProfileRequest extends TCPRequest {
const GetProfileRequest({required int? token}): super(type: TCPRequestType.profile, token: token);
const GetProfileRequest({
required int userid,
required int? token
}): _userid = userid, super(type: TCPRequestType.profile, token: token);
final int _userid;
int get userid => _userid;
@override
Map<String, Object?> get body => {};
Map<String, Object?> get body => {
'userid': _userid
};
}
class ModifyPasswordRequest extends TCPRequest {
@ -205,14 +214,15 @@ class FetchFileRequest extends TCPRequest {
class SearchUserRequest extends TCPRequest {
final String _username;
const SearchUserRequest({required String username, required int? token}): _username = username, super(type: TCPRequestType.searchUser, token: token);
SearchUserRequest({required String username, required int? token}):
_username = base64.encode(utf8.encode(username)), super(type: TCPRequestType.searchUser, token: token);
@override
Map<String, Object?> get body => {
'username': _username,
};
String get username => _username;
String get username => utf8.decode(base64.decode(_username));
}
class AddContactRequest extends TCPRequest {

View File

@ -0,0 +1,79 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 20:18:14
* @LastEditTime : 2022-10-13 21:26:16
* @Description : Repository to cache user info
*/
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tcp_client/repositories/common_models/userinfo.dart';
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';
import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart';
import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart';
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
class UserRepository {
final Map<int, UserInfo> users = {};
final LocalServiceRepository localServiceRepository;
final TCPRepository tcpRepository;
final StreamController<UserInfo> _userInfoStreamController = StreamController();
Stream<UserInfo>? _userInfoStreamBroadcast;
Stream<UserInfo> get userInfoStreamBroadcast {
_userInfoStreamBroadcast ??= _userInfoStreamController.stream.asBroadcastStream();
return _userInfoStreamBroadcast!;
}
UserRepository({
required this.localServiceRepository,
required this.tcpRepository
}) {
tcpRepository.responseStreamBroadcast.listen(_onResponse);
}
Future<void> _onResponse(TCPResponse response) async {
if(response.type == TCPResponseType.profile && response.status == TCPResponseStatus.ok) {
response as GetProfileResponse;
users.update(response.userInfo!.userID, (value) => response.userInfo!, ifAbsent: () => response.userInfo!);
_userInfoStreamController.add(response.userInfo!);
localServiceRepository.storeUserInfo(userInfo: response.userInfo!);
}
}
//Fetch user info
//1. Check if the user info is in the map
// if so, return the user, otherwise consult the database
//2. If the database has the user info
// add it to the map and stream, otherwise consult the tcp repository
//3. Pass the control to tcp response handler
UserInfo getUserInfo({required int userid}) {
if(users.containsKey(userid)) {
return users[userid]!;
}
Future<UserInfo?>(() async {
//Consult the database for info
return await localServiceRepository.fetchUserInfoViaID(userid: userid);
}).then((userInfo) async {
if(userInfo == null) {
//Consult the tcp server for info
tcpRepository.pushRequest(GetProfileRequest(
userid: userid,
token: (await SharedPreferences.getInstance()).getInt('token')
));
}
else {
//Add to map
users.update(userid, (value) => userInfo, ifAbsent: () => userInfo);
//Push to stream
_userInfoStreamController.add(userInfo);
}
});
//Return a mock userinfo
return UserInfo(
userid: userid,
username: userid.toString(),
);
}
}

View File

@ -1,6 +1,78 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 17:09:25
* @LastEditTime : 2022-10-13 17:09:26
* @LastEditTime : 2022-10-13 23:25:20
* @Description :
*/
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';
import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart';
import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart';
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
import 'package:tcp_client/search/cubit/search_state.dart';
import 'package:tcp_client/search/model/history_result.dart';
import 'package:tcp_client/search/model/user_result.dart';
class SearchCubit extends Cubit<SearchState> {
SearchCubit({
required this.localServiceRepository,
required this.tcpRepository
}): super(const SearchState.empty()) {
subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse);
}
final LocalServiceRepository localServiceRepository;
final TCPRepository tcpRepository;
late final StreamSubscription subscription;
void onKeyChanged(String newKey) {
EasyDebounce.debounce(
'Search',
const Duration(milliseconds: 500),
() => _performSearch(newKey)
);
}
Future<void> _performSearch(String newKey) async {
tcpRepository.pushRequest(SearchUserRequest(
username: newKey,
token: (await SharedPreferences.getInstance()).getInt('token')
));
var histories = await localServiceRepository.findMessages(pattern: newKey);
var currentUserID = (await SharedPreferences.getInstance()).getInt('userid');
var historyResults = histories.map((msg) {
var targetID = msg.senderID == currentUserID ? msg.recieverID : msg.senderID;
return HistorySearchResult(contact: targetID, message: msg);
}).toList();
emit(state.copyWith(historyResults: historyResults));
}
Future<void> _onResponse(TCPResponse response) async {
switch(response.type) {
case TCPResponseType.searchUser: {
response as SearchUserResponse;
//TODO: Maybe server search should be ambigious
var userInfo = response.userInfo;
emit(state.copyWith(
userResults: [
if(userInfo != null) UserSearchResult(userInfo: userInfo)
]
));
break;
}
default: break;
}
}
//Override dispose to cancel the subscription
@override
Future<void> close() {
subscription.cancel();
return super.close();
}
}

View File

@ -1,6 +1,34 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 17:08:56
* @LastEditTime : 2022-10-13 17:08:57
* @LastEditTime : 2022-10-13 17:42:33
* @Description :
*/
import 'package:equatable/equatable.dart';
import 'package:tcp_client/search/model/history_result.dart';
import 'package:tcp_client/search/model/user_result.dart';
class SearchState extends Equatable {
const SearchState({
required this.historyResults,
required this.userResults
});
const SearchState.empty(): historyResults = const [], userResults = const [];
final List<HistorySearchResult> historyResults;
final List<UserSearchResult> userResults;
SearchState copyWith({
List<HistorySearchResult>? historyResults,
List<UserSearchResult>? userResults
}) {
return SearchState(
historyResults: historyResults ?? this.historyResults,
userResults: userResults ?? this.userResults
);
}
@override
List<Object> get props => [historyResults, userResults];
}

View File

@ -0,0 +1,22 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 17:30:26
* @LastEditTime : 2022-10-13 21:28:46
* @Description :
*/
import 'package:equatable/equatable.dart';
import 'package:tcp_client/repositories/common_models/message.dart';
class HistorySearchResult extends Equatable {
final int contact;
final Message message;
const HistorySearchResult({
required this.contact,
required this.message
});
@override
List<Object> get props => [contact, message.contentmd5];
}

View File

@ -1,6 +0,0 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 17:09:46
* @LastEditTime : 2022-10-13 17:09:46
* @Description :
*/

View File

@ -0,0 +1,18 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 17:30:36
* @LastEditTime : 2022-10-13 22:39:25
* @Description :
*/
import 'package:equatable/equatable.dart';
import 'package:tcp_client/repositories/common_models/userinfo.dart';
class UserSearchResult extends Equatable {
final UserInfo userInfo;
const UserSearchResult({required this.userInfo});
@override
List<Object?> get props => [userInfo.userID];
}

View File

@ -1,21 +1,106 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 17:04:12
* @LastEditTime : 2022-10-13 17:08:13
* @LastEditTime : 2022-10-13 23:24:13
* @Description :
*/
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
import 'package:tcp_client/repositories/user_repository/user_repository.dart';
import 'package:tcp_client/search/cubit/search_cubit.dart';
import 'package:tcp_client/search/cubit/search_state.dart';
import 'package:tcp_client/search/view/history_tile.dart';
import 'package:tcp_client/search/view/search_bar.dart';
import 'package:tcp_client/search/view/user_tile.dart';
class SearchPage extends StatelessWidget {
const SearchPage({super.key});
const SearchPage({
required this.localServiceRepository,
required this.tcpRepository,
required this.userRepository,
super.key
});
final LocalServiceRepository localServiceRepository;
final TCPRepository tcpRepository;
final UserRepository userRepository;
static Route<void> route({
required LocalServiceRepository localServiceRepository,
required TCPRepository tcpRepository,
required UserRepository userRepository
}) => MaterialPageRoute<void>(builder: (context) => SearchPage(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository,
userRepository: userRepository,
));
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const SearchBar(),
return RepositoryProvider<UserRepository>.value(
value: userRepository,
child: BlocProvider<SearchCubit>(
create: (context) => SearchCubit(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
),
child: Scaffold(
appBar: AppBar(
title: const SearchBar(),
),
body: BlocBuilder<SearchCubit, SearchState>(
builder:(context, state) {
return ListView(
physics: const BouncingScrollPhysics(),
children: [
if(state.userResults.isNotEmpty)
...[
const SizedBox(height: 16.0,),
const Text(
'Users',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20
),
),
const SizedBox(height: 8.0,),
...state.userResults.map((e) => UserTile(userInfo: e.userInfo))
],
if(state.historyResults.isNotEmpty)
...[
const SizedBox(height: 16.0,),
const Text(
'Histories',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20
),
),
const SizedBox(height: 8.0,),
...state.historyResults.map((e) => HistoryTile(
userID: e.contact,
message: e.message
))
],
if(state.historyResults.isEmpty && state.userResults.isEmpty)
const Align(
alignment: Alignment.center,
child: Text(
'No result found',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20
),
),
)
],
);
},
),
),
),
);
}

View File

@ -0,0 +1,98 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 21:41:49
* @LastEditTime : 2022-10-13 22:31:37
* @Description :
*/
import 'package:flutter/material.dart';
import 'package:tcp_client/common/avatar/avatar.dart';
import 'package:tcp_client/common/username/username.dart';
import 'package:tcp_client/repositories/common_models/message.dart';
class HistoryTile extends StatelessWidget {
const HistoryTile({
required this.userID,
required this.message,
super.key
});
final int userID;
final Message message;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 24.0
),
child: IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
UserAvatar(userid: userID),
const SizedBox(width: 12,),
Expanded(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 0
),
child: UserNameText(userid: userID, fontWeight: FontWeight.bold,)
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0
),
child: Text(
message.contentDecoded,
style: const TextStyle(
fontSize: 16,
),
),
)
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 0
),
child: Align(
alignment: Alignment.topCenter,
child: Text(
getTimeStamp(message.timeStamp)
),
),
),
],
),
),
);
}
String getTimeStamp(int timeStamp) {
var date = DateTime.fromMillisecondsSinceEpoch(timeStamp);
var weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
//If date is today, return time
if(date.day == DateTime.now().day) {
return '${date.hour}:${date.minute}';
}
//If date is yesterday, return 'yesterday'
if(date.day == DateTime.now().day - 1) {
return 'yesterday';
}
//If date is within this week, return the weekday in english
if(date.weekday < DateTime.now().weekday) {
return weekdays[date.weekday - 1];
}
//Otherwise return the date in english
return '${date.month}/${date.day}';
}
}

View File

@ -1,17 +1,23 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 17:06:52
* @LastEditTime : 2022-10-13 17:06:53
* @LastEditTime : 2022-10-13 21:35:11
* @Description :
*/
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tcp_client/search/cubit/search_cubit.dart';
class SearchBar extends StatelessWidget {
const SearchBar({super.key});
@override
Widget build(BuildContext context) {
return
return TextField(
onChanged: (value) {
context.read<SearchCubit>().onKeyChanged(value);
},
);
}
}

View File

@ -0,0 +1,73 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 21:41:41
* @LastEditTime : 2022-10-13 23:26:46
* @Description :
*/
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:tcp_client/repositories/common_models/userinfo.dart';
class UserTile extends StatelessWidget {
const UserTile({
required this.userInfo,
super.key
});
final UserInfo userInfo;
@override
Widget build(BuildContext context) {
return Row(
children: [
if(userInfo.avatarEncoded != null && userInfo.avatarEncoded!.isEmpty)
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: Colors.grey[700]!,
width: 1.0
)
),
child: ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.fitWidth,
child: Image.memory(base64.decode(userInfo.avatarEncoded!)),
),
)
),
),
if(userInfo.avatarEncoded == null || userInfo.avatarEncoded!.isEmpty)
Container(
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: Colors.grey[700]!,
width: 1.0
)
),
),
const SizedBox(width: 12,),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12.0
),
child: Text(
userInfo.userName,
style: const TextStyle(
fontSize: 18.0
),
),
)
),
],
);
}
}

View File

@ -71,6 +71,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.5"
easy_debounce:
dependency: "direct main"
description:
name: easy_debounce
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2+1"
equatable:
dependency: "direct main"
description:

View File

@ -53,6 +53,7 @@ dependencies:
pull_to_refresh: ^2.0.0
azlistview: ^2.0.0
lpinyin: ^2.0.3
easy_debounce: ^2.0.2+1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.