mirror of
https://ghfast.top/https://github.com/StarCitizenToolBox/app.git
synced 2025-06-28 05:34:45 +08:00
re init
This commit is contained in:
152
lib/ui/about/about_ui.dart
Normal file
152
lib/ui/about/about_ui.dart
Normal file
@ -0,0 +1,152 @@
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:starcitizen_doctor/base/ui.dart';
|
||||
import 'package:starcitizen_doctor/common/conf.dart';
|
||||
import 'package:starcitizen_doctor/global_ui_model.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'about_ui_model.dart';
|
||||
|
||||
class AboutUI extends BaseUI<AboutUIModel> {
|
||||
bool isTipTextCn = false;
|
||||
|
||||
@override
|
||||
Widget? buildBody(BuildContext context, AboutUIModel model) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Spacer(),
|
||||
const SizedBox(height: 64),
|
||||
Image.asset("assets/app_logo.png", width: 64, height: 64),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
"星际公民盒子 V${AppConf.appVersion}",
|
||||
style: TextStyle(fontSize: 21),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Button(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Text("检查更新"),
|
||||
),
|
||||
onPressed: () async {
|
||||
final hasUpdate = await globalUIModel.checkUpdate(context);
|
||||
if (!hasUpdate) {
|
||||
if (mounted) showToast(context, "已是最新版本");
|
||||
}
|
||||
}),
|
||||
const SizedBox(height: 32),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
"不仅不仅是汉化!\n\n星际公民盒子是你探索宇宙的好帮手,我们致力于为各位公民解决游戏中的常见问题,并为社区汉化、性能调优、常用网站汉化 等操作提供便利。",
|
||||
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.qq),
|
||||
const SizedBox(width: 6),
|
||||
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: 6),
|
||||
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.gitlab),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
"开源",
|
||||
style: TextStyle(
|
||||
fontSize: 14, color: Colors.white.withOpacity(.6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
"https://jihulab.com/StarCitizenCN_Community/StarCitizenDoctor");
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * .35,
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor.withOpacity(.03),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: IconButton(
|
||||
icon: Padding(
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: Text(
|
||||
isTipTextCn ? tipTextCN : tipTextEN,
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Colors.white.withOpacity(.9)),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
isTipTextCn = !isTipTextCn;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 的注册商标。";
|
||||
|
||||
@override
|
||||
String getUITitle(BuildContext context, AboutUIModel model) => "";
|
||||
}
|
5
lib/ui/about/about_ui_model.dart
Normal file
5
lib/ui/about/about_ui_model.dart
Normal file
@ -0,0 +1,5 @@
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
|
||||
class AboutUIModel extends BaseUIModel {
|
||||
|
||||
}
|
42
lib/ui/home/dialogs/md_content_dialog_ui.dart
Normal file
42
lib/ui/home/dialogs/md_content_dialog_ui.dart
Normal file
@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart' show Material;
|
||||
import 'package:starcitizen_doctor/base/ui.dart';
|
||||
import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui_model.dart';
|
||||
|
||||
class MDContentDialogUI extends BaseUI<MDContentDialogUIModel> {
|
||||
@override
|
||||
Widget? buildBody(BuildContext context, MDContentDialogUIModel model) {
|
||||
return Material(
|
||||
child: ContentDialog(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * .6,
|
||||
),
|
||||
title: Text(getUITitle(context, model)),
|
||||
content: model.data == null
|
||||
? makeLoading(context)
|
||||
: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: makeMarkdownView(model.data ?? ""),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
|
||||
child: Text("关闭"),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String getUITitle(BuildContext context, MDContentDialogUIModel model) =>
|
||||
model.title;
|
||||
}
|
19
lib/ui/home/dialogs/md_content_dialog_ui_model.dart
Normal file
19
lib/ui/home/dialogs/md_content_dialog_ui_model.dart
Normal file
@ -0,0 +1,19 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
|
||||
class MDContentDialogUIModel extends BaseUIModel {
|
||||
String title;
|
||||
String url;
|
||||
|
||||
MDContentDialogUIModel(this.title, this.url);
|
||||
|
||||
String? data;
|
||||
|
||||
@override
|
||||
Future loadData() async {
|
||||
final r = await handleError(() => Dio().get(url));
|
||||
if (r == null) return;
|
||||
data = r.data;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
513
lib/ui/home/home_ui.dart
Normal file
513
lib/ui/home/home_ui.dart
Normal file
@ -0,0 +1,513 @@
|
||||
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:starcitizen_doctor/base/ui.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'home_ui_model.dart';
|
||||
|
||||
class HomeUI extends BaseUI<HomeUIModel> {
|
||||
@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<Widget> 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: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
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),
|
||||
makeWebViewButton(model,
|
||||
icon: Row(
|
||||
children: [
|
||||
ExtendedImage.network(
|
||||
"https://ccugame.app/assets/images/logo/logo.png",
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
name: "CCUGame 网站汉化",
|
||||
webTitle: "CCUGame 网站汉化",
|
||||
webURL: "https://ccugame.app",
|
||||
info: "资产管理和舰队规划,一定要理性消费.jpg",
|
||||
useLocalization: true,
|
||||
width: width),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 24,
|
||||
bottom: 0,
|
||||
child: Column(
|
||||
children: [
|
||||
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: 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<String>(
|
||||
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),
|
||||
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<ButtonStates> 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<Widget> 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<String, String> 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;
|
||||
}
|
410
lib/ui/home/home_ui_model.dart
Normal file
410
lib/ui/home/home_ui_model.dart
Normal file
@ -0,0 +1,410 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:starcitizen_doctor/api/api.dart';
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
import 'package:starcitizen_doctor/common/conf.dart';
|
||||
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/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';
|
||||
import 'package:starcitizen_doctor/ui/home/performance/performance_ui_model.dart';
|
||||
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
|
||||
import 'package:starcitizen_doctor/ui/home/webview/webview_localization_capture_ui_model.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'localization/localization_ui.dart';
|
||||
import 'performance/performance_ui.dart';
|
||||
import 'webview/webview_localization_capture_ui.dart';
|
||||
|
||||
class HomeUIModel extends BaseUIModel {
|
||||
var scInstalledPath = "not_install";
|
||||
|
||||
List<String> scInstallPaths = [];
|
||||
|
||||
String _lastScreenInfo = "";
|
||||
|
||||
String get lastScreenInfo => _lastScreenInfo;
|
||||
|
||||
bool isChecking = false;
|
||||
|
||||
bool isFixing = false;
|
||||
String isFixingString = "";
|
||||
|
||||
set lastScreenInfo(String info) {
|
||||
_lastScreenInfo = info;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<MapEntry<String, String>>? checkResult;
|
||||
|
||||
final cnExp = RegExp(r"[^\x00-\xff]");
|
||||
|
||||
AppPlacardData? appPlacardData;
|
||||
|
||||
@override
|
||||
Future loadData() async {
|
||||
if (AppConf.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) return;
|
||||
if (r.alwaysShow != true && version == r.version) return;
|
||||
appPlacardData = r;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
dPrint(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initModel() {
|
||||
reScanPath();
|
||||
super.initModel();
|
||||
}
|
||||
|
||||
Future<void> reScanPath() async {
|
||||
scInstallPaths.clear();
|
||||
scInstalledPath = "not_install";
|
||||
lastScreenInfo = "正在扫描 ...";
|
||||
try {
|
||||
final listData = await SCLoggerHelper.getLauncherLogList();
|
||||
if (listData == null) {
|
||||
lastScreenInfo = "获取log失败!";
|
||||
return;
|
||||
}
|
||||
scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData,
|
||||
withVersion: ["LIVE", "PTU", "EPTU"], checkExists: true);
|
||||
if (scInstallPaths.isNotEmpty) {
|
||||
scInstalledPath = scInstallPaths.first;
|
||||
}
|
||||
lastScreenInfo = "扫描完毕,共找到 ${scInstallPaths.length} 个有效安装目录";
|
||||
} catch (e) {
|
||||
lastScreenInfo = "解析 log 文件失败!";
|
||||
showToast(context!,
|
||||
"解析 log 文件失败! \n请关闭游戏,退出RSI启动器后重试,若仍有问题,请使用工具箱中的 RSI Launcher log 修复。");
|
||||
}
|
||||
}
|
||||
|
||||
VoidCallback? doCheck() {
|
||||
if (isChecking) return null;
|
||||
return () async {
|
||||
isChecking = true;
|
||||
lastScreenInfo = "正在分析...";
|
||||
await _statCheck();
|
||||
isChecking = false;
|
||||
notifyListeners();
|
||||
};
|
||||
}
|
||||
|
||||
Future _statCheck() async {
|
||||
checkResult = [];
|
||||
await _checkPreInstall();
|
||||
await _checkEAC();
|
||||
|
||||
// TODO for debug
|
||||
// checkResult?.add(const MapEntry("unSupport_system", "android"));
|
||||
// checkResult?.add(const MapEntry("nvme_PhysicalBytes", "c"));
|
||||
// checkResult?.add(const MapEntry("no_live_path", ""));
|
||||
|
||||
if (checkResult!.isEmpty) {
|
||||
checkResult = null;
|
||||
lastScreenInfo = "分析完毕,没有发现问题";
|
||||
} else {
|
||||
lastScreenInfo = "分析完毕,发现 ${checkResult!.length} 个问题";
|
||||
}
|
||||
|
||||
if (scInstalledPath == "not_install" && (checkResult?.isEmpty ?? true)) {
|
||||
showToast(context!, "扫描完毕,没有发现问题,若仍然安装失败,请尝试使用工具箱中的 RSI启动器管理员模式。");
|
||||
}
|
||||
}
|
||||
|
||||
Future _checkEAC() async {
|
||||
if (scInstalledPath == "not_install") return;
|
||||
lastScreenInfo = "正在检查:EAC";
|
||||
final eacPath = "$scInstalledPath\\EasyAntiCheat";
|
||||
final eacJsonPath = "$eacPath\\Settings.json";
|
||||
if (!await Directory(eacPath).exists() ||
|
||||
!await File(eacJsonPath).exists()) {
|
||||
checkResult?.add(const MapEntry("eac_file_miss", ""));
|
||||
return;
|
||||
}
|
||||
final eacJsonData = await File(eacJsonPath).readAsBytes();
|
||||
final Map eacJson = json.decode(utf8.decode(eacJsonData));
|
||||
final eacID = eacJson["productid"];
|
||||
final eacDeploymentId = eacJson["deploymentid"];
|
||||
if (eacID == null || eacDeploymentId == null) {
|
||||
checkResult?.add(const MapEntry("eac_file_miss", ""));
|
||||
return;
|
||||
}
|
||||
final eacFilePath =
|
||||
"${Platform.environment["appdata"]}\\EasyAntiCheat\\$eacID\\$eacDeploymentId\\easyanticheat_wow64_x64.eac";
|
||||
if (!await File(eacFilePath).exists()) {
|
||||
checkResult?.add(MapEntry("eac_not_install", eacPath));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future _checkPreInstall() async {
|
||||
lastScreenInfo = "正在检查:运行环境";
|
||||
if (!(Platform.operatingSystemVersion.contains("Windows 10") ||
|
||||
Platform.operatingSystemVersion.contains("Windows 11"))) {
|
||||
checkResult
|
||||
?.add(MapEntry("unSupport_system", Platform.operatingSystemVersion));
|
||||
lastScreenInfo = "不支持的操作系统:${Platform.operatingSystemVersion}";
|
||||
await showToast(context!, lastScreenInfo);
|
||||
}
|
||||
|
||||
if (cnExp.hasMatch(await SCLoggerHelper.getLogFilePath() ?? "")) {
|
||||
checkResult?.add(const MapEntry("cn_user_name", ""));
|
||||
}
|
||||
|
||||
// 检查 RAM
|
||||
final ramSize = await SystemHelper.getSystemMemorySizeGB();
|
||||
if (ramSize < 16) {
|
||||
checkResult?.add(MapEntry("low_ram", "$ramSize"));
|
||||
}
|
||||
|
||||
lastScreenInfo = "正在检查:安装信息";
|
||||
// 检查安装分区
|
||||
try {
|
||||
final listData = await SCLoggerHelper.getGameInstallPath(
|
||||
await SCLoggerHelper.getLauncherLogList() ?? []);
|
||||
final p = [];
|
||||
final checkedPath = [];
|
||||
for (var installPath in listData) {
|
||||
if (!checkedPath.contains(installPath)) {
|
||||
if (cnExp.hasMatch(installPath)) {
|
||||
checkResult?.add(MapEntry("cn_install_path", installPath));
|
||||
}
|
||||
if (scInstalledPath == "not_install") {
|
||||
checkedPath.add(installPath);
|
||||
if (!await Directory(installPath).exists()) {
|
||||
checkResult?.add(MapEntry("no_live_path", installPath));
|
||||
}
|
||||
}
|
||||
final tp = installPath.split(":")[0];
|
||||
if (!p.contains(tp)) {
|
||||
p.add(tp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call check
|
||||
for (var element in p) {
|
||||
var result = await Process.run('powershell', [
|
||||
"(fsutil fsinfo sectorinfo $element: | Select-String 'PhysicalBytesPerSectorForPerformance').ToString().Split(':')[1].Trim()"
|
||||
]);
|
||||
dPrint(result.stdout);
|
||||
if (result.stderr == "") {
|
||||
final rs = result.stdout.toString();
|
||||
final physicalBytesPerSectorForPerformance = (int.tryParse(rs) ?? 0);
|
||||
if (physicalBytesPerSectorForPerformance > 4096) {
|
||||
checkResult?.add(MapEntry("nvme_PhysicalBytes", element));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
dPrint(e);
|
||||
}
|
||||
}
|
||||
|
||||
void resetCheck() {
|
||||
checkResult = null;
|
||||
reScanPath();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> doFix(MapEntry<String, String> item) async {
|
||||
isFixing = true;
|
||||
notifyListeners();
|
||||
switch (item.key) {
|
||||
case "unSupport_system":
|
||||
showToast(context!, "若您的硬件达标,请尝试安装最新的 Windows 系统。");
|
||||
return;
|
||||
case "no_live_path":
|
||||
try {
|
||||
await Directory(item.value).create(recursive: true);
|
||||
showToast(context!, "创建文件夹成功,请尝试继续下载游戏!");
|
||||
checkResult?.remove(item);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
showToast(context!, "创建文件夹失败,请尝试手动创建。\n目录:${item.value} \n错误:$e");
|
||||
}
|
||||
return;
|
||||
case "nvme_PhysicalBytes":
|
||||
final r = await SystemHelper.addNvmePatch();
|
||||
if (r == "") {
|
||||
showToast(context!,
|
||||
"修复成功,请尝试重启后继续安装游戏! 若注册表修改操作导致其他软件出现兼容问题,请使用 工具 中的 NVME 注册表清理。");
|
||||
checkResult?.remove(item);
|
||||
notifyListeners();
|
||||
} else {
|
||||
showToast(context!, "修复失败,$r");
|
||||
}
|
||||
return;
|
||||
case "eac_file_miss":
|
||||
showToast(
|
||||
context!, "未在 LIVE 文件夹找到 EasyAntiCheat 文件 或 文件不完整,请使用 RSI 启动器校验文件");
|
||||
return;
|
||||
case "eac_not_install":
|
||||
final eacJsonPath = "${item.value}\\Settings.json";
|
||||
final eacJsonData = await File(eacJsonPath).readAsBytes();
|
||||
final Map eacJson = json.decode(utf8.decode(eacJsonData));
|
||||
final eacID = eacJson["productid"];
|
||||
try {
|
||||
var result = await Process.run(
|
||||
"${item.value}\\EasyAntiCheat_EOS_Setup.exe", ["install", eacID]);
|
||||
dPrint("${item.value}\\EasyAntiCheat_EOS_Setup.exe install $eacID");
|
||||
if (result.stderr == "") {
|
||||
showToast(context!, "修复成功,请尝试启动游戏。(若问题无法解决,请使用工具箱的 《重装 EAC》)");
|
||||
checkResult?.remove(item);
|
||||
notifyListeners();
|
||||
} else {
|
||||
showToast(context!, "修复失败,${result.stderr}");
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(context!, "修复失败,$e");
|
||||
}
|
||||
return;
|
||||
case "cn_user_name":
|
||||
showToast(context!, "即将跳转,教程来自互联网,请谨慎操作...");
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
launchUrlString(
|
||||
"https://btfy.eu.org/?q=5L+u5pS5d2luZG93c+eUqOaIt+WQjeS7juS4reaWh+WIsOiLseaWhw==");
|
||||
return;
|
||||
default:
|
||||
showToast(context!, "该问题暂不支持自动处理,请提供截图寻求帮助");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
openDir(rsiLauncherInstalledPath) async {
|
||||
await Process.run("powershell.exe",
|
||||
["explorer.exe", "/select,\"$rsiLauncherInstalledPath\""]);
|
||||
}
|
||||
|
||||
onMenuTap(String key) {
|
||||
switch (key) {
|
||||
case "auto_check":
|
||||
doCheck()?.call();
|
||||
return;
|
||||
case "localization":
|
||||
if (scInstalledPath == "not_install") {
|
||||
showToast(context!, "该功能需要一个有效的安装位置");
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context!,
|
||||
dismissWithEsc: false,
|
||||
builder: (BuildContext context) {
|
||||
return BaseUIContainer(
|
||||
uiCreate: () => LocalizationUI(),
|
||||
modelCreate: () => LocalizationUIModel(scInstalledPath));
|
||||
});
|
||||
return;
|
||||
case "performance":
|
||||
if (scInstalledPath == "not_install") {
|
||||
showToast(context!, "该功能需要一个有效的安装位置");
|
||||
return;
|
||||
}
|
||||
BaseUIContainer(
|
||||
uiCreate: () => PerformanceUI(),
|
||||
modelCreate: () => PerformanceUIModel(scInstalledPath))
|
||||
.push(context!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showPlacard() {
|
||||
switch (appPlacardData?.linkType) {
|
||||
case "external":
|
||||
launchUrlString(appPlacardData?.link);
|
||||
return;
|
||||
case "doc":
|
||||
showDialog(
|
||||
context: context!,
|
||||
builder: (context) {
|
||||
return BaseUIContainer(
|
||||
uiCreate: () => MDContentDialogUI(),
|
||||
modelCreate: () => MDContentDialogUIModel(
|
||||
appPlacardData?.title ?? "公告详情", appPlacardData?.link));
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
closePlacard() async {
|
||||
final box = await Hive.openBox("app_conf");
|
||||
await box.put("close_placard", appPlacardData?.version);
|
||||
appPlacardData = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
goWebView(String title, String url, {bool useLocalization = false}) async {
|
||||
if (useLocalization) {
|
||||
const tipVersion = 1;
|
||||
final box = await Hive.openBox("app_conf");
|
||||
final skip =
|
||||
await box.get("skip_web_localization_tip_version", defaultValue: 0);
|
||||
if (skip != tipVersion) {
|
||||
final ok = await showConfirmDialogs(
|
||||
context!,
|
||||
"星际公民官网汉化",
|
||||
const Text(
|
||||
"该汉化功能移植自星际公民汉化组的 Tampermonkey 浏览器插件(https://greasyfork.org/zh-CN/scripts/459084),文本内容由星际公民汉化组进行更新。"
|
||||
"\n\n移植后的脚本源代码随 StarCitizenDoctor 项目一起分发(https://jihulab.com/StarCitizenCN_Community/StarCitizenDoctor)。"
|
||||
"\n\n\n本插功能件仅供大致浏览使用,不对任何有关本功能产生的问题负责!在涉及账号操作前请注意确认网站的原本内容!"
|
||||
"\n\n\n使用此功能登录账号时请确保您的 StarCitizenDoctor 是从可信任的来源下载。",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context!).size.width * .6));
|
||||
if (!ok) return;
|
||||
await box.put("skip_web_localization_tip_version", tipVersion);
|
||||
}
|
||||
}
|
||||
if (!await WebviewWindow.isWebviewAvailable()) {
|
||||
showToast(context!, "需要安装 WebView2 Runtime");
|
||||
launchUrlString(
|
||||
"https://developer.microsoft.com/en-us/microsoft-edge/webview2/");
|
||||
return;
|
||||
}
|
||||
final webViewModel = WebViewModel(context!);
|
||||
if (useLocalization) {
|
||||
isFixingString = "正在初始化汉化资源...";
|
||||
isFixing = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
await webViewModel.initLocalization();
|
||||
} catch (e) {
|
||||
showToast(context!, "初始化网页汉化资源失败!$e");
|
||||
}
|
||||
isFixingString = "";
|
||||
isFixing = false;
|
||||
}
|
||||
|
||||
await webViewModel.initWebView(title: title);
|
||||
if (await File(
|
||||
"${AppConf.applicationSupportDir}\\webview_data\\enable_webview_localization_capture")
|
||||
.exists()) {
|
||||
webViewModel.enableCapture = true;
|
||||
BaseUIContainer(
|
||||
uiCreate: () => WebviewLocalizationCaptureUI(),
|
||||
modelCreate: () =>
|
||||
WebviewLocalizationCaptureUIModel(webViewModel))
|
||||
.push(context!)
|
||||
.then((_) {
|
||||
webViewModel.enableCapture = false;
|
||||
});
|
||||
}
|
||||
|
||||
await webViewModel.launch(url);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
310
lib/ui/home/localization/localization_ui.dart
Normal file
310
lib/ui/home/localization/localization_ui.dart
Normal file
@ -0,0 +1,310 @@
|
||||
import 'package:starcitizen_doctor/base/ui.dart';
|
||||
import 'package:starcitizen_doctor/data/sc_localization_data.dart';
|
||||
|
||||
import 'localization_ui_model.dart';
|
||||
|
||||
class LocalizationUI extends BaseUI<LocalizationUIModel> {
|
||||
@override
|
||||
Widget? buildBody(BuildContext context, LocalizationUIModel model) {
|
||||
return ContentDialog(
|
||||
title: makeTitle(context, model),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * .7,
|
||||
minHeight: MediaQuery.of(context).size.height * .9),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 130),
|
||||
child: model.patchStatus?.key == true &&
|
||||
model.patchStatus?.value == "游戏内置"
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: InfoBar(
|
||||
title: const Text("警告"),
|
||||
content: const Text(
|
||||
"您正在使用游戏内置文本,官方文本目前为机器翻译(截至3.21.0),建议您在下方 [最新版本] 安装社区汉化。"),
|
||||
severity: InfoBarSeverity.info,
|
||||
style: InfoBarThemeData(decoration: (severity) {
|
||||
return const BoxDecoration(
|
||||
color: Color.fromRGBO(155, 7, 7, 1.0));
|
||||
}, iconColor: (severity) {
|
||||
return Colors.white;
|
||||
}),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
),
|
||||
),
|
||||
makeListContainer("汉化状态", [
|
||||
if (model.patchStatus == null)
|
||||
makeLoading(context)
|
||||
else ...[
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
"启用(${LocalizationUIModel.languageSupport[model.selectedLanguage]}):"),
|
||||
),
|
||||
const Spacer(),
|
||||
ToggleSwitch(
|
||||
checked: model.patchStatus?.key == true,
|
||||
onChanged: model.updateLangCfg,
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Text("已安装版本:${model.patchStatus?.value}"),
|
||||
const Spacer(),
|
||||
if (model.patchStatus?.value != "游戏内置")
|
||||
Button(
|
||||
onPressed: model.doDelIniFile(),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Icon(FluentIcons.delete),
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
]),
|
||||
makeListContainer("最新版本", [
|
||||
if (model.apiLocalizationData == null)
|
||||
makeLoading(context)
|
||||
else if (model.apiLocalizationData!.isEmpty)
|
||||
Center(
|
||||
child: Text(
|
||||
"该语言/版本 暂无可用汉化,敬请期待!",
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: Colors.white.withOpacity(.8)),
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final item in model.apiLocalizationData!.entries)
|
||||
makeRemoteList(context, model, item),
|
||||
]),
|
||||
makeListContainer("自定义", [
|
||||
if (model.customizeList == null)
|
||||
makeLoading(context)
|
||||
else if (model.customizeList!.isEmpty)
|
||||
Center(
|
||||
child: Text(
|
||||
"请将 任意名称.ini 文件放入 Customize_ini 文件夹,即可使用此工具快捷安装 / 切换。",
|
||||
style: TextStyle(
|
||||
fontSize: 13, color: Colors.white.withOpacity(.8)),
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
for (final file in model.customizeList!)
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
model.getCustomizeFileName(file),
|
||||
),
|
||||
const Spacer(),
|
||||
if (model.workingVersion == file)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 12),
|
||||
child: ProgressRing(),
|
||||
)
|
||||
else
|
||||
Button(
|
||||
onPressed: model.doLocalInstall(file),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 8, right: 8, top: 4, bottom: 4),
|
||||
child: Text("安装"),
|
||||
))
|
||||
],
|
||||
)
|
||||
],
|
||||
], actions: [
|
||||
Button(
|
||||
onPressed: () => model.openDir(),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Icon(FluentIcons.folder_open),
|
||||
)),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeRemoteList(BuildContext context, LocalizationUIModel model,
|
||||
MapEntry<String, ScLocalizationData> item) {
|
||||
final isWorking = model.workingVersion.isNotEmpty;
|
||||
final isMineWorking = model.workingVersion == item.key;
|
||||
final isInstalled = model.patchStatus?.value == item.key;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${item.value.info}",
|
||||
style: const TextStyle(fontSize: 19),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"版本号:${item.value.versionName}",
|
||||
style: TextStyle(color: Colors.white.withOpacity(.6)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"通道:${item.value.channel}",
|
||||
style: TextStyle(color: Colors.white.withOpacity(.6)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"更新时间:${item.value.updateAt}",
|
||||
style: TextStyle(color: Colors.white.withOpacity(.6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
if (isMineWorking)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 12),
|
||||
child: ProgressRing(),
|
||||
)
|
||||
else
|
||||
Button(
|
||||
onPressed: ((item.value.enable == true &&
|
||||
!isWorking &&
|
||||
!isInstalled)
|
||||
? model.doRemoteInstall(item.value)
|
||||
: null),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8, right: 8, top: 4, bottom: 4),
|
||||
child: Text(isInstalled
|
||||
? "已安装"
|
||||
: ((item.value.enable ?? false) ? "安装" : "不可用")),
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
color: Colors.white.withOpacity(.05),
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeListContainer(String title, List<Widget> children,
|
||||
{List<Widget> actions = const []}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 130),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(7)),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 12, bottom: 12, left: 24, right: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 22),
|
||||
),
|
||||
const Spacer(),
|
||||
if (actions.isNotEmpty) ...actions,
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
Container(
|
||||
color: Colors.white.withOpacity(.1),
|
||||
height: 1,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...children
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeTitle(BuildContext context, LocalizationUIModel model) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
FluentIcons.back,
|
||||
size: 22,
|
||||
),
|
||||
onPressed: model.onBack()),
|
||||
const SizedBox(width: 12),
|
||||
Text(getUITitle(context, model)),
|
||||
const SizedBox(width: 24),
|
||||
Text(
|
||||
model.scInstallPath,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
"语言: ",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
ComboBox<String>(
|
||||
value: model.selectedLanguage,
|
||||
items: [
|
||||
for (final lang
|
||||
in LocalizationUIModel.languageSupport.entries)
|
||||
ComboBoxItem(
|
||||
value: lang.key,
|
||||
child: Text(lang.value),
|
||||
)
|
||||
],
|
||||
onChanged: model.workingVersion.isNotEmpty
|
||||
? null
|
||||
: (v) {
|
||||
if (v == null) return;
|
||||
model.selectLang(v);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Button(
|
||||
onPressed: model.doRefresh(),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(FluentIcons.refresh),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String getUITitle(BuildContext context, LocalizationUIModel model) => "汉化管理";
|
||||
}
|
303
lib/ui/home/localization/localization_ui_model.dart
Normal file
303
lib/ui/home/localization/localization_ui_model.dart
Normal file
@ -0,0 +1,303 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:starcitizen_doctor/api/api.dart';
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
import 'package:starcitizen_doctor/common/conf.dart';
|
||||
import 'package:starcitizen_doctor/data/sc_localization_data.dart';
|
||||
|
||||
class LocalizationUIModel extends BaseUIModel {
|
||||
final String scInstallPath;
|
||||
|
||||
static const languageSupport = {
|
||||
"chinese_(simplified)": "简体中文",
|
||||
"chinese_(traditional)": "繁體中文",
|
||||
};
|
||||
|
||||
late String selectedLanguage;
|
||||
|
||||
Map<String, ScLocalizationData>? apiLocalizationData;
|
||||
|
||||
LocalizationUIModel(this.scInstallPath);
|
||||
|
||||
String workingVersion = "";
|
||||
|
||||
final downloadDir =
|
||||
Directory("${AppConf.applicationSupportDir}\\Localizations");
|
||||
|
||||
late final customizeDir =
|
||||
Directory("${downloadDir.absolute.path}\\Customize_ini");
|
||||
|
||||
late final scDataDir = Directory("$scInstallPath\\data");
|
||||
|
||||
late final cfgFile = File("${scDataDir.absolute.path}\\system.cfg");
|
||||
|
||||
MapEntry<bool, String>? patchStatus;
|
||||
|
||||
List<String>? customizeList;
|
||||
|
||||
StreamSubscription? customizeDirListenSub;
|
||||
|
||||
@override
|
||||
void initModel() {
|
||||
selectedLanguage = languageSupport.entries.first.key;
|
||||
if (!customizeDir.existsSync()) {
|
||||
customizeDir.createSync(recursive: true);
|
||||
}
|
||||
customizeDirListenSub = customizeDir.watch().listen((event) {
|
||||
_scanCustomizeDir();
|
||||
});
|
||||
super.initModel();
|
||||
}
|
||||
|
||||
@override
|
||||
Future loadData() async {
|
||||
await _updateStatus();
|
||||
_scanCustomizeDir();
|
||||
final l =
|
||||
await handleError(() => Api.getScLocalizationData(selectedLanguage));
|
||||
if (l != null) {
|
||||
apiLocalizationData = {};
|
||||
for (var element in l) {
|
||||
final isPTU = !scInstallPath.contains("LIVE");
|
||||
if (isPTU && element.channel == "PTU") {
|
||||
apiLocalizationData![element.versionName ?? ""] = element;
|
||||
} else if (!isPTU && element.channel == "PU") {
|
||||
apiLocalizationData![element.versionName ?? ""] = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
customizeDirListenSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_scanCustomizeDir() {
|
||||
final fileList = customizeDir.listSync();
|
||||
customizeList = [];
|
||||
for (var value in fileList) {
|
||||
if (value is File && value.path.endsWith(".ini")) {
|
||||
customizeList?.add(value.absolute.path);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String getCustomizeFileName(String path) {
|
||||
return path.split("\\").last;
|
||||
}
|
||||
|
||||
_updateStatus() async {
|
||||
patchStatus = MapEntry(await getLangCfgEnableLang(lang: selectedLanguage),
|
||||
await getInstalledIniVersion());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
VoidCallback? onBack() {
|
||||
if (workingVersion.isNotEmpty) return null;
|
||||
return () {
|
||||
Navigator.pop(context!);
|
||||
};
|
||||
}
|
||||
|
||||
void selectLang(String v) {
|
||||
selectedLanguage = v;
|
||||
apiLocalizationData = null;
|
||||
notifyListeners();
|
||||
reloadData();
|
||||
}
|
||||
|
||||
VoidCallback? doRefresh() {
|
||||
if (workingVersion.isNotEmpty) return null;
|
||||
return () {
|
||||
apiLocalizationData = null;
|
||||
notifyListeners();
|
||||
reloadData();
|
||||
};
|
||||
}
|
||||
|
||||
VoidCallback? doRemoteInstall(ScLocalizationData value) {
|
||||
return () async {
|
||||
final downloadUrl =
|
||||
"${AppConf.gitlabLocalizationUrl}/-/archive/${value.versionName}/LocalizationData-${value.versionName}.tar.bz2";
|
||||
final savePath =
|
||||
File("${downloadDir.absolute.path}\\${value.versionName}.sclang");
|
||||
try {
|
||||
workingVersion = value.versionName!;
|
||||
notifyListeners();
|
||||
if (!await savePath.exists()) {
|
||||
// download
|
||||
dPrint("downloading file to $savePath");
|
||||
await Dio().download(downloadUrl, savePath.absolute.path);
|
||||
} else {
|
||||
dPrint("use cache $savePath");
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
// check file
|
||||
final globalIni = await compute(_readArchive, savePath.absolute.path);
|
||||
if (globalIni.isEmpty) {
|
||||
throw "文件受损,请重新下载";
|
||||
}
|
||||
await _installFormString(globalIni, value.versionName ?? "");
|
||||
} catch (e) {
|
||||
await showToast(context!, "安装出错!\n\n $e");
|
||||
if (await savePath.exists()) await savePath.delete();
|
||||
}
|
||||
workingVersion = "";
|
||||
notifyListeners();
|
||||
};
|
||||
}
|
||||
|
||||
Future<bool> getLangCfgEnableLang({String lang = ""}) async {
|
||||
if (!await cfgFile.exists()) return false;
|
||||
final str = (await cfgFile.readAsString()).replaceAll(" ", "");
|
||||
return str.contains("sys_languages=$lang") &&
|
||||
str.contains("g_language=$lang");
|
||||
}
|
||||
|
||||
Future<String> getInstalledIniVersion() async {
|
||||
final iniFile = File(
|
||||
"${scDataDir.absolute.path}\\Localization\\$selectedLanguage\\global.ini");
|
||||
if (!await iniFile.exists()) return "游戏内置";
|
||||
final iniStringSplit = (await iniFile.readAsString()).split("\n");
|
||||
for (var i = iniStringSplit.length - 1; i > 0; i--) {
|
||||
if (iniStringSplit[i]
|
||||
.contains("_starcitizen_doctor_localization_version=")) {
|
||||
final v = iniStringSplit[i]
|
||||
.trim()
|
||||
.split("_starcitizen_doctor_localization_version=")[1];
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return "自定义文件";
|
||||
}
|
||||
|
||||
_installFormString(StringBuffer globalIni, String versionName) async {
|
||||
final iniFile = File(
|
||||
"${scDataDir.absolute.path}\\Localization\\$selectedLanguage\\global.ini");
|
||||
if (versionName.isNotEmpty) {
|
||||
if (!globalIni.toString().endsWith("\n")) {
|
||||
globalIni.write("\n");
|
||||
}
|
||||
globalIni.write("_starcitizen_doctor_localization_version=$versionName");
|
||||
}
|
||||
|
||||
/// write cfg
|
||||
if (await cfgFile.exists()) {}
|
||||
|
||||
/// write ini
|
||||
if (await iniFile.exists()) {
|
||||
await iniFile.delete();
|
||||
}
|
||||
await iniFile.create(recursive: true);
|
||||
await iniFile.writeAsString("\uFEFF${globalIni.toString().trim()}",
|
||||
flush: true);
|
||||
await updateLangCfg(true);
|
||||
await _updateStatus();
|
||||
}
|
||||
|
||||
openDir() async {
|
||||
showToast(context!,
|
||||
"即将打开本地化文件夹,请将自定义的 任意名称.ini 文件放入 Customize_ini 文件夹。\n\n添加新文件后未显示请使用右上角刷新按钮。\n\n安装时请确保选择了正确的语言。");
|
||||
await Process.run("powershell.exe",
|
||||
["explorer.exe", "/select,\"${customizeDir.absolute.path}\"\\"]);
|
||||
}
|
||||
|
||||
updateLangCfg(bool enable) async {
|
||||
final status = await getLangCfgEnableLang(lang: selectedLanguage);
|
||||
final exists = await cfgFile.exists();
|
||||
if (status == enable) {
|
||||
await _updateStatus();
|
||||
return;
|
||||
}
|
||||
StringBuffer newStr = StringBuffer();
|
||||
var str = <String>[];
|
||||
if (exists) {
|
||||
str = (await cfgFile.readAsString()).replaceAll(" ", "").split("\n");
|
||||
}
|
||||
if (enable) {
|
||||
if (exists) {
|
||||
for (var value in str) {
|
||||
if (value.contains("sys_languages=")) {
|
||||
value = "sys_languages=$selectedLanguage";
|
||||
} else if (value.contains("g_language")) {
|
||||
value = "g_language=$selectedLanguage";
|
||||
}
|
||||
if (value.trim().isNotEmpty) newStr.writeln(value);
|
||||
}
|
||||
}
|
||||
if (!newStr.toString().contains("sys_languages=$selectedLanguage")) {
|
||||
newStr.writeln("sys_languages=$selectedLanguage");
|
||||
}
|
||||
if (!newStr.toString().contains("g_language=$selectedLanguage")) {
|
||||
newStr.writeln("g_language=$selectedLanguage");
|
||||
}
|
||||
} else {
|
||||
if (exists) {
|
||||
for (var value in str) {
|
||||
if (value.contains("sys_languages=")) {
|
||||
continue;
|
||||
} else if (value.contains("g_language")) {
|
||||
continue;
|
||||
}
|
||||
newStr.writeln(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (exists) await cfgFile.delete(recursive: true);
|
||||
await cfgFile.create(recursive: true);
|
||||
await cfgFile.writeAsString(newStr.toString());
|
||||
await _updateStatus();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
VoidCallback? doDelIniFile() {
|
||||
final iniFile = File(
|
||||
"${scDataDir.absolute.path}\\Localization\\$selectedLanguage\\global.ini");
|
||||
return () async {
|
||||
if (await iniFile.exists()) await iniFile.delete();
|
||||
await _updateStatus();
|
||||
};
|
||||
}
|
||||
|
||||
/// read locale active
|
||||
static StringBuffer _readArchive(String savePath) {
|
||||
final inputStream = InputFileStream(savePath);
|
||||
final archive =
|
||||
TarDecoder().decodeBytes(BZip2Decoder().decodeBuffer(inputStream));
|
||||
StringBuffer globalIni = StringBuffer("");
|
||||
for (var element in archive.files) {
|
||||
if (element.name.contains("global.ini")) {
|
||||
for (var value
|
||||
in (element.rawContent?.readString() ?? "").split("\n")) {
|
||||
final tv = value.trim();
|
||||
if (tv.isNotEmpty) globalIni.writeln(tv);
|
||||
}
|
||||
}
|
||||
}
|
||||
archive.clear();
|
||||
return globalIni;
|
||||
}
|
||||
|
||||
VoidCallback? doLocalInstall(String filePath) {
|
||||
if (workingVersion.isNotEmpty) return null;
|
||||
return () async {
|
||||
final f = File(filePath);
|
||||
if (!await f.exists()) return;
|
||||
workingVersion = filePath;
|
||||
notifyListeners();
|
||||
final str = await f.readAsString();
|
||||
await _installFormString(
|
||||
StringBuffer(str), "自定义_${getCustomizeFileName(filePath)}");
|
||||
workingVersion = "";
|
||||
notifyListeners();
|
||||
};
|
||||
}
|
||||
}
|
259
lib/ui/home/performance/performance_ui.dart
Normal file
259
lib/ui/home/performance/performance_ui.dart
Normal file
@ -0,0 +1,259 @@
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:starcitizen_doctor/base/ui.dart';
|
||||
import 'package:starcitizen_doctor/data/game_performance_data.dart';
|
||||
|
||||
import 'performance_ui_model.dart';
|
||||
|
||||
class PerformanceUI extends BaseUI<PerformanceUIModel> {
|
||||
@override
|
||||
Widget? buildBody(BuildContext context, PerformanceUIModel model) {
|
||||
var content = makeLoading(context);
|
||||
|
||||
if (model.performanceMap != null) {
|
||||
content = Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, left: 12, right: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
if (model.showGraphicsPerformanceTip)
|
||||
InfoBar(
|
||||
title: const Text("图形优化提示"),
|
||||
content: const Text(
|
||||
"该功能对优化显卡瓶颈有很大帮助,但对 CPU 瓶颈可能起返效果,如果您显卡性能强劲,可以尝试使用更好的画质来获得更高的显卡利用率。",
|
||||
),
|
||||
onClose: () => model.closeTip(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"当前状态:${model.enabled ? "已应用" : "未应用"}",
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(width: 32),
|
||||
const Text(
|
||||
"预设:",
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
for (final item in const {
|
||||
"low": "低",
|
||||
"medium": "中",
|
||||
"high": "高",
|
||||
"ultra": "超级"
|
||||
}.entries)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6, right: 6),
|
||||
child: Button(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 2, bottom: 2, left: 4, right: 4),
|
||||
child: Text(item.value),
|
||||
),
|
||||
onPressed: () =>
|
||||
model.onChangePreProfile(item.key)),
|
||||
),
|
||||
const Text("(预设只修改图形设置)"),
|
||||
const Spacer(),
|
||||
Button(
|
||||
onPressed: () => model.refresh(),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(FluentIcons.refresh),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Button(
|
||||
child: const Text(
|
||||
" 恢复默认 ",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
onPressed: () => model.clean()),
|
||||
const SizedBox(width: 24),
|
||||
Button(
|
||||
child: const Text(
|
||||
"应用",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
onPressed: () => model.applyProfile(false)),
|
||||
const SizedBox(width: 6),
|
||||
Button(
|
||||
child: const Text(
|
||||
"应用并清理着色器(推荐)",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
onPressed: () => model.applyProfile(true)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: MasonryGridView.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 1,
|
||||
crossAxisSpacing: 1,
|
||||
itemCount: model.performanceMap!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return makeItemGroup(
|
||||
model.performanceMap!.entries.elementAt(index));
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (model.workingString.isNotEmpty)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(150),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ProgressRing(),
|
||||
const SizedBox(height: 12),
|
||||
Text(model.workingString),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return makeDefaultPage(context, model, content: content);
|
||||
}
|
||||
|
||||
Widget makeItemGroup(MapEntry<String?, List<GamePerformanceData>> group) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: FluentTheme.of(context).cardColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${group.key}",
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
color: FluentTheme.of(context).cardColor.withOpacity(.2),
|
||||
height: 1),
|
||||
const SizedBox(height: 6),
|
||||
for (final item in group.value) makeItem(item)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeItem(GamePerformanceData item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"${item.name}",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (item.type == "int")
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: TextFormBox(
|
||||
key: UniqueKey(),
|
||||
initialValue: "${item.value}",
|
||||
onFieldSubmitted: (str) {
|
||||
dPrint(str);
|
||||
if (str.isEmpty) return;
|
||||
final v = int.tryParse(str);
|
||||
if (v != null &&
|
||||
v < (item.max ?? 0) &&
|
||||
v >= (item.min ?? 0)) {
|
||||
item.value = v;
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
onTapOutside: (e) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 32),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 4,
|
||||
child: Slider(
|
||||
value: item.value?.toDouble() ?? 0,
|
||||
min: item.min?.toDouble() ?? 0,
|
||||
max: item.max?.toDouble() ?? 0,
|
||||
onChanged: (double value) {
|
||||
item.value = value.toInt();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
else if (item.type == "bool")
|
||||
Column(
|
||||
children: [
|
||||
ToggleSwitch(
|
||||
checked: item.value == 1,
|
||||
onChanged: (bool value) {
|
||||
item.value = value ? 1 : 0;
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
if (item.info != null && item.info!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"${item.info}",
|
||||
style:
|
||||
TextStyle(fontSize: 14, color: Colors.white.withOpacity(.6)),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${item.key} 最小值: ${item.min} / 最大值: ${item.max}",
|
||||
style: TextStyle(color: Colors.white.withOpacity(.6)),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
color: FluentTheme.of(context).cardColor.withOpacity(.1),
|
||||
height: 1),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String getUITitle(BuildContext context, PerformanceUIModel model) =>
|
||||
"性能优化 ${model.scPath}";
|
||||
}
|
184
lib/ui/home/performance/performance_ui_model.dart
Normal file
184
lib/ui/home/performance/performance_ui_model.dart
Normal file
@ -0,0 +1,184 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
|
||||
import 'package:starcitizen_doctor/data/game_performance_data.dart';
|
||||
|
||||
class PerformanceUIModel extends BaseUIModel {
|
||||
String scPath;
|
||||
|
||||
PerformanceUIModel(this.scPath);
|
||||
|
||||
Map<String?, List<GamePerformanceData>>? performanceMap;
|
||||
|
||||
String workingString = "";
|
||||
|
||||
late final confFile = File("$scPath\\USER.cfg");
|
||||
|
||||
bool enabled = false;
|
||||
|
||||
bool showGraphicsPerformanceTip = false;
|
||||
static const _graphicsPerformanceTipVersion = 1;
|
||||
|
||||
@override
|
||||
Future loadData() async {
|
||||
final String jsonString =
|
||||
await rootBundle.loadString('assets/performance.json');
|
||||
final list = json.decode(jsonString);
|
||||
|
||||
if (list is List) {
|
||||
performanceMap = {};
|
||||
for (var element in list) {
|
||||
final item = GamePerformanceData.fromJson(element);
|
||||
performanceMap?[item.group] ??= [];
|
||||
performanceMap?[item.group]?.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (await confFile.exists()) {
|
||||
await _readConf();
|
||||
} else {
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
final box = await Hive.openBox("app_conf");
|
||||
final v = box.get("close_graphics_performance_tip", defaultValue: -1);
|
||||
showGraphicsPerformanceTip = v != _graphicsPerformanceTipVersion;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
onChangePreProfile(String key) {
|
||||
switch (key) {
|
||||
case "low":
|
||||
performanceMap?.forEach((key, v) {
|
||||
if (key?.contains("图形") ?? false) {
|
||||
for (var element in v) {
|
||||
element.value = element.min;
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "medium":
|
||||
performanceMap?.forEach((key, v) {
|
||||
if (key?.contains("图形") ?? false) {
|
||||
for (var element in v) {
|
||||
element.value = ((element.max ?? 0) ~/ 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "high":
|
||||
performanceMap?.forEach((key, v) {
|
||||
if (key?.contains("图形") ?? false) {
|
||||
for (var element in v) {
|
||||
element.value = ((element.max ?? 0) / 1.5).ceil();
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "ultra":
|
||||
performanceMap?.forEach((key, v) {
|
||||
if (key?.contains("图形") ?? false) {
|
||||
for (var element in v) {
|
||||
element.value = element.max;
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
applyProfile(bool cleanShader) async {
|
||||
if (performanceMap == null) return;
|
||||
workingString = "生成配置文件";
|
||||
notifyListeners();
|
||||
String conf = "";
|
||||
for (var v in performanceMap!.entries) {
|
||||
for (var c in v.value) {
|
||||
conf = "$conf${c.key} = ${c.value}\n";
|
||||
}
|
||||
}
|
||||
workingString = "写出配置文件";
|
||||
notifyListeners();
|
||||
if (await confFile.exists()) {
|
||||
await confFile.delete();
|
||||
}
|
||||
await confFile.create();
|
||||
await confFile.writeAsString(conf);
|
||||
if (cleanShader) {
|
||||
workingString = "清理着色器";
|
||||
notifyListeners();
|
||||
await _cleanShaderCache();
|
||||
}
|
||||
workingString = "完成...";
|
||||
notifyListeners();
|
||||
await await Future.delayed(const Duration(milliseconds: 300));
|
||||
await reloadData();
|
||||
workingString = "";
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _cleanShaderCache() async {
|
||||
final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath();
|
||||
final l =
|
||||
await Directory(gameShaderCachePath!).list(recursive: false).toList();
|
||||
for (var value in l) {
|
||||
if (value is Directory) {
|
||||
if (!value.absolute.path.contains("Crashes")) {
|
||||
await value.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
showToast(context!, "清理着色器后首次进入游戏可能会出现卡顿,请耐心等待游戏初始化完毕。");
|
||||
}
|
||||
|
||||
_readConf() async {
|
||||
if (performanceMap == null) return;
|
||||
enabled = true;
|
||||
final confString = await confFile.readAsString();
|
||||
for (var value in confString.split("\n")) {
|
||||
final kv = value.split("=");
|
||||
for (var m in performanceMap!.entries) {
|
||||
for (var value in m.value) {
|
||||
if (value.key == kv[0].trim()) {
|
||||
final v = int.tryParse(kv[1].trim());
|
||||
if (v != null) value.value = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
clean() async {
|
||||
workingString = "删除配置文件...";
|
||||
notifyListeners();
|
||||
if (await confFile.exists()) {
|
||||
await confFile.delete(recursive: true);
|
||||
}
|
||||
workingString = "清理着色器";
|
||||
notifyListeners();
|
||||
await _cleanShaderCache();
|
||||
workingString = "完成...";
|
||||
await await Future.delayed(const Duration(milliseconds: 300));
|
||||
await reloadData();
|
||||
workingString = "";
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
refresh() async {
|
||||
await reloadData();
|
||||
}
|
||||
|
||||
closeTip() async {
|
||||
final box = await Hive.openBox("app_conf");
|
||||
await box.put(
|
||||
"close_graphics_performance_tip", _graphicsPerformanceTipVersion);
|
||||
loadData();
|
||||
}
|
||||
}
|
204
lib/ui/home/webview/webview.dart
Normal file
204
lib/ui/home/webview/webview.dart
Normal file
@ -0,0 +1,204 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:starcitizen_doctor/common/conf.dart';
|
||||
import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart';
|
||||
|
||||
import '../../../api/api.dart';
|
||||
import '../../../base/ui.dart';
|
||||
|
||||
class WebViewModel {
|
||||
late Webview webview;
|
||||
final BuildContext context;
|
||||
|
||||
bool _isClosed = false;
|
||||
|
||||
bool get isClosed => _isClosed;
|
||||
|
||||
WebViewModel(this.context);
|
||||
|
||||
String url = "";
|
||||
bool canGoBack = false;
|
||||
|
||||
final localizationResource = <String, dynamic>{};
|
||||
|
||||
var localizationScript = "";
|
||||
|
||||
bool enableCapture = false;
|
||||
|
||||
initWebView({String title = ""}) async {
|
||||
try {
|
||||
webview = await WebviewWindow.create(
|
||||
configuration: CreateConfiguration(
|
||||
windowWidth: 1920,
|
||||
windowHeight: 1080,
|
||||
userDataFolderWindows:
|
||||
"${AppConf.applicationSupportDir}/webview_data",
|
||||
title: title));
|
||||
// webview.openDevToolsWindow();
|
||||
webview.isNavigating.addListener(() async {
|
||||
if (!webview.isNavigating.value && localizationResource.isNotEmpty) {
|
||||
final uri = Uri.parse(url);
|
||||
if (uri.host.contains("robertsspaceindustries.com")) {
|
||||
// SC 官网
|
||||
dPrint("load script");
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await webview.evaluateJavaScript(localizationScript);
|
||||
dPrint("update replaceWords");
|
||||
final replaceWords = _getLocalizationResource("zh-CN");
|
||||
|
||||
const org = "https://robertsspaceindustries.com/orgs";
|
||||
const citizens = "https://robertsspaceindustries.com/citizens";
|
||||
const organization =
|
||||
"https://robertsspaceindustries.com/account/organization";
|
||||
const concierge =
|
||||
"https://robertsspaceindustries.com/account/concierge";
|
||||
const referral =
|
||||
"https://robertsspaceindustries.com/account/referral-program";
|
||||
const address =
|
||||
"https://robertsspaceindustries.com/account/addresses";
|
||||
|
||||
const hangar = "https://robertsspaceindustries.com/account/pledges";
|
||||
|
||||
if (url.startsWith(org) ||
|
||||
url.startsWith(citizens) ||
|
||||
url.startsWith(organization)) {
|
||||
replaceWords.add({"word": 'members', "replacement": '名成员'});
|
||||
replaceWords.addAll(_getLocalizationResource("orgs"));
|
||||
}
|
||||
|
||||
if (address.startsWith(address)) {
|
||||
replaceWords.addAll(_getLocalizationResource("address"));
|
||||
}
|
||||
|
||||
if (url.startsWith(referral)) {
|
||||
replaceWords.addAll([
|
||||
{"word": 'Total recruits: ', "replacement": '总邀请数:'},
|
||||
{"word": 'Prospects ', "replacement": '未完成的邀请'},
|
||||
{"word": 'Recruits', "replacement": '已完成的邀请'},
|
||||
]);
|
||||
}
|
||||
|
||||
if (url.startsWith(concierge)) {
|
||||
replaceWords.addAll(_getLocalizationResource("concierge"));
|
||||
}
|
||||
if (url.startsWith(hangar)) {
|
||||
replaceWords.addAll(_getLocalizationResource("hangar"));
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await webview.evaluateJavaScript(
|
||||
"WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)");
|
||||
} else if (uri.host.contains("www.erkul.games") ||
|
||||
uri.host.contains("uexcorp.space") ||
|
||||
uri.host.contains("ccugame.app")) {
|
||||
// 工具网站
|
||||
dPrint("load script");
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await webview.evaluateJavaScript(localizationScript);
|
||||
dPrint("update replaceWords");
|
||||
final replaceWords = _getLocalizationResource("UEX");
|
||||
await webview.evaluateJavaScript(
|
||||
"WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)");
|
||||
}
|
||||
}
|
||||
});
|
||||
webview.addOnUrlRequestCallback((url) {
|
||||
dPrint("OnUrlRequestCallback === $url");
|
||||
this.url = url;
|
||||
});
|
||||
webview.onClose.whenComplete(() {
|
||||
_isClosed = true;
|
||||
});
|
||||
} catch (e) {
|
||||
showToast(context, "初始化失败:$e");
|
||||
}
|
||||
}
|
||||
|
||||
launch(String url) async {
|
||||
webview.launch(url);
|
||||
}
|
||||
|
||||
initLocalization() async {
|
||||
localizationScript =
|
||||
await rootBundle.loadString('assets/localization_web_script.js');
|
||||
|
||||
/// https://github.com/CxJuice/Uex_Chinese_Translate
|
||||
// get versions
|
||||
const hostUrl = "https://ch.citizenwiki.cn/json-files";
|
||||
|
||||
final v = AppWebLocalizationVersionsData.fromJson(
|
||||
await _getJson("$hostUrl/versions.json"));
|
||||
dPrint("AppWebLocalizationVersionsData === ${v.toJson()}");
|
||||
|
||||
localizationResource["zh-CN"] = await _getJson("$hostUrl/zh-CN-rsi.json",
|
||||
cacheKey: "rsi", version: v.rsi);
|
||||
localizationResource["concierge"] = await _getJson(
|
||||
"$hostUrl/concierge.json",
|
||||
cacheKey: "concierge",
|
||||
version: v.concierge);
|
||||
localizationResource["orgs"] =
|
||||
await _getJson("$hostUrl/orgs.json", cacheKey: "orgs", version: v.orgs);
|
||||
localizationResource["address"] = await _getJson("$hostUrl/addresses.json",
|
||||
cacheKey: "addresses", version: v.addresses);
|
||||
localizationResource["hangar"] = await _getJson("$hostUrl/hangar.json",
|
||||
cacheKey: "hangar", version: v.hangar);
|
||||
localizationResource["UEX"] = await _getJson("$hostUrl/zh-CN-uex.json",
|
||||
cacheKey: "uex", version: v.uex);
|
||||
}
|
||||
|
||||
List<Map<String, String>> _getLocalizationResource(String key) {
|
||||
final List<Map<String, String>> localizations = [];
|
||||
final dict = localizationResource[key]?["dict"];
|
||||
if (dict is Map) {
|
||||
for (var element in dict.entries) {
|
||||
final k = element.key
|
||||
.toString()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(RegExp("/\xa0/g"), ' ')
|
||||
.replaceAll(RegExp("/s{2,}/g"), ' ');
|
||||
localizations
|
||||
.add({"word": k, "replacement": element.value.toString().trim()});
|
||||
}
|
||||
}
|
||||
return localizations;
|
||||
}
|
||||
|
||||
Future<Map> _getJson(String url,
|
||||
{String cacheKey = "", String? version}) async {
|
||||
final box = await Hive.openBox("web_localization_cache_data");
|
||||
if (cacheKey.isNotEmpty) {
|
||||
final localVersion = box.get("${cacheKey}_version}", defaultValue: "");
|
||||
var data = box.get(cacheKey, defaultValue: {});
|
||||
if (data is Map && data.isNotEmpty && localVersion == version) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
final startTime = DateTime.now();
|
||||
final r = await Api.dio
|
||||
.get(url, options: Options(responseType: ResponseType.plain));
|
||||
final endTime = DateTime.now();
|
||||
final data = json.decode(r.data);
|
||||
if (cacheKey.isNotEmpty) {
|
||||
dPrint(
|
||||
"update $cacheKey v == $version time == ${(endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch) / 1000 / 1000}s");
|
||||
await box.put(cacheKey, data);
|
||||
await box.put("${cacheKey}_version}", version);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void addOnWebMessageReceivedCallback(OnWebMessageReceivedCallback callback) {
|
||||
webview.addOnWebMessageReceivedCallback(callback);
|
||||
}
|
||||
|
||||
void removeOnWebMessageReceivedCallback(
|
||||
OnWebMessageReceivedCallback callback) {
|
||||
webview.removeOnWebMessageReceivedCallback(callback);
|
||||
}
|
||||
}
|
43
lib/ui/home/webview/webview_localization_capture_ui.dart
Normal file
43
lib/ui/home/webview/webview_localization_capture_ui.dart
Normal file
@ -0,0 +1,43 @@
|
||||
import 'package:markdown_widget/config/all.dart';
|
||||
import 'package:markdown_widget/widget/blocks/leaf/code_block.dart';
|
||||
import 'package:markdown_widget/widget/markdown.dart';
|
||||
import 'package:starcitizen_doctor/base/ui.dart';
|
||||
|
||||
import 'webview_localization_capture_ui_model.dart';
|
||||
|
||||
class WebviewLocalizationCaptureUI
|
||||
extends BaseUI<WebviewLocalizationCaptureUIModel> {
|
||||
@override
|
||||
Widget? buildBody(
|
||||
BuildContext context, WebviewLocalizationCaptureUIModel model) {
|
||||
return makeDefaultPage(context, model,
|
||||
content: model.data.isEmpty
|
||||
? const Center(
|
||||
child: Text("等待数据"),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MarkdownWidget(
|
||||
data: model.renderString,
|
||||
config: MarkdownConfig(configs: [
|
||||
const PreConfig(
|
||||
decoration: BoxDecoration(
|
||||
color: Color.fromRGBO(0, 0, 0, .4),
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
)),
|
||||
]),
|
||||
))
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(FluentIcons.refresh), onPressed: model.doClean)
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
String getUITitle(
|
||||
BuildContext context, WebviewLocalizationCaptureUIModel model) =>
|
||||
"Webview 翻译捕获工具";
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
|
||||
|
||||
class WebviewLocalizationCaptureUIModel extends BaseUIModel {
|
||||
final WebViewModel webViewModel;
|
||||
|
||||
WebviewLocalizationCaptureUIModel(this.webViewModel);
|
||||
|
||||
Map<String, dynamic> data = {};
|
||||
Map<String, dynamic> oldData = {};
|
||||
|
||||
String renderString = "";
|
||||
|
||||
final jsonEncoder = const JsonEncoder.withIndent(' ');
|
||||
|
||||
@override
|
||||
void initModel() {
|
||||
webViewModel.addOnWebMessageReceivedCallback(_onMessage);
|
||||
super.initModel();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
webViewModel.removeOnWebMessageReceivedCallback(_onMessage);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onMessage(String message) {
|
||||
final map = json.decode(message);
|
||||
if (map["action"] == "webview_localization_capture") {
|
||||
dPrint(
|
||||
"<WebviewLocalizationCaptureUIModel> webview_localization_capture message == $map");
|
||||
if (!oldData.containsKey(map["key"])) {
|
||||
data[map["key"].toString().trim().toLowerCase().replaceAll(" ", "_")] =
|
||||
map["value"];
|
||||
}
|
||||
_updateRenderString();
|
||||
}
|
||||
}
|
||||
|
||||
_updateRenderString() {
|
||||
renderString = "```json\n${jsonEncoder.convert(data)}\n```";
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
doClean() {
|
||||
oldData.addAll(data);
|
||||
data.clear();
|
||||
_updateRenderString();
|
||||
}
|
||||
}
|
104
lib/ui/index_ui.dart
Normal file
104
lib/ui/index_ui.dart
Normal file
@ -0,0 +1,104 @@
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
import 'package:starcitizen_doctor/common/conf.dart';
|
||||
import 'package:starcitizen_doctor/main.dart';
|
||||
import 'package:starcitizen_doctor/ui/about/about_ui.dart';
|
||||
import 'package:starcitizen_doctor/ui/about/about_ui_model.dart';
|
||||
import 'package:starcitizen_doctor/ui/home/home_ui.dart';
|
||||
import 'package:starcitizen_doctor/ui/settings/settings_ui.dart';
|
||||
import 'package:starcitizen_doctor/ui/settings/settings_ui_model.dart';
|
||||
import 'package:starcitizen_doctor/ui/tools/tools_ui.dart';
|
||||
import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'home/home_ui_model.dart';
|
||||
import 'index_ui_model.dart';
|
||||
|
||||
class IndexUI extends BaseUI<IndexUIModel> {
|
||||
@override
|
||||
Widget? buildBody(BuildContext context, IndexUIModel model) {
|
||||
return NavigationView(
|
||||
appBar: NavigationAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
title: () {
|
||||
return DragToMoveArea(
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset("assets/app_logo.png", width: 24, height: 24),
|
||||
const SizedBox(width: 12),
|
||||
const Text("星际公民盒子 V${AppConf.appVersion}"),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}(),
|
||||
actions: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [WindowButtons()],
|
||||
)),
|
||||
pane: NavigationPane(
|
||||
selected: model.curIndex,
|
||||
items: getNavigationPaneItems(model),
|
||||
size: const NavigationPaneSize(openWidth: 160),
|
||||
),
|
||||
paneBodyBuilder: (item, child) {
|
||||
// final name =
|
||||
// item?.key is ValueKey ? (item!.key as ValueKey).value : null;
|
||||
return FocusTraversalGroup(
|
||||
key: ValueKey('body_${model.curIndex}'),
|
||||
child: getPage(model),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget getPage(IndexUIModel model) {
|
||||
switch (model.curIndex) {
|
||||
case 0:
|
||||
return BaseUIContainer(
|
||||
uiCreate: () => HomeUI(),
|
||||
modelCreate: () =>
|
||||
model.getChildUIModelProviders<HomeUIModel>("home"));
|
||||
case 1:
|
||||
return BaseUIContainer(
|
||||
uiCreate: () => ToolsUI(),
|
||||
modelCreate: () =>
|
||||
model.getChildUIModelProviders<ToolsUIModel>("tools"));
|
||||
case 2:
|
||||
return BaseUIContainer(
|
||||
uiCreate: () => SettingUI(),
|
||||
modelCreate: () =>
|
||||
model.getChildUIModelProviders<SettingUIModel>("settings"));
|
||||
case 3:
|
||||
return BaseUIContainer(
|
||||
uiCreate: () => AboutUI(),
|
||||
modelCreate: () =>
|
||||
model.getChildUIModelProviders<AboutUIModel>("about"));
|
||||
}
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
List<NavigationPaneItem> getNavigationPaneItems(IndexUIModel model) {
|
||||
final menus = {
|
||||
FluentIcons.home: "首页",
|
||||
FluentIcons.toolbox: "工具",
|
||||
FluentIcons.settings: "设置",
|
||||
FluentIcons.info: "关于",
|
||||
};
|
||||
return [
|
||||
for (final kv in menus.entries)
|
||||
PaneItem(
|
||||
icon: Icon(kv.key),
|
||||
title: Text(kv.value),
|
||||
body: const SizedBox.shrink(),
|
||||
onTap: () {
|
||||
model.onIndexMenuTap(kv.value);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
String getUITitle(BuildContext context, IndexUIModel model) => "";
|
||||
}
|
75
lib/ui/index_ui_model.dart
Normal file
75
lib/ui/index_ui_model.dart
Normal file
@ -0,0 +1,75 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
import 'package:starcitizen_doctor/global_ui_model.dart';
|
||||
import 'package:starcitizen_doctor/ui/about/about_ui_model.dart';
|
||||
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
|
||||
import 'package:starcitizen_doctor/ui/settings/settings_ui_model.dart';
|
||||
import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class IndexUIModel extends BaseUIModel {
|
||||
int curIndex = 0;
|
||||
|
||||
@override
|
||||
void initModel() {
|
||||
_checkRunTime();
|
||||
Future.delayed(const Duration(milliseconds: 300))
|
||||
.then((value) => globalUIModel.checkUpdate(context!));
|
||||
super.initModel();
|
||||
}
|
||||
|
||||
@override
|
||||
BaseUIModel? onCreateChildUIModel(modelKey) {
|
||||
switch (modelKey) {
|
||||
case "home":
|
||||
return HomeUIModel();
|
||||
case "tools":
|
||||
return ToolsUIModel();
|
||||
case "settings":
|
||||
return SettingUIModel();
|
||||
case "about":
|
||||
return AboutUIModel();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void onIndexMenuTap(String value) {
|
||||
final index = {
|
||||
"首页": 0,
|
||||
"工具": 1,
|
||||
"设置": 2,
|
||||
"关于": 3,
|
||||
};
|
||||
curIndex = index[value] ?? 0;
|
||||
switch (curIndex) {
|
||||
case 0:
|
||||
getCreatedChildUIModel("home")?.reloadData();
|
||||
break;
|
||||
case 1:
|
||||
getCreatedChildUIModel("tools")?.reloadData();
|
||||
break;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _checkRunTime() async {
|
||||
Future<void> onError() async {
|
||||
await showToast(context!, "运行环境出错,请检查系统环境变量 (PATH)!");
|
||||
await launchUrlString(
|
||||
"https://answers.microsoft.com/zh-hans/windows/forum/all/%E7%B3%BB%E7%BB%9F%E7%8E%AF%E5%A2%83%E5%8F%98/b88369e6-2620-4a77-b07a-d0af50894a07");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
var result = await Process.run('powershell.exe', ["echo", "ping"]);
|
||||
if (result.stdout.toString().startsWith("ping")) {
|
||||
dPrint("powershell check pass");
|
||||
} else {
|
||||
onError();
|
||||
}
|
||||
} catch (e) {
|
||||
onError();
|
||||
}
|
||||
}
|
||||
}
|
14
lib/ui/settings/settings_ui.dart
Normal file
14
lib/ui/settings/settings_ui.dart
Normal file
@ -0,0 +1,14 @@
|
||||
import 'package:starcitizen_doctor/base/ui.dart';
|
||||
import 'package:starcitizen_doctor/ui/settings/settings_ui_model.dart';
|
||||
|
||||
class SettingUI extends BaseUI<SettingUIModel> {
|
||||
@override
|
||||
Widget? buildBody(BuildContext context, SettingUIModel model) {
|
||||
return const Center(
|
||||
child: Text("暂时没啥好设置的。"),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String getUITitle(BuildContext context, SettingUIModel model) => "SettingUI";
|
||||
}
|
5
lib/ui/settings/settings_ui_model.dart
Normal file
5
lib/ui/settings/settings_ui_model.dart
Normal file
@ -0,0 +1,5 @@
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
|
||||
class SettingUIModel extends BaseUIModel {
|
||||
|
||||
}
|
85
lib/ui/settings/upgrade_dialog_ui.dart
Normal file
85
lib/ui/settings/upgrade_dialog_ui.dart
Normal file
@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart' show Material;
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
import 'package:starcitizen_doctor/common/conf.dart';
|
||||
|
||||
import 'upgrade_dialog_ui_model.dart';
|
||||
|
||||
class UpgradeDialogUI extends BaseUI<UpgradeDialogUIModel> {
|
||||
@override
|
||||
Widget? buildBody(BuildContext context, UpgradeDialogUIModel model) {
|
||||
return Material(
|
||||
child: ContentDialog(
|
||||
title: const Text("发现新版本"),
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .55),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (model.description == null) ...[
|
||||
const Center(
|
||||
child: Column(
|
||||
children: [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 16),
|
||||
Text("正在获取新版本详情...")
|
||||
],
|
||||
),
|
||||
)
|
||||
] else
|
||||
...makeMarkdownView(model.description!),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (model.isUpgrading) ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Text(model.progress == 100
|
||||
? "正在安装: "
|
||||
: "正在下载: ${model.progress?.toStringAsFixed(2) ?? 0}% "),
|
||||
Expanded(
|
||||
child: ProgressBar(
|
||||
value: model.progress == 100 ? null : model.progress,
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: model.isUpgrading
|
||||
? null
|
||||
: [
|
||||
if (model.downloadUrl.isNotEmpty)
|
||||
FilledButton(
|
||||
onPressed: model.doUpgrade,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 4, bottom: 4, left: 8, right: 8),
|
||||
child: Text("立即更新"),
|
||||
)),
|
||||
if (AppConf.appVersionCode <=
|
||||
(AppConf.networkVersionData?.minVersionCode ?? 0))
|
||||
Button(
|
||||
onPressed: model.doCancel,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 4, bottom: 4, left: 8, right: 8),
|
||||
child: Text("下次吧"),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String getUITitle(BuildContext context, UpgradeDialogUIModel model) => "";
|
||||
}
|
72
lib/ui/settings/upgrade_dialog_ui_model.dart
Normal file
72
lib/ui/settings/upgrade_dialog_ui_model.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:starcitizen_doctor/api/api.dart';
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
import 'package:starcitizen_doctor/common/conf.dart';
|
||||
|
||||
class UpgradeDialogUIModel extends BaseUIModel {
|
||||
String? description;
|
||||
String downloadUrl = "";
|
||||
|
||||
bool isUpgrading = false;
|
||||
|
||||
double? progress;
|
||||
|
||||
@override
|
||||
Future loadData() async {
|
||||
// get download url for gitlab release
|
||||
try {
|
||||
final r = await Api.getAppReleaseDataByVersionName(
|
||||
AppConf.networkVersionData!.lastVersion!);
|
||||
description = r["description"];
|
||||
final assetsLinks = List.of(r["assets"]?["links"] ?? []);
|
||||
for (var link in assetsLinks) {
|
||||
if (link["name"].toString().contains("SETUP.exe")) {
|
||||
downloadUrl = link["direct_asset_url"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
Navigator.pop(context!, false);
|
||||
}
|
||||
}
|
||||
|
||||
doUpgrade() async {
|
||||
isUpgrading = true;
|
||||
notifyListeners();
|
||||
final fileName = "${AppConf.getUpgradePath()}/next_SETUP.exe";
|
||||
try {
|
||||
await Dio().download(downloadUrl, fileName,
|
||||
onReceiveProgress: (int count, int total) {
|
||||
progress = (count / total) * 100;
|
||||
notifyListeners();
|
||||
});
|
||||
} catch (_) {
|
||||
isUpgrading = false;
|
||||
progress = null;
|
||||
showToast(context!, "下载失败,请尝试手动安装!");
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
try {
|
||||
final r =
|
||||
await (Process.run("powershell", ["start", fileName, "/SILENT"]));
|
||||
if (r.stderr.toString().isNotEmpty) {
|
||||
throw r.stderr;
|
||||
}
|
||||
exit(0);
|
||||
} catch (_) {
|
||||
isUpgrading = false;
|
||||
progress = null;
|
||||
showToast(context!, "运行失败,请尝试手动安装!");
|
||||
Process.run("powershell.exe", ["explorer.exe", "/select,\"$fileName\""]);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void doCancel() {
|
||||
Navigator.pop(context!, true);
|
||||
}
|
||||
}
|
60
lib/ui/tools/downloader/downloader_dialog_ui.dart
Normal file
60
lib/ui/tools/downloader/downloader_dialog_ui.dart
Normal file
@ -0,0 +1,60 @@
|
||||
import 'package:file_sizes/file_sizes.dart';
|
||||
import 'package:starcitizen_doctor/base/ui.dart';
|
||||
|
||||
import 'downloader_dialog_ui_model.dart';
|
||||
|
||||
class DownloaderDialogUI extends BaseUI<DownloaderDialogUIModel> {
|
||||
@override
|
||||
Widget? buildBody(BuildContext context, DownloaderDialogUIModel model) {
|
||||
return ContentDialog(
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .54),
|
||||
title: const Text("文件下载..."),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("文件名:${model.fileName}"),
|
||||
const SizedBox(height: 6),
|
||||
Text("保存位置:${model.savePath}"),
|
||||
const SizedBox(height: 6),
|
||||
Text("线程数:${model.threadCount}"),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"文件大小: ${FileSize.getSize(model.count ?? 0)} / ${FileSize.getSize(model.total ?? 0)}"),
|
||||
const SizedBox(height: 6),
|
||||
Text("下载速度: ${FileSize.getSize(model.speed?.toInt() ?? 0)}/s"),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Text(getStatus(model)),
|
||||
const SizedBox(width: 24),
|
||||
Expanded(
|
||||
child: ProgressBar(
|
||||
value: model.progress == 100 ? null : model.progress,
|
||||
)),
|
||||
const SizedBox(width: 24),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
|
||||
child: Text("取消下载"),
|
||||
),
|
||||
onPressed: () => model.doCancel()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String getUITitle(BuildContext context, DownloaderDialogUIModel model) => "";
|
||||
|
||||
String getStatus(DownloaderDialogUIModel model) {
|
||||
if (model.progress == null && !model.isInMerging) return "准备中...";
|
||||
if (model.isInMerging) return "正在合并文件...";
|
||||
return "${model.progress?.toStringAsFixed(2) ?? "0"}% ";
|
||||
}
|
||||
}
|
100
lib/ui/tools/downloader/downloader_dialog_ui_model.dart
Normal file
100
lib/ui/tools/downloader/downloader_dialog_ui_model.dart
Normal file
@ -0,0 +1,100 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:hyper_thread_downloader/hyper_thread_downloader.dart';
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
|
||||
class DownloaderDialogUIModel extends BaseUIModel {
|
||||
final String fileName;
|
||||
String savePath;
|
||||
final String downloadUrl;
|
||||
final bool showChangeSavePathDialog;
|
||||
final int threadCount;
|
||||
|
||||
DownloaderDialogUIModel(this.fileName, this.savePath, this.downloadUrl,
|
||||
{this.showChangeSavePathDialog = false, this.threadCount = 1});
|
||||
|
||||
final downloader = HyperDownload();
|
||||
|
||||
int? downloadTaskId;
|
||||
|
||||
bool isInMerging = false;
|
||||
|
||||
double? progress;
|
||||
double? speed;
|
||||
double? remainTime;
|
||||
int? count;
|
||||
int? total;
|
||||
|
||||
@override
|
||||
void initModel() {
|
||||
super.initModel();
|
||||
_initDownload();
|
||||
}
|
||||
|
||||
_initDownload() async {
|
||||
if (showChangeSavePathDialog) {
|
||||
final userSelect = await FilePicker.platform.saveFile(
|
||||
initialDirectory: savePath,
|
||||
fileName: fileName,
|
||||
lockParentWindow: true);
|
||||
if (userSelect == null) {
|
||||
Navigator.pop(context!);
|
||||
return;
|
||||
}
|
||||
final f = File(userSelect);
|
||||
if (await f.exists()) {
|
||||
await f.delete();
|
||||
}
|
||||
savePath = userSelect;
|
||||
dPrint(savePath);
|
||||
notifyListeners();
|
||||
} else {
|
||||
savePath = "$savePath/$fileName";
|
||||
}
|
||||
// start download
|
||||
downloader.startDownload(
|
||||
url: downloadUrl,
|
||||
savePath: savePath,
|
||||
threadCount: threadCount,
|
||||
prepareWorking: (bool done) {},
|
||||
workingMerge: (bool done) {
|
||||
isInMerging = true;
|
||||
progress = null;
|
||||
notifyListeners();
|
||||
},
|
||||
downloadProgress: ({
|
||||
required double progress,
|
||||
required double speed,
|
||||
required double remainTime,
|
||||
required int count,
|
||||
required int total,
|
||||
}) {
|
||||
this.progress = ((progress) * 100);
|
||||
this.speed = speed;
|
||||
this.remainTime = remainTime;
|
||||
this.count = count;
|
||||
this.total = total;
|
||||
notifyListeners();
|
||||
},
|
||||
downloadComplete: () {
|
||||
notifyListeners();
|
||||
Navigator.pop(context!, savePath);
|
||||
},
|
||||
downloadFailed: (String reason) {
|
||||
notifyListeners();
|
||||
showToast(context!, "下载失败! $reason");
|
||||
},
|
||||
downloadTaskId: (int id) {
|
||||
downloadTaskId = id;
|
||||
},
|
||||
downloadingLog: (String log) {});
|
||||
}
|
||||
|
||||
doCancel() {
|
||||
if (downloadTaskId != null) {
|
||||
downloader.stopDownload(id: downloadTaskId!);
|
||||
}
|
||||
Navigator.pop(context!, "cancel");
|
||||
}
|
||||
}
|
235
lib/ui/tools/tools_ui.dart
Normal file
235
lib/ui/tools/tools_ui.dart
Normal file
@ -0,0 +1,235 @@
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:starcitizen_doctor/base/ui.dart';
|
||||
|
||||
import 'tools_ui_model.dart';
|
||||
|
||||
class ToolsUI extends BaseUI<ToolsUIModel> {
|
||||
@override
|
||||
Widget? buildBody(BuildContext context, ToolsUIModel model) {
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, right: 22),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
makeGameLauncherPathSelect(context, model),
|
||||
const SizedBox(height: 12),
|
||||
makeGamePathSelect(context, model),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Button(
|
||||
onPressed: model.working ? null : model.loadData,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 32, bottom: 32, left: 12, right: 12),
|
||||
child: Icon(FluentIcons.refresh),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (model.items.isEmpty)
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 12),
|
||||
Text("正在扫描..."),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: AlignedGridView.count(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
itemCount: model.items.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final item = model.items[index];
|
||||
return Container(
|
||||
width: 300,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: FluentTheme.of(context).cardColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(.2),
|
||||
borderRadius:
|
||||
BorderRadius.circular(1000)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: item.icon,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.name,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
)),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.infoString,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(.6)),
|
||||
)),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Button(
|
||||
onPressed: model.working
|
||||
? null
|
||||
: item.onTap == null
|
||||
? null
|
||||
: () {
|
||||
try {
|
||||
item.onTap?.call();
|
||||
} catch (e) {
|
||||
showToast(
|
||||
context, "处理失败!:$e");
|
||||
}
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(FluentIcons.play),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (model.working)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withAlpha(150),
|
||||
),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 12),
|
||||
Text("正在处理..."),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeGamePathSelect(BuildContext context, ToolsUIModel model) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text("游戏安装位置: "),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: ComboBox<String>(
|
||||
value: model.scInstalledPath,
|
||||
items: [
|
||||
for (final path in model.scInstallPaths)
|
||||
ComboBoxItem(
|
||||
value: path,
|
||||
child: Text(path),
|
||||
)
|
||||
],
|
||||
onChanged: (v) {
|
||||
model.loadData(skipPathScan: true);
|
||||
model.scInstalledPath = v!;
|
||||
model.notifyListeners();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Button(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(FluentIcons.folder_open),
|
||||
),
|
||||
onPressed: () => model.openDir(model.scInstalledPath))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeGameLauncherPathSelect(BuildContext context, ToolsUIModel model) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text("RSI启动器位置:"),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: ComboBox<String>(
|
||||
value: model.rsiLauncherInstalledPath,
|
||||
items: [
|
||||
for (final path in model.rsiLauncherInstallPaths)
|
||||
ComboBoxItem(
|
||||
value: path,
|
||||
child: Text(path),
|
||||
)
|
||||
],
|
||||
onChanged: (v) {
|
||||
model.loadData(skipPathScan: true);
|
||||
model.rsiLauncherInstalledPath = v!;
|
||||
model.notifyListeners();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Button(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(FluentIcons.folder_open),
|
||||
),
|
||||
onPressed: () => model.openDir(model.rsiLauncherInstalledPath))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String getUITitle(BuildContext context, ToolsUIModel model) => "ToolsUI";
|
||||
}
|
334
lib/ui/tools/tools_ui_model.dart
Normal file
334
lib/ui/tools/tools_ui_model.dart
Normal file
@ -0,0 +1,334 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:starcitizen_doctor/base/ui_model.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
|
||||
import 'package:starcitizen_doctor/ui/tools/downloader/downloader_dialog_ui_model.dart';
|
||||
|
||||
import 'downloader/downloader_dialog_ui.dart';
|
||||
|
||||
class ToolsUIModel extends BaseUIModel {
|
||||
bool _working = false;
|
||||
|
||||
String scInstalledPath = "";
|
||||
String rsiLauncherInstalledPath = "";
|
||||
|
||||
List<String> scInstallPaths = [];
|
||||
List<String> rsiLauncherInstallPaths = [];
|
||||
|
||||
set working(bool b) {
|
||||
_working = b;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get working => _working;
|
||||
|
||||
var items = <_ToolsItemData>[];
|
||||
|
||||
@override
|
||||
Future loadData({bool skipPathScan = false}) async {
|
||||
items.clear();
|
||||
notifyListeners();
|
||||
|
||||
if (!skipPathScan) {
|
||||
await reScanPath();
|
||||
}
|
||||
try {
|
||||
final nvmePatchStatus = await SystemHelper.checkNvmePatchStatus();
|
||||
|
||||
final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath();
|
||||
|
||||
double logPathLen = 0;
|
||||
try {
|
||||
logPathLen =
|
||||
(await File(await SCLoggerHelper.getLogFilePath() ?? "").length()) /
|
||||
1024 /
|
||||
1024;
|
||||
} catch (_) {}
|
||||
|
||||
items = [
|
||||
_ToolsItemData(
|
||||
"systeminfo",
|
||||
"查看系统信息",
|
||||
"查看系统关键信息,用于快速问诊 \n\n耗时操作,请耐心等待。",
|
||||
const Icon(FluentIcons.system, size: 28),
|
||||
onTap: _showSystemInfo,
|
||||
),
|
||||
_ToolsItemData(
|
||||
"p4k_downloader",
|
||||
"P4K 分流下载",
|
||||
"使用星际公民中文百科提供的分流下载服务。 \n\n资源有限,请勿滥用。请确保您的硬盘拥有至少大于 200G 的可用空间。",
|
||||
const Icon(FontAwesomeIcons.download, size: 28),
|
||||
onTap: _downloadP4k,
|
||||
),
|
||||
_ToolsItemData(
|
||||
"reinstall_eac",
|
||||
"重装 EasyAntiCheat 反作弊",
|
||||
"若您遇到 EAC 错误,且自动修复无效,请尝试使用此功能重装 EAC。",
|
||||
const Icon(FluentIcons.game, size: 28),
|
||||
onTap: _reinstallEAC,
|
||||
),
|
||||
_ToolsItemData(
|
||||
"rsilauncher_admin_mode",
|
||||
"RSI Launcher 管理员模式",
|
||||
"在某些情况下 RSI启动器 无法正确获得管理员权限,您可尝试使用该功能以管理员模式运行启动器。",
|
||||
const Icon(FluentIcons.admin, size: 28),
|
||||
onTap: _adminRSILauncher,
|
||||
),
|
||||
_ToolsItemData(
|
||||
"clean_shaders",
|
||||
"清理着色器缓存",
|
||||
"若游戏画面出现异常或版本更新后可使用本工具清理过期的着色器(当大于500M时,建议清理) \n\n缓存大小:${((await SystemHelper.getDirLen(gameShaderCachePath ?? "", skipPath: [
|
||||
"$gameShaderCachePath\\Crashes"
|
||||
])) / 1024 / 1024).toStringAsFixed(4)} MB",
|
||||
const Icon(FontAwesomeIcons.shapes, size: 28),
|
||||
onTap: _cleanShaderCache,
|
||||
),
|
||||
_ToolsItemData(
|
||||
"rsilauncher_log_fix",
|
||||
"RSI Launcher Log 修复",
|
||||
"在某些情况下 RSI启动器 的 log 文件会损坏,导致无法完成问题扫描,使用此工具清理损坏的 log 文件。\n\n当前日志文件大小:${(logPathLen.toStringAsFixed(4))} MB",
|
||||
const Icon(FontAwesomeIcons.bookBible, size: 28),
|
||||
onTap: _rsiLogFix,
|
||||
),
|
||||
_ToolsItemData(
|
||||
"rsilauncher_log_select",
|
||||
"RSI Launcher Log 查看",
|
||||
"打开 RSI启动器 Log文件 所在文件夹",
|
||||
const Icon(FontAwesomeIcons.bookBible, size: 28),
|
||||
onTap: _selectLog,
|
||||
),
|
||||
_ToolsItemData(
|
||||
"remove_nvme_settings",
|
||||
"移除 nvme 注册表补丁",
|
||||
"若您使用 nvme 补丁出现问题,请运行此工具。(可能导致游戏 安装/更新 不可用。)\n\n当前补丁状态:${(nvmePatchStatus) ? "已安装" : "未安装"}",
|
||||
const Icon(FluentIcons.hard_drive, size: 28),
|
||||
onTap: nvmePatchStatus
|
||||
? () async {
|
||||
working = true;
|
||||
await SystemHelper.doRemoveNvmePath();
|
||||
working = false;
|
||||
showToast(context!, "已移除,重启生效!");
|
||||
loadData(skipPathScan: true);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
_ToolsItemData(
|
||||
"add_nvme_settings",
|
||||
"写入 nvme 注册表补丁",
|
||||
"手动写入NVM补丁,该功能仅在您知道自己在作什么的情况下使用",
|
||||
const Icon(FontAwesomeIcons.cashRegister, size: 28),
|
||||
onTap: () async {
|
||||
working = true;
|
||||
final r = await SystemHelper.addNvmePatch();
|
||||
if (r == "") {
|
||||
showToast(context!,
|
||||
"修复成功,请尝试重启后继续安装游戏! 若注册表修改操作导致其他软件出现兼容问题,请使用 工具 中的 NVME 注册表清理。");
|
||||
notifyListeners();
|
||||
} else {
|
||||
showToast(context!, "修复失败,$r");
|
||||
}
|
||||
working = false;
|
||||
loadData(skipPathScan: true);
|
||||
},
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
showToast(context!, "初始化失败,请截图报告给开发者。$e");
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> reScanPath() async {
|
||||
scInstallPaths.clear();
|
||||
rsiLauncherInstallPaths.clear();
|
||||
scInstalledPath = "";
|
||||
rsiLauncherInstalledPath = "";
|
||||
try {
|
||||
rsiLauncherInstalledPath = await SystemHelper.getRSILauncherPath();
|
||||
rsiLauncherInstallPaths.add(rsiLauncherInstalledPath);
|
||||
final listData = await SCLoggerHelper.getLauncherLogList();
|
||||
if (listData == null) {
|
||||
return;
|
||||
}
|
||||
scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData,
|
||||
checkExists: false, withVersion: ["LIVE", "PTU", "EPTU"]);
|
||||
if (scInstallPaths.isNotEmpty) {
|
||||
scInstalledPath = scInstallPaths.first;
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(context!, "解析 log 文件失败!\n请尝试使用 RSI Launcher log 修复 工具!");
|
||||
}
|
||||
notifyListeners();
|
||||
|
||||
if (rsiLauncherInstalledPath == "") {
|
||||
showToast(context!,
|
||||
"未找到 RSI 启动器,请尝试重新安装。 \n\n下载链接:https://robertsspaceindustries.com/download");
|
||||
}
|
||||
if (scInstalledPath == "") {
|
||||
showToast(context!, "未找到星际公民游戏安装位置,请至少完成一次游戏启动操作。");
|
||||
}
|
||||
}
|
||||
|
||||
/// 重装EAC
|
||||
Future<void> _reinstallEAC() async {
|
||||
if (scInstalledPath.isEmpty) {
|
||||
showToast(context!, "该功能需要一个有效的游戏安装目录");
|
||||
return;
|
||||
}
|
||||
working = true;
|
||||
try {
|
||||
final eacPath = "$scInstalledPath\\EasyAntiCheat";
|
||||
final eacJsonPath = "$eacPath\\Settings.json";
|
||||
if (await File(eacJsonPath).exists()) {
|
||||
Map<String, String> envVars = Platform.environment;
|
||||
final eacJsonData = await File(eacJsonPath).readAsString();
|
||||
final Map eacJson = json.decode(eacJsonData);
|
||||
final eacID = eacJson["productid"];
|
||||
if (eacID != null) {
|
||||
final eacCacheDir =
|
||||
Directory("${envVars["appdata"]}\\EasyAntiCheat\\$eacID");
|
||||
if (await eacCacheDir.exists()) {
|
||||
await eacCacheDir.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
final dir = Directory(eacPath);
|
||||
if (await dir.exists()) {
|
||||
await dir.delete(recursive: true);
|
||||
}
|
||||
final eacLauncher = File("$scInstalledPath\\StarCitizen_Launcher.exe");
|
||||
if (await eacLauncher.exists()) {
|
||||
await eacLauncher.delete(recursive: true);
|
||||
}
|
||||
showToast(context!,
|
||||
"已为您移除 EAC 文件,接下来将为您打开 RSI 启动器,请您前往 SETTINGS -> VERIFY 重装 EAC。");
|
||||
_adminRSILauncher();
|
||||
} catch (e) {
|
||||
showToast(context!, "出现错误:$e");
|
||||
}
|
||||
working = false;
|
||||
loadData(skipPathScan: true);
|
||||
}
|
||||
|
||||
Future<String> getSystemInfo() async {
|
||||
return "系统:${await SystemHelper.getSystemName()}\n\n"
|
||||
"处理器:${await SystemHelper.getSystemCimInstance("Win32_Processor")}\n\n"
|
||||
"内存大小:${await SystemHelper.getSystemMemorySizeGB()}GB\n\n"
|
||||
"显卡信息:\n${await SystemHelper.getGpuInfo()}\n\n"
|
||||
"硬盘信息:\n${await SystemHelper.getDiskInfo()}\n\n";
|
||||
}
|
||||
|
||||
/// 管理员模式运行 RSI 启动器
|
||||
Future _adminRSILauncher() async {
|
||||
if (rsiLauncherInstalledPath == "") {
|
||||
showToast(context!, "未找到 RSI 启动器目录,请您尝试手动操作。");
|
||||
}
|
||||
handleError(
|
||||
() => SystemHelper.checkAndLaunchRSILauncher(rsiLauncherInstalledPath));
|
||||
}
|
||||
|
||||
Future<void> _rsiLogFix() async {
|
||||
working = true;
|
||||
final path = await SCLoggerHelper.getLogFilePath();
|
||||
if (!await File(path!).exists()) {
|
||||
showToast(
|
||||
context!, "日志文件不存在,请尝试进行一次游戏启动或游戏安装,并退出启动器,若无法解决问题,请尝试将启动器更新至最新版本!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
SystemHelper.killRSILauncher();
|
||||
await File(path).delete(recursive: true);
|
||||
showToast(context!, "清理完毕,请完成一次安装 / 游戏启动 操作。");
|
||||
SystemHelper.checkAndLaunchRSILauncher(rsiLauncherInstalledPath);
|
||||
} catch (_) {
|
||||
showToast(context!, "清理失败,请手动移除,文件位置:$path");
|
||||
}
|
||||
working = false;
|
||||
}
|
||||
|
||||
Future<void> _selectLog() async {
|
||||
final path = await SCLoggerHelper.getLogFilePath();
|
||||
if (path == null) return;
|
||||
openDir(path);
|
||||
}
|
||||
|
||||
openDir(path) async {
|
||||
await Process.run("powershell.exe", ["explorer.exe", "/select,\"$path\""]);
|
||||
}
|
||||
|
||||
Future _showSystemInfo() async {
|
||||
working = true;
|
||||
final systemInfo = await getSystemInfo();
|
||||
showDialog<String>(
|
||||
context: context!,
|
||||
builder: (context) => ContentDialog(
|
||||
title: const Text('系统信息'),
|
||||
content: Text(systemInfo),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * .65,
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
|
||||
child: Text('关闭'),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
working = false;
|
||||
}
|
||||
|
||||
Future<void> _cleanShaderCache() async {
|
||||
working = true;
|
||||
final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath();
|
||||
final l =
|
||||
await Directory(gameShaderCachePath!).list(recursive: false).toList();
|
||||
for (var value in l) {
|
||||
if (value is Directory) {
|
||||
if (!value.absolute.path.contains("Crashes")) {
|
||||
await value.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
loadData(skipPathScan: true);
|
||||
working = false;
|
||||
}
|
||||
|
||||
Future<void> _downloadP4k() async {
|
||||
const downloadUrl = "https://r2test.citizenwiki.cn/Data.p4k";
|
||||
final r = await showDialog(
|
||||
context: context!,
|
||||
dismissWithEsc: false,
|
||||
builder: (context) {
|
||||
return BaseUIContainer(
|
||||
uiCreate: () => DownloaderDialogUI(),
|
||||
modelCreate: () => DownloaderDialogUIModel(
|
||||
"Data.p4k", scInstalledPath, downloadUrl,
|
||||
showChangeSavePathDialog: true, threadCount: 10));
|
||||
});
|
||||
if (r != null) {
|
||||
if (r == "cancel") {
|
||||
return showToast(context!, "下载已取消,下载进度已保留,如果您无需恢复下载,请手动删除下载临时文件。");
|
||||
}
|
||||
showToast(context!, "下载完毕,文件已保存到:$r");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ToolsItemData {
|
||||
String key;
|
||||
|
||||
_ToolsItemData(this.key, this.name, this.infoString, this.icon, {this.onTap});
|
||||
|
||||
String name;
|
||||
String infoString;
|
||||
Widget icon;
|
||||
AsyncCallback? onTap;
|
||||
}
|
Reference in New Issue
Block a user