✨ Login & register
This commit is contained in:
parent
5e12a8860c
commit
4d12d243b3
@ -1,5 +1,39 @@
|
|||||||
{
|
{
|
||||||
"screenHome": "Home",
|
"screen": "Screen",
|
||||||
"screenExplore": "Explore",
|
"screenHome": "Home",
|
||||||
"screenAccount": "Account"
|
"screenExplore": "Explore",
|
||||||
}
|
"screenAccount": "Account",
|
||||||
|
"screenAuthLogin": "Login",
|
||||||
|
"screenAuthLoginSubtitle": "Login to Solar Network using Solarpass",
|
||||||
|
"screenAuthLoginGreeting": "Welcome back",
|
||||||
|
"screenAuthRegister": "Create an account",
|
||||||
|
"screenAuthRegisterSubtitle": "Create a Solarpass account",
|
||||||
|
"dialogOkay": "Okay",
|
||||||
|
"dialogCancel": "Cancel",
|
||||||
|
"dialogConfirm": "Confirm",
|
||||||
|
"dialogDismiss": "Dismiss",
|
||||||
|
"dialogError": "Something went wrong",
|
||||||
|
"errorRequestBad": "Bad request, please check your input.",
|
||||||
|
"errorRequestUnauthorized": "Unauthorized request, please login or try re-login.",
|
||||||
|
"errorRequestForbidden": "Forbidden request, you have not enough permission to do that.",
|
||||||
|
"errorRequestNotFound": "The resource that you looking for is not found.",
|
||||||
|
"errorRequestConnection": "Network connection error, please check your network or the service status.",
|
||||||
|
"errorRequestUnknown": "Unknown request error, maybe you want to take screenshot and report it to us.",
|
||||||
|
"prev": "Next",
|
||||||
|
"next": "Previous",
|
||||||
|
"fieldUsername": "Username",
|
||||||
|
"fieldNickname": "Nickname",
|
||||||
|
"fieldEmail": "Email address",
|
||||||
|
"fieldPassword": "Password",
|
||||||
|
"fieldUsernameLookupHint": "You can use username, phone number or email to login",
|
||||||
|
"forgotPassword": "Forgot password",
|
||||||
|
"loginPickFactor": "Pick a factor",
|
||||||
|
"loginMultiFactor": {
|
||||||
|
"one": "{} step left",
|
||||||
|
"other": "{} steps left"
|
||||||
|
},
|
||||||
|
"loginEnterPassword": "Enter the code",
|
||||||
|
"loginSuccess": "Logged in as {}",
|
||||||
|
"authFactorPassword": "Password",
|
||||||
|
"authFactorEmail": "Email verification code"
|
||||||
|
}
|
||||||
|
@ -1,5 +1,39 @@
|
|||||||
{
|
{
|
||||||
"screenHome": "首页",
|
"screen": "页面",
|
||||||
"screenExplore": "探索" ,
|
"screenHome": "首页",
|
||||||
"screenAccount": "您"
|
"screenExplore": "探索",
|
||||||
}
|
"screenAccount": "您",
|
||||||
|
"screenAuthLogin": "登陆",
|
||||||
|
"screenAuthLoginSubtitle": "使用 Solarpass 登陆 Solar Network",
|
||||||
|
"screenAuthLoginGreeting": "欢迎回来",
|
||||||
|
"screenAuthRegister": "创建账号",
|
||||||
|
"screenAuthRegisterSubtitle": "创建一个 Solarpass 账号",
|
||||||
|
"dialogOkay": "好的",
|
||||||
|
"dialogCancel": "取消",
|
||||||
|
"dialogConfirm": "确认",
|
||||||
|
"dialogDismiss": "忽略",
|
||||||
|
"dialogError": "出了点问题",
|
||||||
|
"errorRequestBad": "服务器拒绝了您的请求,请检查您的输入。",
|
||||||
|
"errorRequestUnauthorized": "未授权的请求,请登录或者尝试重新登陆。",
|
||||||
|
"errorRequestForbidden": "被禁止的请求,您没有足够的权限去做那件事。",
|
||||||
|
"errorRequestNotFound": "您正查找的资源无法被找到。",
|
||||||
|
"errorRequestConnection": "网络连接错误,请检查您的网络状态或者检查我们的服务状态。",
|
||||||
|
"errorRequestUnknown": "位置请求错误,您可能想将此对话框截图并发送给我们。",
|
||||||
|
"prev": "上一步",
|
||||||
|
"next": "下一步",
|
||||||
|
"fieldUsername": "用户名",
|
||||||
|
"fieldNickname": "显示名",
|
||||||
|
"fieldEmail": "电子邮箱地址",
|
||||||
|
"fieldPassword": "密码",
|
||||||
|
"fieldUsernameLookupHint": "支持用户名、电话号码或邮箱地址",
|
||||||
|
"forgotPassword": "忘记密码",
|
||||||
|
"loginPickFactor": "选择方式验证",
|
||||||
|
"loginMultiFactor": {
|
||||||
|
"one": "{} 步验证",
|
||||||
|
"other": "{} 步验证"
|
||||||
|
},
|
||||||
|
"loginEnterPassword": "验证代码",
|
||||||
|
"loginSuccess": "登录为 {}",
|
||||||
|
"authFactorPassword": "密码",
|
||||||
|
"authFactorEmail": "电邮一次性验证码"
|
||||||
|
}
|
||||||
|
@ -7,6 +7,8 @@ PODS:
|
|||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_native_splash (0.0.1):
|
- flutter_native_splash (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_secure_storage (3.3.1):
|
||||||
|
- Flutter
|
||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@ -24,6 +26,7 @@ DEPENDENCIES:
|
|||||||
- cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
|
- cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
@ -38,6 +41,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
|
flutter_secure_storage:
|
||||||
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
@ -52,6 +57,7 @@ SPEC CHECKSUMS:
|
|||||||
cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec
|
cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||||
|
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
|
@ -33,11 +33,15 @@ class SolianApp extends StatelessWidget {
|
|||||||
providers: [
|
providers: [
|
||||||
Provider(create: (_) => SnNetworkProvider()),
|
Provider(create: (_) => SnNetworkProvider()),
|
||||||
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (_) => UserProvider()),
|
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
||||||
],
|
],
|
||||||
child: Builder(builder: (context) {
|
child: Builder(builder: (context) {
|
||||||
|
// Initialize some providers
|
||||||
|
context.read<UserProvider>();
|
||||||
|
|
||||||
final th = context.watch<ThemeProvider>();
|
final th = context.watch<ThemeProvider>();
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
theme: th.theme.light,
|
theme: th.theme.light,
|
||||||
darkTheme: th.theme.dark,
|
darkTheme: th.theme.dark,
|
||||||
|
@ -1,15 +1,23 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:native_dio_adapter/native_dio_adapter.dart';
|
import 'package:native_dio_adapter/native_dio_adapter.dart';
|
||||||
|
|
||||||
const kUseLocalNetwork = true;
|
const kUseLocalNetwork = true;
|
||||||
|
|
||||||
|
const kAtkStoreKey = 'nex_user_atk';
|
||||||
|
const kRtkStoreKey = 'nex_user_rtk';
|
||||||
|
|
||||||
class SnNetworkProvider {
|
class SnNetworkProvider {
|
||||||
late final Dio client;
|
late final Dio client;
|
||||||
|
|
||||||
|
late final FlutterSecureStorage _storage = FlutterSecureStorage();
|
||||||
|
|
||||||
SnNetworkProvider() {
|
SnNetworkProvider() {
|
||||||
client = Dio();
|
client = Dio();
|
||||||
|
|
||||||
@ -27,6 +35,56 @@ class SnNetworkProvider {
|
|||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
client.interceptors.add(
|
||||||
|
InterceptorsWrapper(
|
||||||
|
onRequest: (
|
||||||
|
RequestOptions options,
|
||||||
|
RequestInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
var atk = await _storage.read(key: kAtkStoreKey);
|
||||||
|
if (atk != null) {
|
||||||
|
final atkParts = atk.split('.');
|
||||||
|
if (atkParts.length != 3) {
|
||||||
|
throw Exception('invalid format of access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawPayload =
|
||||||
|
atkParts[1].replaceAll('-', '+').replaceAll('_', '/');
|
||||||
|
switch (rawPayload.length % 4) {
|
||||||
|
case 0:
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
rawPayload += '==';
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
rawPayload += '=';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Exception('illegal format of access token payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
final b64 = utf8.fuse(base64Url);
|
||||||
|
final payload = b64.decode(rawPayload);
|
||||||
|
final exp = jsonDecode(payload)['exp'];
|
||||||
|
if (exp >= DateTime.now().millisecondsSinceEpoch) {
|
||||||
|
log('Access token need refresh, doing it at ${DateTime.now()}');
|
||||||
|
atk = await refreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (atk != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $atk';
|
||||||
|
} else {
|
||||||
|
log('Access token refresh failed...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
handler.next(options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
|
||||||
// Switch to native implementation if possible
|
// Switch to native implementation if possible
|
||||||
client.httpClientAdapter = NativeAdapter();
|
client.httpClientAdapter = NativeAdapter();
|
||||||
@ -37,4 +95,34 @@ class SnNetworkProvider {
|
|||||||
if (ky.startsWith("http://")) return ky;
|
if (ky.startsWith("http://")) return ky;
|
||||||
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
|
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setTokenPair(String atk, String rtk) async {
|
||||||
|
await Future.wait([
|
||||||
|
_storage.write(key: kAtkStoreKey, value: atk),
|
||||||
|
_storage.write(key: kRtkStoreKey, value: rtk),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearTokenPair() async {
|
||||||
|
await Future.wait([
|
||||||
|
_storage.delete(key: kAtkStoreKey),
|
||||||
|
_storage.delete(key: kRtkStoreKey),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> refreshToken() async {
|
||||||
|
final rtk = await _storage.read(key: kRtkStoreKey);
|
||||||
|
if (rtk == null) return null;
|
||||||
|
|
||||||
|
final resp = await client.post('/cgi/id/auth/token', data: {
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'refresh_token': rtk,
|
||||||
|
});
|
||||||
|
|
||||||
|
final atk = resp.data['access_token'];
|
||||||
|
final nRtk = resp.data['refresh_token'];
|
||||||
|
await setTokenPair(atk, nRtk);
|
||||||
|
|
||||||
|
return atk;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,42 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'dart:developer';
|
||||||
|
|
||||||
class UserProvider extends ChangeNotifier {}
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
|
|
||||||
|
class UserProvider extends ChangeNotifier {
|
||||||
|
bool isAuthorized = false;
|
||||||
|
SnAccount? user;
|
||||||
|
|
||||||
|
late final SnNetworkProvider sn;
|
||||||
|
|
||||||
|
late final FlutterSecureStorage _storage = FlutterSecureStorage();
|
||||||
|
|
||||||
|
UserProvider(BuildContext context) {
|
||||||
|
sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
_storage.read(key: kAtkStoreKey).then((value) {
|
||||||
|
isAuthorized = value != null;
|
||||||
|
notifyListeners();
|
||||||
|
refreshUser().then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
log('Logged in as @${value.name}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnAccount?> refreshUser() async {
|
||||||
|
if (!isAuthorized) return null;
|
||||||
|
|
||||||
|
final resp = await sn.client.get('/cgi/id/users/me');
|
||||||
|
final out = SnAccount.fromJson(resp.data);
|
||||||
|
|
||||||
|
user = out;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:surface/screens/account.dart';
|
import 'package:surface/screens/account.dart';
|
||||||
|
import 'package:surface/screens/auth/login.dart';
|
||||||
|
import 'package:surface/screens/auth/register.dart';
|
||||||
import 'package:surface/screens/explore.dart';
|
import 'package:surface/screens/explore.dart';
|
||||||
import 'package:surface/screens/home.dart';
|
import 'package:surface/screens/home.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
@ -28,6 +30,24 @@ final appRouter = GoRouter(
|
|||||||
builder: (context, state) => const AccountScreen(),
|
builder: (context, state) => const AccountScreen(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
|
ShellRoute(
|
||||||
|
builder: (context, state, child) => AppScaffold(
|
||||||
|
body: child,
|
||||||
|
autoImplyAppBar: true,
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/auth/login',
|
||||||
|
name: 'authLogin',
|
||||||
|
builder: (context, state) => const LoginScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/auth.register',
|
||||||
|
name: 'authRegister',
|
||||||
|
builder: (context, state) => const RegisterScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
|
|
||||||
class AccountScreen extends StatefulWidget {
|
class AccountScreen extends StatefulWidget {
|
||||||
@ -14,7 +15,29 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("screenHome").tr(),
|
title: Text("screenAccount").tr(),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text('screenAuthLogin').tr(),
|
||||||
|
subtitle: Text('screenAuthLoginSubtitle').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('authLogin');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('screenAuthRegister').tr(),
|
||||||
|
subtitle: Text('screenAuthRegisterSubtitle').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed('authRegister');
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
556
lib/screens/auth/login.dart
Normal file
556
lib/screens/auth/login.dart
Normal file
@ -0,0 +1,556 @@
|
|||||||
|
import 'package:animations/animations.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/types/auth.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
|
||||||
|
0: ('authFactorPassword'.tr(), Icons.password, false),
|
||||||
|
1: ('authFactorEmail'.tr(), Icons.email, true),
|
||||||
|
};
|
||||||
|
|
||||||
|
class LoginScreen extends StatefulWidget {
|
||||||
|
const LoginScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoginScreen> createState() => _LoginScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginScreenState extends State<LoginScreen> {
|
||||||
|
SnAuthTicket? _currentTicket;
|
||||||
|
|
||||||
|
List<SnAuthFactor>? _factors;
|
||||||
|
SnAuthFactor? _factorPicked;
|
||||||
|
|
||||||
|
int _period = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 280),
|
||||||
|
child: Theme(
|
||||||
|
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: PageTransitionSwitcher(
|
||||||
|
transitionBuilder: (
|
||||||
|
Widget child,
|
||||||
|
Animation<double> primaryAnimation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
) {
|
||||||
|
return SharedAxisTransition(
|
||||||
|
animation: primaryAnimation,
|
||||||
|
secondaryAnimation: secondaryAnimation,
|
||||||
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: switch (_period % 3) {
|
||||||
|
1 => _LoginPickerScreen(
|
||||||
|
key: const ValueKey(1),
|
||||||
|
ticket: _currentTicket,
|
||||||
|
factors: _factors,
|
||||||
|
onTicket: (p0) => setState(() {
|
||||||
|
_currentTicket = p0;
|
||||||
|
}),
|
||||||
|
onPickFactor: (p0) => setState(() {
|
||||||
|
_factorPicked = p0;
|
||||||
|
}),
|
||||||
|
onNext: () => setState(() {
|
||||||
|
_period++;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
2 => _LoginCheckScreen(
|
||||||
|
key: const ValueKey(2),
|
||||||
|
ticket: _currentTicket,
|
||||||
|
factor: _factorPicked,
|
||||||
|
onTicket: (p0) => setState(() {
|
||||||
|
_currentTicket = p0;
|
||||||
|
}),
|
||||||
|
onNext: (p0) => setState(() {
|
||||||
|
_period = 1;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
_ => _LoginLookupScreen(
|
||||||
|
key: const ValueKey(0),
|
||||||
|
ticket: _currentTicket,
|
||||||
|
onTicket: (p0) => setState(() {
|
||||||
|
_currentTicket = p0;
|
||||||
|
}),
|
||||||
|
onFactor: (p0) => setState(() {
|
||||||
|
_factors = p0;
|
||||||
|
}),
|
||||||
|
onNext: () => setState(() {
|
||||||
|
_period++;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
).padding(all: 24),
|
||||||
|
).center(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginCheckScreen extends StatefulWidget {
|
||||||
|
final SnAuthTicket? ticket;
|
||||||
|
final SnAuthFactor? factor;
|
||||||
|
final Function(SnAuthTicket?) onTicket;
|
||||||
|
final Function onNext;
|
||||||
|
const _LoginCheckScreen({
|
||||||
|
super.key,
|
||||||
|
required this.ticket,
|
||||||
|
required this.factor,
|
||||||
|
required this.onTicket,
|
||||||
|
required this.onNext,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_LoginCheckScreen> createState() => _LoginCheckScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
|
||||||
|
void _performCheckTicket() async {
|
||||||
|
final password = _passwordController.value.text;
|
||||||
|
if (password.isEmpty) return;
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check ticket
|
||||||
|
final resp = await sn.client.patch('/cgi/id/auth', data: {
|
||||||
|
'ticket_id': widget.ticket!.id,
|
||||||
|
'factor_id': widget.factor!.id,
|
||||||
|
'code': password,
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = SnAuthResult.fromJson(resp.data);
|
||||||
|
widget.onTicket(result.ticket);
|
||||||
|
|
||||||
|
if (!result.isFinished) {
|
||||||
|
widget.onNext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish sign in if possible
|
||||||
|
final tokenResp = await sn.client.post('/cgi/id/auth/token', data: {
|
||||||
|
'grant_type': 'grant_token',
|
||||||
|
'code': result.ticket!.grantToken,
|
||||||
|
});
|
||||||
|
final atk = tokenResp.data['access_token'];
|
||||||
|
final rtk = tokenResp.data['refresh_token'];
|
||||||
|
await sn.setTokenPair(atk, rtk);
|
||||||
|
if (!mounted) return;
|
||||||
|
final user = context.read<UserProvider>();
|
||||||
|
final userinfo = await user.refreshUser();
|
||||||
|
context.showSnackbar('loginSuccess'.tr(args: [
|
||||||
|
'@${userinfo!.name} (${userinfo.nick})',
|
||||||
|
]));
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
} catch (err) {
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_passwordController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
child: const Icon(
|
||||||
|
Symbols.password,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'loginEnterPassword'.tr(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
).padding(left: 4, bottom: 16),
|
||||||
|
TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _passwordController,
|
||||||
|
obscureText: true,
|
||||||
|
autofillHints: [
|
||||||
|
(_factorLabelMap[widget.factor!.type]?.$3 ?? true)
|
||||||
|
? AutofillHints.password
|
||||||
|
: AutofillHints.oneTimeCode
|
||||||
|
],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldPassword'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onSubmitted: _isBusy ? null : (_) => _performCheckTicket(),
|
||||||
|
).padding(horizontal: 7),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _performCheckTicket(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next').tr(),
|
||||||
|
const Icon(Icons.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPickerScreen extends StatefulWidget {
|
||||||
|
final SnAuthTicket? ticket;
|
||||||
|
final List<SnAuthFactor>? factors;
|
||||||
|
final Function(SnAuthTicket?) onTicket;
|
||||||
|
final Function(SnAuthFactor) onPickFactor;
|
||||||
|
final Function onNext;
|
||||||
|
const _LoginPickerScreen({
|
||||||
|
super.key,
|
||||||
|
required this.ticket,
|
||||||
|
required this.factors,
|
||||||
|
required this.onTicket,
|
||||||
|
required this.onPickFactor,
|
||||||
|
required this.onNext,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_LoginPickerScreen> createState() => _LoginPickerScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
||||||
|
bool _isBusy = false;
|
||||||
|
int? _factorPicked;
|
||||||
|
|
||||||
|
Color get _unFocusColor =>
|
||||||
|
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||||
|
|
||||||
|
void _performGetFactorCode() async {
|
||||||
|
if (_factorPicked == null) return;
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Request one-time-password code
|
||||||
|
sn.client.post('/cgi/id/auth/factors/$_factorPicked');
|
||||||
|
widget.onPickFactor(
|
||||||
|
widget.factors!.where((x) => x.id == _factorPicked).first,
|
||||||
|
);
|
||||||
|
widget.onNext();
|
||||||
|
} catch (err) {
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
key: const ValueKey<int>(1),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
child: const Icon(
|
||||||
|
Symbols.security,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'loginPickFactor',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
).tr().padding(left: 4),
|
||||||
|
const Gap(8),
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Column(
|
||||||
|
children: widget.factors
|
||||||
|
?.map(
|
||||||
|
(x) => CheckboxListTile(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
secondary: Icon(
|
||||||
|
_factorLabelMap[x.type]?.$2 ?? Icons.question_mark,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
|
||||||
|
),
|
||||||
|
enabled: !widget.ticket!.factorTrail.contains(x.id),
|
||||||
|
value: _factorPicked == x.id,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == true) {
|
||||||
|
setState(() => _factorPicked = x.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
List.empty(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
'loginMultiFactor'.plural(
|
||||||
|
widget.ticket!.stepRemain,
|
||||||
|
),
|
||||||
|
style: TextStyle(color: _unFocusColor, fontSize: 13),
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _performGetFactorCode(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next'.tr()),
|
||||||
|
const Icon(Icons.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginLookupScreen extends StatefulWidget {
|
||||||
|
final SnAuthTicket? ticket;
|
||||||
|
final Function(SnAuthTicket?) onTicket;
|
||||||
|
final Function(List<SnAuthFactor>?) onFactor;
|
||||||
|
final Function onNext;
|
||||||
|
const _LoginLookupScreen({
|
||||||
|
super.key,
|
||||||
|
required this.ticket,
|
||||||
|
required this.onTicket,
|
||||||
|
required this.onFactor,
|
||||||
|
required this.onNext,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_LoginLookupScreen> createState() => _LoginLookupScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
||||||
|
final _usernameController = TextEditingController();
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
void _requestResetPassword() async {
|
||||||
|
final username = _usernameController.value.text;
|
||||||
|
if (username.isEmpty) {
|
||||||
|
context.showErrorDialog('signinResetPasswordHint'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final lookupResp =
|
||||||
|
await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||||
|
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
||||||
|
'user_id': lookupResp.data['id'],
|
||||||
|
});
|
||||||
|
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||||
|
} catch (err) {
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _performNewTicket() async {
|
||||||
|
final username = _usernameController.value.text;
|
||||||
|
if (username.isEmpty) return;
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create ticket
|
||||||
|
final resp = await sn.client.post('/cgi/id/auth', data: {
|
||||||
|
'username': username,
|
||||||
|
});
|
||||||
|
final result = SnAuthResult.fromJson(resp.data);
|
||||||
|
widget.onTicket(result.ticket);
|
||||||
|
|
||||||
|
// Pull factors
|
||||||
|
final factorResp =
|
||||||
|
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||||
|
'ticketId': result.ticket!.id.toString(),
|
||||||
|
});
|
||||||
|
widget.onFactor(
|
||||||
|
List<SnAuthFactor>.from(
|
||||||
|
factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
widget.onNext();
|
||||||
|
} catch (err) {
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_usernameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
child: const Icon(
|
||||||
|
Symbols.login,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'screenAuthLoginGreeting',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
).tr().padding(left: 4, bottom: 16),
|
||||||
|
TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _usernameController,
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldUsername'.tr(),
|
||||||
|
helperText: 'fieldUsernameLookupHint'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onSubmitted: _isBusy ? null : (_) => _performNewTicket(),
|
||||||
|
).padding(horizontal: 7),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _requestResetPassword(),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.grey),
|
||||||
|
child: Text('forgotPassword'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _performNewTicket(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next').tr(),
|
||||||
|
const Icon(Icons.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: StyledWidget(
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 290),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'termAcceptNextWithAgree'.tr(),
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha((255 * 0.75).round()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('termAcceptLink'.tr()),
|
||||||
|
const Gap(4),
|
||||||
|
const Icon(Icons.launch, size: 14),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString('https://solsynth.dev/terms');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(horizontal: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
161
lib/screens/auth/register.dart
Normal file
161
lib/screens/auth/register.dart
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
|
class RegisterScreen extends StatefulWidget {
|
||||||
|
const RegisterScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RegisterScreen> createState() => _RegisterScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegisterScreenState extends State<RegisterScreen> {
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _usernameController = TextEditingController();
|
||||||
|
final _nicknameController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
|
||||||
|
void _performAction(BuildContext context) async {
|
||||||
|
final email = _emailController.value.text;
|
||||||
|
final username = _usernameController.value.text;
|
||||||
|
final nickname = _nicknameController.value.text;
|
||||||
|
final password = _passwordController.value.text;
|
||||||
|
if (email.isEmpty ||
|
||||||
|
username.isEmpty ||
|
||||||
|
nickname.isEmpty ||
|
||||||
|
password.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
await sn.client.post('/cgi/id/users', data: {
|
||||||
|
'name': username,
|
||||||
|
'nick': nickname,
|
||||||
|
'email': email,
|
||||||
|
'password': password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// TODO make celebration here
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
|
Navigator.pop(context);
|
||||||
|
} catch (err) {
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 280),
|
||||||
|
child: StyledWidget(
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 26,
|
||||||
|
child: const Icon(
|
||||||
|
Symbols.person_add,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
).padding(bottom: 8),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'screenAuthRegister',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
).tr().padding(left: 4, bottom: 16),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _usernameController,
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldUsername'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _nicknameController,
|
||||||
|
autofillHints: const [AutofillHints.nickname],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldNickname'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextField(
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
controller: _emailController,
|
||||||
|
autofillHints: const [AutofillHints.email],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldEmail'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
TextField(
|
||||||
|
obscureText: true,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
controller: _passwordController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
labelText: 'fieldPassword'.tr(),
|
||||||
|
),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onSubmitted: (_) => _performAction(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 7),
|
||||||
|
const Gap(16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => _performAction(context),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('next').tr(),
|
||||||
|
const Icon(Icons.chevron_right),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(all: 24).center(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
69
lib/types/account.dart
Normal file
69
lib/types/account.dart
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'account.freezed.dart';
|
||||||
|
part 'account.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAccount with _$SnAccount {
|
||||||
|
const factory SnAccount({
|
||||||
|
required int id,
|
||||||
|
required int? affiliatedId,
|
||||||
|
required int? affiliatedTo,
|
||||||
|
required int? automatedBy,
|
||||||
|
required int? automatedId,
|
||||||
|
required String avatar,
|
||||||
|
required String banner,
|
||||||
|
required DateTime? confirmedAt,
|
||||||
|
required List<SnAccountContact> contacts,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required String description,
|
||||||
|
required String name,
|
||||||
|
required String nick,
|
||||||
|
required Map<String, dynamic> permNodes,
|
||||||
|
required SnAccountProfile? profile,
|
||||||
|
required DateTime? suspendedAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
}) = _SnAccount;
|
||||||
|
|
||||||
|
factory SnAccount.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAccountFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAccountContact with _$SnAccountContact {
|
||||||
|
const factory SnAccountContact({
|
||||||
|
required int accountId,
|
||||||
|
required String content,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required int id,
|
||||||
|
required bool isPrimary,
|
||||||
|
required bool isPublic,
|
||||||
|
required int type,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? verifiedAt,
|
||||||
|
}) = _SnAccountContact;
|
||||||
|
|
||||||
|
factory SnAccountContact.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAccountContactFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAccountProfile with _$SnAccountProfile {
|
||||||
|
const factory SnAccountProfile({
|
||||||
|
required int id,
|
||||||
|
required int accountId,
|
||||||
|
required DateTime? birthday,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required int experience,
|
||||||
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
|
required DateTime? lastSeenAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
}) = _SnAccountProfile;
|
||||||
|
|
||||||
|
factory SnAccountProfile.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAccountProfileFromJson(json);
|
||||||
|
}
|
1266
lib/types/account.freezed.dart
Normal file
1266
lib/types/account.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
131
lib/types/account.g.dart
Normal file
131
lib/types/account.g.dart
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'account.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnAccountImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
affiliatedId: (json['affiliated_id'] as num?)?.toInt(),
|
||||||
|
affiliatedTo: (json['affiliated_to'] as num?)?.toInt(),
|
||||||
|
automatedBy: (json['automated_by'] as num?)?.toInt(),
|
||||||
|
automatedId: (json['automated_id'] as num?)?.toInt(),
|
||||||
|
avatar: json['avatar'] as String,
|
||||||
|
banner: json['banner'] as String,
|
||||||
|
confirmedAt: json['confirmed_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['confirmed_at'] as String),
|
||||||
|
contacts: (json['contacts'] as List<dynamic>)
|
||||||
|
.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
description: json['description'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
nick: json['nick'] as String,
|
||||||
|
permNodes: json['perm_nodes'] as Map<String, dynamic>,
|
||||||
|
profile: json['profile'] == null
|
||||||
|
? null
|
||||||
|
: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
|
||||||
|
suspendedAt: json['suspended_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['suspended_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'affiliated_id': instance.affiliatedId,
|
||||||
|
'affiliated_to': instance.affiliatedTo,
|
||||||
|
'automated_by': instance.automatedBy,
|
||||||
|
'automated_id': instance.automatedId,
|
||||||
|
'avatar': instance.avatar,
|
||||||
|
'banner': instance.banner,
|
||||||
|
'confirmed_at': instance.confirmedAt?.toIso8601String(),
|
||||||
|
'contacts': instance.contacts.map((e) => e.toJson()).toList(),
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'description': instance.description,
|
||||||
|
'name': instance.name,
|
||||||
|
'nick': instance.nick,
|
||||||
|
'perm_nodes': instance.permNodes,
|
||||||
|
'profile': instance.profile?.toJson(),
|
||||||
|
'suspended_at': instance.suspendedAt?.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnAccountContactImpl _$$SnAccountContactImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SnAccountContactImpl(
|
||||||
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
content: json['content'] as String,
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
isPrimary: json['is_primary'] as bool,
|
||||||
|
isPublic: json['is_public'] as bool,
|
||||||
|
type: (json['type'] as num).toInt(),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
verifiedAt: json['verified_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['verified_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAccountContactImplToJson(
|
||||||
|
_$SnAccountContactImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'content': instance.content,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'id': instance.id,
|
||||||
|
'is_primary': instance.isPrimary,
|
||||||
|
'is_public': instance.isPublic,
|
||||||
|
'type': instance.type,
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'verified_at': instance.verifiedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnAccountProfileImpl _$$SnAccountProfileImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SnAccountProfileImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
birthday: json['birthday'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['birthday'] as String),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
experience: (json['experience'] as num).toInt(),
|
||||||
|
firstName: json['first_name'] as String,
|
||||||
|
lastName: json['last_name'] as String,
|
||||||
|
lastSeenAt: json['last_seen_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['last_seen_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAccountProfileImplToJson(
|
||||||
|
_$SnAccountProfileImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'birthday': instance.birthday?.toIso8601String(),
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'experience': instance.experience,
|
||||||
|
'first_name': instance.firstName,
|
||||||
|
'last_name': instance.lastName,
|
||||||
|
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
};
|
57
lib/types/auth.dart
Normal file
57
lib/types/auth.dart
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'auth.freezed.dart';
|
||||||
|
part 'auth.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAuthResult with _$SnAuthResult {
|
||||||
|
const factory SnAuthResult({
|
||||||
|
required bool isFinished,
|
||||||
|
required SnAuthTicket? ticket,
|
||||||
|
}) = _SnAuthResult;
|
||||||
|
|
||||||
|
factory SnAuthResult.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAuthResultFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAuthTicket with _$SnAuthTicket {
|
||||||
|
const factory SnAuthTicket({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required int stepRemain,
|
||||||
|
required String? grantToken,
|
||||||
|
required String? accessToken,
|
||||||
|
required String? refreshToken,
|
||||||
|
required String ipAddress,
|
||||||
|
required String location,
|
||||||
|
required String userAgent,
|
||||||
|
required DateTime? expiredAt,
|
||||||
|
required DateTime? lastGrantAt,
|
||||||
|
required DateTime? availableAt,
|
||||||
|
required String? nonce,
|
||||||
|
required int? accountId,
|
||||||
|
@Default([]) List<int> factorTrail,
|
||||||
|
}) = _SnAuthTicket;
|
||||||
|
|
||||||
|
factory SnAuthTicket.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAuthTicketFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAuthFactor with _$SnAuthFactor {
|
||||||
|
const factory SnAuthFactor({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required int type,
|
||||||
|
required Map<String, dynamic>? config,
|
||||||
|
required int? accountId,
|
||||||
|
}) = _SnAuthFactor;
|
||||||
|
|
||||||
|
factory SnAuthFactor.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnAuthFactorFromJson(json);
|
||||||
|
}
|
1002
lib/types/auth.freezed.dart
Normal file
1002
lib/types/auth.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
98
lib/types/auth.g.dart
Normal file
98
lib/types/auth.g.dart
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'auth.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$SnAuthResultImpl _$$SnAuthResultImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnAuthResultImpl(
|
||||||
|
isFinished: json['is_finished'] as bool,
|
||||||
|
ticket: json['ticket'] == null
|
||||||
|
? null
|
||||||
|
: SnAuthTicket.fromJson(json['ticket'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAuthResultImplToJson(_$SnAuthResultImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'is_finished': instance.isFinished,
|
||||||
|
'ticket': instance.ticket?.toJson(),
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnAuthTicketImpl _$$SnAuthTicketImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnAuthTicketImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
stepRemain: (json['step_remain'] as num).toInt(),
|
||||||
|
grantToken: json['grant_token'] as String?,
|
||||||
|
accessToken: json['access_token'] as String?,
|
||||||
|
refreshToken: json['refresh_token'] as String?,
|
||||||
|
ipAddress: json['ip_address'] as String,
|
||||||
|
location: json['location'] as String,
|
||||||
|
userAgent: json['user_agent'] as String,
|
||||||
|
expiredAt: json['expired_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['expired_at'] as String),
|
||||||
|
lastGrantAt: json['last_grant_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['last_grant_at'] as String),
|
||||||
|
availableAt: json['available_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['available_at'] as String),
|
||||||
|
nonce: json['nonce'] as String?,
|
||||||
|
accountId: (json['account_id'] as num?)?.toInt(),
|
||||||
|
factorTrail: (json['factor_trail'] as List<dynamic>?)
|
||||||
|
?.map((e) => (e as num).toInt())
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAuthTicketImplToJson(_$SnAuthTicketImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'step_remain': instance.stepRemain,
|
||||||
|
'grant_token': instance.grantToken,
|
||||||
|
'access_token': instance.accessToken,
|
||||||
|
'refresh_token': instance.refreshToken,
|
||||||
|
'ip_address': instance.ipAddress,
|
||||||
|
'location': instance.location,
|
||||||
|
'user_agent': instance.userAgent,
|
||||||
|
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||||
|
'last_grant_at': instance.lastGrantAt?.toIso8601String(),
|
||||||
|
'available_at': instance.availableAt?.toIso8601String(),
|
||||||
|
'nonce': instance.nonce,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
'factor_trail': instance.factorTrail,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnAuthFactorImpl _$$SnAuthFactorImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnAuthFactorImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
type: (json['type'] as num).toInt(),
|
||||||
|
config: json['config'] as Map<String, dynamic>?,
|
||||||
|
accountId: (json['account_id'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAuthFactorImplToJson(_$SnAuthFactorImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'type': instance.type,
|
||||||
|
'config': instance.config,
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
};
|
151
lib/widgets/dialog.dart
Normal file
151
lib/widgets/dialog.dart
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
extension AppPromptExtension on BuildContext {
|
||||||
|
void showSnackbar(String content, {SnackBarAction? action}) {
|
||||||
|
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
|
||||||
|
content: Text(content),
|
||||||
|
action: action,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearSnackbar() {
|
||||||
|
ScaffoldMessenger.of(this).clearSnackBars();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showModalDialog(String title, desc) {
|
||||||
|
return showDialog<void>(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(desc),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showInfoDialog(String title, body) {
|
||||||
|
return showDialog<void>(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(body),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> showConfirmDialog(String title, body) async {
|
||||||
|
return await showDialog<bool>(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(body),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: Text('dialogCancel').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: Text('dialogConfirm').tr(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showErrorDialog(dynamic exception) {
|
||||||
|
Widget content = Text(exception.toString().capitalize());
|
||||||
|
if (exception is DioException) {
|
||||||
|
String preview;
|
||||||
|
switch (exception.response?.statusCode) {
|
||||||
|
case 400:
|
||||||
|
preview = 'errorRequestBad'.tr();
|
||||||
|
break;
|
||||||
|
case 401:
|
||||||
|
preview = 'errorRequestUnauthorized'.tr();
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
preview = 'errorRequestForbidden'.tr();
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
preview = 'errorRequestNotFound'.tr();
|
||||||
|
break;
|
||||||
|
case null:
|
||||||
|
preview = 'errorRequestConnection'.tr();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
preview = 'errorRequestUnknown'.tr();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception.response != null) {
|
||||||
|
content = Text(
|
||||||
|
'$preview\n\n(${exception.response?.statusCode}) ${exception.response?.data}',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = Text(preview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return showDialog<void>(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: this,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text('dialogError').tr(),
|
||||||
|
content: content,
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ByteFormatter on int {
|
||||||
|
String formatBytes({int decimals = 2}) {
|
||||||
|
if (this == 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
final dm = decimals < 0 ? 0 : decimals;
|
||||||
|
final sizes = [
|
||||||
|
'Bytes',
|
||||||
|
'KiB',
|
||||||
|
'MiB',
|
||||||
|
'GiB',
|
||||||
|
'TiB',
|
||||||
|
'PiB',
|
||||||
|
'EiB',
|
||||||
|
'ZiB',
|
||||||
|
'YiB'
|
||||||
|
];
|
||||||
|
final i = (math.log(this) / math.log(k)).floor().toInt();
|
||||||
|
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StringFormatter on String {
|
||||||
|
String capitalize() {
|
||||||
|
return "${this[0].toUpperCase()}${substring(1)}";
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,48 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_background.dart';
|
import 'package:surface/widgets/navigation/app_background.dart';
|
||||||
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
|
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
|
||||||
|
|
||||||
class AppScaffold extends StatelessWidget {
|
class AppScaffold extends StatelessWidget {
|
||||||
final PreferredSizeWidget? appBar;
|
final PreferredSizeWidget? appBar;
|
||||||
|
final String? title;
|
||||||
final Widget? body;
|
final Widget? body;
|
||||||
final bool? showBottomNavigation;
|
final bool autoImplyAppBar;
|
||||||
const AppScaffold(
|
final bool showBottomNavigation;
|
||||||
{super.key, this.appBar, this.body, this.showBottomNavigation});
|
const AppScaffold({
|
||||||
|
super.key,
|
||||||
|
this.appBar,
|
||||||
|
this.title,
|
||||||
|
this.body,
|
||||||
|
this.autoImplyAppBar = false,
|
||||||
|
this.showBottomNavigation = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isShowBottomNavigation = (showBottomNavigation ?? false)
|
final isShowBottomNavigation = (showBottomNavigation)
|
||||||
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
final state = GoRouter.maybeOf(context);
|
||||||
|
|
||||||
return AppBackground(
|
return AppBackground(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: appBar,
|
appBar: appBar ??
|
||||||
|
(autoImplyAppBar
|
||||||
|
? AppBar(
|
||||||
|
title: title != null
|
||||||
|
? Text(title!)
|
||||||
|
: state != null
|
||||||
|
? Text(
|
||||||
|
('screen${state.routerDelegate.currentConfiguration.last.route.name?.capitalize()}')
|
||||||
|
.tr(),
|
||||||
|
)
|
||||||
|
: null)
|
||||||
|
: null),
|
||||||
body: body,
|
body: body,
|
||||||
bottomNavigationBar:
|
bottomNavigationBar:
|
||||||
isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
isShowBottomNavigation ? AppBottomNavigationBar() : null,
|
||||||
|
@ -6,9 +6,13 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <flutter_secure_storage/flutter_secure_storage_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStoragePlugin");
|
||||||
|
flutter_secure_storage_plugin_register_with_registrar(flutter_secure_storage_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
flutter_secure_storage
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
12
pubspec.lock
12
pubspec.lock
@ -416,6 +416,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.4.2"
|
||||||
|
flutter_secure_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage
|
||||||
|
sha256: "9f3dd2ac3b6875b0fde5b04734789c3ef35ba3965c18e99dd564a7a2f8056df6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.1"
|
||||||
flutter_shaders:
|
flutter_shaders:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -886,10 +894,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
|
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.3"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -58,6 +58,7 @@ dependencies:
|
|||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
relative_time: ^5.0.0
|
relative_time: ^5.0.0
|
||||||
|
flutter_secure_storage: ^4.2.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -87,6 +88,7 @@ flutter:
|
|||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
|
- assets/icon/icon.png
|
||||||
- assets/translations/
|
- assets/translations/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
Loading…
Reference in New Issue
Block a user