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/profile
/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_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';
void main() {
sqfliteFfiInit();
@ -15,103 +28,43 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Flutter Demo',
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,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
home: const SplashPage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// 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++;
});
}
class SplashPage extends StatelessWidget {
const SplashPage({super.key});
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// 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.
//
// 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.
return BlocProvider<InitializationCubit>(
create: (context) {
return InitializationCubit(
serverAddress: '127.0.0.1',
serverPort: 20706
);
},
child: BlocListener<InitializationCubit, InitializationState>(
listener: (context, state) {
if(state.isDone) {
Future.delayed(const Duration(seconds: 1)).then((_) async {
if((await SharedPreferences.getInstance()).getInt('userid') != null) {
Navigator.of(context).pushReplacement(HomePage.route());
}
else {
Navigator.of(context).pushReplacement(LoginPage.route(
localServiceRepository: state.localServiceRepository!,
tcpRepository: state.tcpRepository!
));
}
});
}
},
child: const InitializePage(),
)
);
}
}

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
* @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
*/
@ -74,8 +74,7 @@ class LocalServiceRepository {
var file = File(filePickResult.files.single.path!);
return LocalFile(
file: file,
filemd5: md5.convert(await file.readAsBytes()).toString(),
ext: file.path.substring(file.path.lastIndexOf('.'))
filemd5: md5.convert(await file.readAsBytes()).toString()
);
}
@ -157,7 +156,7 @@ class LocalServiceRepository {
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(
'files',
where: 'filemd5 = ?',
@ -173,7 +172,25 @@ class LocalServiceRepository {
//Try if the file exists
var file = File(filePath);
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 {
//Delete all linked files
@ -184,6 +201,7 @@ class LocalServiceRepository {
filemd5
]
);
//TODO: maybe throw some error here?
return null;
}
}
@ -194,7 +212,9 @@ class LocalServiceRepository {
}) async {
//Write to file library
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 _database.insert(
'files',
@ -234,7 +254,7 @@ class LocalServiceRepository {
_userInfoChangeStreamController.add(userInfo);
}
Future<UserInfo?> fetchUserInfo({required userid}) async {
Future<UserInfo?> fetchUserInfoViaID({required int userid}) async {
var targetUser = await _database.query(
'users',
where: 'userid = ?',
@ -247,4 +267,8 @@ class LocalServiceRepository {
return UserInfo.fromJSONObject(jsonObject: targetUser[0]);
}
}
Future<UserInfo?> fetchUserInfoViaUsername({required String username}) async {
}
}

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @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
*/
@ -12,9 +12,8 @@ import 'package:equatable/equatable.dart';
class LocalFile extends Equatable {
final File file;
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
List<Object> get props => [filemd5];

View File

@ -1,7 +1,7 @@
/*
* @Author : Linloir
* @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
*/
@ -43,12 +43,12 @@ enum TCPRequestType {
abstract class TCPRequest {
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;
int get token => _token;
int? get token => _token;
Map<String, Object?> get body;
@ -56,7 +56,7 @@ abstract class TCPRequest {
return jsonEncode({
'request': type.value,
'body': body,
'token': token
'tokenid': token
});
}
@ -70,7 +70,7 @@ abstract class 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
Map<String, Object?> get body => {};
@ -81,7 +81,7 @@ class RegisterRequest extends TCPRequest {
RegisterRequest({
required UserIdentity identity,
required token
required int? token
}): _identity = identity, super(type: TCPRequestType.register, token: token);
@override
@ -93,7 +93,7 @@ class LoginRequest extends TCPRequest {
LoginRequest({
required UserIdentity identity,
required token
required int? token
}): _identity = identity, super(type: TCPRequestType.login, token: token);
@override
@ -101,14 +101,14 @@ class LoginRequest 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
Map<String, Object?> get body => {};
}
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
Map<String, Object?> get body => {};
@ -119,7 +119,7 @@ class ModifyPasswordRequest extends TCPRequest {
ModifyPasswordRequest({
required UserIdentity identity,
required token
required int? token
}): _identity = identity, super(type: TCPRequestType.modifyPassword, token: token);
@override
@ -131,7 +131,7 @@ class ModifyProfileRequest extends TCPRequest {
const ModifyProfileRequest ({
required UserInfo userInfo,
required int token
required int? token
}): _userinfo = userInfo, super(type: TCPRequestType.modifyProfile, token: token);
@override
@ -143,7 +143,7 @@ class SendMessageRequest extends TCPRequest {
SendMessageRequest({
required Message message,
required int token
required int? token
}):
_message = message,
super(type: TCPRequestType.sendMessage, token: token);
@ -170,7 +170,7 @@ class SendMessageRequest 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
Map<String, Object?> get body => {};
@ -179,7 +179,7 @@ class FetchMessageRequest extends TCPRequest {
class FindFileRequest extends TCPRequest {
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
Map<String, Object?> get body => {
@ -192,7 +192,7 @@ class FindFileRequest extends TCPRequest {
class FetchFileRequest extends TCPRequest {
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
Map<String, Object?> get body => {
@ -205,7 +205,7 @@ class FetchFileRequest extends TCPRequest {
class SearchUserRequest extends TCPRequest {
final String _username;
const SearchUserRequest({required String username, required int token}): _username = username, super(type: TCPRequestType.searchUser, token: token);
const SearchUserRequest({required String username, required int? token}): _username = username, super(type: TCPRequestType.searchUser, token: token);
@override
Map<String, Object?> get body => {
@ -218,7 +218,7 @@ class SearchUserRequest extends TCPRequest {
class AddContactRequest extends TCPRequest {
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
Map<String, Object?> get body => {
@ -229,7 +229,7 @@ class AddContactRequest 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
Map<String, Object?> get body => {};

View File

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

View File

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

View File

@ -2,7 +2,7 @@
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
dependency: "direct main"
description:
name: async
url: "https://pub.flutter-io.cn"
@ -156,6 +156,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
@ -380,6 +387,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:

View File

@ -46,6 +46,9 @@ dependencies:
git:
url: https://github.com/crazecoder/open_file
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.
# Use with the CupertinoIcons class for iOS style icons.