More Codes

- Splash page
- Login page
- Register page
This commit is contained in:
Linloir 2022-10-12 18:25:17 +08:00
parent d3a5a32fdb
commit 7b89d8ce14
No known key found for this signature in database
GPG Key ID: 58EEB209A0F2C366
27 changed files with 1319 additions and 165 deletions

2
.gitignore vendored
View File

@ -42,3 +42,5 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
.tmp/

View File

@ -1,6 +0,0 @@
/*
* @Author : Linloir
* @Date : 2022-10-11 11:05:08
* @LastEditTime : 2022-10-11 11:05:08
* @Description :
*/

19
lib/home/home_page.dart Normal file
View File

@ -0,0 +1,19 @@
/*
* @Author : Linloir
* @Date : 2022-10-11 11:05:08
* @LastEditTime : 2022-10-12 11:03:13
* @Description :
*/
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
static Route<void> route() => MaterialPageRoute<void>(builder: (context) => const HomePage());
@override
Widget build(BuildContext context) {
return const Scaffold();
}
}

View File

@ -0,0 +1,68 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 09:56:04
* @LastEditTime : 2022-10-12 17:54:35
* @Description :
*/
import 'package:bloc/bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tcp_client/initialization/cubit/initialization_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';
class InitializationCubit extends Cubit<InitializationState> {
InitializationCubit({
required String serverAddress,
required int serverPort
}): super(const InitializationState.init()) {
TCPRepository? tcpRepository;
LocalServiceRepository? localServiceRepository;
Future(() async {
localServiceRepository = await LocalServiceRepository.create(databaseFilePath: '${(await getApplicationDocumentsDirectory()).path}/.data/database.db');
}).then((_) {
emit(state.copyWith(
databaseStatus: InitializationStatus.done,
localServiceRepository: localServiceRepository
));
});
Future(() async {
tcpRepository = await TCPRepository.create(serverAddress: serverAddress, serverPort: serverPort);
}).then((_) {
emit(state.copyWith(
mainSocketStatus: InitializationStatus.done,
tcpRepository: tcpRepository
));
});
Future(() async {
var tempConnection = await TCPRepository.create(serverAddress: serverAddress, serverPort: serverPort);
var pref = await SharedPreferences.getInstance();
var tokenid = pref.getInt('token');
tempConnection.pushRequest(CheckStateRequest(token: tokenid));
await for(var response in tempConnection.responseStreamBroadcast) {
if(response.type == TCPResponseType.token) {
pref.setInt('token', (response as SetTokenReponse).token);
}
else if(response.type == TCPResponseType.checkState) {
if(response.status == TCPResponseStatus.ok) {
pref.setInt('userid', (response as CheckStateResponse).userInfo!.userID);
}
else {
pref.remove('userid');
}
break;
}
}
tempConnection.dispose();
}).then((_) {
emit(state.copyWith(
tokenStatus: InitializationStatus.done
));
});
}
InitializationCubit.failed(): super(const InitializationState.init());
}

View File

@ -0,0 +1,57 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 09:57:48
* @LastEditTime : 2022-10-12 13:59:14
* @Description :
*/
import 'package:equatable/equatable.dart';
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
enum InitializationStatus { pending, done }
class InitializationState extends Equatable {
const InitializationState({
required this.databaseStatus,
required this.mainSocketStatus,
required this.tokenStatus,
this.localServiceRepository,
this.tcpRepository
});
const InitializationState.init(): this(
databaseStatus: InitializationStatus.pending,
mainSocketStatus: InitializationStatus.pending,
tokenStatus: InitializationStatus.pending
);
final InitializationStatus databaseStatus;
final InitializationStatus mainSocketStatus;
final InitializationStatus tokenStatus;
final LocalServiceRepository? localServiceRepository;
final TCPRepository? tcpRepository;
InitializationState copyWith({
InitializationStatus? databaseStatus,
InitializationStatus? mainSocketStatus,
InitializationStatus? tokenStatus,
LocalServiceRepository? localServiceRepository,
TCPRepository? tcpRepository
}) {
return InitializationState(
databaseStatus: databaseStatus ?? this.databaseStatus,
mainSocketStatus: mainSocketStatus ?? this.mainSocketStatus,
tokenStatus: tokenStatus ?? this.tokenStatus,
localServiceRepository: localServiceRepository ?? this.localServiceRepository,
tcpRepository: tcpRepository ?? this.tcpRepository
);
}
bool get isDone => databaseStatus == InitializationStatus.done &&
mainSocketStatus == InitializationStatus.done &&
tokenStatus == InitializationStatus.done;
@override
List<Object> get props => [databaseStatus, mainSocketStatus, tokenStatus];
}

View File

@ -0,0 +1,151 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 09:53:48
* @LastEditTime : 2022-10-12 14:53:19
* @Description : Splash page before main TCP connection and database is ready
*/
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:loading_indicator/loading_indicator.dart';
import 'package:tcp_client/initialization/cubit/initialization_cubit.dart';
import 'package:tcp_client/initialization/cubit/initialization_state.dart';
class InitializePage extends StatelessWidget {
const InitializePage({super.key});
static Route<void> route({
required String serverAddress,
required int serverPort,
required String databasePath
}) {
return MaterialPageRoute<void>(builder: (context) => const InitializePage());
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Container(
constraints: const BoxConstraints(maxWidth: 48, maxHeight: 48),
child: const LoadingIndicator(
indicatorType: Indicator.ballScale,
colors: [Colors.grey],
),
),
const SizedBox(height: 12,),
BlocBuilder<InitializationCubit, InitializationState>(
builder:(context, state) {
if(state.databaseStatus == InitializationStatus.done) {
return Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
Icons.check_rounded,
color: Colors.green,
),
SizedBox(width: 6,),
Text('Database initialized.')
],
);
}
else {
return Row(
mainAxisSize: MainAxisSize.min,
children: const [
SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(
color: Colors.grey,
strokeWidth: 2,
),
),
SizedBox(width: 6,),
Text('Database initializing...')
],
);
}
},
),
const SizedBox(height: 8,),
BlocBuilder<InitializationCubit, InitializationState>(
builder:(context, state) {
if(state.mainSocketStatus == InitializationStatus.done) {
return Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
Icons.check_rounded,
color: Colors.green,
),
SizedBox(width: 6,),
Text('TCP connection initialized.')
],
);
}
else {
return Row(
mainAxisSize: MainAxisSize.min,
children: const [
SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(
color: Colors.grey,
strokeWidth: 2,
),
),
SizedBox(width: 6,),
Text('TCP connection initializing...')
],
);
}
},
),
const SizedBox(height: 8,),
BlocBuilder<InitializationCubit, InitializationState>(
builder:(context, state) {
if(state.tokenStatus == InitializationStatus.done) {
return Row(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(
Icons.check_rounded,
color: Colors.green,
),
SizedBox(width: 6,),
Text('Device status verified.')
],
);
}
else {
return Row(
mainAxisSize: MainAxisSize.min,
children: const [
SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(
color: Colors.grey,
strokeWidth: 2,
),
),
SizedBox(width: 6,),
Text('Verifying device login status...')
],
);
}
},
),
],
),
),
);
}
}

