feat:riverpod 迁移

This commit is contained in:
2024-03-07 23:01:32 +08:00
parent c6b69c4e08
commit 8e37ebece5
66 changed files with 1126 additions and 10749 deletions

View File

@ -1,92 +0,0 @@
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/widgets/countdown_time_text.dart';
import 'countdown_dialog_ui_model.dart';
class CountdownDialogUI extends BaseUI<CountdownDialogUIModel> {
@override
Widget? buildBody(BuildContext context, CountdownDialogUIModel model) {
return ContentDialog(
constraints:
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .65),
title: Row(
children: [
IconButton(
icon: const Icon(
FluentIcons.back,
size: 22,
),
onPressed: model.onBack),
const SizedBox(width: 12),
const Text("节日倒计时"),
],
),
content: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(left: 12, right: 12),
child: Column(
children: [
AlignedGridView.count(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
itemCount: model.countdownFestivalListData.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final item = model.countdownFestivalListData[index];
return Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
if (item.icon != null && item.icon != "") ...[
ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: Image.asset(
"assets/countdown/${item.icon}",
width: 38,
height: 38,
),
),
const SizedBox(width: 12),
] else
const SizedBox(width: 50),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${item.name}",
),
CountdownTimeText(
targetTime: DateTime.fromMillisecondsSinceEpoch(
item.time ?? 0),
)
],
)
],
),
),
);
},
),
const SizedBox(height: 12),
Text(
"* 以上节日日期由人工收录、维护,可能存在错误,欢迎反馈!",
style: TextStyle(
fontSize: 13, color: Colors.white.withOpacity(.3)),
)
],
),
),
),
);
}
@override
String getUITitle(BuildContext context, CountdownDialogUIModel model) => "";
}

View File

@ -1,12 +0,0 @@
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/data/countdown_festival_item_data.dart';
class CountdownDialogUIModel extends BaseUIModel {
final List<CountdownFestivalItemData> countdownFestivalListData;
CountdownDialogUIModel(this.countdownFestivalListData);
onBack() {
Navigator.pop(context!);
}
}

View File

@ -1,43 +0,0 @@
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,
crossAxisAlignment: CrossAxisAlignment.start,
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;
}

View File

@ -1,19 +0,0 @@
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
class MDContentDialogUIModel extends BaseUIModel {
String title;
String url;
MDContentDialogUIModel(this.title, this.url);
String? data;
@override
Future loadData() async {
final r = await handleError(() => RSHttp.getText(url));
if (r == null) return;
data = r;
notifyListeners();
}
}

View File

@ -1,247 +0,0 @@
import 'package:file_sizes/file_sizes.dart';
import 'package:intl/intl.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/io/aria2c.dart';
import 'downloader_ui_model.dart';
class DownloaderUI extends BaseUI<DownloaderUIModel> {
final DateFormat formatter = DateFormat('yyyy-MM-dd HH:mm:ss');
@override
Widget? buildBody(BuildContext context, DownloaderUIModel model) {
return makeDefaultPage(context, model,
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Row(
children: [
const Spacer(),
const SizedBox(width: 24),
const SizedBox(width: 12),
for (final item in <MapEntry<String, IconData>, String>{
const MapEntry("settings", FluentIcons.settings): "限速设置",
if (model.tasks.isNotEmpty)
const MapEntry("pause_all", FluentIcons.pause): "全部暂停",
if (model.waitingTasks.isNotEmpty)
const MapEntry("resume_all", FluentIcons.download): "恢复全部",
if (model.tasks.isNotEmpty || model.waitingTasks.isNotEmpty)
const MapEntry("cancel_all", FluentIcons.cancel): "全部取消",
}.entries)
Padding(
padding: const EdgeInsets.only(left: 6, right: 6),
child: Button(
child: Padding(
padding: const EdgeInsets.all(4),
child: Row(
children: [
Icon(item.key.value),
const SizedBox(width: 6),
Text(item.value),
],
),
),
onPressed: () => model.onTapButton(item.key.key)),
),
const SizedBox(width: 12),
],
),
if (model.getTasksLen() == 0)
const Expanded(
child: Center(
child: Text("无下载任务"),
))
else
Expanded(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
final (task, type, isFirstType) = model.getTaskAndType(index);
final nt = DownloaderUIModel.getTaskTypeAndName(task);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isFirstType)
Column(
children: [
Container(
padding: EdgeInsets.only(
left: 24,
right: 24,
top: index == 0 ? 0 : 12,
bottom: 12),
margin: const EdgeInsets.only(top: 6, bottom: 6),
child: Row(
children: [
Expanded(
child: Row(
children: [
Text(
"${model.listHeaderStatusMap[type]}",
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold),
),
],
)),
],
),
),
],
),
Container(
padding: const EdgeInsets.only(
left: 12, right: 12, top: 12, bottom: 12),
margin: const EdgeInsets.only(
left: 12, right: 12, top: 6, bottom: 6),
decoration: BoxDecoration(
color: FluentTheme.of(context)
.cardColor
.withOpacity(.06),
borderRadius: BorderRadius.circular(7),
),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
nt.value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
Row(
children: [
Text(
"总大小:${FileSize.getSize(task.totalLength ?? 0)}",
style: const TextStyle(fontSize: 14),
),
const SizedBox(width: 12),
if (nt.key == "torrent" &&
task.verifiedLength != null &&
task.verifiedLength != 0)
Text(
"校验中...${FileSize.getSize(task.verifiedLength)}",
style: const TextStyle(fontSize: 14),
)
else if (task.status == "active")
Text(
"下载中... (${((task.completedLength ?? 0) * 100 / (task.totalLength ?? 1)).toStringAsFixed(4)}%)")
else
Text(
"状态:${model.statusMap[task.status]}",
style: const TextStyle(fontSize: 14),
),
const SizedBox(width: 24),
if (task.status == "active" &&
task.verifiedLength == null)
Text(
"ETA: ${formatter.format(DateTime.now().add(Duration(seconds: model.getETA(task))))}"),
],
),
],
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"已上传:${FileSize.getSize(task.uploadLength)}"),
Text(
"已下载:${FileSize.getSize(task.completedLength)}"),
],
),
const SizedBox(width: 18),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"↑:${FileSize.getSize(task.uploadSpeed)}/s"),
Text(
"↓:${FileSize.getSize(task.downloadSpeed)}/s"),
],
),
const SizedBox(width: 32),
if (type != "stopped")
DropDownButton(
closeAfterClick: false,
title: const Padding(
padding: EdgeInsets.all(3),
child: Text('选项'),
),
items: [
if (task.status == "paused")
MenuFlyoutItem(
leading:
const Icon(FluentIcons.download),
text: const Text('继续下载'),
onPressed: () =>
model.resumeTask(task.gid))
else if (task.status == "active")
MenuFlyoutItem(
leading: const Icon(FluentIcons.pause),
text: const Text('暂停下载'),
onPressed: () =>
model.pauseTask(task.gid)),
const MenuFlyoutSeparator(),
MenuFlyoutItem(
leading: const Icon(
FluentIcons.chrome_close,
size: 14,
),
text: const Text('取消下载'),
onPressed: () =>
model.cancelTask(task.gid)),
MenuFlyoutItem(
leading: const Icon(
FluentIcons.folder_open,
size: 14,
),
text: const Text('打开文件夹'),
onPressed: () => model.openFolder(task)),
],
),
const SizedBox(width: 12),
],
),
),
],
);
},
itemCount: model.getTasksLen(),
)),
Container(
color: FluentTheme.of(context).cardColor.withOpacity(.06),
child: Padding(
padding: const EdgeInsets.only(left: 12, bottom: 3, top: 3),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Aria2cManager.isAvailable
? Colors.green
: Colors.white,
borderRadius: BorderRadius.circular(1000),
),
),
const SizedBox(width: 12),
Text(
"下载: ${FileSize.getSize(model.globalStat?.downloadSpeed ?? 0)}/s 上传:${FileSize.getSize(model.globalStat?.uploadSpeed ?? 0)}/s",
style: const TextStyle(fontSize: 12),
)
],
),
),
),
],
));
}
@override
String getUITitle(BuildContext context, DownloaderUIModel model) => "下载管理";
}

View File

