Compare commits

...

6 Commits

Author SHA1 Message Date
8bb62b5992 🚀 Launch 2.2.2+58 2025-01-28 21:15:11 +08:00
1e8a6dea5b 💄 Optimize attachment list 2025-01-28 20:39:34 +08:00
5c2804cc4d 💄 Optimize news design 2025-01-28 20:21:51 +08:00
0dbb8f132a Factor settings with TOTP, In app notify authenticate method 2025-01-28 19:55:35 +08:00
3395f3dbd0 Create auth factor 2025-01-28 00:52:44 +08:00
d258ba776e ♻️ Splitting up account page and settings 2025-01-27 20:14:02 +08:00
20 changed files with 783 additions and 339 deletions

View File

@ -17,6 +17,7 @@
"screenAccountProfileEdit": "Edit Profile", "screenAccountProfileEdit": "Edit Profile",
"screenAbuseReport": "Abuse Reports", "screenAbuseReport": "Abuse Reports",
"screenSettings": "Settings", "screenSettings": "Settings",
"screenAccountSettings": "Account Settings",
"screenNews": "News", "screenNews": "News",
"screenAlbum": "Album", "screenAlbum": "Album",
"screenChat": "Chat", "screenChat": "Chat",
@ -28,6 +29,7 @@
"screenNotification": "Notification", "screenNotification": "Notification",
"screenPostSearch": "Search Posts", "screenPostSearch": "Search Posts",
"screenFriend": "Friends", "screenFriend": "Friends",
"screenFactorSettings": "Auth Factors",
"dialogOkay": "Okay", "dialogOkay": "Okay",
"dialogCancel": "Cancel", "dialogCancel": "Cancel",
"dialogConfirm": "Confirm", "dialogConfirm": "Confirm",
@ -104,8 +106,18 @@
}, },
"loginEnterPassword": "Enter the code", "loginEnterPassword": "Enter the code",
"loginSuccess": "Logged in as {}", "loginSuccess": "Logged in as {}",
"authFactorDelete": "Delete Auth Factor",
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
"authFactorPassword": "Password", "authFactorPassword": "Password",
"authFactorPasswordDescription": "The password you set when you registered.",
"authFactorEmail": "Email verification code", "authFactorEmail": "Email verification code",
"authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
"authFactorTOTP": "Time-based OTP",
"authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
"authFactorInAppNotify": "In-app notification",
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
"authFactorAdd": "Add a factor",
"authFactorAddSubtitle": "Provide another way to login your account.",
"accountIntroTitle": "Hello there!", "accountIntroTitle": "Hello there!",
"accountIntroSubtitle": "Pick an option below to get started.", "accountIntroSubtitle": "Pick an option below to get started.",
"accountLogout": "Logout", "accountLogout": "Logout",
@ -114,8 +126,12 @@
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.", "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", "accountPublishers": "Your publishers",
"accountPublishersSubtitle": "Manage your publish identities.", "accountPublishersSubtitle": "Manage your publish identities.",
"accountSettings": "Account Settings",
"accountSettingsSubtitle": "Manage your account and make it yours.",
"accountProfileEdit": "Edit your profile", "accountProfileEdit": "Edit your profile",
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.", "accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
"factorSettings": "Auth Factors",
"factorSettingsSubtitle": "Manage your authentication factors.",
"accountProfileEditApplied": "Profile modification applied.", "accountProfileEditApplied": "Profile modification applied.",
"publishersNew": "New Publisher", "publishersNew": "New Publisher",
"publisherNewSubtitle": "Create a new publisher identity.", "publisherNewSubtitle": "Create a new publisher identity.",
@ -565,5 +581,8 @@
"newsReadingFromReader": "You're reading from HyperNet.Reader", "newsReadingFromReader": "You're reading from HyperNet.Reader",
"newsReadingFromOriginal": "You're reading the original article", "newsReadingFromOriginal": "You're reading the original article",
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.", "newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
"newsToday": "Today's News" "newsToday": "Today's News",
"totpPostSetup": "One More Thing",
"totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.",
"totpNeverShare": "Never share this QR Code"
} }

View File