View File

@ -0,0 +1,78 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:38:07
* @LastEditTime : 2022-10-12 17:36:53
* @Description :
*/
import 'package:bloc/bloc.dart';
import 'package:formz/formz.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tcp_client/login/bloc/login_state.dart';
import 'package:tcp_client/login/models/password.dart';
import 'package:tcp_client/login/models/username.dart';
import 'package:tcp_client/repositories/common_models/useridentity.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 LoginCubit extends Cubit<LoginState> {
LoginCubit({
required this.localServiceRepository,
required this.tcpRepository
}): super(const LoginState());
final LocalServiceRepository localServiceRepository;
final TCPRepository tcpRepository;
void onPasswordChange(Password password) {
emit(state.copyWith(
status: Formz.validate([state.username, password]),
password: password
));
}
Future<void> onUsernameChange(Username username) async {
emit(state.copyWith(
status: Formz.validate([username, state.password]),
username: username,
));
var userinfo = await localServiceRepository.fetchUserInfoViaUsername(username: username.value);
emit(state.copyWith(
avatar: userinfo?.avatarEncoded
));
}
Future<void> onSubmission() async {
if(state.status.isValidated) {
emit(state.copyWith(
status: FormzStatus.submissionInProgress
));
tcpRepository.pushRequest(LoginRequest(
identity: UserIdentity(
username: state.username.value,
password: state.password.value
),
token: (await SharedPreferences.getInstance()).getInt('token')
));
await for(var response in tcpRepository.responseStreamBroadcast) {
if(response.type == TCPResponseType.login) {
if(response.status == TCPResponseStatus.ok) {
var pref = await SharedPreferences.getInstance();
pref.setInt('userid', (response as LoginResponse).userInfo!.userID);
emit(state.copyWith(
status: FormzStatus.submissionSuccess
));
}
else {
emit(state.copyWith(
status: FormzStatus.submissionFailure
));
}
break;
}
}
}
}
}

View File

@ -0,0 +1,43 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:38:13
* @LastEditTime : 2022-10-12 16:24:42
* @Description :
*/
import 'package:equatable/equatable.dart';
import 'package:formz/formz.dart';
import 'package:tcp_client/login/models/password.dart';
import 'package:tcp_client/login/models/username.dart';
class LoginState extends Equatable {
final Username username;
final Password password;
final String avatar;
final FormzStatus status;
const LoginState({
this.status = FormzStatus.pure,
this.username = const Username.pure(),
this.password = const Password.pure(),
this.avatar = ""
});
LoginState copyWith({
FormzStatus? status,
Username? username,
Password? password,
String? avatar
}) {
return LoginState(
status: status ?? this.status,
username: username ?? this.username,
password: password ?? this.password,
avatar: avatar ?? this.avatar
);
}
@override
List<Object?> get props => [status, username, password, avatar];
}

122
lib/login/login_page.dart Normal file
View File

@ -0,0 +1,122 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:06:30
* @LastEditTime : 2022-10-12 18:03:29
* @Description :
*/
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:formz/formz.dart';
import 'package:tcp_client/home/home_page.dart';
import 'package:tcp_client/login/bloc/login_cubit.dart';
import 'package:tcp_client/login/bloc/login_state.dart';
import 'package:tcp_client/login/view/login_form.dart';
import 'package:tcp_client/register/register_page.dart';
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
class LoginPage extends StatelessWidget {
const LoginPage({
required this.localServiceRepository,
required this.tcpRepository,
super.key
});
static Route<void> route({
required LocalServiceRepository localServiceRepository,
required TCPRepository tcpRepository
}) => MaterialPageRoute<void>(builder: (context) => LoginPage(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
));
final LocalServiceRepository localServiceRepository;
final TCPRepository tcpRepository;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LoginCubit(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
),
child: Scaffold(
body: BlocListener<LoginCubit, LoginState>(
listener:(context, state) {
if(state.status == FormzStatus.submissionFailure) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Login Failed'))
);
}
else if(state.status == FormzStatus.submissionSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Login Successed'))
);
Future.delayed(const Duration(seconds: 1)).then((_) {
Navigator.of(context).pushReplacement(HomePage.route());
});
}
},
listenWhen: (previous, current) => previous.status != current.status,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 2,
child: Container(),
),
Expanded(
flex: 6,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: const LoginPanel()
)
),
Expanded(
flex: 2,
child: Container(
alignment: Alignment.topCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Does not have an account?'),
const SizedBox(width: 8,),
TextButton(
onPressed: () => Navigator.of(context).push(RegisterPage.route(localServiceRepository: localServiceRepository, tcpRepository: tcpRepository)),
style: ButtonStyle(
overlayColor: MaterialStateProperty.all(Colors.transparent),
foregroundColor: MaterialStateProperty.all(Colors.blue[800])
),
child: const Text('Register'),
)
],
),
),
),
],
),
),
),
),
);
}
}
class LoginPanel extends StatelessWidget {
const LoginPanel({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
LoginForm()
],
);
}
}

View File

@ -0,0 +1,20 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:37:29
* @LastEditTime : 2022-10-12 15:46:38
* @Description :
*/
import 'package:formz/formz.dart';
enum PasswordValidationError { empty }
class Password extends FormzInput<String, PasswordValidationError> {
const Password.pure() : super.pure('');
const Password.dirty([super.value = '']) : super.dirty();
@override
PasswordValidationError? validator(String? value) {
return value?.isNotEmpty == true ? null : PasswordValidationError.empty;
}
}