@ -1,251 +0,0 @@
import 'dart:io';
import 'package:aria2/aria2.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/io/aria2c.dart';
class DownloaderUIModel extends BaseUIModel {
List<Aria2Task> tasks = [];
List<Aria2Task> waitingTasks = [];
List<Aria2Task> stoppedTasks = [];
Aria2GlobalStat? globalStat;
final statusMap = {
"active": "下载中...",
"waiting": "等待中",
"paused": "已暂停",
"error": "下载失败",
"complete": "下载完成",
"removed": "已删除",
};
final listHeaderStatusMap = {
"active": "下载中",
"waiting": "等待中",
"stopped": "已结束",
};
@override
initModel() {
super.initModel();
_listenDownloader();
}
onTapButton(String key) async {
switch (key) {
case "pause_all":
if (!Aria2cManager.isAvailable) return;
await Aria2cManager.getClient().pauseAll();
await Aria2cManager.getClient().saveSession();
return;
case "resume_all":
if (!Aria2cManager.isAvailable) return;
await Aria2cManager.getClient().unpauseAll();
await Aria2cManager.getClient().saveSession();
return;
case "cancel_all":
final userOK = await showConfirmDialogs(
context!, "确认取消全部任务?", const Text("如果文件不再需要,你可能需要手动删除下载文件。"));
if (userOK == true) {
if (!Aria2cManager.isAvailable) return;
try {
for (var value in [...tasks, ...waitingTasks]) {
await Aria2cManager.getClient().remove(value.gid!);
}
await Aria2cManager.getClient().saveSession();
} catch (e) {
dPrint("DownloadsUIModel cancel_all Error: $e");
}
}
return;
case "settings":
_showDownloadSpeedSettings();
return;
}
}
_listenDownloader() async {
try {
while (true) {
if (!mounted) return;
if (Aria2cManager.isAvailable) {
final aria2c = Aria2cManager.getClient();
tasks.clear();
tasks = await aria2c.tellActive();
waitingTasks = await aria2c.tellWaiting(0, 1000000);
stoppedTasks = await aria2c.tellStopped(0, 1000000);
globalStat = await aria2c.getGlobalStat();
notifyListeners();
}
await Future.delayed(const Duration(seconds: 1));
}
} catch (e) {
dPrint("[DownloadsUIModel]._listenDownloader Error: $e");
}
}
int getTasksLen() {
return tasks.length + waitingTasks.length + stoppedTasks.length;
}
(Aria2Task, String, bool) getTaskAndType(int index) {
final tempList = <Aria2Task>[...tasks, ...waitingTasks, ...stoppedTasks];
if (index >= 0 && index < tasks.length) {
return (tempList[index], "active", index == 0);
}
if (index >= tasks.length && index < tasks.length + waitingTasks.length) {
return (tempList[index], "waiting", index == tasks.length);
}
if (index >= tasks.length + waitingTasks.length &&
index < tempList.length) {
return (
tempList[index],
"stopped",
index == tasks.length + waitingTasks.length
);
}
throw Exception("Index out of range or element is null");
}
static MapEntry<String, String> getTaskTypeAndName(Aria2Task task) {
if (task.bittorrent == null) {
String uri = task.files?[0]['uris'][0]['uri'] as String;
return MapEntry("url", uri.split('/').last);
} else if (task.bittorrent != null) {
if (task.bittorrent!.containsKey('info')) {
var btName = task.bittorrent?["info"]["name"];
return MapEntry("torrent", btName ?? 'torrent');
} else {
return MapEntry("magnet", '[METADATA]${task.infoHash}');
}
} else {
return const MapEntry("metaLink", '==========metaLink============');
}
}
List<Aria2File> getFilesFormTask(Aria2Task task) {
List<Aria2File> l = [];
if (task.files != null) {
for (var element in task.files!) {
final f = Aria2File.fromJson(element);
l.add(f);
}
}
return l;
}
int getETA(Aria2Task task) {
if (task.downloadSpeed == null || task.downloadSpeed == 0) return 0;
final remainingBytes =
(task.totalLength ?? 0) - (task.completedLength ?? 0);
return remainingBytes ~/ (task.downloadSpeed!);
}
Future<void> resumeTask(String? gid) async {
final aria2c = Aria2cManager.getClient();
if (gid != null) {
await aria2c.unpause(gid);
}
}
Future<void> pauseTask(String? gid) async {
final aria2c = Aria2cManager.getClient();
if (gid != null) {
await aria2c.pause(gid);
}
}
Future<void> cancelTask(String? gid) async {
await Future.delayed(const Duration(milliseconds: 300));
if (gid != null) {
final ok = await showConfirmDialogs(
context!, "确认取消下载?", const Text("如果文件不再需要,你可能需要手动删除下载文件。"));
if (ok == true) {
final aria2c = Aria2cManager.getClient();
await aria2c.remove(gid);
await Aria2cManager.getClient().saveSession();
}
}
}
openFolder(Aria2Task task) {
final f = getFilesFormTask(task).firstOrNull;
if (f != null) {
SystemHelper.openDir(File(f.path!).absolute.path.replaceAll("/", "\\"));
}
}
Future<void> _showDownloadSpeedSettings() async {
final box = await Hive.openBox("app_conf");
final upCtrl = TextEditingController(
text: box.get("downloader_up_limit", defaultValue: ""));
final downCtrl = TextEditingController(
text: box.get("downloader_down_limit", defaultValue: ""));
final ifr = FilteringTextInputFormatter.allow(RegExp(r'^\d*[km]?$'));
final ok = await showConfirmDialogs(
context!,
"限速设置",
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"SC 汉化盒子使用 p2p 网络来加速文件下载,如果您流量有限,可在此处将上传带宽设置为 1(byte)。",
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(.6),
),
),
const SizedBox(height: 24),
const Text("请输入下载单位1、100k、10m 0或留空为不限速。"),
const SizedBox(height: 12),
const Text("上传限速:"),
const SizedBox(height: 6),
TextFormBox(
placeholder: "1、100k、10m、0",
controller: upCtrl,
placeholderStyle: TextStyle(color: Colors.white.withOpacity(.6)),
inputFormatters: [ifr],
),
const SizedBox(height: 12),
const Text("下载限速:"),
const SizedBox(height: 6),
TextFormBox(
placeholder: "1、100k、10m、0",
controller: downCtrl,
placeholderStyle: TextStyle(color: Colors.white.withOpacity(.6)),
inputFormatters: [ifr],
),
const SizedBox(height: 24),
Text(
"* P2P 上传仅在下载文件时进行,下载完成后会关闭 p2p 连接。如您想参与做种,请通过关于页面联系我们。",
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(.6),
),
)
],
));
if (ok == true) {
await handleError(() => Aria2cManager.launchDaemon());
final aria2c = Aria2cManager.getClient();
final upByte = Aria2cManager.textToByte(upCtrl.text.trim());
final downByte = Aria2cManager.textToByte(downCtrl.text.trim());
final r = await handleError(() => aria2c.changeGlobalOption(Aria2Option()
..maxOverallUploadLimit = upByte
..maxOverallDownloadLimit = downByte));
if (r != null) {
await box.put('downloader_up_limit', upCtrl.text.trim());
await box.put('downloader_down_limit', downCtrl.text.trim());
notifyListeners();
}
}
}
}

View File

@ -1,255 +0,0 @@
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'game_doctor_ui_model.dart';
class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
@override
Widget? buildBody(BuildContext context, GameDoctorUIModel model) {
return makeDefaultPage(context, model,
content: Stack(
children: [
Column(
children: [
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
for (final item in const {
"rsi_log": "RSI启动器log",
"game_log": "游戏运行log",
}.entries)
Padding(
padding: const EdgeInsets.only(left: 6, right: 6),
child: Button(
child: Padding(
padding: const EdgeInsets.all(4),
child: Row(
children: [
const Icon(FluentIcons.folder_open),
const SizedBox(width: 6),
Text(item.value),
],
),
),
onPressed: () => model.onTapButton(item.key)),
),
],
),
if (model.isChecking)
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ProgressRing(),
const SizedBox(height: 12),
Text(model.lastScreenInfo)
],
),
))
else if (model.checkResult == null ||
model.checkResult!.isEmpty) ...[
const Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 12),
Text("扫描完毕,没有找到问题!", maxLines: 1),
SizedBox(height: 64),
],
),
))
] else
...makeResult(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
: "正在处理..."),
],
),
),
),
Positioned(
bottom: 20,
right: 20,
child: makeRescueBanner(context),
)
],
));
}
List<Widget> makeResult(BuildContext context, GameDoctorUIModel model) {
return [
const SizedBox(height: 24),
Text(model.lastScreenInfo, maxLines: 1),
const SizedBox(height: 12),
Text(
"注意:本工具检测结果仅供参考,若您不理解以下操作,请提供截图给有经验的玩家!",
style: TextStyle(color: Colors.red, fontSize: 16),
),
const SizedBox(height: 24),
ListView.builder(
itemCount: model.checkResult!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final item = model.checkResult![index];
return makeResultItem(item, model);
},
),
const SizedBox(height: 64),
];
}
Widget makeRescueBanner(BuildContext context) {
return GestureDetector(
onTap: () async {
await showToast(context,
"您即将前往由 深空治疗中心QQ群号536454632 提供的游戏异常救援服务,主要解决游戏安装失败与频繁闪退,如游戏玩法问题,请勿加群。");
launchUrlString(
"https://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=-M4wEme_bCXbUGT4LFKLH0bAYTFt70Ad&authKey=vHVr0TNgRmKu%2BHwywoJV6EiLa7La2VX74Vkyixr05KA0H9TqB6qWlCdY%2B9jLQ4Ha&noverify=0&group_code=536454632");
},
child: Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .2),
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset("assets/rescue.png", width: 24, height: 24),
const SizedBox(width: 12),
const Text("需要帮助? 点击加群寻求免费人工支援!"),
],
),
)),
),
);
}
Widget makeResultItem(
MapEntry<String, String> item, GameDoctorUIModel 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 未安装,请点击修复为您一键安装。(在游戏正常启动并结束前,该问题会一直出现,若您因为其他原因游戏闪退,可忽略此条目)"),
"cn_user_name":
const MapEntry("中文用户名!", "中文用户名可能会导致游戏启动/安装错误! 点击修复按钮查看修改教程!"),
"cn_install_path": MapEntry("中文安装路径!",
"中文安装路径!这可能会导致游戏 启动/安装 错误!(${item.value}请在RSI启动器更换安装路径。"),
"low_ram": MapEntry(
"物理内存过低", "您至少需要 16GB 的物理内存Memory才可运行此游戏。当前大小${item.value}"),
};
bool isCheckedError = errorNames.containsKey(item.key);
if (isCheckedError) {
return Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
),
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(
errorNames[item.key]?.key ?? "",
style: const TextStyle(fontSize: 18),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4, bottom: 4),
child: Column(
children: [
const SizedBox(height: 4),
Text(
"修复建议: ${errorNames[item.key]?.value ?? "暂无解决方法,请截图反馈"}",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.7)),
),
],
),
),
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, top: 4, bottom: 4),
child: Text("修复"),
),
),
),
);
}
final isSubTitleUrl = item.value.startsWith("https://");
return Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
),
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(
item.key,
style: const TextStyle(fontSize: 18),
),
subtitle: isSubTitleUrl
? null
: Column(
children: [
const SizedBox(height: 4),
Text(
item.value,
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.7)),
),
],
),
trailing: isSubTitleUrl
? Button(
onPressed: () {
launchUrlString(item.value);
},
child: const Padding(
padding:
EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
child: Text("查看解决方案"),
),
)
: null,
),
);
}
@override
String getUITitle(BuildContext context, GameDoctorUIModel model) =>
"一键诊断 > ${model.scInstalledPath}";
}

