Compare commits

..

1 Commits
v2.0.0 ... main

Author SHA1 Message Date
Linloir
26f330f962
Update README.md 2022-10-28 23:55:40 +08:00
28 changed files with 289 additions and 583 deletions

View File

@ -4,8 +4,8 @@ This is a simple chat client based on Flutter
## Screenshots
![Message Page](http://pic.linloir.xyz/images/2022/10/21/message_page.png)
![Contacts Page](http://pic.linloir.xyz/images/2022/10/21/contacts_page.png)
![Profile Page](http://pic.linloir.xyz/images/2022/10/21/profile_page.png)
![Chat Page](http://pic.linloir.xyz/images/2022/10/21/chat_page.png)
![Search Page](http://pic.linloir.xyz/images/2022/10/21/search_page.png)
![Message Page](https://pic.linloir.cn/images/2022/10/21/message_page.png)
![Contacts Page](https://pic.linloir.cn/images/2022/10/21/contacts_page.png)
![Profile Page](https://pic.linloir.cn/images/2022/10/21/profile_page.png)
![Chat Page](https://pic.linloir.cn/images/2022/10/21/chat_page.png)
![Search Page](https://pic.linloir.cn/images/2022/10/21/search_page.png)

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 14:03:16
* @LastEditTime : 2022-10-22 21:30:04
* @LastEditTime : 2022-10-20 10:52:30
* @Description :
*/
@ -88,7 +88,7 @@ class ChatPage extends StatelessWidget {
//Return history tile
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
horizontal: 24,
vertical: 8
),
child: HistoryTile(

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 14:03:56
* @LastEditTime : 2022-10-22 22:46:18
* @LastEditTime : 2022-10-20 11:04:40
* @Description :
*/

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-14 17:07:13
* @LastEditTime : 2022-10-22 23:31:19
* @LastEditTime : 2022-10-18 15:44:34
* @Description :
*/
@ -35,10 +35,7 @@ class FileBox extends StatelessWidget {
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 0.0,
vertical: 6.0
),
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -46,38 +43,38 @@ class FileBox extends StatelessWidget {
child: history.status == ChatHistoryStatus.none || history.status == ChatHistoryStatus.done ?
Icon(
Icons.file_present_rounded,
size: 20,
size: 24,
color: history.type == ChatHistoryType.income ? Colors.blue[800] : Colors.white.withOpacity(0.8),
) : history.status == ChatHistoryStatus.failed ?
Icon(
Icons.refresh_rounded,
size: 20,
size: 24,
color: history.type == ChatHistoryType.income ? Colors.red[800] : Colors.white.withOpacity(0.8),
) : history.status == ChatHistoryStatus.processing ?
SizedBox(
height: 16.0,
width: 16.0,
height: 18.0,
width: 18.0,
child: LoadingIndicator(
indicatorType: Indicator.ballPulseSync,
colors: [Colors.white.withOpacity(0.8)],
),
) :
SizedBox(
height: 16.0,
width: 16.0,
height: 18.0,
width: 18.0,
child: CircularProgressIndicator(
color: history.type == ChatHistoryType.income ? Colors.blue[800] : Colors.white.withOpacity(0.8),
strokeWidth: 2.5,
strokeWidth: 3,
),
)
),
const SizedBox(width: 16.0,),
const SizedBox(width: 18.0,),
Flexible(
child: Text(
history.message.contentDecoded,
softWrap: true,
style: TextStyle(
fontSize: 16.0,
fontSize: 20.0,
color: history.type == ChatHistoryType.income ? Colors.grey[900] : Colors.white
),
),

View File

@ -1,14 +1,13 @@
/*
* @Author : Linloir
* @Date : 2022-10-14 17:04:20
* @LastEditTime : 2022-10-23 11:35:10
* @LastEditTime : 2022-10-20 13:47:29
* @Description :
*/
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:tcp_client/chat/model/chat_history.dart';
class ImageBox extends StatelessWidget {
@ -26,56 +25,14 @@ class ImageBox extends StatelessWidget {
child: Stack(
children: [
Container(
constraints: const BoxConstraints(maxWidth: 200, maxHeight: 150),
child: Hero(
tag: history.message.contentmd5,
child: history.preCachedImage ?? Image.memory(base64Decode(history.message.contentDecoded)),
),
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 200),
child: history.preCachedImage ?? Image.memory(base64Decode(history.message.contentDecoded)),
),
Material(
color: Colors.transparent,
child: InkWell(
splashColor: Colors.white.withOpacity(0.1),
onTap: (){
var image = history.preCachedImage?.image ?? Image.memory(base64.decode(history.message.contentDecoded)).image;
Navigator.of(context).push(MaterialPageRoute(
builder:(context) {
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: PhotoView(
heroAttributes: PhotoViewHeroAttributes(
tag: history.message.contentmd5
),
imageProvider: image,
minScale: PhotoViewComputedScale.contained,
)
),
Positioned.fill(
child: SafeArea(
child: Align(
alignment: Alignment.topRight,
child: IconButton(
icon: Icon(
Icons.close_rounded,
shadows: [
Shadow(blurRadius: 8.0, color: Colors.white.withOpacity(0.5))
],
),
onPressed: () {
Navigator.of(context).pop();
},
),
),
),
),
],
),
);
},
));
},
onTap: (){},
)
),
]

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-14 17:04:12
* @LastEditTime : 2022-10-23 10:49:14
* @LastEditTime : 2022-10-15 10:53:28
* @Description :
*/
@ -25,10 +25,7 @@ class TextBox extends StatelessWidget {
return InkWell(
onTap: (){},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 0.0,
vertical: 6.0
),
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
@ -38,14 +35,14 @@ class TextBox extends StatelessWidget {
if(history.status == ChatHistoryStatus.sending)
...[
SizedBox(
height: 12.0,
width: 12.0,
height: 15.0,
width: 15.0,
child: CircularProgressIndicator(
color: Colors.white.withOpacity(0.5),
strokeWidth: 2.0,
),
),
const SizedBox(width: 12.0,),
const SizedBox(width: 16.0,),
],
if(history.status == ChatHistoryStatus.failed)
...[
@ -56,7 +53,7 @@ class TextBox extends StatelessWidget {
child: Icon(
Icons.error_rounded,
color: Colors.white.withOpacity(0.5),
size: 18,
size: 20,
),
onTap: () async {
context.read<ChatCubit>().tcpRepository.pushRequest(SendMessageRequest(
@ -74,9 +71,9 @@ class TextBox extends StatelessWidget {
Icon(
Icons.check_rounded,
color: Colors.white.withOpacity(0.5),
size: 18,
size: 20,
),
const SizedBox(width: 8.0,),
const SizedBox(width: 12.0,),
],
],
Flexible(
@ -84,7 +81,7 @@ class TextBox extends StatelessWidget {
history.message.contentDecoded,
softWrap: true,
style: TextStyle(
fontSize: 16,
fontSize: 18,
color: history.type == ChatHistoryType.income ? Colors.grey[900] : Colors.white
),
),

View File

@ -1,20 +1,15 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 14:03:45
* @LastEditTime : 2022-10-23 10:55:42
* @LastEditTime : 2022-10-15 10:52:30
* @Description :
*/
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tcp_client/chat/cubit/chat_cubit.dart';
import 'package:tcp_client/chat/model/chat_history.dart';
import 'package:tcp_client/chat/view/in_message_box.dart';
import 'package:tcp_client/chat/view/out_message_box.dart';
import 'package:tcp_client/common/avatar/avatar.dart';
import 'package:tcp_client/repositories/common_models/message.dart';
import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart';
class HistoryTile extends StatelessWidget {
const HistoryTile({
@ -39,7 +34,7 @@ class HistoryTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
UserAvatar(userid: history.message.senderID, size: 42,),
UserAvatar(userid: history.message.senderID),
const SizedBox(width: 16.0,),
Flexible(
child: InMessageBox(history: history)
@ -60,59 +55,10 @@ class HistoryTile extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: history.message.type == MessageType.image ? Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
if(history.type == ChatHistoryType.outcome && history.status == ChatHistoryStatus.sending)
...[
SizedBox(
height: 12.0,
width: 12.0,
child: CircularProgressIndicator(
color: Colors.grey.withOpacity(0.5),
strokeWidth: 2.0,
),
),
const SizedBox(width: 12.0,),
],
if(history.type == ChatHistoryType.outcome && history.status == ChatHistoryStatus.done)
...[
Icon(
Icons.check_rounded,
color: Colors.grey.withOpacity(0.5),
size: 18,
),
const SizedBox(width: 8.0,),
],
if(history.type == ChatHistoryType.outcome && history.status == ChatHistoryStatus.failed)
...[
ClipOval(
child: Material(
color: Colors.transparent,
child: InkWell(
child: Icon(
Icons.error_rounded,
color: Colors.white.withOpacity(0.5),
size: 18,
),
onTap: () async {
context.read<ChatCubit>().tcpRepository.pushRequest(SendMessageRequest(
message: history.message,
token: (await SharedPreferences.getInstance()).getInt('token')
));
},
),
),
),
const SizedBox(width: 8.0,),
],
OutMessageBox(history: history),
],
) : OutMessageBox(history: history),
child: OutMessageBox(history: history),
),
const SizedBox(width: 16.0,),
UserAvatar(userid: history.message.senderID, size: 42,),
UserAvatar(userid: history.message.senderID),
],
)
),

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-14 13:49:47
* @LastEditTime : 2022-10-23 10:07:48
* @LastEditTime : 2022-10-20 13:56:20
* @Description :
*/
@ -42,37 +42,37 @@ class InMessageBox extends StatelessWidget {
),
boxShadow: [BoxShadow(blurRadius: 5.0, color: Colors.grey.withOpacity(0.3))]
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if(history.message.type == MessageType.file)
FileBox(history: history),
if(history.message.type == MessageType.image)
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8.0),
topRight: Radius.circular(8.0),
bottomLeft: Radius.zero,
bottomRight: Radius.circular(8.0)
),
child: ImageBox(history: history),
),
if(history.message.type == MessageType.plaintext)
TextBox(history: history),
if(history.message.type != MessageType.image)
...[
const SizedBox(height: 4.0,),
Text(
_getTimeStamp(history.message.timeStamp),
style: TextStyle(
fontSize: 12,
color: Colors.grey[400],
),
)
]
],
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8.0),
topRight: Radius.circular(8.0),
bottomLeft: Radius.zero,
bottomRight: Radius.circular(8.0)
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if(history.message.type == MessageType.file)
FileBox(history: history),
if(history.message.type == MessageType.image)
ImageBox(history: history),
if(history.message.type == MessageType.plaintext)
TextBox(history: history),
if(history.message.type != MessageType.image)
...[
const SizedBox(height: 4.0,),
Text(
_getTimeStamp(history.message.timeStamp),
style: TextStyle(
fontSize: 12,
color: Colors.grey[400],
),
)
]
],
),
)
),
if(history.message.type == MessageType.image)
...[
@ -96,9 +96,9 @@ class InMessageBox extends StatelessWidget {
if(date.day == DateTime.now().day) {
return '${date.hour}:${date.minute.toString().padLeft(2, '0')}';
}
//If date is yda, return 'yda'
//If date is yesterday, return 'yesterday'
if(date.day == DateTime.now().day - 1) {
return 'yda ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
return 'yesterday ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
}
//If date is within this week, return the weekday in english
if(date.weekday < DateTime.now().weekday) {

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-14 17:54:30
* @LastEditTime : 2022-10-23 10:13:22
* @LastEditTime : 2022-10-20 11:18:48
* @Description :
*/
@ -34,12 +34,7 @@ class InputBox extends StatelessWidget {
),
child: Container(
// height: 64,
padding: const EdgeInsets.only(
left: 16.0,
right: 4.0,
top: 16.0,
bottom: 16.0
),
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -84,6 +79,7 @@ class InputBox extends StatelessWidget {
),
),
),
const SizedBox(width: 8.0,),
IconButton(
onPressed: () {
var chatCubit = context.read<ChatCubit>();
@ -102,6 +98,7 @@ class InputBox extends StatelessWidget {
},
icon: Icon(Icons.attach_file_rounded, color: Colors.grey[700],)
),
const SizedBox(width: 8.0,),
IconButton(
onPressed: () {
var chatCubit = context.read<ChatCubit>();
@ -119,6 +116,7 @@ class InputBox extends StatelessWidget {
},
icon: Icon(Icons.photo_rounded, color: Colors.grey[700],)
),
const SizedBox(width: 8.0,),
BlocBuilder<MessageInputCubit, MessageInputState>(
builder:(context, state) {
return IconButton(

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-14 13:49:28
* @LastEditTime : 2022-10-23 10:07:32
* @LastEditTime : 2022-10-19 23:47:20
* @Description :
*/
@ -42,37 +42,37 @@ class OutMessageBox extends StatelessWidget {
),
boxShadow: [BoxShadow(blurRadius: 5.0, color: Colors.grey.withOpacity(0.2))]
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
if(history.message.type == MessageType.file)
FileBox(history: history),
if(history.message.type == MessageType.image)
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8.0),
topRight: Radius.circular(8.0),
bottomLeft: Radius.circular(8.0),
bottomRight: Radius.zero
),
child: ImageBox(history: history),
),
if(history.message.type == MessageType.plaintext)
TextBox(history: history),
if(history.message.type != MessageType.image)
...[
const SizedBox(height: 4.0,),
Text(
_getTimeStamp(history.message.timeStamp),
style: TextStyle(
fontSize: 12,
color: Colors.grey[200],
),
)
],
],
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8.0),
topRight: Radius.circular(8.0),
bottomLeft: Radius.circular(8.0),
bottomRight: Radius.zero
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
if(history.message.type == MessageType.file)
FileBox(history: history),
if(history.message.type == MessageType.image)
ImageBox(history: history),
if(history.message.type == MessageType.plaintext)
TextBox(history: history),
if(history.message.type != MessageType.image)
...[
const SizedBox(height: 4.0,),
Text(
_getTimeStamp(history.message.timeStamp),
style: TextStyle(
fontSize: 12,
color: Colors.grey[200],
),
)
],
],
),
)
),
if(history.message.type == MessageType.image)
...[
@ -96,9 +96,9 @@ class OutMessageBox extends StatelessWidget {
if(date.day == DateTime.now().day) {
return '${date.hour}:${date.minute.toString().padLeft(2, '0')}';
}
//If date is yda, return 'yda'
//If date is yesterday, return 'yesterday'
if(date.day == DateTime.now().day - 1) {
return 'yda ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
return 'yesterday ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
}
//If date is within this week, return the weekday in english
if(date.weekday < DateTime.now().weekday) {

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 14:02:28
* @LastEditTime : 2022-10-22 21:08:39
* @LastEditTime : 2022-10-17 19:26:13
* @Description :
*/
@ -9,10 +9,8 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tcp_client/home/cubit/home_state.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';
@ -21,39 +19,11 @@ class HomeCubit extends Cubit<HomeState> {
required this.localServiceRepository,
required this.tcpRepository,
required this.pageController
}): super(const HomeState(page: HomePagePosition.message, status: HomePageStatus.initializing)) {
}): super(const HomeState(page: HomePagePosition.message)) {
pageController.addListener(() {
emit(state.copyWith(page: HomePagePosition.fromValue((pageController.page ?? 0).round())));
});
subscription = tcpRepository.responseStreamBroadcast.listen(_onTCPResponse);
Future(() async {
// var cloned = await tcpRepository.clone();
// cloned.pushRequest(FetchMessageRequest(
// token: (await SharedPreferences.getInstance()).getInt('token')
// ));
// await for(var response in cloned.responseStreamBroadcast) {
// if(response.type == TCPResponseType.fetchMessage) {
// if(response.status == TCPResponseStatus.ok) {
// response as FetchMessageResponse;
// localServiceRepository.storeMessages(response.messages);
// break;
// }
// }
// }
// cloned.dispose();
tcpRepository.pushRequest(FetchMessageRequest(
token: (await SharedPreferences.getInstance()).getInt('token')
));
// await for(var response in tcpRepository.responseStreamBroadcast) {
// if(response.type == TCPResponseType.fetchMessage) {
// if(response.status == TCPResponseStatus.ok) {
// // response as FetchMessageResponse;
// // localServiceRepository.storeMessages(response.messages);
// break;
// }
// }
// }
});
}
final LocalServiceRepository localServiceRepository;
@ -69,26 +39,16 @@ class HomeCubit extends Cubit<HomeState> {
);
}
void _onTCPResponse(TCPResponse response) async {
if(response.status == TCPResponseStatus.err) {
return;
}
void _onTCPResponse(TCPResponse response) {
switch(response.type) {
case TCPResponseType.forwardMessage: {
response as ForwardMessageResponse;
await localServiceRepository.storeMessages([response.message]);
localServiceRepository.storeMessages([response.message]);
break;
}
case TCPResponseType.fetchMessage: {
response as FetchMessageResponse;
await localServiceRepository.storeMessages(response.messages);
emit(state.copyWith(status: HomePageStatus.done));
if(response.messages.isNotEmpty) {
tcpRepository.pushRequest(AckFetchRequest(
timeStamp: response.messages[0].timeStamp,
token: (await SharedPreferences.getInstance()).getInt('token')
));
}
localServiceRepository.storeMessages(response.messages);
break;
}
default: {

View File

@ -1,14 +1,12 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 14:02:24
* @LastEditTime : 2022-10-21 23:28:33
* @LastEditTime : 2022-10-13 16:55:05
* @Description :
*/
import 'package:equatable/equatable.dart';
enum HomePageStatus { initializing, done }
enum HomePagePosition {
message(0),
contact(1),
@ -28,14 +26,13 @@ enum HomePagePosition {
class HomeState extends Equatable {
final HomePagePosition page;
final HomePageStatus status;
const HomeState({required this.page, required this.status});
const HomeState({required this.page});
HomeState copyWith({HomePagePosition? page, HomePageStatus? status}) {
return HomeState(page: page ?? this.page, status: status ?? this.status);
HomeState copyWith({HomePagePosition? page}) {
return HomeState(page: page ?? this.page);
}
@override
List<Object> get props => [page, status];
List<Object> get props => [page];
}

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-11 11:05:08
* @LastEditTime : 2022-10-21 23:56:24
* @LastEditTime : 2022-10-19 11:08:50
* @Description :
*/
@ -119,56 +119,16 @@ class HomePageView extends StatelessWidget {
)
],
),
body: BlocBuilder<HomeCubit, HomeState>(
builder:(context, state) => Stack(
children: [
Positioned.fill(
child: Center(
child: PageView(
controller: context.read<HomeCubit>().pageController,
children: [
MessagePage(),
const ContactPage(),
MyProfilePage(userID: userID)
],
),
),
),
if(state.status == HomePageStatus.initializing)
Positioned.fill(
child: AbsorbPointer(
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[800]!.withOpacity(0.5),
borderRadius: BorderRadius.circular(8.0)
),
height: 200,
width: 200,
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: const [
CircularProgressIndicator(
color: Colors.white,
strokeWidth: 4.0,
),
SizedBox(height: 16.0,),
Text(
'Fetching Messages',
style: TextStyle(
color: Colors.white,
fontSize: 18.0
),
)
],
),
),
),
),
),
]
body: Center(
child: BlocBuilder<HomeCubit, HomeState>(
builder:(context, state) => PageView(
controller: context.read<HomeCubit>().pageController,
children: [
MessagePage(),
const ContactPage(),
MyProfilePage(userID: userID)
],
),
),
),
bottomNavigationBar: BlocBuilder<HomeCubit, HomeState>(

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 23:38:31
* @LastEditTime : 2022-10-21 23:14:02
* @LastEditTime : 2022-10-19 18:02:22
* @Description :
*/
@ -40,7 +40,10 @@ class MessageListCubit extends Cubit<MessageListState> {
}
}
return msgList;
}).then((msgList) => emit(state.updateWithList(orderedNewMessages: msgList)));
}).then((msgList) => emit(state.updateWithList(orderedNewMessages: msgList)))
.then((_) async => tcpRepository.pushRequest(FetchMessageRequest(
token: (await SharedPreferences.getInstance()).getInt('token'))
));
}
final LocalServiceRepository localServiceRepository;

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 13:17:52
* @LastEditTime : 2022-10-23 10:09:09
* @LastEditTime : 2022-10-20 00:52:14
* @Description :
*/
@ -150,9 +150,9 @@ class MessageTile extends StatelessWidget {
if(date.day == DateTime.now().day) {
return '${date.hour}:${date.minute.toString().padLeft(2, '0')}';
}
//If date is yda, return 'yda'
//If date is yesterday, return 'yesterday'
if(date.day == DateTime.now().day - 1) {
return 'yda ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
return 'yesterday';
}
//If date is within this week, return the weekday in english
if(date.weekday < DateTime.now().weekday) {

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:06:30
* @LastEditTime : 2022-10-23 10:15:11
* @LastEditTime : 2022-10-20 20:55:07
* @Description :
*/
@ -81,7 +81,7 @@ class LoginPage extends StatelessWidget {
Expanded(
flex: 6,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.7,
width: MediaQuery.of(context).size.width * 0.5,
child: const LoginPanel()
)
),

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 16:29:25
* @LastEditTime : 2022-10-23 10:28:05
* @LastEditTime : 2022-10-20 20:43:51
* @Description :
*/
@ -44,11 +44,6 @@ class UsernameInput extends StatelessWidget {
onChanged: (username) {
context.read<LoginCubit>().onUsernameChange(Username.dirty(username));
},
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.none,
autocorrect: false,
enableIMEPersonalizedLearning: false,
enableSuggestions: false,
decoration: InputDecoration(
labelText: 'Username',
errorText: state.username.invalid ? 'Invalid username' : null
@ -65,21 +60,12 @@ class PasswordInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginCubit, LoginState>(
buildWhen: (previous, current) => previous.password != current.password || previous.status != current.status,
buildWhen: (previous, current) => previous.password != current.password,
builder: (context, state) {
return TextField(
onChanged: (password) {
context.read<LoginCubit>().onPasswordChange(Password.dirty(password));
},
onEditingComplete: () {
if(
state.status == FormzStatus.valid ||
state.status == FormzStatus.submissionFailure
) {
context.read<LoginCubit>().onSubmission();
}
},
textInputAction: TextInputAction.done,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',

View File

@ -1,20 +1,53 @@
/*
* @Author : Linloir
* @Date : 2022-10-10 08:04:53
* @LastEditTime : 2022-10-23 11:36:04
* @LastEditTime : 2022-10-20 23:11:45
* @Description :
*/
import 'package:easy_debounce/easy_debounce.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:tcp_client/home/home_page.dart';
import 'package:tcp_client/initialization/cubit/initialization_cubit.dart';
import 'package:tcp_client/initialization/cubit/initialization_state.dart';
import 'package:tcp_client/initialization/initialization_page.dart';
import 'package:tcp_client/login/login_page.dart';
import 'package:window_manager/window_manager.dart';
void main() async {
sqfliteFfiInit();
//The code below is for desktop platforms only-------------------------
WidgetsFlutterBinding.ensureInitialized();
// Must add this line.
await windowManager.ensureInitialized();
//Get preferred window size
var pref = await SharedPreferences.getInstance();
var width = pref.getDouble('windowWidth');
var height = pref.getDouble('windowHeight');
var posX = pref.getDouble('windowPosX');
var posY = pref.getDouble('windowPosY');
WindowOptions windowOptions = WindowOptions(
size: Size(width ?? 800, height ?? 600),
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.normal
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
if(posX != null && posY != null) {
await windowManager.setPosition(Offset(posX, posY));
}
await windowManager.show();
await windowManager.focus();
});
//---------------------------------------------------------------------
runApp(const MyApp());
}
@ -25,7 +58,44 @@ class MyApp extends StatefulWidget {
State<MyApp> createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
class MyAppState extends State<MyApp> with WindowListener {
// This widget is the root of your application.
@override
void initState() {
windowManager.addListener(this);
super.initState();
}
@override
void onWindowMove() {
EasyDebounce.debounce(
'WindowMove',
const Duration(milliseconds: 50),
() async {
var pref = await SharedPreferences.getInstance();
var pos = await windowManager.getPosition();
pref.setDouble('windowPosX', pos.dx);
pref.setDouble('windowPosY', pos.dy);
}
);
super.onWindowMove();
}
@override
void onWindowResize() {
EasyDebounce.debounce(
'WindowResize',
const Duration(milliseconds: 50),
() async {
var pref = await SharedPreferences.getInstance();
var size = await windowManager.getSize();
pref.setDouble('windowWidth', size.width);
pref.setDouble('windowHeight', size.height);
}
);
super.onWindowResize();
}
@override
Widget build(BuildContext context) {
return MaterialApp(

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 17:36:38
* @LastEditTime : 2022-10-23 10:15:31
* @LastEditTime : 2022-10-20 20:55:17
* @Description :
*/
/*
@ -82,7 +82,7 @@ class RegisterPage extends StatelessWidget {
listenWhen: (previous, current) => previous.status != current.status,
child: Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.7,
width: MediaQuery.of(context).size.width * 0.5,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 16:29:25
* @LastEditTime : 2022-10-23 10:28:12
* @LastEditTime : 2022-10-20 20:54:22
* @Description :
*/
@ -44,11 +44,6 @@ class UsernameInput extends StatelessWidget {
onChanged: (username) {
context.read<RegisterCubit>().onUsernameChange(Username.dirty(username));
},
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.none,
autocorrect: false,
enableIMEPersonalizedLearning: false,
enableSuggestions: false,
decoration: InputDecoration(
labelText: 'Username',
errorText: state.username.invalid ? 'Invalid username' : null
@ -65,21 +60,12 @@ class PasswordInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<RegisterCubit, RegisterState>(
buildWhen: (previous, current) => previous.password != current.password || previous.status != current.status,
buildWhen: (previous, current) => previous.password != current.password,
builder: (context, state) {
return TextField(
onChanged: (password) {
context.read<RegisterCubit>().onPasswordChange(Password.dirty(password));
},
onEditingComplete: () {
if(
state.status == FormzStatus.valid ||
state.status == FormzStatus.submissionFailure
) {
context.read<RegisterCubit>().onSubmission();
}
},
textInputAction: TextInputAction.done,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',

View File

@ -1,13 +1,14 @@
/*
* @Author : Linloir
* @Date : 2022-10-11 10:56:02
* @LastEditTime : 2022-10-22 01:22:58
* @LastEditTime : 2022-10-20 17:24:26
* @Description : Local Service Repository
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:file_picker/file_picker.dart';
@ -16,7 +17,11 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:tcp_client/repositories/common_models/message.dart';
import 'package:tcp_client/repositories/common_models/userinfo.dart';
import 'package:tcp_client/repositories/local_service_repository/models/local_file.dart';
import 'package:sqflite/sqflite.dart';
//Windows platform
import 'package:sqflite_common/sqlite_api.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
//Android platform
// import 'package:sqflite/sqflite.dart';
class LocalServiceRepository {
late final Database _database;
@ -57,22 +62,6 @@ class LocalServiceRepository {
);
'''
);
await txn.execute(
'''
create table msgimgs (
msgmd5 text primary key,
imgmd5 text not null
);
'''
);
await txn.execute(
'''
create table imgs (
imgmd5 text primary key,
dir text not null
);
'''
);
});
// await db.execute(
// '''
@ -102,11 +91,20 @@ class LocalServiceRepository {
UserInfo? currentUser,
required String databaseFilePath
}) async {
var database = await openDatabase(
//Windows platform
var database = await databaseFactoryFfi.openDatabase(
databaseFilePath,
version: 1,
onCreate: _onDatabaseCreate
options: OpenDatabaseOptions(
version: 1,
onCreate: _onDatabaseCreate
)
);
//Android platform
// var database = await openDatabase(
// databaseFilePath,
// version: 1,
// onCreate: _onDatabaseCreate
// );
return LocalServiceRepository._internal(database: database);
}
@ -118,31 +116,27 @@ class LocalServiceRepository {
);
if (filePickResult == null) return null;
var file = File(filePickResult.files.single.path!);
// var md5Output = AccumulatorSink<Digest>();
// ByteConversionSink md5Input = md5.startChunkedConversion(md5Output);
// await for(var bytes in file.openRead()) {
// md5Input.add(bytes);
// }
// md5Input.close();
// return LocalFile(
// file: file,
// filemd5: md5Output.events.single.toString()
// );
return file;
}
Future<void> storeMessages(List<Message> messages) async {
await _database.transaction((txn) async {
for(var message in messages) {
if(message.type == MessageType.image) {
//store image first
storeImage(
image: base64.decode(message.contentDecoded),
msgmd5: message.contentmd5
);
await txn.insert(
'msgs',
message.jsonObject..['content'] = "",
conflictAlgorithm: ConflictAlgorithm.replace
);
}
else {
await txn.insert(
'msgs',
message.jsonObject,
conflictAlgorithm: ConflictAlgorithm.replace
);
}
await txn.insert(
'msgs',
message.jsonObject,
conflictAlgorithm: ConflictAlgorithm.replace
);
}
});
}
@ -168,20 +162,6 @@ class LocalServiceRepository {
for(var rawMessage in rawMessages) {
var message = Message.fromJSONObject(jsonObject: rawMessage);
if(message.contentDecoded.toLowerCase().contains(pattern.toLowerCase())) {
//Since history page does not show message
//There is no need to fetch message here
// if(message.type == MessageType.image) {
// var image = await fetchImage(msgmd5: message.contentmd5);
// if(image != null) {
// alikeMessages.add(message.copyWith(
// content: base64.encode(image),
// ));
// continue;
// }
// else {
// //TODO: do something
// }
// }
alikeMessages.add(message);
}
}
@ -207,8 +187,6 @@ class LocalServiceRepository {
messages.add([]);
}
else {
//Since message page does not show message
//There is no need to fetch message here
messages.add([Message.fromJSONObject(jsonObject: queryResult[0])]);
}
}
@ -234,18 +212,7 @@ class LocalServiceRepository {
limit: num,
offset: position
);
List<Message> messages = [];
for(var result in queryResult) {
var message = Message.fromJSONObject(jsonObject: result);
if(message.type == MessageType.image) {
var image = await fetchImage(msgmd5: message.contentmd5);
if(image != null) {
message = message.copyWith(content: base64.encode(image));
}
}
messages.add(message);
}
return messages;
return queryResult.map((e) => Message.fromJSONObject(jsonObject: e)).toList();
}
Future<File?> findFile({required String filemd5, required String fileName}) async {
@ -400,59 +367,4 @@ class LocalServiceRepository {
return null;
}
}
Future<void> storeImage({required List<int> image, required String msgmd5}) async {
var md5Output = AccumulatorSink<Digest>();
ByteConversionSink md5Input = md5.startChunkedConversion(md5Output);
md5Input.add(image);
md5Input.close();
var imagemd5 = md5Output.events.single.toString();
//Write to image library
var documentPath = (await getApplicationDocumentsDirectory()).path;
await Directory('$documentPath/LChatClient/imgs').create();
var permanentFilePath = '$documentPath/LChatClient/imgs/$imagemd5';
var imageFile = await File(permanentFilePath).create();
imageFile.writeAsBytes(image);
await _database.transaction((txn) async {
txn.insert(
'msgimgs',
{
'msgmd5': msgmd5,
'imgmd5': imagemd5
},
conflictAlgorithm: ConflictAlgorithm.replace
);
txn.insert(
'imgs',
{
'imgmd5': imagemd5,
'dir': permanentFilePath
},
conflictAlgorithm: ConflictAlgorithm.replace
);
});
}
Future<List<int>?> fetchImage({required String msgmd5}) async {
var imageQueryResult = await _database.query(
'msgimgs natural join imgs',
where: 'msgimgs.msgmd5 = ?',
whereArgs: [
msgmd5
],
columns: [
'imgs.dir as dir'
]
);
if(imageQueryResult.isEmpty) {
return null;
}
var path = imageQueryResult[0]['dir'] as String;
var image = File(path);
if(!await image.exists()) {
return null;
}
var imageContent = await image.readAsBytes();
return imageContent;
}
}

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-11 09:44:03
* @LastEditTime : 2022-10-22 21:01:35
* @LastEditTime : 2022-10-18 14:45:20
* @Description : Abstract TCP request class
*/
@ -24,7 +24,6 @@ enum TCPRequestType {
modifyProfile ('MODIFYPROFILE'), //Modify user profile
sendMessage ('SENDMSG'), //Send message
fetchMessage ('FETCHMSG'), //Fetch message
ackFetch ('ACKFETCH'), //Acknowledge message fetch
findFile ('FINDFILE'), //Find file by md5 before transmitting the file
fetchFile ('FETCHFILE'), //Fetch file and file md5 by message md5
searchUser ('SEARCHUSR'), //Search username and userid by username
@ -244,15 +243,4 @@ class FetchContactRequest extends TCPRequest {
@override
Map<String, Object?> get body => {};
}
class AckFetchRequest extends TCPRequest {
final int _timeStamp;
const AckFetchRequest({required int timeStamp, required int? token}): _timeStamp = timeStamp, super(type: TCPRequestType.ackFetch, token: token);
@override
Map<String, Object?> get body => {
'timestamp': _timeStamp
};
}

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-11 09:42:05
* @LastEditTime : 2022-10-22 17:46:28
* @LastEditTime : 2022-10-19 10:41:43
* @Description : TCP repository
*/
@ -26,25 +26,13 @@ class TCPRepository {
required int remotePort
}): _socket = socket, _remoteAddress = remoteAddress, _remotePort = remotePort {
Future(() async {
while(true) {
try{
await for(var response in _socket!) {
_pullResponse(response);
await Future.delayed(const Duration(microseconds: 0));
}
break;
} catch(e) {
_socket?.close();
_socket = null;
while(true) {
try{
_socket = await Socket.connect(remoteAddress, remotePort);
break;
} catch (e) {
continue;
}
}
try{
await for(var response in _socket) {
_pullResponse(response);
await Future.delayed(const Duration(microseconds: 0));
}
} catch(e) {
_socket.close();
}
// _responseRawStreamController.close();
// _payloadPullStreamController.close();
@ -54,40 +42,10 @@ class TCPRepository {
});
//This future never ends, would that be bothersome?
Future(() async {
TCPRequest? failedRequest;
while(true) {
try{
if(failedRequest != null) {
await Future.doWhile(() async {
await Future.delayed(const Duration(microseconds: 0));
return _socket == null;
});
await _socket!.addStream(failedRequest.stream);
}
await for(var request in _requestStreamController.stream) {
failedRequest = request;
await Future.doWhile(() async {
await Future.delayed(const Duration(microseconds: 0));
return _socket == null;
});
await _socket!.addStream(request.stream);
failedRequest = null;
}
break;
} catch (e) {
_socket?.close();
_socket = null;
while(true) {
try{
_socket = await Socket.connect(remoteAddress, remotePort);
break;
} catch (e) {
continue;
}
}
}
await for(var request in _requestStreamController.stream) {
await _socket.addStream(request.stream);
}
});
}).onError((error, stackTrace) {_socket.close();});
Future(() async {
var responseQueue = StreamQueue(_responseRawStreamController.stream);
var payloadQueue = StreamQueue(_payloadRawStreamController.stream);
@ -98,22 +56,14 @@ class TCPRepository {
}
responseQueue.cancel();
payloadQueue.cancel();
}).onError((error, stackTrace) {_socket?.close();});
}).onError((error, stackTrace) {_socket.close();});
}
static Future<TCPRepository> create({
required String serverAddress,
required int serverPort
}) async {
Socket socket;
while(true) {
try{
socket = await Socket.connect(serverAddress, serverPort);
break;
} catch (e) {
continue;
}
}
var socket = await Socket.connect(serverAddress, serverPort);
return TCPRepository._internal(
socket: socket,
remoteAddress: serverAddress,
@ -128,7 +78,7 @@ class TCPRepository {
);
}
Socket? _socket;
final Socket _socket;
final String _remoteAddress;
final int _remotePort;
@ -409,7 +359,7 @@ class TCPRepository {
}
void dispose() async {
await _socket?.flush();
await _socket?.close();
await _socket.flush();
await _socket.close();
}
}

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 17:04:12
* @LastEditTime : 2022-10-23 10:30:41
* @LastEditTime : 2022-10-14 10:38:44
* @Description :
*/
@ -61,7 +61,7 @@ class SearchPage extends StatelessWidget {
const SizedBox(height: 16.0,),
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 24.0,
horizontal: 36.0,
vertical: 8.0
),
child: Text(
@ -80,7 +80,7 @@ class SearchPage extends StatelessWidget {
const SizedBox(height: 16.0,),
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 24.0,
horizontal: 36.0,
vertical: 8.0
),
child: Text(
@ -102,7 +102,7 @@ class SearchPage extends StatelessWidget {
const SizedBox(height: 16.0,),
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 24.0,
horizontal: 36.0,
vertical: 8.0
),
child: Text(

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 21:41:49
* @LastEditTime : 2022-10-23 10:30:06
* @LastEditTime : 2022-10-17 22:23:46
* @Description :
*/
@ -24,8 +24,8 @@ class HistoryTile extends StatelessWidget {
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 24.0
vertical: 16.0,
horizontal: 36.0
),
child: IntrinsicHeight(
child: Row(
@ -87,9 +87,9 @@ class HistoryTile extends StatelessWidget {
if(date.day == DateTime.now().day) {
return '${date.hour}:${date.minute}';
}
//If date is yda, return 'yda'
//If date is yesterday, return 'yesterday'
if(date.day == DateTime.now().day - 1) {
return 'yda ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
return 'yesterday';
}
//If date is within this week, return the weekday in english
if(date.weekday < DateTime.now().weekday) {

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @Date : 2022-10-13 21:41:41
* @LastEditTime : 2022-10-23 10:30:24
* @LastEditTime : 2022-10-18 11:28:17
* @Description :
*/
@ -35,8 +35,8 @@ class UserTile extends StatelessWidget {
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8,
horizontal: 36,
vertical: 16,
),
child: Row(
children: [

View File

@ -78,6 +78,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2+1"
easy_image_viewer:
dependency: "direct main"
description:
name: easy_image_viewer
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
equatable:
dependency: "direct main"
description:
@ -284,13 +291,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.3"
photo_view:
dependency: "direct main"
description:
name: photo_view
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.14.0"
platform:
dependency: transitive
description:

View File

@ -57,8 +57,7 @@ dependencies:
easy_debounce: ^2.0.2+1
path: ^1.8.2
window_manager: ^0.2.7
# easy_image_viewer: ^1.1.0
photo_view: ^0.14.0
easy_image_viewer: ^1.1.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.