@ -15,6 +15,7 @@
"screenAccountProfileEdit": "编辑资料", "screenAccountProfileEdit": "编辑资料",
"screenAbuseReport": "滥用检举", "screenAbuseReport": "滥用检举",
"screenSettings": "设置", "screenSettings": "设置",
"screenAccountSettings": "账号设置",
"screenNews": "新闻", "screenNews": "新闻",
"screenAlbum": "相册", "screenAlbum": "相册",
"screenChat": "聊天", "screenChat": "聊天",
@ -88,8 +89,18 @@
}, },
"loginEnterPassword": "验证代码", "loginEnterPassword": "验证代码",
"loginSuccess": "登录为 {}", "loginSuccess": "登录为 {}",
"authFactorDelete": "删除验证因子",
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
"authFactorPassword": "密码", "authFactorPassword": "密码",
"authFactorPasswordDescription": "注册时选择设置的密码。",
"authFactorEmail": "电邮一次性验证码", "authFactorEmail": "电邮一次性验证码",
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
"authFactorTOTP": "时序验证码",
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
"authFactorInAppNotify": "应用内通知验证码",
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
"authFactorAdd": "添加新验证因子",
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
"accountIntroTitle": "喜欢您来!", "accountIntroTitle": "喜欢您来!",
"accountIntroSubtitle": "登陆以探索更广大的世界。", "accountIntroSubtitle": "登陆以探索更广大的世界。",
"accountLogout": "退出登录", "accountLogout": "退出登录",
@ -98,8 +109,12 @@
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。", "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
"accountPublishers": "你的发布者", "accountPublishers": "你的发布者",
"accountPublishersSubtitle": "管理你的公共形象。", "accountPublishersSubtitle": "管理你的公共形象。",
"accountSettings": "帐号设置",
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
"accountProfileEdit": "编辑资料", "accountProfileEdit": "编辑资料",
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。", "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
"factorSettings": "验证因子",
"factorSettingsSubtitle": "管理你的登陆验证方式。",
"accountProfileEditApplied": "个人资料修改已被应用。", "accountProfileEditApplied": "个人资料修改已被应用。",
"publishersNew": "新发布者", "publishersNew": "新发布者",
"publisherNewSubtitle": "创建一个新的公共身份。", "publisherNewSubtitle": "创建一个新的公共身份。",
@ -563,5 +578,8 @@
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章", "newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
"newsReadingFromOriginal": "你正在阅读原始文章", "newsReadingFromOriginal": "你正在阅读原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。", "newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
"newsToday": "快讯" "newsToday": "快讯",
"totpPostSetup": "还有一件事",
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。",
"totpNeverShare": "永远不要分享这个 QR Code"
} }

View File

