Compare commits

...

4 Commits

Author SHA1 Message Date
9cc577adbe Programs, members
🐛 Fix web assets redirecting issue
2025-03-23 22:34:58 +08:00
dd196b7754 Golden points 2025-03-23 18:23:18 +08:00
16c07c2133 🐛 Fix deps 2025-03-23 17:01:21 +08:00
6bcb658d44 🐛 Fix platform specific captcha solution cause build failed. 2025-03-23 16:47:06 +08:00
22 changed files with 1412 additions and 210 deletions

View File

@ -897,5 +897,23 @@
"albumDescription": "View albums and manage attachments.",
"stickers": "Stickers",
"stickersDescription": "View sticker packs and manage stickers.",
"navBottomUnauthorizedCaption": "Or create an account"
"navBottomUnauthorizedCaption": "Or create an account",
"walletCurrencyGoldenShort": "GDP",
"walletCurrencyGolden": {
"one": "{} Golden Point",
"other": "{} Golden Points"
},
"walletTransactionTypeNormal": "Source Point",
"walletTransactionTypeGolden": "Golden Point",
"accountProgram": "Programs",
"accountProgramDescription": "Explore the available member programs.",
"accountProgramJoin": "Join Program",
"accountProgramJoinRequirements": "Requirements",
"accountProgramJoinPricing": "Pricing",
"accountProgramJoinPricingHint": "Billed every (30 days) month.",
"accountProgramLeaveHint": "After leaving the program, the source points will not be refunded.",
"accountProgramJoined": "Joined Program.",
"accountProgramAlreadyJoined": "Joined",
"accountProgramLeft": "Left Program.",
"leave": "Leave"
}

View File

@ -895,5 +895,23 @@
"albumDescription": "查看相册与管理上传附件。",
"stickers": "贴图",
"stickersDescription": "查看贴图包与管理贴图。",
"navBottomUnauthorizedCaption": "或者注册一个账号"
"navBottomUnauthorizedCaption": "或者注册一个账号",
"walletCurrencyGoldenShort": "金点",
"walletCurrencyGolden": {
"one": "{} 金点",
"other": "{} 金点"
},
"walletTransactionTypeNormal": "源点",
"walletTransactionTypeGolden": "金点",
"accountProgram": "计划",
"accountProgramDescription": "了解可用的成员计划。",
"accountProgramJoin": "加入计划",
"accountProgramJoinRequirements": "要求",
"accountProgramJoinPricing": "价格",
"accountProgramJoinPricingHint": "按月30 天)收费",
"accountProgramLeaveHint": "离开计划后,之前花费的源点不会退款。",
"accountProgramJoined": "已加入计划。",
"accountProgramLeft": "已离开计划。",
"accountProgramAlreadyJoined": "已加入",
"leave": "离开"
}

View File

@ -13,6 +13,7 @@ import 'package:surface/screens/account/prefs/notify.dart';
import 'package:surface/screens/account/prefs/security.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/screens/account/profile_edit.dart';
import 'package:surface/screens/account/programs.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart';
import 'package:surface/screens/account/publishers/publisher_new.dart';
import 'package:surface/screens/account/publishers/publishers.dart';
@ -130,6 +131,11 @@ final _appRoutes = [
name: 'account',
builder: (context, state) => const AccountScreen(),
routes: [
GoRoute(
path: '/programs',
name: 'accountProgram',
builder: (context, state) => const AccountProgramScreen(),
),
GoRoute(
path: '/contacts',
name: 'accountContactMethods',

View File

@ -145,6 +145,16 @@ class _AuthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('accountPublishers');
},
),
ListTile(
title: Text('accountProgram').tr(),
subtitle: Text('accountProgramDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.communities),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountProgram');
},
),
ListTile(
title: Text('friends').tr(),
subtitle: Text('friendsDescription').tr(),

View File

@ -0,0 +1,284 @@
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/experience.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountProgramScreen extends StatefulWidget {
const AccountProgramScreen({super.key});
@override
State<AccountProgramScreen> createState() => _AccountProgramScreenState();
}
class _AccountProgramScreenState extends State<AccountProgramScreen> {
bool _isBusy = false;
final List<SnProgram> _programs = List.empty(growable: true);
final List<SnProgramMember> _programMembers = List.empty(growable: true);
Future<void> _fetchPrograms() async {
_programs.clear();
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/programs');
_programs.addAll(
resp.data.map((ele) => SnProgram.fromJson(ele)).cast<SnProgram>(),
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _fetchProgramMembers() async {
_programMembers.clear();
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/programs/members');
_programMembers.addAll(
resp.data
.map((ele) => SnProgramMember.fromJson(ele))
.cast<SnProgramMember>(),
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchPrograms();
_fetchProgramMembers();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
title: Text('accountProgram').tr(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: _programs.length,
itemBuilder: (context, idx) {
final ele = _programs[idx];
return Card(
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(8)),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => _ProgramJoinPopup(
program: ele,
isJoined: _programMembers
.any((ele) => ele.programId == ele.id),
),
).then((value) {
_fetchProgramMembers();
});
},
child: Column(
children: [
if (ele.appearance['banner'] != null)
AspectRatio(
aspectRatio: 16 / 5,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context)
.colorScheme
.surfaceVariant,
child: Image.network(
ele.appearance['banner'],
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ele.name,
style: Theme.of(context)
.textTheme
.titleMedium,
).bold(),
Text(
ele.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
if (_programMembers
.any((ele) => ele.programId == ele.id))
Text('accountProgramAlreadyJoined'.tr())
.opacity(0.75),
],
),
),
],
),
),
],
),
),
).padding(horizontal: 8);
},
),
),
],
),
);
}
}
class _ProgramJoinPopup extends StatefulWidget {
final SnProgram program;
final bool isJoined;
const _ProgramJoinPopup({required this.program, required this.isJoined});
@override
State<_ProgramJoinPopup> createState() => _ProgramJoinPopupState();
}
class _ProgramJoinPopupState extends State<_ProgramJoinPopup> {
bool _isBusy = false;
Future<void> _joinProgram() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/programs/${widget.program.id}');
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('accountProgramJoined'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _leaveProgram() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/id/programs/${widget.program.id}');
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('accountProgramLeft'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.add, size: 24),
const Gap(16),
Text(
'accountProgramJoin',
style: Theme.of(context).textTheme.titleLarge,
).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.program.appearance['banner'] != null)
AspectRatio(
aspectRatio: 16 / 5,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Image.network(
widget.program.appearance['banner'],
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
).padding(bottom: 12),
Text(
widget.program.name,
style: Theme.of(context).textTheme.titleMedium,
).bold(),
Text(
widget.program.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const Gap(8),
Text(
'accountProgramJoinRequirements',
style: Theme.of(context).textTheme.titleMedium,
).tr().bold(),
Text('≥EXP ${widget.program.expRequirement}'),
Text('≥Lv${getLevelFromExp(widget.program.expRequirement)}'),
const Gap(8),
Text(
'accountProgramJoinPricing',
style: Theme.of(context).textTheme.titleMedium,
).tr().bold(),
Text('walletCurrency${widget.program.price['currency'].toString().capitalize().replaceFirst('Normal', '')}')
.plural(widget.program.price['amount'].toDouble()),
Text('accountProgramJoinPricingHint').tr().opacity(0.75),
const Gap(8),
if (widget.isJoined)
Text('accountProgramLeaveHint')
.tr()
.opacity(0.75)
.padding(bottom: 8),
if (!widget.isJoined)
ElevatedButton(
onPressed: _isBusy ? null : _joinProgram,
child: Text('join').tr(),
)
else
ElevatedButton(
onPressed: _isBusy ? null : _leaveProgram,
child: Text('leave').tr(),
),
],
).padding(horizontal: 24),
],
);
}
}

View File

@ -7,7 +7,7 @@ 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/screens/captcha.dart';
import 'package:surface/screens/captcha/captcha.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart';

View File

@ -0,0 +1,3 @@
import 'package:flutter/foundation.dart' show kIsWeb;
export 'captcha_native.dart' if (kIsWeb) 'captcha_web.dart';

View File

@ -0,0 +1,37 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class CaptchaScreen extends StatefulWidget {
const CaptchaScreen({super.key});
@override
State<CaptchaScreen> createState() => _CaptchaScreenState();
}
class _CaptchaScreenState extends State<CaptchaScreen> {
@override
Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
return AppScaffold(
appBar: AppBar(title: Text("reCaptcha").tr()),
body: InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri('${cfg.serverUrl}/captcha?redirect_uri=solink://captcha'),
),
shouldOverrideUrlLoading: (controller, navigationAction) async {
Uri? url = navigationAction.request.url;
if (url != null && url.queryParameters.containsKey('captcha_tk')) {
Navigator.pop(context, url.queryParameters['captcha_tk']!);
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
},
),
);
}
}

View File

@ -1,17 +1,13 @@
import 'dart:html' as html;
import 'dart:ui_web' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class CaptchaScreen extends StatefulWidget {
const CaptchaScreen({
super.key,
});
const CaptchaScreen({super.key});
@override
State<CaptchaScreen> createState() => _CaptchaScreenState();
@ -21,9 +17,7 @@ class _CaptchaScreenState extends State<CaptchaScreen> {
@override
void initState() {
super.initState();
if (kIsWeb) {
_setupWebListener();
}
_setupWebListener();
}
void _setupWebListener() {
@ -37,9 +31,8 @@ class _CaptchaScreenState extends State<CaptchaScreen> {
}
});
// Create an iframe for the captcha page
final iframe = html.IFrameElement()
..src = '${context.read<ConfigProvider>().serverUrl}/captcha?redirect_uri=solink://captcha'
..src = '${context.read<ConfigProvider>().serverUrl}/captcha?redirect_uri=web'
..style.border = 'none'
..width = '100%'
..height = '100%';
@ -47,36 +40,15 @@ class _CaptchaScreenState extends State<CaptchaScreen> {
html.document.body!.append(iframe);
ui.platformViewRegistry.registerViewFactory(
'captcha-iframe',
(int viewId) => iframe,
(int viewId) => iframe,
);
}
@override
Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
if (kIsWeb) {
return AppScaffold(
appBar: AppBar(title: Text("reCaptcha").tr()),
body: HtmlElementView(viewType: 'captcha-iframe'),
);
}
return AppScaffold(
appBar: AppBar(title: Text("reCaptcha").tr()),
body: InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri('${cfg.serverUrl}/captcha?redirect_uri=solink://captcha'),
),
shouldOverrideUrlLoading: (controller, navigationAction) async {
Uri? url = navigationAction.request.url;
if (url != null && url.queryParameters.containsKey('captcha_tk')) {
Navigator.pop(context, url.queryParameters['captcha_tk']!);
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
},
),
body: HtmlElementView(viewType: 'captcha-iframe'),
);
}
}
}

View File

