feat:关于页面 FlowNumberText

This commit is contained in:
xkeyC 2024-03-11 20:43:16 +08:00
parent 1c73217ea7
commit b950d5dc8a
4 changed files with 244 additions and 83 deletions

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart'; import 'package:starcitizen_doctor/common/io/rs_http.dart';
@ -17,4 +19,12 @@ class AnalyticsApi {
dPrint("AnalyticsApi.touch === $key Error:$e"); dPrint("AnalyticsApi.touch === $key Error:$e");
} }
} }
static Future<Map<String,dynamic>> getAnalyticsData() async {
final r = await RSHttp.get("${URLConf.analyticsApiHome}/analytics");
if (r.data == null) return {};
final jsonData = json.decode(utf8.decode(r.data!));
dPrint("AnalyticsApi.getAnalyticsData");
return jsonData;
}
} }

View File

@ -2,10 +2,12 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/app.dart'; import 'package:starcitizen_doctor/app.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart'; import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart'; import 'package:starcitizen_doctor/widgets/src/flow_number_text.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class AboutUI extends HookConsumerWidget { class AboutUI extends HookConsumerWidget {
@ -20,7 +22,7 @@ class AboutUI extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Spacer(), const Spacer(),
const SizedBox(height: 64), const SizedBox(height: 32),
Image.asset("assets/app_logo.png", width: 128, height: 128), Image.asset("assets/app_logo.png", width: 128, height: 128),
const SizedBox(height: 6), const SizedBox(height: 6),
const Text( const Text(
@ -40,88 +42,21 @@ class AboutUI extends HookConsumerWidget {
borderRadius: BorderRadius.circular(12)), borderRadius: BorderRadius.circular(12)),
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Text( child: Column(
"不仅仅是汉化!\n\nSC汉化盒子是你探索宇宙的好帮手我们致力于为各位公民解决游戏中的常见问题并为社区汉化、性能调优、常用网站汉化 等操作提供便利。", children: [
style: TextStyle( Text(
fontSize: 14, color: Colors.white.withOpacity(.9)), "不仅仅是汉化!\n\nSC汉化盒子是你探索宇宙的好帮手我们致力于为各位公民解决游戏中的常见问题并为社区汉化、性能调优、常用网站汉化 等操作提供便利。",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.9)),
),
],
), ),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Row( makeAnalyticsWidget(context),
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Row(
children: [
const Icon(FontAwesomeIcons.link),
const SizedBox(width: 8),
Text(
"在线反馈",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.6)),
),
],
),
onPressed: () {
launchUrlString(URLConf.feedbackUrl);
},
),
const SizedBox(width: 24),
IconButton(
icon: Row(
children: [
const Icon(FontAwesomeIcons.qq),
const SizedBox(width: 8),
Text(
"QQ群: 940696487",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.6)),
),
],
),
onPressed: () {
launchUrlString(
"https://qm.qq.com/cgi-bin/qm/qr?k=TdyR3QU-x77OeD0NQ5w--F0uiNxPq-Tn&jump_from=webapi&authKey=m8s5GhF/7bRCvm5vI4aNl7RQEx5KOViwkzzIl54K+u9w2hzFpr9N/3avG4W/HaVS");
},
),
const SizedBox(width: 24),
IconButton(
icon: Row(
children: [
const Icon(FontAwesomeIcons.envelope),
const SizedBox(width: 8),
Text(
"邮箱: scbox@xkeyc.com",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.6)),
),
],
),
onPressed: () {
launchUrlString("mailto:scbox@xkeyc.com");
},
),
const SizedBox(width: 24),
IconButton(
icon: Row(
children: [
const Icon(FontAwesomeIcons.github),
const SizedBox(width: 8),
Text(
"开源",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.6)),
),
],
),
onPressed: () {
launchUrlString("https://github.com/StarCitizenToolBox/app");
},
),
],
),
const SizedBox(height: 24), const SizedBox(height: 24),
makeLinksRow(),
const Spacer(), const Spacer(),
Row( Row(
children: [ children: [
@ -129,7 +64,7 @@ class AboutUI extends HookConsumerWidget {
Container( Container(
width: MediaQuery.of(context).size.width * .35, width: MediaQuery.of(context).size.width * .35,
decoration: BoxDecoration( decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withOpacity(.03), color: FluentTheme.of(context).cardColor.withOpacity(.01),
borderRadius: BorderRadius.circular(12)), borderRadius: BorderRadius.circular(12)),
child: IconButton( child: IconButton(
icon: Padding( icon: Padding(
@ -155,12 +90,160 @@ class AboutUI extends HookConsumerWidget {
); );
} }
Widget makeLinksRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Row(
children: [
const Icon(FontAwesomeIcons.link),
const SizedBox(width: 8),
Text(
"在线反馈",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.6)),
),
],
),
onPressed: () {
launchUrlString(URLConf.feedbackUrl);
},
),
const SizedBox(width: 24),
IconButton(
icon: Row(
children: [
const Icon(FontAwesomeIcons.qq),
const SizedBox(width: 8),
Text(
"QQ群: 940696487",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.6)),
),
],
),
onPressed: () {
launchUrlString(
"https://qm.qq.com/cgi-bin/qm/qr?k=TdyR3QU-x77OeD0NQ5w--F0uiNxPq-Tn&jump_from=webapi&authKey=m8s5GhF/7bRCvm5vI4aNl7RQEx5KOViwkzzIl54K+u9w2hzFpr9N/3avG4W/HaVS");
},
),
const SizedBox(width: 24),
IconButton(
icon: Row(
children: [
const Icon(FontAwesomeIcons.envelope),
const SizedBox(width: 8),
Text(
"邮箱: scbox@xkeyc.com",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.6)),
),
],
),
onPressed: () {
launchUrlString("mailto:scbox@xkeyc.com");
},
),
const SizedBox(width: 24),
IconButton(
icon: Row(
children: [
const Icon(FontAwesomeIcons.github),
const SizedBox(width: 8),
Text(
"开源",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.6)),
),
],
),
onPressed: () {
launchUrlString("https://github.com/StarCitizenToolBox/app");
},
),
],
);
}
static const tipTextEN = static const tipTextEN =
"This is an unofficial Star Citizen fan-made tools, not affiliated with the Cloud Imperium group of companies. All content on this Software not authored by its host or users are property of their respective owners. \nStar Citizen®, Roberts Space Industries® and Cloud Imperium® are registered trademarks of Cloud Imperium Rights LLC."; "This is an unofficial Star Citizen fan-made tools, not affiliated with the Cloud Imperium group of companies. All content on this Software not authored by its host or users are property of their respective owners. \nStar Citizen®, Roberts Space Industries® and Cloud Imperium® are registered trademarks of Cloud Imperium Rights LLC.";
static const tipTextCN = static const tipTextCN =
"这是一个非官方的星际公民工具,不隶属于 Cloud Imperium 公司集团。 本软件中非由其主机或用户创作的所有内容均为其各自所有者的财产。 \nStar Citizen®、Roberts Space Industries® 和 Cloud Imperium® 是 Cloud Imperium Rights LLC 的注册商标。"; "这是一个非官方的星际公民工具,不隶属于 Cloud Imperium 公司集团。 本软件中非由其主机或用户创作的所有内容均为其各自所有者的财产。 \nStar Citizen®、Roberts Space Industries® 和 Cloud Imperium® 是 Cloud Imperium Rights LLC 的注册商标。";
Widget makeAnalyticsWidget(BuildContext context) {
return LoadingWidget(
onLoadData: AnalyticsApi.getAnalyticsData,
autoRefreshDuration: const Duration(seconds: 60),
childBuilder: (BuildContext context, Map<String, dynamic> data) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (data["total"] is List)
for (var item in data["total"])
if (item is Map)
if ([
"launch",
"gameLaunch",
"firstLaunch",
"install_localization",
"performance_apply",
"p4k_download",
].contains(item["Type"]))
makeAnalyticsItem(
context: context,
name: item["Type"] as String,
value: item["Count"] as int)
],
);
},
);
}
Widget makeAnalyticsItem(
{required BuildContext context,
required String name,
required int value}) {
const names = {
"launch": "启动",
"gameLaunch": "启动游戏",
"firstLaunch": "独立用户",
"install_localization": "汉化安装",
"performance_apply": "性能调优",
"p4k_download": "P4K分流"
};
return Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(left: 18, right: 18),
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor.withOpacity(.06),
borderRadius: BorderRadius.circular(12)),
child: Column(
children: [
Text(
names[name] ?? name,
style: TextStyle(fontSize: 13, color: Colors.white.withOpacity(.6)),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FlowNumberText(
targetValue: value,
style: const TextStyle(
fontSize: 20,
),
),
Text(" ${name == "firstLaunch" ? "" : ""}")
],
),
],
),
);
}
_onCheckUpdate(BuildContext context, WidgetRef ref) async { _onCheckUpdate(BuildContext context, WidgetRef ref) async {
if (ConstConf.isMSE) { if (ConstConf.isMSE) {
launchUrlString("ms-windows-store://pdp/?productid=9NF3SWFWNKL1"); launchUrlString("ms-windows-store://pdp/?productid=9NF3SWFWNKL1");

View File

@ -0,0 +1,54 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:intl/intl.dart';
class FlowNumberText extends HookConsumerWidget {
final int targetValue;
final Duration duration;
final TextStyle? style;
final Curve curve;
FlowNumberText(
{super.key,
required this.targetValue,
this.duration = const Duration(seconds: 1),
this.style,
this.curve = Curves.bounceOut});
final _formatter = NumberFormat.decimalPattern();
@override
Widget build(BuildContext context, WidgetRef ref) {
final value = useState<double>(0.0);
final timer = useState<Timer?>(null);
useEffect(() {
final totalTicks = duration.inMilliseconds ~/ 10;
var currentTick = 0;
if (value.value != 0) {
currentTick = (value.value / targetValue * totalTicks).toInt();
}
timer.value = Timer.periodic(const Duration(milliseconds: 10), (timer) {
final progress = curve.transform(currentTick / totalTicks);
value.value = (progress * targetValue).toDouble();
if (currentTick >= totalTicks) {
value.value = targetValue.toDouble();
timer.cancel();
} else {
currentTick++;
}
});
return timer.value?.cancel;
}, [targetValue]);
return Text(
_formatter.format(value.value.toInt()),
style: style,
);
}
}

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -159,9 +161,14 @@ class LoadingWidget<T> extends HookConsumerWidget {
final T? data; final T? data;
final Future<T?> Function()? onLoadData; final Future<T?> Function()? onLoadData;
final Widget Function(BuildContext context, T data) childBuilder; final Widget Function(BuildContext context, T data) childBuilder;
final Duration? autoRefreshDuration;
const LoadingWidget( const LoadingWidget(
{super.key, this.data, required this.childBuilder, this.onLoadData}); {super.key,
this.data,
required this.childBuilder,
this.onLoadData,
this.autoRefreshDuration});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -170,7 +177,14 @@ class LoadingWidget<T> extends HookConsumerWidget {
useEffect(() { useEffect(() {
if (data == null && onLoadData != null) { if (data == null && onLoadData != null) {
_loadData(dataState, errorMsg); _loadData(dataState, errorMsg);
return null; }
if (autoRefreshDuration != null) {
final timer = Timer.periodic(autoRefreshDuration!, (timer) {
if (onLoadData != null) {
_loadData(dataState, errorMsg);
}
});
return timer.cancel;
} }
return null; return null;
}, const []); }, const []);