View File

@ -0,0 +1,20 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:37:34
* @LastEditTime : 2022-10-12 15:45:11
* @Description :
*/
import 'package:formz/formz.dart';
enum UsernameValidationError { empty }
class Username extends FormzInput<String, UsernameValidationError> {
const Username.pure(): super.pure('');
const Username.dirty([super.value = '']): super.dirty();
@override
UsernameValidationError? validator(String? value) {
return value?.isNotEmpty == true ? null : UsernameValidationError.empty;
}
}

View File

@ -0,0 +1,6 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:37:19
* @LastEditTime : 2022-10-12 17:10:58
* @Description :
*/

View File

@ -0,0 +1,129 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 16:29:25
* @LastEditTime : 2022-10-12 17:31:23
* @Description :
*/
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:formz/formz.dart';
import 'package:tcp_client/login/bloc/login_cubit.dart';
import 'package:tcp_client/login/bloc/login_state.dart';
import 'package:tcp_client/login/models/password.dart';
import 'package:tcp_client/login/models/username.dart';
class LoginForm extends StatelessWidget {
const LoginForm({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
UsernameInput(),
SizedBox(height: 8,),
PasswordInput(),
SizedBox(height: 28,),
SubmitButton()
],
);
}
}
class UsernameInput extends StatelessWidget {
const UsernameInput({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginCubit, LoginState>(
buildWhen: (previous, current) => previous.username != current.username,
builder: (context, state) {
return TextField(
onChanged: (username) {
context.read<LoginCubit>().onUsernameChange(Username.dirty(username));
},
decoration: InputDecoration(
labelText: 'Username',
errorText: state.username.invalid ? 'Invalid username' : null
),
);
},
);
}
}
class PasswordInput extends StatelessWidget {
const PasswordInput({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginCubit, LoginState>(
buildWhen: (previous, current) => previous.password != current.password,
builder: (context, state) {
return TextField(
onChanged: (password) {
context.read<LoginCubit>().onPasswordChange(Password.dirty(password));
},
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
errorText: state.password.invalid ? 'Invalid password' : null
),
);
},
);
}
}
class SubmitButton extends StatelessWidget {
const SubmitButton({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginCubit, LoginState>(
buildWhen: (previous, current) => previous.status != current.status,
builder: (context, state) {
return SizedBox(
height: 40.0,
width: 90.0,
child: TextButton(
style: ButtonStyle(
shape: MaterialStateProperty.all(const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0)))),
backgroundColor: MaterialStateProperty.resolveWith((states) {
switch(states) {
case {MaterialState.disabled}:
return Colors.grey;
case {MaterialState.pressed}:
return Colors.blue[800];
default:
return Colors.blue[700];
}
}),
overlayColor: MaterialStateProperty.all(Colors.blue[900]!.withOpacity(0.2))
),
onPressed: state.status == FormzStatus.submissionInProgress ? null : () {
context.read<LoginCubit>().onSubmission();
},
child: state.status == FormzStatus.submissionInProgress ?
const SizedBox(
height: 14.0,
width: 14.0,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
) :
const Text(
'LOGIN',
style: TextStyle(
color: Colors.white
),
)
)
);
},
);
}
}

View File