View File

@ -1,260 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
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:url_launcher/url_launcher_string.dart';
class GameDoctorUIModel extends BaseUIModel {
String scInstalledPath = "";
GameDoctorUIModel(this.scInstalledPath);
String _lastScreenInfo = "";
String get lastScreenInfo => _lastScreenInfo;
List<MapEntry<String, String>>? checkResult;
set lastScreenInfo(String info) {
_lastScreenInfo = info;
notifyListeners();
}
bool isChecking = false;
bool isFixing = false;
String isFixingString = "";
final cnExp = RegExp(r"[^\x00-\xff]");
@override
void initModel() {
doCheck()?.call();
super.initModel();
}
VoidCallback? doCheck() {
if (isChecking) return null;
return () async {
isChecking = true;
lastScreenInfo = "正在分析...";
await _statCheck();
isChecking = false;
notifyListeners();
};
}
Future _statCheck() async {
checkResult = [];
// TODO for debug
// checkResult?.add(const MapEntry("unSupport_system", "android"));
// checkResult?.add(const MapEntry("nvme_PhysicalBytes", "C"));
// checkResult?.add(const MapEntry("no_live_path", ""));
await _checkPreInstall();
await _checkEAC();
await _checkGameRunningLog();
if (checkResult!.isEmpty) {
checkResult = null;
lastScreenInfo = "分析完毕,没有发现问题";
} else {
lastScreenInfo = "分析完毕,发现 ${checkResult!.length} 个问题";
}
if (scInstalledPath == "not_install" && (checkResult?.isEmpty ?? true)) {
showToast(context!, "扫描完毕,没有发现问题,若仍然安装失败,请尝试使用工具箱中的 RSI启动器管理员模式。");
}
}
Future _checkGameRunningLog() async {
if (scInstalledPath == "not_install") return;
lastScreenInfo = "正在检查Game.log";
final logs = await SCLoggerHelper.getGameRunningLogs(scInstalledPath);
if (logs == null) return;
final info = SCLoggerHelper.getGameRunningLogInfo(logs);
if (info != null) {
if (info.key != "_") {
checkResult?.add(MapEntry("游戏异常退出:${info.key}", info.value));
} else {
checkResult
?.add(MapEntry("游戏异常退出:未知异常", "info:${info.value},请点击右下角加群反馈。"));
}
}
}
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\\anticheatlauncher.log";
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(
"fsutil info sector info: ->>> ${result.stdout.toString().trim()}");
if (result.stderr == "") {
final rs = result.stdout.toString().trim();
final physicalBytesPerSectorForPerformance = (int.tryParse(rs) ?? 0);
if (physicalBytesPerSectorForPerformance > 4096) {
checkResult?.add(MapEntry("nvme_PhysicalBytes", element));
}
}
}
} catch (e) {
dPrint(e);
}
}
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;
}
}
onTapButton(String key) async {
switch (key) {
case "rsi_log":
final path = await SCLoggerHelper.getLogFilePath();
if (path == null) return;
SystemHelper.openDir(path);
return;
case "game_log":
if (scInstalledPath == "not_install") {
showToast(context!, "请在首页选择游戏安装目录。");
return;
}
SystemHelper.openDir("$scInstalledPath\\Game.log");
return;
}
}
}

View File