@ -53,14 +53,14 @@ PODS:
- Firebase/Messaging (11.6.0): - Firebase/Messaging (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.6.0) - FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.4.0): - firebase_analytics (11.4.1):
- Firebase/Analytics (= 11.6.0) - Firebase/Analytics (= 11.6.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_core (3.10.0): - firebase_core (3.10.1):
- Firebase/CoreOnly (= 11.6.0) - Firebase/CoreOnly (= 11.6.0)
- Flutter - Flutter
- firebase_messaging (15.2.0): - firebase_messaging (15.2.1):
- Firebase/Messaging (= 11.6.0) - Firebase/Messaging (= 11.6.0)
- firebase_core - firebase_core
- Flutter - Flutter
@ -382,9 +382,9 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e
firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10 firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b
firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2

View File

@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:surface/screens/abuse_report.dart'; import 'package:surface/screens/abuse_report.dart';
import 'package:surface/screens/account.dart'; import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/account_settings.dart';
import 'package:surface/screens/account/factor_settings.dart';
import 'package:surface/screens/account/profile_page.dart'; import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/screens/account/profile_edit.dart'; import 'package:surface/screens/account/profile_edit.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart'; import 'package:surface/screens/account/publishers/publisher_edit.dart';
@ -96,11 +98,47 @@ final _appRoutes = [
), ),
], ],
), ),
GoRoute( GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
path: '/account', GoRoute(
name: 'account', path: '/settings',
builder: (context, state) => const AccountScreen(), name: 'accountSettings',
), builder: (context, state) => AccountSettingsScreen(),
),
GoRoute(
path: '/settings/factors',
name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
GoRoute(
path: '/:name',
name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
),
),
]),
GoRoute( GoRoute(
path: '/chat', path: '/chat',
name: 'chat', name: 'chat',
@ -161,20 +199,15 @@ final _appRoutes = [
), ),
], ],
), ),
GoRoute( GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
path: '/news', GoRoute(
name: 'news', path: '/:hash',
builder: (context, state) => const NewsScreen(), name: 'newsDetail',
routes: [ builder: (context, state) => NewsDetailScreen(
GoRoute( hash: state.pathParameters['hash']!,
path: '/:hash',
name: 'newsDetail',
builder: (context, state) => NewsDetailScreen(
hash: state.pathParameters['hash']!,
),
), ),
] ),
), ]),
GoRoute( GoRoute(
path: '/album', path: '/album',
name: 'album', name: 'album',
@ -205,35 +238,6 @@ final _appRoutes = [
name: 'abuseReport', name: 'abuseReport',
builder: (context, state) => AbuseReportScreen(), builder: (context, state) => AbuseReportScreen(),
), ),
GoRoute(
path: '/account/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/account/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/account/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/account/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
GoRoute(
path: '/account/:name',
name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
),
),
GoRoute( GoRoute(
path: '/settings', path: '/settings',
name: 'settings', name: 'settings',

View File

@ -1,3 +1,5 @@
import 'dart:ui';
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:gap/gap.dart'; import 'package:gap/gap.dart';
@ -13,6 +15,7 @@ import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart';
class AccountScreen extends StatelessWidget { class AccountScreen extends StatelessWidget {
const AccountScreen({super.key}); const AccountScreen({super.key});
@ -20,11 +23,39 @@ class AccountScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenAccount").tr(), title: Text("screenAccount").tr(),
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
? Stack(
fit: StackFit.expand,
children: [
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
Positioned(
top: 0,
left: 0,
right: 0,
height: 56 + MediaQuery.of(context).padding.top,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
child: Container(
color: Colors.black.withOpacity(
clampDouble(10 * 0.1, 0, 0.5),
),
),
),
),
),
],
)
: null,
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.settings, fill: 1), icon: const Icon(Symbols.settings, fill: 1),
@ -83,16 +114,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
); );
}).padding(all: 20), }).padding(all: 20),
).padding(horizontal: 8, top: 16, bottom: 4), ).padding(horizontal: 8, top: 16, bottom: 4),
ListTile(
title: Text('accountProfileEdit').tr(),
subtitle: Text('accountProfileEditSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.contact_page),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountProfileEdit');
},
),
ListTile( ListTile(
title: Text('accountPublishers').tr(), title: Text('accountPublishers').tr(),
subtitle: Text('accountPublishersSubtitle').tr(), subtitle: Text('accountPublishersSubtitle').tr(),
@ -113,6 +134,26 @@ class _AuthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('abuseReport'); GoRouter.of(context).pushNamed('abuseReport');
}, },
), ),
ListTile(
title: Text('factorSettings').tr(),
subtitle: Text('factorSettingsSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.lock),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('factorSettings');
},
),
ListTile(
title: Text('accountSettings').tr(),
subtitle: Text('accountSettingsSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.manage_accounts),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountSettings');
},
),
ListTile( ListTile(
title: Text('accountLogout').tr(), title: Text('accountLogout').tr(),
subtitle: Text('accountLogoutSubtitle').tr(), subtitle: Text('accountLogoutSubtitle').tr(),
@ -134,33 +175,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
await Hive.initFlutter(); await Hive.initFlutter();
}, },
), ),
ListTile(
title: Text('accountDeletion'.tr()),
subtitle: Text('accountDeletionActionDescription'.tr()),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.person_cancel),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context
.showConfirmDialog(
'accountDeletion'.tr(),
'accountDeletionDescription'.tr(),
)
.then((value) {
if (!value || !context.mounted) return;
final sn = context.read<SnNetworkProvider>();
sn.client.post('/cgi/id/users/me/deletion').then((value) {
if (context.mounted) {
context.showSnackbar('accountDeletionSubmitted'.tr());
}
}).catchError((err) {
if (context.mounted) {
context.showErrorDialog(err);
}
});
});
},
),
], ],
); );
} }

View File

@ -0,0 +1,66 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountSettingsScreen extends StatelessWidget {
const AccountSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenAccountSettings').tr(),
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text('accountProfileEdit').tr(),
subtitle: Text('accountProfileEditSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.contact_page),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountProfileEdit');
},
),
ListTile(
title: Text('accountDeletion'.tr()),
subtitle: Text('accountDeletionActionDescription'.tr()),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.person_cancel),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context
.showConfirmDialog(
'accountDeletion'.tr(),
'accountDeletionDescription'.tr(),
)
.then((value) {
if (!value || !context.mounted) return;
final sn = context.read<SnNetworkProvider>();
sn.client.post('/cgi/id/users/me/deletion').then((value) {
if (context.mounted) {
context.showSnackbar('accountDeletionSubmitted'.tr());
}
}).catchError((err) {
if (context.mounted) {
context.showErrorDialog(err);
}
});
});
},
),
],
),
),
);
}
}

