mirror of
https://ghfast.top/https://github.com/StarCitizenToolBox/app.git
synced 2025-06-28 14:54:45 +08:00
feat:riverpod 迁移
This commit is contained in:
@ -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) => "";
|
||||
}
|
@ -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!);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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) => "下载管理";
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) => "汉化管理";
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) => "";
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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\")");
|
||||
}
|
||||
}
|
||||
}
|
@ -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 翻译捕获工具";
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user