@ -1,727 +0,0 @@
import 'package:card_swiper/card_swiper.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/widgets/cache_image.dart';
import 'package:starcitizen_doctor/widgets/countdown_time_text.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(
padding: EdgeInsets.zero,
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: 6),
],
...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) {
const double width = 280;
return [
Stack(
children: [
Padding(
padding: const EdgeInsets.only(top: 64, bottom: 0),
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Image.asset(
"assets/sc_logo.png",
fit: BoxFit.fitHeight,
height: 260,
),
),
makeGameStatusCard(context, model, 340)
],
),
),
),
),
Positioned(
top: 0,
left: 24,
child: makeLeftColumn(context, model, width),
),
Positioned(
right: 24,
top: 0,
child: makeNewsCard(context, model),
),
],
),
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),
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: model.isRsiLauncherStarting
? const ProgressRing()
: Button(
onPressed: model.appWebLocalizationVersionsData == null
? null
: () => model.launchRSI(),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
model.isCurGameRunning
? FluentIcons.stop_solid
: FluentIcons.play,
color: model.isCurGameRunning
? Colors.red.withOpacity(.8)
: null,
),
)),
),
const SizedBox(width: 12),
Button(
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.folder_open),
),
onPressed: () => model.openDir(model.scInstalledPath)),
const SizedBox(width: 12),
Button(
onPressed: model.reScanPath,
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.refresh),
),
),
],
),
),
const SizedBox(height: 8),
Text(model.lastScreenInfo, maxLines: 1),
makeIndexActionLists(context, model),
];
}
Widget makeLeftColumn(BuildContext context, HomeUIModel model, double width) {
return Stack(
children: [
Column(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: FluentTheme.of(context).cardColor.withOpacity(.03),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
makeWebViewButton(model,
icon: SvgPicture.asset(
"assets/rsi.svg",
colorFilter: makeSvgColor(Colors.white),
height: 18,
),
name: "星际公民官网汉化",
webTitle: "星际公民官网汉化",
webURL: "https://robertsspaceindustries.com",
info: "罗伯茨航天工业公司,万物的起源",
useLocalization: true,
width: width,
touchKey: "webLocalization_rsi"),
const SizedBox(height: 12),
makeWebViewButton(model,
icon: Row(
children: [
SvgPicture.asset(
"assets/uex.svg",
height: 18,
),
const SizedBox(width: 12),
],
),
name: "UEX 汉化",
webTitle: "UEX 汉化",
webURL: "https://uexcorp.space/",
info: "采矿、精炼、贸易计算器、价格、船信息",
useLocalization: true,
width: width,
touchKey: "webLocalization_uex"),
const SizedBox(height: 12),
makeWebViewButton(model,
icon: Row(
children: [
Image.asset(
"assets/dps.png",
height: 20,
),
const SizedBox(width: 12),
],
),
name: "DPS计算器汉化",
webTitle: "DPS计算器汉化",
webURL: "https://www.erkul.games/live/calculator",
info: "在线改船,查询伤害数值和配件购买地点",
useLocalization: true,
width: width,
touchKey: "webLocalization_dps"),
const SizedBox(height: 12),
const Text("外部浏览器拓展:"),
const SizedBox(height: 12),
Row(
children: [
Button(
child:
const FaIcon(FontAwesomeIcons.chrome, size: 18),
onPressed: () {
launchUrlString(
"https://chrome.google.com/webstore/detail/gocnjckojmledijgmadmacoikibcggja?authuser=0&hl=zh-CN");
},
),
const SizedBox(width: 12),
Button(
child: const FaIcon(FontAwesomeIcons.edge, size: 18),
onPressed: () {
launchUrlString(
"https://microsoftedge.microsoft.com/addons/detail/lipbbcckldklpdcpfagicipecaacikgi");
},
),
const SizedBox(width: 12),
Button(
child: const FaIcon(FontAwesomeIcons.firefoxBrowser,
size: 18),
onPressed: () {
launchUrlString(
"https://addons.mozilla.org/zh-CN/firefox/"
"addon/%E6%98%9F%E9%99%85%E5%85%AC%E6%B0%91%E7%9B%92%E5%AD%90%E6%B5%8F%E8%A7%88%E5%99%A8%E6%8B%93%E5%B1%95/");
},
),
const SizedBox(width: 12),
Button(
child:
const FaIcon(FontAwesomeIcons.github, size: 18),
onPressed: () {
launchUrlString(
"https://github.com/StarCitizenToolBox/StarCitizenBoxBrowserEx");
},
),
],
)
],
),
),
),
const SizedBox(height: 16),
makeActivityBanner(context, model, width),
],
),
if (model.appWebLocalizationVersionsData == null)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(.3),
borderRadius: BorderRadius.circular(12)),
child: const Center(
child: ProgressRing(),
),
))
],
);
}
Widget makeNewsCard(BuildContext context, HomeUIModel model) {
return ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: Container(
width: 316,
height: 386,
decoration: BoxDecoration(
color: Colors.white.withOpacity(.1),
borderRadius: BorderRadius.circular(12)),
child: SingleChildScrollView(
child: Column(
children: [
SizedBox(
height: 190,
width: 316,
child: Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
child: model.rssVideoItems == null
? Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(.1)),
child: makeLoading(context),
)
: Swiper(
itemCount: model.rssVideoItems?.length ?? 0,
itemBuilder: (context, index) {
final item = model.rssVideoItems![index];
return GestureDetector(
onTap: () {
if (item.link != null) {
launchUrlString(item.link!);
}
},
child: CacheNetImage(
url: model.getRssImage(item),
fit: BoxFit.cover,
),
);
},
autoplay: true,
),
)),
const SizedBox(height: 1),
if (model.rssTextItems == null)
makeLoading(context)
else
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final item = model.rssTextItems![index];
return Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: BorderRadius.circular(12),
child: GestureDetector(
onTap: () {
if (item.link != null) {
launchUrlString(item.link!);
}
},
child: Padding(
padding: const EdgeInsets.only(
left: 12, right: 12, top: 4, bottom: 4),
child: Row(
children: [
getRssIcon(item.link ?? ""),
const SizedBox(width: 6),
Expanded(
child: Text(
"${model.handleTitle(item.title)}",
textAlign: TextAlign.start,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12.2),
),
),
const SizedBox(width: 12),
Icon(
FluentIcons.chevron_right,
size: 12,
color: Colors.white.withOpacity(.4),
)
],
),
),
));
},
itemCount: model.rssTextItems?.length,
),
const SizedBox(height: 12),
],
),
)),
);
}
Widget getRssIcon(String url) {
if (url.startsWith("https://tieba.baidu.com")) {
return SvgPicture.asset("assets/tieba.svg", width: 14, height: 14);
}
if (url.startsWith("https://www.bilibili.com")) {
return const FaIcon(
FontAwesomeIcons.bilibili,
size: 14,
color: Color.fromRGBO(0, 161, 214, 1),
);
}
return const FaIcon(FontAwesomeIcons.rss, size: 14);
}
Widget makeIndexActionLists(BuildContext context, HomeUIModel model) {
final items = [
_HomeItemData("auto_check", "一键诊断", "一键诊断星际公民常见问题",
FluentIcons.auto_deploy_settings),
_HomeItemData(
"localization", "汉化管理", "快捷安装汉化资源", FluentIcons.locale_language),
_HomeItemData("performance", "性能优化", "调整引擎配置文件,优化游戏性能",
FluentIcons.process_meta_task),
];
return Padding(
padding: const EdgeInsets.all(24),
child: AlignedGridView.count(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
itemCount: items.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final item = items.elementAt(index);
return HoverButton(
onPressed: () => model.onMenuTap(item.key),
builder: (BuildContext context, Set<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),
],
)),
if (item.key == "localization" &&
model.localizationUpdateInfo != null)
Container(
padding: const EdgeInsets.only(
top: 3, bottom: 3, left: 8, right: 8),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12)),
child:
Text(model.localizationUpdateInfo?.key ?? " "),
),
const SizedBox(width: 12),
const Icon(
FluentIcons.chevron_right,
size: 16,
)
],
),
),
);
},
);
}),
);
}
@override
String getUITitle(BuildContext context, HomeUIModel model) => "HOME";
Widget makeWebViewButton(HomeUIModel model,
{required Widget icon,
required String name,
required String webTitle,
required String webURL,
required bool useLocalization,
required double width,
String? info,
String? touchKey}) {
return Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: BorderRadius.circular(12),
child: GestureDetector(
onTap: () {
if (touchKey != null) {
AnalyticsApi.touch(touchKey);
}
model.goWebView(webTitle, webURL, useLocalization: true);
},
child: Container(
width: width,
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
icon,
Text(
name,
style: const TextStyle(fontSize: 14),
),
],
),
if (info != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
info,
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(.6)),
),
)
],
),
),
const SizedBox(width: 12),
Icon(
FluentIcons.chevron_right,
size: 14,
color: Colors.white.withOpacity(.6),
)
],
),
),
),
),
);
}
Widget makeGameStatusCard(
BuildContext context, HomeUIModel model, double width) {
return Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .2),
borderRadius: BorderRadius.circular(12),
child: GestureDetector(
onTap: () {
model.goWebView(
"RSI 服务器状态", "https://status.robertsspaceindustries.com/",
useLocalization: true);
},
child: Container(
width: width,
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(children: [
if (model.scServerStatus == null)
makeLoading(context, width: 20)
else
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("状态:"),
for (final item in model.scServerStatus ?? [])
Row(
children: [
SizedBox(
height: 14,
child: Center(
child: Icon(
FontAwesomeIcons.solidCircle,
color: model.isRSIServerStatusOK(item)
? Colors.green
: Colors.red,
size: 12,
),
),
),
const SizedBox(width: 5),
Text(
"${model.statusCnName[item["name"]] ?? item["name"]}",
style: const TextStyle(fontSize: 13),
),
],
),
Icon(
FluentIcons.chevron_right,
size: 12,
color: Colors.white.withOpacity(.4),
)
],
)
]),
),
),
),
);
}
Widget makeActivityBanner(
BuildContext context, HomeUIModel model, double width) {
return Tilt(
borderRadius: BorderRadius.circular(12),
shadowConfig: const ShadowConfig(disable: true),
child: GestureDetector(
onTap: () => model.onTapFestival(),
child: Container(
width: width + 24,
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor),
child: Padding(
padding:
const EdgeInsets.only(left: 12, right: 12, top: 8, bottom: 8),
child: (model.countdownFestivalListData == null)
? SizedBox(
width: width,
height: 62,
child: const Center(
child: ProgressRing(),
),
)
: SizedBox(
width: width,
height: 62,
child: Swiper(
itemCount: model.countdownFestivalListData!.length,
autoplay: true,
autoplayDelay: 5000,
itemBuilder: (context, index) {
final item = model.countdownFestivalListData![index];
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (item.icon != null && item.icon != "") ...[
ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: Image.asset(
"assets/countdown/${item.icon}",
width: 48,
height: 48,
fit: BoxFit.cover,
),
),
],
Column(
children: [
Text(
item.name ?? "",
style: const TextStyle(fontSize: 15),
),
const SizedBox(height: 3),
CountdownTimeText(
targetTime:
DateTime.fromMillisecondsSinceEpoch(
item.time ?? 0),
),
],
),
const SizedBox(width: 12),
Icon(
FluentIcons.chevron_right,
size: 14,
color: Colors.white.withOpacity(.6),
)
],
);
},
),
),
)),
),
);
}
}
class _HomeItemData {
String key;
_HomeItemData(this.key, this.name, this.infoString, this.icon);
String name;
String infoString;
IconData icon;
}

View File

@ -1,481 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dart_rss/dart_rss.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:hive/hive.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/api/rss.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/data/app_placard_data.dart';
import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart';
import 'package:starcitizen_doctor/data/countdown_festival_item_data.dart';
import 'package:starcitizen_doctor/ui/home/countdown/countdown_dialog_ui_model.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/login/login_dialog_ui.dart';
import 'package:starcitizen_doctor/ui/home/login/login_dialog_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 'package:html/parser.dart' as html;
import 'package:html/dom.dart' as html_dom;
import 'package:windows_ui/windows_ui.dart';
import 'countdown/countdown_dialog_ui.dart';
import 'game_doctor/game_doctor_ui.dart';
import 'game_doctor/game_doctor_ui_model.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 = "";
bool isFixing = false;
String isFixingString = "";
final Map<String, bool> _isGameRunning = {};
bool get isCurGameRunning => _isGameRunning[scInstalledPath] ?? false;
List<RssItem>? rssVideoItems;
List<RssItem>? rssTextItems;
AppWebLocalizationVersionsData? appWebLocalizationVersionsData;
List<CountdownFestivalItemData>? countdownFestivalListData;
MapEntry<String, bool>? localizationUpdateInfo;
bool _isSendLocalizationUpdateNotification = false;
AppPlacardData? appPlacardData;
List? scServerStatus;
Timer? serverUpdateTimer;
Timer? appUpdateTimer;
final statusCnName = const {
"Platform": "平台",
"Persistent Universe": "持续宇宙",
"Electronic Access": "电子访问",
"Arena Commander": "竞技场指挥官"
};
bool isRsiLauncherStarting = false;
@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) {
if (r.alwaysShow != true && version == r.version) {
} else {
appPlacardData = r;
}
}
updateSCServerStatus();
notifyListeners();
appWebLocalizationVersionsData = AppWebLocalizationVersionsData.fromJson(
json.decode((await RSHttp.getText(
"${URLConf.webTranslateHomeUrl}/versions.json"))));
countdownFestivalListData = await Api.getFestivalCountdownList();
notifyListeners();
_loadRRS();
} catch (e) {
dPrint(e);
}
// check Localization update
_checkLocalizationUpdate();
notifyListeners();
}
@override
void initModel() {
reScanPath();
serverUpdateTimer = Timer.periodic(
const Duration(minutes: 10),
(timer) {
updateSCServerStatus();
},
);
appUpdateTimer = Timer.periodic(const Duration(minutes: 30), (timer) {
_checkLocalizationUpdate();
});
super.initModel();
}
@override
void dispose() {
serverUpdateTimer?.cancel();
serverUpdateTimer = null;
appUpdateTimer?.cancel();
appUpdateTimer = null;
super.dispose();
}
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 文件失败!";
AnalyticsApi.touch("error_launchLogs");
showToast(context!,
"解析 log 文件失败! \n请关闭游戏退出RSI启动器后重试若仍有问题请使用工具箱中的 RSI Launcher log 修复。");
}
}
updateSCServerStatus() async {
try {
final s = await Api.getScServerStatus();
dPrint("updateSCServerStatus===$s");
scServerStatus = s;
notifyListeners();
} catch (e) {
dPrint(e);
}
}
Future _loadRRS() async {
try {
final v = await RSSApi.getRssVideo();
rssVideoItems = v;
notifyListeners();
final t = await RSSApi.getRssText();
rssTextItems = t;
notifyListeners();
dPrint("RSS update Success !");
} catch (e) {
dPrint("_loadRRS Error:$e");
}
}
openDir(rsiLauncherInstalledPath) async {
await Process.run(SystemHelper.powershellPath,
["explorer.exe", "/select,\"$rsiLauncherInstalledPath\""]);
}
onMenuTap(String key) async {
const String gameInstallReqInfo =
"该功能需要一个有效的安装位置\n\n如果您的游戏未下载完成,请等待下载完毕后使用此功能。\n\n如果您的游戏已下载完毕但未识别,请启动一次游戏后重新打开盒子 或 在设置选项中手动设置安装位置。";
switch (key) {
case "auto_check":
BaseUIContainer(
uiCreate: () => GameDoctorUI(),
modelCreate: () => GameDoctorUIModel(scInstalledPath))
.push(context!);
return;
case "localization":
if (scInstalledPath == "not_install") {
showToast(context!, gameInstallReqInfo);
return;
}
await showDialog(
context: context!,
dismissWithEsc: false,
builder: (BuildContext context) {
return BaseUIContainer(
uiCreate: () => LocalizationUI(),
modelCreate: () => LocalizationUIModel(scInstalledPath));
});
_checkLocalizationUpdate();
return;
case "performance":
if (scInstalledPath == "not_install") {
showToast(context!, gameInstallReqInfo);
return;
}
AnalyticsApi.touch("performance_launch");
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,
bool loginMode = false,
RsiLoginCallback? rsiLoginCallback}) async {
if (useLocalization) {
const tipVersion = 2;
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(
"本插功能件仅供大致浏览使用,不对任何有关本功能产生的问题负责!在涉及账号操作前请注意确认网站的原本内容!"
"\n\n\n使用此功能登录账号时请确保您的 SC汉化盒子 是从可信任的来源下载。",
style: TextStyle(fontSize: 16),
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .6));
if (!ok) {
if (loginMode) {
rsiLoginCallback?.call(null, false);
}
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!,
loginMode: loginMode, loginCallback: rsiLoginCallback);
if (useLocalization) {
isFixingString = "正在初始化汉化资源...";
isFixing = true;
notifyListeners();
try {
await webViewModel.initLocalization(appWebLocalizationVersionsData!);
} 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 Future.delayed(const Duration(milliseconds: 500));
await webViewModel.launch(url);
notifyListeners();
}
launchRSI() async {
if (scInstalledPath == "not_install") {
showToast(context!, "该功能需要一个有效的安装位置");
return;
}
if (AppConf.isMSE) {
if (isCurGameRunning) {
await Process.run(
SystemHelper.powershellPath, ["ps \"StarCitizen\" | kill"]);
return;
}
AnalyticsApi.touch("gameLaunch");
showDialog(
context: context!,
dismissWithEsc: false,
builder: (context) {
return BaseUIContainer(
uiCreate: () => LoginDialog(),
modelCreate: () => LoginDialogModel(scInstalledPath, this));
});
} else {
final ok = await showConfirmDialogs(
context!,
"一键启动功能提示",
const Text("为确保账户安全,一键启动功能已在开发版中禁用,我们将在微软商店版本中提供此功能。"
"\n\n微软商店版由微软提供可靠的分发下载与数字签名,可有效防止软件被恶意篡改。\n\n提示:您无需使用盒子启动游戏也可使用汉化。"),
confirm: "安装微软商店版本",
cancel: "取消");
if (ok == true) {
await launchUrlString(
"https://apps.microsoft.com/detail/9NF3SWFWNKL1?launch=true");
await Future.delayed(const Duration(seconds: 2));
exit(0);
}
}
}
bool isRSIServerStatusOK(Map map) {
return (map["status"] == "ok" || map["status"] == "operational");
}
doLaunchGame(String launchExe, List<String> args, String installPath,
String? processorAffinity) async {
_isGameRunning[installPath] = true;
notifyListeners();
try {
late ProcessResult result;
if (processorAffinity == null) {
result = await Process.run(launchExe, args);
} else {
dPrint("set Affinity === $processorAffinity launchExe === $launchExe");
result = await Process.run("cmd.exe", [
'/C',
'Start',
'"StarCitizen"',
'/High',
'/Affinity',
processorAffinity,
launchExe,
...args
]);
}
dPrint('Exit code: ${result.exitCode}');
dPrint('stdout: ${result.stdout}');
dPrint('stderr: ${result.stderr}');
if (result.exitCode != 0) {
final logs = await SCLoggerHelper.getGameRunningLogs(scInstalledPath);
MapEntry<String, String>? exitInfo;
bool hasUrl = false;
if (logs != null) {
exitInfo = SCLoggerHelper.getGameRunningLogInfo(logs);
if (exitInfo!.value.startsWith("https://")) {
hasUrl = true;
}
}
showToast(context!,
"游戏非正常退出\nexitCode=${result.exitCode}\nstdout=${result.stdout ?? ""}\nstderr=${result.stderr ?? ""}\n\n诊断信息:${exitInfo == null ? "未知错误,请通过一键诊断加群反馈。" : exitInfo.key} \n${hasUrl ? "请查看弹出的网页链接获得详细信息。" : exitInfo?.value ?? ""}");
if (hasUrl) {
await Future.delayed(const Duration(seconds: 3));
launchUrlString(exitInfo!.value);
}
}
final launchFile = File("$installPath\\loginData.json");
if (await launchFile.exists()) {
await launchFile.delete();
}
} catch (_) {}
_isGameRunning[installPath] = false;
notifyListeners();
}
onTapFestival() {
if (countdownFestivalListData == null) return;
showDialog(
context: context!,
builder: (context) {
return BaseUIContainer(
uiCreate: () => CountdownDialogUI(),
modelCreate: () =>
CountdownDialogUIModel(countdownFestivalListData!));
});
}
getRssImage(RssItem item) {
final h = html.parse(item.description ?? "");
if (h.body == null) return "";
for (var node in h.body!.nodes) {
if (node is html_dom.Element) {
if (node.localName == "img") {
return node.attributes["src"]?.trim() ?? "";
}
}
}
return "";
}
handleTitle(String? title) {
if (title == null) return "";
title = title.replaceAll("", "[ ");
title = title.replaceAll("", " ] ");
return title;
}
Future<void> _checkLocalizationUpdate() async {
final info = await handleError(
() => LocalizationUIModel.checkLocalizationUpdates(scInstallPaths));
dPrint("lUpdateInfo === $info");
localizationUpdateInfo = info;
notifyListeners();
if (info?.value == true && !_isSendLocalizationUpdateNotification) {
final toastNotifier =
ToastNotificationManager.createToastNotifierWithId("SC汉化盒子");
if (toastNotifier != null) {
final toastContent = ToastNotificationManager.getTemplateContent(
ToastTemplateType.toastText02);
if (toastContent != null) {
final xmlNodeList = toastContent.getElementsByTagName('text');
const title = '汉化有新版本!';
final content = '您在 ${info?.key} 安装的汉化有新版本啦!';
xmlNodeList.item(0)?.appendChild(toastContent.createTextNode(title));
xmlNodeList
.item(1)
?.appendChild(toastContent.createTextNode(content));
final toastNotification =
ToastNotification.createToastNotification(toastContent);
toastNotifier.show(toastNotification);
_isSendLocalizationUpdateNotification = true;
}
}
}
}
}

