From 7b89d8ce144bc0a97f8fbb35b2ef12d2e76733ec Mon Sep 17 00:00:00 2001 From: Linloir <3145078758@qq.com> Date: Wed, 12 Oct 2022 18:25:17 +0800 Subject: [PATCH] More Codes - Splash page - Login page - Register page --- .gitignore | 2 + lib/home/home.dart | 6 - lib/home/home_page.dart | 19 +++ .../cubit/initialization_cubit.dart | 68 ++++++++ .../cubit/initialization_state.dart | 57 +++++++ lib/initialization/initialization_page.dart | 151 ++++++++++++++++++ lib/login/bloc/login_cubit.dart | 78 +++++++++ lib/login/bloc/login_state.dart | 43 +++++ lib/login/login_page.dart | 122 ++++++++++++++ lib/login/models/password.dart | 20 +++ lib/login/models/username.dart | 20 +++ lib/login/view/avatar_widget.dart | 6 + lib/login/view/login_form.dart | 129 +++++++++++++++ lib/main.dart | 131 +++++---------- lib/register/bloc/register_cubit.dart | 78 +++++++++ lib/register/bloc/register_state.dart | 43 +++++ lib/register/models/password.dart | 20 +++ lib/register/models/username.dart | 20 +++ lib/register/register_page.dart | 114 +++++++++++++ lib/register/view/register_form.dart | 129 +++++++++++++++ .../local_service_repository.dart | 38 ++++- .../models/local_file.dart | 5 +- .../tcp_repository/models/tcp_request.dart | 38 ++--- .../tcp_repository/models/tcp_response.dart | 41 +++-- .../tcp_repository/tcp_repository.dart | 87 +++++++--- pubspec.lock | 16 +- pubspec.yaml | 3 + 27 files changed, 1319 insertions(+), 165 deletions(-) delete mode 100644 lib/home/home.dart create mode 100644 lib/home/home_page.dart create mode 100644 lib/initialization/cubit/initialization_cubit.dart create mode 100644 lib/initialization/cubit/initialization_state.dart create mode 100644 lib/initialization/initialization_page.dart create mode 100644 lib/login/bloc/login_cubit.dart create mode 100644 lib/login/bloc/login_state.dart create mode 100644 lib/login/login_page.dart create mode 100644 lib/login/models/password.dart create mode 100644 lib/login/models/username.dart create mode 100644 lib/login/view/avatar_widget.dart create mode 100644 lib/login/view/login_form.dart create mode 100644 lib/register/bloc/register_cubit.dart create mode 100644 lib/register/bloc/register_state.dart create mode 100644 lib/register/models/password.dart create mode 100644 lib/register/models/username.dart create mode 100644 lib/register/register_page.dart create mode 100644 lib/register/view/register_form.dart diff --git a/.gitignore b/.gitignore index 24476c5..0ea06b8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +.tmp/ diff --git a/lib/home/home.dart b/lib/home/home.dart deleted file mode 100644 index fdaeaae..0000000 --- a/lib/home/home.dart +++ /dev/null @@ -1,6 +0,0 @@ -/* - * @Author : Linloir - * @Date : 2022-10-11 11:05:08 - * @LastEditTime : 2022-10-11 11:05:08 - * @Description : - */ diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart new file mode 100644 index 0000000..b64287d --- /dev/null +++ b/lib/home/home_page.dart @@ -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 route() => MaterialPageRoute(builder: (context) => const HomePage()); + + @override + Widget build(BuildContext context) { + return const Scaffold(); + } +} diff --git a/lib/initialization/cubit/initialization_cubit.dart b/lib/initialization/cubit/initialization_cubit.dart new file mode 100644 index 0000000..e78733c --- /dev/null +++ b/lib/initialization/cubit/initialization_cubit.dart @@ -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 { + 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()); +} \ No newline at end of file diff --git a/lib/initialization/cubit/initialization_state.dart b/lib/initialization/cubit/initialization_state.dart new file mode 100644 index 0000000..c65ad31 --- /dev/null +++ b/lib/initialization/cubit/initialization_state.dart @@ -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 get props => [databaseStatus, mainSocketStatus, tokenStatus]; +} diff --git a/lib/initialization/initialization_page.dart b/lib/initialization/initialization_page.dart new file mode 100644 index 0000000..65e3347 --- /dev/null +++ b/lib/initialization/initialization_page.dart @@ -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 route({ + required String serverAddress, + required int serverPort, + required String databasePath + }) { + return MaterialPageRoute(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( + 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( + 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( + 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...') + ], + ); + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/login/bloc/login_cubit.dart b/lib/login/bloc/login_cubit.dart new file mode 100644 index 0000000..86f6562 --- /dev/null +++ b/lib/login/bloc/login_cubit.dart @@ -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 { + 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 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 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; + } + } + } + } +} diff --git a/lib/login/bloc/login_state.dart b/lib/login/bloc/login_state.dart new file mode 100644 index 0000000..f8ba983 --- /dev/null +++ b/lib/login/bloc/login_state.dart @@ -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 get props => [status, username, password, avatar]; +} diff --git a/lib/login/login_page.dart b/lib/login/login_page.dart new file mode 100644 index 0000000..7ca0dd0 --- /dev/null +++ b/lib/login/login_page.dart @@ -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 route({ + required LocalServiceRepository localServiceRepository, + required TCPRepository tcpRepository + }) => MaterialPageRoute(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( + 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() + ], + ); + } +} + diff --git a/lib/login/models/password.dart b/lib/login/models/password.dart new file mode 100644 index 0000000..ef1cc73 --- /dev/null +++ b/lib/login/models/password.dart @@ -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 { + const Password.pure() : super.pure(''); + const Password.dirty([super.value = '']) : super.dirty(); + + @override + PasswordValidationError? validator(String? value) { + return value?.isNotEmpty == true ? null : PasswordValidationError.empty; + } +} diff --git a/lib/login/models/username.dart b/lib/login/models/username.dart new file mode 100644 index 0000000..09310b8 --- /dev/null +++ b/lib/login/models/username.dart @@ -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 { + const Username.pure(): super.pure(''); + const Username.dirty([super.value = '']): super.dirty(); + + @override + UsernameValidationError? validator(String? value) { + return value?.isNotEmpty == true ? null : UsernameValidationError.empty; + } +} diff --git a/lib/login/view/avatar_widget.dart b/lib/login/view/avatar_widget.dart new file mode 100644 index 0000000..b525b80 --- /dev/null +++ b/lib/login/view/avatar_widget.dart @@ -0,0 +1,6 @@ +/* + * @Author : Linloir + * @Date : 2022-10-12 15:37:19 + * @LastEditTime : 2022-10-12 17:10:58 + * @Description : + */ diff --git a/lib/login/view/login_form.dart b/lib/login/view/login_form.dart new file mode 100644 index 0000000..1626c88 --- /dev/null +++ b/lib/login/view/login_form.dart @@ -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( + buildWhen: (previous, current) => previous.username != current.username, + builder: (context, state) { + return TextField( + onChanged: (username) { + context.read().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( + buildWhen: (previous, current) => previous.password != current.password, + builder: (context, state) { + return TextField( + onChanged: (password) { + context.read().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( + 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().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 + ), + ) + ) + ); + }, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 4ae32a1..d9784b5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - 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: [ - 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( + create: (context) { + return InitializationCubit( + serverAddress: '127.0.0.1', + serverPort: 20706 + ); + }, + child: BlocListener( + 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(), + ) ); } -} +} \ No newline at end of file diff --git a/lib/register/bloc/register_cubit.dart b/lib/register/bloc/register_cubit.dart new file mode 100644 index 0000000..f5e7a1e --- /dev/null +++ b/lib/register/bloc/register_cubit.dart @@ -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 { + 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 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 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; + } + } + } + } +} diff --git a/lib/register/bloc/register_state.dart b/lib/register/bloc/register_state.dart new file mode 100644 index 0000000..5783f0e --- /dev/null +++ b/lib/register/bloc/register_state.dart @@ -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 get props => [status, username, password, avatar]; +} diff --git a/lib/register/models/password.dart b/lib/register/models/password.dart new file mode 100644 index 0000000..ef1cc73 --- /dev/null +++ b/lib/register/models/password.dart @@ -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 { + const Password.pure() : super.pure(''); + const Password.dirty([super.value = '']) : super.dirty(); + + @override + PasswordValidationError? validator(String? value) { + return value?.isNotEmpty == true ? null : PasswordValidationError.empty; + } +} diff --git a/lib/register/models/username.dart b/lib/register/models/username.dart new file mode 100644 index 0000000..09310b8 --- /dev/null +++ b/lib/register/models/username.dart @@ -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 { + const Username.pure(): super.pure(''); + const Username.dirty([super.value = '']): super.dirty(); + + @override + UsernameValidationError? validator(String? value) { + return value?.isNotEmpty == true ? null : UsernameValidationError.empty; + } +} diff --git a/lib/register/register_page.dart b/lib/register/register_page.dart new file mode 100644 index 0000000..8f7faff --- /dev/null +++ b/lib/register/register_page.dart @@ -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 route({ + required LocalServiceRepository localServiceRepository, + required TCPRepository tcpRepository + }) => MaterialPageRoute(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( + 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() + ], + ); + } +} + diff --git a/lib/register/view/register_form.dart b/lib/register/view/register_form.dart new file mode 100644 index 0000000..b3515a6 --- /dev/null +++ b/lib/register/view/register_form.dart @@ -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( + buildWhen: (previous, current) => previous.username != current.username, + builder: (context, state) { + return TextField( + onChanged: (username) { + context.read().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( + buildWhen: (previous, current) => previous.password != current.password, + builder: (context, state) { + return TextField( + onChanged: (password) { + context.read().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( + 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().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 + ), + ) + ) + ); + }, + ); + } +} diff --git a/lib/repositories/local_service_repository/local_service_repository.dart b/lib/repositories/local_service_repository/local_service_repository.dart index a6f84ca..783b869 100644 --- a/lib/repositories/local_service_repository/local_service_repository.dart +++ b/lib/repositories/local_service_repository/local_service_repository.dart @@ -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 findFile({required String filemd5}) async { + Future 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 fetchUserInfo({required userid}) async { + Future 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 fetchUserInfoViaUsername({required String username}) async { + + } } diff --git a/lib/repositories/local_service_repository/models/local_file.dart b/lib/repositories/local_service_repository/models/local_file.dart index 7df6edb..f5938b6 100644 --- a/lib/repositories/local_service_repository/models/local_file.dart +++ b/lib/repositories/local_service_repository/models/local_file.dart @@ -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 get props => [filemd5]; diff --git a/lib/repositories/tcp_repository/models/tcp_request.dart b/lib/repositories/tcp_repository/models/tcp_request.dart index 6ce522a..e450c19 100644 --- a/lib/repositories/tcp_repository/models/tcp_request.dart +++ b/lib/repositories/tcp_repository/models/tcp_request.dart @@ -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 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 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 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 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 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 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 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 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 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 get body => {}; diff --git a/lib/repositories/tcp_repository/models/tcp_response.dart b/lib/repositories/tcp_repository/models/tcp_response.dart index 9d92c0e..00eec44 100644 --- a/lib/repositories/tcp_repository/models/tcp_response.dart +++ b/lib/repositories/tcp_repository/models/tcp_response.dart @@ -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 jsonObject }): super(jsonObject: jsonObject) { - _userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); + _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); } - UserInfo get userInfo => _userInfo; + UserInfo? get userInfo => _userInfo; } class RegisterResponse extends TCPResponse { - late final UserInfo _userInfo; + late final UserInfo? _userInfo; RegisterResponse({ required Map jsonObject }): super(jsonObject: jsonObject) { - _userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); + _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); } - UserInfo get userInfo => _userInfo; + UserInfo? get userInfo => _userInfo; } class LoginResponse extends TCPResponse { - late final UserInfo _userInfo; + late final UserInfo? _userInfo; LoginResponse({ required Map jsonObject }): super(jsonObject: jsonObject) { - _userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); + _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); } - 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 jsonObject }): super(jsonObject: jsonObject) { - _userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); + _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); } - 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 jsonObject }): super(jsonObject: jsonObject) { - _userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); + _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); } - 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)['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 jsonObject }): super(jsonObject: jsonObject) { - _userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); + _userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map); } - UserInfo get userInfo => _userInfo; + UserInfo? get userInfo => _userInfo; } class AddContactResponse extends TCPResponse { diff --git a/lib/repositories/tcp_repository/tcp_repository.dart b/lib/repositories/tcp_repository/tcp_repository.dart index c73a09f..b238619 100644 --- a/lib/repositories/tcp_repository/tcp_repository.dart +++ b/lib/repositories/tcp_repository/tcp_repository.dart @@ -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(() => !_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 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 _requestStreamController = StreamController(); - Future 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 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(); } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 5140352..d8ed5f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index d9c52a2..92634ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.