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'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:starcitizen_doctor/base/ui.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'home_ui_model.dart'; class HomeUI extends BaseUI { @override Widget? buildBody(BuildContext context, HomeUIModel model) { return Stack( children: [ Center( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ if (model.appPlacardData != null) ...[ InfoBar( title: Text("${model.appPlacardData?.title}"), content: Text("${model.appPlacardData?.content}"), severity: InfoBarSeverity.info, action: model.appPlacardData?.link == null ? null : Button( child: const Text('查看详情'), onPressed: () => model.showPlacard(), ), onClose: model.appPlacardData?.alwaysShow == true ? null : () => model.closePlacard(), ), const SizedBox(height: 12), ], if (!model.isChecking && model.checkResult != null && model.checkResult!.isNotEmpty) ...makeResult(context, model) else ...makeIndex(context, model) ], ), ), ), if (model.isFixing) Container( decoration: BoxDecoration( color: Colors.black.withAlpha(150), ), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const ProgressRing(), const SizedBox(height: 12), Text(model.isFixingString.isNotEmpty ? model.isFixingString : "正在处理..."), ], ), ), ) ], ); } List makeIndex(BuildContext context, HomeUIModel model) { final width = MediaQuery.of(context).size.width * .21; return [ Stack( children: [ Padding( padding: const EdgeInsets.only(top: 64, bottom: 64), child: Image.asset( "assets/sc_logo.png", height: 256, width: MediaQuery.of(context).size.width, fit: BoxFit.fitHeight, ), ), Positioned( top: 0, right: 24, child: 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), 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), 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: "DPSCalculator 汉化", webTitle: "DPSCalculatorLIVE 汉化", webURL: "https://www.erkul.games/live/calculator", info: "在线改船,查询伤害数值和配件购买地点", useLocalization: true, width: width), 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"); }, ), ], ) ], ), ), ), ), Positioned( left: 24, bottom: 0, child: Column( children: [ const SizedBox(height: 12), makeADCard(context, model, bgURl: "https://i2.hdslb.com/bfs/face/7582c8d46fc03004f4f8032c667c0ea4dbbb1088.jpg", title: "Anicat", subtitle: "高质量星际公民资讯UP主", jumpUrl: "https://space.bilibili.com/27976358/video"), const SizedBox(height: 12), makeADCard(context, model, bgURl: "https://citizenwiki.cn/images/f/f2/890Jump_beach.jpg.webp", title: "星际公民中文百科", subtitle: "探索宇宙的好伙伴", jumpUrl: "https://citizenwiki.cn"), const SizedBox(height: 12), Container( width: width, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: FluentTheme.of(context).cardColor, ), child: IconButton( icon: 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, children: [ for (final item in model.scServerStatus ?? []) Row( children: [ Center( child: Icon( item["status"] == "ok" ? FontAwesomeIcons.circleCheck : FluentIcons.error, color: item["status"] == "ok" ? Colors.green : Colors.red, size: 20, ), ), const SizedBox(width: 4), Text( "${model.statusCnName[item["name"]] ?? item["name"]}", style: const TextStyle(fontSize: 13), ), ], ) ], ) ]), ), onPressed: () { launchUrlString( "https://status.robertsspaceindustries.com/"); }, )), ], )) ], ), 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: model.scInstalledPath, items: [ const ComboBoxItem( value: "not_install", child: Text("未安装 或 安装失败"), ), for (final path in model.scInstallPaths) ComboBoxItem( value: path, child: Text(path), ) ], onChanged: (v) { model.scInstalledPath = v!; model.notifyListeners(); }, ), ), const SizedBox(width: 12), AnimatedSize( duration: const Duration(milliseconds: 130), child: model.isRsiLauncherStarting ? makeLoading(context, width: 28) : Button( child: const Padding( padding: EdgeInsets.all(8.0), child: Icon(FluentIcons.play), ), onPressed: () => model.launchRSI()), ), const SizedBox(width: 12), Button( child: const Padding( padding: EdgeInsets.all(8.0), child: Icon(FluentIcons.folder_open), ), onPressed: () => model.openDir(model.scInstalledPath)), const SizedBox(width: 12), Button( onPressed: model.isChecking ? null : model.reScanPath, child: const Padding( padding: EdgeInsets.all(6), child: Icon(FluentIcons.refresh), ), ), ], ), ), const SizedBox(height: 8), Text(model.lastScreenInfo, maxLines: 1), const SizedBox(height: 32), makeIndexActionLists(context, model), const SizedBox(height: 32), ]; } Widget makeIndexActionLists(BuildContext context, HomeUIModel model) { 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: item.key == "auto_check" && model.isChecking ? null : () => 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), ], )), const SizedBox(width: 12), if (item.key == "auto_check" && model.isChecking) const ProgressRing() else const Icon( FluentIcons.chevron_right, size: 16, ), ], ), ), ); }, ); }), ); } List makeResult(BuildContext context, HomeUIModel model) { return [ const SizedBox(height: 24), const Text( "检测结果", style: TextStyle(fontSize: 20), ), const SizedBox(height: 6), Text(model.lastScreenInfo, maxLines: 1), const SizedBox(height: 24), ListView.builder( itemCount: model.checkResult!.length, shrinkWrap: true, itemBuilder: (BuildContext context, int index) { final item = model.checkResult![index]; return makeResultItem(item, model); }, ), Text( "注意:本工具检测结果仅供参考,若您不理解以上操作,请提供截图给有经验的玩家!", style: TextStyle(color: Colors.red, fontSize: 16), ), const SizedBox(height: 64), FilledButton( onPressed: model.resetCheck, child: const Padding( padding: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4), child: Text('返回'), ), ), const SizedBox(height: 38), ]; } @override String getUITitle(BuildContext context, HomeUIModel model) => "HOME"; Widget makeResultItem(MapEntry item, HomeUIModel model) { final errorNames = { "unSupport_system": MapEntry("不支持的操作系统,游戏可能无法运行", "请升级您的系统 (${item.value})"), "no_live_path": MapEntry("安装目录缺少LIVE文件夹,可能导致安装失败", "点击修复为您创建 LIVE 文件夹,完成后重试安装。(${item.value})"), "nvme_PhysicalBytes": MapEntry("新型 NVME 设备,与 RSI 启动器暂不兼容,可能导致安装失败", "为注册表项添加 ForcedPhysicalSectorSizeInBytes 值 模拟旧设备。硬盘分区(${item.value})"), "eac_file_miss": const MapEntry("EasyAntiCheat 文件丢失", "未在 LIVE 文件夹找到 EasyAntiCheat 文件 或 文件不完整,请使用 RSI 启动器校验文件"), "eac_not_install": const MapEntry("EasyAntiCheat 未安装", "EasyAntiCheat 未安装,请点击修复为您一键安装。(在 EAC 完成首次启动前,本条目持续存在)"), "cn_user_name": const MapEntry("中文用户名!", "中文用户名可能会导致游戏启动/安装错误! 点击修复按钮查看修改教程!"), "cn_install_path": MapEntry("中文安装路径!", "中文安装路径!这可能会导致游戏 启动/安装 错误!(${item.value}),请在RSI启动器更换安装路径。"), "low_ram": MapEntry( "物理内存过低", "您至少需要 16GB 的物理内存(Memory)才可运行此游戏。(当前大小:${item.value})"), }; return ListTile( title: Text(errorNames[item.key]?.key ?? item.key), subtitle: Padding( padding: const EdgeInsets.only(top: 4, bottom: 4), child: Text("修复建议: ${errorNames[item.key]?.value ?? "暂无解决方法,请截图反馈"}"), ), trailing: Button( onPressed: (errorNames[item.key]?.value == null || model.isFixing) ? null : () async { await model.doFix(item); model.isFixing = false; model.notifyListeners(); }, child: const Padding( padding: EdgeInsets.only(left: 8, right: 8), child: Text("修复"), ), ), ); } Widget makeADCard( BuildContext context, HomeUIModel model, { required String bgURl, required String title, required String subtitle, required String jumpUrl, }) { final width = MediaQuery.of(context).size.width * .21; return Container( width: width, height: 128, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: FluentTheme.of(context).cardColor, ), child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: ExtendedImage.network( bgURl, fit: BoxFit.cover, width: width, ), ), Container( width: width, height: 128, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Colors.black.withOpacity(.7), ), ), Positioned( top: 0, bottom: 0, left: 0, right: 0, child: IconButton( icon: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( title, style: const TextStyle( fontSize: 24, ), ), const SizedBox(height: 6), Text( subtitle, style: TextStyle( color: Colors.white.withOpacity(.8), fontSize: 14), ), ], ), ), onPressed: () { launchUrlString(jumpUrl); }, ), ) ], ), ); } Widget makeWebViewButton(HomeUIModel model, {required Widget icon, required String name, required String webTitle, required String webURL, required bool useLocalization, required double width, String? info}) { return Container( width: width, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: FluentTheme.of(context).cardColor, ), child: Padding( padding: const EdgeInsets.all(8.0), child: IconButton( icon: 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)), ), ) ], ), onPressed: () => model.goWebView(webTitle, webURL, useLocalization: true)), ), ); } } class _HomeItemData { String key; _HomeItemData(this.key, this.name, this.infoString, this.icon); String name; String infoString; IconData icon; }