View File

@ -1,419 +0,0 @@
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) {
final curInstallInfo = model.apiLocalizationData?[model.patchStatus?.value];
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 != "游戏内置")
Row(
children: [
Button(
onPressed: model.goFeedback,
child: const Padding(
padding: EdgeInsets.all(4),
child: Row(
children: [
Icon(FluentIcons.feedback),
SizedBox(width: 6),
Text("汉化反馈"),
],
),
)),
const SizedBox(width: 16),
Button(
onPressed: model.doDelIniFile(),
child: const Padding(
padding: EdgeInsets.all(4),
child: Row(
children: [
Icon(FluentIcons.delete),
SizedBox(width: 6),
Text("卸载汉化"),
],
),
)),
],
),
],
),
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: (curInstallInfo != null &&
curInstallInfo.note != null &&
curInstallInfo.note!.isNotEmpty)
? Padding(
padding: const EdgeInsets.only(top: 12),
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(7)),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"备注:",
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
Text(
"${curInstallInfo.note}",
style: TextStyle(
color: Colors.white.withOpacity(.8)),
)
],
),
),
),
)
: SizedBox(
width: MediaQuery.of(context).size.width,
),
),
],
]),
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),
]),
const SizedBox(height: 12),
IconButton(
icon: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(model.enableCustomize
? FluentIcons.chevron_up
: FluentIcons.chevron_down),
const SizedBox(width: 12),
const Text("高级功能"),
],
),
onPressed: () {
model.enableCustomize = !model.enableCustomize;
model.notifyListeners();
}),
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: Column(
children: [
const SizedBox(height: 12),
model.enableCustomize
? makeListContainer("自定义文本", [
if (model.customizeList == null)
makeLoading(context)
else if (model.customizeList!.isEmpty)
Center(
child: Text(
"暂无自定义文本",
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: Row(
children: [
Icon(FluentIcons.folder_open),
SizedBox(width: 6),
Text("打开文件夹"),
],
),
)),
])
: SizedBox(
width: MediaQuery.of(context).size.width,
)
],
),
),
],
),
),
),
);
}
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.gameChannel}",
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: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 6),
child: Icon(isInstalled
? FluentIcons.check_mark
: (item.value.enable ?? false)
? FluentIcons.download
: FluentIcons.disable_updates),
),
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) => "汉化管理";
}

View File

@ -1,394 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/data/sc_localization_data.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:starcitizen_doctor/common/utils/log.dart' as log_utils;
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;
bool enableCustomize = false;
@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();
_checkUserCfg();
_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.gameChannel == "PTU") {
apiLocalizationData![element.versionName ?? ""] = element;
} else if (!isPTU && element.gameChannel == "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(
"${scDataDir.absolute.path}\\Localization\\$selectedLanguage\\global.ini"));
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 {
AnalyticsApi.touch("install_localization");
final downloadUrl =
"${URLConf.gitlabLocalizationUrl}/archive/${value.versionName}.tar.gz";
final savePath =
File("${downloadDir.absolute.path}\\${value.versionName}.sclang");
try {
workingVersion = value.versionName!;
notifyListeners();
if (!await savePath.exists()) {
// download
dPrint("downloading file to $savePath");
final r = await RSHttp.get(downloadUrl);
if (r.statusCode == 200 && r.data != null) {
await savePath.writeAsBytes(r.data!);
} else {
throw "statusCode Error : ${r.statusCode}";
}
} 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") &&
str.contains("g_languageAudio=english");
}
static Future<String> getInstalledIniVersion(String iniPath) async {
final iniFile = File(iniPath);
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(SystemHelper.powershellPath,
["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";
} else if (value.contains("g_languageAudio")) {
value = "g_language=english";
}
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");
}
if (!newStr.toString().contains("g_languageAudio")) {
newStr.writeln("g_languageAudio=english");
}
} 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() {
return () async {
final iniFile = File(
"${scDataDir.absolute.path}\\Localization\\$selectedLanguage\\global.ini");
if (await iniFile.exists()) await iniFile.delete();
await updateLangCfg(false);
await _updateStatus();
};
}
/// read locale active
static StringBuffer _readArchive(String savePath) {
final inputStream = InputFileStream(savePath);
final archive =
TarDecoder().decodeBytes(GZipDecoder().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();
};
}
void _checkUserCfg() async {
final userCfgFile = File("$scInstallPath\\USER.cfg");
if (await userCfgFile.exists()) {
final cfgString = await userCfgFile.readAsString();
if (cfgString.contains("g_language") &&
!cfgString.contains("g_language=$selectedLanguage")) {
final ok = await showConfirmDialogs(
context!,
"是否移除不兼容的汉化参数",
const Text(
"USER.cfg 包含不兼容的汉化参数,这可能是以前的汉化文件的残留信息。\n\n这将可能导致汉化无效或乱码,点击确认为您一键移除(不会影响其他配置)。"),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .35));
if (ok == true) {
var finalString = "";
for (var item in cfgString.split("\n")) {
if (!item.trim().startsWith("g_language")) {
finalString = "$finalString$item\n";
}
}
await userCfgFile.delete();
await userCfgFile.create();
await userCfgFile.writeAsString(finalString, flush: true);
reloadData();
}
}
}
}
static Future<MapEntry<String, bool>?> checkLocalizationUpdates(
List<String> gameInstallPaths) async {
final updateInfo = <String, bool>{};
for (var kv in languageSupport.entries) {
final l = await Api.getScLocalizationData(kv.key);
for (var value in gameInstallPaths) {
final iniPath = "$value\\data\\Localization\\${kv.key}\\global.ini";
if (!await File(iniPath).exists()) {
continue;
}
final installed = await getInstalledIniVersion(iniPath);
if (installed == "游戏内置" || installed == "自定义文件") {
continue;
}
final hasUpdate = l
.where((element) => element.versionName == installed)
.firstOrNull ==
null;
updateInfo[value] = hasUpdate;
}
}
log_utils.dPrint("checkLocalizationUpdates ==== $updateInfo");
for (var v in updateInfo.entries) {
if (v.value) {
for (var element in AppConf.gameChannels) {
if (v.key.contains("StarCitizen\\$element")) {
return MapEntry(element, true);
} else {
return const MapEntry("", true);
}
}
}
}
return null;
}
void goFeedback() {
launchUrlString(URLConf.feedbackUrl);
}
}

View File

@ -1,115 +0,0 @@
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/widgets/cache_image.dart';
import 'login_dialog_ui_model.dart';
class LoginDialog extends BaseUI<LoginDialogModel> {
@override
Widget? buildBody(BuildContext context, LoginDialogModel model) {
return ContentDialog(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .56,
),
title: (model.loginStatus == 2) ? null : const Text("一键启动"),
content: AnimatedSize(
duration: const Duration(milliseconds: 230),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Row(),
if (model.loginStatus == 0) ...[
Center(
child: Column(
children: [
const Text("登录中..."),
const SizedBox(height: 12),
const ProgressRing(),
if (model.isDeviceSupportWinHello)
const SizedBox(height: 24),
Text(
"* 若开启了自动填充,请留意弹出的 Windows Hello 窗口",
style: TextStyle(
fontSize: 13, color: Colors.white.withOpacity(.6)),
)
],
),
),
] else if (model.loginStatus == 1) ...[
Text("请输入RSI账户 [${model.nickname}] 的邮箱,以保存登录状态(输入错误会导致无法进入游戏!)"),
const SizedBox(height: 12),
TextFormBox(
// controller: model.emailCtrl,
),
const SizedBox(height: 6),
Text(
"*该操作同一账号只需执行一次,输入错误请在盒子设置中清理,切换账号请在汉化浏览器中操作。",
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(.6),
),
)
] else if (model.loginStatus == 2 || model.loginStatus == 3) ...[
Center(
child: Column(
children: [
const SizedBox(height: 12),
const Text(
"欢迎回来!",
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 24),
if (model.avatarUrl != null)
ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: CacheNetImage(
url: model.avatarUrl!,
width: 128,
height: 128,
fit: BoxFit.fill,
),
),
const SizedBox(height: 12),
Text(
model.nickname,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 32),
Text(model.loginStatus == 2
? "正在为您启动游戏..."
: "正在等待优化CPU参数..."),
const SizedBox(height: 12),
const ProgressRing(),
],
),
)
]
],
),
),
actions: const [
// if (model.loginStatus == 1) ...[
// Button(
// child: const Padding(
// padding: EdgeInsets.all(4),
// child: Text("取消"),
// ),
// onPressed: () {
// Navigator.pop(context);
// }),
// const SizedBox(width: 80),
// FilledButton(
// child: const Padding(
// padding: EdgeInsets.all(4),
// child: Text("保存"),
// ),
// onPressed: () => model.onSaveEmail()),
// ],
],
);
}
@override
String getUITitle(BuildContext context, LoginDialogModel model) => "";
}

