diff --git a/assets/countdown/bis.png b/assets/countdown/bis.png new file mode 100644 index 0000000..fb0de63 Binary files /dev/null and b/assets/countdown/bis.png differ diff --git a/assets/countdown/ff.png b/assets/countdown/ff.png new file mode 100644 index 0000000..5825f30 Binary files /dev/null and b/assets/countdown/ff.png differ diff --git a/assets/countdown/iae.png b/assets/countdown/iae.png new file mode 100644 index 0000000..40138bc Binary files /dev/null and b/assets/countdown/iae.png differ diff --git a/assets/countdown/ilw.png b/assets/countdown/ilw.png new file mode 100644 index 0000000..2ec48d6 Binary files /dev/null and b/assets/countdown/ilw.png differ diff --git a/lib/api/api.dart b/lib/api/api.dart index 9185b5b..a38d6d7 100644 --- a/lib/api/api.dart +++ b/lib/api/api.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:starcitizen_doctor/common/conf.dart'; import 'package:starcitizen_doctor/data/app_placard_data.dart'; import 'package:starcitizen_doctor/data/app_version_data.dart'; +import 'package:starcitizen_doctor/data/countdown_festival_item_data.dart'; import 'package:starcitizen_doctor/data/sc_localization_data.dart'; class Api { @@ -20,6 +21,19 @@ class Api { await getRepoJson("sc_doctor", "placard.json")); } + static Future> + getFestivalCountdownList() async { + List l = []; + final r = json.decode(await getRepoData("sc_doctor", "countdown.json")); + if (r is List) { + for (var element in r) { + l.add(CountdownFestivalItemData.fromJson(element)); + } + } + l.sort((a, b) => (a.time ?? 0) - (b.time ?? 0)); + return l; + } + static Future> getAppReleaseDataByVersionName( String version) async { final r = await dio diff --git a/lib/data/countdown_festival_item_data.dart b/lib/data/countdown_festival_item_data.dart new file mode 100644 index 0000000..91f2c0a --- /dev/null +++ b/lib/data/countdown_festival_item_data.dart @@ -0,0 +1,24 @@ +class CountdownFestivalItemData { + CountdownFestivalItemData({ + this.name, + this.time, + this.icon,}); + + CountdownFestivalItemData.fromJson(dynamic json) { + name = json['name']; + time = json['time']; + icon = json['icon']; + } + String? name; + int? time; + String? icon; + + Map toJson() { + final map = {}; + map['name'] = name; + map['time'] = time; + map['icon'] = icon; + return map; + } + +} \ No newline at end of file diff --git a/lib/ui/home/home_ui.dart b/lib/ui/home/home_ui.dart index 0a885f6..93345df 100644 --- a/lib/ui/home/home_ui.dart +++ b/lib/ui/home/home_ui.dart @@ -1,3 +1,4 @@ +import 'package:card_swiper/card_swiper.dart'; import 'package:extended_image/extended_image.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -5,6 +6,7 @@ import 'package:flutter_tilt/flutter_tilt.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:starcitizen_doctor/api/analytics.dart'; import 'package:starcitizen_doctor/base/ui.dart'; +import 'package:starcitizen_doctor/widgets/countdown_time_text.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'home_ui_model.dart'; @@ -91,112 +93,183 @@ class HomeUI extends BaseUI { right: 24, child: Stack( children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: FluentTheme.of(context).cardColor.withOpacity(.03), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - makeWebViewButton(model, - icon: SvgPicture.asset( - "assets/rsi.svg", - colorFilter: makeSvgColor(Colors.white), - height: 18, - ), - name: "星际公民官网汉化", - webTitle: "星际公民官网汉化", - webURL: "https://robertsspaceindustries.com", - info: "罗伯茨航天工业公司,万物的起源", - useLocalization: true, - width: width, - touchKey: "webLocalization_rsi"), - const SizedBox(height: 12), - makeWebViewButton(model, - icon: Row( - children: [ - SvgPicture.asset( - "assets/uex.svg", + Column( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: + FluentTheme.of(context).cardColor.withOpacity(.03), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + makeWebViewButton(model, + icon: SvgPicture.asset( + "assets/rsi.svg", + colorFilter: makeSvgColor(Colors.white), height: 18, ), - const SizedBox(width: 12), - ], - ), - name: "UEX 汉化", - webTitle: "UEX 汉化", - webURL: "https://uexcorp.space", - info: "采矿、精炼、贸易计算器、价格、船信息", - useLocalization: true, - width: width, - touchKey: "webLocalization_uex"), - const SizedBox(height: 12), - makeWebViewButton(model, - icon: Row( + name: "星际公民官网汉化", + webTitle: "星际公民官网汉化", + webURL: "https://robertsspaceindustries.com", + info: "罗伯茨航天工业公司,万物的起源", + useLocalization: true, + width: width, + touchKey: "webLocalization_rsi"), + const SizedBox(height: 12), + makeWebViewButton(model, + icon: Row( + children: [ + SvgPicture.asset( + "assets/uex.svg", + height: 18, + ), + const SizedBox(width: 12), + ], + ), + name: "UEX 汉化", + webTitle: "UEX 汉化", + webURL: "https://uexcorp.space", + info: "采矿、精炼、贸易计算器、价格、船信息", + useLocalization: true, + width: width, + touchKey: "webLocalization_uex"), + const SizedBox(height: 12), + makeWebViewButton(model, + icon: Row( + children: [ + ExtendedImage.network( + "https://www.erkul.games/assets/icons/icon-512x512.png", + height: 20, + ), + const SizedBox(width: 12), + ], + ), + name: "DPS计算器汉化", + webTitle: "DPS计算器汉化", + webURL: + "https://www.erkul.games/live/calculator", + info: "在线改船,查询伤害数值和配件购买地点", + useLocalization: true, + width: width, + touchKey: "webLocalization_dps"), + const SizedBox(height: 12), + const Text("外部浏览器拓展:"), + const SizedBox(height: 8), + Row( children: [ - ExtendedImage.network( - "https://www.erkul.games/assets/icons/icon-512x512.png", - height: 20, + Button( + child: const FaIcon(FontAwesomeIcons.chrome, + size: 18), + onPressed: () { + launchUrlString( + "https://chrome.google.com/webstore/detail/gocnjckojmledijgmadmacoikibcggja?authuser=0&hl=zh-CN"); + }, ), const SizedBox(width: 12), + Button( + child: const FaIcon(FontAwesomeIcons.edge, + size: 18), + onPressed: () { + launchUrlString( + "https://microsoftedge.microsoft.com/addons/detail/lipbbcckldklpdcpfagicipecaacikgi"); + }, + ), + const SizedBox(width: 12), + Button( + child: const FaIcon( + FontAwesomeIcons.firefoxBrowser, + size: 18), + onPressed: () { + launchUrlString( + "https://addons.mozilla.org/zh-CN/firefox/" + "addon/%E6%98%9F%E9%99%85%E5%85%AC%E6%B0%91%E7%9B%92%E5%AD%90%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8B%93%E5%B1%95/"); + }, + ), + const SizedBox(width: 12), + Button( + child: const FaIcon(FontAwesomeIcons.github, + size: 18), + onPressed: () { + launchUrlString( + "https://github.com/xkeyC/StarCitizenBoxBrowserEx"); + }, + ), ], - ), - name: "DPS计算器汉化", - webTitle: "DPS计算器汉化", - webURL: "https://www.erkul.games/live/calculator", - info: "在线改船,查询伤害数值和配件购买地点", - useLocalization: true, - width: width, - touchKey: "webLocalization_dps"), - const SizedBox(height: 12), - const Text("外部浏览器拓展:"), - const SizedBox(height: 8), - Row( - children: [ - Button( - child: const FaIcon(FontAwesomeIcons.chrome, - size: 18), - onPressed: () { - launchUrlString( - "https://chrome.google.com/webstore/detail/gocnjckojmledijgmadmacoikibcggja?authuser=0&hl=zh-CN"); - }, - ), - const SizedBox(width: 12), - Button( - child: - const FaIcon(FontAwesomeIcons.edge, size: 18), - onPressed: () { - launchUrlString( - "https://microsoftedge.microsoft.com/addons/detail/lipbbcckldklpdcpfagicipecaacikgi"); - }, - ), - const SizedBox(width: 12), - Button( - child: const FaIcon( - FontAwesomeIcons.firefoxBrowser, - size: 18), - onPressed: () { - launchUrlString( - "https://addons.mozilla.org/zh-CN/firefox/" - "addon/%E6%98%9F%E9%99%85%E5%85%AC%E6%B0%91%E7%9B%92%E5%AD%90%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8B%93%E5%B1%95/"); - }, - ), - const SizedBox(width: 12), - Button( - child: const FaIcon(FontAwesomeIcons.github, - size: 18), - onPressed: () { - launchUrlString( - "https://github.com/xkeyC/StarCitizenBoxBrowserEx"); - }, - ), + ) ], - ) - ], + ), + ), ), - ), + const SizedBox(height: 12), + Container( + width: width + 24, + decoration: BoxDecoration( + color: FluentTheme.of(context).cardColor, + borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.only( + left: 12, right: 12, top: 6, bottom: 6), + child: (model.countdownFestivalListData == null) + ? SizedBox( + width: width, + height: 62, + child: const Center( + child: ProgressRing(), + ), + ) + : SizedBox( + width: width, + height: 62, + child: Swiper( + itemCount: + model.countdownFestivalListData!.length, + autoplay: true, + autoplayDelay: 5000, + itemBuilder: (context, index) { + final item = model + .countdownFestivalListData![index]; + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + if (item.icon != null && + item.icon != "") ...[ + ClipRRect( + borderRadius: + BorderRadius.circular(1000), + child: Image.asset( + "assets/countdown/${item.icon}", + width: 48, + height: 48, + ), + ), + ], + Column( + children: [ + Text( + item.name ?? "", + style: const TextStyle( + fontSize: 15), + ), + const SizedBox(height: 3), + CountdownTimeText( + targetTime: DateTime + .fromMillisecondsSinceEpoch( + item.time ?? 0), + ), + ], + ), + ], + ); + }, + ), + ), + )), + ], ), if (model.appWebLocalizationVersionsData == null) Positioned.fill( @@ -234,60 +307,68 @@ class HomeUI extends BaseUI { Tilt( shadowConfig: const ShadowConfig(maxIntensity: .2), borderRadius: BorderRadius.circular(12), - child: Container( - width: width, - decoration: BoxDecoration( - color: FluentTheme.of(context).cardColor, - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column(children: [ - const Row( - children: [ - Text("星际公民服务器状态:"), - ], - ), - const SizedBox(height: 12), - if (model.scServerStatus == null) - makeLoading(context, width: 20) - else - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: GestureDetector( + onTap: () { + model.goWebView("RSI 服务器状态", + "https://status.robertsspaceindustries.com/", + useLocalization: true); + }, + child: Container( + width: width, + decoration: BoxDecoration( + color: FluentTheme.of(context).cardColor, + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column(children: [ + const Row( children: [ - for (final item in model.scServerStatus ?? []) - Row( - children: [ - SizedBox( - height: 14, - child: Center( - child: Icon( - FontAwesomeIcons.solidCircle, - color: - model.isRSIServerStatusOK(item) - ? Colors.green - : Colors.red, - size: 12, + Text("星际公民服务器状态:"), + ], + ), + const SizedBox(height: 12), + if (model.scServerStatus == null) + makeLoading(context, width: 20) + else + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + for (final item in model.scServerStatus ?? []) + Row( + children: [ + SizedBox( + height: 14, + child: Center( + child: Icon( + FontAwesomeIcons.solidCircle, + color: model + .isRSIServerStatusOK(item) + ? Colors.green + : Colors.red, + size: 12, + ), ), ), - ), - const SizedBox(width: 3), - Text( - "${model.statusCnName[item["name"]] ?? item["name"]}", - style: const TextStyle(fontSize: 13), - ), - ], - ) - ], - ) - ]), + const SizedBox(width: 3), + Text( + "${model.statusCnName[item["name"]] ?? item["name"]}", + style: const TextStyle(fontSize: 13), + ), + ], + ) + ], + ) + ]), + ), + // child: IconButton( + // icon: , + // onPressed: () { + // launchUrlString( + // "https://status.robertsspaceindustries.com/"); + // }, + // ), ), - // child: IconButton( - // icon: , - // onPressed: () { - // launchUrlString( - // "https://status.robertsspaceindustries.com/"); - // }, - // ), ), ), ], diff --git a/lib/ui/home/home_ui_model.dart b/lib/ui/home/home_ui_model.dart index d3c00c9..42711ec 100644 --- a/lib/ui/home/home_ui_model.dart +++ b/lib/ui/home/home_ui_model.dart @@ -13,6 +13,7 @@ import 'package:starcitizen_doctor/common/helper/log_helper.dart'; import 'package:starcitizen_doctor/common/helper/system_helper.dart'; import 'package:starcitizen_doctor/data/app_placard_data.dart'; import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart'; +import 'package:starcitizen_doctor/data/countdown_festival_item_data.dart'; import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui.dart'; import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui_model.dart'; import 'package:starcitizen_doctor/ui/home/localization/localization_ui_model.dart'; @@ -54,6 +55,8 @@ class HomeUIModel extends BaseUIModel { AppWebLocalizationVersionsData? appWebLocalizationVersionsData; + List? countdownFestivalListData; + final cnExp = RegExp(r"[^\x00-\xff]"); AppPlacardData? appPlacardData; @@ -89,6 +92,8 @@ class HomeUIModel extends BaseUIModel { "${AppConf.webTranslateHomeUrl}/versions.json", options: Options(responseType: ResponseType.plain))) .data)); + countdownFestivalListData = await Api.getFestivalCountdownList(); + notifyListeners(); } catch (e) { dPrint(e); } diff --git a/lib/widgets/countdown_time_text.dart b/lib/widgets/countdown_time_text.dart new file mode 100644 index 0000000..bf54c4b --- /dev/null +++ b/lib/widgets/countdown_time_text.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:fluent_ui/fluent_ui.dart'; + +class CountdownTimeText extends StatefulWidget { + final DateTime targetTime; + + const CountdownTimeText({super.key, required this.targetTime}); + + @override + State createState() => _CountdownTimeTextState(); +} + +class _CountdownTimeTextState extends State { + Timer? _timer; + + Widget? textWidget; + + bool stopTimer = false; + + @override + initState() { + _onUpdateTime(null); + if (!stopTimer) { + _timer = Timer.periodic(const Duration(seconds: 1), _onUpdateTime); + } + super.initState(); + } + + @override + dispose() { + _timer?.cancel(); + _timer = null; + super.dispose(); + } + + _onUpdateTime(_) { + final now = DateTime.now(); + final dur = widget.targetTime.difference(now); + setState(() { + textWidget = _chineseTimeText(dur); + }); + // 时间到,停止计时,并向宿主传递超时信息 + if (dur.inMilliseconds <= 0) { + stopTimer = true; + setState(() {}); + } + if (stopTimer) { + _timer?.cancel(); + _timer = null; + } + } + + Widget _chineseTimeText(Duration duration) { + final surplus = duration; + int day = (surplus.inSeconds ~/ 3600) ~/ 24; + int hour = (surplus.inSeconds ~/ 3600) % 24; + int minute = surplus.inSeconds % 3600 ~/ 60; + int second = surplus.inSeconds % 60; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "$day天 ", + style: TextStyle( + fontSize: 24, color: day < 30 ? Colors.red : Colors.white), + ), + Text("${timePart(hour)}:${timePart(minute)}:${timePart(second)}"), + ], + ); + } + + String timePart(int p) { + if (p.toString().length == 1) return "0$p"; + return "$p"; + } + + @override + Widget build(BuildContext context) { + if (stopTimer) { + return const Text( + "正在进行中", + style: TextStyle( + fontSize: 18, + color: Color.fromRGBO(32, 220, 89, 1.0), + fontWeight: FontWeight.bold), + ); + } + return textWidget ?? const Text(""); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index e208361..95c3307 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: jwt_decode: ^0.3.1 uuid: ^4.1.0 flutter_tilt: ^2.0.10 + card_swiper: ^3.0.1 dev_dependencies: flutter_test: @@ -77,6 +78,7 @@ flutter: uses-material-design: true assets: - assets/ + - assets/countdown/ # To add assets to your application, add an assets section, like this: # assets: