mirror of
https://github.com/Linloir/Simple-TCP-Client.git
synced 2025-12-17 00:38:11 +08:00
More Codes
- Splash page - Login page - Register page
This commit is contained in:
parent
d3a5a32fdb
commit
7b89d8ce14
2
.gitignore
vendored
2
.gitignore
vendored
@ -42,3 +42,5 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
.tmp/
|
||||||
|
|||||||
@ -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
19
lib/home/home_page.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
68
lib/initialization/cubit/initialization_cubit.dart
Normal file
68
lib/initialization/cubit/initialization_cubit.dart
Normal 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());
|
||||||
|
}
|
||||||
57
lib/initialization/cubit/initialization_state.dart
Normal file
57
lib/initialization/cubit/initialization_state.dart
Normal 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];
|
||||||
|
}
|
||||||
151
lib/initialization/initialization_page.dart
Normal file
151
lib/initialization/initialization_page.dart
Normal 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...')
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
lib/login/bloc/login_cubit.dart
Normal file
78
lib/login/bloc/login_cubit.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
lib/login/bloc/login_state.dart
Normal file
43
lib/login/bloc/login_state.dart
Normal 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
122
lib/login/login_page.dart
Normal 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()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
20
lib/login/models/password.dart
Normal file
20
lib/login/models/password.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/login/models/username.dart
Normal file
20
lib/login/models/username.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
lib/login/view/avatar_widget.dart
Normal file
6
lib/login/view/avatar_widget.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
* @Author : Linloir
|
||||||
|
* @Date : 2022-10-12 15:37:19
|
||||||
|
* @LastEditTime : 2022-10-12 17:10:58
|
||||||
|
* @Description :
|
||||||
|
*/
|
||||||
129
lib/login/view/login_form.dart
Normal file
129
lib/login/view/login_form.dart
Normal 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
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
131
lib/main.dart
131
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/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
|
import 'package:tcp_client/home/home_page.dart';
|
||||||
|
import 'package:tcp_client/initialization/cubit/initialization_cubit.dart';
|
||||||
|
import 'package:tcp_client/initialization/cubit/initialization_state.dart';
|
||||||
|
import 'package:tcp_client/initialization/initialization_page.dart';
|
||||||
|
import 'package:tcp_client/login/login_page.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
sqfliteFfiInit();
|
sqfliteFfiInit();
|
||||||
@ -15,103 +28,43 @@ class MyApp extends StatelessWidget {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Flutter Demo',
|
title: 'Flutter Demo',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
// This is the theme of your application.
|
|
||||||
//
|
|
||||||
// Try running your application with "flutter run". You'll see the
|
|
||||||
// application has a blue toolbar. Then, without quitting the app, try
|
|
||||||
// changing the primarySwatch below to Colors.green and then invoke
|
|
||||||
// "hot reload" (press "r" in the console where you ran "flutter run",
|
|
||||||
// or simply save your changes to "hot reload" in a Flutter IDE).
|
|
||||||
// Notice that the counter didn't reset back to zero; the application
|
|
||||||
// is not restarted.
|
|
||||||
primarySwatch: Colors.blue,
|
primarySwatch: Colors.blue,
|
||||||
),
|
),
|
||||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
home: const SplashPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget {
|
class SplashPage extends StatelessWidget {
|
||||||
const MyHomePage({super.key, required this.title});
|
const SplashPage({super.key});
|
||||||
|
|
||||||
// This widget is the home page of your application. It is stateful, meaning
|
|
||||||
// that it has a State object (defined below) that contains fields that affect
|
|
||||||
// how it looks.
|
|
||||||
|
|
||||||
// This class is the configuration for the state. It holds the values (in this
|
|
||||||
// case the title) provided by the parent (in this case the App widget) and
|
|
||||||
// used by the build method of the State. Fields in a Widget subclass are
|
|
||||||
// always marked "final".
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<MyHomePage> createState() => _MyHomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
|
||||||
int _counter = 0;
|
|
||||||
|
|
||||||
void _incrementCounter() {
|
|
||||||
setState(() {
|
|
||||||
// This call to setState tells the Flutter framework that something has
|
|
||||||
// changed in this State, which causes it to rerun the build method below
|
|
||||||
// so that the display can reflect the updated values. If we changed
|
|
||||||
// _counter without calling setState(), then the build method would not be
|
|
||||||
// called again, and so nothing would appear to happen.
|
|
||||||
_counter++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// This method is rerun every time setState is called, for instance as done
|
return BlocProvider<InitializationCubit>(
|
||||||
// by the _incrementCounter method above.
|
create: (context) {
|
||||||
//
|
return InitializationCubit(
|
||||||
// The Flutter framework has been optimized to make rerunning build methods
|
serverAddress: '127.0.0.1',
|
||||||
// fast, so that you can just rebuild anything that needs updating rather
|
serverPort: 20706
|
||||||
// than having to individually change instances of widgets.
|
);
|
||||||
return Scaffold(
|
},
|
||||||
appBar: AppBar(
|
child: BlocListener<InitializationCubit, InitializationState>(
|
||||||
// Here we take the value from the MyHomePage object that was created by
|
listener: (context, state) {
|
||||||
// the App.build method, and use it to set our appbar title.
|
if(state.isDone) {
|
||||||
title: Text(widget.title),
|
Future.delayed(const Duration(seconds: 1)).then((_) async {
|
||||||
),
|
if((await SharedPreferences.getInstance()).getInt('userid') != null) {
|
||||||
body: Center(
|
Navigator.of(context).pushReplacement(HomePage.route());
|
||||||
// Center is a layout widget. It takes a single child and positions it
|
}
|
||||||
// in the middle of the parent.
|
else {
|
||||||
child: Column(
|
Navigator.of(context).pushReplacement(LoginPage.route(
|
||||||
// Column is also a layout widget. It takes a list of children and
|
localServiceRepository: state.localServiceRepository!,
|
||||||
// arranges them vertically. By default, it sizes itself to fit its
|
tcpRepository: state.tcpRepository!
|
||||||
// children horizontally, and tries to be as tall as its parent.
|
));
|
||||||
//
|
}
|
||||||
// Invoke "debug painting" (press "p" in the console, choose the
|
});
|
||||||
// "Toggle Debug Paint" action from the Flutter Inspector in Android
|
}
|
||||||
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
|
},
|
||||||
// to see the wireframe for each widget.
|
child: const InitializePage(),
|
||||||
//
|
)
|
||||||
// Column has various properties to control how it sizes itself and
|
|
||||||
// how it positions its children. Here we use mainAxisAlignment to
|
|
||||||
// center the children vertically; the main axis here is the vertical
|
|
||||||
// axis because Columns are vertical (the cross axis would be
|
|
||||||
// horizontal).
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
const Text(
|
|
||||||
'You have pushed the button this many times:',
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'$_counter',
|
|
||||||
style: Theme.of(context).textTheme.headline4,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: _incrementCounter,
|
|
||||||
tooltip: 'Increment',
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
78
lib/register/bloc/register_cubit.dart
Normal file
78
lib/register/bloc/register_cubit.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
lib/register/bloc/register_state.dart
Normal file
43
lib/register/bloc/register_state.dart
Normal 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];
|
||||||
|
}
|
||||||
20
lib/register/models/password.dart
Normal file
20
lib/register/models/password.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/register/models/username.dart
Normal file
20
lib/register/models/username.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
lib/register/register_page.dart
Normal file
114
lib/register/register_page.dart
Normal 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()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
129
lib/register/view/register_form.dart
Normal file
129
lib/register/view/register_form.dart
Normal 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
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 10:56:02
|
* @Date : 2022-10-11 10:56:02
|
||||||
* @LastEditTime : 2022-10-11 23:49:16
|
* @LastEditTime : 2022-10-12 15:35:30
|
||||||
* @Description : Local Service Repository
|
* @Description : Local Service Repository
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -74,8 +74,7 @@ class LocalServiceRepository {
|
|||||||
var file = File(filePickResult.files.single.path!);
|
var file = File(filePickResult.files.single.path!);
|
||||||
return LocalFile(
|
return LocalFile(
|
||||||
file: file,
|
file: file,
|
||||||
filemd5: md5.convert(await file.readAsBytes()).toString(),
|
filemd5: md5.convert(await file.readAsBytes()).toString()
|
||||||
ext: file.path.substring(file.path.lastIndexOf('.'))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +156,7 @@ class LocalServiceRepository {
|
|||||||
return queryResult.map((e) => Message.fromJSONObject(jsonObject: e)).toList();
|
return queryResult.map((e) => Message.fromJSONObject(jsonObject: e)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File?> findFile({required String filemd5}) async {
|
Future<File?> findFile({required String filemd5, required String fileName}) async {
|
||||||
var directory = await _database.query(
|
var directory = await _database.query(
|
||||||
'files',
|
'files',
|
||||||
where: 'filemd5 = ?',
|
where: 'filemd5 = ?',
|
||||||
@ -173,7 +172,25 @@ class LocalServiceRepository {
|
|||||||
//Try if the file exists
|
//Try if the file exists
|
||||||
var file = File(filePath);
|
var file = File(filePath);
|
||||||
if(await file.exists()) {
|
if(await file.exists()) {
|
||||||
return file;
|
//Copy to desired file path
|
||||||
|
var pref = await SharedPreferences.getInstance();
|
||||||
|
var userID = pref.getInt('userid');
|
||||||
|
var documentPath = (await getApplicationDocumentsDirectory()).path;
|
||||||
|
var fileBaseName = fileName.substring(0, fileName.lastIndexOf('.'));
|
||||||
|
var fileExt = fileName.substring(fileName.lastIndexOf('.'));
|
||||||
|
var duplicate = 0;
|
||||||
|
//Rename target file
|
||||||
|
await Directory('$documentPath/files').create();
|
||||||
|
await Directory('$documentPath/files/$userID').create();
|
||||||
|
var targetFilePath = '$documentPath/files/$userID/$fileBaseName$fileExt';
|
||||||
|
var targetFile = File(targetFilePath);
|
||||||
|
while(await targetFile.exists()) {
|
||||||
|
duplicate += 1;
|
||||||
|
targetFilePath = '$documentPath/files/$userID/$fileBaseName($duplicate)$fileExt';
|
||||||
|
targetFile = File(targetFilePath);
|
||||||
|
}
|
||||||
|
targetFile = await file.copy(targetFilePath);
|
||||||
|
return targetFile;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//Delete all linked files
|
//Delete all linked files
|
||||||
@ -184,6 +201,7 @@ class LocalServiceRepository {
|
|||||||
filemd5
|
filemd5
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
//TODO: maybe throw some error here?
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,7 +212,9 @@ class LocalServiceRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
//Write to file library
|
//Write to file library
|
||||||
var documentPath = (await getApplicationDocumentsDirectory()).path;
|
var documentPath = (await getApplicationDocumentsDirectory()).path;
|
||||||
var permanentFilePath = '$documentPath/files/${tempFile.filemd5}${tempFile.ext}';
|
await Directory('$documentPath/files').create();
|
||||||
|
await Directory('$documentPath/files/.lib').create();
|
||||||
|
var permanentFilePath = '$documentPath/files/.lib/${tempFile.filemd5}';
|
||||||
await tempFile.file.copy(permanentFilePath);
|
await tempFile.file.copy(permanentFilePath);
|
||||||
await _database.insert(
|
await _database.insert(
|
||||||
'files',
|
'files',
|
||||||
@ -234,7 +254,7 @@ class LocalServiceRepository {
|
|||||||
_userInfoChangeStreamController.add(userInfo);
|
_userInfoChangeStreamController.add(userInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UserInfo?> fetchUserInfo({required userid}) async {
|
Future<UserInfo?> fetchUserInfoViaID({required int userid}) async {
|
||||||
var targetUser = await _database.query(
|
var targetUser = await _database.query(
|
||||||
'users',
|
'users',
|
||||||
where: 'userid = ?',
|
where: 'userid = ?',
|
||||||
@ -247,4 +267,8 @@ class LocalServiceRepository {
|
|||||||
return UserInfo.fromJSONObject(jsonObject: targetUser[0]);
|
return UserInfo.fromJSONObject(jsonObject: targetUser[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<UserInfo?> fetchUserInfoViaUsername({required String username}) async {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 10:55:36
|
* @Date : 2022-10-11 10:55:36
|
||||||
* @LastEditTime : 2022-10-11 22:54:59
|
* @LastEditTime : 2022-10-12 09:19:50
|
||||||
* @Description : Local File Model
|
* @Description : Local File Model
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -12,9 +12,8 @@ import 'package:equatable/equatable.dart';
|
|||||||
class LocalFile extends Equatable {
|
class LocalFile extends Equatable {
|
||||||
final File file;
|
final File file;
|
||||||
final String filemd5;
|
final String filemd5;
|
||||||
final String ext;
|
|
||||||
|
|
||||||
const LocalFile({required this.file, required this.filemd5, required this.ext});
|
const LocalFile({required this.file, required this.filemd5});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [filemd5];
|
List<Object> get props => [filemd5];
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 09:44:03
|
* @Date : 2022-10-11 09:44:03
|
||||||
* @LastEditTime : 2022-10-11 16:55:08
|
* @LastEditTime : 2022-10-12 13:56:56
|
||||||
* @Description : Abstract TCP request class
|
* @Description : Abstract TCP request class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -43,12 +43,12 @@ enum TCPRequestType {
|
|||||||
|
|
||||||
abstract class TCPRequest {
|
abstract class TCPRequest {
|
||||||
final TCPRequestType _type;
|
final TCPRequestType _type;
|
||||||
final int _token;
|
final int? _token;
|
||||||
|
|
||||||
const TCPRequest({required TCPRequestType type, required int token}): _type = type, _token = token;
|
const TCPRequest({required TCPRequestType type, required int? token}): _type = type, _token = token;
|
||||||
|
|
||||||
TCPRequestType get type => _type;
|
TCPRequestType get type => _type;
|
||||||
int get token => _token;
|
int? get token => _token;
|
||||||
|
|
||||||
Map<String, Object?> get body;
|
Map<String, Object?> get body;
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ abstract class TCPRequest {
|
|||||||
return jsonEncode({
|
return jsonEncode({
|
||||||
'request': type.value,
|
'request': type.value,
|
||||||
'body': body,
|
'body': body,
|
||||||
'token': token
|
'tokenid': token
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ abstract class TCPRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CheckStateRequest extends TCPRequest {
|
class CheckStateRequest extends TCPRequest {
|
||||||
const CheckStateRequest({required int token}): super(type: TCPRequestType.checkState, token: token);
|
const CheckStateRequest({required int? token}): super(type: TCPRequestType.checkState, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> get body => {};
|
Map<String, Object?> get body => {};
|
||||||
@ -81,7 +81,7 @@ class RegisterRequest extends TCPRequest {
|
|||||||
|
|
||||||
RegisterRequest({
|
RegisterRequest({
|
||||||
required UserIdentity identity,
|
required UserIdentity identity,
|
||||||
required token
|
required int? token
|
||||||
}): _identity = identity, super(type: TCPRequestType.register, token: token);
|
}): _identity = identity, super(type: TCPRequestType.register, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -93,7 +93,7 @@ class LoginRequest extends TCPRequest {
|
|||||||
|
|
||||||
LoginRequest({
|
LoginRequest({
|
||||||
required UserIdentity identity,
|
required UserIdentity identity,
|
||||||
required token
|
required int? token
|
||||||
}): _identity = identity, super(type: TCPRequestType.login, token: token);
|
}): _identity = identity, super(type: TCPRequestType.login, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -101,14 +101,14 @@ class LoginRequest extends TCPRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LogoutRequest extends TCPRequest {
|
class LogoutRequest extends TCPRequest {
|
||||||
const LogoutRequest({required int token}): super(type: TCPRequestType.logout, token: token);
|
const LogoutRequest({required int? token}): super(type: TCPRequestType.logout, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> get body => {};
|
Map<String, Object?> get body => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
class GetProfileRequest extends TCPRequest {
|
class GetProfileRequest extends TCPRequest {
|
||||||
const GetProfileRequest({required int token}): super(type: TCPRequestType.profile, token: token);
|
const GetProfileRequest({required int? token}): super(type: TCPRequestType.profile, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> get body => {};
|
Map<String, Object?> get body => {};
|
||||||
@ -119,7 +119,7 @@ class ModifyPasswordRequest extends TCPRequest {
|
|||||||
|
|
||||||
ModifyPasswordRequest({
|
ModifyPasswordRequest({
|
||||||
required UserIdentity identity,
|
required UserIdentity identity,
|
||||||
required token
|
required int? token
|
||||||
}): _identity = identity, super(type: TCPRequestType.modifyPassword, token: token);
|
}): _identity = identity, super(type: TCPRequestType.modifyPassword, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -131,7 +131,7 @@ class ModifyProfileRequest extends TCPRequest {
|
|||||||
|
|
||||||
const ModifyProfileRequest ({
|
const ModifyProfileRequest ({
|
||||||
required UserInfo userInfo,
|
required UserInfo userInfo,
|
||||||
required int token
|
required int? token
|
||||||
}): _userinfo = userInfo, super(type: TCPRequestType.modifyProfile, token: token);
|
}): _userinfo = userInfo, super(type: TCPRequestType.modifyProfile, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -143,7 +143,7 @@ class SendMessageRequest extends TCPRequest {
|
|||||||
|
|
||||||
SendMessageRequest({
|
SendMessageRequest({
|
||||||
required Message message,
|
required Message message,
|
||||||
required int token
|
required int? token
|
||||||
}):
|
}):
|
||||||
_message = message,
|
_message = message,
|
||||||
super(type: TCPRequestType.sendMessage, token: token);
|
super(type: TCPRequestType.sendMessage, token: token);
|
||||||
@ -170,7 +170,7 @@ class SendMessageRequest extends TCPRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FetchMessageRequest extends TCPRequest {
|
class FetchMessageRequest extends TCPRequest {
|
||||||
const FetchMessageRequest({required int token}): super(type: TCPRequestType.fetchMessage, token: token);
|
const FetchMessageRequest({required int? token}): super(type: TCPRequestType.fetchMessage, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> get body => {};
|
Map<String, Object?> get body => {};
|
||||||
@ -179,7 +179,7 @@ class FetchMessageRequest extends TCPRequest {
|
|||||||
class FindFileRequest extends TCPRequest {
|
class FindFileRequest extends TCPRequest {
|
||||||
final LocalFile _file;
|
final LocalFile _file;
|
||||||
|
|
||||||
const FindFileRequest({required LocalFile file, required int token}): _file = file, super(type: TCPRequestType.findFile, token: token);
|
const FindFileRequest({required LocalFile file, required int? token}): _file = file, super(type: TCPRequestType.findFile, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> get body => {
|
Map<String, Object?> get body => {
|
||||||
@ -192,7 +192,7 @@ class FindFileRequest extends TCPRequest {
|
|||||||
class FetchFileRequest extends TCPRequest {
|
class FetchFileRequest extends TCPRequest {
|
||||||
final String _msgmd5;
|
final String _msgmd5;
|
||||||
|
|
||||||
const FetchFileRequest({required String msgmd5, required int token}): _msgmd5 = msgmd5, super(type: TCPRequestType.fetchFile, token: token);
|
const FetchFileRequest({required String msgmd5, required int? token}): _msgmd5 = msgmd5, super(type: TCPRequestType.fetchFile, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> get body => {
|
Map<String, Object?> get body => {
|
||||||
@ -205,7 +205,7 @@ class FetchFileRequest extends TCPRequest {
|
|||||||
class SearchUserRequest extends TCPRequest {
|
class SearchUserRequest extends TCPRequest {
|
||||||
final String _username;
|
final String _username;
|
||||||
|
|
||||||
const SearchUserRequest({required String username, required int token}): _username = username, super(type: TCPRequestType.searchUser, token: token);
|
const SearchUserRequest({required String username, required int? token}): _username = username, super(type: TCPRequestType.searchUser, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> get body => {
|
Map<String, Object?> get body => {
|
||||||
@ -218,7 +218,7 @@ class SearchUserRequest extends TCPRequest {
|
|||||||
class AddContactRequest extends TCPRequest {
|
class AddContactRequest extends TCPRequest {
|
||||||
final int _userid;
|
final int _userid;
|
||||||
|
|
||||||
const AddContactRequest({required int userid, required int token}): _userid = userid, super(type: TCPRequestType.addContact, token: token);
|
const AddContactRequest({required int userid, required int? token}): _userid = userid, super(type: TCPRequestType.addContact, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> get body => {
|
Map<String, Object?> get body => {
|
||||||
@ -229,7 +229,7 @@ class AddContactRequest extends TCPRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FetchContactRequest extends TCPRequest {
|
class FetchContactRequest extends TCPRequest {
|
||||||
const FetchContactRequest({required int token}): super(type: TCPRequestType.fetchContact, token: token);
|
const FetchContactRequest({required int? token}): super(type: TCPRequestType.fetchContact, token: token);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, Object?> get body => {};
|
Map<String, Object?> get body => {};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 11:02:19
|
* @Date : 2022-10-11 11:02:19
|
||||||
* @LastEditTime : 2022-10-11 22:55:48
|
* @LastEditTime : 2022-10-12 13:25:49
|
||||||
* @Description :
|
* @Description :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -84,39 +84,39 @@ class SetTokenReponse extends TCPResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CheckStateResponse extends TCPResponse {
|
class CheckStateResponse extends TCPResponse {
|
||||||
late final UserInfo _userInfo;
|
late final UserInfo? _userInfo;
|
||||||
|
|
||||||
CheckStateResponse({
|
CheckStateResponse({
|
||||||
required Map<String, Object?> jsonObject
|
required Map<String, Object?> jsonObject
|
||||||
}): super(jsonObject: jsonObject) {
|
}): super(jsonObject: jsonObject) {
|
||||||
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
_userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserInfo get userInfo => _userInfo;
|
UserInfo? get userInfo => _userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RegisterResponse extends TCPResponse {
|
class RegisterResponse extends TCPResponse {
|
||||||
late final UserInfo _userInfo;
|
late final UserInfo? _userInfo;
|
||||||
|
|
||||||
RegisterResponse({
|
RegisterResponse({
|
||||||
required Map<String, Object?> jsonObject
|
required Map<String, Object?> jsonObject
|
||||||
}): super(jsonObject: jsonObject) {
|
}): super(jsonObject: jsonObject) {
|
||||||
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
_userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserInfo get userInfo => _userInfo;
|
UserInfo? get userInfo => _userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoginResponse extends TCPResponse {
|
class LoginResponse extends TCPResponse {
|
||||||
late final UserInfo _userInfo;
|
late final UserInfo? _userInfo;
|
||||||
|
|
||||||
LoginResponse({
|
LoginResponse({
|
||||||
required Map<String, Object?> jsonObject
|
required Map<String, Object?> jsonObject
|
||||||
}): super(jsonObject: jsonObject) {
|
}): super(jsonObject: jsonObject) {
|
||||||
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
_userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserInfo get userInfo => _userInfo;
|
UserInfo? get userInfo => _userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogoutResponse extends TCPResponse {
|
class LogoutResponse extends TCPResponse {
|
||||||
@ -126,15 +126,15 @@ class LogoutResponse extends TCPResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GetProfileResponse extends TCPResponse {
|
class GetProfileResponse extends TCPResponse {
|
||||||
late final UserInfo _userInfo;
|
late final UserInfo? _userInfo;
|
||||||
|
|
||||||
GetProfileResponse({
|
GetProfileResponse({
|
||||||
required Map<String, Object?> jsonObject
|
required Map<String, Object?> jsonObject
|
||||||
}): super(jsonObject: jsonObject) {
|
}): super(jsonObject: jsonObject) {
|
||||||
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
_userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserInfo get userInfo => _userInfo;
|
UserInfo? get userInfo => _userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModifyPasswordResponse extends TCPResponse {
|
class ModifyPasswordResponse extends TCPResponse {
|
||||||
@ -144,15 +144,15 @@ class ModifyPasswordResponse extends TCPResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ModifyProfileResponse extends TCPResponse {
|
class ModifyProfileResponse extends TCPResponse {
|
||||||
late final UserInfo _userInfo;
|
late final UserInfo? _userInfo;
|
||||||
|
|
||||||
ModifyProfileResponse({
|
ModifyProfileResponse({
|
||||||
required Map<String, Object?> jsonObject
|
required Map<String, Object?> jsonObject
|
||||||
}): super(jsonObject: jsonObject) {
|
}): super(jsonObject: jsonObject) {
|
||||||
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
_userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserInfo get userInfo => _userInfo;
|
UserInfo? get userInfo => _userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SendMessageResponse extends TCPResponse {
|
class SendMessageResponse extends TCPResponse {
|
||||||
@ -203,8 +203,7 @@ class FetchFileResponse extends TCPResponse {
|
|||||||
}): super(jsonObject: jsonObject) {
|
}): super(jsonObject: jsonObject) {
|
||||||
_payload = LocalFile(
|
_payload = LocalFile(
|
||||||
file: payload.file,
|
file: payload.file,
|
||||||
filemd5: payload.filemd5,
|
filemd5: payload.filemd5
|
||||||
ext: (jsonObject['body'] as Map<String, Object?>)['ext'] as String
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,15 +211,15 @@ class FetchFileResponse extends TCPResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SearchUserResponse extends TCPResponse {
|
class SearchUserResponse extends TCPResponse {
|
||||||
late final UserInfo _userInfo;
|
late final UserInfo? _userInfo;
|
||||||
|
|
||||||
SearchUserResponse({
|
SearchUserResponse({
|
||||||
required Map<String, Object?> jsonObject
|
required Map<String, Object?> jsonObject
|
||||||
}): super(jsonObject: jsonObject) {
|
}): super(jsonObject: jsonObject) {
|
||||||
_userInfo = UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
_userInfo = jsonObject['body'] == null ? null : UserInfo.fromJSONObject(jsonObject: jsonObject['body'] as Map<String, Object?>);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserInfo get userInfo => _userInfo;
|
UserInfo? get userInfo => _userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AddContactResponse extends TCPResponse {
|
class AddContactResponse extends TCPResponse {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author : Linloir
|
* @Author : Linloir
|
||||||
* @Date : 2022-10-11 09:42:05
|
* @Date : 2022-10-11 09:42:05
|
||||||
* @LastEditTime : 2022-10-11 22:55:28
|
* @LastEditTime : 2022-10-12 16:57:50
|
||||||
* @Description : TCP repository
|
* @Description : TCP repository
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -9,15 +9,17 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:tcp_client/repositories/common_models/message.dart';
|
||||||
import 'package:tcp_client/repositories/local_service_repository/models/local_file.dart';
|
import 'package:tcp_client/repositories/local_service_repository/models/local_file.dart';
|
||||||
import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart';
|
import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart';
|
||||||
import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart';
|
import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart';
|
||||||
|
|
||||||
class TCPRepository {
|
class TCPRepository {
|
||||||
TCPRepository({
|
TCPRepository._internal({
|
||||||
required Socket socket,
|
required Socket socket,
|
||||||
required String remoteAddress,
|
required String remoteAddress,
|
||||||
required int remotePort
|
required int remotePort
|
||||||
@ -30,13 +32,30 @@ class TCPRepository {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
Future(() async {
|
Future(() async {
|
||||||
await for(var response in _responseRawStreamController.stream) {
|
var responseQueue = StreamQueue(_responseRawStreamController.stream);
|
||||||
var payloadFile = await _payloadRawStreamController.stream.single;
|
var payloadQueue = StreamQueue(_payloadRawStreamController.stream);
|
||||||
await _pushResponse(responseBytes: response, tempFile: payloadFile);
|
while(await Future<bool>(() => !_responseRawStreamController.isClosed && !_payloadRawStreamController.isClosed)) {
|
||||||
|
var response = await responseQueue.next;
|
||||||
|
var payload = await payloadQueue.next;
|
||||||
|
await _pushResponse(responseBytes: response, tempFile: payload);
|
||||||
}
|
}
|
||||||
|
responseQueue.cancel();
|
||||||
|
payloadQueue.cancel();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<TCPRepository> create({
|
||||||
|
required String serverAddress,
|
||||||
|
required int serverPort
|
||||||
|
}) async {
|
||||||
|
var socket = await Socket.connect(serverAddress, serverPort);
|
||||||
|
return TCPRepository._internal(
|
||||||
|
socket: socket,
|
||||||
|
remoteAddress: serverAddress,
|
||||||
|
remotePort: serverPort
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final Socket _socket;
|
final Socket _socket;
|
||||||
final String _remoteAddress;
|
final String _remoteAddress;
|
||||||
final int _remotePort;
|
final int _remotePort;
|
||||||
@ -67,8 +86,32 @@ class TCPRepository {
|
|||||||
//Provide a request stream for widgets to push to
|
//Provide a request stream for widgets to push to
|
||||||
final StreamController<TCPRequest> _requestStreamController = StreamController();
|
final StreamController<TCPRequest> _requestStreamController = StreamController();
|
||||||
|
|
||||||
Future<void> pushRequest(TCPRequest request) async {
|
void pushRequest(TCPRequest request) {
|
||||||
_requestStreamController.add(request);
|
if(request.type == TCPRequestType.sendMessage) {
|
||||||
|
request as SendMessageRequest;
|
||||||
|
if(request.message.type == MessageType.file) {
|
||||||
|
Future(() async {
|
||||||
|
//Duplicate current socket
|
||||||
|
Socket socket = await Socket.connect(_remoteAddress, _remotePort);
|
||||||
|
TCPRepository duplicatedRepository = TCPRepository._internal(
|
||||||
|
socket: socket,
|
||||||
|
remoteAddress: _remoteAddress,
|
||||||
|
remotePort: _remotePort
|
||||||
|
);
|
||||||
|
duplicatedRepository._requestStreamController.add(request);
|
||||||
|
await for(var response in duplicatedRepository.responseStream) {
|
||||||
|
if(response.type == TCPResponseType.sendMessage) {
|
||||||
|
_responseStreamController.add(response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
duplicatedRepository.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_requestStreamController.add(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Listen to the incoming stream and emits event whenever there is a intact request
|
//Listen to the incoming stream and emits event whenever there is a intact request
|
||||||
@ -86,16 +129,20 @@ class TCPRepository {
|
|||||||
//Clear the length indicator bytes
|
//Clear the length indicator bytes
|
||||||
buffer.removeRange(0, 8);
|
buffer.removeRange(0, 8);
|
||||||
//Create temp file to read payload (might be huge)
|
//Create temp file to read payload (might be huge)
|
||||||
var tempFile = File('${Directory.current.path}/.tmp/${DateTime.now().microsecondsSinceEpoch}')..createSync();
|
Directory('${Directory.current.path}/.tmp').createSync();
|
||||||
//Create a pull stream for payload file
|
//Create a pull stream for payload file
|
||||||
_payloadPullStreamController = StreamController();
|
_payloadPullStreamController = StreamController();
|
||||||
//Create a future that listens to the status of the payload transmission
|
//Create a future that listens to the status of the payload transmission
|
||||||
Future(() async {
|
() {
|
||||||
await for(var data in _payloadPullStreamController.stream) {
|
var payloadPullStream = _payloadPullStreamController.stream;
|
||||||
await tempFile.writeAsBytes(data, mode: FileMode.append, flush: true);
|
var tempFile = File('${Directory.current.path}/.tmp/${DateTime.now().microsecondsSinceEpoch}')..createSync();
|
||||||
}
|
Future(() async {
|
||||||
_payloadRawStreamController.add(tempFile);
|
await for(var data in payloadPullStream) {
|
||||||
});
|
await tempFile.writeAsBytes(data, mode: FileMode.append, flush: true);
|
||||||
|
}
|
||||||
|
_payloadRawStreamController.add(tempFile);
|
||||||
|
});
|
||||||
|
}();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//Buffered data is not long enough
|
//Buffered data is not long enough
|
||||||
@ -111,7 +158,6 @@ class TCPRepository {
|
|||||||
//Got intact request json
|
//Got intact request json
|
||||||
//Emit request buffer through stream
|
//Emit request buffer through stream
|
||||||
_responseRawStreamController.add(buffer.sublist(0, responseLength));
|
_responseRawStreamController.add(buffer.sublist(0, responseLength));
|
||||||
_responseRawStreamController.close();
|
|
||||||
//Remove proccessed buffer
|
//Remove proccessed buffer
|
||||||
buffer.removeRange(0, responseLength);
|
buffer.removeRange(0, responseLength);
|
||||||
//Clear awaiting request length
|
//Clear awaiting request length
|
||||||
@ -156,6 +202,10 @@ class TCPRepository {
|
|||||||
required List<int> responseBytes,
|
required List<int> responseBytes,
|
||||||
required File tempFile
|
required File tempFile
|
||||||
}) async {
|
}) async {
|
||||||
|
if(_responseStreamController.isClosed) {
|
||||||
|
await tempFile.delete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var responseJSON = String.fromCharCodes(responseBytes);
|
var responseJSON = String.fromCharCodes(responseBytes);
|
||||||
var responseObject = jsonDecode(responseJSON);
|
var responseObject = jsonDecode(responseJSON);
|
||||||
TCPResponseType responseType = TCPResponseType.fromValue(responseObject['response'] as String);
|
TCPResponseType responseType = TCPResponseType.fromValue(responseObject['response'] as String);
|
||||||
@ -225,8 +275,7 @@ class TCPRepository {
|
|||||||
jsonObject: responseObject,
|
jsonObject: responseObject,
|
||||||
payload: LocalFile(
|
payload: LocalFile(
|
||||||
file: tempFile,
|
file: tempFile,
|
||||||
filemd5: md5.convert(await tempFile.readAsBytes()).toString(),
|
filemd5: md5.convert(await tempFile.readAsBytes()).toString()
|
||||||
ext: ""
|
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
@ -258,7 +307,7 @@ class TCPRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
//Duplicate current socket
|
//Duplicate current socket
|
||||||
Socket socket = await Socket.connect(_remoteAddress, _remotePort);
|
Socket socket = await Socket.connect(_remoteAddress, _remotePort);
|
||||||
TCPRepository duplicatedRepository = TCPRepository(
|
TCPRepository duplicatedRepository = TCPRepository._internal(
|
||||||
socket: socket,
|
socket: socket,
|
||||||
remoteAddress: _remoteAddress,
|
remoteAddress: _remoteAddress,
|
||||||
remotePort: _remotePort
|
remotePort: _remotePort
|
||||||
@ -278,11 +327,11 @@ class TCPRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_socket.close();
|
||||||
_responseRawStreamController.close();
|
_responseRawStreamController.close();
|
||||||
_payloadPullStreamController.close();
|
_payloadPullStreamController.close();
|
||||||
_payloadRawStreamController.close();
|
_payloadRawStreamController.close();
|
||||||
_responseStreamController.close();
|
_responseStreamController.close();
|
||||||
_requestStreamController.close();
|
_requestStreamController.close();
|
||||||
_socket.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16
pubspec.lock
16
pubspec.lock
@ -2,7 +2,7 @@
|
|||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
@ -156,6 +156,13 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
loading_indicator:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: loading_indicator
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -380,6 +387,13 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
stream_transform:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -46,6 +46,9 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/crazecoder/open_file
|
url: https://github.com/crazecoder/open_file
|
||||||
shared_preferences: ^2.0.15
|
shared_preferences: ^2.0.15
|
||||||
|
loading_indicator: ^3.1.0
|
||||||
|
async: ^2.9.0
|
||||||
|
stream_transform: ^2.0.1
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user