View File

@ -0,0 +1,294 @@
import 'package:dropdown_button2/dropdown_button2.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:qr_flutter/qr_flutter.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/auth.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
final Map<int, (String, String, IconData)> kFactorTypes = {
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
};
class FactorSettingsScreen extends StatefulWidget {
const FactorSettingsScreen({super.key});
@override
State<FactorSettingsScreen> createState() => _FactorSettingsScreenState();
}
class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
bool _isBusy = false;
List<SnAuthFactor>? _factors;
Future<void> _fetchFactors() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/factors');
_factors = List<SnAuthFactor>.from(
resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [],
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchFactors();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenFactorSettings').tr(),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LoadingIndicator(
isActive: _isBusy,
),
ListTile(
title: Text('authFactorAdd').tr(),
subtitle: Text('authFactorAddSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (context) => _FactorNewDialog(
currentlyHave: _factors!,
),
).then((val) {
if (val == true) _fetchFactors();
});
},
),
const Divider(height: 1),
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: _fetchFactors,
child: ListView.builder(
itemCount: _factors?.length ?? 0,
itemBuilder: (context, idx) {
final ele = _factors![idx];
return ListTile(
title: Text(kFactorTypes[ele.type]!.$1).tr(),
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 12),
leading: Icon(kFactorTypes[ele.type]!.$3),
trailing: IconButton(
icon: const Icon(Symbols.close),
onPressed: ele.type > 0
? () {
context
.showConfirmDialog(
'authFactorDelete'.tr(),
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
)
.then((val) async {
if (!val) return;
try {
if (!context.mounted) return;
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
_fetchFactors();
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
}
});
}
: null,
),
);
},
),
),
),
),
],
),
);
}
}
class _FactorNewDialog extends StatefulWidget {
final List<SnAuthFactor> currentlyHave;
const _FactorNewDialog({required this.currentlyHave});
@override
State<_FactorNewDialog> createState() => _FactorNewDialogState();
}
class _FactorNewDialogState extends State<_FactorNewDialog> {
int? _factorType;
bool _isBusy = false;
Future<void> _submit() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.post('/cgi/id/users/me/factors', data: {
'type': _factorType,
});
final factor = SnAuthFactor.fromJson(resp.data);
if (!mounted) return;
if (factor.type == 2) {
await showModalBottomSheet(
context: context,
builder: (context) => _FactorTotpFactorDialog(factor: factor),
);
}
if (!mounted) return;
Navigator.of(context).pop(true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('authFactorAdd').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButtonHideUnderline(
child: DropdownButton2<int>(
hint: Text(
'Select Item',
style: TextStyle(
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
value: _factorType,
items: kFactorTypes.entries.map(
(ele) {
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
return DropdownMenuItem<int>(
enabled: !contains,
value: ele.key,
child: Text(
ele.value.$1.tr(),
style: const TextStyle(
fontSize: 14,
),
).opacity(contains ? 0.75 : 1),
);
},
).toList(),
onChanged: (val) => setState(() {
_factorType = val;
}),
buttonStyleData: ButtonStyleData(
height: 50,
padding: const EdgeInsets.only(left: 14, right: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: Theme.of(context).dividerColor,
),
),
),
),
),
],
),
actions: [
TextButton(
onPressed: _isBusy ? null : () => Navigator.of(context).pop(),
child: Text('dialogCancel').tr(),
),
TextButton(
onPressed: _isBusy ? null : () => _submit(),
child: Text('dialogConfirm').tr(),
),
],
);
}
}
class _FactorTotpFactorDialog extends StatelessWidget {
final SnAuthFactor factor;
const _FactorTotpFactorDialog({super.key, required this.factor});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: Text(
'totpPostSetup',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge,
).tr().width(280),
),
const Gap(4),
Center(
child: Text(
'totpPostSetupDescription',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
).tr().width(280),
),
const Gap(16),
QrImageView(
padding: EdgeInsets.zero,
data: factor.config!['url'],
errorCorrectionLevel: QrErrorCorrectLevel.H,
version: QrVersions.auto,
size: 160,
gapless: true,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.circle,
color: Theme.of(context).colorScheme.onSurface,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Theme.of(context).colorScheme.onSurface,
),
),
const Gap(16),
Center(
child: Text(
'totpNeverShare',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
).tr().bold().width(280),
),
],
),
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/account/factor_settings.dart';
import 'package:surface/types/auth.dart'; import 'package:surface/types/auth.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
@ -14,11 +15,6 @@ import 'package:url_launcher/url_launcher_string.dart';
import '../../providers/websocket.dart'; import '../../providers/websocket.dart';
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
0: ('authFactorPassword'.tr(), Symbols.password, false),
1: ('authFactorEmail'.tr(), Symbols.email, true),
};
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@ -212,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
controller: _passwordController, controller: _passwordController,
obscureText: true, obscureText: true,
autofillHints: [ autofillHints: [
(_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode widget.factor!.type == 0
? AutofillHints.password
: AutofillHints.oneTimeCode
], ],
decoration: InputDecoration( decoration: InputDecoration(
isDense: true, isDense: true,
@ -267,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
bool _isBusy = false; bool _isBusy = false;
int? _factorPicked; int? _factorPicked;
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()); Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
void _performGetFactorCode() async { void _performGetFactorCode() async {
if (_factorPicked == null) return; if (_factorPicked == null) return;
@ -328,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
), ),
), ),
secondary: Icon( secondary: Icon(
_factorLabelMap[x.type]?.$2 ?? Symbols.question_mark, kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
), ),
title: Text( title: Text(
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(), kFactorTypes[x.type]?.$1 ?? 'unknown',
), ).tr(),
enabled: !widget.ticket!.factorTrail.contains(x.id), enabled: !widget.ticket!.factorTrail.contains(x.id),
value: _factorPicked == x.id, value: _factorPicked == x.id,
onChanged: (value) { onChanged: (value) {
@ -408,11 +407,13 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username'); final lookupResp =
await sn.client.get('/cgi/id/users/lookup?probe=$username');
await sn.client.post('/cgi/id/users/me/password-reset', data: { await sn.client.post('/cgi/id/users/me/password-reset', data: {
'user_id': lookupResp.data['id'], 'user_id': lookupResp.data['id'],
}); });
if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr()); if (mounted)
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
} catch (err) { } catch (err) {
if (mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
} finally { } finally {
@ -437,7 +438,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
widget.onTicket(result.ticket); widget.onTicket(result.ticket);
// Pull factors // Pull factors
final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: { final factorResp =
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
'ticketId': result.ticket!.id.toString(), 'ticketId': result.ticket!.id.toString(),
}); });
widget.onFactor( widget.onFactor(
@ -531,7 +533,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
'termAcceptNextWithAgree'.tr(), 'termAcceptNextWithAgree'.tr(),
textAlign: TextAlign.end, textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()), color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha((255 * 0.75).round()),
), ),
), ),
Material( Material(

View File

@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_detail.dart';
@ -96,6 +97,8 @@ class _ExploreScreenState extends State<ExploreScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
return AppScaffold( return AppScaffold(
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
@ -243,8 +246,10 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
openColor: Colors.transparent, openColor: Colors.transparent,
openElevation: 0, openElevation: 0,
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75),
transitionType: ContainerTransitionType.fade, transitionType: ContainerTransitionType.fade,
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedShape: const RoundedRectangleBorder( closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),

View File

@ -51,7 +51,7 @@ class HomeScreen extends StatefulWidget {
} }
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
static const List<HomeScreenDashEntry> kCards = [ late final List<HomeScreenDashEntry> kCards = [
HomeScreenDashEntry( HomeScreenDashEntry(
name: 'dashEntryRecommendation', name: 'dashEntryRecommendation',
child: _HomeDashRecommendationPostWidget(), child: _HomeDashRecommendationPostWidget(),
@ -69,7 +69,7 @@ class _HomeScreenState extends State<HomeScreen> {
HomeScreenDashEntry( HomeScreenDashEntry(
name: 'dashEntryTodayNews', name: 'dashEntryTodayNews',
child: _HomeDashTodayNews(), child: _HomeDashTodayNews(),
cols: 2, cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
), ),
]; ];
@ -293,7 +293,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
Text( Text(
_article!.title, _article!.title,
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18), style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
maxLines: 2, maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text( Text(

View File

@ -175,54 +175,57 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
), ),
if (_articleFragment != null && _isReadingFromReader) if (_articleFragment != null && _isReadingFromReader)
Expanded( Expanded(
child: SingleChildScrollView( child: Container(
child: Column( constraints: BoxConstraints(maxWidth: 640),
crossAxisAlignment: CrossAxisAlignment.start, child: SingleChildScrollView(
spacing: 8, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge), spacing: 8,
Builder(builder: (context) { children: [
final htmlDescription = parse(_article!.description); Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
return Text( Builder(builder: (context) {
htmlDescription.children.map((ele) => ele.text.trim()).join(), final htmlDescription = parse(_article!.description);
style: Theme.of(context).textTheme.bodyMedium, return Text(
); htmlDescription.children.map((ele) => ele.text.trim()).join(),
}), style: Theme.of(context).textTheme.bodyMedium,
Builder(builder: (context) { );
final date = _article!.publishedAt ?? _article!.createdAt; }),
return Row( Builder(builder: (context) {
spacing: 2, final date = _article!.publishedAt ?? _article!.createdAt;
children: [ return Row(
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), spacing: 2,
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(), children: [
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
], Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
).opacity(0.75); Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
}), ],
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75), ).opacity(0.75);
const Divider(), }),
..._parseHtmlToWidgets(_articleFragment!.children), Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
const Divider(), const Divider(),
InkWell( ..._parseHtmlToWidgets(_articleFragment!.children),
child: Row( const Divider(),
mainAxisSize: MainAxisSize.min, InkWell(
children: [ child: Row(
Text( mainAxisSize: MainAxisSize.min,
'Reference from original website', children: [
style: TextStyle(decoration: TextDecoration.underline), Text(
), 'Reference from original website',
const Gap(4), style: TextStyle(decoration: TextDecoration.underline),
Icon(Icons.launch, size: 16), ),
], const Gap(4),
).opacity(0.85), Icon(Icons.launch, size: 16),
onTap: () { ],
launchUrlString(_article!.url); ).opacity(0.85),
}, onTap: () {
), launchUrlString(_article!.url);
Gap(MediaQuery.of(context).padding.bottom), },
], ),
).padding(horizontal: 12, vertical: 16), Gap(MediaQuery.of(context).padding.bottom),
), ],
).padding(horizontal: 12, vertical: 16),
),
).center(),
) )
else if (_article != null) else if (_article != null)
Expanded( Expanded(

View File

@ -70,11 +70,16 @@ class _NewsScreenState extends State<NewsScreen> {
sliver: SliverAppBar( sliver: SliverAppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenNews').tr(), title: Text('screenNews').tr(),
floating: true,
snap: true,
bottom: TabBar( bottom: TabBar(
isScrollable: true, isScrollable: true,
tabs: [ tabs: [
Tab(child: Text('newsAllSources'.tr())), Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)),
for (final source in _sources!) Tab(child: Text(source.label)), for (final source in _sources!)
Tab(
child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor),
),
], ],
), ),
), ),
@ -146,80 +151,87 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
return MediaQuery.removePadding( return MediaQuery.removePadding(
context: context, context: context,
removeTop: true, removeTop: true,
child: RefreshIndicator( child: Center(
onRefresh: _fetchArticles, child: Container(
child: InfiniteList( constraints: BoxConstraints(maxWidth: 640),
isLoading: _isBusy, child: RefreshIndicator(
itemCount: _articles.length, onRefresh: _fetchArticles,
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!, child: InfiniteList(
onFetchData: () { isLoading: _isBusy,
_fetchArticles(); itemCount: _articles.length,
}, hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
itemBuilder: (context, index) { onFetchData: () {
final article = _articles[index]; _fetchArticles();
},
itemBuilder: (context, index) {
final article = _articles[index];
final baseUri = Uri.parse(article.url); final baseUri = Uri.parse(article.url);
final baseUrl = '${baseUri.scheme}://${baseUri.host}'; final baseUrl = '${baseUri.scheme}://${baseUri.host}';
final htmlDescription = parse(article.description); final htmlDescription = parse(article.description);
final date = article.publishedAt ?? article.createdAt; final date = article.publishedAt ?? article.createdAt;
return Card( return Card(
child: InkWell( child: InkWell(
radius: 8, radius: 8,
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'newsDetail', 'newsDetail',
pathParameters: {'hash': article.hash}, pathParameters: {'hash': article.hash},
); );
}, },
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg')) if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
ClipRRect( ClipRRect(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topRight: Radius.circular(8), topRight: Radius.circular(8),
topLeft: Radius.circular(8), topLeft: Radius.circular(8),
), ),
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: Container( child: Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
article.thumbnail.startsWith('http') ? article.thumbnail : '$baseUrl/${article.thumbnail}', article.thumbnail.startsWith('http')
? article.thumbnail
: '$baseUrl/${article.thumbnail}',
),
),
), ),
), ),
), const Gap(16),
), Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
const Gap(16), const Gap(8),
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16), Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
const Gap(8), .textStyle(Theme.of(context).textTheme.bodyMedium!)
Text(htmlDescription.children.map((ele) => ele.text.trim()).join()) .padding(horizontal: 16),
.textStyle(Theme.of(context).textTheme.bodyMedium!) const Gap(8),
.padding(horizontal: 16), Row(
const Gap(8), spacing: 2,
Row( children: [
spacing: 2, Text(widget.allSources.where((x) => x.id == article.source).first.label)
children: [ .textStyle(Theme.of(context).textTheme.bodySmall!),
Text(widget.allSources.where((x) => x.id == article.source).first.label) ],
.textStyle(Theme.of(context).textTheme.bodySmall!), ).opacity(0.75).padding(horizontal: 16),
Row(
spacing: 2,
children: [
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75).padding(horizontal: 16),
const Gap(16),
], ],
).opacity(0.75).padding(horizontal: 16), ),
Row( ),
spacing: 2, );
children: [ },
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), ),
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(), ),
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75).padding(horizontal: 16),
const Gap(16),
],
),
),
);
},
), ),
), ),
); );

