diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 8381f11..64df944 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -35,5 +35,13 @@ "loginEnterPassword": "Enter the code", "loginSuccess": "Logged in as {}", "authFactorPassword": "Password", - "authFactorEmail": "Email verification code" + "authFactorEmail": "Email verification code", + "accountIntroTitle": "Hello there!", + "accountIntroSubtitle": "Pick an option below to get started.", + "accountLogout": "Logout", + "accountLogoutSubtitle": "Log out of the current account.", + "accountLogoutConfirmTitle": "Are you sure you want to logout?", + "accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.", + "accountPublishers": "Your publishers", + "accountPublishersSubtitle": "Manage your publish identities." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index d2da117..74cbce6 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -35,5 +35,13 @@ "loginEnterPassword": "验证代码", "loginSuccess": "登录为 {}", "authFactorPassword": "密码", - "authFactorEmail": "电邮一次性验证码" + "authFactorEmail": "电邮一次性验证码", + "accountIntroTitle": "喜欢您来!", + "accountIntroSubtitle": "登陆以探索更广大的世界。", + "accountLogout": "退出登录", + "accountLogoutSubtitle": "注销当前账户的登陆状态。", + "accountLogoutConfirmTitle": "您确定要退出登录吗?", + "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。", + "accountPublishers": "你的发布者", + "accountPublishersSubtitle": "管理你的公共形象。" } diff --git a/lib/providers/sn_attachment.dart b/lib/providers/sn_attachment.dart index 04161a9..8a739e1 100644 --- a/lib/providers/sn_attachment.dart +++ b/lib/providers/sn_attachment.dart @@ -4,12 +4,11 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; class SnAttachmentProvider { - late final SnNetworkProvider sn; - + late final SnNetworkProvider _sn; final Map _cache = {}; SnAttachmentProvider(BuildContext context) { - sn = context.read(); + _sn = context.read(); } Future getOne(String rid, {noCache = false}) async { @@ -17,7 +16,7 @@ class SnAttachmentProvider { return _cache[rid]!; } - final resp = await sn.client.get('/cgi/uc/attachments/$rid/meta'); + final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta'); final out = SnAttachment.fromJson(resp.data); _cache[rid] = out; @@ -33,7 +32,7 @@ class SnAttachmentProvider { return rids.map((rid) => _cache[rid]!).toList(); } - final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: { + final resp = await _sn.client.get('/cgi/uc/attachments', queryParameters: { 'take': pendingFetch.length, 'id': pendingFetch.join(','), }); diff --git a/lib/providers/sn_network.dart b/lib/providers/sn_network.dart index 8eed0e1..9dd4557 100644 --- a/lib/providers/sn_network.dart +++ b/lib/providers/sn_network.dart @@ -67,7 +67,7 @@ class SnNetworkProvider { final b64 = utf8.fuse(base64Url); final payload = b64.decode(rawPayload); final exp = jsonDecode(payload)['exp']; - if (exp >= DateTime.now().millisecondsSinceEpoch) { + if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) { log('Access token need refresh, doing it at ${DateTime.now()}'); atk = await refreshToken(); } @@ -78,6 +78,8 @@ class SnNetworkProvider { log('Access token refresh failed...'); } } + } catch (err) { + log('Failed to authenticate user: $err'); } finally { handler.next(options); } @@ -114,7 +116,12 @@ class SnNetworkProvider { final rtk = await _storage.read(key: kRtkStoreKey); if (rtk == null) return null; - final resp = await client.post('/cgi/id/auth/token', data: { + final dio = Dio(); + dio.options.baseUrl = kUseLocalNetwork + ? 'http://localhost:8001' + : 'https://api.sn.solsynth.dev'; + + final resp = await dio.post('/cgi/id/auth/token', data: { 'grant_type': 'refresh_token', 'refresh_token': rtk, }); diff --git a/lib/providers/userinfo.dart b/lib/providers/userinfo.dart index 4b37797..5c3d6f8 100644 --- a/lib/providers/userinfo.dart +++ b/lib/providers/userinfo.dart @@ -10,12 +10,11 @@ class UserProvider extends ChangeNotifier { bool isAuthorized = false; SnAccount? user; - late final SnNetworkProvider sn; - + late final SnNetworkProvider _sn; late final FlutterSecureStorage _storage = FlutterSecureStorage(); UserProvider(BuildContext context) { - sn = context.read(); + _sn = context.read(); _storage.read(key: kAtkStoreKey).then((value) { isAuthorized = value != null; @@ -31,7 +30,7 @@ class UserProvider extends ChangeNotifier { Future refreshUser() async { if (!isAuthorized) return null; - final resp = await sn.client.get('/cgi/id/users/me'); + final resp = await _sn.client.get('/cgi/id/users/me'); final out = SnAccount.fromJson(resp.data); user = out; @@ -39,4 +38,11 @@ class UserProvider extends ChangeNotifier { return out; } + + void logoutUser() async { + _sn.clearTokenPair(); + isAuthorized = false; + user = null; + notifyListeners(); + } } diff --git a/lib/screens/account.dart b/lib/screens/account.dart index b0ae87a..99914be 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -1,44 +1,153 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/userinfo.dart'; +import 'package:surface/widgets/account/account_image.dart'; +import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; -class AccountScreen extends StatefulWidget { +class AccountScreen extends StatelessWidget { const AccountScreen({super.key}); - @override - State createState() => _AccountScreenState(); -} - -class _AccountScreenState extends State { @override Widget build(BuildContext context) { + final ua = context.watch(); + return AppScaffold( appBar: AppBar( 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'); - }, - ) - ], + body: SingleChildScrollView( + child: ua.isAuthorized + ? _AuthorizedAccountScreen() + : _UnauthorizedAccountScreen(), ), ); } } + +class _AuthorizedAccountScreen extends StatelessWidget { + const _AuthorizedAccountScreen({super.key}); + + @override + Widget build(BuildContext context) { + final ua = context.watch(); + + return Column( + children: [ + Card( + child: Builder(builder: (context) { + if (ua.user == null) { + return SizedBox( + width: double.infinity, + height: 120, + child: CircularProgressIndicator().center(), + ); + } + + return SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AccountImage(content: ua.user!.avatar, radius: 28), + const Gap(8), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text(ua.user!.nick) + .textStyle(Theme.of(context).textTheme.titleLarge!), + const Gap(4), + Text('@${ua.user!.name}') + .textStyle(Theme.of(context).textTheme.bodySmall!), + ], + ), + Text(ua.user!.description) + .textStyle(Theme.of(context).textTheme.bodyMedium!), + ], + ), + ); + }).padding(all: 20), + ).padding(horizontal: 8, top: 16, bottom: 4), + ListTile( + title: Text('accountPublishers').tr(), + subtitle: Text('accountPublishersSubtitle').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.face), + trailing: const Icon(Icons.chevron_right), + onTap: () {}, + ), + ListTile( + title: Text('accountLogout').tr(), + subtitle: Text('accountLogoutSubtitle').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.logout), + trailing: const Icon(Icons.chevron_right), + onTap: () { + context + .showConfirmDialog( + 'accountLogoutConfirmTitle'.tr(), + 'accountLogoutConfirm'.tr(), + ) + .then((value) { + if (value) ua.logoutUser(); + }); + }, + ), + ], + ); + } +} + +class _UnauthorizedAccountScreen extends StatelessWidget { + const _UnauthorizedAccountScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Card( + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Symbols.waving_hand, size: 32), + const Gap(8), + Text('accountIntroTitle') + .tr() + .textStyle(Theme.of(context).textTheme.titleLarge!), + Text('accountIntroSubtitle').tr(), + ], + ).padding(all: 20), + ), + ).padding(horizontal: 8, top: 16, bottom: 4), + ListTile( + title: Text('screenAuthLogin').tr(), + subtitle: Text('screenAuthLoginSubtitle').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.login), + 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), + leading: const Icon(Symbols.person_add), + trailing: const Icon(Icons.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed('authRegister'); + }, + ), + ], + ); + } +}