@ -46,9 +46,7 @@ class _FriendScreenState extends State<FriendScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
_relations = List<SnRelationship>.from(
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
);
_relations = List<SnRelationship>.from(resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -66,9 +64,7 @@ class _FriendScreenState extends State<FriendScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/relations?status=0,3');
_requests = List<SnRelationship>.from(
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
);
_requests = List<SnRelationship>.from(resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -86,9 +82,7 @@ class _FriendScreenState extends State<FriendScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/relations?status=2');
_blocks = List<SnRelationship>.from(
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
);
_blocks = List<SnRelationship>.from(resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -104,11 +98,7 @@ class _FriendScreenState extends State<FriendScreen> {
try {
final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship(
relation.relatedId,
dstStatus,
relation.permNodes,
);
await rel.updateRelationship(relation.relatedId, dstStatus, relation.permNodes);
if (!mounted) return;
_fetchRelations();
} catch (err) {
@ -122,9 +112,7 @@ class _FriendScreenState extends State<FriendScreen> {
Future<void> _deleteRelation(SnRelationship relation) async {
final confirm = await context.showConfirmDialog(
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
'friendDeleteDescription'.tr(args: [
relation.related?.nick ?? 'unknown'.tr(),
]),
'friendDeleteDescription'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
);
if (!confirm) return;
if (!mounted) return;
@ -145,10 +133,9 @@ class _FriendScreenState extends State<FriendScreen> {
}
void _showRequests() {
showModalBottomSheet(
context: context,
builder: (context) => _FriendshipListWidget(relations: _requests),
).then((value) {
showModalBottomSheet(context: context, builder: (context) => _FriendshipListWidget(relations: _requests)).then((
value,
) {
if (value != null) {
_fetchRequests();
_fetchRelations();
@ -157,10 +144,9 @@ class _FriendScreenState extends State<FriendScreen> {
}
void _showBlocks() {
showModalBottomSheet(
context: context,
builder: (context) => _FriendshipListWidget(relations: _blocks),
).then((value) {
showModalBottomSheet(context: context, builder: (context) => _FriendshipListWidget(relations: _blocks)).then((
value,
) {
if (value != null) {
_fetchBlocks();
_fetchRelations();
@ -173,9 +159,7 @@ class _FriendScreenState extends State<FriendScreen> {
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/users/me/relations', data: {
'related': user.name,
});
await sn.client.post('/cgi/id/users/me/relations', data: {'related': user.name});
if (!mounted) return;
context.showSnackbar('friendRequestSent'.tr());
} catch (err) {
@ -200,29 +184,19 @@ class _FriendScreenState extends State<FriendScreen> {
if (!ua.isAuthorized) {
return AppScaffold(
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenFriend').tr(),
),
body: Center(
child: UnauthorizedHint(),
),
appBar: AppBar(leading: PageBackButton(), title: Text('screenFriend').tr()),
body: Center(child: UnauthorizedHint()),
);
}
return AppScaffold(
appBar: AppBar(
leading: AutoAppBarLeading(),
title: Text('screenFriend').tr(),
),
appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenFriend').tr()),
floatingActionButton: FloatingActionButton(
child: const Icon(Symbols.add),
onPressed: () async {
final user = await showModalBottomSheet<SnAccount?>(
context: context,
builder: (context) => AccountSelect(
title: 'friendNew'.tr(),
),
builder: (context) => AccountSelect(title: 'friendNew'.tr()),
);
if (!mounted) return;
if (user == null) return;
@ -235,9 +209,7 @@ class _FriendScreenState extends State<FriendScreen> {
if (_requests.isNotEmpty)
ListTile(
title: Text('friendRequests').tr(),
subtitle: Text(
'friendRequestsDescription',
).plural(_requests.length),
subtitle: Text('friendRequestsDescription').plural(_requests.length),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.group_add),
trailing: const Icon(Symbols.chevron_right),
@ -246,25 +218,19 @@ class _FriendScreenState extends State<FriendScreen> {
if (_blocks.isNotEmpty)
ListTile(
title: Text('friendBlocklist').tr(),
subtitle: Text(
'friendBlocklistDescription',
).plural(_blocks.length),
subtitle: Text('friendBlocklistDescription').plural(_blocks.length),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.block),
trailing: const Icon(Symbols.chevron_right),
onTap: _showBlocks,
),
if (_requests.isNotEmpty || _blocks.isNotEmpty)
const Divider(height: 1),
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: () => Future.wait([
_fetchRelations(),
_fetchRequests(),
]),
onRefresh: () => Future.wait([_fetchRelations(), _fetchRequests()]),
child: ListView.builder(
itemCount: _relations.length,
itemBuilder: (context, index) {
@ -288,16 +254,12 @@ class _FriendScreenState extends State<FriendScreen> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
onTap: _isUpdating
? null
: () => _changeRelation(relation, 2),
onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
child: Text('friendBlock').tr(),
),
const Gap(8),
InkWell(
onTap: _isUpdating
? null
: () => _deleteRelation(relation),
onTap: _isUpdating ? null : () => _deleteRelation(relation),
child: Text('friendDeleteAction').tr(),
),
],
@ -366,11 +328,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
try {
final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship(
relation.relatedId,
dstStatus,
relation.permNodes,
);
await rel.updateRelationship(relation.relatedId, dstStatus, relation.permNodes);
if (!mounted) return;
Navigator.pop(context, true);
} catch (err) {
@ -384,9 +342,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
Future<void> _deleteRelation(SnRelationship relation) async {
final confirm = await context.showConfirmDialog(
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
'friendDeleteDescription'.tr(args: [
relation.related?.nick ?? 'unknown'.tr(),
]),
'friendDeleteDescription'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
);
if (!confirm) return;
if (!mounted) return;
@ -426,9 +382,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(kFriendStatus[relation.status] ?? 'unknown')
.tr()
.opacity(0.75),
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
if (relation.status == 0)
Row(
mainAxisAlignment: MainAxisAlignment.end,
@ -449,8 +403,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
onTap:
_isBusy ? null : () => _changeRelation(relation, 1),
onTap: _isBusy ? null : () => _changeRelation(relation, 1),
child: Text('friendUnblock').tr(),
),
const Gap(8),

View File

@ -18,7 +18,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/widget.dart';
import 'package:surface/screens/captcha.dart';
import 'package:surface/screens/captcha/captcha.dart';
import 'package:surface/types/check_in.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/app_bar_leading.dart';

View File

@ -28,11 +28,8 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
setState(() => _isBusy = true);
try {
final pt = context.read<SnPostContentProvider>();
final result = await pt.listPosts(
take: 10,
offset: _posts.length,
isShuffle: true,
);
final result =
await pt.listPosts(take: 10, offset: _posts.length, isShuffle: true);
_posts.addAll(result.$1);
} catch (err) {
if (!mounted) return;
@ -57,19 +54,14 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
title: Text('postShuffle').tr(),
),
appBar: AppBar(title: Text('postShuffle').tr()),
body: Stack(
children: [
Column(
children: [
if (_isBusy || _posts.isEmpty)
const Expanded(
child: Center(
child: CircularProgressIndicator(),
),
)
child: Center(child: CircularProgressIndicator()))
else
Expanded(
child: CardSwiper(
@ -81,17 +73,20 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
final ele = _posts[idx];
return SingleChildScrollView(
child: Center(
child: OpenablePostItem(
key: ValueKey(ele),
data: ele,
maxWidth: 640,
onChanged: (ele) {
_posts[idx] = ele;
setState(() {});
},
onDeleted: () {
_fetchPosts();
},
child: Card(
color: Theme.of(context).colorScheme.surface,
child: OpenablePostItem(
key: ValueKey(ele),
data: ele,
maxWidth: 640,
onChanged: (ele) {
_posts[idx] = ele;
setState(() {});
},
onDeleted: () {
_fetchPosts();
},
).padding(all: 8),
).padding(
all: 24,
bottom:

View File

@ -45,10 +45,7 @@ class _WalletScreenState extends State<WalletScreen> {
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenAccountWallet').tr(),
),
appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
@ -66,11 +63,6 @@ class _WalletScreenState extends State<WalletScreen> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 28,
child: Icon(Symbols.wallet, size: 28),
),
const Gap(12),
SizedBox(width: double.infinity),
Text(
NumberFormat.compactCurrency(
@ -81,6 +73,16 @@ class _WalletScreenState extends State<WalletScreen> {
style: Theme.of(context).textTheme.titleLarge,
),
Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
const Gap(16),
Text(
NumberFormat.compactCurrency(
locale: EasyLocalization.of(context)!.currentLocale.toString(),
symbol: '${'walletCurrencyGoldenShort'.tr()} ',
decimalDigits: 2,
).format(double.parse(_wallet!.goldenBalance)),
style: Theme.of(context).textTheme.titleLarge,
),
Text('walletCurrencyGolden'.plural(double.parse(_wallet!.goldenBalance))),
],
).padding(horizontal: 20, vertical: 24),
).padding(horizontal: 8, top: 16, bottom: 4),
@ -109,14 +111,12 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: {
'take': 10,
'offset': _transactions.length,
});
_totalCount = resp.data['count'];
_transactions.addAll(
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
final resp = await sn.client.get(
'/cgi/wa/transactions/me',
queryParameters: {'take': 10, 'offset': _transactions.length},
);
_totalCount = resp.data['count'];
_transactions.addAll(resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? []);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -159,12 +159,18 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
children: [
Text(ele.remark),
const Gap(2),
Text(
DateFormat(
null,
EasyLocalization.of(context)!.currentLocale.toString(),
).format(ele.createdAt),
style: Theme.of(context).textTheme.labelSmall,
Row(
children: [
Text(
'walletTransactionType${ele.currency.capitalize()}'.tr(),
style: Theme.of(context).textTheme.labelSmall,
),
Text(' · ').textStyle(Theme.of(context).textTheme.labelSmall!).padding(right: 4),
Text(
DateFormat(null, EasyLocalization.of(context)!.currentLocale.toString()).format(ele.createdAt),
style: Theme.of(context).textTheme.labelSmall,
),
],
),
],
),
@ -193,37 +199,33 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
final TextEditingController passwordController = TextEditingController();
final password = await showDialog<String?>(
context: context,
builder: (ctx) => AlertDialog(
title: Text('walletCreate').tr(),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('walletCreatePassword').tr(),
const Gap(8),
TextField(
autofocus: true,
obscureText: true,
controller: passwordController,
decoration: InputDecoration(
labelText: 'fieldPassword'.tr(),
),
builder:
(ctx) => AlertDialog(
title: Text('walletCreate').tr(),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('walletCreatePassword').tr(),
const Gap(8),
TextField(
autofocus: true,
obscureText: true,
controller: passwordController,
decoration: InputDecoration(labelText: 'fieldPassword'.tr()),
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text('cancel').tr(),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text('cancel').tr()),
TextButton(
onPressed: () {
Navigator.of(ctx).pop(passwordController.text);
},
child: Text('next').tr(),
),
],
),
TextButton(
onPressed: () {
Navigator.of(ctx).pop(passwordController.text);
},
child: Text('next').tr(),
),
],
),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
passwordController.dispose();
@ -234,9 +236,7 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/wa/wallets/me', data: {
'password': password,
});
await sn.client.post('/cgi/wa/wallets/me', data: {'password': password});
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -255,20 +255,14 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 28,
child: Icon(Symbols.add, size: 28),
),
CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)),
const Gap(12),
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
const Gap(8),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _isBusy ? null : () => _createWallet(),
child: Text('next').tr(),
),
child: TextButton(onPressed: _isBusy ? null : () => _createWallet(), child: Text('next').tr()),
),
],
).padding(horizontal: 20, vertical: 24),

View File

@ -184,3 +184,42 @@ abstract class SnActionEvent with _$SnActionEvent {
factory SnActionEvent.fromJson(Map<String, Object?> json) =>
_$SnActionEventFromJson(json);
}
@freezed
abstract class SnProgram with _$SnProgram {
const factory SnProgram({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String name,
required String description,
required String alias,
required int expRequirement,
required Map<String, dynamic> price,
required Map<String, dynamic> badge,
required Map<String, dynamic> group,
required Map<String, dynamic> appearance,
}) = _SnProgram;
factory SnProgram.fromJson(Map<String, Object?> json) =>
_$SnProgramFromJson(json);
}
@freezed
abstract class SnProgramMember with _$SnProgramMember {
const factory SnProgramMember({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required DateTime lastPaid,
required SnAccount account,
required int accountId,
required SnProgram program,
required int programId,
}) = _SnProgramMember;
factory SnProgramMember.fromJson(Map<String, Object?> json) =>
_$SnProgramMemberFromJson(json);
}

View File

@ -3470,4 +3470,763 @@ class __$SnActionEventCopyWithImpl<$Res>
}
}
/// @nodoc
mixin _$SnProgram {
int get id;
DateTime get createdAt;
DateTime get updatedAt;
DateTime? get deletedAt;
String get name;
String get description;
String get alias;
int get expRequirement;
Map<String, dynamic> get price;
Map<String, dynamic> get badge;
Map<String, dynamic> get group;
Map<String, dynamic> get appearance;
/// Create a copy of SnProgram
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnProgramCopyWith<SnProgram> get copyWith =>
_$SnProgramCopyWithImpl<SnProgram>(this as SnProgram, _$identity);
/// Serializes this SnProgram to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is SnProgram &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.alias, alias) || other.alias == alias) &&
(identical(other.expRequirement, expRequirement) ||
other.expRequirement == expRequirement) &&
const DeepCollectionEquality().equals(other.price, price) &&
const DeepCollectionEquality().equals(other.badge, badge) &&
const DeepCollectionEquality().equals(other.group, group) &&
const DeepCollectionEquality()
.equals(other.appearance, appearance));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
name,
description,
alias,
expRequirement,
const DeepCollectionEquality().hash(price),
const DeepCollectionEquality().hash(badge),
const DeepCollectionEquality().hash(group),
const DeepCollectionEquality().hash(appearance));
@override
String toString() {
return 'SnProgram(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, name: $name, description: $description, alias: $alias, expRequirement: $expRequirement, price: $price, badge: $badge, group: $group, appearance: $appearance)';
}
}
/// @nodoc
abstract mixin class $SnProgramCopyWith<$Res> {
factory $SnProgramCopyWith(SnProgram value, $Res Function(SnProgram) _then) =
_$SnProgramCopyWithImpl;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String name,
String description,
String alias,
int expRequirement,
Map<String, dynamic> price,
Map<String, dynamic> badge,
Map<String, dynamic> group,
Map<String, dynamic> appearance});
}
/// @nodoc
class _$SnProgramCopyWithImpl<$Res> implements $SnProgramCopyWith<$Res> {
_$SnProgramCopyWithImpl(this._self, this._then);
final SnProgram _self;
final $Res Function(SnProgram) _then;
/// Create a copy of SnProgram
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? name = null,
Object? description = null,
Object? alias = null,
Object? expRequirement = null,
Object? price = null,
Object? badge = null,
Object? group = null,
Object? appearance = null,
}) {
return _then(_self.copyWith(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _self.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _self.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
name: null == name
? _self.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _self.description
: description // ignore: cast_nullable_to_non_nullable
as String,
alias: null == alias
? _self.alias
: alias // ignore: cast_nullable_to_non_nullable
as String,
expRequirement: null == expRequirement
? _self.expRequirement
: expRequirement // ignore: cast_nullable_to_non_nullable
as int,
price: null == price
? _self.price
: price // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
badge: null == badge
? _self.badge
: badge // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
group: null == group
? _self.group
: group // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
appearance: null == appearance
? _self.appearance
: appearance // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnProgram implements SnProgram {
const _SnProgram(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.name,
required this.description,
required this.alias,
required this.expRequirement,
required final Map<String, dynamic> price,
required final Map<String, dynamic> badge,
required final Map<String, dynamic> group,
required final Map<String, dynamic> appearance})
: _price = price,
_badge = badge,
_group = group,
_appearance = appearance;
factory _SnProgram.fromJson(Map<String, dynamic> json) =>
_$SnProgramFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final String name;
@override
final String description;
@override
final String alias;
@override
final int expRequirement;
final Map<String, dynamic> _price;
@override
Map<String, dynamic> get price {
if (_price is EqualUnmodifiableMapView) return _price;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_price);
}
final Map<String, dynamic> _badge;
@override
Map<String, dynamic> get badge {
if (_badge is EqualUnmodifiableMapView) return _badge;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_badge);
}
final Map<String, dynamic> _group;
@override
Map<String, dynamic> get group {
if (_group is EqualUnmodifiableMapView) return _group;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_group);
}
final Map<String, dynamic> _appearance;
@override
Map<String, dynamic> get appearance {
if (_appearance is EqualUnmodifiableMapView) return _appearance;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_appearance);
}
/// Create a copy of SnProgram
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnProgramCopyWith<_SnProgram> get copyWith =>
__$SnProgramCopyWithImpl<_SnProgram>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnProgramToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _SnProgram &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.alias, alias) || other.alias == alias) &&
(identical(other.expRequirement, expRequirement) ||
other.expRequirement == expRequirement) &&
const DeepCollectionEquality().equals(other._price, _price) &&
const DeepCollectionEquality().equals(other._badge, _badge) &&
const DeepCollectionEquality().equals(other._group, _group) &&
const DeepCollectionEquality()
.equals(other._appearance, _appearance));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
name,
description,
alias,
expRequirement,
const DeepCollectionEquality().hash(_price),
const DeepCollectionEquality().hash(_badge),
const DeepCollectionEquality().hash(_group),
const DeepCollectionEquality().hash(_appearance));
@override
String toString() {
return 'SnProgram(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, name: $name, description: $description, alias: $alias, expRequirement: $expRequirement, price: $price, badge: $badge, group: $group, appearance: $appearance)';
}
}
/// @nodoc
abstract mixin class _$SnProgramCopyWith<$Res>
implements $SnProgramCopyWith<$Res> {
factory _$SnProgramCopyWith(
_SnProgram value, $Res Function(_SnProgram) _then) =
__$SnProgramCopyWithImpl;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String name,
String description,
String alias,
int expRequirement,
Map<String, dynamic> price,
Map<String, dynamic> badge,
Map<String, dynamic> group,
Map<String, dynamic> appearance});
}
/// @nodoc
class __$SnProgramCopyWithImpl<$Res> implements _$SnProgramCopyWith<$Res> {
__$SnProgramCopyWithImpl(this._self, this._then);
final _SnProgram _self;
final $Res Function(_SnProgram) _then;
/// Create a copy of SnProgram
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? name = null,
Object? description = null,
Object? alias = null,
Object? expRequirement = null,
Object? price = null,
Object? badge = null,
Object? group = null,
Object? appearance = null,
}) {
return _then(_SnProgram(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _self.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _self.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
name: null == name
? _self.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _self.description
: description // ignore: cast_nullable_to_non_nullable
as String,
alias: null == alias
? _self.alias
: alias // ignore: cast_nullable_to_non_nullable
as String,
expRequirement: null == expRequirement
? _self.expRequirement
: expRequirement // ignore: cast_nullable_to_non_nullable
as int,
price: null == price
? _self._price
: price // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
badge: null == badge
? _self._badge
: badge // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
group: null == group
? _self._group
: group // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
appearance: null == appearance
? _self._appearance
: appearance // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
mixin _$SnProgramMember {
int get id;
DateTime get createdAt;
DateTime get updatedAt;
DateTime? get deletedAt;
DateTime get lastPaid;
SnAccount get account;
int get accountId;
SnProgram get program;
int get programId;
/// Create a copy of SnProgramMember
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnProgramMemberCopyWith<SnProgramMember> get copyWith =>
_$SnProgramMemberCopyWithImpl<SnProgramMember>(
this as SnProgramMember, _$identity);
/// Serializes this SnProgramMember to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is SnProgramMember &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.lastPaid, lastPaid) ||
other.lastPaid == lastPaid) &&
(identical(other.account, account) || other.account == account) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.program, program) || other.program == program) &&
(identical(other.programId, programId) ||
other.programId == programId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, lastPaid, account, accountId, program, programId);
@override
String toString() {
return 'SnProgramMember(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, lastPaid: $lastPaid, account: $account, accountId: $accountId, program: $program, programId: $programId)';
}
}
/// @nodoc
abstract mixin class $SnProgramMemberCopyWith<$Res> {
factory $SnProgramMemberCopyWith(
SnProgramMember value, $Res Function(SnProgramMember) _then) =
_$SnProgramMemberCopyWithImpl;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
DateTime lastPaid,
SnAccount account,
int accountId,
SnProgram program,
int programId});
$SnAccountCopyWith<$Res> get account;
$SnProgramCopyWith<$Res> get program;
}
/// @nodoc
class _$SnProgramMemberCopyWithImpl<$Res>
implements $SnProgramMemberCopyWith<$Res> {
_$SnProgramMemberCopyWithImpl(this._self, this._then);
final SnProgramMember _self;
final $Res Function(SnProgramMember) _then;
/// Create a copy of SnProgramMember
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? lastPaid = null,
Object? account = null,
Object? accountId = null,
Object? program = null,
Object? programId = null,
}) {
return _then(_self.copyWith(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _self.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _self.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
lastPaid: null == lastPaid
? _self.lastPaid
: lastPaid // ignore: cast_nullable_to_non_nullable
as DateTime,
account: null == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount,
accountId: null == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
program: null == program
? _self.program
: program // ignore: cast_nullable_to_non_nullable
as SnProgram,
programId: null == programId
? _self.programId
: programId // ignore: cast_nullable_to_non_nullable
as int,
));
}
/// Create a copy of SnProgramMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res> get account {
return $SnAccountCopyWith<$Res>(_self.account, (value) {
return _then(_self.copyWith(account: value));
});
}
/// Create a copy of SnProgramMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnProgramCopyWith<$Res> get program {
return $SnProgramCopyWith<$Res>(_self.program, (value) {
return _then(_self.copyWith(program: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnProgramMember implements SnProgramMember {
const _SnProgramMember(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.lastPaid,
required this.account,
required this.accountId,
required this.program,
required this.programId});
factory _SnProgramMember.fromJson(Map<String, dynamic> json) =>
_$SnProgramMemberFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final DateTime lastPaid;
@override
final SnAccount account;
@override
final int accountId;
@override
final SnProgram program;
@override
final int programId;
/// Create a copy of SnProgramMember
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnProgramMemberCopyWith<_SnProgramMember> get copyWith =>
__$SnProgramMemberCopyWithImpl<_SnProgramMember>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnProgramMemberToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _SnProgramMember &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.lastPaid, lastPaid) ||
other.lastPaid == lastPaid) &&
(identical(other.account, account) || other.account == account) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.program, program) || other.program == program) &&
(identical(other.programId, programId) ||
other.programId == programId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, lastPaid, account, accountId, program, programId);
@override
String toString() {
return 'SnProgramMember(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, lastPaid: $lastPaid, account: $account, accountId: $accountId, program: $program, programId: $programId)';
}
}
/// @nodoc
abstract mixin class _$SnProgramMemberCopyWith<$Res>
implements $SnProgramMemberCopyWith<$Res> {
factory _$SnProgramMemberCopyWith(
_SnProgramMember value, $Res Function(_SnProgramMember) _then) =
__$SnProgramMemberCopyWithImpl;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
DateTime lastPaid,
SnAccount account,
int accountId,
SnProgram program,
int programId});
@override
$SnAccountCopyWith<$Res> get account;
@override
$SnProgramCopyWith<$Res> get program;
}
/// @nodoc
class __$SnProgramMemberCopyWithImpl<$Res>
implements _$SnProgramMemberCopyWith<$Res> {
__$SnProgramMemberCopyWithImpl(this._self, this._then);
final _SnProgramMember _self;
final $Res Function(_SnProgramMember) _then;
/// Create a copy of SnProgramMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? lastPaid = null,
Object? account = null,
Object? accountId = null,
Object? program = null,
Object? programId = null,
}) {
return _then(_SnProgramMember(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _self.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _self.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
lastPaid: null == lastPaid
? _self.lastPaid
: lastPaid // ignore: cast_nullable_to_non_nullable
as DateTime,
account: null == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount,
accountId: null == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
program: null == program
? _self.program
: program // ignore: cast_nullable_to_non_nullable
as SnProgram,
programId: null == programId
? _self.programId
: programId // ignore: cast_nullable_to_non_nullable
as int,
));
}
/// Create a copy of SnProgramMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res> get account {
return $SnAccountCopyWith<$Res>(_self.account, (value) {
return _then(_self.copyWith(account: value));
});
}
/// Create a copy of SnProgramMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnProgramCopyWith<$Res> get program {
return $SnProgramCopyWith<$Res>(_self.program, (value) {
return _then(_self.copyWith(program: value));
});
}
}
// dart format on

View File

@ -319,3 +319,64 @@ Map<String, dynamic> _$SnActionEventToJson(_SnActionEvent instance) =>
'account': instance.account.toJson(),
'account_id': instance.accountId,
};
_SnProgram _$SnProgramFromJson(Map<String, dynamic> json) => _SnProgram(
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),
name: json['name'] as String,
description: json['description'] as String,
alias: json['alias'] as String,
expRequirement: (json['exp_requirement'] as num).toInt(),
price: json['price'] as Map<String, dynamic>,
badge: json['badge'] as Map<String, dynamic>,
group: json['group'] as Map<String, dynamic>,
appearance: json['appearance'] as Map<String, dynamic>,
);
Map<String, dynamic> _$SnProgramToJson(_SnProgram instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'name': instance.name,
'description': instance.description,
'alias': instance.alias,
'exp_requirement': instance.expRequirement,
'price': instance.price,
'badge': instance.badge,
'group': instance.group,
'appearance': instance.appearance,
};
_SnProgramMember _$SnProgramMemberFromJson(Map<String, dynamic> json) =>
_SnProgramMember(
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),
lastPaid: DateTime.parse(json['last_paid'] as String),
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(),
program: SnProgram.fromJson(json['program'] as Map<String, dynamic>),
programId: (json['program_id'] as num).toInt(),
);
Map<String, dynamic> _$SnProgramMemberToJson(_SnProgramMember instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'last_paid': instance.lastPaid.toIso8601String(),
'account': instance.account.toJson(),
'account_id': instance.accountId,
'program': instance.program.toJson(),
'program_id': instance.programId,
};