View File

@ -15,8 +15,8 @@ class SnAccount with _$SnAccount {
required DateTime? deletedAt, required DateTime? deletedAt,
required DateTime? confirmedAt, required DateTime? confirmedAt,
required List<SnAccountContact>? contacts, required List<SnAccountContact>? contacts,
required String avatar, @Default("") String avatar,
required String banner, @Default("") String banner,
required String description, required String description,
required String name, required String name,
required String nick, required String nick,

View File

@ -367,8 +367,8 @@ class _$SnAccountImpl extends _SnAccount {
required this.deletedAt, required this.deletedAt,
required this.confirmedAt, required this.confirmedAt,
required final List<SnAccountContact>? contacts, required final List<SnAccountContact>? contacts,
required this.avatar, this.avatar = "",
required this.banner, this.banner = "",
required this.description, required this.description,
required this.name, required this.name,
required this.nick, required this.nick,
@ -410,8 +410,10 @@ class _$SnAccountImpl extends _SnAccount {
} }
@override @override
@JsonKey()
final String avatar; final String avatar;
@override @override
@JsonKey()
final String banner; final String banner;
@override @override
final String description; final String description;
@ -540,8 +542,8 @@ abstract class _SnAccount extends SnAccount {
required final DateTime? deletedAt, required final DateTime? deletedAt,
required final DateTime? confirmedAt, required final DateTime? confirmedAt,
required final List<SnAccountContact>? contacts, required final List<SnAccountContact>? contacts,
required final String avatar, final String avatar,
required final String banner, final String banner,
required final String description, required final String description,
required final String name, required final String name,
required final String nick, required final String nick,

View File

@ -20,8 +20,8 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
contacts: (json['contacts'] as List<dynamic>?) contacts: (json['contacts'] as List<dynamic>?)
?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>)) ?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
avatar: json['avatar'] as String, avatar: json['avatar'] as String? ?? "",
banner: json['banner'] as String, banner: json['banner'] as String? ?? "",
description: json['description'] as String, description: json['description'] as String,
name: json['name'] as String, name: json['name'] as String,
nick: json['nick'] as String, nick: json['nick'] as String,

