Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
ethan
/
appframe
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Commit 990951ff
authored
2026-01-19 14:42:27 +0800
by
tanghuan
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
增加通过Apple登录的功能,以及优化了toast方式的错误信息提示
1 parent
75241a4c
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
123 additions
and
95 deletions
lib/bloc/login_main_cubit.dart
lib/bloc/login_phone_cubit.dart
lib/bloc/login_qr_cubit.dart
lib/bloc/setting/account_user_cubit.dart
lib/bloc/web_cubit.dart
lib/config/locator.dart
lib/data/repositories/phone_auth_repository.dart
lib/data/repositories/user_auth_repository.dart
lib/data/repositories/wechat_auth_repository.dart
lib/ui/pages/login_main_page.dart
lib/ui/pages/login_phone_page.dart
lib/ui/pages/login_qr_page.dart
lib/ui/pages/web_page.dart
lib/bloc/login_main_cubit.dart
View file @
990951f
This diff is collapsed.
Click to expand it.
lib/bloc/login_phone_cubit.dart
View file @
990951f
...
...
@@ -7,22 +7,19 @@ import 'package:appframe/data/repositories/phone_auth_repository.dart';
import
'package:equatable/equatable.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:fluttertoast/fluttertoast.dart'
;
import
'package:shared_preferences/shared_preferences.dart'
;
class
LoginPhoneState
extends
Equatable
{
final
bool
agreed
;
final
bool
showAgreed
;
final
bool
showSnackBar
;
final
String
snackBarMsg
;
final
bool
allowSend
;
final
int
seconds
;
const
LoginPhoneState
({
this
.
agreed
=
false
,
this
.
showAgreed
=
false
,
this
.
showSnackBar
=
false
,
this
.
snackBarMsg
=
''
,
this
.
allowSend
=
true
,
this
.
seconds
=
0
,
});
...
...
@@ -30,16 +27,12 @@ class LoginPhoneState extends Equatable {
LoginPhoneState
copyWith
({
bool
?
agreed
,
bool
?
showAgreed
,
bool
?
showSnackBar
,
String
?
snackBarMsg
,
bool
?
allowSend
,
int
?
seconds
,
})
{
return
LoginPhoneState
(
agreed:
agreed
??
this
.
agreed
,
showAgreed:
showAgreed
??
this
.
showAgreed
,
showSnackBar:
showSnackBar
??
this
.
showSnackBar
,
snackBarMsg:
snackBarMsg
??
this
.
snackBarMsg
,
allowSend:
allowSend
??
this
.
allowSend
,
seconds:
seconds
??
this
.
seconds
,
);
...
...
@@ -49,8 +42,6 @@ class LoginPhoneState extends Equatable {
List
<
Object
?>
get
props
=>
[
agreed
,
showAgreed
,
showSnackBar
,
snackBarMsg
,
allowSend
,
seconds
,
];
...
...
@@ -100,16 +91,15 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
// 验证手机号码
String
phone
=
_phoneController
.
text
;
if
(!
RegExp
(
r'^1[3-9][0-9]{9}$'
).
hasMatch
(
phone
))
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'请输入正确的手机号码'
)
);
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
'请输入正确的手机号码'
,
backgroundColor:
Colors
.
red
);
return
;
}
// 发送验证码
var
result
=
await
_phoneAuthRepository
.
verifyCode
(
phone
,
0
);
if
(
result
[
'code'
]
!=
0
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
result
[
'error'
]));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
result
[
'error'
],
backgroundColor:
Colors
.
red
);
return
;
}
...
...
@@ -125,14 +115,12 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
String
verifyCode
=
_codeController
.
text
;
if
(!
RegExp
(
r'^1[3-9][0-9]{9}$'
).
hasMatch
(
phone
))
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'请输入正确的手机号码'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
'请输入正确的手机号码'
,
backgroundColor:
Colors
.
red
);
return
;
}
if
(!
RegExp
(
r'^\d{4}$'
).
hasMatch
(
verifyCode
))
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'请输入正确的验证码'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
'请输入正确的验证码'
,
backgroundColor:
Colors
.
red
);
return
;
}
...
...
@@ -143,17 +131,19 @@ class LoginPhoneCubit extends Cubit<LoginPhoneState> {
var
resultData
=
await
_phoneAuthRepository
.
login
(
phone
,
verifyCode
);
if
(
resultData
==
null
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'登录请求失败'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
'登录请求失败'
,
backgroundColor:
Colors
.
red
);
return
;
}
if
(
resultData
[
'code'
]
!=
0
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
resultData
[
'error'
]));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
resultData
[
'error'
],
backgroundColor:
Colors
.
red
);
return
;
}
var
data
=
resultData
[
'data'
]
as
Map
<
String
,
dynamic
>;
_handleLoginSuccess
(
data
);
}
void
_handleLoginSuccess
(
Map
<
String
,
dynamic
>
data
)
{
var
roles
=
data
[
'roles'
];
// 过滤出家长角色的数据
roles
.
removeWhere
((
element
)
=>
element
[
'userType'
]
!=
2
);
...
...
lib/bloc/login_qr_cubit.dart
View file @
990951f
import
'dart:convert'
;
import
'dart:typed_data'
;
import
'package:appframe/config/constant.dart'
;
import
'package:appframe/config/locator.dart'
;
...
...
@@ -8,7 +7,9 @@ import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import
'package:crypto/crypto.dart'
;
import
'package:equatable/equatable.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:fluttertoast/fluttertoast.dart'
;
import
'package:fluwx/fluwx.dart'
;
import
'package:shared_preferences/shared_preferences.dart'
;
...
...
@@ -16,30 +17,22 @@ class LoginQrState extends Equatable {
final
int
status
;
final
Uint8List
?
image
;
final
String
tip
;
final
bool
showSnackBar
;
final
String
snackBarMsg
;
const
LoginQrState
({
this
.
status
=
0
,
this
.
image
,
this
.
tip
=
''
,
this
.
showSnackBar
=
false
,
this
.
snackBarMsg
=
''
,
});
LoginQrState
copyWith
({
int
?
status
,
Uint8List
?
image
,
String
?
tip
,
bool
?
showSnackBar
,
String
?
snackBarMsg
,
})
{
return
LoginQrState
(
status:
status
??
this
.
status
,
image:
image
??
this
.
image
,
tip:
tip
??
this
.
tip
,
showSnackBar:
showSnackBar
??
this
.
showSnackBar
,
snackBarMsg:
snackBarMsg
??
this
.
snackBarMsg
,
);
}
...
...
@@ -48,8 +41,6 @@ class LoginQrState extends Equatable {
status
,
image
,
tip
,
showSnackBar
,
snackBarMsg
,
];
}
...
...
@@ -71,15 +62,13 @@ class LoginQrCubit extends Cubit<LoginQrState> {
var
resultData
=
await
_wechatAuthRepository
.
getTicket
()
as
Map
<
String
,
dynamic
>?;
// 请求接口异常
if
(
resultData
==
null
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'生成二维码失败'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
'生成二维码失败'
,
backgroundColor:
Colors
.
red
);
return
;
}
// 状态码错误
if
(
resultData
[
'resultCode'
]
!=
'001'
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'生成二维码状态错误'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
'生成二维码状态错误'
,
backgroundColor:
Colors
.
red
);
return
;
}
...
...
@@ -100,8 +89,7 @@ class LoginQrCubit extends Cubit<LoginQrState> {
);
if
(!
authResult
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'请求微信失败'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
'请求微信失败'
,
backgroundColor:
Colors
.
red
);
return
;
}
}
...
...
@@ -151,15 +139,13 @@ class LoginQrCubit extends Cubit<LoginQrState> {
// 请求接口异常
if
(
resultData
==
null
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'登录请求处理失败'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
'登录请求处理失败'
,
backgroundColor:
Colors
.
red
);
return
;
}
// 状态码错误
if
(
resultData
[
'resultCode'
]
!=
'001'
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'登录请求状态失败'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
));
Fluttertoast
.
showToast
(
msg:
'登录请求状态失败'
,
backgroundColor:
Colors
.
red
);
return
;
}
...
...
lib/bloc/setting/account_user_cubit.dart
View file @
990951f
...
...
@@ -5,7 +5,7 @@ import 'package:appframe/config/constant.dart';
import
'package:appframe/config/env_config.dart'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/config/routes.dart'
;
import
'package:appframe/data/repositories/
phone
_auth_repository.dart'
;
import
'package:appframe/data/repositories/
user
_auth_repository.dart'
;
import
'package:appframe/services/api_service.dart'
;
import
'package:dio/dio.dart'
;
import
'package:equatable/equatable.dart'
;
...
...
@@ -68,7 +68,7 @@ class AccountUserCubit extends Cubit<AccountUserState> {
late
TextEditingController
_nameController
;
late
TextEditingController
_nickNameController
;
late
final
PhoneAuthRepository
_phone
AuthRepository
;
late
final
UserAuthRepository
_user
AuthRepository
;
TextEditingController
get
nameController
=>
_nameController
;
...
...
@@ -78,7 +78,7 @@ class AccountUserCubit extends Cubit<AccountUserState> {
_nameController
=
TextEditingController
(
text:
state
.
name
);
_nickNameController
=
TextEditingController
(
text:
state
.
nickname
);
_
phoneAuthRepository
=
getIt
.
get
<
Phone
AuthRepository
>();
_
userAuthRepository
=
getIt
.
get
<
User
AuthRepository
>();
}
void
updateAvatar
(
String
avatarPath
)
{
...
...
@@ -110,7 +110,7 @@ class AccountUserCubit extends Cubit<AccountUserState> {
var
sharedPreferences
=
getIt
.
get
<
SharedPreferences
>();
var
userCode
=
sharedPreferences
.
getString
(
'auth_userCode'
)
??
''
;
var
result
=
await
_
phone
AuthRepository
.
updateUser
(
userCode
,
name
,
nickname
,
imgPath
);
var
result
=
await
_
user
AuthRepository
.
updateUser
(
userCode
,
name
,
nickname
,
imgPath
);
emit
(
state
.
copyWith
(
isLoading:
false
));
...
...
lib/bloc/web_cubit.dart
View file @
990951f
...
...
@@ -17,6 +17,7 @@ import 'package:dio/dio.dart';
import
'package:equatable/equatable.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:fluttertoast/fluttertoast.dart'
;
import
'package:fluwx/fluwx.dart'
;
import
'package:path_provider/path_provider.dart'
;
import
'package:permission_handler/permission_handler.dart'
;
...
...
@@ -66,10 +67,6 @@ class WebState extends Equatable {
final
bool
chooseVideoCmdFlag
;
final
String
chooseVideoCmdMessage
;
/// 提示信息
final
bool
showSnackBar
;
final
String
snackBarMsg
;
final
String
h5Version
;
/// 用于测试监测问题
...
...
@@ -99,8 +96,6 @@ class WebState extends Equatable {
this
.
chooseImageCmdMessage
=
''
,
this
.
chooseVideoCmdFlag
=
false
,
this
.
chooseVideoCmdMessage
=
''
,
this
.
showSnackBar
=
false
,
this
.
snackBarMsg
=
''
,
this
.
h5Version
=
''
,
this
.
testMsg
=
''
,
});
...
...
@@ -130,8 +125,6 @@ class WebState extends Equatable {
String
?
chooseImageCmdMessage
,
bool
?
chooseVideoCmdFlag
,
String
?
chooseVideoCmdMessage
,
bool
?
showSnackBar
,
String
?
snackBarMsg
,
String
?
h5Version
,
String
?
testMsg
,
})
{
...
...
@@ -159,8 +152,6 @@ class WebState extends Equatable {
chooseImageCmdMessage:
chooseImageCmdMessage
??
this
.
chooseImageCmdMessage
,
chooseVideoCmdFlag:
chooseVideoCmdFlag
??
this
.
chooseVideoCmdFlag
,
chooseVideoCmdMessage:
chooseVideoCmdMessage
??
this
.
chooseVideoCmdMessage
,
showSnackBar:
showSnackBar
??
this
.
showSnackBar
,
snackBarMsg:
snackBarMsg
??
this
.
snackBarMsg
,
h5Version:
h5Version
??
this
.
h5Version
,
testMsg:
testMsg
??
this
.
testMsg
,
);
...
...
@@ -190,8 +181,6 @@ class WebState extends Equatable {
chooseImageCmdMessage
,
chooseVideoCmdFlag
,
chooseVideoCmdMessage
,
showSnackBar
,
snackBarMsg
,
h5Version
,
testMsg
,
];
...
...
@@ -1128,12 +1117,22 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
var
vol
=
await
volumeController
.
getVolume
();
debugPrint
(
'检测音量:
$vol
'
);
if
(
vol
==
0
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'设备处于静音状态,请调整音量'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
,
snackBarMsg:
''
));
// emit(state.copyWith(showSnackBar: true, snackBarMsg: '设备处于静音状态,请调整音量'));
// emit(state.copyWith(showSnackBar: false, snackBarMsg: ''));
Fluttertoast
.
showToast
(
msg:
'设备处于静音状态,请调整音量'
,
backgroundColor:
Colors
.
red
,
gravity:
ToastGravity
.
TOP
,
);
_isFirstTimeOfCheck
=
false
;
}
else
if
(
vol
<=
0.15
)
{
emit
(
state
.
copyWith
(
showSnackBar:
true
,
snackBarMsg:
'设备音量过低,建议调高音量'
));
emit
(
state
.
copyWith
(
showSnackBar:
false
,
snackBarMsg:
''
));
// emit(state.copyWith(showSnackBar: true, snackBarMsg: '设备音量过低,建议调高音量'));
// emit(state.copyWith(showSnackBar: false, snackBarMsg: ''));
Fluttertoast
.
showToast
(
msg:
'设备音量过低,建议调高音量'
,
backgroundColor:
Colors
.
red
,
gravity:
ToastGravity
.
TOP
,
);
_isFirstTimeOfCheck
=
false
;
}
}
...
...
lib/config/locator.dart
View file @
990951f
...
...
@@ -34,6 +34,7 @@ import 'package:appframe/data/repositories/message/video_info_handler.dart';
import
'package:appframe/data/repositories/message/wifi_info_handler.dart'
;
import
'package:appframe/data/repositories/message/window_info_handler.dart'
;
import
'package:appframe/data/repositories/phone_auth_repository.dart'
;
import
'package:appframe/data/repositories/user_auth_repository.dart'
;
import
'package:appframe/data/repositories/wechat_auth_repository.dart'
;
import
'package:appframe/services/api_service.dart'
;
import
'package:appframe/services/dispatcher.dart'
;
...
...
@@ -219,4 +220,5 @@ Future<void> setupLocator() async {
/// repository
getIt
.
registerLazySingleton
<
WechatAuthRepository
>(()
=>
WechatAuthRepository
());
getIt
.
registerLazySingleton
<
PhoneAuthRepository
>(()
=>
PhoneAuthRepository
());
getIt
.
registerLazySingleton
<
UserAuthRepository
>(()
=>
UserAuthRepository
());
}
lib/data/repositories/phone_auth_repository.dart
View file @
990951f
...
...
@@ -112,23 +112,4 @@ class PhoneAuthRepository {
);
return
resp
.
data
;
}
///
/// {
/// "code": 0,
/// "error": "操作成功"
/// }
///
Future
<
dynamic
>
updateUser
(
String
userid
,
String
name
,
String
nickName
,
String
avatar
)
async
{
Response
resp
=
await
_appService
.
post
(
'/api/v1/comm/user/update'
,
{
"userid"
:
userid
,
"name"
:
name
,
"nickName"
:
nickName
,
"avatar"
:
avatar
,
},
);
return
resp
.
data
;
}
}
lib/data/repositories/user_auth_repository.dart
0 → 100644
View file @
990951f
import
'dart:io'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/services/api_service.dart'
;
import
'package:dio/dio.dart'
;
class
UserAuthRepository
{
late
final
ApiService
_appService
;
UserAuthRepository
()
{
_appService
=
getIt
<
ApiService
>(
instanceName:
'appApiService'
);
}
///
/// {
/// "code": 0,
/// "error": "操作成功"
/// }
///
Future
<
dynamic
>
updateUser
(
String
userid
,
String
name
,
String
nickName
,
String
avatar
)
async
{
Response
resp
=
await
_appService
.
post
(
'/api/v1/comm/user/update'
,
{
"userid"
:
userid
,
"name"
:
name
,
"nickName"
:
nickName
,
"avatar"
:
avatar
,
},
);
return
resp
.
statusCode
==
HttpStatus
.
ok
?
resp
.
data
:
null
;
}
Future
<
dynamic
>
appleLogin
(
String
userid
,
String
authorizationCode
,
String
identityToken
)
async
{
Response
resp
=
await
_appService
.
post
(
'/api/v1/comm/user/applelogin'
,
{
"user"
:
userid
,
"authorizationCode"
:
authorizationCode
,
"identityToken"
:
identityToken
,
},
);
return
resp
.
statusCode
==
HttpStatus
.
ok
?
resp
.
data
:
null
;
}
///
/// {
/// "code": 1,
/// "data":"bxe userid" // 存在会返回
/// "error": "",
/// }
Future
<
dynamic
>
exchangeId
(
String
userid
)
async
{
Response
resp
=
await
_appService
.
post
(
'/api/v1/comm/user/exchangeid'
,
{
"userId"
:
userid
,
"type"
:
"apple"
,
},
);
return
resp
.
statusCode
==
HttpStatus
.
ok
?
resp
.
data
:
null
;
}
///
/// {
/// "error": "",
/// "code": 0,
/// }
Future
<
dynamic
>
newBinding
(
String
userid
,
String
bxeUserId
)
async
{
Response
resp
=
await
_appService
.
post
(
'/api/v1/comm/user/newbinding'
,
{
"userId"
:
userid
,
"bxeUserId"
:
userid
,
"type"
:
"apple"
,
},
);
return
resp
.
statusCode
==
HttpStatus
.
ok
?
resp
.
data
:
null
;
}
}
lib/data/repositories/wechat_auth_repository.dart
View file @
990951f
import
'dart:io'
;
import
'package:appframe/config/locator.dart'
;
import
'package:appframe/services/api_service.dart'
;
import
'package:dio/dio.dart'
;
...
...
@@ -21,7 +23,7 @@ class WechatAuthRepository {
debugPrint
(
'登录结果:
$resp
'
);
return
resp
.
statusCode
==
200
?
resp
.
data
:
null
;
return
resp
.
statusCode
==
HttpStatus
.
ok
?
resp
.
data
:
null
;
}
Future
<
dynamic
>
getTicket
()
async
{
...
...
@@ -35,6 +37,6 @@ class WechatAuthRepository {
debugPrint
(
'获取ticket:
$resp
'
);
return
resp
.
statusCode
==
200
?
resp
.
data
:
null
;
return
resp
.
statusCode
==
HttpStatus
.
ok
?
resp
.
data
:
null
;
}
}
lib/ui/pages/login_main_page.dart
View file @
990951f
This diff is collapsed.
Click to expand it.
lib/ui/pages/login_phone_page.dart
View file @
990951f
import
'package:appframe/bloc/login_phone_cubit.dart'
;
import
'package:appframe/config/routes.dart'
;
import
'package:appframe/ui/widgets/login/login_page_agreed_widget.dart'
;
import
'package:appframe/ui/widgets/tip_overlay_widget.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
...
...
@@ -81,8 +80,6 @@ class LoginPhonePage extends StatelessWidget {
listener:
(
context
,
state
)
{
if
(
state
.
showAgreed
)
{
_showAgreementDialog
(
context
,
context
.
read
<
LoginPhoneCubit
>());
}
else
if
(
state
.
showSnackBar
)
{
TipOverlayUtil
.
showTip
(
context
,
state
.
snackBarMsg
);
}
},
),
...
...
lib/ui/pages/login_qr_page.dart
View file @
990951f
import
'package:appframe/bloc/login_qr_cubit.dart'
;
import
'package:appframe/ui/widgets/tip_overlay_widget.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
...
...
@@ -61,11 +60,7 @@ class LoginQrPage extends StatelessWidget {
),
);
},
listener:
(
context
,
state
)
{
if
(
state
.
showSnackBar
)
{
TipOverlayUtil
.
showTip
(
context
,
state
.
snackBarMsg
);
}
},
listener:
(
context
,
state
)
{},
));
}
...
...
lib/ui/pages/web_page.dart
View file @
990951f
...
...
@@ -151,8 +151,6 @@ class WebPage extends StatelessWidget {
context
.
read
<
WebCubit
>().
chooseImage
(
context
);
}
else
if
(
state
.
chooseVideoCmdFlag
)
{
context
.
read
<
WebCubit
>().
chooseVideo
(
context
);
}
else
if
(
state
.
showSnackBar
)
{
TipOverlayUtil
.
showTip
(
context
,
state
.
snackBarMsg
);
}
},
),
...
...
Write
Preview
Styling with
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment