feat: Donation page

This commit is contained in:
2025-03-01 13:42:30 +08:00
parent d65eb82ece
commit 721ba6a3c2
10 changed files with 4613 additions and 3314 deletions

View File

@ -1,7 +1,9 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/app.dart';
import 'package:starcitizen_doctor/common/conf/conf.dart';
@ -16,86 +18,414 @@ class AboutUI extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isTipTextCn = useState(false);
final pageCtrl = usePageController();
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Spacer(),
const SizedBox(height: 32),
Image.asset("assets/app_logo.png", width: 128, height: 128),
const SizedBox(height: 6),
Text(
S.current.app_index_version_info(
ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev"),
style: const TextStyle(fontSize: 18)),
const SizedBox(height: 12),
Button(
onPressed: () => _onCheckUpdate(context, ref),
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(S.current.about_check_update),
)),
const SizedBox(height: 32),
Container(
margin: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
S.current.about_app_description,
style: TextStyle(
fontSize: 14,
color: Colors.white.withValues(alpha: .9)),
return PageView(
scrollDirection: Axis.vertical,
controller: pageCtrl,
children: [
_makeAbout(context, ref, isTipTextCn, pageCtrl),
_makeDonate(context, ref, pageCtrl),
],
);
}
Widget _makeAbout(BuildContext context, WidgetRef ref,
ValueNotifier<bool> isTipTextCn, PageController pageCtrl) {
return Stack(
children: [
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Spacer(),
const SizedBox(height: 32),
Image.asset("assets/app_logo.png", width: 128, height: 128),
const SizedBox(height: 6),
Text(
S.current.app_index_version_info(
ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev"),
style: const TextStyle(fontSize: 18)),
const SizedBox(height: 12),
Button(
onPressed: () => _onCheckUpdate(context, ref),
child: Padding(
padding: const EdgeInsets.all(4),
child: Text(S.current.about_check_update),
)),
const SizedBox(height: 32),
Container(
margin: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
S.current.about_app_description,
style: TextStyle(
fontSize: 14,
color: Colors.white.withValues(alpha: .9)),
),
],
),
),
),
const SizedBox(height: 24),
makeAnalyticsWidget(context),
const SizedBox(height: 24),
makeLinksRow(),
const Spacer(),
Row(
children: [
const Spacer(),
AnimatedSize(
duration: const Duration(milliseconds: 200),
child: Container(
width: MediaQuery.of(context).size.width * .35,
decoration: BoxDecoration(
color: FluentTheme.of(context)
.cardColor
.withValues(alpha: .06),
borderRadius: BorderRadius.circular(12)),
child: IconButton(
icon: Padding(
padding: const EdgeInsets.all(3),
child: Text(
isTipTextCn.value ? tipTextCN : tipTextEN,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 12,
color: Colors.white.withValues(alpha: .9)),
),
),
onPressed: () {
isTipTextCn.value = !isTipTextCn.value;
},
),
),
),
const SizedBox(width: 12),
],
),
const SizedBox(height: 12),
],
),
),
Positioned(
bottom: 12,
left: 0,
right: 0,
child: Center(
child: makeNavButton(pageCtrl, 1),
),
),
],
);
}
Widget _makeDonate(
BuildContext context, WidgetRef ref, PageController pageCtrl) {
final donationTypeNotifier = useState('alipay');
final bubbleMessages = [
S.current.support_dev_thanks_message,
S.current.support_dev_referral_code_message,
];
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
const SizedBox(height: 8),
makeNavButton(pageCtrl, 0),
const SizedBox(height: 12),
Text(
S.current.support_dev_title,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 32),
// 聊天头像和气泡消息
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(25),
child: CacheNetImage(
url:
"https://git.scbox.xkeyc.cn/avatars/56a93334e892ba48f4fab453b8205624d661e4f7748cdb52bed47e5dc0c85de5?size=512",
width: 50,
height: 50,
fit: BoxFit.cover,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < bubbleMessages.length; i++)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: SelectionArea(
child: ChatBubble(message: bubbleMessages[i])),
),
],
),
),
],
),
const SizedBox(height: 32),
// 捐赠方式选择
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_donationMethodButton(
context: context,
title: 'WeChat',
icon: FontAwesomeIcons.weixin,
isSelected: donationTypeNotifier.value == 'wechat',
color: const Color(0xFF07C160),
onTap: () => donationTypeNotifier.value = 'wechat',
),
_donationMethodButton(
context: context,
title: 'AliPay',
icon: FontAwesomeIcons.alipay,
isSelected: donationTypeNotifier.value == 'alipay',
color: const Color(0xFF1677FF),
onTap: () => donationTypeNotifier.value = 'alipay',
),
_donationMethodButton(
context: context,
title: 'QQ',
icon: FontAwesomeIcons.qq,
isSelected: donationTypeNotifier.value == 'qq',
color: const Color(0xFF12B7F5),
onTap: () => donationTypeNotifier.value = 'qq',
),
_donationMethodButton(
context: context,
title: 'aUEC',
icon: FontAwesomeIcons.gamepad,
isSelected: donationTypeNotifier.value == 'uec',
color: const Color(0xFFFFD700),
onTap: () => donationTypeNotifier.value = 'uec',
),
_donationMethodButton(
context: context,
title: 'GitHub',
icon: FontAwesomeIcons.github,
isSelected: donationTypeNotifier.value == 'github',
color: Colors.white,
onTap: () => donationTypeNotifier.value = 'github',
),
],
),
const SizedBox(height: 24),
// 二维码显示区域
Container(
constraints: BoxConstraints(minHeight: 300),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(opacity: animation, child: child);
},
child: _buildDonationQRCode(donationTypeNotifier.value, context),
),
),
const SizedBox(height: 48),
],
),
);
}
Widget _donationMethodButton({
required BuildContext context,
required String title,
required IconData icon,
required bool isSelected,
required Color color,
required VoidCallback onTap,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Button(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) =>
isSelected
? ButtonThemeData.buttonColor(context, states)
.withAlpha((255.0 * 0.08).round())
: ButtonThemeData.buttonColor(context, states)
.withAlpha((255.0 * 0.005).round())),
padding: WidgetStateProperty.all(
EdgeInsets.symmetric(horizontal: 16, vertical: 8)),
),
onPressed: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 4),
Text(title, style: TextStyle(fontSize: 12)),
],
),
),
);
}
Widget _buildDonationQRCode(String type, BuildContext context) {
// 处理 GitHub 特殊情况
if (type == 'github') {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
key: ValueKey('github'),
children: [
SizedBox(height: 28),
const Icon(FontAwesomeIcons.github, size: 64),
const SizedBox(height: 32),
Text(S.current.support_dev_github_star_message),
const SizedBox(height: 32),
Button(
onPressed: () {
launchUrlString("https://github.com/StarCitizenToolBox/app");
},
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(FontAwesomeIcons.github),
const SizedBox(width: 12),
Text(S.current.support_dev_github_star_button),
],
),
),
),
const SizedBox(height: 24),
makeAnalyticsWidget(context),
const SizedBox(height: 24),
makeLinksRow(),
const Spacer(),
Row(
children: [
const Spacer(),
AnimatedSize(
duration: const Duration(milliseconds: 200),
child: Container(
width: MediaQuery.of(context).size.width * .35,
decoration: BoxDecoration(
color: FluentTheme.of(context)
.cardColor
.withValues(alpha: .06),
borderRadius: BorderRadius.circular(12)),
child: IconButton(
icon: Padding(
padding: const EdgeInsets.all(3),
child: Text(
isTipTextCn.value ? tipTextCN : tipTextEN,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 12,
color: Colors.white.withValues(alpha: .9)),
),
),
onPressed: () {
isTipTextCn.value = !isTipTextCn.value;
},
),
),
),
const SizedBox(width: 12),
],
],
);
}
// UEC 游戏内捐赠也特殊处理
if (type == 'uec') {
return Column(
key: ValueKey('uec'),
children: [
Image.asset("assets/sc_logo.png", width: 128, height: 128),
const SizedBox(height: 16),
Text(S.current.support_dev_in_game_currency_title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: FluentTheme.of(context)
.cardColor
.withAlpha((255 * .1).round()),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(S.current.support_dev_in_game_id,
style: TextStyle(fontSize: 16)),
const SizedBox(width: 12),
Button(
onPressed: () {
// ${S.current.support_dev_copy_button}游戏ID到剪贴板
Clipboard.setData(ClipboardData(text: "xkeyC"));
showToast(context, S.current.support_dev_in_game_id_copied);
},
child: Text(S.current.support_dev_copy_button),
)
],
),
),
const SizedBox(height: 12),
const SizedBox(height: 22),
Text(
S.current.support_dev_in_game_currency_message,
textAlign: TextAlign.start,
),
],
);
}
// 其他支付方式显示二维码
String qrData;
String title;
switch (type) {
case 'alipay':
qrData = DonationQrCodeData.alipay;
title = S.current.support_dev_alipay;
break;
case 'wechat':
qrData = DonationQrCodeData.wechat;
title = S.current.support_dev_wechat;
break;
case 'qq':
qrData = DonationQrCodeData.qq;
title = "QQ";
break;
default:
qrData = "";
title = "";
break;
}
return Column(
key: ValueKey(type),
children: [
Text(title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Container(
width: 200,
height: 200,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: QrImageView(data: qrData),
),
const SizedBox(height: 16),
Text(
S.current.support_dev_donation_disclaimer,
style: TextStyle(fontSize: 16),
),
],
);
}
Widget makeNavButton(PageController pageCtrl, int pageIndex) {
return IconButton(
icon: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
pageIndex == 0
? FluentIcons.chevron_up
: FluentIcons.chevron_down,
size: 12),
SizedBox(width: 8),
Text(pageIndex == 0
? S.current.support_dev_back_button
: S.current.support_dev_scroll_hint),
],
),
onPressed: () => pageCtrl.animateToPage(pageIndex,
duration: const Duration(milliseconds: 300), curve: Curves.ease),
);
}
@ -285,3 +615,38 @@ class AboutUI extends HookConsumerWidget {
}
}
}
class ChatBubble extends StatelessWidget {
final String message;
const ChatBubble({super.key, required this.message});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color:
FluentTheme.of(context).accentColor.withAlpha((255.0 * .2).round()),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(18),
bottomLeft: Radius.circular(18),
bottomRight: Radius.circular(18),
),
),
child: Text(
message,
style: TextStyle(fontSize: 14),
),
);
}
}
class DonationQrCodeData {
static const alipay = "https://qr.alipay.com/tsx16308c4uai0ticmz4j96";
static const wechat =
"wxp://f2f0J40rTCX7Vt79yooWNbiqH3U6UmwGJkqjcAYnrv9OZVzKyS5_W6trp8mo3KP-CTQ5";
static const qq =
"https://i.qianbao.qq.com/wallet/sqrcode.htm?m=tenpay&f=wallet&a=1&u=3334969096&n=xkeyC&ac=CAEQiK6etgwY8ZuKvgYyGOa1geWKqOaRiuS9jee7j-iQpeaUtuasvjgBQiAzY2Y4NWY3MDI1MWUxYWEwMGYyN2Q0OTM4Y2U2ODFlMw%3D%3D_xxx_sign";
}