View File

@ -196,68 +196,71 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
return Container( return AspectRatio(
constraints: BoxConstraints(maxHeight: constraints.maxHeight), aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
child: ScrollConfiguration( child: Container(
behavior: _AttachmentListScrollBehavior(), constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: ListView.separated( child: ScrollConfiguration(
padding: widget.padding, behavior: _AttachmentListScrollBehavior(),
shrinkWrap: true, child: ListView.separated(
itemCount: widget.data.length, padding: widget.padding,
itemBuilder: (context, idx) { shrinkWrap: true,
return Container( itemCount: widget.data.length,
constraints: constraints.copyWith(maxWidth: widget.maxWidth), itemBuilder: (context, idx) {
child: AspectRatio( return Container(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), constraints: constraints.copyWith(maxWidth: widget.maxWidth),
child: GestureDetector( child: AspectRatio(
onTap: () { aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
if (widget.data[idx]?.mediaType != SnMediaType.image) return; child: GestureDetector(
context.pushTransparentRoute( onTap: () {
AttachmentZoomView( if (widget.data[idx]?.mediaType != SnMediaType.image) return;
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), context.pushTransparentRoute(
initialIndex: idx, AttachmentZoomView(
heroTags: heroTags, data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
), initialIndex: idx,
backgroundColor: Colors.black.withOpacity(0.7), heroTags: heroTags,
rootNavigator: true,
);
},
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
), ),
child: ClipRRect( backgroundColor: Colors.black.withOpacity(0.7),
borderRadius: AttachmentList.kDefaultRadius, rootNavigator: true,
child: AttachmentItem( );
data: widget.data[idx], },
heroTag: heroTags[idx], child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: widget.data[idx],
heroTag: heroTags[idx],
),
), ),
), ),
), Positioned(
Positioned( right: 8,
right: 8, bottom: 8,
bottom: 8, child: Chip(
child: Chip( label: Text('${idx + 1}/${widget.data.length}'),
label: Text('${idx + 1}/${widget.data.length}'), ),
), ),
), ],
], ),
), ),
), ),
), );
); },
}, separatorBuilder: (context, index) => const Gap(8),
separatorBuilder: (context, index) => const Gap(8), physics: const BouncingScrollPhysics(),
physics: const BouncingScrollPhysics(), scrollDirection: Axis.horizontal,
scrollDirection: Axis.horizontal, ),
), ),
), ),
); );