View File

@ -1,273 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:cryptography/cryptography.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:hive/hive.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'package:local_auth/local_auth.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/win32/credentials.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:uuid/uuid.dart';
class LoginDialogModel extends BaseUIModel {
int loginStatus = 0;
String nickname = "";
String? avatarUrl;
String? authToken;
String? webToken;
Map? releaseInfo;
final String installPath;
final HomeUIModel homeUIModel;
// TextEditingController emailCtrl = TextEditingController();
LoginDialogModel(this.installPath, this.homeUIModel);
final LocalAuthentication localAuth = LocalAuthentication();
var isDeviceSupportWinHello = false;
@override
void initModel() {
_launchWebLogin();
super.initModel();
}
Future<void> _launchWebLogin() async {
isDeviceSupportWinHello = await localAuth.isDeviceSupported();
notifyListeners();
goWebView("登录 RSI 账户", "https://robertsspaceindustries.com/connect",
loginMode: true, rsiLoginCallback: (message, ok) async {
// dPrint(
// "======rsiLoginCallback=== $ok ===== data==\n${json.encode(message)}");
if (message == null || !ok) {
Navigator.pop(context!);
return;
}
// final emailBox = await Hive.openBox("quick_login_email");
final data = message["data"];
authToken = data["authToken"];
webToken = data["webToken"];
releaseInfo = data["releaseInfo"];
avatarUrl = data["avatar"]
?.toString()
.replaceAll("url(\"", "")
.replaceAll("\")", "");
Map<String, dynamic> payload = Jwt.parseJwt(authToken!);
nickname = payload["nickname"] ?? "";
final inputEmail = data["inputEmail"];
final inputPassword = data["inputPassword"];
final userBox = await Hive.openBox("rsi_account_data");
if (inputEmail != null && inputEmail != "") {
await userBox.put("account_email", inputEmail);
}
if (isDeviceSupportWinHello) {
if (await userBox.get("enable", defaultValue: true)) {
if (inputEmail != null &&
inputEmail != "" &&
inputPassword != null &&
inputPassword != "") {
final ok = await showConfirmDialogs(
context!,
"是否开启自动密码填充?",
const Text(
"盒子将使用 PIN 与 Windows 凭据加密保存您的密码,密码只存储在您的设备中。\n\n当下次登录需要输入密码时您只需授权PIN即可自动填充登录。"));
if (ok == true) {
if (await localAuth.authenticate(localizedReason: "输入PIN以启用加密") ==
true) {
await _savePwd(inputEmail, inputPassword);
}
} else {
await userBox.put("enable", false);
}
}
}
}
final buildInfoFile = File("$installPath\\build_manifest.id");
if (await buildInfoFile.exists()) {
final buildInfo =
json.decode(await buildInfoFile.readAsString())["Data"];
dPrint("buildInfo ======= $buildInfo");
if (releaseInfo?["versionLabel"] != null &&
buildInfo["RequestedP4ChangeNum"] != null) {
if (!(releaseInfo!["versionLabel"]!
.toString()
.endsWith(buildInfo["RequestedP4ChangeNum"]!.toString()))) {
final ok = await showConfirmDialogs(
context!,
"游戏版本过期",
Text(
"RSI 服务器报告版本号:${releaseInfo?["versionLabel"]} \n\n本地版本号:${buildInfo["RequestedP4ChangeNum"]} \n\n建议使用 RSI Launcher 更新游戏!"),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .4),
cancel: "忽略");
if (ok == true) {
Navigator.pop(context!);
return;
}
}
}
}
_readyForLaunch();
}, useLocalization: true);
}
goWebView(String title, String url,
{bool useLocalization = false,
bool loginMode = false,
RsiLoginCallback? rsiLoginCallback}) async {
if (useLocalization) {
const tipVersion = 2;
final box = await Hive.openBox("app_conf");
final skip = await box.get("skip_web_login_version", defaultValue: 0);
if (skip != tipVersion) {
final ok = await showConfirmDialogs(
context!,
"盒子一键启动",
const Text(
"本功能可以帮您更加便利的启动游戏。\n\n为确保账户安全 ,本功能使用汉化浏览器保留登录状态,且不会保存您的密码信息(除非你启用了自动填充功能)。"
"\n\n使用此功能登录账号时请确保您的 SC汉化盒子 是从可信任的来源下载。",
style: TextStyle(fontSize: 16),
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .6));
if (!ok) {
if (loginMode) {
rsiLoginCallback?.call(null, false);
}
return;
}
await box.put("skip_web_login_version", tipVersion);
}
}
if (!await WebviewWindow.isWebviewAvailable()) {
await showToast(context!, "需要安装 WebView2 Runtime");
await launchUrlString(
"https://developer.microsoft.com/en-us/microsoft-edge/webview2/");
Navigator.pop(context!);
return;
}
final webViewModel = WebViewModel(context!,
loginMode: loginMode,
loginCallback: rsiLoginCallback,
loginChannel: getChannelID());
if (useLocalization) {
try {
await webViewModel
.initLocalization(homeUIModel.appWebLocalizationVersionsData!);
} catch (_) {}
}
await Future.delayed(const Duration(milliseconds: 500));
await webViewModel.initWebView(
title: title,
);
await webViewModel.launch(url);
notifyListeners();
}
// onSaveEmail() async {
// final RegExp emailRegex = RegExp(r'^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$');
// if (!emailRegex.hasMatch(emailCtrl.text.trim())) {
// showToast(context!, "邮箱输入有误!");
// return;
// }
// final emailBox = await Hive.openBox("quick_login_email");
// await emailBox.put(nickname, emailCtrl.text.trim());
// _readyForLaunch();
// notifyListeners();
// }
Future<void> _readyForLaunch() async {
final userBox = await Hive.openBox("rsi_account_data");
loginStatus = 2;
notifyListeners();
final launchData = {
"username": userBox.get("account_email", defaultValue: ""),
"token": webToken,
"auth_token": authToken,
"star_network": {
"services_endpoint": releaseInfo?["servicesEndpoint"],
"hostname": releaseInfo?["universeHost"],
"port": releaseInfo?["universePort"],
},
"TMid": const Uuid().v4(),
};
final executable = releaseInfo?["executable"];
final launchOptions = releaseInfo?["launchOptions"];
dPrint("----------launch data ====== -----------\n$launchData");
dPrint(
"----------executable data ====== -----------\n$installPath\\$executable $launchOptions");
final launchFile = File("$installPath\\loginData.json");
if (await launchFile.exists()) {
await launchFile.delete();
}
await launchFile.create();
await launchFile.writeAsString(json.encode(launchData));
notifyListeners();
await Future.delayed(const Duration(seconds: 1));
await Future.delayed(const Duration(seconds: 3));
final processorAffinity = await SystemHelper.getCpuAffinity();
homeUIModel.doLaunchGame(
'$installPath\\$executable',
["-no_login_dialog", ...launchOptions.toString().split(" ")],
installPath,
processorAffinity);
await Future.delayed(const Duration(seconds: 1));
Navigator.pop(context!);
}
String getChannelID() {
if (installPath.endsWith("\\LIVE")) {
return "LIVE";
} else if (installPath.endsWith("\\PTU")) {
return "PTU";
} else if (installPath.endsWith("\\EPTU")) {
return "EPTU";
}
return "LIVE";
}
_savePwd(String inputEmail, String inputPassword) async {
final algorithm = AesGcm.with256bits();
final secretKey = await algorithm.newSecretKey();
final nonce = algorithm.newNonce();
final secretBox = await algorithm.encrypt(utf8.encode(inputPassword),
secretKey: secretKey, nonce: nonce);
await algorithm.decrypt(
SecretBox(secretBox.cipherText,
nonce: secretBox.nonce, mac: secretBox.mac),
secretKey: secretKey);
final pwdEncrypted = base64.encode(secretBox.cipherText);
final userBox = await Hive.openBox("rsi_account_data");
await userBox.put("account_email", inputEmail);
await userBox.put("account_pwd_encrypted", pwdEncrypted);
await userBox.put("nonce", base64.encode(secretBox.nonce));
await userBox.put("mac", base64.encode(secretBox.mac.bytes));
final secretKeyStr = base64.encode((await secretKey.extractBytes()));
Win32Credentials.write(
credentialName: "SCToolbox_RSI_Account_secret",
userName: inputEmail,
password: secretKeyStr);
}
}