@ -1,5 +1,18 @@
/*
* @Author : Linloir
* @Date : 2022-10-10 08:04:53
* @LastEditTime : 2022-10-12 17:55:07
* @Description :
*/
import 'package:flutter/material.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: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';
void main() { void main() {
sqfliteFfiInit(); sqfliteFfiInit();
@ -15,103 +28,43 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'Flutter Demo',
theme: ThemeData( theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
), ),
home: const MyHomePage(title: 'Flutter Demo Home Page'), home: const SplashPage(),
); );
} }
} }
class MyHomePage extends StatefulWidget { class SplashPage extends StatelessWidget {
const MyHomePage({super.key, required this.title}); const SplashPage({super.key});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done return BlocProvider<InitializationCubit>(
// by the _incrementCounter method above. create: (context) {
// return InitializationCubit(
// The Flutter framework has been optimized to make rerunning build methods serverAddress: '127.0.0.1',
// fast, so that you can just rebuild anything that needs updating rather serverPort: 20706
// than having to individually change instances of widgets. );
return Scaffold( },
appBar: AppBar( child: BlocListener<InitializationCubit, InitializationState>(
// Here we take the value from the MyHomePage object that was created by listener: (context, state) {
// the App.build method, and use it to set our appbar title. if(state.isDone) {
title: Text(widget.title), Future.delayed(const Duration(seconds: 1)).then((_) async {
), if((await SharedPreferences.getInstance()).getInt('userid') != null) {
body: Center( Navigator.of(context).pushReplacement(HomePage.route());
// Center is a layout widget. It takes a single child and positions it }
// in the middle of the parent. else {
child: Column( Navigator.of(context).pushReplacement(LoginPage.route(
// Column is also a layout widget. It takes a list of children and localServiceRepository: state.localServiceRepository!,
// arranges them vertically. By default, it sizes itself to fit its tcpRepository: state.tcpRepository!
// children horizontally, and tries to be as tall as its parent. ));
// }
// Invoke "debug painting" (press "p" in the console, choose the });
// "Toggle Debug Paint" action from the Flutter Inspector in Android }
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code) },
// to see the wireframe for each widget. child: const InitializePage(),
// )
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
); );
} }
} }

View File

@ -0,0 +1,78 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:38:07
* @LastEditTime : 2022-10-12 17:52:03
* @Description :
*/
import 'package:bloc/bloc.dart';
import 'package:formz/formz.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tcp_client/register/bloc/register_state.dart';
import 'package:tcp_client/register/models/password.dart';
import 'package:tcp_client/register/models/username.dart';
import 'package:tcp_client/repositories/common_models/useridentity.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 RegisterCubit extends Cubit<RegisterState> {
RegisterCubit({
required this.localServiceRepository,
required this.tcpRepository
}): super(const RegisterState());
final LocalServiceRepository localServiceRepository;
final TCPRepository tcpRepository;
void onPasswordChange(Password password) {
emit(state.copyWith(
status: Formz.validate([state.username, password]),
password: password
));
}
Future<void> onUsernameChange(Username username) async {
emit(state.copyWith(
status: Formz.validate([username, state.password]),
username: username,
));
var userinfo = await localServiceRepository.fetchUserInfoViaUsername(username: username.value);
emit(state.copyWith(
avatar: userinfo?.avatarEncoded
));
}
Future<void> onSubmission() async {
if(state.status.isValidated) {
emit(state.copyWith(
status: FormzStatus.submissionInProgress
));
tcpRepository.pushRequest(RegisterRequest(
identity: UserIdentity(
username: state.username.value,
password: state.password.value
),
token: (await SharedPreferences.getInstance()).getInt('token')
));
await for(var response in tcpRepository.responseStreamBroadcast) {
if(response.type == TCPResponseType.register) {
if(response.status == TCPResponseStatus.ok) {
var pref = await SharedPreferences.getInstance();
pref.setInt('userid', (response as RegisterResponse).userInfo!.userID);
emit(state.copyWith(
status: FormzStatus.submissionSuccess
));
}
else {
emit(state.copyWith(
status: FormzStatus.submissionFailure
));
}
break;
}
}
}
}
}

View File

@ -0,0 +1,43 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:38:13
* @LastEditTime : 2022-10-12 17:40:39
* @Description :
*/
import 'package:equatable/equatable.dart';
import 'package:formz/formz.dart';
import 'package:tcp_client/register/models/password.dart';
import 'package:tcp_client/register/models/username.dart';
class RegisterState extends Equatable {
final Username username;
final Password password;
final String avatar;
final FormzStatus status;
const RegisterState({
this.status = FormzStatus.pure,
this.username = const Username.pure(),
this.password = const Password.pure(),
this.avatar = ""
});
RegisterState copyWith({
FormzStatus? status,
Username? username,
Password? password,
String? avatar
}) {
return RegisterState(
status: status ?? this.status,
username: username ?? this.username,
password: password ?? this.password,
avatar: avatar ?? this.avatar
);
}
@override
List<Object?> get props => [status, username, password, avatar];
}

View File

@ -0,0 +1,20 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:37:29
* @LastEditTime : 2022-10-12 15:46:38
* @Description :
*/
import 'package:formz/formz.dart';
enum PasswordValidationError { empty }
class Password extends FormzInput<String, PasswordValidationError> {
const Password.pure() : super.pure('');
const Password.dirty([super.value = '']) : super.dirty();
@override
PasswordValidationError? validator(String? value) {
return value?.isNotEmpty == true ? null : PasswordValidationError.empty;
}
}

View File

@ -0,0 +1,20 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 15:37:34
* @LastEditTime : 2022-10-12 15:45:11
* @Description :
*/
import 'package:formz/formz.dart';
enum UsernameValidationError { empty }
class Username extends FormzInput<String, UsernameValidationError> {
const Username.pure(): super.pure('');
const Username.dirty([super.value = '']): super.dirty();
@override
UsernameValidationError? validator(String? value) {
return value?.isNotEmpty == true ? null : UsernameValidationError.empty;
}
}

View File

@ -0,0 +1,114 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 17:36:38
* @LastEditTime : 2022-10-12 17:50:42
* @Description :
*/
/*
* @Author : Linloir
* @Date : 2022-10-12 15:06:30
* @LastEditTime : 2022-10-12 17:34:10
* @Description :
*/
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:formz/formz.dart';
import 'package:tcp_client/home/home_page.dart';
import 'package:tcp_client/register/bloc/register_cubit.dart';
import 'package:tcp_client/register/bloc/register_state.dart';
import 'package:tcp_client/register/view/register_form.dart';
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';
class RegisterPage extends StatelessWidget {
const RegisterPage({
required this.localServiceRepository,
required this.tcpRepository,
super.key
});
static Route<void> route({
required LocalServiceRepository localServiceRepository,
required TCPRepository tcpRepository
}) => MaterialPageRoute<void>(builder: (context) => RegisterPage(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
));
final LocalServiceRepository localServiceRepository;
final TCPRepository tcpRepository;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => RegisterCubit(
localServiceRepository: localServiceRepository,
tcpRepository: tcpRepository
),
child: Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.blue[800],
),
body: BlocListener<RegisterCubit, RegisterState>(
listener:(context, state) {
if(state.status == FormzStatus.submissionFailure) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Register Failed'))
);
}
else if(state.status == FormzStatus.submissionSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Register Successed'))
);
Future.delayed(const Duration(seconds: 1)).then((_) {
Navigator.of(context).pushReplacement(HomePage.route());
});
}
},
listenWhen: (previous, current) => previous.status != current.status,
child: Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 2,
child: Container(),
),
const Expanded(
flex: 6,
child: RegisterPanel()
),
Expanded(
flex: 2,
child: Container(),
)
],
),
),
),
),
),
);
}
}
class RegisterPanel extends StatelessWidget {
const RegisterPanel({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
RegisterForm()
],
);
}
}

View File