View File

@ -5,10 +5,9 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:surface/providers/config.dart';
// Keep this import to make the web image render work // Keep this import to make the web image render work
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:surface/providers/config.dart';
class UniversalImage extends StatelessWidget { class UniversalImage extends StatelessWidget {
final String url; final String url;

View File

@ -22,14 +22,14 @@ PODS:
- Firebase/Messaging (11.6.0): - Firebase/Messaging (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.6.0) - FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.4.0): - firebase_analytics (11.4.1):
- Firebase/Analytics (= 11.6.0) - Firebase/Analytics (= 11.6.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- firebase_core (3.10.0): - firebase_core (3.10.1):
- Firebase/CoreOnly (~> 11.6.0) - Firebase/CoreOnly (~> 11.6.0)
- FlutterMacOS - FlutterMacOS
- firebase_messaging (15.2.0): - firebase_messaging (15.2.1):
- Firebase/CoreOnly (~> 11.6.0) - Firebase/CoreOnly (~> 11.6.0)
- Firebase/Messaging (~> 11.6.0) - Firebase/Messaging (~> 11.6.0)
- firebase_core - firebase_core
@ -296,9 +296,9 @@ SPEC CHECKSUMS:
file_saver: 44e6fbf666677faf097302460e214e977fdd977b file_saver: 44e6fbf666677faf097302460e214e977fdd977b
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6
firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd
firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226 firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae" sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.49" version: "1.3.50"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
@ -338,10 +338,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.7" version: "2.3.8"
dart_webrtc: dart_webrtc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -418,10 +418,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: easy_localization name: easy_localization
sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7+1"
easy_localization_loader: easy_localization_loader:
dependency: "direct main" dependency: "direct main"
description: description:
@ -538,34 +538,34 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_analytics name: firebase_analytics
sha256: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea" sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.4.0" version: "11.4.1"
firebase_analytics_platform_interface: firebase_analytics_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_platform_interface name: firebase_analytics_platform_interface
sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9 sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.0" version: "4.3.1"
firebase_analytics_web: firebase_analytics_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_web name: firebase_analytics_web
sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f" sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.10+6" version: "0.5.10+7"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568" sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.10.0" version: "3.10.1"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -586,26 +586,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c" sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.2.0" version: "15.2.1"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd" sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.6.0" version: "4.6.1"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77" sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.10.0" version: "3.10.1"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -830,10 +830,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712" sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.6" version: "0.12.7"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -878,18 +878,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: glob name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
go_router: go_router:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" sha256: daf3ff5570f55396b2d2c9bf8136d7db3a8acf208ac0cef92a3ae2beb9a81550
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.6.3" version: "14.7.1"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@ -950,10 +950,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.2" version: "1.3.0"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -1030,10 +1030,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_macos name: image_picker_macos
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1+1" version: "0.2.1+2"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1710,18 +1710,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a sha256: c59819dacc6669a1165d54d2735a9543f136f9b3cec94ca65cea6ab8dffc422e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" version: "2.4.0"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5" sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.4"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
@ -2171,10 +2171,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: web_socket_channel name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
webrtc_interface: webrtc_interface:
dependency: transitive dependency: transitive
description: description:
@ -2187,10 +2187,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.10.0" version: "5.10.1"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.2.2+57 version: 2.2.2+58
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4