View File

@ -11,6 +11,7 @@ abstract class SnWallet with _$SnWallet {
required DateTime updatedAt,
required DateTime? deletedAt,
required String balance,
required String goldenBalance,
required String password,
required int accountId,
}) = _SnWallet;
@ -27,6 +28,7 @@ abstract class SnTransaction with _$SnTransaction {
required DateTime? deletedAt,
required String remark,
required String amount,
required String currency,
required SnWallet? payer,
required SnWallet? payee,
required int? payerId,

View File

@ -20,6 +20,7 @@ mixin _$SnWallet {
DateTime get updatedAt;
DateTime? get deletedAt;
String get balance;
String get goldenBalance;
String get password;
int get accountId;
@ -46,6 +47,8 @@ mixin _$SnWallet {
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.balance, balance) || other.balance == balance) &&
(identical(other.goldenBalance, goldenBalance) ||
other.goldenBalance == goldenBalance) &&
(identical(other.password, password) ||
other.password == password) &&
(identical(other.accountId, accountId) ||
@ -55,11 +58,11 @@ mixin _$SnWallet {
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, balance, password, accountId);
deletedAt, balance, goldenBalance, password, accountId);
@override
String toString() {
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, password: $password, accountId: $accountId)';
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, goldenBalance: $goldenBalance, password: $password, accountId: $accountId)';
}
}
@ -74,6 +77,7 @@ abstract mixin class $SnWalletCopyWith<$Res> {
DateTime updatedAt,
DateTime? deletedAt,
String balance,
String goldenBalance,
String password,
int accountId});
}
@ -95,6 +99,7 @@ class _$SnWalletCopyWithImpl<$Res> implements $SnWalletCopyWith<$Res> {
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? balance = null,
Object? goldenBalance = null,
Object? password = null,
Object? accountId = null,
}) {
@ -119,6 +124,10 @@ class _$SnWalletCopyWithImpl<$Res> implements $SnWalletCopyWith<$Res> {
? _self.balance
: balance // ignore: cast_nullable_to_non_nullable
as String,
goldenBalance: null == goldenBalance
? _self.goldenBalance
: goldenBalance // ignore: cast_nullable_to_non_nullable
as String,
password: null == password
? _self.password
: password // ignore: cast_nullable_to_non_nullable
@ -140,6 +149,7 @@ class _SnWallet implements SnWallet {
required this.updatedAt,
required this.deletedAt,
required this.balance,
required this.goldenBalance,
required this.password,
required this.accountId});
factory _SnWallet.fromJson(Map<String, dynamic> json) =>
@ -156,6 +166,8 @@ class _SnWallet implements SnWallet {
@override
final String balance;
@override
final String goldenBalance;
@override
final String password;
@override
final int accountId;
@ -188,6 +200,8 @@ class _SnWallet implements SnWallet {
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.balance, balance) || other.balance == balance) &&
(identical(other.goldenBalance, goldenBalance) ||
other.goldenBalance == goldenBalance) &&
(identical(other.password, password) ||
other.password == password) &&
(identical(other.accountId, accountId) ||
@ -197,11 +211,11 @@ class _SnWallet implements SnWallet {
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, balance, password, accountId);
deletedAt, balance, goldenBalance, password, accountId);
@override
String toString() {
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, password: $password, accountId: $accountId)';
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, goldenBalance: $goldenBalance, password: $password, accountId: $accountId)';
}
}
@ -218,6 +232,7 @@ abstract mixin class _$SnWalletCopyWith<$Res>
DateTime updatedAt,
DateTime? deletedAt,
String balance,
String goldenBalance,
String password,
int accountId});
}
@ -239,6 +254,7 @@ class __$SnWalletCopyWithImpl<$Res> implements _$SnWalletCopyWith<$Res> {
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? balance = null,
Object? goldenBalance = null,
Object? password = null,
Object? accountId = null,
}) {
@ -263,6 +279,10 @@ class __$SnWalletCopyWithImpl<$Res> implements _$SnWalletCopyWith<$Res> {
? _self.balance
: balance // ignore: cast_nullable_to_non_nullable
as String,
goldenBalance: null == goldenBalance
? _self.goldenBalance
: goldenBalance // ignore: cast_nullable_to_non_nullable
as String,
password: null == password
? _self.password
: password // ignore: cast_nullable_to_non_nullable
@ -283,6 +303,7 @@ mixin _$SnTransaction {
DateTime? get deletedAt;
String get remark;
String get amount;
String get currency;
SnWallet? get payer;
SnWallet? get payee;
int? get payerId;
@ -313,6 +334,8 @@ mixin _$SnTransaction {
other.deletedAt == deletedAt) &&
(identical(other.remark, remark) || other.remark == remark) &&
(identical(other.amount, amount) || other.amount == amount) &&
(identical(other.currency, currency) ||
other.currency == currency) &&
(identical(other.payer, payer) || other.payer == payer) &&
(identical(other.payee, payee) || other.payee == payee) &&
(identical(other.payerId, payerId) || other.payerId == payerId) &&
@ -322,11 +345,11 @@ mixin _$SnTransaction {
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, remark, amount, payer, payee, payerId, payeeId);
deletedAt, remark, amount, currency, payer, payee, payerId, payeeId);
@override
String toString() {
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, currency: $currency, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
}
}
@ -343,6 +366,7 @@ abstract mixin class $SnTransactionCopyWith<$Res> {
DateTime? deletedAt,
String remark,
String amount,
String currency,
SnWallet? payer,
SnWallet? payee,
int? payerId,
@ -371,6 +395,7 @@ class _$SnTransactionCopyWithImpl<$Res>
Object? deletedAt = freezed,
Object? remark = null,
Object? amount = null,
Object? currency = null,
Object? payer = freezed,
Object? payee = freezed,
Object? payerId = freezed,
@ -401,6 +426,10 @@ class _$SnTransactionCopyWithImpl<$Res>
? _self.amount
: amount // ignore: cast_nullable_to_non_nullable
as String,
currency: null == currency
? _self.currency
: currency // ignore: cast_nullable_to_non_nullable
as String,
payer: freezed == payer
? _self.payer
: payer // ignore: cast_nullable_to_non_nullable
@ -459,6 +488,7 @@ class _SnTransaction implements SnTransaction {
required this.deletedAt,
required this.remark,
required this.amount,
required this.currency,
required this.payer,
required this.payee,
required this.payerId,
@ -479,6 +509,8 @@ class _SnTransaction implements SnTransaction {
@override
final String amount;
@override
final String currency;
@override
final SnWallet? payer;
@override
final SnWallet? payee;
@ -516,6 +548,8 @@ class _SnTransaction implements SnTransaction {
other.deletedAt == deletedAt) &&
(identical(other.remark, remark) || other.remark == remark) &&
(identical(other.amount, amount) || other.amount == amount) &&
(identical(other.currency, currency) ||
other.currency == currency) &&
(identical(other.payer, payer) || other.payer == payer) &&
(identical(other.payee, payee) || other.payee == payee) &&
(identical(other.payerId, payerId) || other.payerId == payerId) &&
@ -525,11 +559,11 @@ class _SnTransaction implements SnTransaction {
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, remark, amount, payer, payee, payerId, payeeId);
deletedAt, remark, amount, currency, payer, payee, payerId, payeeId);
@override
String toString() {
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, currency: $currency, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
}
}
@ -548,6 +582,7 @@ abstract mixin class _$SnTransactionCopyWith<$Res>
DateTime? deletedAt,
String remark,
String amount,
String currency,
SnWallet? payer,
SnWallet? payee,
int? payerId,
@ -578,6 +613,7 @@ class __$SnTransactionCopyWithImpl<$Res>
Object? deletedAt = freezed,
Object? remark = null,
Object? amount = null,
Object? currency = null,
Object? payer = freezed,
Object? payee = freezed,
Object? payerId = freezed,
@ -608,6 +644,10 @@ class __$SnTransactionCopyWithImpl<$Res>
? _self.amount
: amount // ignore: cast_nullable_to_non_nullable
as String,
currency: null == currency
? _self.currency
: currency // ignore: cast_nullable_to_non_nullable
as String,
payer: freezed == payer
? _self.payer
: payer // ignore: cast_nullable_to_non_nullable

View File

@ -14,6 +14,7 @@ _SnWallet _$SnWalletFromJson(Map<String, dynamic> json) => _SnWallet(
? null
: DateTime.parse(json['deleted_at'] as String),
balance: json['balance'] as String,
goldenBalance: json['golden_balance'] as String,
password: json['password'] as String,
accountId: (json['account_id'] as num).toInt(),
);
@ -24,6 +25,7 @@ Map<String, dynamic> _$SnWalletToJson(_SnWallet instance) => <String, dynamic>{
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'balance': instance.balance,
'golden_balance': instance.goldenBalance,
'password': instance.password,
'account_id': instance.accountId,
};
@ -38,6 +40,7 @@ _SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) =>
: DateTime.parse(json['deleted_at'] as String),
remark: json['remark'] as String,
amount: json['amount'] as String,
currency: json['currency'] as String,
payer: json['payer'] == null
? null
: SnWallet.fromJson(json['payer'] as Map<String, dynamic>),
@ -56,6 +59,7 @@ Map<String, dynamic> _$SnTransactionToJson(_SnTransaction instance) =>
'deleted_at': instance.deletedAt?.toIso8601String(),
'remark': instance.remark,
'amount': instance.amount,
'currency': instance.currency,
'payer': instance.payer?.toJson(),
'payee': instance.payee?.toJson(),
'payer_id': instance.payerId,

View File

@ -2525,10 +2525,11 @@ packages:
workmanager:
dependency: "direct main"
description:
name: workmanager
sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
url: "https://pub.dev"
source: hosted
path: workmanager
ref: main
resolved-ref: "4ce065135dc1b91fee918f81596b42a56850391d"
url: "https://github.com/fluttercommunity/flutter_workmanager.git"
source: git
version: "0.5.2"
xdg_directories:
dependency: transitive

View File

@ -59,7 +59,7 @@ dependencies:
relative_time: ^5.0.0
image_picker: ^1.1.2
cross_file: ^0.3.4+2
file_picker: ^9.0.0 # pinned due to compile failed on android, https://github.com/miguelpruivo/flutter_file_picker/issues/1643
file_picker: ^9.2.1
croppy: ^1.3.1
flutter_expandable_fab: ^2.3.0
dropdown_button2: ^2.3.9
@ -103,7 +103,11 @@ dependencies:
flutter_svg: ^2.0.16
home_widget: ^0.7.0
receive_sharing_intent: ^1.8.1
workmanager: ^0.5.2
workmanager: # use git due to: https://github.com/fluttercommunity/flutter_workmanager/issues/588#issuecomment-2660871645
git:
url: https://github.com/fluttercommunity/flutter_workmanager.git
path: workmanager
ref: main
flutter_app_update: ^3.2.2
in_app_review: ^2.0.10
version: ^3.0.2

2
web/_redirects Normal file
View File

@ -0,0 +1,2 @@
/assets/assets/translations/en.json /assets/assets/translations/en-US.json 301
/assets/assets/translations/zh.json /assets/assets/translations/zh-CN.json 301