View File

@ -1,268 +0,0 @@
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) {
final model = ref.watch(provider);
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(() {});
},
)
],
)
else if (item.type == "customize")
TextFormBox(
maxLines: 10,
placeholder:
"您可以在这里输入未收录进盒子的自定义参数。配置示例:\n\nr_displayinfo=0\nr_VSync=0",
controller: model.customizeCtrl,
),
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),
if (item.type != "customize")
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}";
}

View File

@ -1,211 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:starcitizen_doctor/api/analytics.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);
TextEditingController customizeCtrl = TextEditingController();
Map<String?, List<GamePerformanceData>>? performanceMap;
List<String> inAppKeys = [];
String workingString = "";
late final confFile = File("$scPath\\USER.cfg");
bool enabled = false;
bool showGraphicsPerformanceTip = false;
static const _graphicsPerformanceTipVersion = 1;
@override
Future loadData() async {
customizeCtrl.clear();
inAppKeys.clear();
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);
if (item.key != "customize") {
inAppKeys.add(item.key ?? "");
}
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;
AnalyticsApi.touch("performance_apply");
workingString = "生成配置文件";
notifyListeners();
String conf = "";
for (var v in performanceMap!.entries) {
for (var c in v.value) {
if (c.key != "customize") {
conf = "$conf${c.key}=${c.value}\n";
}
}
}
if (customizeCtrl.text.trim().isNotEmpty) {
final lines = customizeCtrl.text.split("\n");
for (var value in lines) {
final sp = value.split("=");
// 忽略无效的配置文件
if (sp.length == 2) {
conf = "$conf${sp[0].trim()}=${sp[1].trim()}\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;
}
}
}
if (kv.length == 2 && !inAppKeys.contains(kv[0].trim())) {
customizeCtrl.text =
"${customizeCtrl.text}${kv[0].trim()}=${kv[1].trim()}\n";
}
}
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();
}
}

View File

@ -1,318 +0,0 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:local_auth/local_auth.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/win32/credentials.dart';
import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart';
import '../../../base/ui.dart';
typedef RsiLoginCallback = void Function(Map? data, bool success);
class WebViewModel {
late Webview webview;
final BuildContext context;
bool _isClosed = false;
bool get isClosed => _isClosed;
WebViewModel(this.context,
{this.loginMode = false, this.loginCallback, this.loginChannel = "LIVE"});
String url = "";
bool canGoBack = false;
final localizationResource = <String, dynamic>{};
var localizationScript = "";
bool enableCapture = false;
bool isEnableToolSiteMirrors = false;
Map<String, String>? _curReplaceWords;
Map<String, String>? get curReplaceWords => _curReplaceWords;
final bool loginMode;
final String loginChannel;
bool _loginModeSuccess = false;
final RsiLoginCallback? loginCallback;
initWebView({String title = ""}) async {
try {
final userBox = await Hive.openBox("app_conf");
isEnableToolSiteMirrors =
userBox.get("isEnableToolSiteMirrors", defaultValue: false);
webview = await WebviewWindow.create(
configuration: CreateConfiguration(
windowWidth: loginMode ? 960 : 1920,
windowHeight: loginMode ? 720 : 1080,
userDataFolderWindows:
"${AppConf.applicationSupportDir}/webview_data",
title: title));
// webview.openDevToolsWindow();
webview.isNavigating.addListener(() async {
if (!webview.isNavigating.value && localizationResource.isNotEmpty) {
dPrint("webview Navigating url === $url");
if (url.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";
const spectrum =
"https://robertsspaceindustries.com/spectrum/community/";
// 跳过光谱论坛 https://github.com/StarCitizenToolBox/StarCitizenBoxBrowserEx/issues/1
if (url.startsWith(spectrum)) {
return;
}
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.clear();
replaceWords.addAll(_getLocalizationResource("concierge"));
}
if (url.startsWith(hangar)) {
replaceWords.addAll(_getLocalizationResource("hangar"));
}
_curReplaceWords = {};
for (var element in replaceWords) {
_curReplaceWords?[element["word"] ?? ""] =
element["replacement"] ?? "";
}
await Future.delayed(const Duration(milliseconds: 100));
await webview.evaluateJavaScript(
"WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)");
/// loginMode
if (loginMode) {
dPrint(
"--- do rsi login ---\n run === getRSILauncherToken(\"$loginChannel\");");
await Future.delayed(const Duration(milliseconds: 200));
webview.evaluateJavaScript(
"getRSILauncherToken(\"$loginChannel\");");
}
} else if (url
.startsWith(await _handleMirrorsUrl("https://www.erkul.games"))) {
dPrint("load script");
await Future.delayed(const Duration(milliseconds: 100));
await webview.evaluateJavaScript(localizationScript);
dPrint("update replaceWords");
final replaceWords = _getLocalizationResource("DPS");
await webview.evaluateJavaScript(
"WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)");
} else if (url
.startsWith(await _handleMirrorsUrl("https://uexcorp.space"))) {
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(dispose);
if (loginMode) {
webview.addOnWebMessageReceivedCallback((messageString) {
final message = json.decode(messageString);
if (message["action"] == "webview_rsi_login_show_window") {
webview.setWebviewWindowVisibility(true);
_checkAutoLogin(webview);
} else if (message["action"] == "webview_rsi_login_success") {
_loginModeSuccess = true;
loginCallback?.call(message, true);
webview.close();
}
});
Future.delayed(const Duration(seconds: 1))
.then((value) => {webview.setWebviewWindowVisibility(false)});
}
} catch (e) {
showToast(context, "初始化失败:$e");
}
}
Future<String> _handleMirrorsUrl(String url) async {
var finalUrl = url;
if (isEnableToolSiteMirrors) {
for (var kv in AppConf.networkVersionData!.webMirrors!.entries) {
if (url.startsWith(kv.key)) {
finalUrl = url.replaceFirst(kv.key, kv.value);
}
}
}
return finalUrl;
}
launch(String url) async {
webview.launch(await _handleMirrorsUrl(url));
}
initLocalization(AppWebLocalizationVersionsData v) async {
localizationScript = await rootBundle.loadString('assets/web_script.js');
/// https://github.com/CxJuice/Uex_Chinese_Translate
// get versions
final hostUrl = URLConf.webTranslateHomeUrl;
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);
localizationResource["DPS"] = await _getJson("$hostUrl/zh-CN-dps.json",
cacheKey: "dps", version: v.dps);
}
List<Map<String, String>> _getLocalizationResource(String key) {
final List<Map<String, String>> localizations = [];
final dict = localizationResource[key];
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 RSHttp.getText(url);
final endTime = DateTime.now();
final data = json.decode(r);
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);
}
FutureOr<void> dispose() {
if (loginMode && !_loginModeSuccess) {
loginCallback?.call(null, false);
}
_isClosed = true;
}
Future<void> _checkAutoLogin(Webview webview) async {
final LocalAuthentication localAuth = LocalAuthentication();
if (!await localAuth.isDeviceSupported()) return;
final userBox = await Hive.openBox("rsi_account_data");
final email = await userBox.get("account_email", defaultValue: "");
final pwdE = await userBox.get("account_pwd_encrypted", defaultValue: "");
final nonceStr = await userBox.get("nonce", defaultValue: "");
final macStr = await userBox.get("mac", defaultValue: "");
if (email == "") return;
webview.evaluateJavaScript("RSIAutoLogin(\"$email\",\"\")");
if (pwdE != "" && nonceStr != "" && macStr != "") {
// send toast
webview.evaluateJavaScript("SCTShowToast(\"请完成 Windows Hello 验证以填充密码\")");
// decrypt
if (await localAuth.authenticate(localizedReason: "请输入设备PIN以自动登录RSI账户") !=
true) return;
final kv = Win32Credentials.read("SCToolbox_RSI_Account_secret");
if (kv == null || kv.key != email) return;
final algorithm = AesGcm.with256bits();
final r = await algorithm.decrypt(
SecretBox(base64.decode(pwdE),
nonce: base64.decode(nonceStr), mac: Mac(base64.decode(macStr))),
secretKey: SecretKey(base64.decode(kv.value)));
final decryptedPwd = utf8.decode(r);
webview.evaluateJavaScript("RSIAutoLogin(\"$email\",\"$decryptedPwd\")");
}
}
}

View File

@ -1,43 +0,0 @@
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 翻译捕获工具";
}

View File

@ -1,53 +0,0 @@
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 = {};
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");
final key = map["key"];
if (key != null && key.toString().trim() != "") {
if (!(webViewModel.curReplaceWords?.containsKey(key) ?? false)) {
data[key] = map["value"];
}
_updateRenderString();
}
}
}
_updateRenderString() {
renderString = "```json\n${jsonEncoder.convert(data)}\n```";
notifyListeners();
}
doClean() {
data.clear();
_updateRenderString();
}
}