From e70fca4899d9a2e1f64dca2eabe94de88ec3e941 Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Sat, 9 Mar 2024 21:53:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:riverpod=20=E8=BF=81=E7=A7=BB=20HomeUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app.dart | 1 - lib/ui/home/home_ui.dart | 738 +++++++++++++++++++++++ lib/ui/home/home_ui_model.dart | 246 ++++++++ lib/ui/home/home_ui_model.freezed.dart | 458 ++++++++++++++ lib/ui/home/home_ui_model.g.dart | 25 + lib/ui/index_ui.dart | 16 +- lib/ui/settings/upgrade_dialog.dart | 1 - lib/widgets/src/cache_image.dart | 41 ++ lib/widgets/src/countdown_time_text.dart | 91 +++ lib/widgets/widgets.dart | 23 + 10 files changed, 1635 insertions(+), 5 deletions(-) create mode 100644 lib/ui/home/home_ui.dart create mode 100644 lib/ui/home/home_ui_model.dart create mode 100644 lib/ui/home/home_ui_model.freezed.dart create mode 100644 lib/ui/home/home_ui_model.g.dart create mode 100644 lib/widgets/src/cache_image.dart create mode 100644 lib/widgets/src/countdown_time_text.dart diff --git a/lib/app.dart b/lib/app.dart index 727b403..52af49f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -22,7 +22,6 @@ import 'api/api.dart'; import 'common/helper/system_helper.dart'; import 'common/io/rs_http.dart'; import 'common/rust/frb_generated.dart'; -import 'common/utils/base_utils.dart'; import 'data/app_version_data.dart'; import 'ui/index_ui.dart'; import 'ui/settings/upgrade_dialog.dart'; diff --git a/lib/ui/home/home_ui.dart b/lib/ui/home/home_ui.dart new file mode 100644 index 0000000..31139fc --- /dev/null +++ b/lib/ui/home/home_ui.dart @@ -0,0 +1,738 @@ +import 'package:card_swiper/card_swiper.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_tilt/flutter_tilt.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/common/helper/system_helper.dart'; +import 'package:starcitizen_doctor/widgets/widgets.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import 'home_ui_model.dart'; + +class HomeUI extends HookConsumerWidget { + const HomeUI({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final homeState = ref.watch(homeUIModelProvider); + final model = ref.watch(homeUIModelProvider.notifier); + return Stack( + children: [ + Center( + child: SingleChildScrollView( + padding: EdgeInsets.zero, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (homeState.appPlacardData != null) ...[ + InfoBar( + title: Text("${homeState.appPlacardData?.title}"), + content: Text("${homeState.appPlacardData?.content}"), + severity: InfoBarSeverity.info, + action: homeState.appPlacardData?.link == null + ? null + : Button( + child: const Text('查看详情'), + onPressed: () => _showPlacard(), + ), + onClose: homeState.appPlacardData?.alwaysShow == true + ? null + : () => model.closePlacard(), + ), + const SizedBox(height: 6), + ], + ...makeIndex(context, model, homeState) + ], + ), + ), + ), + if (homeState.isFixing) + Container( + decoration: BoxDecoration( + color: Colors.black.withAlpha(150), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ProgressRing(), + const SizedBox(height: 12), + Text(homeState.isFixingString.isNotEmpty + ? homeState.isFixingString + : "正在处理..."), + ], + ), + ), + ) + ], + ); + } + + List makeIndex( + BuildContext context, HomeUIModel model, HomeUIModelState homeState) { + const double width = 280; + return [ + Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 64, bottom: 0), + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 30), + child: Image.asset( + "assets/sc_logo.png", + fit: BoxFit.fitHeight, + height: 260, + ), + ), + makeGameStatusCard(context, model, 340, homeState) + ], + ), + ), + ), + ), + Positioned( + top: 0, + left: 24, + child: makeLeftColumn(context, model, width, homeState), + ), + Positioned( + right: 24, + top: 0, + child: makeNewsCard(context, model, homeState), + ), + ], + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.only(left: 24, right: 24), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("安装位置:"), + const SizedBox(width: 6), + Expanded( + child: ComboBox( + value: homeState.scInstalledPath, + items: [ + const ComboBoxItem( + value: "not_install", + child: Text("未安装 或 安装失败"), + ), + for (final path in homeState.scInstallPaths) + ComboBoxItem( + value: path, + child: Text(path), + ) + ], + onChanged: model.onChangeInstallPath, + ), + ), + const SizedBox(width: 12), + Button( + onPressed: homeState.webLocalizationVersionsData == null + ? null + : () => model.launchRSI(context), + child: Padding( + padding: const EdgeInsets.all(6), + child: Icon( + homeState.isCurGameRunning + ? FluentIcons.stop_solid + : FluentIcons.play, + color: homeState.isCurGameRunning + ? Colors.red.withOpacity(.8) + : null, + ), + )), + const SizedBox(width: 12), + Button( + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon(FluentIcons.folder_open), + ), + onPressed: () => SystemHelper.openDir(homeState.scInstalledPath), + ), + const SizedBox(width: 12), + Button( + onPressed: model.reScanPath, + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon(FluentIcons.refresh), + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Text(homeState.lastScreenInfo, maxLines: 1), + makeIndexActionLists(context, model, homeState), + ]; + } + + Widget makeLeftColumn(BuildContext context, HomeUIModel model, double width, + HomeUIModelState homeState) { + return Stack( + children: [ + 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(context, 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(context, 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(context, model, + icon: Row( + children: [ + Image.asset( + "assets/dps.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: 12), + 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/StarCitizenToolBox/StarCitizenBoxBrowserEx"); + }, + ), + ], + ) + ], + ), + ), + ), + const SizedBox(height: 16), + makeActivityBanner(context, model, width, homeState), + ], + ), + if (homeState.webLocalizationVersionsData == null) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(.3), + borderRadius: BorderRadius.circular(12)), + child: const Center( + child: ProgressRing(), + ), + )) + ], + ); + } + + Widget makeNewsCard( + BuildContext context, HomeUIModel model, HomeUIModelState homeState) { + return ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: Container( + width: 316, + height: 386, + decoration: BoxDecoration( + color: Colors.white.withOpacity(.1), + borderRadius: BorderRadius.circular(12)), + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox( + height: 190, + width: 316, + child: Tilt( + shadowConfig: const ShadowConfig(maxIntensity: .3), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: homeState.rssVideoItems == null + ? Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(.1)), + child: makeLoading(context), + ) + : Swiper( + itemCount: homeState.rssVideoItems?.length ?? 0, + itemBuilder: (context, index) { + final item = homeState.rssVideoItems![index]; + return GestureDetector( + onTap: () { + if (item.link != null) { + launchUrlString(item.link!); + } + }, + child: CacheNetImage( + url: model.getRssImage(item), + fit: BoxFit.cover, + ), + ); + }, + autoplay: true, + ), + )), + const SizedBox(height: 1), + if (homeState.rssTextItems == null) + makeLoading(context) + else + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + final item = homeState.rssTextItems![index]; + return Tilt( + shadowConfig: const ShadowConfig(maxIntensity: .3), + borderRadius: BorderRadius.circular(12), + child: GestureDetector( + onTap: () { + if (item.link != null) { + launchUrlString(item.link!); + } + }, + child: Padding( + padding: const EdgeInsets.only( + left: 12, right: 12, top: 4, bottom: 4), + child: Row( + children: [ + getRssIcon(item.link ?? ""), + const SizedBox(width: 6), + Expanded( + child: Text( + model.handleTitle(item.title), + textAlign: TextAlign.start, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12.2), + ), + ), + const SizedBox(width: 12), + Icon( + FluentIcons.chevron_right, + size: 12, + color: Colors.white.withOpacity(.4), + ) + ], + ), + ), + )); + }, + itemCount: homeState.rssTextItems?.length, + ), + const SizedBox(height: 12), + ], + ), + )), + ); + } + + Widget getRssIcon(String url) { + if (url.startsWith("https://tieba.baidu.com")) { + return SvgPicture.asset("assets/tieba.svg", width: 14, height: 14); + } + + if (url.startsWith("https://www.bilibili.com")) { + return const FaIcon( + FontAwesomeIcons.bilibili, + size: 14, + color: Color.fromRGBO(0, 161, 214, 1), + ); + } + + return const FaIcon(FontAwesomeIcons.rss, size: 14); + } + + Widget makeIndexActionLists( + BuildContext context, HomeUIModel model, HomeUIModelState homeState) { + final items = [ + _HomeItemData("auto_check", "一键诊断", "一键诊断星际公民常见问题", + FluentIcons.auto_deploy_settings), + _HomeItemData( + "localization", "汉化管理", "快捷安装汉化资源", FluentIcons.locale_language), + _HomeItemData("performance", "性能优化", "调整引擎配置文件,优化游戏性能", + FluentIcons.process_meta_task), + ]; + return Padding( + padding: const EdgeInsets.all(24), + child: AlignedGridView.count( + crossAxisCount: 3, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemCount: items.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final item = items.elementAt(index); + return HoverButton( + onPressed: () => model.onMenuTap(item.key), + builder: (BuildContext context, Set states) { + return Container( + width: 300, + height: 120, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: states.isHovering + ? FluentTheme.of(context).cardColor.withOpacity(.1) + : FluentTheme.of(context).cardColor, + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(.2), + borderRadius: BorderRadius.circular(1000)), + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + item.icon, + size: 26, + ), + ), + ), + const SizedBox(width: 24), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + style: const TextStyle(fontSize: 18), + ), + const SizedBox(height: 4), + Text(item.infoString), + ], + )), + if (item.key == "localization" && + homeState.localizationUpdateInfo != null) + Container( + padding: const EdgeInsets.only( + top: 3, bottom: 3, left: 8, right: 8), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(12)), + child: Text( + homeState.localizationUpdateInfo?.key ?? " "), + ), + const SizedBox(width: 12), + const Icon( + FluentIcons.chevron_right, + size: 16, + ) + ], + ), + ), + ); + }, + ); + }), + ); + } + + Widget makeWebViewButton(BuildContext context, HomeUIModel model, + {required Widget icon, + required String name, + required String webTitle, + required String webURL, + required bool useLocalization, + required double width, + String? info, + String? touchKey}) { + return Tilt( + shadowConfig: const ShadowConfig(maxIntensity: .3), + borderRadius: BorderRadius.circular(12), + child: GestureDetector( + onTap: () { + if (touchKey != null) { + AnalyticsApi.touch(touchKey); + } + model.goWebView(webTitle, webURL, useLocalization: true); + }, + child: Container( + width: width, + decoration: BoxDecoration( + color: FluentTheme.of(context).cardColor, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + icon, + Text( + name, + style: const TextStyle(fontSize: 14), + ), + ], + ), + if (info != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + info, + style: TextStyle( + fontSize: 12, + color: Colors.white.withOpacity(.6)), + ), + ) + ], + ), + ), + const SizedBox(width: 12), + Icon( + FluentIcons.chevron_right, + size: 14, + color: Colors.white.withOpacity(.6), + ) + ], + ), + ), + ), + ), + ); + } + + Widget makeGameStatusCard(BuildContext context, HomeUIModel model, + double width, HomeUIModelState homeState) { + const statusCnName = { + "Platform": "平台", + "Persistent Universe": "持续宇宙", + "Electronic Access": "电子访问", + "Arena Commander": "竞技场指挥官" + }; + + return Tilt( + shadowConfig: const ShadowConfig(maxIntensity: .2), + borderRadius: BorderRadius.circular(12), + 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: [ + if (homeState.scServerStatus == null) + makeLoading(context, width: 20) + else + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("状态:"), + for (final item in homeState.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: 5), + Text( + "${statusCnName[item["name"]] ?? item["name"]}", + style: const TextStyle(fontSize: 13), + ), + ], + ), + Icon( + FluentIcons.chevron_right, + size: 12, + color: Colors.white.withOpacity(.4), + ) + ], + ) + ]), + ), + ), + ), + ); + } + + Widget makeActivityBanner(BuildContext context, HomeUIModel model, + double width, HomeUIModelState homeState) { + return Tilt( + borderRadius: BorderRadius.circular(12), + shadowConfig: const ShadowConfig(disable: true), + child: GestureDetector( + onTap: () => _onTapFestival(), + child: Container( + width: width + 24, + decoration: BoxDecoration(color: FluentTheme.of(context).cardColor), + child: Padding( + padding: + const EdgeInsets.only(left: 12, right: 12, top: 8, bottom: 8), + child: (homeState.countdownFestivalListData == null) + ? SizedBox( + width: width, + height: 62, + child: const Center( + child: ProgressRing(), + ), + ) + : SizedBox( + width: width, + height: 62, + child: Swiper( + itemCount: homeState.countdownFestivalListData!.length, + autoplay: true, + autoplayDelay: 5000, + itemBuilder: (context, index) { + final item = + homeState.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, + fit: BoxFit.cover, + ), + ), + ], + Column( + children: [ + Text( + item.name ?? "", + style: const TextStyle(fontSize: 15), + ), + const SizedBox(height: 3), + CountdownTimeText( + targetTime: + DateTime.fromMillisecondsSinceEpoch( + item.time ?? 0), + ), + ], + ), + const SizedBox(width: 12), + Icon( + FluentIcons.chevron_right, + size: 14, + color: Colors.white.withOpacity(.6), + ) + ], + ); + }, + ), + ), + )), + ), + ); + } + + _showPlacard() {} + + _onTapFestival() {} +} + +class _HomeItemData { + String key; + + _HomeItemData(this.key, this.name, this.infoString, this.icon); + + String name; + String infoString; + IconData icon; +} diff --git a/lib/ui/home/home_ui_model.dart b/lib/ui/home/home_ui_model.dart new file mode 100644 index 0000000..52ac3f1 --- /dev/null +++ b/lib/ui/home/home_ui_model.dart @@ -0,0 +1,246 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dart_rss/domain/rss_item.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hive/hive.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:starcitizen_doctor/api/analytics.dart'; +import 'package:starcitizen_doctor/api/api.dart'; +import 'package:starcitizen_doctor/api/rss.dart'; +import 'package:starcitizen_doctor/common/conf/const_conf.dart'; +import 'package:starcitizen_doctor/common/conf/url_conf.dart'; +import 'package:starcitizen_doctor/common/helper/log_helper.dart'; +import 'package:starcitizen_doctor/common/helper/system_helper.dart'; +import 'package:starcitizen_doctor/common/io/rs_http.dart'; +import 'package:starcitizen_doctor/common/utils/base_utils.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/common/utils/provider.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:url_launcher/url_launcher_string.dart'; +import 'package:html/parser.dart' as html; +import 'package:html/dom.dart' as html_dom; + +part 'home_ui_model.freezed.dart'; + +part 'home_ui_model.g.dart'; + +@freezed +class HomeUIModelState with _$HomeUIModelState { + factory HomeUIModelState({ + AppPlacardData? appPlacardData, + @Default(false) bool isFixing, + @Default("") String isFixingString, + String? scInstalledPath, + @Default([]) List scInstallPaths, + AppWebLocalizationVersionsData? webLocalizationVersionsData, + @Default(false) bool isCurGameRunning, + @Default("") String lastScreenInfo, + List? rssVideoItems, + List? rssTextItems, + MapEntry? localizationUpdateInfo, + List? scServerStatus, + List? countdownFestivalListData, + }) = _HomeUIModelState; +} + +@riverpod +class HomeUIModel extends _$HomeUIModel { + @override + HomeUIModelState build() { + state = HomeUIModelState(); + _init(); + _loadData(); + return state; + } + + closePlacard() async { + final box = await Hive.openBox("app_conf"); + await box.put("close_placard", state.appPlacardData?.version); + state = state.copyWith(appPlacardData: null); + } + + Future reScanPath() async { + state = state.copyWith( + scInstalledPath: "not_install", lastScreenInfo: "正在扫描 ..."); + try { + final listData = await SCLoggerHelper.getLauncherLogList(); + if (listData == null) { + state = state.copyWith(scInstalledPath: "not_install"); + return; + } + final scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData, + withVersion: ["LIVE", "PTU", "EPTU"], checkExists: true); + String? scInstalledPath; + if (scInstallPaths.isNotEmpty) { + scInstalledPath = scInstallPaths.first; + } + final lastScreenInfo = "扫描完毕,共找到 ${scInstallPaths.length} 个有效安装目录"; + state = state.copyWith( + scInstalledPath: scInstalledPath, + scInstallPaths: scInstallPaths, + lastScreenInfo: lastScreenInfo); + } catch (e) { + state = state.copyWith( + scInstalledPath: "not_install", lastScreenInfo: "解析 log 文件失败!"); + AnalyticsApi.touch("error_launchLogs"); + // showToast(context!, + // "解析 log 文件失败! \n请关闭游戏,退出RSI启动器后重试,若仍有问题,请使用工具箱中的 RSI Launcher log 修复。"); + } + } + + String getRssImage(RssItem item) { + final h = html.parse(item.description ?? ""); + if (h.body == null) return ""; + for (var node in h.body!.nodes) { + if (node is html_dom.Element) { + if (node.localName == "img") { + return node.attributes["src"]?.trim() ?? ""; + } + } + } + return ""; + } + + String handleTitle(String? title) { + if (title == null) return ""; + title = title.replaceAll("【", "[ "); + title = title.replaceAll("】", " ] "); + return title; + } + + onMenuTap(String key) {} + + void goWebView(String webTitle, String webURL, + {required bool useLocalization}) {} + + bool isRSIServerStatusOK(Map map) { + return (map["status"] == "ok" || map["status"] == "operational"); + } + + Timer? _serverUpdateTimer; + Timer? _appUpdateTimer; + + void _init() { + reScanPath(); + _serverUpdateTimer = Timer.periodic( + const Duration(minutes: 10), + (timer) { + _updateSCServerStatus(); + }, + ); + + _appUpdateTimer = Timer.periodic(const Duration(minutes: 30), (timer) { + _checkLocalizationUpdate(); + }); + + ref.onDispose(() { + _serverUpdateTimer?.cancel(); + _serverUpdateTimer = null; + _appUpdateTimer?.cancel(); + _appUpdateTimer = null; + }); + } + + void _loadData() async { + if (appGlobalState.networkVersionData == null) return; + try { + final r = await Api.getAppPlacard(); + final box = await Hive.openBox("app_conf"); + final version = box.get("close_placard", defaultValue: ""); + if (r.enable == true) { + if (r.alwaysShow != true && version == r.version) { + } else { + state = state.copyWith(appPlacardData: r); + } + } + + final appWebLocalizationVersionsData = + AppWebLocalizationVersionsData.fromJson(json.decode( + (await RSHttp.getText( + "${URLConf.webTranslateHomeUrl}/versions.json")))); + final countdownFestivalListData = await Api.getFestivalCountdownList(); + state = state.copyWith( + webLocalizationVersionsData: appWebLocalizationVersionsData, + countdownFestivalListData: countdownFestivalListData); + _updateSCServerStatus(); + _loadRRS(); + } catch (e) { + dPrint(e); + } + // check Localization update + _checkLocalizationUpdate(); + } + + Future _updateSCServerStatus() async { + try { + final s = await Api.getScServerStatus(); + dPrint("updateSCServerStatus===$s"); + state = state.copyWith(scServerStatus: s); + } catch (e) { + dPrint(e); + } + } + + Future _loadRRS() async { + try { + state = state.copyWith( + rssVideoItems: await RSSApi.getRssVideo(), + rssTextItems: await RSSApi.getRssText()); + dPrint("RSS update Success !"); + } catch (e) { + dPrint("_loadRRS Error:$e"); + } + } + + void _checkLocalizationUpdate() {} + + // ignore: avoid_build_context_in_providers + launchRSI(BuildContext context) async { + if (state.scInstalledPath == "not_install") { + showToast(context, "该功能需要一个有效的安装位置"); + return; + } + + if (ConstConf.isMSE) { + if (state.isCurGameRunning) { + await Process.run( + SystemHelper.powershellPath, ["ps \"StarCitizen\" | kill"]); + return; + } + AnalyticsApi.touch("gameLaunch"); + // showDialog( + // context: context, + // dismissWithEsc: false, + // builder: (context) { + // return BaseUIContainer( + // uiCreate: () => LoginDialog(), + // modelCreate: () => LoginDialogModel(scInstalledPath, this)); + // }); + } else { + final ok = await showConfirmDialogs( + context, + "一键启动功能提示", + const Text("为确保账户安全,一键启动功能已在开发版中禁用,我们将在微软商店版本中提供此功能。" + "\n\n微软商店版由微软提供可靠的分发下载与数字签名,可有效防止软件被恶意篡改。\n\n提示:您无需使用盒子启动游戏也可使用汉化。"), + confirm: "安装微软商店版本", + cancel: "取消"); + if (ok == true) { + await launchUrlString( + "https://apps.microsoft.com/detail/9NF3SWFWNKL1?launch=true"); + await Future.delayed(const Duration(seconds: 2)); + exit(0); + } + } + } + + void onChangeInstallPath(String? value) { + if (value == null) return; + state = state.copyWith(scInstalledPath: value); + } +} diff --git a/lib/ui/home/home_ui_model.freezed.dart b/lib/ui/home/home_ui_model.freezed.dart new file mode 100644 index 0000000..ffebe40 --- /dev/null +++ b/lib/ui/home/home_ui_model.freezed.dart @@ -0,0 +1,458 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'home_ui_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$HomeUIModelState { + AppPlacardData? get appPlacardData => throw _privateConstructorUsedError; + bool get isFixing => throw _privateConstructorUsedError; + String get isFixingString => throw _privateConstructorUsedError; + String? get scInstalledPath => throw _privateConstructorUsedError; + List get scInstallPaths => throw _privateConstructorUsedError; + AppWebLocalizationVersionsData? get webLocalizationVersionsData => + throw _privateConstructorUsedError; + bool get isCurGameRunning => throw _privateConstructorUsedError; + String get lastScreenInfo => throw _privateConstructorUsedError; + List? get rssVideoItems => throw _privateConstructorUsedError; + List? get rssTextItems => throw _privateConstructorUsedError; + MapEntry? get localizationUpdateInfo => + throw _privateConstructorUsedError; + List? get scServerStatus => throw _privateConstructorUsedError; + List? get countdownFestivalListData => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $HomeUIModelStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HomeUIModelStateCopyWith<$Res> { + factory $HomeUIModelStateCopyWith( + HomeUIModelState value, $Res Function(HomeUIModelState) then) = + _$HomeUIModelStateCopyWithImpl<$Res, HomeUIModelState>; + @useResult + $Res call( + {AppPlacardData? appPlacardData, + bool isFixing, + String isFixingString, + String? scInstalledPath, + List scInstallPaths, + AppWebLocalizationVersionsData? webLocalizationVersionsData, + bool isCurGameRunning, + String lastScreenInfo, + List? rssVideoItems, + List? rssTextItems, + MapEntry? localizationUpdateInfo, + List? scServerStatus, + List? countdownFestivalListData}); +} + +/// @nodoc +class _$HomeUIModelStateCopyWithImpl<$Res, $Val extends HomeUIModelState> + implements $HomeUIModelStateCopyWith<$Res> { + _$HomeUIModelStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? appPlacardData = freezed, + Object? isFixing = null, + Object? isFixingString = null, + Object? scInstalledPath = freezed, + Object? scInstallPaths = null, + Object? webLocalizationVersionsData = freezed, + Object? isCurGameRunning = null, + Object? lastScreenInfo = null, + Object? rssVideoItems = freezed, + Object? rssTextItems = freezed, + Object? localizationUpdateInfo = freezed, + Object? scServerStatus = freezed, + Object? countdownFestivalListData = freezed, + }) { + return _then(_value.copyWith( + appPlacardData: freezed == appPlacardData + ? _value.appPlacardData + : appPlacardData // ignore: cast_nullable_to_non_nullable + as AppPlacardData?, + isFixing: null == isFixing + ? _value.isFixing + : isFixing // ignore: cast_nullable_to_non_nullable + as bool, + isFixingString: null == isFixingString + ? _value.isFixingString + : isFixingString // ignore: cast_nullable_to_non_nullable + as String, + scInstalledPath: freezed == scInstalledPath + ? _value.scInstalledPath + : scInstalledPath // ignore: cast_nullable_to_non_nullable + as String?, + scInstallPaths: null == scInstallPaths + ? _value.scInstallPaths + : scInstallPaths // ignore: cast_nullable_to_non_nullable + as List, + webLocalizationVersionsData: freezed == webLocalizationVersionsData + ? _value.webLocalizationVersionsData + : webLocalizationVersionsData // ignore: cast_nullable_to_non_nullable + as AppWebLocalizationVersionsData?, + isCurGameRunning: null == isCurGameRunning + ? _value.isCurGameRunning + : isCurGameRunning // ignore: cast_nullable_to_non_nullable + as bool, + lastScreenInfo: null == lastScreenInfo + ? _value.lastScreenInfo + : lastScreenInfo // ignore: cast_nullable_to_non_nullable + as String, + rssVideoItems: freezed == rssVideoItems + ? _value.rssVideoItems + : rssVideoItems // ignore: cast_nullable_to_non_nullable + as List?, + rssTextItems: freezed == rssTextItems + ? _value.rssTextItems + : rssTextItems // ignore: cast_nullable_to_non_nullable + as List?, + localizationUpdateInfo: freezed == localizationUpdateInfo + ? _value.localizationUpdateInfo + : localizationUpdateInfo // ignore: cast_nullable_to_non_nullable + as MapEntry?, + scServerStatus: freezed == scServerStatus + ? _value.scServerStatus + : scServerStatus // ignore: cast_nullable_to_non_nullable + as List?, + countdownFestivalListData: freezed == countdownFestivalListData + ? _value.countdownFestivalListData + : countdownFestivalListData // ignore: cast_nullable_to_non_nullable + as List?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HomeUIModelStateImplCopyWith<$Res> + implements $HomeUIModelStateCopyWith<$Res> { + factory _$$HomeUIModelStateImplCopyWith(_$HomeUIModelStateImpl value, + $Res Function(_$HomeUIModelStateImpl) then) = + __$$HomeUIModelStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {AppPlacardData? appPlacardData, + bool isFixing, + String isFixingString, + String? scInstalledPath, + List scInstallPaths, + AppWebLocalizationVersionsData? webLocalizationVersionsData, + bool isCurGameRunning, + String lastScreenInfo, + List? rssVideoItems, + List? rssTextItems, + MapEntry? localizationUpdateInfo, + List? scServerStatus, + List? countdownFestivalListData}); +} + +/// @nodoc +class __$$HomeUIModelStateImplCopyWithImpl<$Res> + extends _$HomeUIModelStateCopyWithImpl<$Res, _$HomeUIModelStateImpl> + implements _$$HomeUIModelStateImplCopyWith<$Res> { + __$$HomeUIModelStateImplCopyWithImpl(_$HomeUIModelStateImpl _value, + $Res Function(_$HomeUIModelStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? appPlacardData = freezed, + Object? isFixing = null, + Object? isFixingString = null, + Object? scInstalledPath = freezed, + Object? scInstallPaths = null, + Object? webLocalizationVersionsData = freezed, + Object? isCurGameRunning = null, + Object? lastScreenInfo = null, + Object? rssVideoItems = freezed, + Object? rssTextItems = freezed, + Object? localizationUpdateInfo = freezed, + Object? scServerStatus = freezed, + Object? countdownFestivalListData = freezed, + }) { + return _then(_$HomeUIModelStateImpl( + appPlacardData: freezed == appPlacardData + ? _value.appPlacardData + : appPlacardData // ignore: cast_nullable_to_non_nullable + as AppPlacardData?, + isFixing: null == isFixing + ? _value.isFixing + : isFixing // ignore: cast_nullable_to_non_nullable + as bool, + isFixingString: null == isFixingString + ? _value.isFixingString + : isFixingString // ignore: cast_nullable_to_non_nullable + as String, + scInstalledPath: freezed == scInstalledPath + ? _value.scInstalledPath + : scInstalledPath // ignore: cast_nullable_to_non_nullable + as String?, + scInstallPaths: null == scInstallPaths + ? _value._scInstallPaths + : scInstallPaths // ignore: cast_nullable_to_non_nullable + as List, + webLocalizationVersionsData: freezed == webLocalizationVersionsData + ? _value.webLocalizationVersionsData + : webLocalizationVersionsData // ignore: cast_nullable_to_non_nullable + as AppWebLocalizationVersionsData?, + isCurGameRunning: null == isCurGameRunning + ? _value.isCurGameRunning + : isCurGameRunning // ignore: cast_nullable_to_non_nullable + as bool, + lastScreenInfo: null == lastScreenInfo + ? _value.lastScreenInfo + : lastScreenInfo // ignore: cast_nullable_to_non_nullable + as String, + rssVideoItems: freezed == rssVideoItems + ? _value._rssVideoItems + : rssVideoItems // ignore: cast_nullable_to_non_nullable + as List?, + rssTextItems: freezed == rssTextItems + ? _value._rssTextItems + : rssTextItems // ignore: cast_nullable_to_non_nullable + as List?, + localizationUpdateInfo: freezed == localizationUpdateInfo + ? _value.localizationUpdateInfo + : localizationUpdateInfo // ignore: cast_nullable_to_non_nullable + as MapEntry?, + scServerStatus: freezed == scServerStatus + ? _value._scServerStatus + : scServerStatus // ignore: cast_nullable_to_non_nullable + as List?, + countdownFestivalListData: freezed == countdownFestivalListData + ? _value._countdownFestivalListData + : countdownFestivalListData // ignore: cast_nullable_to_non_nullable + as List?, + )); + } +} + +/// @nodoc + +class _$HomeUIModelStateImpl implements _HomeUIModelState { + _$HomeUIModelStateImpl( + {this.appPlacardData, + this.isFixing = false, + this.isFixingString = "", + this.scInstalledPath, + final List scInstallPaths = const [], + this.webLocalizationVersionsData, + this.isCurGameRunning = false, + this.lastScreenInfo = "", + final List? rssVideoItems, + final List? rssTextItems, + this.localizationUpdateInfo, + final List? scServerStatus, + final List? countdownFestivalListData}) + : _scInstallPaths = scInstallPaths, + _rssVideoItems = rssVideoItems, + _rssTextItems = rssTextItems, + _scServerStatus = scServerStatus, + _countdownFestivalListData = countdownFestivalListData; + + @override + final AppPlacardData? appPlacardData; + @override + @JsonKey() + final bool isFixing; + @override + @JsonKey() + final String isFixingString; + @override + final String? scInstalledPath; + final List _scInstallPaths; + @override + @JsonKey() + List get scInstallPaths { + if (_scInstallPaths is EqualUnmodifiableListView) return _scInstallPaths; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_scInstallPaths); + } + + @override + final AppWebLocalizationVersionsData? webLocalizationVersionsData; + @override + @JsonKey() + final bool isCurGameRunning; + @override + @JsonKey() + final String lastScreenInfo; + final List? _rssVideoItems; + @override + List? get rssVideoItems { + final value = _rssVideoItems; + if (value == null) return null; + if (_rssVideoItems is EqualUnmodifiableListView) return _rssVideoItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + final List? _rssTextItems; + @override + List? get rssTextItems { + final value = _rssTextItems; + if (value == null) return null; + if (_rssTextItems is EqualUnmodifiableListView) return _rssTextItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + final MapEntry? localizationUpdateInfo; + final List? _scServerStatus; + @override + List? get scServerStatus { + final value = _scServerStatus; + if (value == null) return null; + if (_scServerStatus is EqualUnmodifiableListView) return _scServerStatus; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + final List? _countdownFestivalListData; + @override + List? get countdownFestivalListData { + final value = _countdownFestivalListData; + if (value == null) return null; + if (_countdownFestivalListData is EqualUnmodifiableListView) + return _countdownFestivalListData; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'HomeUIModelState(appPlacardData: $appPlacardData, isFixing: $isFixing, isFixingString: $isFixingString, scInstalledPath: $scInstalledPath, scInstallPaths: $scInstallPaths, webLocalizationVersionsData: $webLocalizationVersionsData, isCurGameRunning: $isCurGameRunning, lastScreenInfo: $lastScreenInfo, rssVideoItems: $rssVideoItems, rssTextItems: $rssTextItems, localizationUpdateInfo: $localizationUpdateInfo, scServerStatus: $scServerStatus, countdownFestivalListData: $countdownFestivalListData)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HomeUIModelStateImpl && + (identical(other.appPlacardData, appPlacardData) || + other.appPlacardData == appPlacardData) && + (identical(other.isFixing, isFixing) || + other.isFixing == isFixing) && + (identical(other.isFixingString, isFixingString) || + other.isFixingString == isFixingString) && + (identical(other.scInstalledPath, scInstalledPath) || + other.scInstalledPath == scInstalledPath) && + const DeepCollectionEquality() + .equals(other._scInstallPaths, _scInstallPaths) && + (identical(other.webLocalizationVersionsData, + webLocalizationVersionsData) || + other.webLocalizationVersionsData == + webLocalizationVersionsData) && + (identical(other.isCurGameRunning, isCurGameRunning) || + other.isCurGameRunning == isCurGameRunning) && + (identical(other.lastScreenInfo, lastScreenInfo) || + other.lastScreenInfo == lastScreenInfo) && + const DeepCollectionEquality() + .equals(other._rssVideoItems, _rssVideoItems) && + const DeepCollectionEquality() + .equals(other._rssTextItems, _rssTextItems) && + (identical(other.localizationUpdateInfo, localizationUpdateInfo) || + other.localizationUpdateInfo == localizationUpdateInfo) && + const DeepCollectionEquality() + .equals(other._scServerStatus, _scServerStatus) && + const DeepCollectionEquality().equals( + other._countdownFestivalListData, _countdownFestivalListData)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + appPlacardData, + isFixing, + isFixingString, + scInstalledPath, + const DeepCollectionEquality().hash(_scInstallPaths), + webLocalizationVersionsData, + isCurGameRunning, + lastScreenInfo, + const DeepCollectionEquality().hash(_rssVideoItems), + const DeepCollectionEquality().hash(_rssTextItems), + localizationUpdateInfo, + const DeepCollectionEquality().hash(_scServerStatus), + const DeepCollectionEquality().hash(_countdownFestivalListData)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HomeUIModelStateImplCopyWith<_$HomeUIModelStateImpl> get copyWith => + __$$HomeUIModelStateImplCopyWithImpl<_$HomeUIModelStateImpl>( + this, _$identity); +} + +abstract class _HomeUIModelState implements HomeUIModelState { + factory _HomeUIModelState( + {final AppPlacardData? appPlacardData, + final bool isFixing, + final String isFixingString, + final String? scInstalledPath, + final List scInstallPaths, + final AppWebLocalizationVersionsData? webLocalizationVersionsData, + final bool isCurGameRunning, + final String lastScreenInfo, + final List? rssVideoItems, + final List? rssTextItems, + final MapEntry? localizationUpdateInfo, + final List? scServerStatus, + final List? countdownFestivalListData}) = + _$HomeUIModelStateImpl; + + @override + AppPlacardData? get appPlacardData; + @override + bool get isFixing; + @override + String get isFixingString; + @override + String? get scInstalledPath; + @override + List get scInstallPaths; + @override + AppWebLocalizationVersionsData? get webLocalizationVersionsData; + @override + bool get isCurGameRunning; + @override + String get lastScreenInfo; + @override + List? get rssVideoItems; + @override + List? get rssTextItems; + @override + MapEntry? get localizationUpdateInfo; + @override + List? get scServerStatus; + @override + List? get countdownFestivalListData; + @override + @JsonKey(ignore: true) + _$$HomeUIModelStateImplCopyWith<_$HomeUIModelStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/home/home_ui_model.g.dart b/lib/ui/home/home_ui_model.g.dart new file mode 100644 index 0000000..4ac8371 --- /dev/null +++ b/lib/ui/home/home_ui_model.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'home_ui_model.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$homeUIModelHash() => r'7ab7b3721ff81a18d67717c9bc91632226c516f6'; + +/// See also [HomeUIModel]. +@ProviderFor(HomeUIModel) +final homeUIModelProvider = + AutoDisposeNotifierProvider.internal( + HomeUIModel.new, + name: r'homeUIModelProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$homeUIModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$HomeUIModel = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/ui/index_ui.dart b/lib/ui/index_ui.dart index 522ca74..803bff7 100644 --- a/lib/ui/index_ui.dart +++ b/lib/ui/index_ui.dart @@ -3,6 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:starcitizen_doctor/common/conf/const_conf.dart'; import 'package:starcitizen_doctor/provider/aria2c.dart'; +import 'package:starcitizen_doctor/ui/home/home_ui.dart'; +import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:window_manager/window_manager.dart'; @@ -11,6 +13,9 @@ class IndexUI extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + // pre init child + ref.watch(homeUIModelProvider.select((value) => null)); + final curIndex = useState(0); return NavigationView( appBar: NavigationAppBar( @@ -107,9 +112,14 @@ class IndexUI extends HookConsumerWidget { } Widget getPage(int value) { - return Center( - child: Text("$value"), - ); + switch (value) { + case 0: + return const HomeUI(); + default: + return Center( + child: Text("UnimplPage $value"), + ); + } } void _onTapIndexMenu(String value, ValueNotifier curIndexState) { diff --git a/lib/ui/settings/upgrade_dialog.dart b/lib/ui/settings/upgrade_dialog.dart index 4f030fa..1e4db2d 100644 --- a/lib/ui/settings/upgrade_dialog.dart +++ b/lib/ui/settings/upgrade_dialog.dart @@ -11,7 +11,6 @@ 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/helper/system_helper.dart'; -import 'package:starcitizen_doctor/common/utils/base_utils.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:html/parser.dart' as html_parser; diff --git a/lib/widgets/src/cache_image.dart b/lib/widgets/src/cache_image.dart new file mode 100644 index 0000000..5cc350a --- /dev/null +++ b/lib/widgets/src/cache_image.dart @@ -0,0 +1,41 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class CacheNetImage extends StatelessWidget { + final String url; + final double? width; + final double? height; + final BoxFit? fit; + + const CacheNetImage( + {super.key, required this.url, this.width, this.height, this.fit}); + + @override + Widget build(BuildContext context) { + return ExtendedImage.network( + url, + width: width, + height: height, + fit: fit, + loadStateChanged: (ExtendedImageState state) { + switch (state.extendedImageLoadState) { + case LoadState.loading: + return const Center( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Column( + children: [ + ProgressRing(), + ], + ), + ), + ); + case LoadState.failed: + return const Text("Loading Image error"); + case LoadState.completed: + return null; + } + }, + ); + } +} diff --git a/lib/widgets/src/countdown_time_text.dart b/lib/widgets/src/countdown_time_text.dart new file mode 100644 index 0000000..bf54c4b --- /dev/null +++ b/lib/widgets/src/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/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 804f0e4..f9bf759 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -5,6 +5,25 @@ import 'package:window_manager/window_manager.dart'; import 'package:markdown_widget/config/all.dart'; import 'package:markdown_widget/widget/all.dart'; import 'package:extended_image/extended_image.dart'; +import 'dart:ui' as ui; + +export 'src/cache_image.dart'; +export 'src/countdown_time_text.dart'; +export '../common/utils/base_utils.dart'; + +Widget makeLoading( + BuildContext context, { + double? width, +}) { + width ??= 30; + return Center( + child: SizedBox( + width: width, + height: width, + child: const ProgressRing(), + ), + ); +} Widget makeDefaultPage(BuildContext context, {Widget? titleRow, @@ -99,6 +118,10 @@ List makeMarkdownView(String description, {String? attachmentsUrl}) { ])); } +ColorFilter makeSvgColor(Color color) { + return ui.ColorFilter.mode(color, ui.BlendMode.srcIn); +} + CustomTransitionPage myPageBuilder( BuildContext context, GoRouterState state, Widget child) { return CustomTransitionPage(