@ -0,0 +1,129 @@
/*
* @Author : Linloir
* @Date : 2022-10-12 16:29:25
* @LastEditTime : 2022-10-12 17:44:33
* @Description :
*/
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:formz/formz.dart';
import 'package:tcp_client/register/bloc/register_cubit.dart';
import 'package:tcp_client/register/bloc/register_state.dart';
import 'package:tcp_client/register/models/password.dart';
import 'package:tcp_client/register/models/username.dart';
class RegisterForm extends StatelessWidget {
const RegisterForm({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
UsernameInput(),
SizedBox(height: 8,),
PasswordInput(),
SizedBox(height: 28,),
SubmitButton()
],
);
}
}
class UsernameInput extends StatelessWidget {
const UsernameInput({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RegisterCubit, RegisterState>(
buildWhen: (previous, current) => previous.username != current.username,
builder: (context, state) {
return TextField(
onChanged: (username) {
context.read<RegisterCubit>().onUsernameChange(Username.dirty(username));
},
decoration: InputDecoration(
labelText: 'Username',
errorText: state.username.invalid ? 'Invalid username' : null
),
);
},
);
}
}
class PasswordInput extends StatelessWidget {
const PasswordInput({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RegisterCubit, RegisterState>(
buildWhen: (previous, current) => previous.password != current.password,
builder: (context, state) {
return TextField(
onChanged: (password) {
context.read<RegisterCubit>().onPasswordChange(Password.dirty(password));
},
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
errorText: state.password.invalid ? 'Invalid password' : null
),
);
},
);
}
}
class SubmitButton extends StatelessWidget {
const SubmitButton({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RegisterCubit, RegisterState>(
buildWhen: (previous, current) => previous.status != current.status,
builder: (context, state) {
return SizedBox(
height: 40.0,
width: 100.0,
child: TextButton(
style: ButtonStyle(
shape: MaterialStateProperty.all(const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0)))),
backgroundColor: MaterialStateProperty.resolveWith((states) {
switch(states) {
case {MaterialState.disabled}:
return Colors.grey;
case {MaterialState.pressed}:
return Colors.blue[800];
default:
return Colors.blue[700];
}
}),
overlayColor: MaterialStateProperty.all(Colors.blue[900]!.withOpacity(0.2))
),
onPressed: state.status == FormzStatus.submissionInProgress ? null : () {
context.read<RegisterCubit>().onSubmission();
},
child: state.status == FormzStatus.submissionInProgress ?
const SizedBox(
height: 14.0,
width: 14.0,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
) :
const Text(
'REGISTER',
style: TextStyle(
color: Colors.white
),
)
)
);
},
);
}
}

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-11 10:56:02 * @Date : 2022-10-11 10:56:02
* @LastEditTime : 2022-10-11 23:49:16 * @LastEditTime : 2022-10-12 15:35:30
* @Description : Local Service Repository * @Description : Local Service Repository
*/ */
@ -74,8 +74,7 @@ class LocalServiceRepository {
var file = File(filePickResult.files.single.path!); var file = File(filePickResult.files.single.path!);
return LocalFile( return LocalFile(
file: file, file: file,
filemd5: md5.convert(await file.readAsBytes()).toString(), filemd5: md5.convert(await file.readAsBytes()).toString()
ext: file.path.substring(file.path.lastIndexOf('.'))
); );
} }
@ -157,7 +156,7 @@ class LocalServiceRepository {
return queryResult.map((e) => Message.fromJSONObject(jsonObject: e)).toList(); return queryResult.map((e) => Message.fromJSONObject(jsonObject: e)).toList();
} }
Future<File?> findFile({required String filemd5}) async { Future<File?> findFile({required String filemd5, required String fileName}) async {
var directory = await _database.query( var directory = await _database.query(
'files', 'files',
where: 'filemd5 = ?', where: 'filemd5 = ?',
@ -173,7 +172,25 @@ class LocalServiceRepository {
//Try if the file exists //Try if the file exists
var file = File(filePath); var file = File(filePath);
if(await file.exists()) { if(await file.exists()) {
return file; //Copy to desired file path
var pref = await SharedPreferences.getInstance();
var userID = pref.getInt('userid');
var documentPath = (await getApplicationDocumentsDirectory()).path;
var fileBaseName = fileName.substring(0, fileName.lastIndexOf('.'));
var fileExt = fileName.substring(fileName.lastIndexOf('.'));
var duplicate = 0;
//Rename target file
await Directory('$documentPath/files').create();
await Directory('$documentPath/files/$userID').create();
var targetFilePath = '$documentPath/files/$userID/$fileBaseName$fileExt';
var targetFile = File(targetFilePath);
while(await targetFile.exists()) {
duplicate += 1;
targetFilePath = '$documentPath/files/$userID/$fileBaseName($duplicate)$fileExt';
targetFile = File(targetFilePath);
}
targetFile = await file.copy(targetFilePath);
return targetFile;
} }
else { else {
//Delete all linked files //Delete all linked files
@ -184,6 +201,7 @@ class LocalServiceRepository {
filemd5 filemd5
] ]
); );
//TODO: maybe throw some error here?
return null; return null;
} }
} }
@ -194,7 +212,9 @@ class LocalServiceRepository {
}) async { }) async {
//Write to file library //Write to file library
var documentPath = (await getApplicationDocumentsDirectory()).path; var documentPath = (await getApplicationDocumentsDirectory()).path;
var permanentFilePath = '$documentPath/files/${tempFile.filemd5}${tempFile.ext}'; await Directory('$documentPath/files').create();
await Directory('$documentPath/files/.lib').create();
var permanentFilePath = '$documentPath/files/.lib/${tempFile.filemd5}';
await tempFile.file.copy(permanentFilePath); await tempFile.file.copy(permanentFilePath);
await _database.insert( await _database.insert(
'files', 'files',
@ -234,7 +254,7 @@ class LocalServiceRepository {
_userInfoChangeStreamController.add(userInfo); _userInfoChangeStreamController.add(userInfo);
} }
Future<UserInfo?> fetchUserInfo({required userid}) async { Future<UserInfo?> fetchUserInfoViaID({required int userid}) async {
var targetUser = await _database.query( var targetUser = await _database.query(
'users', 'users',
where: 'userid = ?', where: 'userid = ?',
@ -247,4 +267,8 @@ class LocalServiceRepository {
return UserInfo.fromJSONObject(jsonObject: targetUser[0]); return UserInfo.fromJSONObject(jsonObject: targetUser[0]);
} }
} }
Future<UserInfo?> fetchUserInfoViaUsername({required String username}) async {
}
} }

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-11 10:55:36 * @Date : 2022-10-11 10:55:36
* @LastEditTime : 2022-10-11 22:54:59 * @LastEditTime : 2022-10-12 09:19:50
* @Description : Local File Model * @Description : Local File Model
*/ */
@ -12,9 +12,8 @@ import 'package:equatable/equatable.dart';
class LocalFile extends Equatable { class LocalFile extends Equatable {
final File file; final File file;
final String filemd5; final String filemd5;
final String ext;
const LocalFile({required this.file, required this.filemd5, required this.ext}); const LocalFile({required this.file, required this.filemd5});
@override @override
List<Object> get props => [filemd5]; List<Object> get props => [filemd5];

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-11 09:44:03 * @Date : 2022-10-11 09:44:03
* @LastEditTime : 2022-10-11 16:55:08 * @LastEditTime : 2022-10-12 13:56:56
* @Description : Abstract TCP request class * @Description : Abstract TCP request class
*/ */
@ -43,12 +43,12 @@ enum TCPRequestType {
abstract class TCPRequest { abstract class TCPRequest {
final TCPRequestType _type; final TCPRequestType _type;
final int _token; final int? _token;
const TCPRequest({required TCPRequestType type, required int token}): _type = type, _token = token; const TCPRequest({required TCPRequestType type, required int? token}): _type = type, _token = token;
TCPRequestType get type => _type; TCPRequestType get type => _type;
int get token => _token; int? get token => _token;
Map<String, Object?> get body; Map<String, Object?> get body;
@ -56,7 +56,7 @@ abstract class TCPRequest {
return jsonEncode({ return jsonEncode({
'request': type.value, 'request': type.value,
'body': body, 'body': body,
'token': token 'tokenid': token
}); });
} }
@ -70,7 +70,7 @@ abstract class TCPRequest {
} }
class CheckStateRequest extends TCPRequest { class CheckStateRequest extends TCPRequest {
const CheckStateRequest({required int token}): super(type: TCPRequestType.checkState, token: token); const CheckStateRequest({required int? token}): super(type: TCPRequestType.checkState, token: token);
@override @override
Map<String, Object?> get body => {}; Map<String, Object?> get body => {};
@ -81,7 +81,7 @@ class RegisterRequest extends TCPRequest {
RegisterRequest({ RegisterRequest({
required UserIdentity identity, required UserIdentity identity,
required token required int? token
}): _identity = identity, super(type: TCPRequestType.register, token: token); }): _identity = identity, super(type: TCPRequestType.register, token: token);
@override @override
@ -93,7 +93,7 @@ class LoginRequest extends TCPRequest {
LoginRequest({ LoginRequest({
required UserIdentity identity, required UserIdentity identity,
required token required int? token
}): _identity = identity, super(type: TCPRequestType.login, token: token); }): _identity = identity, super(type: TCPRequestType.login, token: token);
@override @override
@ -101,14 +101,14 @@ class LoginRequest extends TCPRequest {
} }
class LogoutRequest extends TCPRequest { class LogoutRequest extends TCPRequest {
const LogoutRequest({required int token}): super(type: TCPRequestType.logout, token: token); const LogoutRequest({required int? token}): super(type: TCPRequestType.logout, token: token);
@override @override
Map<String, Object?> get body => {}; Map<String, Object?> get body => {};
} }
class GetProfileRequest extends TCPRequest { class GetProfileRequest extends TCPRequest {
const GetProfileRequest({required int token}): super(type: TCPRequestType.profile, token: token); const GetProfileRequest({required int? token}): super(type: TCPRequestType.profile, token: token);
@override @override
Map<String, Object?> get body => {}; Map<String, Object?> get body => {};
@ -119,7 +119,7 @@ class ModifyPasswordRequest extends TCPRequest {
ModifyPasswordRequest({ ModifyPasswordRequest({
required UserIdentity identity, required UserIdentity identity,
required token required int? token
}): _identity = identity, super(type: TCPRequestType.modifyPassword, token: token); }): _identity = identity, super(type: TCPRequestType.modifyPassword, token: token);
@override @override
@ -131,7 +131,7 @@ class ModifyProfileRequest extends TCPRequest {
const ModifyProfileRequest ({ const ModifyProfileRequest ({
required UserInfo userInfo, required UserInfo userInfo,
required int token required int? token
}): _userinfo = userInfo, super(type: TCPRequestType.modifyProfile, token: token); }): _userinfo = userInfo, super(type: TCPRequestType.modifyProfile, token: token);
@override @override
@ -143,7 +143,7 @@ class SendMessageRequest extends TCPRequest {
SendMessageRequest({ SendMessageRequest({
required Message message, required Message message,
required int token required int? token
}): }):
_message = message, _message = message,
super(type: TCPRequestType.sendMessage, token: token); super(type: TCPRequestType.sendMessage, token: token);
@ -170,7 +170,7 @@ class SendMessageRequest extends TCPRequest {
} }
class FetchMessageRequest extends TCPRequest { class FetchMessageRequest extends TCPRequest {
const FetchMessageRequest({required int token}): super(type: TCPRequestType.fetchMessage, token: token); const FetchMessageRequest({required int? token}): super(type: TCPRequestType.fetchMessage, token: token);
@override @override
Map<String, Object?> get body => {}; Map<String, Object?> get body => {};
@ -179,7 +179,7 @@ class FetchMessageRequest extends TCPRequest {
class FindFileRequest extends TCPRequest { class FindFileRequest extends TCPRequest {
final LocalFile _file; final LocalFile _file;
const FindFileRequest({required LocalFile file, required int token}): _file = file, super(type: TCPRequestType.findFile, token: token); const FindFileRequest({required LocalFile file, required int? token}): _file = file, super(type: TCPRequestType.findFile, token: token);
@override @override
Map<String, Object?> get body => { Map<String, Object?> get body => {
@ -192,7 +192,7 @@ class FindFileRequest extends TCPRequest {
class FetchFileRequest extends TCPRequest { class FetchFileRequest extends TCPRequest {
final String _msgmd5; final String _msgmd5;
const FetchFileRequest({required String msgmd5, required int token}): _msgmd5 = msgmd5, super(type: TCPRequestType.fetchFile, token: token); const FetchFileRequest({required String msgmd5, required int? token}): _msgmd5 = msgmd5, super(type: TCPRequestType.fetchFile, token: token);
@override @override
Map<String, Object?> get body => { Map<String, Object?> get body => {
@ -205,7 +205,7 @@ class FetchFileRequest extends TCPRequest {
class SearchUserRequest extends TCPRequest { class SearchUserRequest extends TCPRequest {
final String _username; final String _username;
const SearchUserRequest({required String username, required int token}): _username = username, super(type: TCPRequestType.searchUser, token: token); const SearchUserRequest({required String username, required int? token}): _username = username, super(type: TCPRequestType.searchUser, token: token);
@override @override
Map<String, Object?> get body => { Map<String, Object?> get body => {
@ -218,7 +218,7 @@ class SearchUserRequest extends TCPRequest {
class AddContactRequest extends TCPRequest { class AddContactRequest extends TCPRequest {
final int _userid; final int _userid;
const AddContactRequest({required int userid, required int token}): _userid = userid, super(type: TCPRequestType.addContact, token: token); const AddContactRequest({required int userid, required int? token}): _userid = userid, super(type: TCPRequestType.addContact, token: token);
@override @override
Map<String, Object?> get body => { Map<String, Object?> get body => {
@ -229,7 +229,7 @@ class AddContactRequest extends TCPRequest {
} }
class FetchContactRequest extends TCPRequest { class FetchContactRequest extends TCPRequest {
const FetchContactRequest({required int token}): super(type: TCPRequestType.fetchContact, token: token); const FetchContactRequest({required int? token}): super(type: TCPRequestType.fetchContact, token: token);
@override @override
Map<String, Object?> get body => {}; Map<String, Object?> get body => {};

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-11 11:02:19 * @Date : 2022-10-11 11:02:19
* @LastEditTime : 2022-10-11 22:55:48 * @LastEditTime : 2022-10-12 13:25:49
* @Description : * @Description :
*/ */
@ -84,39 +84,39 @@ class SetTokenReponse extends TCPResponse {
} }
class CheckStateResponse extends TCPResponse { class CheckStateResponse extends TCPResponse {
late final UserInfo _userInfo; late final UserInfo? _userInfo;
CheckStateResponse({ CheckStateResponse({
required Map<String, Object?> jsonObject required Map<String, Object?> jsonObject
}): super(jsonObject: jsonObject) { }): super(jsonObject: jsonObject) {
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>); _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
} }
UserInfo get userInfo => _userInfo; UserInfo? get userInfo => _userInfo;
} }
class RegisterResponse extends TCPResponse { class RegisterResponse extends TCPResponse {
late final UserInfo _userInfo; late final UserInfo? _userInfo;
RegisterResponse({ RegisterResponse({
required Map<String, Object?> jsonObject required Map<String, Object?> jsonObject
}): super(jsonObject: jsonObject) { }): super(jsonObject: jsonObject) {
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>); _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
} }
UserInfo get userInfo => _userInfo; UserInfo? get userInfo => _userInfo;
} }
class LoginResponse extends TCPResponse { class LoginResponse extends TCPResponse {
late final UserInfo _userInfo; late final UserInfo? _userInfo;
LoginResponse({ LoginResponse({
required Map<String, Object?> jsonObject required Map<String, Object?> jsonObject
}): super(jsonObject: jsonObject) { }): super(jsonObject: jsonObject) {
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>); _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
} }
UserInfo get userInfo => _userInfo; UserInfo? get userInfo => _userInfo;
} }
class LogoutResponse extends TCPResponse { class LogoutResponse extends TCPResponse {
@ -126,15 +126,15 @@ class LogoutResponse extends TCPResponse {
} }
class GetProfileResponse extends TCPResponse { class GetProfileResponse extends TCPResponse {
late final UserInfo _userInfo; late final UserInfo? _userInfo;
GetProfileResponse({ GetProfileResponse({
required Map<String, Object?> jsonObject required Map<String, Object?> jsonObject
}): super(jsonObject: jsonObject) { }): super(jsonObject: jsonObject) {
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>); _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
} }
UserInfo get userInfo => _userInfo; UserInfo? get userInfo => _userInfo;
} }
class ModifyPasswordResponse extends TCPResponse { class ModifyPasswordResponse extends TCPResponse {
@ -144,15 +144,15 @@ class ModifyPasswordResponse extends TCPResponse {
} }
class ModifyProfileResponse extends TCPResponse { class ModifyProfileResponse extends TCPResponse {
late final UserInfo _userInfo; late final UserInfo? _userInfo;
ModifyProfileResponse({ ModifyProfileResponse({
required Map<String, Object?> jsonObject required Map<String, Object?> jsonObject
}): super(jsonObject: jsonObject) { }): super(jsonObject: jsonObject) {
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>); _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
} }
UserInfo get userInfo => _userInfo; UserInfo? get userInfo => _userInfo;
} }
class SendMessageResponse extends TCPResponse { class SendMessageResponse extends TCPResponse {
@ -203,8 +203,7 @@ class FetchFileResponse extends TCPResponse {
}): super(jsonObject: jsonObject) { }): super(jsonObject: jsonObject) {
_payload = LocalFile( _payload = LocalFile(
file: payload.file, file: payload.file,
filemd5: payload.filemd5, filemd5: payload.filemd5
ext: (jsonObject['body'] as Map<String, Object?>)['ext'] as String
); );
} }
@ -212,15 +211,15 @@ class FetchFileResponse extends TCPResponse {
} }
class SearchUserResponse extends TCPResponse { class SearchUserResponse extends TCPResponse {
late final UserInfo _userInfo; late final UserInfo? _userInfo;
SearchUserResponse({ SearchUserResponse({
required Map<String, Object?> jsonObject required Map<String, Object?> jsonObject
}): super(jsonObject: jsonObject) { }): super(jsonObject: jsonObject) {
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>); _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
} }
UserInfo get userInfo => _userInfo; UserInfo? get userInfo => _userInfo;
} }
class AddContactResponse extends TCPResponse { class AddContactResponse extends TCPResponse {

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-11 09:42:05 * @Date : 2022-10-11 09:42:05
* @LastEditTime : 2022-10-11 22:55:28 * @LastEditTime : 2022-10-12 16:57:50
* @Description : TCP repository * @Description : TCP repository
*/ */
@ -9,15 +9,17 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:async/async.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tcp_client/repositories/common_models/message.dart';
import 'package:tcp_client/repositories/local_service_repository/models/local_file.dart'; import 'package:tcp_client/repositories/local_service_repository/models/local_file.dart';
import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.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/models/tcp_response.dart';
class TCPRepository { class TCPRepository {
TCPRepository({ TCPRepository._internal({
required Socket socket, required Socket socket,
required String remoteAddress, required String remoteAddress,
required int remotePort required int remotePort
@ -30,13 +32,30 @@ class TCPRepository {
} }
}); });
Future(() async { Future(() async {
await for(var response in _responseRawStreamController.stream) { var responseQueue = StreamQueue(_responseRawStreamController.stream);
var payloadFile = await _payloadRawStreamController.stream.single; var payloadQueue = StreamQueue(_payloadRawStreamController.stream);
await _pushResponse(responseBytes: response, tempFile: payloadFile); while(await Future<bool>(() => !_responseRawStreamController.isClosed && !_payloadRawStreamController.isClosed)) {
var response = await responseQueue.next;
var payload = await payloadQueue.next;
await _pushResponse(responseBytes: response, tempFile: payload);
} }
responseQueue.cancel();
payloadQueue.cancel();
}); });
} }
static Future<TCPRepository> create({
required String serverAddress,
required int serverPort
}) async {
var socket = await Socket.connect(serverAddress, serverPort);
return TCPRepository._internal(
socket: socket,
remoteAddress: serverAddress,
remotePort: serverPort
);
}
final Socket _socket; final Socket _socket;
final String _remoteAddress; final String _remoteAddress;
final int _remotePort; final int _remotePort;
@ -67,8 +86,32 @@ class TCPRepository {
//Provide a request stream for widgets to push to //Provide a request stream for widgets to push to
final StreamController<TCPRequest> _requestStreamController = StreamController(); final StreamController<TCPRequest> _requestStreamController = StreamController();
Future<void> pushRequest(TCPRequest request) async { void pushRequest(TCPRequest request) {
_requestStreamController.add(request); if(request.type == TCPRequestType.sendMessage) {
request as SendMessageRequest;
if(request.message.type == MessageType.file) {
Future(() async {
//Duplicate current socket
Socket socket = await Socket.connect(_remoteAddress, _remotePort);
TCPRepository duplicatedRepository = TCPRepository._internal(
socket: socket,
remoteAddress: _remoteAddress,
remotePort: _remotePort
);
duplicatedRepository._requestStreamController.add(request);
await for(var response in duplicatedRepository.responseStream) {
if(response.type == TCPResponseType.sendMessage) {
_responseStreamController.add(response);
break;
}
}
duplicatedRepository.dispose();
});
}
}
else {
_requestStreamController.add(request);
}
} }
//Listen to the incoming stream and emits event whenever there is a intact request //Listen to the incoming stream and emits event whenever there is a intact request
@ -86,16 +129,20 @@ class TCPRepository {
//Clear the length indicator bytes //Clear the length indicator bytes
buffer.removeRange(0, 8); buffer.removeRange(0, 8);
//Create temp file to read payload (might be huge) //Create temp file to read payload (might be huge)
var tempFile = File('${Directory.current.path}/.tmp/${DateTime.now().microsecondsSinceEpoch}')..createSync(); Directory('${Directory.current.path}/.tmp').createSync();
//Create a pull stream for payload file //Create a pull stream for payload file
_payloadPullStreamController = StreamController(); _payloadPullStreamController = StreamController();
//Create a future that listens to the status of the payload transmission //Create a future that listens to the status of the payload transmission
Future(() async { () {
await for(var data in _payloadPullStreamController.stream) { var payloadPullStream = _payloadPullStreamController.stream;
await tempFile.writeAsBytes(data, mode: FileMode.append, flush: true); var tempFile = File('${Directory.current.path}/.tmp/${DateTime.now().microsecondsSinceEpoch}')..createSync();
} Future(() async {
_payloadRawStreamController.add(tempFile); await for(var data in payloadPullStream) {
}); await tempFile.writeAsBytes(data, mode: FileMode.append, flush: true);
}
_payloadRawStreamController.add(tempFile);
});
}();
} }
else { else {
//Buffered data is not long enough //Buffered data is not long enough
@ -111,7 +158,6 @@ class TCPRepository {
//Got intact request json //Got intact request json
//Emit request buffer through stream //Emit request buffer through stream
_responseRawStreamController.add(buffer.sublist(0, responseLength)); _responseRawStreamController.add(buffer.sublist(0, responseLength));
_responseRawStreamController.close();
//Remove proccessed buffer //Remove proccessed buffer
buffer.removeRange(0, responseLength); buffer.removeRange(0, responseLength);
//Clear awaiting request length //Clear awaiting request length
@ -156,6 +202,10 @@ class TCPRepository {
required List<int> responseBytes, required List<int> responseBytes,
required File tempFile required File tempFile
}) async { }) async {
if(_responseStreamController.isClosed) {
await tempFile.delete();
return;
}
var responseJSON = String.fromCharCodes(responseBytes); var responseJSON = String.fromCharCodes(responseBytes);
var responseObject = jsonDecode(responseJSON); var responseObject = jsonDecode(responseJSON);
TCPResponseType responseType = TCPResponseType.fromValue(responseObject['response'] as String); TCPResponseType responseType = TCPResponseType.fromValue(responseObject['response'] as String);
@ -225,8 +275,7 @@ class TCPRepository {
jsonObject: responseObject, jsonObject: responseObject,
payload: LocalFile( payload: LocalFile(
file: tempFile, file: tempFile,
filemd5: md5.convert(await tempFile.readAsBytes()).toString(), filemd5: md5.convert(await tempFile.readAsBytes()).toString()
ext: ""
) )
)); ));
break; break;
@ -258,7 +307,7 @@ class TCPRepository {
}) async { }) async {
//Duplicate current socket //Duplicate current socket
Socket socket = await Socket.connect(_remoteAddress, _remotePort); Socket socket = await Socket.connect(_remoteAddress, _remotePort);
TCPRepository duplicatedRepository = TCPRepository( TCPRepository duplicatedRepository = TCPRepository._internal(
socket: socket, socket: socket,
remoteAddress: _remoteAddress, remoteAddress: _remoteAddress,
remotePort: _remotePort remotePort: _remotePort
@ -278,11 +327,11 @@ class TCPRepository {
} }
void dispose() { void dispose() {
_socket.close();
_responseRawStreamController.close(); _responseRawStreamController.close();
_payloadPullStreamController.close(); _payloadPullStreamController.close();
_payloadRawStreamController.close(); _payloadRawStreamController.close();
_responseStreamController.close(); _responseStreamController.close();
_requestStreamController.close(); _requestStreamController.close();
_socket.close();
} }
} }

View File

@ -2,7 +2,7 @@
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
async: async:
dependency: transitive dependency: "direct main"
description: description:
name: async name: async
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
@ -156,6 +156,13 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
loading_indicator:
dependency: "direct main"
description:
name: loading_indicator
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -380,6 +387,13 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
stream_transform:
dependency: "direct main"
description:
name: stream_transform
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:

View File

@ -46,6 +46,9 @@ dependencies:
git: git:
url: https://github.com/crazecoder/open_file url: https://github.com/crazecoder/open_file
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15
loading_indicator: ^3.1.0
async: ^2.9.0
stream_transform: ^2.0.1
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.