From b950d5dc8a37e6740944a6a7125d093c0c55bc56 Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Mon, 11 Mar 2024 20:43:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=85=B3=E4=BA=8E=E9=A1=B5=E9=9D=A2=20Flo?= =?UTF-8?q?wNumberText?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/analytics.dart | 10 ++ lib/ui/about/about_ui.dart | 243 +++++++++++++++++--------- lib/widgets/src/flow_number_text.dart | 54 ++++++ lib/widgets/widgets.dart | 20 ++- 4 files changed, 244 insertions(+), 83 deletions(-) create mode 100644 lib/widgets/src/flow_number_text.dart diff --git a/lib/api/analytics.dart b/lib/api/analytics.dart index cd0adfb..b5f93a2 100644 --- a/lib/api/analytics.dart +++ b/lib/api/analytics.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/common/io/rs_http.dart'; @@ -17,4 +19,12 @@ class AnalyticsApi { dPrint("AnalyticsApi.touch === $key Error:$e"); } } + + static Future> 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; + } } diff --git a/lib/ui/about/about_ui.dart b/lib/ui/about/about_ui.dart index 4a1ef96..6bda271 100644 --- a/lib/ui/about/about_ui.dart +++ b/lib/ui/about/about_ui.dart @@ -2,10 +2,12 @@ import 'package:fluent_ui/fluent_ui.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:starcitizen_doctor/api/analytics.dart'; import 'package:starcitizen_doctor/app.dart'; import 'package:starcitizen_doctor/common/conf/const_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'; class AboutUI extends HookConsumerWidget { @@ -20,7 +22,7 @@ class AboutUI extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ const Spacer(), - const SizedBox(height: 64), + const SizedBox(height: 32), Image.asset("assets/app_logo.png", width: 128, height: 128), const SizedBox(height: 6), const Text( @@ -40,88 +42,21 @@ class AboutUI extends HookConsumerWidget { borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(24), - child: Text( - "不仅仅是汉化!\n\nSC汉化盒子是你探索宇宙的好帮手,我们致力于为各位公民解决游戏中的常见问题,并为社区汉化、性能调优、常用网站汉化 等操作提供便利。", - style: TextStyle( - fontSize: 14, color: Colors.white.withOpacity(.9)), + child: Column( + children: [ + Text( + "不仅仅是汉化!\n\nSC汉化盒子是你探索宇宙的好帮手,我们致力于为各位公民解决游戏中的常见问题,并为社区汉化、性能调优、常用网站汉化 等操作提供便利。", + style: TextStyle( + fontSize: 14, color: Colors.white.withOpacity(.9)), + ), + ], ), ), ), const SizedBox(height: 24), - 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"); - }, - ), - ], - ), + makeAnalyticsWidget(context), const SizedBox(height: 24), + makeLinksRow(), const Spacer(), Row( children: [ @@ -129,7 +64,7 @@ class AboutUI extends HookConsumerWidget { Container( width: MediaQuery.of(context).size.width * .35, decoration: BoxDecoration( - color: FluentTheme.of(context).cardColor.withOpacity(.03), + color: FluentTheme.of(context).cardColor.withOpacity(.01), borderRadius: BorderRadius.circular(12)), child: IconButton( 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 = "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 = "这是一个非官方的星际公民工具,不隶属于 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 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 { if (ConstConf.isMSE) { launchUrlString("ms-windows-store://pdp/?productid=9NF3SWFWNKL1"); diff --git a/lib/widgets/src/flow_number_text.dart b/lib/widgets/src/flow_number_text.dart new file mode 100644 index 0000000..6086044 --- /dev/null +++ b/lib/widgets/src/flow_number_text.dart @@ -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(0.0); + final timer = useState(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, + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 624e5ef..40e6d67 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; @@ -159,9 +161,14 @@ class LoadingWidget extends HookConsumerWidget { final T? data; final Future Function()? onLoadData; final Widget Function(BuildContext context, T data) childBuilder; + final Duration? autoRefreshDuration; const LoadingWidget( - {super.key, this.data, required this.childBuilder, this.onLoadData}); + {super.key, + this.data, + required this.childBuilder, + this.onLoadData, + this.autoRefreshDuration}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -170,7 +177,14 @@ class LoadingWidget extends HookConsumerWidget { useEffect(() { if (data == null && onLoadData != null) { _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; }, const []); @@ -205,4 +219,4 @@ addPostFrameCallback(Function() callback) { WidgetsBinding.instance.addPostFrameCallback((_) { callback(); }); -} \ No newline at end of file +}