Compare commits

..

14 Commits

Author SHA1 Message Date
xkeyC
2b7be216a8 bump: WEB 3.0.0 2025-12-23 17:23:30 +08:00
xkeyC
9e324c01fb fix: web zip 2025-12-23 17:22:52 +08:00
xkeyC
79cc157e04 feat: update 2025-12-20 17:31:36 +08:00
xkeyC
6d2bb89c64 feat: deply config 2025-12-20 16:40:53 +08:00
xkeyC
66ab76e784 feat: web yearly_report 2025-12-20 16:04:51 +08:00
xkeyC
acea7bc68c fix: icon 2025-11-13 20:52:37 +08:00
xkeyC
4a8b18fed0 fix: lang 2025-11-13 20:48:29 +08:00
xkeyC
16cc835f23 fix: icon 2025-11-13 20:33:10 +08:00
xkeyC
6984da58b8 fix: update icon 2025-11-13 20:29:43 +08:00
xkeyC
71c3b61bdd feat: AnalyticsApi disable firstLaunch 2025-11-13 20:24:09 +08:00
xkeyC
c31b31516f fix: RSI Status 2025-11-13 20:19:22 +08:00
xkeyC
476c40f4cd feat: WASM web support 2025-11-13 20:07:47 +08:00
xkeyC
193d2c7496 feat: web support 2025-11-13 17:29:46 +08:00
xkeyC
334ed424e9 feat: web support 2025-11-13 17:29:27 +08:00
306 changed files with 9597 additions and 63357 deletions

View File

@@ -1,77 +0,0 @@
name: "Linux Nightly Build"
on:
schedule:
- cron: "0 0 * * *" # every day at midnight
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Linux dependencies
run: |
sudo apt-get update
# libwebkit2gtk-4.1-dev is required for wry webview (4.0 is not available in Ubuntu 24.04+)
sudo apt-get install -y \
clang cmake ninja-build pkg-config nasm \
libgtk-3-dev liblzma-dev \
libsecret-1-dev libjsoncpp-dev \
libnotify-dev libayatana-appindicator3-dev \
libwebkit2gtk-4.1-dev
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' # optional, change this to force refresh cache
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:' # optional, change this to specify the cache path
- run: flutter --version
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Rust Version
run: |
rustup --version
cargo --version
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "rust"
cache-all-crates: true
- name: Flutter pub get
run: flutter pub get
- name: Flutter build runner
run: dart run build_runner build --delete-conflicting-outputs
- name: Rust cargo update
run: cargo update
working-directory: rust
- name: Set up Flutter rust bridge
run: |
cargo install cargo-expand
cargo install 'flutter_rust_bridge_codegen@^2.0.0-dev.0'
- name: Flutter Rust bridge generate
run: flutter_rust_bridge_codegen generate
- name: flutter gen l10n
run: |
flutter pub global activate intl_utils
flutter pub global run intl_utils:generate
- name: Flutter build Linux
run: flutter build linux -v
- name: Archive build
uses: actions/upload-artifact@v4
with:
name: linux
path: build/linux/x64/release/bundle

View File

@@ -40,8 +40,7 @@ jobs:
uses: KyleMayes/install-llvm-action@v2
with:
version: "18"
- name: Set up NASM
uses: ilammy/setup-nasm@v1
- name: Flutter pub get
run: flutter pub get
- name: Flutter build runner
@@ -69,3 +68,4 @@ jobs:
with:
name: windows
path: build/windows/x64/runner/Release

11
.gitignore vendored
View File

@@ -5,11 +5,9 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
@@ -49,4 +47,11 @@ app.*.map.json
/lib/generated/l10n_temp_fix.json
# FVM Version Cache
.fvm/
.fvm/
# Web-only branch: ignore other platform folders
/android/
/ios/
/linux/
/macos/
/windows/

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3"
revision: "a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7"
channel: "stable"
project_type: app
@@ -13,26 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: android
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: ios
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: linux
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: macos
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
create_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
base_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
- platform: web
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: windows
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
create_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
base_revision: a0e9b9dbf78c8a5ef39b45a7efd40ed2de19c1a7
# User provided section

View File

@@ -1,4 +0,0 @@
{
"dart.flutterSdkPath": ".fvm/versions/stable",
"cmake.ignoreCMakeListsMissing": true
}

View File

@@ -4,7 +4,7 @@
该工具为 星际公民玩家 提供 一键诊断,官网及工具网站汉化,游戏汉化,游戏性能优化 等功能,致力于带来更愉快的游戏体验。
[![Windows Nightly Build](https://github.com/StarCitizenToolBox/app/actions/workflows/windows_nightly.yml/badge.svg)](https://github.com/StarCitizenToolBox/app/actions/workflows/windows_nightly.yml) [![Linux Nightly Build](https://github.com/StarCitizenToolBox/app/actions/workflows/linux_nightly.yml/badge.svg)](https://github.com/StarCitizenToolBox/app/actions/workflows/windows_nightly.yml) [![Translate](http://translate.42kit.com/widget/sctoolbox/CoreApp/svg-badge.svg)](http://translate.42kit.com/engage/sctoolbox/)
[![Windows Nightly Build](https://github.com/StarCitizenToolBox/app/actions/workflows/windows_nightly.yml/badge.svg)](https://github.com/StarCitizenToolBox/app/actions/workflows/windows_nightly.yml) [![Translate](http://translate.42kit.com/widget/sctoolbox/CoreApp/svg-badge.svg)](http://translate.42kit.com/engage/sctoolbox/)
[![](https://get.microsoft.com/images/zh-cn%20dark.svg)](https://apps.microsoft.com/detail/9NF3SWFWNKL1?launch=true)

View File

@@ -9,14 +9,11 @@ This tool provides Star Citizen players with one-click diagnosis, official websi
### ✨ Feature
- Localization management: install the localization of the community and switch languages with one click
- Advanced localization: You can choose to translate only parts of the game content, or display it in both languages.
- Vehicle sorting: Utilizing game mechanics, vehicles are assigned numerical prefixes to help you quickly find your favorite spaceship.
- One-click diagnosis: log files from hundreds of guinea pig users, which can handle common problems of Star Citizen
- Website Localization: Provide manual translation for the Star Citizen official website and Star Citizen tool website (thanks to the Star Citizen Chinese Encyclopedia project), and also provide [Browser Extension (Github)] (https://github.com/xkeyC/StarCitizenBoxBrowserEx).
- Website Chineseization: Provide manual translation for the Star Citizen official website and Star Citizen tool website (thanks to the Star Citizen Chinese Encyclopedia project), and also provide [Browser Extension (Github)] (https://github.com/xkeyC/StarCitizenBoxBrowserEx).
- Performance optimization: Add more detailed performance parameter control to the Star Citizen game, which can be used to optimize performance and obtain better image quality.
- Server status indicator: The server status indicator function is added a few hours earlier than the official website launcher, and the indication is more detailed.
- Other commonly used tools: a toolbox including p4k offload downloads, cleaning shaders, reinstalling EAC and other functions.
- unp4k_rs: Uses our own powerful engine to quickly unpack games and perform data mining.
### 📸 Screenshot
![image.png](https://s2.loli.net/2024/05/06/iHmsGd7htjE9uzy.png)
@@ -31,4 +28,4 @@ This tool provides Star Citizen players with one-click diagnosis, official websi
### ❤️ Thanks
Special thanks to [Visual Studio Code](https://code.visualstudio.com/) for providing free development tools.
Special thanks to [Visual Studio Code](https://code.visualstudio.com/) for providing free development tools.

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 965 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

BIN
assets/binary/aria2c.zip Normal file

Binary file not shown.

BIN
assets/binary/unp4kc.zip Normal file

Binary file not shown.

View File

@@ -1,14 +1,14 @@
/// ------- Request Interceptor Script --------------
/// 轻量级网络请求拦截器,不破坏网页正常功能
(function () {
(function() {
'use strict';
if (window._sctRequestInterceptorInstalled) {
console.log('[SCToolbox] Request interceptor already installed');
return;
}
window._sctRequestInterceptorInstalled = true;
// 被屏蔽的域名和路径
const blockedPatterns = [
'google-analytics.com',
@@ -28,24 +28,24 @@
'facebook.net',
'gstatic.com/firebasejs'
];
// 判断 URL 是否应该被屏蔽
const shouldBlock = (url) => {
if (!url || typeof url !== 'string') return false;
const urlLower = url.toLowerCase();
return blockedPatterns.some(pattern => urlLower.includes(pattern.toLowerCase()));
};
// 记录被拦截的请求
const logBlocked = (type, url) => {
console.log(`[SCToolbox] ❌ Blocked ${type}:`, url);
};
const TRANSPARENT_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
// ============ 1. 拦截 Fetch API ============
const originalFetch = window.fetch;
window.fetch = function (...args) {
window.fetch = function(...args) {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
if (shouldBlock(url)) {
logBlocked('fetch', url);
@@ -53,13 +53,13 @@
}
return originalFetch.apply(this, args);
};
// ============ 2. 拦截 XMLHttpRequest ============
const OriginalXHR = window.XMLHttpRequest;
const originalXHROpen = OriginalXHR.prototype.open;
const originalXHRSend = OriginalXHR.prototype.send;
OriginalXHR.prototype.open = function (method, url, ...rest) {
OriginalXHR.prototype.open = function(method, url, ...rest) {
this._url = url;
if (shouldBlock(url)) {
logBlocked('XHR', url);
@@ -67,8 +67,8 @@
}
return originalXHROpen.apply(this, [method, url, ...rest]);
};
OriginalXHR.prototype.send = function (...args) {
OriginalXHR.prototype.send = function(...args) {
if (this._blocked) {
setTimeout(() => {
const errorEvent = new Event('error');
@@ -78,13 +78,13 @@
}
return originalXHRSend.apply(this, args);
};
// ============ 3. 拦截 Image 元素的 src 属性 ============
const imgSrcDescriptor = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');
if (imgSrcDescriptor && imgSrcDescriptor.set) {
Object.defineProperty(HTMLImageElement.prototype, 'src', {
get: imgSrcDescriptor.get,
set: function (value) {
set: function(value) {
if (shouldBlock(value)) {
logBlocked('IMG.src', value);
// 设置为透明 GIF避免请求
@@ -98,13 +98,13 @@
enumerable: true
});
}
// ============ 3.5. 拦截 Script 元素的 src 属性 ============
const scriptSrcDescriptor = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src');
if (scriptSrcDescriptor && scriptSrcDescriptor.set) {
Object.defineProperty(HTMLScriptElement.prototype, 'src', {
get: scriptSrcDescriptor.get,
set: function (value) {
set: function(value) {
if (shouldBlock(value)) {
logBlocked('SCRIPT.src', value);
// 阻止加载,不设置 src
@@ -117,10 +117,10 @@
enumerable: true
});
}
// ============ 4. 拦截 setAttribute用于 img.setAttribute('src', ...)============
const originalSetAttribute = Element.prototype.setAttribute;
Element.prototype.setAttribute = function (name, value) {
Element.prototype.setAttribute = function(name, value) {
if (name.toLowerCase() === 'src' && this.tagName === 'IMG' && shouldBlock(value)) {
logBlocked('IMG setAttribute', value);
originalSetAttribute.call(this, name, TRANSPARENT_GIF);
@@ -133,11 +133,11 @@
}
return originalSetAttribute.call(this, name, value);
};
// ============ 5. 拦截 navigator.sendBeacon ============
if (navigator.sendBeacon) {
const originalSendBeacon = navigator.sendBeacon.bind(navigator);
navigator.sendBeacon = function (url, data) {
navigator.sendBeacon = function(url, data) {
if (shouldBlock(url)) {
logBlocked('sendBeacon', url);
return true; // 假装成功
@@ -145,13 +145,13 @@
return originalSendBeacon(url, data);
};
}
// ============ 6. 使用 MutationObserver 监听动态添加的元素 ============
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== 1) return; // 只处理元素节点
try {
// 检查 IMG 元素
if (node.tagName === 'IMG') {
@@ -180,7 +180,7 @@
node.style.cssText += 'display:none !important;';
}
}
// 递归检查子元素
if (node.querySelectorAll) {
node.querySelectorAll('img').forEach(img => {
@@ -191,7 +191,7 @@
img.style.cssText += 'display:none !important;width:0;height:0;';
}
});
node.querySelectorAll('script[src]').forEach(script => {
const src = script.getAttribute('src');
if (src && shouldBlock(src)) {
@@ -207,7 +207,7 @@
});
});
});
// 延迟启动 observer等待页面初始化完成
const startObserver = () => {
if (document.body) {
@@ -220,13 +220,13 @@
setTimeout(startObserver, 50);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startObserver);
} else {
startObserver();
}
console.log('[SCToolbox] ✅ Request interceptor installed');
console.log('[SCToolbox] 🛡️ Blocking', blockedPatterns.length, 'patterns');
})();

View File

@@ -10,7 +10,7 @@
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 1.5rem; /* 24px */
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;

View File

@@ -207,7 +207,7 @@ function ReportUnTranslate(k, v) {
const jsRegex = /(?:^|[^<])<script[^>]*>[\s\S]*?<\/script>(?:[^>]|$)/i;
if (k.trim() !== "" && !cnPattern.test(k) && !htmlPattern.test(k) && !cssRegex.test(k) && !jsRegex.test(k)
&& enPattern.test(k) && !k.startsWith("http://") && !k.startsWith("https://")) {
window.ipc.postMessage(JSON.stringify({ action: 'webview_localization_capture', key: k, value: v }));
window.chrome.webview.postMessage({ action: 'webview_localization_capture', key: k, value: v });
}
}
}
@@ -217,149 +217,93 @@ InitWebLocalization();
/// ----- Login Script ----
async function getRSILauncherToken(channelId) {
console.log('[SCToolbox] getRSILauncherToken called with channel:', channelId);
try {
if (!window.location.href.includes("robertsspaceindustries.com")) {
console.log('[SCToolbox] Not on RSI site, skipping');
return;
}
if (!window.location.href.includes("robertsspaceindustries.com")) return;
// check if logged in and fix redirect
if (window.location.href.endsWith('/connect?jumpto=/account/dashboard')) {
if (document.body.textContent.trim() === "/account/dashboard") {
window.location.href = "https://robertsspaceindustries.com/account/dashboard";
return;
}
}
// Wait for jQuery to be ready
let waitCount = 0;
while (typeof $ === 'undefined' && waitCount < 50) {
console.log('[SCToolbox] Waiting for jQuery... attempt', waitCount);
await new Promise(r => setTimeout(r, 100));
waitCount++;
}
if (typeof $ === 'undefined') {
console.error('[SCToolbox] jQuery not available after waiting');
return;
}
// Get RSI token from cookie (don't rely on $.cookie plugin)
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
const rsiToken = getCookie('Rsi-Token');
console.log('[SCToolbox] RSI Token available:', !!rsiToken);
if (!rsiToken) {
console.log('[SCToolbox] No RSI token, showing login window');
let loginBodyElement = $(".c-form authenticationForm sign_in");
loginBodyElement.show();
window.ipc.postMessage(JSON.stringify({ action: 'webview_rsi_login_show_window' }));
return;
}
// get claims
console.log('[SCToolbox] Fetching claims...');
let claimsR = await fetch("https://robertsspaceindustries.com/api/launcher/v3/games/claims", {
method: 'POST', headers: {
'x-rsi-token': rsiToken,
},
});
console.log('[SCToolbox] Claims response status:', claimsR.status);
if (claimsR.status !== 200) {
console.error('[SCToolbox] Claims request failed');
return;
}
SCTShowToast("登录游戏中...");
let claimsData = (await claimsR.json())["data"];
console.log('[SCToolbox] Claims data received');
let tokenFormData = new FormData();
tokenFormData.append('claims', claimsData);
tokenFormData.append('gameId', 'SC');
let tokenR = await fetch("https://robertsspaceindustries.com/api/launcher/v3/games/token", {
method: 'POST', headers: {
'x-rsi-token': rsiToken,
},
body: tokenFormData
});
console.log('[SCToolbox] Token response status:', tokenR.status);
if (tokenR.status !== 200) {
console.error('[SCToolbox] Token request failed');
return;
}
let TokenData = (await tokenR.json())["data"]["token"];
console.log('[SCToolbox] Token received');
// get release Data
let releaseFormData = new FormData();
releaseFormData.append("channelId", channelId);
releaseFormData.append("claims", claimsData);
releaseFormData.append("gameId", "SC");
releaseFormData.append("platformId", "prod");
let releaseR = await fetch("https://robertsspaceindustries.com/api/launcher/v3/games/release", {
method: 'POST', headers: {
'x-rsi-token': rsiToken,
},
body: releaseFormData
});
console.log('[SCToolbox] Release response status:', releaseR.status);
if (releaseR.status !== 200) {
console.error('[SCToolbox] Release request failed');
return;
}
let releaseDataJson = (await releaseR.json())['data'];
console.log('[SCToolbox] Release data received');
// get game library
let libraryR = await fetch("https://robertsspaceindustries.com/api/launcher/v3/games/library", {
method: 'POST', headers: {
'x-rsi-token': rsiToken,
},
body: releaseFormData
});
let libraryData = (await libraryR.json())["data"];
console.log('[SCToolbox] Library data received');
// get user avatar
let avatarUrl = $(".orion-c-avatar__image").attr("src") || '';
console.log('[SCToolbox] Avatar URL:', avatarUrl);
//post message
console.log('[SCToolbox] Sending login success message...');
window.ipc.postMessage(JSON.stringify({
action: 'webview_rsi_login_success', data: {
'webToken': rsiToken,
'claims': claimsData,
'authToken': TokenData,
'releaseInfo': releaseDataJson,
"avatar": avatarUrl,
'libraryData': libraryData,
}
}));
console.log('[SCToolbox] Login success message sent');
} catch (error) {
console.error('[SCToolbox] Error in getRSILauncherToken:', error);
// check if logged in and fix redirect
if (window.location.href.endsWith('/connect?jumpto=/account/dashboard')) {
if (document.body.textContent.trim() === "/account/dashboard") {
window.location.href = "https://robertsspaceindustries.com/account/dashboard";
return;
}
}
let loginBodyElement = $(".c-form authenticationForm sign_in");
loginBodyElement.show();
// wait login
window.chrome.webview.postMessage({ action: 'webview_rsi_login_show_window' });
// get claims
let claimsR = await fetch("https://robertsspaceindustries.com/api/launcher/v3/games/claims", {
method: 'POST', headers: {
'x-rsi-token': $.cookie('Rsi-Token'),
},
});
if (claimsR.status !== 200) return;
loginBodyElement.hide();
SCTShowToast("登录游戏中...");
let claimsData = (await claimsR.json())["data"];
let tokenFormData = new FormData();
tokenFormData.append('claims', claimsData);
tokenFormData.append('gameId', 'SC');
let tokenR = await fetch("https://robertsspaceindustries.com/api/launcher/v3/games/token", {
method: 'POST', headers: {
'x-rsi-token': $.cookie('Rsi-Token'),
},
body: tokenFormData
});
if (tokenR.status !== 200) return;
let TokenData = (await tokenR.json())["data"]["token"];
console.log(TokenData);
// get release Data
let releaseFormData = new FormData();
releaseFormData.append("channelId", channelId);
releaseFormData.append("claims", claimsData);
releaseFormData.append("gameId", "SC");
releaseFormData.append("platformId", "prod");
let releaseR = await fetch("https://robertsspaceindustries.com/api/launcher/v3/games/release", {
method: 'POST', headers: {
'x-rsi-token': $.cookie('Rsi-Token'),
},
body: releaseFormData
});
if (releaseR.status !== 200) return;
let releaseDataJson = (await releaseR.json())['data'];
console.log(releaseDataJson);
// get game library
let libraryR = await fetch("https://robertsspaceindustries.com/api/launcher/v3/games/library", {
method: 'POST', headers: {
'x-rsi-token': $.cookie('Rsi-Token'),
},
body: releaseFormData
});
let libraryData = (await libraryR.json())["data"]
// get user avatar
let avatarUrl = $(".orion-c-avatar__image").attr("src");
//post message
window.chrome.webview.postMessage({
action: 'webview_rsi_login_success', data: {
'webToken': $.cookie('Rsi-Token'),
'claims': claimsData,
'authToken': TokenData,
'releaseInfo': releaseDataJson,
"avatar": avatarUrl,
'libraryData': libraryData,
}
});
}
function SCTShowToast(message) {
let m = document.createElement('div');
m.innerHTML = message;
m.style.cssText = "font-family:siyuan;max-width:60%;min-width: 9.375rem;padding:0 0.875rem;height: 2.5rem;color: rgb(255, 255, 255);line-height: 2.5rem;text-align: center;border-radius: 0.25rem;position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 999999;background: rgba(0, 0, 0,.7);font-size: 1rem;";
m.style.cssText = "font-family:siyuan;max-width:60%;min-width: 150px;padding:0 14px;height: 40px;color: rgb(255, 255, 255);line-height: 40px;text-align: center;border-radius: 4px;position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 999999;background: rgba(0, 0, 0,.7);font-size: 16px;";
document.body.appendChild(m);
setTimeout(function () {
let d = 0.5;

View File

@@ -1,123 +0,0 @@
# SCToolBox OAuth 认证系统
## 快速开始
### 授权流程
```
┌─────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐
│ Web App │──1──▶│ Browser │──2──▶│ SCToolBox │──3──▶│ Server │
└─────────┘ └──────────┘ └────────────┘ └──────────┘
▲ │ │
│ ├──4──▶ 验证域名 │
│ │ 安全性 │
│ │ │
│ ├──5──▶ 生成 JWT │
│ │ 令牌 │
│ │ │
└─────────────────6─────────────────┘ │
返回令牌 │
```
### URL Scheme 格式
```
sctoolbox://auth/{domain}?callbackUrl={回调地址}
```
### 示例
```
sctoolbox://auth/example.com?callbackUrl=https%3A%2F%2Fexample.com%2Fauth%2Fcallback
```
### 回调格式
```
{callbackUrl}#access_token={jwt_token}&token_type=Bearer
```
## 功能特性
- ✅ 基于 JWT 的安全认证
- ✅ 域名白名单验证
- ✅ 跨平台支持Windows、macOS、Linux
- ✅ 两种授权方式(直接跳转 / 复制链接)
- ✅ 符合 OAuth 2.0 Implicit Flow 标准
## 实现文件
### 核心文件
- `lib/ui/auth/auth_page.dart` - 授权页面 UI
- `lib/ui/auth/auth_ui_model.dart` - 授权页面状态管理
- `lib/common/utils/url_scheme_handler.dart` - URL Scheme 处理器
### 平台配置
- `macos/Runner/Info.plist` - macOS URL Scheme 配置
- `windows/runner/main.cpp` - Windows Deep Link 处理
- `linux/my_application.cc` - Linux Deep Link 处理
- `linux/sctoolbox.desktop` - Linux MIME 类型注册
- `pubspec.yaml` - MSIX 协议激活配置
## 使用方法
### 初始化
URL Scheme handler 在 `IndexUI` 中自动初始化:
```dart
useEffect(() {
UrlSchemeHandler().initialize(context);
return () => UrlSchemeHandler().dispose();
}, const []);
```
### Web 应用集成
```javascript
// 发起授权
const authUrl = `sctoolbox://auth/example.com?callbackUrl=${encodeURIComponent(callbackUrl)}`;
window.location.href = authUrl;
// 处理回调
const params = new URLSearchParams(window.location.hash.substring(1));
const token = params.get('access_token');
```
## 平台要求
- **Windows**: 需要使用 MSIX 打包版本
- **macOS**: 需要配置 Info.plist
- **Linux**: 需要注册 .desktop 文件
## 安全性
- ✅ JWT 签名验证
- ✅ 域名白名单检查
- ✅ 令牌过期时间控制
- ✅ 使用 Fragment (#) 传递令牌(更安全)
## 详细文档
查看 [完整文档](./AUTH_SYSTEM.md) 了解更多信息,包括:
- 详细的授权流程
- API 接口说明
- Web 应用集成示例
- 安全最佳实践
- 常见问题解答
## API 端点
认证服务提供以下 gRPC 接口:
- `GenerateToken` - 生成 JWT 令牌
- `ValidateToken` - 验证令牌有效性
- `GetPublicKey` - 获取公钥用于验证
- `GetJWTDomainList` - 获取可信域名列表
## 测试
```bash
# macOS/Linux
open "sctoolbox://auth/test.example.com?callbackUrl=https%3A%2F%2Ftest.example.com%2Fcallback"
# Windows
start "sctoolbox://auth/test.example.com?callbackUrl=https%3A%2F%2Ftest.example.com%2Fcallback"
```

View File

@@ -7,15 +7,13 @@ import 'package:starcitizen_doctor/common/utils/log.dart';
class AnalyticsApi {
static Future<void> touch(String key) async {
if (kDebugMode || kProfileMode) {
if (kDebugMode || kProfileMode || kIsWeb) {
dPrint("AnalyticsApi.touch === $key skip");
return;
}
dPrint("AnalyticsApi.touch === $key start");
try {
final r = await RSHttp.postData(
"${URLConf.analyticsApiHome}/analytics/$key",
data: null);
final r = await RSHttp.postData("${URLConf.analyticsApiHome}/analytics/$key", data: null);
dPrint("AnalyticsApi.touch === $key over statusCode == ${r.statusCode}");
} catch (e) {
dPrint("AnalyticsApi.touch === $key Error:$e");

View File

@@ -73,7 +73,7 @@ class Api {
}
static Future<List> getScServerStatus() async {
final r = await RSHttp.getText("https://status.robertsspaceindustries.com/index.json");
final r = await RSHttp.getText("https:///web-proxy.scbox.xkeyc.cn/rsi_status/index.json");
final map = json.decode(r);
return map["systems"];
}

View File

@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:go_router/go_router.dart';
import 'package:hexcolor/hexcolor.dart';
@@ -15,18 +15,12 @@ import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/ui/guide/guide_ui.dart';
import 'package:starcitizen_doctor/ui/home/performance/performance_ui.dart';
import 'package:starcitizen_doctor/ui/splash_ui.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:uuid/uuid.dart';
import 'package:window_manager/window_manager.dart';
import 'api/analytics.dart';
import 'api/api.dart';
import 'common/conf/url_conf.dart';
import 'common/helper/system_helper.dart';
import 'common/io/rs_http.dart';
import 'common/rust/frb_generated.dart';
import 'common/rust/api/applinks_api.dart' as applinks;
import 'common/rust/api/win32_api.dart' as win32;
// import 'common/rust/api/win32_api.dart' as win32; // Web 不支持
import 'data/app_version_data.dart';
import 'generated/no_l10n_strings.dart';
import 'ui/home/downloader/home_downloader_ui.dart';
@@ -34,8 +28,8 @@ import 'ui/home/game_doctor/game_doctor_ui.dart';
import 'ui/home/localization/advanced_localization_ui.dart';
import 'ui/index_ui.dart';
import 'ui/settings/upgrade_dialog.dart';
import 'ui/tools/unp4kc/dcb_viewer_ui.dart';
import 'ui/tools/unp4kc/unp4kc_ui.dart';
import 'ui/tools/yearly_report/yearly_report_entry.dart';
part 'app.g.dart';
@@ -51,17 +45,18 @@ abstract class AppGlobalState with _$AppGlobalState {
@Default(ThemeConf()) ThemeConf themeConf,
Locale? appLocale,
Box? appConfBox,
@Default(10) windowsVersion,
@Default("assets/backgrounds/SC_01_Wallpaper_3840x2160.webp") String backgroundImageAssetsPath,
}) = _AppGlobalState;
}
@riverpod
GoRouter router(Ref ref) {
return GoRouter(
initialLocation: '/splash',
routes: [
GoRoute(path: '/', pageBuilder: (context, state) => myPageBuilder(context, state, const SplashUI())),
GoRoute(path: '/splash', pageBuilder: (context, state) => myPageBuilder(context, state, const SplashUI())),
GoRoute(
path: '/index',
path: '/',
pageBuilder: (context, state) => myPageBuilder(context, state, const IndexUI()),
routes: [
GoRoute(
@@ -88,9 +83,8 @@ GoRouter router(Ref ref) {
routes: [
GoRoute(path: 'unp4kc', pageBuilder: (context, state) => myPageBuilder(context, state, const UnP4kcUI())),
GoRoute(
path: 'dcb_viewer',
pageBuilder: (context, state) =>
myPageBuilder(context, state, DcbViewerUI(initialFilePath: (state.extra as Map?)?['path'])),
path: 'yearly_report',
pageBuilder: (context, state) => myPageBuilder(context, state, const YearlyReportEntryUIRoute()),
),
],
),
@@ -121,27 +115,18 @@ class AppGlobalModel extends _$AppGlobalModel {
if (_initialized) return;
// init Data
final applicationSupportDir = await _initAppDir();
// init Rust bridge
await RustLib.init();
await RSHttp.init();
dPrint("---- rust bridge init -----");
// Register URL scheme
if ((!ConstConf.isMSE || kDebugMode) && Platform.isWindows) {
await _registerUrlScheme();
}
// init Hive
try {
Hive.init("$applicationSupportDir/db");
await Future.delayed(const Duration(milliseconds: 100));
if (!kIsWeb) Hive.init("$applicationSupportDir/db");
final box = await Hive.openBox("app_conf");
state = state.copyWith(appConfBox: box);
if (box.get("install_id", defaultValue: "") == "") {
await box.put("install_id", const Uuid().v4());
AnalyticsApi.touch("firstLaunch");
}
// if (box.get("install_id", defaultValue: "") == "") {
// await box.put("install_id", const Uuid().v4());
// AnalyticsApi.touch("firstLaunch");
// }
final deviceUUID = box.get("install_id", defaultValue: "");
final localeCode = box.get("app_locale", defaultValue: null);
Locale? locale;
@@ -155,44 +140,82 @@ class AppGlobalModel extends _$AppGlobalModel {
}
state = state.copyWith(deviceUUID: deviceUUID, appLocale: locale);
} catch (e) {
await win32.setForegroundWindow(windowName: "SCToolBox");
// Web 平台不支持 win32 API
if (!kIsWeb) {
// await win32.setForegroundWindow(windowName: "SCToolBox");
}
dPrint("exit: db is locking ...");
exit(0);
if (!kIsWeb) exit(0);
}
// init powershell
if (!kIsWeb && Platform.isWindows) {
try {
await SystemHelper.initPowershellPath();
dPrint("---- Powershell init -----");
} catch (e) {
dPrint("powershell init failed : $e");
}
}
// get windows info
WindowsDeviceInfo? windowsDeviceInfo;
try {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
windowsDeviceInfo = await deviceInfo.windowsInfo;
} catch (e) {
dPrint("DeviceInfo.windowsInfo error: $e");
}
// WindowsDeviceInfo? windowsDeviceInfo;
// try {
// DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
// windowsDeviceInfo = await deviceInfo.windowsInfo;
// } catch (e) {
// dPrint("DeviceInfo.windowsInfo error: $e");
// }
// init windows
windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setTitle("SCToolBox");
await windowManager.setSkipTaskbar(false);
if (Platform.isWindows) {
if (windowsDeviceInfo?.productName.contains("Windows 11") ?? false) {
// Apply acrylic effect before showing window
await Window.setEffect(effect: WindowEffect.acrylic, color: Colors.transparent, dark: true);
state = state.copyWith(windowsVersion: 11);
dPrint("---- Windows 11 Acrylic Effect applied -----");
} else {
state = state.copyWith(windowsVersion: 10);
await Window.setEffect(effect: WindowEffect.disabled);
}
}
// Show window after acrylic effect is applied
await windowManager.show();
});
// if (!kIsWeb) {
// windowManager.waitUntilReadyToShow().then((_) async {
// await windowManager.setTitle("SCToolBox");
// await windowManager.setSkipTaskbar(false);
// await windowManager.show();
// if (Platform.isWindows) {
// await Window.initialize();
// await Window.hideWindowControls();
// if (windowsDeviceInfo?.productName.contains("Windows 11") ?? false) {
// await Window.setEffect(effect: WindowEffect.acrylic);
// }
// }
// });
// }
dPrint("---- Window init -----");
if (kIsWeb) {
_startBackgroundLoop();
}
_initialized = true;
ref.keepAlive();
}
Timer? _loopTimer;
void _startBackgroundLoop() async {
_loopTimer?.cancel();
_loopTimer = null;
final assetManifest = await AssetManifest.loadFromAssetBundle(rootBundle);
final imageAssetsList = assetManifest
.listAssets()
.where((string) => string.startsWith("assets/backgrounds"))
.toList();
void rollImage() {
final random = DateTime.now().millisecondsSinceEpoch % imageAssetsList.length;
final image = imageAssetsList[random];
state = state.copyWith(backgroundImageAssetsPath: image);
dPrint("rollImage: [$random] $image");
}
rollImage();
// 使用 timer 每 30 秒 更换一次随机图片
_loopTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
rollImage();
});
}
String getUpgradePath() {
return "${state.applicationSupportDir}/._upgrade";
}
@@ -201,7 +224,7 @@ class AppGlobalModel extends _$AppGlobalModel {
// ignore: avoid_build_context_in_providers
Future<bool> checkUpdate(BuildContext context) async {
if (!ConstConf.isMSE) {
if (!kIsWeb && !ConstConf.isMSE) {
final dir = Directory(getUpgradePath());
if (await dir.exists()) {
dir.delete(recursive: true);
@@ -238,6 +261,7 @@ class AppGlobalModel extends _$AppGlobalModel {
);
return false;
}
if (kIsWeb) return false; // Web 版本不支持自动更新
if (!Platform.isWindows) return false;
final lastVersion = ConstConf.isMSE
? state.networkVersionData?.mSELastVersionCode
@@ -328,28 +352,13 @@ class AppGlobalModel extends _$AppGlobalModel {
}
}
/// Register sctoolbox:// URL scheme for non-MSE builds
Future<void> _registerUrlScheme() async {
try {
const scheme = "sctoolbox";
const appName = "SCToolBox";
final result = await applinks.registerApplinks(scheme: scheme, appName: appName);
if (result.success) {
if (result.wasModified) {
dPrint("URL scheme '$scheme' registered successfully: ${result.message}");
} else {
dPrint("URL scheme '$scheme' already registered: ${result.message}");
}
} else {
dPrint("URL scheme '$scheme' registration check: ${result.message}");
// Even if check fails, the registration might have succeeded
}
} catch (e) {
dPrint("Failed to register URL scheme: $e");
}
}
Future<String> _initAppDir() async {
if (kIsWeb) {
await Future.delayed(const Duration(milliseconds: 10));
// Web 版本不需要本地目录
state = state.copyWith(applicationSupportDir: "", applicationBinaryModuleDir: "");
return Future.value("");
}
if (Platform.isWindows) {
final userProfileDir = Platform.environment["USERPROFILE"];
final applicationSupportDir = (await getApplicationSupportDirectory()).absolute.path;

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AppGlobalState {
String? get deviceUUID; String? get applicationSupportDir; String? get applicationBinaryModuleDir; AppVersionData? get networkVersionData; ThemeConf get themeConf; Locale? get appLocale; Box? get appConfBox; dynamic get windowsVersion;
String? get deviceUUID; String? get applicationSupportDir; String? get applicationBinaryModuleDir; AppVersionData? get networkVersionData; ThemeConf get themeConf; Locale? get appLocale; Box? get appConfBox; String get backgroundImageAssetsPath;
/// Create a copy of AppGlobalState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $AppGlobalStateCopyWith<AppGlobalState> get copyWith => _$AppGlobalStateCopyWith
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppGlobalState&&(identical(other.deviceUUID, deviceUUID) || other.deviceUUID == deviceUUID)&&(identical(other.applicationSupportDir, applicationSupportDir) || other.applicationSupportDir == applicationSupportDir)&&(identical(other.applicationBinaryModuleDir, applicationBinaryModuleDir) || other.applicationBinaryModuleDir == applicationBinaryModuleDir)&&(identical(other.networkVersionData, networkVersionData) || other.networkVersionData == networkVersionData)&&(identical(other.themeConf, themeConf) || other.themeConf == themeConf)&&(identical(other.appLocale, appLocale) || other.appLocale == appLocale)&&(identical(other.appConfBox, appConfBox) || other.appConfBox == appConfBox)&&const DeepCollectionEquality().equals(other.windowsVersion, windowsVersion));
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppGlobalState&&(identical(other.deviceUUID, deviceUUID) || other.deviceUUID == deviceUUID)&&(identical(other.applicationSupportDir, applicationSupportDir) || other.applicationSupportDir == applicationSupportDir)&&(identical(other.applicationBinaryModuleDir, applicationBinaryModuleDir) || other.applicationBinaryModuleDir == applicationBinaryModuleDir)&&(identical(other.networkVersionData, networkVersionData) || other.networkVersionData == networkVersionData)&&(identical(other.themeConf, themeConf) || other.themeConf == themeConf)&&(identical(other.appLocale, appLocale) || other.appLocale == appLocale)&&(identical(other.appConfBox, appConfBox) || other.appConfBox == appConfBox)&&(identical(other.backgroundImageAssetsPath, backgroundImageAssetsPath) || other.backgroundImageAssetsPath == backgroundImageAssetsPath));
}
@override
int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox,const DeepCollectionEquality().hash(windowsVersion));
int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox,backgroundImageAssetsPath);
@override
String toString() {
return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox, windowsVersion: $windowsVersion)';
return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox, backgroundImageAssetsPath: $backgroundImageAssetsPath)';
}
@@ -45,7 +45,7 @@ abstract mixin class $AppGlobalStateCopyWith<$Res> {
factory $AppGlobalStateCopyWith(AppGlobalState value, $Res Function(AppGlobalState) _then) = _$AppGlobalStateCopyWithImpl;
@useResult
$Res call({
String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, dynamic windowsVersion
String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath
});
@@ -62,7 +62,7 @@ class _$AppGlobalStateCopyWithImpl<$Res>
/// Create a copy of AppGlobalState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? deviceUUID = freezed,Object? applicationSupportDir = freezed,Object? applicationBinaryModuleDir = freezed,Object? networkVersionData = freezed,Object? themeConf = null,Object? appLocale = freezed,Object? appConfBox = freezed,Object? windowsVersion = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? deviceUUID = freezed,Object? applicationSupportDir = freezed,Object? applicationBinaryModuleDir = freezed,Object? networkVersionData = freezed,Object? themeConf = null,Object? appLocale = freezed,Object? appConfBox = freezed,Object? backgroundImageAssetsPath = null,}) {
return _then(_self.copyWith(
deviceUUID: freezed == deviceUUID ? _self.deviceUUID : deviceUUID // ignore: cast_nullable_to_non_nullable
as String?,applicationSupportDir: freezed == applicationSupportDir ? _self.applicationSupportDir : applicationSupportDir // ignore: cast_nullable_to_non_nullable
@@ -71,8 +71,8 @@ as String?,networkVersionData: freezed == networkVersionData ? _self.networkVers
as AppVersionData?,themeConf: null == themeConf ? _self.themeConf : themeConf // ignore: cast_nullable_to_non_nullable
as ThemeConf,appLocale: freezed == appLocale ? _self.appLocale : appLocale // ignore: cast_nullable_to_non_nullable
as Locale?,appConfBox: freezed == appConfBox ? _self.appConfBox : appConfBox // ignore: cast_nullable_to_non_nullable
as Box?,windowsVersion: freezed == windowsVersion ? _self.windowsVersion : windowsVersion // ignore: cast_nullable_to_non_nullable
as dynamic,
as Box?,backgroundImageAssetsPath: null == backgroundImageAssetsPath ? _self.backgroundImageAssetsPath : backgroundImageAssetsPath // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of AppGlobalState
@@ -166,10 +166,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, dynamic windowsVersion)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AppGlobalState() when $default != null:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.windowsVersion);case _:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.backgroundImageAssetsPath);case _:
return orElse();
}
@@ -187,10 +187,10 @@ return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBi
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, dynamic windowsVersion) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath) $default,) {final _that = this;
switch (_that) {
case _AppGlobalState():
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.windowsVersion);case _:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.backgroundImageAssetsPath);case _:
throw StateError('Unexpected subclass');
}
@@ -207,10 +207,10 @@ return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBi
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, dynamic windowsVersion)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath)? $default,) {final _that = this;
switch (_that) {
case _AppGlobalState() when $default != null:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.windowsVersion);case _:
return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.backgroundImageAssetsPath);case _:
return null;
}
@@ -222,7 +222,7 @@ return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBi
class _AppGlobalState implements AppGlobalState {
const _AppGlobalState({this.deviceUUID, this.applicationSupportDir, this.applicationBinaryModuleDir, this.networkVersionData, this.themeConf = const ThemeConf(), this.appLocale, this.appConfBox, this.windowsVersion = 10});
const _AppGlobalState({this.deviceUUID, this.applicationSupportDir, this.applicationBinaryModuleDir, this.networkVersionData, this.themeConf = const ThemeConf(), this.appLocale, this.appConfBox, this.backgroundImageAssetsPath = "assets/backgrounds/SC_01_Wallpaper_3840x2160.webp"});
@override final String? deviceUUID;
@@ -232,7 +232,7 @@ class _AppGlobalState implements AppGlobalState {
@override@JsonKey() final ThemeConf themeConf;
@override final Locale? appLocale;
@override final Box? appConfBox;
@override@JsonKey() final dynamic windowsVersion;
@override@JsonKey() final String backgroundImageAssetsPath;
/// Create a copy of AppGlobalState
/// with the given fields replaced by the non-null parameter values.
@@ -244,16 +244,16 @@ _$AppGlobalStateCopyWith<_AppGlobalState> get copyWith => __$AppGlobalStateCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppGlobalState&&(identical(other.deviceUUID, deviceUUID) || other.deviceUUID == deviceUUID)&&(identical(other.applicationSupportDir, applicationSupportDir) || other.applicationSupportDir == applicationSupportDir)&&(identical(other.applicationBinaryModuleDir, applicationBinaryModuleDir) || other.applicationBinaryModuleDir == applicationBinaryModuleDir)&&(identical(other.networkVersionData, networkVersionData) || other.networkVersionData == networkVersionData)&&(identical(other.themeConf, themeConf) || other.themeConf == themeConf)&&(identical(other.appLocale, appLocale) || other.appLocale == appLocale)&&(identical(other.appConfBox, appConfBox) || other.appConfBox == appConfBox)&&const DeepCollectionEquality().equals(other.windowsVersion, windowsVersion));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppGlobalState&&(identical(other.deviceUUID, deviceUUID) || other.deviceUUID == deviceUUID)&&(identical(other.applicationSupportDir, applicationSupportDir) || other.applicationSupportDir == applicationSupportDir)&&(identical(other.applicationBinaryModuleDir, applicationBinaryModuleDir) || other.applicationBinaryModuleDir == applicationBinaryModuleDir)&&(identical(other.networkVersionData, networkVersionData) || other.networkVersionData == networkVersionData)&&(identical(other.themeConf, themeConf) || other.themeConf == themeConf)&&(identical(other.appLocale, appLocale) || other.appLocale == appLocale)&&(identical(other.appConfBox, appConfBox) || other.appConfBox == appConfBox)&&(identical(other.backgroundImageAssetsPath, backgroundImageAssetsPath) || other.backgroundImageAssetsPath == backgroundImageAssetsPath));
}
@override
int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox,const DeepCollectionEquality().hash(windowsVersion));
int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox,backgroundImageAssetsPath);
@override
String toString() {
return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox, windowsVersion: $windowsVersion)';
return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox, backgroundImageAssetsPath: $backgroundImageAssetsPath)';
}
@@ -264,7 +264,7 @@ abstract mixin class _$AppGlobalStateCopyWith<$Res> implements $AppGlobalStateCo
factory _$AppGlobalStateCopyWith(_AppGlobalState value, $Res Function(_AppGlobalState) _then) = __$AppGlobalStateCopyWithImpl;
@override @useResult
$Res call({
String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, dynamic windowsVersion
String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, String backgroundImageAssetsPath
});
@@ -281,7 +281,7 @@ class __$AppGlobalStateCopyWithImpl<$Res>
/// Create a copy of AppGlobalState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? deviceUUID = freezed,Object? applicationSupportDir = freezed,Object? applicationBinaryModuleDir = freezed,Object? networkVersionData = freezed,Object? themeConf = null,Object? appLocale = freezed,Object? appConfBox = freezed,Object? windowsVersion = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? deviceUUID = freezed,Object? applicationSupportDir = freezed,Object? applicationBinaryModuleDir = freezed,Object? networkVersionData = freezed,Object? themeConf = null,Object? appLocale = freezed,Object? appConfBox = freezed,Object? backgroundImageAssetsPath = null,}) {
return _then(_AppGlobalState(
deviceUUID: freezed == deviceUUID ? _self.deviceUUID : deviceUUID // ignore: cast_nullable_to_non_nullable
as String?,applicationSupportDir: freezed == applicationSupportDir ? _self.applicationSupportDir : applicationSupportDir // ignore: cast_nullable_to_non_nullable
@@ -290,8 +290,8 @@ as String?,networkVersionData: freezed == networkVersionData ? _self.networkVers
as AppVersionData?,themeConf: null == themeConf ? _self.themeConf : themeConf // ignore: cast_nullable_to_non_nullable
as ThemeConf,appLocale: freezed == appLocale ? _self.appLocale : appLocale // ignore: cast_nullable_to_non_nullable
as Locale?,appConfBox: freezed == appConfBox ? _self.appConfBox : appConfBox // ignore: cast_nullable_to_non_nullable
as Box?,windowsVersion: freezed == windowsVersion ? _self.windowsVersion : windowsVersion // ignore: cast_nullable_to_non_nullable
as dynamic,
as Box?,backgroundImageAssetsPath: null == backgroundImageAssetsPath ? _self.backgroundImageAssetsPath : backgroundImageAssetsPath // ignore: cast_nullable_to_non_nullable
as String,
));
}

View File

@@ -10,12 +10,12 @@ part of 'app.dart';
// ignore_for_file: type=lint, type=warning
@ProviderFor(router)
final routerProvider = RouterProvider._();
const routerProvider = RouterProvider._();
final class RouterProvider
extends $FunctionalProvider<GoRouter, GoRouter, GoRouter>
with $Provider<GoRouter> {
RouterProvider._()
const RouterProvider._()
: super(
from: null,
argument: null,
@@ -48,14 +48,14 @@ final class RouterProvider
}
}
String _$routerHash() => r'e89f3f0277879147cdce5373cbe2554821e9cd31';
String _$routerHash() => r'eb81af4202a3a92cf95447ecfb6a698f9a8cd122';
@ProviderFor(AppGlobalModel)
final appGlobalModelProvider = AppGlobalModelProvider._();
const appGlobalModelProvider = AppGlobalModelProvider._();
final class AppGlobalModelProvider
extends $NotifierProvider<AppGlobalModel, AppGlobalState> {
AppGlobalModelProvider._()
const AppGlobalModelProvider._()
: super(
from: null,
argument: null,
@@ -82,13 +82,14 @@ final class AppGlobalModelProvider
}
}
String _$appGlobalModelHash() => r'74128d2194d00a0e3dbb000dcaf6452e0b966d9c';
String _$appGlobalModelHash() => r'8bd5efc6fb95ad9d06875e6fb18bf46ae222fafa';
abstract class _$AppGlobalModel extends $Notifier<AppGlobalState> {
AppGlobalState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AppGlobalState, AppGlobalState>;
final element =
ref.element
@@ -98,6 +99,6 @@ abstract class _$AppGlobalModel extends $Notifier<AppGlobalState> {
Object?,
Object?
>;
element.handleCreate(ref, build);
element.handleValue(ref, created);
}
}

View File

@@ -6,8 +6,10 @@ import 'package:flutter/services.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
class BinaryModuleConf {
// aria2c has been replaced by rqbit (Rust-based torrent library)
static const _modules = <String, String>{};
static const _modules = {
"aria2c": "0",
"unp4kc": "1",
};
static Future extractModule(List<String> modules, String workingDir) async {
for (var m in _modules.entries) {
@@ -16,8 +18,11 @@ class BinaryModuleConf {
final version = m.value;
final dir = "$workingDir\\$name";
final versionFile = File("$dir\\version");
if (kReleaseMode && await versionFile.exists() && (await versionFile.readAsString()).trim() == version) {
dPrint("BinaryModuleConf.extractModule skip $name version == $version");
if (kReleaseMode &&
await versionFile.exists() &&
(await versionFile.readAsString()).trim() == version) {
dPrint(
"BinaryModuleConf.extractModule skip $name version == $version");
continue;
}
// write model file

View File

@@ -1,9 +1,7 @@
import 'dart:io';
class ConstConf {
static const String appVersion = "3.1.0";
static const int appVersionCode = 80;
static const String appVersionDate = "2026-01-19";
static const String appVersion = "3.0.0";
static const int appVersionCode = 79;
static const String appVersionDate = "2025-12-23";
static const _gameChannels = ["LIVE", "4.0_PREVIEW", "PTU", "EPTU", "TECH-PREVIEW", "HOTFIX"];
static const isMSE = String.fromEnvironment("MSE", defaultValue: "false") == "true";
static const win32AppId = isMSE
@@ -20,12 +18,5 @@ class AppConf {
_networkGameChannels = channels;
}
static List<String> get gameChannels {
final baseChannels = _networkGameChannels ?? ConstConf._gameChannels;
// On Linux, add lowercase variants for case-sensitive filesystem
if (Platform.isLinux) {
return [...baseChannels, ...baseChannels.map((c) => c.toLowerCase())];
}
return baseChannels;
}
static List<String> get gameChannels => _networkGameChannels ?? ConstConf._gameChannels;
}

View File

@@ -1,19 +1,14 @@
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/common/io/doh_client.dart';
import 'package:starcitizen_doctor/common/rust/api/http_api.dart' as rust_http;
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/common/rust/http_package.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
class URLConf {
/// HOME API
static String gitApiHome = "https://git.scbox.xkeyc.cn";
static String newsApiHome = "https://scbox.citizenwiki.cn";
static const String analyticsApiHome = "https://scbox.org";
/// PartyRoom Server
// static const String partyRoomServerAddress = "localhost";
// static const int partyRoomServerPort = 50051;
static const String partyRoomServerAddress = "ecdn.partyroom.grpc.scbox.xkeyc.cn";
static const int partyRoomServerPort = 443;
static String gitApiHome = "https://ecdn.git.scbox.xkeyc.cn";
static String newsApiHome = "https://ecdn.news.scbox.xkeyc.cn";
static const String analyticsApiHome = "https://web-proxy.scbox.xkeyc.cn/analytics/analytics";
static bool isUrlCheckPass = false;
@@ -33,27 +28,21 @@ class URLConf {
static const feedbackUrl = "https://support.citizenwiki.cn/all";
static const feedbackFAQUrl = "https://support.citizenwiki.cn/t/sc-toolbox";
static String nav42KitUrl =
"https://payload.citizenwiki.cn/api/community-navs?sort=is_sponsored&depth=2&page=1&limit=1000";
"https://ecdn.42nav.xkeyc.cn/api/community-navs?sort=is_sponsored&depth=2&page=1&limit=1000";
static String get devReleaseUrl => "$gitApiHome/SCToolBox/Release/releases";
/// RSI Avatar Base URL
static const String rsiAvatarBaseUrl = "https://robertsspaceindustries.com";
static Future<bool> checkHost() async {
// 使用 DNS 获取可用列表
final gitApiList = _genFinalList(await dnsLookupTxt("git.dns.scbox.org"));
dPrint("DNS gitApiList ==== $gitApiList");
final fasterGit = await rust_http.getFasterUrl(
urls: gitApiList,
pathSuffix: "/SCToolBox/Api/raw/branch/main/sc_doctor/version.json",
);
final fasterGit = await getFasterUrl(gitApiList, "git");
dPrint("gitApiList.Faster ==== $fasterGit");
if (fasterGit != null) {
gitApiHome = fasterGit;
}
final newsApiList = _genFinalList(await dnsLookupTxt("news.dns.scbox.org"));
final fasterNews = await rust_http.getFasterUrl(urls: newsApiList, pathSuffix: "/api/latest");
final fasterNews = await getFasterUrl(newsApiList, "news");
dPrint("DNS newsApiList ==== $newsApiList");
dPrint("newsApiList.Faster ==== $fasterNews");
if (fasterNews != null) {
@@ -66,12 +55,53 @@ class URLConf {
static Future<List<String>> dnsLookupTxt(String host) async {
if (await Api.isUseInternalDNS()) {
dPrint("[URLConf] use internal DNS LookupTxt $host");
return rust_http.dnsLookupTxt(host: host);
return RSHttp.dnsLookupTxt(host);
}
dPrint("[URLConf] use DOH LookupTxt $host");
return (await DohClient.resolveTXT(host)) ?? [];
}
static Future<String?> getFasterUrl(List<String> urls, String mode) async {
String firstUrl = "";
int callLen = 0;
void onCall(RustHttpResponse? response, String url) {
callLen++;
if (response != null && response.statusCode == 200 && firstUrl.isEmpty) {
firstUrl = url;
}
}
for (var url in urls) {
var reqUrl = url;
switch (mode) {
case "git":
reqUrl = "$url/SCToolBox/Api/raw/branch/main/sc_doctor/version.json";
break;
case "news":
reqUrl = "$url/api/latest";
break;
}
RSHttp.head(reqUrl).then(
(resp) => onCall(resp, url),
onError: (err) {
callLen++;
dPrint("RSHttp.head error $err");
},
);
}
while (true) {
await Future.delayed(const Duration(milliseconds: 16));
if (firstUrl.isNotEmpty) {
return firstUrl;
}
if (callLen == urls.length && firstUrl.isEmpty) {
return null;
}
}
}
static List<String> _genFinalList(List<String> sList) {
List<String> list = [];
for (var ll in sList) {

View File

@@ -1,8 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:intl/intl.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/generated/l10n.dart';
/// 日志分析结果数据类
@@ -11,13 +7,11 @@ class LogAnalyzeLineData {
final String title;
final String? data;
final String? dateTime;
final String? tag; // 统计标签,用于定位日志(如 "game_start"),不依赖本地化
// 格式化后的字段
final String? victimId; // 受害者ID (actor_death)
final String? location; // 位置信息 (request_location_inventory)
final String? area; // 区域信息
final String? playerName; // 玩家名称 (player_login)
final String? tag;
final String? victimId;
final String? location;
final String? area;
final String? playerName;
const LogAnalyzeLineData({
required this.type,
@@ -47,7 +41,7 @@ class LogAnalyzeStatistics {
final int vehicleDestructionCountHard;
final DateTime? gameStartTime;
final int gameCrashLineNumber;
final String? latestLocation; // 最新位置信息(全量查找)
final String? latestLocation;
const LogAnalyzeStatistics({
required this.playerName,
@@ -66,7 +60,6 @@ class LogAnalyzeStatistics {
class GameLogAnalyzer {
static const String unknownValue = "<Unknown>";
// 正则表达式定义
static final _baseRegExp = RegExp(r'\[Notice\]\s+<([^>]+)>');
static final _gameLoadingRegExp = RegExp(
r'<[^>]+>\s+Loading screen for\s+(\w+)\s+:\s+SC_Frontend closed after\s+(\d+\.\d+)\s+seconds',
@@ -74,7 +67,6 @@ class GameLogAnalyzer {
static final _logDateTimeRegExp = RegExp(r'<(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)>');
static final DateFormat _dateTimeFormatter = DateFormat('yyyy-MM-dd HH:mm:ss:SSS');
// 致命碰撞解析
static final _fatalCollisionPatterns = {
'vehicle': RegExp(r'Fatal Collision occured for vehicle\s+(\S+)'),
'zone': RegExp(r'Zone:\s*([^,\]]+)'),
@@ -83,76 +75,37 @@ class GameLogAnalyzer {
'distance': RegExp(r'Distance:\s*([\d.]+)'),
};
// 载具损毁解析
static final _vehicleDestructionPattern = RegExp(
r"Vehicle\s+'([^']+)'.*?" // 载具型号
r"in zone\s+'([^']+)'.*?" // Zone
r"destroy level \d+ to (\d+).*?" // 损毁等级
r"caused by\s+'([^']+)'", // 责任方
r"Vehicle\s+'([^']+)'.*?"
r"in zone\s+'([^']+)'.*?"
r"destroy level \d+ to (\d+).*?"
r"caused by\s+'([^']+)'",
);
// 角色死亡解析
static final _actorDeathPattern = RegExp(
r"Actor '([^']+)'.*?" // 受害者ID
r"ejected from zone '([^']+)'.*?" // 原载具/区域
r"to zone '([^']+)'", // 目标区域
r"Actor '([^']+)'.*?"
r"ejected from zone '([^']+)'.*?"
r"to zone '([^']+)'",
);
// 角色名称解析
static final _characterNamePattern = RegExp(r"name\s+([^-]+)");
// 本地库存请求解析
static final _requestLocationInventoryPattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
// 载具控制解析
static final vehicleControlPattern = RegExp(r"granted control token for '([^']+)'\s+\[(\d+)\]");
/// 公开的日期时间解析方法,供其他模块使用
static DateTime? getLogLineDateTime(String line) => _getLogLineDateTime(line);
/// 公开的日期时间字符串解析方法
static String? getLogLineDateTimeString(String line) => _getLogLineDateTimeString(line);
/// 从载具名称中移除末尾的ID
/// 示例: ANVL_Hornet_F7A_Mk2_3467069517923 -> ANVL_Hornet_F7A_Mk2
static String removeVehicleId(String vehicleName) {
final regex = RegExp(r'_\d+$');
return vehicleName.replaceAll(regex, '');
}
/// 分析整个日志文件
///
/// [logFile] 日志文件
/// [startTime] 开始时间,如果提供则只统计此时间之后的数据
/// 返回日志分析结果列表和统计数据
static Future<(List<LogAnalyzeLineData>, LogAnalyzeStatistics)> analyzeLogFile(
File logFile, {
DateTime? startTime,
}) async {
if (!(await logFile.exists())) {
return (
[LogAnalyzeLineData(type: "error", title: S.current.log_analyzer_no_log_file)],
LogAnalyzeStatistics(
playerName: "",
killCount: 0,
deathCount: 0,
selfKillCount: 0,
vehicleDestructionCount: 0,
vehicleDestructionCountHard: 0,
gameCrashLineNumber: -1,
),
);
}
final logLines = utf8.decode((await logFile.readAsBytes()), allowMalformed: true).split("\n");
/// 从字符串内容分析日志
static (List<LogAnalyzeLineData>, LogAnalyzeStatistics) analyzeLogContent(String content, {DateTime? startTime}) {
final logLines = content.split("\n");
return _analyzeLogLines(logLines, startTime: startTime);
}
/// 分析日志行列表
///
/// [logLines] 日志行列表
/// [startTime] 开始时间,如果提供则只影响计数统计,不影响 gameStartTime 和位置的全量查找
/// 返回日志分析结果列表和统计数据
static (List<LogAnalyzeLineData>, LogAnalyzeStatistics) _analyzeLogLines(
List<String> logLines, {
DateTime? startTime,
@@ -164,16 +117,15 @@ class GameLogAnalyzer {
int selfKillCount = 0;
int vehicleDestructionCount = 0;
int vehicleDestructionCountHard = 0;
DateTime? gameStartTime; // 全量查找,不受 startTime 影响
String? latestLocation; // 全量查找最新位置
DateTime? gameStartTime;
String? latestLocation;
int gameCrashLineNumber = -1;
bool shouldCount = startTime == null; // 只影响计数
bool shouldCount = startTime == null;
for (var i = 0; i < logLines.length; i++) {
final line = logLines[i];
if (line.isEmpty) continue;
// 如果设置了 startTime检查当前行时间
if (startTime != null && !shouldCount) {
final lineTime = _getLogLineDateTime(line);
if (lineTime != null && lineTime.isAfter(startTime)) {
@@ -181,21 +133,13 @@ class GameLogAnalyzer {
}
}
// 处理游戏开始(全量查找第一次出现)
if (gameStartTime == null) {
gameStartTime = _getLogLineDateTime(line);
if (gameStartTime != null) {
results.add(
LogAnalyzeLineData(
type: "info",
title: S.current.log_analyzer_game_start,
tag: "game_start", // 使用 tag 标识,不依赖本地化
),
);
results.add(LogAnalyzeLineData(type: "info", title: S.current.log_analyzer_game_start, tag: "game_start"));
}
}
// 游戏加载时间
final gameLoading = _parseGameLoading(line);
if (gameLoading != null) {
results.add(
@@ -209,7 +153,6 @@ class GameLogAnalyzer {
continue;
}
// 基础事件解析
final baseEvent = _parseBaseEvent(line);
if (baseEvent != null) {
LogAnalyzeLineData? data;
@@ -217,7 +160,7 @@ class GameLogAnalyzer {
case "AccountLoginCharacterStatus_Character":
data = _parseCharacterName(line);
if (data != null && data.playerName != null) {
playerName = data.playerName!; // 全量更新玩家名称
playerName = data.playerName!;
}
break;
case "FatalCollision":
@@ -245,7 +188,7 @@ class GameLogAnalyzer {
case "RequestLocationInventory":
data = _parseRequestLocationInventory(line);
if (data != null && data.location != null) {
latestLocation = data.location; // 全量更新最新位置
latestLocation = data.location;
}
break;
}
@@ -255,7 +198,6 @@ class GameLogAnalyzer {
}
}
// 游戏关闭
if (line.contains("[CIG] CCIGBroker::FastShutdown")) {
results.add(
LogAnalyzeLineData(
@@ -267,28 +209,16 @@ class GameLogAnalyzer {
continue;
}
// 游戏崩溃
if (line.contains("Cloud Imperium Games public crash handler")) {
gameCrashLineNumber = i;
}
}
// 处理崩溃信息
if (gameCrashLineNumber > 0) {
final lastLineDateTime = gameStartTime != null
? _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20")))
: null;
final crashInfo = logLines.sublist(gameCrashLineNumber);
final info = SCLoggerHelper.getGameRunningLogInfo(crashInfo);
crashInfo.add(S.current.log_analyzer_one_click_diagnosis_header);
if (info != null) {
crashInfo.add(info.key);
if (info.value.isNotEmpty) {
crashInfo.add(S.current.log_analyzer_details_info(info.value));
}
} else {
crashInfo.add(S.current.log_analyzer_no_crash_detected);
}
results.add(
LogAnalyzeLineData(
type: "game_crash",
@@ -299,7 +229,6 @@ class GameLogAnalyzer {
);
}
// 添加统计信息
if (killCount > 0 || deathCount > 0) {
results.add(
LogAnalyzeLineData(
@@ -316,7 +245,6 @@ class GameLogAnalyzer {
);
}
// 统计游戏时长
if (gameStartTime != null) {
final lastLineDateTime = _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20"), orElse: () => ""));
if (lastLineDateTime != null) {
@@ -350,8 +278,6 @@ class GameLogAnalyzer {
return (results, statistics);
}
// ==================== 解析辅助方法 ====================
static String? _parseBaseEvent(String line) {
final match = _baseRegExp.firstMatch(line);
return match?.group(1);
@@ -464,7 +390,7 @@ class GameLogAnalyzer {
return LogAnalyzeLineData(
type: "actor_death",
title: S.current.log_analyzer_filter_character_death,
data: S.current.log_analyzer_death_details(victimId, fromZone, toZone),
data: S.current.log_analyzer_death_details(victimId, unknownValue, unknownValue, "$fromZone -> $toZone"),
dateTime: _getLogLineDateTimeString(line),
location: fromZone,
area: toZone,
@@ -483,7 +409,7 @@ class GameLogAnalyzer {
type: "player_login",
title: S.current.log_analyzer_player_login(characterName),
dateTime: _getLogLineDateTimeString(line),
playerName: characterName, // 格式化字段
playerName: characterName,
);
}
return null;
@@ -500,7 +426,7 @@ class GameLogAnalyzer {
title: S.current.log_analyzer_view_local_inventory,
data: S.current.log_analyzer_player_location(playerId, location),
dateTime: _getLogLineDateTimeString(line),
location: location, // 格式化字段
location: location,
);
}
return null;

View File

@@ -1,22 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hive_ce/hive.dart';
import 'package:starcitizen_doctor/common/conf/conf.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
class SCLoggerHelper {
static Future<String?> getLogFilePath() async {
if (!Platform.isWindows) {
final wineUserPath = await getWineUserPath();
if (wineUserPath == null) return null;
// /home/xkeyc/Games/star-citizen/drive_c/users/xkeyc/AppData/Roaming/rsilauncher/
final rsiLauncherPath = "$wineUserPath/AppData/Roaming/rsilauncher";
dPrint("rsiLauncherPath Wine:$rsiLauncherPath");
final jsonLogPath = "$rsiLauncherPath/logs/log.log";
return jsonLogPath;
}
if (kIsWeb || !Platform.isWindows) return null;
Map<String, String> envVars = Platform.environment;
final appDataPath = envVars["appdata"];
if (appDataPath == null) {
@@ -29,14 +21,6 @@ class SCLoggerHelper {
}
static Future<String?> getShaderCachePath() async {
if (!Platform.isWindows) {
final wineUserPath = await getWineUserPath();
if (wineUserPath == null) return null;
// /home/xkeyc/Games/star-citizen/drive_c/users/xkeyc/AppData/Local/star citizen/
final scCachePath = "$wineUserPath/AppData/Local/star citizen";
dPrint("getShaderCachePath Wine === $scCachePath");
return scCachePath;
}
Map<String, String> envVars = Platform.environment;
final appDataPath = envVars["LOCALAPPDATA"];
if (appDataPath == null) {
@@ -47,25 +31,8 @@ class SCLoggerHelper {
return scCachePath;
}
static Future<String?> getWineUserPath() async {
// get game path in hiveBox
final confBox = await Hive.openBox("app_conf");
final path = confBox.get("custom_game_path");
if (path?.isEmpty ?? true) return null;
// path eg: /home/xkeyc/Games/star-citizen/drive_c/Program Files/Roberts Space Industries/StarCitizen/LIVE/
// resolve wine c_drive path
final wineCDrivePath = path.toString().split('/drive_c/').first;
// scan wine user path == current_unix_user
final wineUserPath = "$wineCDrivePath/drive_c/users/${Platform.environment['USER']}";
// check exists
final wineUserDir = Directory(wineUserPath);
if (!await wineUserDir.exists()) return null;
dPrint("getWineUserPath === $wineUserPath");
return wineUserPath;
}
static Future<List?> getLauncherLogList() async {
if (!Platform.isWindows) return [];
if (kIsWeb || !Platform.isWindows) return [];
try {
final jsonLogPath = await getLogFilePath();
if (jsonLogPath == null) throw "no file path";
@@ -85,13 +52,9 @@ class SCLoggerHelper {
List<String> scInstallPaths = [];
checkAndAddPath(String path, bool checkExists) async {
// Handle JSON-escaped backslashes (\\\\) -> single backslash (\\)
path = path.replaceAll(r'\\', r'\');
// Normalize path separators to current platform format
path = path.platformPath;
// Case-insensitive check for existing paths
if (path.isNotEmpty && !scInstallPaths.any((p) => p.toLowerCase() == path.toLowerCase())) {
// 将所有连续的 \\ 替换为 \
path = path.replaceAll(RegExp(r'\\+'), "\\");
if (path.isNotEmpty && !scInstallPaths.contains(path)) {
if (!checkExists) {
dPrint("find installPath == $path");
scInstallPaths.add(path);
@@ -106,25 +69,13 @@ class SCLoggerHelper {
final path = confBox.get("custom_game_path");
if (path != null && path != "") {
for (var v in withVersion) {
await checkAndAddPath("$path\\$v".platformPath, checkExists);
await checkAndAddPath("$path\\$v", checkExists);
}
}
try {
for (var v in withVersion) {
// Platform-specific regex patterns for game install path detection
// Uses restrictive character class to avoid matching across JSON delimiters
String pattern;
if (Platform.isWindows) {
// Windows: Match paths like C:\...\StarCitizen\LIVE
// Path segments can only contain: letters, numbers, space, dot, underscore, hyphen, parentheses
// Handles both single backslash, forward slash, and JSON-escaped double backslash
pattern =
r'([a-zA-Z]:(?:[/\\]|\\\\)(?:[a-zA-Z0-9 ._()-]+(?:[/\\]|\\\\))*StarCitizen(?:[/\\]|\\\\)' + v + r')';
} else {
// Unix (Wine): Match paths like /home/user/.../StarCitizen/LIVE
pattern = r'(/(?:[a-zA-Z0-9 ._()-]+/)*StarCitizen/' + v + r')';
}
String pattern = r'([a-zA-Z]:\\\\[^\\\\]*\\\\[^\\\\]*\\\\StarCitizen\\\\' + v + r')';
RegExp regExp = RegExp(pattern, caseSensitive: false);
for (var i = listData.length - 1; i > 0; i--) {
final line = listData[i];
@@ -139,14 +90,9 @@ class SCLoggerHelper {
// 动态检测更多位置
for (var fileName in List.from(scInstallPaths)) {
for (var v in withVersion) {
final suffix = '\\$v'.platformPath.toLowerCase();
if (fileName.toString().toLowerCase().endsWith(suffix)) {
if (fileName.toString().endsWith(v)) {
for (var nv in withVersion) {
final basePath = fileName.toString().replaceAll(
RegExp('${RegExp.escape(suffix)}\$', caseSensitive: false),
'',
);
final nextName = "$basePath\\$nv".platformPath;
final nextName = "${fileName.toString().replaceAll("\\$v", "")}\\$nv";
await checkAndAddPath(nextName, true);
}
}
@@ -162,10 +108,9 @@ class SCLoggerHelper {
}
static String getGameChannelID(String installPath) {
final pathLower = installPath.platformPath.toLowerCase();
for (var value in AppConf.gameChannels) {
if (pathLower.endsWith('\\${value.toLowerCase()}'.platformPath)) {
return value.toUpperCase();
if (installPath.endsWith("\\$value")) {
return value;
}
}
return "UNKNOWN";

View File

@@ -1,36 +1,79 @@
import 'dart:io';
import 'package:hive_ce/hive.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/rust/api/win32_api.dart' as win32;
class SystemHelper {
static String powershellPath = "powershell.exe";
static Future<void> initPowershellPath() async {
try {
var result = await Process.run(powershellPath, ["echo", "pong"]);
if (!result.stdout.toString().startsWith("pong") && powershellPath == "powershell.exe") {
throw "powershell check failed";
}
} catch (e) {
Map<String, String> envVars = Platform.environment;
final systemRoot = envVars["SYSTEMROOT"];
if (systemRoot != null) {
final autoSearchPath = "$systemRoot\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
dPrint("auto search powershell path === $autoSearchPath");
powershellPath = autoSearchPath;
}
}
}
static Future<bool> checkNvmePatchStatus() async {
try {
return await win32.checkNvmePatchStatus();
var result = await Process.run(SystemHelper.powershellPath, [
"Get-ItemProperty",
"-Path",
"\"HKLM:\\SYSTEM\\CurrentControlSet\\Services\\stornvme\\Parameters\\Device\"",
"-Name",
"\"ForcedPhysicalSectorSizeInBytes\"",
]);
dPrint("checkNvmePatchStatus result ==== ${result.stdout}");
if (result.stderr == "" && result.stdout.toString().contains("{* 4095}")) {
return true;
} else {
return false;
}
} catch (e) {
dPrint("checkNvmePatchStatus error: $e");
return false;
}
}
static Future<String> addNvmePatch() async {
try {
await win32.addNvmePatch();
return "";
} catch (e) {
dPrint("addNvmePatch error: $e");
return e.toString();
}
var result = await Process.run(powershellPath, [
'New-ItemProperty',
"-Path",
"\"HKLM:\\SYSTEM\\CurrentControlSet\\Services\\stornvme\\Parameters\\Device\"",
"-Name",
"ForcedPhysicalSectorSizeInBytes",
"-PropertyType MultiString",
"-Force -Value",
"\"* 4095\"",
]);
dPrint("nvme_PhysicalBytes result == ${result.stdout}");
return result.stderr;
}
static Future<bool> doRemoveNvmePath() async {
try {
await win32.removeNvmePatch();
return true;
var result = await Process.run(powershellPath, [
"Clear-ItemProperty",
"-Path",
"\"HKLM:\\SYSTEM\\CurrentControlSet\\Services\\stornvme\\Parameters\\Device\"",
"-Name",
"\"ForcedPhysicalSectorSizeInBytes\"",
]);
dPrint("doRemoveNvmePath result ==== ${result.stdout}");
if (result.stderr == "") {
return true;
} else {
return false;
}
} catch (e) {
dPrint("doRemoveNvmePath error: $e");
return false;
}
}
@@ -42,7 +85,7 @@ class SystemHelper {
if (path != null && path != "") {
if (await File(path).exists()) {
if (skipEXE) {
return "${path.toString().replaceAll("\\RSI Launcher.exe".platformPath, "")}\\".platformPath;
return "${path.toString().replaceAll("\\RSI Launcher.exe", "")}\\";
}
return path;
}
@@ -54,37 +97,43 @@ class SystemHelper {
"$programDataPath\\Microsoft\\Windows\\Start Menu\\Programs\\Roberts Space Industries\\RSI Launcher.lnk";
final rsiLinkFile = File(rsiFilePath);
if (await rsiLinkFile.exists()) {
try {
final targetPath = await win32.resolveShortcut(lnkPath: rsiFilePath);
if (targetPath.contains("RSI Launcher.exe")) {
final start = targetPath.split("RSI Launcher.exe");
if (skipEXE) {
return start[0];
}
return "${start[0]}RSI Launcher.exe";
final r = await Process.run(SystemHelper.powershellPath, [
"(New-Object -ComObject WScript.Shell).CreateShortcut(\"$rsiFilePath\").targetpath",
]);
if (r.stdout.toString().contains("RSI Launcher.exe")) {
final start = r.stdout.toString().split("RSI Launcher.exe");
if (skipEXE) {
return start[0];
}
} catch (e) {
dPrint("resolveShortcut error: $e");
return "${start[0]}RSI Launcher.exe";
}
}
return "";
}
static Future<void> killRSILauncher() async {
var pList = await getPID("RSI Launcher");
for (var pid in pList) {
try {
Process.killPid(pid);
} catch (e) {
dPrint("killRSILauncher Error: $e");
var psr = await Process.run(powershellPath, ["ps", "\"RSI Launcher\"", "|select -expand id"]);
if (psr.stderr == "") {
for (var value in (psr.stdout ?? "").toString().split("\n")) {
dPrint(value);
if (value != "") {
Process.killPid(int.parse(value));
}
}
}
}
static Future<List<int>> getPID(String name) async {
static Future<List<String>> getPID(String name) async {
try {
final pList = await win32.getProcessListByName(processName: name);
return pList.map((e) => e.pid).toList();
final r = await Process.run(powershellPath, ["(ps $name).Id"]);
if (r.stderr.toString().trim().isNotEmpty) {
dPrint("getPID Error: ${r.stderr}");
return [];
}
final str = r.stdout.toString().trim();
dPrint("getPID result: $str");
if (str.isEmpty) return [];
return str.split("\n");
} catch (e) {
dPrint("getPID Error: $e");
return [];
@@ -105,78 +154,45 @@ class SystemHelper {
}
static Future<int> getSystemMemorySizeGB() async {
try {
final memoryGb = await win32.getSystemMemorySizeGb();
return memoryGb.toInt();
} catch (e) {
dPrint("getSystemMemorySizeGB error: $e");
return 0;
}
final r = await Process.run(powershellPath, [
"(Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property capacity -Sum).sum /1gb",
]);
return int.tryParse(r.stdout.toString().trim()) ?? 0;
}
static Future<String> getSystemCimInstance(String win32InstanceName, {pathName = "Name"}) async {
// This method is deprecated, use getSystemInfo() instead
try {
final sysInfo = await win32.getSystemInfo();
if (win32InstanceName.contains("OperatingSystem")) {
return sysInfo.osName;
} else if (win32InstanceName.contains("Processor")) {
return sysInfo.cpuName;
} else if (win32InstanceName.contains("VideoController")) {
return sysInfo.gpuInfo;
} else if (win32InstanceName.contains("DiskDrive")) {
return sysInfo.diskInfo;
}
} catch (e) {
dPrint("getSystemCimInstance error: $e");
}
return "";
final r = await Process.run(powershellPath, ["(Get-CimInstance $win32InstanceName).$pathName"]);
return r.stdout.toString().trim();
}
static Future<String> getSystemName() async {
try {
final sysInfo = await win32.getSystemInfo();
return sysInfo.osName;
} catch (e) {
dPrint("getSystemName error: $e");
return "";
}
final r = await Process.run(powershellPath, ["(Get-ComputerInfo | Select-Object -expand OsName)"]);
return r.stdout.toString().trim();
}
static Future<String> getCpuName() async {
try {
final sysInfo = await win32.getSystemInfo();
return sysInfo.cpuName;
} catch (e) {
dPrint("getCpuName error: $e");
return "";
}
final r = await Process.run(powershellPath, ["(Get-WmiObject -Class Win32_Processor).Name"]);
return r.stdout.toString().trim();
}
static Future<String> getGpuInfo() async {
try {
// Try registry first for more accurate VRAM info
final regInfo = await win32.getGpuInfoFromRegistry();
if (regInfo.isNotEmpty) {
return regInfo;
}
// Fallback to WMI
final sysInfo = await win32.getSystemInfo();
return sysInfo.gpuInfo;
} catch (e) {
dPrint("getGpuInfo error: $e");
return "";
}
const cmd = r"""
$adapterMemory = (Get-ItemProperty -Path "HKLM:\SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0*" -Name "HardwareInformation.AdapterString", "HardwareInformation.qwMemorySize" -Exclude PSPath -ErrorAction SilentlyContinue)
foreach ($adapter in $adapterMemory) {
[PSCustomObject] @{
Model=$adapter."HardwareInformation.AdapterString"
"VRAM (GB)"=[math]::round($adapter."HardwareInformation.qwMemorySize"/1GB)
}
}
""";
final r = await Process.run(powershellPath, [cmd]);
return r.stdout.toString().trim();
}
static Future<String> getDiskInfo() async {
try {
final sysInfo = await win32.getSystemInfo();
return sysInfo.diskInfo;
} catch (e) {
dPrint("getDiskInfo error: $e");
return "";
}
return (await Process.run(powershellPath, [
"Get-PhysicalDisk | format-table BusType,FriendlyName,Size",
])).stdout.toString().trim();
}
static Future<int> getDirLen(String path, {List<String>? skipPath}) async {
@@ -203,12 +219,11 @@ class SystemHelper {
}
static Future<int> getNumberOfLogicalProcessors() async {
try {
return await win32.getNumberOfLogicalProcessors();
} catch (e) {
dPrint("getNumberOfLogicalProcessors error: $e");
return 0;
}
final cpuNumberResult = await Process.run(powershellPath, [
"(Get-WmiObject -Class Win32_Processor).NumberOfLogicalProcessors",
]);
if (cpuNumberResult.exitCode != 0) return 0;
return int.tryParse(cpuNumberResult.stdout.toString().trim()) ?? 0;
}
static Future<String?> getCpuAffinity() async {
@@ -236,11 +251,10 @@ class SystemHelper {
static Future openDir(dynamic path, {bool isFile = false}) async {
dPrint("SystemHelper.openDir path === $path");
if (Platform.isWindows) {
try {
await win32.openDirWithExplorer(path: path.toString(), isFile: isFile);
} catch (e) {
dPrint("openDir error: $e");
}
await Process.run(SystemHelper.powershellPath, [
"explorer.exe",
isFile ? "/select,$path" : "\"/select,\"$path\"\"",
]);
}
}

View File

@@ -1,68 +1,65 @@
import 'dart:io';
import 'dart:convert';
import 'dart:isolate';
import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart';
/// 年度报告数据类
class YearlyReportData {
// 基础统计
final int totalLaunchCount; // 累计启动次数
final Duration totalPlayTime; // 累计游玩时长
final int yearlyLaunchCount; // 年度启动次数
final Duration yearlyPlayTime; // 年度游玩时长
final int totalCrashCount; // 总崩溃次数
final int yearlyCrashCount; // 年度崩溃次数
final int totalLaunchCount;
final Duration totalPlayTime;
final int yearlyLaunchCount;
final Duration yearlyPlayTime;
final int totalCrashCount;
final int yearlyCrashCount;
// 时间统计
final DateTime? yearlyFirstLaunchTime; // 年度第一次启动时间
final DateTime? earliestPlayDate; // 年度游玩最早的一天 (05:00及以后)
final DateTime? latestPlayDate; // 年度游玩最晚的一天 (04:00及以前)
final DateTime? yearlyFirstLaunchTime;
final DateTime? earliestPlayDate;
final DateTime? latestPlayDate;
// 游玩时长统计
final Duration? longestSession; // 最长单次游玩时长
final DateTime? longestSessionDate; // 最长游玩那一天
final Duration? shortestSession; // 最短单次游玩时长 (超过5分钟的)
final DateTime? shortestSessionDate; // 最短游玩那一天
final Duration? averageSessionTime; // 平均单次游玩时长
final Duration? longestSession;
final DateTime? longestSessionDate;
final Duration? shortestSession;
final DateTime? shortestSessionDate;
final Duration? averageSessionTime;
// 载具统计
final int yearlyVehicleDestructionCount; // 年度炸船次数
final String? mostDestroyedVehicle; // 年度炸的最多的船
final int mostDestroyedVehicleCount; // 炸的最多的船的次数
final String? mostPilotedVehicle; // 年度最爱驾驶的载具
final int mostPilotedVehicleCount; // 驾驶次数
final int yearlyVehicleDestructionCount;
final String? mostDestroyedVehicle;
final int mostDestroyedVehicleCount;
final String? mostPilotedVehicle;
final int mostPilotedVehicleCount;
// 账号统计
final int accountCount; // 账号数量
final String? mostPlayedAccount; // 游玩最多的账号
final int mostPlayedAccountSessionCount; // 游玩最多的账号的会话次数
final int accountCount;
final String? mostPlayedAccount;
final int mostPlayedAccountSessionCount;
// 地点统计
final List<MapEntry<String, int>> topLocations; // Top 地点访问统计
final List<MapEntry<String, int>> topLocations;
// 击杀统计 (K/D)
final int yearlyKillCount; // 年度击杀次数
final int yearlyDeathCount; // 年度死亡次数
final int yearlySelfKillCount; // 年度自杀次数
final int yearlyKillCount;
final int yearlyDeathCount;
final int yearlySelfKillCount;
// 月份统计
final int? mostPlayedMonth; // 游玩最多的月份 (1-12)
final int mostPlayedMonthCount; // 该月游玩次数
final int? leastPlayedMonth; // 游玩最少的月份 (1-12, 不包括完全没上游戏的月份)
final int leastPlayedMonthCount; // 该月游玩次数
final int? mostPlayedMonth;
final int mostPlayedMonthCount;
final int? leastPlayedMonth;
final int leastPlayedMonthCount;
// 连续游玩/离线统计
final int longestPlayStreak; // 最长连续游玩天数
final DateTime? playStreakStartDate; // 连续游玩开始日期
final DateTime? playStreakEndDate; // 连续游玩结束日期
final int longestOfflineStreak; // 最长连续离线天数
final DateTime? offlineStreakStartDate; // 连续离线开始日期
final DateTime? offlineStreakEndDate; // 连续离线结束日期
final int longestPlayStreak;
final DateTime? playStreakStartDate;
final DateTime? playStreakEndDate;
final int longestOfflineStreak;
final DateTime? offlineStreakStartDate;
final DateTime? offlineStreakEndDate;
// 详细数据 (用于展示)
final Map<String, int> vehiclePilotedDetails; // 驾驶载具详情
final Map<String, int> accountSessionDetails; // 账号会话详情
final Map<String, int> locationDetails; // 地点访问详情
// 详细数据
final Map<String, int> vehiclePilotedDetails;
final Map<String, int> accountSessionDetails;
final Map<String, int> locationDetails;
const YearlyReportData({
required this.totalLaunchCount,
@@ -106,79 +103,54 @@ class YearlyReportData {
required this.locationDetails,
});
/// 将 DateTime 转换为 UTC 毫秒时间戳
static int? _toUtcTimestamp(DateTime? dateTime) {
if (dateTime == null) return null;
return dateTime.toUtc().millisecondsSinceEpoch;
}
/// 转换为 JSON Map
///
/// 时间字段使用 UTC 毫秒时间戳 (int),配合 timezoneOffsetMinutes 可在客户端还原本地时间
Map<String, dynamic> toJson() {
final now = DateTime.now();
final offset = now.timeZoneOffset;
return {
// 元数据
'generatedAtUtc': _toUtcTimestamp(now),
'timezoneOffsetMinutes': offset.inMinutes,
// 基础统计
'totalLaunchCount': totalLaunchCount,
'totalPlayTimeMs': totalPlayTime.inMilliseconds,
'yearlyLaunchCount': yearlyLaunchCount,
'yearlyPlayTimeMs': yearlyPlayTime.inMilliseconds,
'totalCrashCount': totalCrashCount,
'yearlyCrashCount': yearlyCrashCount,
// 时间统计 (UTC 毫秒时间戳)
'yearlyFirstLaunchTimeUtc': _toUtcTimestamp(yearlyFirstLaunchTime),
'earliestPlayDateUtc': _toUtcTimestamp(earliestPlayDate),
'latestPlayDateUtc': _toUtcTimestamp(latestPlayDate),
// 游玩时长统计
'longestSessionMs': longestSession?.inMilliseconds,
'longestSessionDateUtc': _toUtcTimestamp(longestSessionDate),
'shortestSessionMs': shortestSession?.inMilliseconds,
'shortestSessionDateUtc': _toUtcTimestamp(shortestSessionDate),
'averageSessionTimeMs': averageSessionTime?.inMilliseconds,
// 载具统计
'yearlyVehicleDestructionCount': yearlyVehicleDestructionCount,
'mostDestroyedVehicle': mostDestroyedVehicle,
'mostDestroyedVehicleCount': mostDestroyedVehicleCount,
'mostPilotedVehicle': mostPilotedVehicle,
'mostPilotedVehicleCount': mostPilotedVehicleCount,
// 账号统计
'accountCount': accountCount,
'mostPlayedAccount': mostPlayedAccount,
'mostPlayedAccountSessionCount': mostPlayedAccountSessionCount,
// 地点统计
'topLocations': topLocations.map((e) => {'location': e.key, 'count': e.value}).toList(),
// 击杀统计
'yearlyKillCount': yearlyKillCount,
'yearlyDeathCount': yearlyDeathCount,
'yearlySelfKillCount': yearlySelfKillCount,
// 月份统计
'mostPlayedMonth': mostPlayedMonth,
'mostPlayedMonthCount': mostPlayedMonthCount,
'leastPlayedMonth': leastPlayedMonth,
'leastPlayedMonthCount': leastPlayedMonthCount,
// 连续游玩/离线统计
'longestPlayStreak': longestPlayStreak,
'playStreakStartDateUtc': _toUtcTimestamp(playStreakStartDate),
'playStreakEndDateUtc': _toUtcTimestamp(playStreakEndDate),
'longestOfflineStreak': longestOfflineStreak,
'offlineStreakStartDateUtc': _toUtcTimestamp(offlineStreakStartDate),
'offlineStreakEndDateUtc': _toUtcTimestamp(offlineStreakEndDate),
// 详细数据
'vehiclePilotedDetails': vehiclePilotedDetails,
'accountSessionDetails': accountSessionDetails,
'locationDetails': locationDetails,
@@ -220,25 +192,15 @@ class _LogFileStats {
int selfKillCount = 0;
Set<String> playerNames = {};
String? currentPlayerName;
String? firstPlayerName; // 第一个检测到的玩家名,用于去重
String? firstPlayerName;
// 载具损毁: 载具型号 (去除ID后) -> 次数
Map<String, int> vehicleDestruction = {};
// 驾驶载具: 载具型号 (去除ID后) -> 次数
Map<String, int> vehiclePiloted = {};
// 地点访问: 地点名 -> 次数
Map<String, int> locationVisits = {};
// 上次记录死亡的时间 (用于 2s 内去重)
DateTime? _lastDeathTime;
// 年度内的会话记录
List<_SessionInfo> yearlySessions = [];
/// 生成用于去重的唯一标识
/// 基于启动时间和第一个玩家名生成
String? get uniqueKey {
if (startTime == null) return null;
final timeKey = startTime!.toUtc().toIso8601String();
@@ -257,40 +219,33 @@ class _SessionInfo {
Duration get duration => endTime.difference(startTime);
}
/// 年度报告分析器
/// 年度报告分析器 (Web 版本)
class YearlyReportAnalyzer {
static final _characterNamePattern = RegExp(r'name\s+([^-]+)');
static final _vehicleDestructionPattern = RegExp(
r"Vehicle\s+'([^']+)'.*?" // 载具型号
r"in zone\s+'([^']+)'.*?" // Zone
r"destroy level \d+ to (\d+).*?" // 损毁等级
r"caused by\s+'([^']+)'", // 责任方
r"Vehicle\s+'([^']+)'.*?"
r"in zone\s+'([^']+)'.*?"
r"destroy level \d+ to (\d+).*?"
r"caused by\s+'([^']+)'",
);
static final _actorDeathPattern = RegExp(
r"Actor '([^']+)'.*?" // 受害者ID
r"ejected from zone '([^']+)'.*?" // 原载具/区域
r"to zone '([^']+)'", // 目标区域
r"Actor '([^']+)'.*?"
r"ejected from zone '([^']+)'.*?"
r"to zone '([^']+)'",
);
// Legacy 格式的正则表达式 (旧版日志)
static final _legacyActorDeathPattern = RegExp(
r"CActor::Kill: '([^']+)'.*?" // 受害者ID
r"in zone '([^']+)'.*?" // 死亡位置区域
r"killed by '([^']+)'.*?" // 击杀者ID
r"with damage type '([^']+)'", // 伤害类型
r"CActor::Kill: '([^']+)'.*?"
r"in zone '([^']+)'.*?"
r"killed by '([^']+)'.*?"
r"with damage type '([^']+)'",
);
static final _requestLocationInventoryPattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
/// 分析单个日志文件
static Future<_LogFileStats> _analyzeLogFile(File logFile, int targetYear) async {
/// 分析单个日志文件内容
static _LogFileStats _analyzeLogContent(String content, int targetYear) {
final stats = _LogFileStats();
try {
if (!(await logFile.exists())) {
return stats;
}
final content = utf8.decode(await logFile.readAsBytes(), allowMalformed: true);
final lines = content.split('\n');
for (final line in lines) {
@@ -298,22 +253,18 @@ class YearlyReportAnalyzer {
final lineTime = GameLogAnalyzer.getLogLineDateTime(line);
// 记录开始时间 (第一个有效时间)
if (stats.startTime == null && lineTime != null) {
stats.startTime = lineTime;
}
// 更新结束时间 (最后一个有效时间)
if (lineTime != null) {
stats.endTime = lineTime;
}
// 检测崩溃
if (line.contains("Cloud Imperium Games public crash handler")) {
stats.hasCrash = true;
}
// 检测玩家登录
if (line.contains('AccountLoginCharacterStatus_Character')) {
final nameMatch = _characterNamePattern.firstMatch(line);
if (nameMatch != null) {
@@ -325,7 +276,6 @@ class YearlyReportAnalyzer {
!playerName.contains(r'\\') &&
!playerName.contains('.')) {
stats.currentPlayerName = playerName;
// 去重添加到玩家列表 (忽略大小写)
if (!stats.playerNames.any((n) => n.toLowerCase() == playerName.toLowerCase())) {
stats.playerNames.add(playerName);
}
@@ -334,9 +284,7 @@ class YearlyReportAnalyzer {
}
}
// 年度内的统计
if (lineTime != null && lineTime.year == targetYear) {
// 检测载具损毁
final destructionMatch = _vehicleDestructionPattern.firstMatch(line);
if (destructionMatch != null) {
final vehicleModel = destructionMatch.group(1);
@@ -351,25 +299,21 @@ class YearlyReportAnalyzer {
}
}
// 检测驾驶载具
final controlMatch = GameLogAnalyzer.vehicleControlPattern.firstMatch(line);
if (controlMatch != null) {
final vehicleName = controlMatch.group(1);
if (vehicleName != null) {
final cleanVehicleName = GameLogAnalyzer.removeVehicleId(vehicleName);
// 过滤掉名为 "Default" 的载具
if (cleanVehicleName != 'Default') {
stats.vehiclePiloted[cleanVehicleName] = (stats.vehiclePiloted[cleanVehicleName] ?? 0) + 1;
}
}
}
// 检测死亡 (新版格式)
var deathMatch = _actorDeathPattern.firstMatch(line);
if (deathMatch != null) {
final victimId = deathMatch.group(1)?.trim();
if (victimId != null && stats.currentPlayerName != null && victimId == stats.currentPlayerName) {
// 防抖去重 (2秒内不重复计数)
if (stats._lastDeathTime == null || lineTime.difference(stats._lastDeathTime!).abs().inSeconds > 2) {
stats.deathCount++;
stats._lastDeathTime = lineTime;
@@ -377,7 +321,6 @@ class YearlyReportAnalyzer {
}
}
// 检测死亡 (旧版格式 - Legacy)
final legacyDeathMatch = _legacyActorDeathPattern.firstMatch(line);
if (legacyDeathMatch != null) {
final victimId = legacyDeathMatch.group(1)?.trim();
@@ -387,34 +330,24 @@ class YearlyReportAnalyzer {
bool isRecent =
stats._lastDeathTime != null && lineTime.difference(stats._lastDeathTime!).abs().inSeconds <= 2;
// 检测自杀
// 自杀逻辑selfKillCount 独立统计自杀次数
// deathCount 包含所有死亡(普通死亡+自杀),因此自杀时不再从 deathCount 回退
if (victimId == killerId) {
if (victimId == stats.currentPlayerName) {
if (isRecent) {
// 如果最近已经记录过一次死亡 (通用格式记录的),说明已经在 deathCount 中计入
// 只需额外标记为自杀
stats.selfKillCount++;
// 更新时间以保持锁定
stats._lastDeathTime = lineTime;
} else {
// 没有被新版格式记录过,需要同时计入 deathCount 和 selfKillCount
stats.deathCount++;
stats.selfKillCount++;
stats._lastDeathTime = lineTime;
}
}
} else {
// 检测死亡 (被击杀)
if (victimId == stats.currentPlayerName) {
// 如果最近已经记录过 (可能是通用格式),则认为是同一事件,忽略
if (!isRecent) {
stats.deathCount++;
stats._lastDeathTime = lineTime;
}
}
// 检测击杀 (杀别人)
if (killerId == stats.currentPlayerName) {
stats.killCount++;
}
@@ -422,12 +355,10 @@ class YearlyReportAnalyzer {
}
}
// 检测地点访问 (RequestLocationInventory)
final locationMatch = _requestLocationInventoryPattern.firstMatch(line);
if (locationMatch != null) {
final location = locationMatch.group(2)?.trim();
if (location != null && location.isNotEmpty) {
// 清理地点名称移除数字ID后缀
final cleanLocation = _cleanLocationName(location);
stats.locationVisits[cleanLocation] = (stats.locationVisits[cleanLocation] ?? 0) + 1;
}
@@ -435,107 +366,45 @@ class YearlyReportAnalyzer {
}
}
// 记录会话信息
if (stats.startTime != null && stats.endTime != null && stats.startTime!.year == targetYear) {
stats.yearlySessions.add(_SessionInfo(startTime: stats.startTime!, endTime: stats.endTime!));
}
} catch (e) {
// Error handled silently in isolate
// Error handled silently
}
return stats;
}
/// 清理地点名称移除数字ID后缀
static String _cleanLocationName(String location) {
// 移除末尾的数字ID (如 "_12345678")
final cleanPattern = RegExp(r'_\d{6,}$');
return location.replaceAll(cleanPattern, '');
}
/// 生成年度报告
///
/// [gameInstallPaths] 游戏安装路径列表 (完整路径,如 ["D:/Games/StarCitizen/LIVE", "D:/Games/StarCitizen/PTU"])
/// [targetYear] 目标年份
///
/// 该方法在独立 Isolate 中运行,避免阻塞 UI
static Future<YearlyReportData> generateReport(List<String> gameInstallPaths, int targetYear) async {
// 在独立 Isolate 中运行以避免阻塞 UI
return await Isolate.run(() async {
return await _generateReportInIsolate(gameInstallPaths, targetYear);
});
/// 从日志文件内容列表生成年度报告 (Web 版本)
static Future<YearlyReportData> generateReportFromContents(List<String> logContents, int targetYear) async {
final allStats = <_LogFileStats>[];
final seenKeys = <String>{};
for (final content in logContents) {
try {
final stats = _analyzeLogContent(content, targetYear);
final key = stats.uniqueKey;
if (key == null) {
allStats.add(stats);
} else if (!seenKeys.contains(key)) {
seenKeys.add(key);
allStats.add(stats);
}
} catch (_) {
// 忽略单个文件分析错误
}
}
return _generateReportFromStats(allStats, targetYear);
}
/// 内部方法:在 Isolate 中执行的报告生成逻辑
static Future<YearlyReportData> _generateReportInIsolate(List<String> gameInstallPaths, int targetYear) async {
final List<File> allLogFiles = [];
// 从所有安装路径收集日志文件
for (final installPath in gameInstallPaths) {
try {
final installDir = Directory(installPath);
// 检查安装目录是否存在
if (!await installDir.exists()) {
continue;
}
final gameLogFile = File('$installPath/Game.log');
final logBackupsDir = Directory('$installPath/logbackups');
// 添加当前 Game.log
try {
if (await gameLogFile.exists()) {
allLogFiles.add(gameLogFile);
}
} catch (_) {
// 忽略单个文件检查错误
}
// 添加备份日志
try {
if (await logBackupsDir.exists()) {
await for (final entity in logBackupsDir.list()) {
if (entity is File && entity.path.endsWith('.log')) {
allLogFiles.add(entity);
}
}
}
} catch (_) {
// 忽略备份目录读取错误
}
} catch (_) {
// 忽略单个安装路径的错误,继续处理其他路径
continue;
}
}
// 并发分析所有日志文件,使用错误处理确保单个文件失败不影响其他文件
final futures = allLogFiles.map((file) async {
try {
return await _analyzeLogFile(file, targetYear);
} catch (_) {
// 单个文件分析失败时返回空的统计数据
return _LogFileStats();
}
});
final allStatsRaw = await Future.wait(futures);
// 去重: 使用 uniqueKey (启动时间 + 玩家名) 来过滤重复的日志
final seenKeys = <String>{};
final allStats = <_LogFileStats>[];
for (final stats in allStatsRaw) {
final key = stats.uniqueKey;
if (key == null) {
allStats.add(stats);
} else if (!seenKeys.contains(key)) {
seenKeys.add(key);
allStats.add(stats);
}
}
// 合并统计数据
static YearlyReportData _generateReportFromStats(List<_LogFileStats> allStats, int targetYear) {
int totalLaunchCount = allStats.length;
Duration totalPlayTime = Duration.zero;
int yearlyLaunchCount = 0;
@@ -546,14 +415,12 @@ class YearlyReportAnalyzer {
DateTime? earliestPlayDate;
DateTime? latestPlayDate;
// 会话时长统计
Duration? longestSession;
DateTime? longestSessionDate;
Duration? shortestSession;
DateTime? shortestSessionDate;
List<Duration> allSessionDurations = [];
// K/D 统计
int yearlyKillCount = 0;
int yearlyDeathCount = 0;
int yearlySelfKillCount = 0;
@@ -564,12 +431,10 @@ class YearlyReportAnalyzer {
final Map<String, int> locationDetails = {};
for (final stats in allStats) {
// 累计游玩时长
if (stats.startTime != null && stats.endTime != null) {
totalPlayTime += stats.endTime!.difference(stats.startTime!);
}
// 崩溃统计
if (stats.hasCrash) {
totalCrashCount++;
if (stats.endTime != null && stats.endTime!.year == targetYear) {
@@ -577,39 +442,33 @@ class YearlyReportAnalyzer {
}
}
// 年度会话统计
for (final session in stats.yearlySessions) {
yearlyLaunchCount++;
final sessionDuration = session.duration;
yearlyPlayTime += sessionDuration;
allSessionDurations.add(sessionDuration);
// 年度第一次启动时间
if (yearlyFirstLaunchTime == null || session.startTime.isBefore(yearlyFirstLaunchTime)) {
yearlyFirstLaunchTime = session.startTime;
}
// 最早游玩的一天 (05:00及以后开始游戏)
if (session.startTime.hour >= 5) {
if (earliestPlayDate == null || _timeOfDayIsEarlier(session.startTime, earliestPlayDate)) {
earliestPlayDate = session.startTime;
}
}
// 最晚游玩的一天 (04:00及以前结束游戏)
if (session.endTime.hour <= 4) {
if (latestPlayDate == null || _timeOfDayIsLater(session.endTime, latestPlayDate)) {
latestPlayDate = session.endTime;
}
}
// 最长游玩时长
if (longestSession == null || sessionDuration > longestSession) {
longestSession = sessionDuration;
longestSessionDate = session.startTime;
}
// 最短游玩时长 (超过5分钟的)
if (sessionDuration.inMinutes >= 5) {
if (shortestSession == null || sessionDuration < shortestSession) {
shortestSession = sessionDuration;
@@ -618,28 +477,23 @@ class YearlyReportAnalyzer {
}
}
// 合并载具损毁详情 (过滤包含 PU 的载具)
for (final entry in stats.vehicleDestruction.entries) {
if (!entry.key.contains('PU_')) {
vehicleDestructionDetails[entry.key] = (vehicleDestructionDetails[entry.key] ?? 0) + entry.value;
}
}
// 合并驾驶载具详情
for (final entry in stats.vehiclePiloted.entries) {
vehiclePilotedDetails[entry.key] = (vehiclePilotedDetails[entry.key] ?? 0) + entry.value;
}
// 累计 K/D
yearlyKillCount += stats.killCount;
yearlyDeathCount += stats.deathCount;
yearlySelfKillCount += stats.selfKillCount;
// 合并账号会话详情
for (final playerName in stats.playerNames) {
if (playerName.length > 16) continue;
String targetKey = playerName;
// 查找是否存在忽略大小写的相同 key
for (final key in accountSessionDetails.keys) {
if (key.toLowerCase() == playerName.toLowerCase()) {
targetKey = key;
@@ -649,20 +503,17 @@ class YearlyReportAnalyzer {
accountSessionDetails[targetKey] = (accountSessionDetails[targetKey] ?? 0) + 1;
}
// 合并地点访问详情
for (final entry in stats.locationVisits.entries) {
locationDetails[entry.key] = (locationDetails[entry.key] ?? 0) + entry.value;
}
}
// 计算平均游玩时长
Duration? averageSessionTime;
if (allSessionDurations.isNotEmpty) {
final totalMs = allSessionDurations.fold<int>(0, (sum, d) => sum + d.inMilliseconds);
averageSessionTime = Duration(milliseconds: totalMs ~/ allSessionDurations.length);
}
// 计算派生统计
final yearlyVehicleDestructionCount = vehicleDestructionDetails.values.fold(0, (a, b) => a + b);
String? mostDestroyedVehicle;
@@ -692,19 +543,16 @@ class YearlyReportAnalyzer {
}
}
// 计算 Top 10 地点
final sortedLocations = locationDetails.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
final topLocations = sortedLocations.take(10).toList();
// 计算月份统计
final Map<int, int> monthlyPlayCount = {};
final Set<DateTime> playDates = {}; // 所有游玩的日期 (仅日期部分)
final Set<DateTime> playDates = {};
for (final stats in allStats) {
for (final session in stats.yearlySessions) {
final month = session.startTime.month;
monthlyPlayCount[month] = (monthlyPlayCount[month] ?? 0) + 1;
// 记录游玩日期 (只保留年月日)
playDates.add(DateTime(session.startTime.year, session.startTime.month, session.startTime.day));
}
}
@@ -715,14 +563,12 @@ class YearlyReportAnalyzer {
int leastPlayedMonthCount = 0;
if (monthlyPlayCount.isNotEmpty) {
// 最多游玩的月份
for (final entry in monthlyPlayCount.entries) {
if (entry.value > mostPlayedMonthCount) {
mostPlayedMonth = entry.key;
mostPlayedMonthCount = entry.value;
}
}
// 最少游玩的月份 (不包括完全没上游戏的月份)
leastPlayedMonthCount = monthlyPlayCount.values.first;
for (final entry in monthlyPlayCount.entries) {
if (entry.value <= leastPlayedMonthCount) {
@@ -732,7 +578,6 @@ class YearlyReportAnalyzer {
}
}
// 计算连续游玩天数和连续离线天数
int longestPlayStreak = 0;
DateTime? playStreakStartDate;
DateTime? playStreakEndDate;
@@ -741,10 +586,8 @@ class YearlyReportAnalyzer {
DateTime? offlineStreakEndDate;
if (playDates.isNotEmpty) {
// 将日期排序
final sortedDates = playDates.toList()..sort();
// 计算连续游玩天数
int currentStreak = 1;
DateTime streakStart = sortedDates.first;
@@ -762,14 +605,12 @@ class YearlyReportAnalyzer {
streakStart = sortedDates[i];
}
}
// 检查最后一段连续
if (currentStreak > longestPlayStreak) {
longestPlayStreak = currentStreak;
playStreakStartDate = streakStart;
playStreakEndDate = sortedDates.last;
}
// 计算连续离线天数 (在游玩日期之间的间隔)
for (int i = 1; i < sortedDates.length; i++) {
final gapDays = sortedDates[i].difference(sortedDates[i - 1]).inDays - 1;
if (gapDays > longestOfflineStreak) {
@@ -823,14 +664,12 @@ class YearlyReportAnalyzer {
);
}
/// 比较两个时间的 时:分 是否更早
static bool _timeOfDayIsEarlier(DateTime a, DateTime b) {
if (a.hour < b.hour) return true;
if (a.hour > b.hour) return false;
return a.minute < b.minute;
}
/// 比较两个时间的 时:分 是否更晚
static bool _timeOfDayIsLater(DateTime a, DateTime b) {
if (a.hour > b.hour) return true;
if (a.hour < b.hour) return false;

View File

@@ -1,17 +1,24 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:starcitizen_doctor/common/conf/conf.dart';
import 'package:starcitizen_doctor/common/rust/api/http_api.dart' as rust_http;
import 'package:starcitizen_doctor/common/rust/api/http_api.dart';
import 'package:starcitizen_doctor/common/rust/http_package.dart';
class RSHttp {
static late Dio _dio;
static Map<String, String> _defaultHeaders = {};
static Future<void> init() async {
await rust_http.setDefaultHeader(headers: {
"User-Agent":
"SCToolBox/${ConstConf.appVersion} (${ConstConf.appVersionCode})${ConstConf.isMSE ? "" : " DEV"} RSHttp"
});
_defaultHeaders = {};
_dio = Dio(
BaseOptions(
headers: _defaultHeaders,
responseType: ResponseType.bytes,
validateStatus: (status) => true, // 接受所有状态码
),
);
}
static Future<RustHttpResponse> get(
@@ -20,14 +27,13 @@ class RSHttp {
String? withIpAddress,
bool? withCustomDns,
}) async {
final r = await rust_http.fetch(
method: MyMethod.gets,
return await _fetch(
method: 'GET',
url: url,
headers: headers,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns,
);
return r;
}
static Future<String> getText(
@@ -36,10 +42,7 @@ class RSHttp {
String? withIpAddress,
bool? withCustomDns,
}) async {
final r = await get(url,
headers: headers,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns);
final r = await get(url, headers: headers, withIpAddress: withIpAddress, withCustomDns: withCustomDns);
if (r.data == null) return "";
final str = utf8.decode(r.data!);
return str;
@@ -57,15 +60,14 @@ class RSHttp {
headers ??= {};
headers["Content-Type"] = contentType;
}
final r = await rust_http.fetch(
method: MyMethod.post,
return await _fetch(
method: 'POST',
url: url,
headers: headers,
inputData: data,
data: data,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns,
);
return r;
}
static Future<RustHttpResponse> head(
@@ -74,21 +76,81 @@ class RSHttp {
String? withIpAddress,
bool? withCustomDns,
}) async {
final r = await rust_http.fetch(
method: MyMethod.head,
return await _fetch(
method: 'HEAD',
url: url,
headers: headers,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns,
);
return r;
}
static Future<List<String>> dnsLookupTxt(String host) async {
return await rust_http.dnsLookupTxt(host: host);
// TXT 记录查询在 Web 平台上无法实现,返回空列表
return [];
}
static Future<List<String>> dnsLookupIps(String host) async {
return await rust_http.dnsLookupIps(host: host);
// Web 平台无法直接查询 DNS返回空列表
// 在 Web 平台上DNS 解析由浏览器自动处理
return [];
}
static Future<RustHttpResponse> _fetch({
required String method,
required String url,
Map<String, String>? headers,
Uint8List? data,
String? withIpAddress,
bool? withCustomDns,
}) async {
try {
final mergedHeaders = {..._defaultHeaders, ...?headers};
final response = await _dio.request(
url,
data: data,
options: Options(
method: method,
headers: mergedHeaders,
responseType: ResponseType.bytes,
validateStatus: (status) => true,
),
);
// 将 Dio Response 转换为 RustHttpResponse
return RustHttpResponse(
statusCode: response.statusCode ?? 0,
headers: _convertHeaders(response.headers.map),
url: response.realUri.toString(),
contentLength: response.headers.value('content-length') != null
? BigInt.from(int.parse(response.headers.value('content-length')!))
: null,
version: MyHttpVersion.httpUnknown,
remoteAddr: response.realUri.host,
data: response.data is Uint8List ? response.data : null,
);
} catch (e) {
// 发生错误时返回默认响应
return RustHttpResponse(
statusCode: 0,
headers: {},
url: url,
contentLength: null,
version: MyHttpVersion.httpUnknown,
remoteAddr: '',
data: null,
);
}
}
static Map<String, String> _convertHeaders(Map<String, List<String>> headers) {
final result = <String, String>{};
headers.forEach((key, value) {
if (value.isNotEmpty) {
result[key] = value.join(', ');
}
});
return result;
}
}

View File

@@ -1,68 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt`
/// Check if the URL scheme is already registered with the correct executable path
Future<ApplinksRegistrationResult> checkApplinksRegistration({
required String scheme,
}) => RustLib.instance.api.crateApiApplinksApiCheckApplinksRegistration(
scheme: scheme,
);
/// Register URL scheme in Windows registry
/// This will create or update the registry keys for the custom URL scheme
///
/// # Arguments
/// * `scheme` - The URL scheme to register (e.g., "sctoolbox")
/// * `app_name` - Optional application display name (e.g., "SCToolBox"). If provided,
/// the registry will show "URL:{app_name} Protocol" as the scheme description.
Future<ApplinksRegistrationResult> registerApplinks({
required String scheme,
String? appName,
}) => RustLib.instance.api.crateApiApplinksApiRegisterApplinks(
scheme: scheme,
appName: appName,
);
/// Unregister URL scheme from Windows registry
Future<ApplinksRegistrationResult> unregisterApplinks({
required String scheme,
}) =>
RustLib.instance.api.crateApiApplinksApiUnregisterApplinks(scheme: scheme);
/// Applinks URL scheme registration result
class ApplinksRegistrationResult {
/// Whether registration was successful
final bool success;
/// Detailed message about the operation
final String message;
/// Whether the registry was modified (false if already configured correctly)
final bool wasModified;
const ApplinksRegistrationResult({
required this.success,
required this.message,
required this.wasModified,
});
@override
int get hashCode =>
success.hashCode ^ message.hashCode ^ wasModified.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ApplinksRegistrationResult &&
runtimeType == other.runtimeType &&
success == other.success &&
message == other.message &&
wasModified == other.wasModified;
}

View File

@@ -1,44 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
Future<RsiLauncherAsarData> getRsiLauncherAsarData({
required String asarPath,
}) => RustLib.instance.api.crateApiAsarApiGetRsiLauncherAsarData(
asarPath: asarPath,
);
class RsiLauncherAsarData {
final String asarPath;
final String mainJsPath;
final Uint8List mainJsContent;
const RsiLauncherAsarData({
required this.asarPath,
required this.mainJsPath,
required this.mainJsContent,
});
Future<void> writeMainJs({required List<int> content}) =>
RustLib.instance.api.crateApiAsarApiRsiLauncherAsarDataWriteMainJs(
that: this,
content: content,
);
@override
int get hashCode =>
asarPath.hashCode ^ mainJsPath.hashCode ^ mainJsContent.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RsiLauncherAsarData &&
runtimeType == other.runtimeType &&
asarPath == other.asarPath &&
mainJsPath == other.mainJsPath &&
mainJsContent == other.mainJsContent;
}

View File

@@ -1,284 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_session`, `get_task_status`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `eq`, `fmt`, `fmt`, `fmt`
/// Initialize the download manager session with persistence enabled
///
/// Parameters:
/// - working_dir: The directory to store session data (persistence, DHT, etc.)
/// - default_download_dir: The default directory to store downloads
/// - upload_limit_bps: Upload speed limit in bytes per second (0 = unlimited)
/// - download_limit_bps: Download speed limit in bytes per second (0 = unlimited)
Future<void> downloaderInit({
required String workingDir,
required String defaultDownloadDir,
int? uploadLimitBps,
int? downloadLimitBps,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderInit(
workingDir: workingDir,
defaultDownloadDir: defaultDownloadDir,
uploadLimitBps: uploadLimitBps,
downloadLimitBps: downloadLimitBps,
);
/// Check if the downloader is initialized
bool downloaderIsInitialized() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderIsInitialized();
/// Check if there are pending tasks to restore from session file (without starting the downloader)
/// This reads the session.json file directly to check if there are any torrents saved.
///
/// Parameters:
/// - working_dir: The directory where session data is stored (same as passed to downloader_init)
///
/// Returns: true if there are tasks to restore, false otherwise
bool downloaderHasPendingSessionTasks({required String workingDir}) =>
RustLib.instance.api.crateApiDownloaderApiDownloaderHasPendingSessionTasks(
workingDir: workingDir,
);
/// Add a torrent from bytes (e.g., .torrent file content)
Future<BigInt> downloaderAddTorrent({
required List<int> torrentBytes,
String? outputFolder,
List<String>? trackers,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderAddTorrent(
torrentBytes: torrentBytes,
outputFolder: outputFolder,
trackers: trackers,
);
/// Add a torrent from a magnet link
Future<BigInt> downloaderAddMagnet({
required String magnetLink,
String? outputFolder,
List<String>? trackers,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderAddMagnet(
magnetLink: magnetLink,
outputFolder: outputFolder,
trackers: trackers,
);
/// Add a torrent from URL (HTTP download not supported, only torrent file URLs)
Future<BigInt> downloaderAddUrl({
required String url,
String? outputFolder,
List<String>? trackers,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderAddUrl(
url: url,
outputFolder: outputFolder,
trackers: trackers,
);
/// Pause a download task
Future<void> downloaderPause({required BigInt taskId}) =>
RustLib.instance.api.crateApiDownloaderApiDownloaderPause(taskId: taskId);
/// Resume a download task
Future<void> downloaderResume({required BigInt taskId}) =>
RustLib.instance.api.crateApiDownloaderApiDownloaderResume(taskId: taskId);
/// Remove a download task
/// Handles both active tasks (task_id < 10000) and cached completed tasks (task_id >= 10000)
Future<void> downloaderRemove({
required BigInt taskId,
required bool deleteFiles,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderRemove(
taskId: taskId,
deleteFiles: deleteFiles,
);
/// Get information about a specific task
Future<DownloadTaskInfo> downloaderGetTaskInfo({required BigInt taskId}) =>
RustLib.instance.api.crateApiDownloaderApiDownloaderGetTaskInfo(
taskId: taskId,
);
/// Get all tasks (includes both active and completed tasks from cache)
Future<List<DownloadTaskInfo>> downloaderGetAllTasks() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderGetAllTasks();
/// Get global statistics
Future<DownloadGlobalStat> downloaderGetGlobalStats() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderGetGlobalStats();
/// Check if a task with given name exists
///
/// Parameters:
/// - name: Task name to search for
/// - downloading_only: If true, only search in active/waiting tasks. If false, include completed tasks (default: true)
Future<bool> downloaderIsNameInTask({
required String name,
bool? downloadingOnly,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderIsNameInTask(
name: name,
downloadingOnly: downloadingOnly,
);
/// Pause all tasks
Future<void> downloaderPauseAll() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderPauseAll();
/// Resume all tasks
Future<void> downloaderResumeAll() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderResumeAll();
/// Stop the downloader session (pauses all tasks but keeps session)
Future<void> downloaderStop() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderStop();
/// Shutdown the downloader session completely (allows restart with new settings)
Future<void> downloaderShutdown() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderShutdown();
/// Get all completed tasks from cache (tasks removed by downloader_remove_completed_tasks)
/// This cache is cleared when the downloader is shutdown/restarted
List<DownloadTaskInfo> downloaderGetCompletedTasksCache() => RustLib
.instance
.api
.crateApiDownloaderApiDownloaderGetCompletedTasksCache();
/// Clear the completed tasks cache manually
void downloaderClearCompletedTasksCache() => RustLib.instance.api
.crateApiDownloaderApiDownloaderClearCompletedTasksCache();
/// Update global speed limits
/// Note: rqbit Session doesn't support runtime limit changes,
/// this function is a placeholder that returns an error.
/// Speed limits should be set during downloader_init.
Future<void> downloaderUpdateSpeedLimits({
int? uploadLimitBps,
int? downloadLimitBps,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderUpdateSpeedLimits(
uploadLimitBps: uploadLimitBps,
downloadLimitBps: downloadLimitBps,
);
/// Remove all completed tasks (equivalent to aria2's --seed-time=0 behavior)
/// Removed tasks are cached in memory and can be queried via downloader_get_completed_tasks_cache
Future<int> downloaderRemoveCompletedTasks() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderRemoveCompletedTasks();
/// Check if there are any active (non-completed) tasks
Future<bool> downloaderHasActiveTasks() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderHasActiveTasks();
/// Global statistics
class DownloadGlobalStat {
final BigInt downloadSpeed;
final BigInt uploadSpeed;
final BigInt numActive;
final BigInt numWaiting;
const DownloadGlobalStat({
required this.downloadSpeed,
required this.uploadSpeed,
required this.numActive,
required this.numWaiting,
});
static Future<DownloadGlobalStat> default_() =>
RustLib.instance.api.crateApiDownloaderApiDownloadGlobalStatDefault();
@override
int get hashCode =>
downloadSpeed.hashCode ^
uploadSpeed.hashCode ^
numActive.hashCode ^
numWaiting.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DownloadGlobalStat &&
runtimeType == other.runtimeType &&
downloadSpeed == other.downloadSpeed &&
uploadSpeed == other.uploadSpeed &&
numActive == other.numActive &&
numWaiting == other.numWaiting;
}
/// Download task information
class DownloadTaskInfo {
final BigInt id;
final String name;
final DownloadTaskStatus status;
final BigInt totalBytes;
final BigInt downloadedBytes;
final BigInt uploadedBytes;
final BigInt downloadSpeed;
final BigInt uploadSpeed;
final double progress;
final BigInt numPeers;
final String outputFolder;
const DownloadTaskInfo({
required this.id,
required this.name,
required this.status,
required this.totalBytes,
required this.downloadedBytes,
required this.uploadedBytes,
required this.downloadSpeed,
required this.uploadSpeed,
required this.progress,
required this.numPeers,
required this.outputFolder,
});
@override
int get hashCode =>
id.hashCode ^
name.hashCode ^
status.hashCode ^
totalBytes.hashCode ^
downloadedBytes.hashCode ^
uploadedBytes.hashCode ^
downloadSpeed.hashCode ^
uploadSpeed.hashCode ^
progress.hashCode ^
numPeers.hashCode ^
outputFolder.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DownloadTaskInfo &&
runtimeType == other.runtimeType &&
id == other.id &&
name == other.name &&
status == other.status &&
totalBytes == other.totalBytes &&
downloadedBytes == other.downloadedBytes &&
uploadedBytes == other.uploadedBytes &&
downloadSpeed == other.downloadSpeed &&
uploadSpeed == other.uploadSpeed &&
progress == other.progress &&
numPeers == other.numPeers &&
outputFolder == other.outputFolder;
}
/// Download task status
enum DownloadTaskStatus {
/// Checking/verifying existing files
checking,
/// Actively downloading
live,
/// Paused
paused,
/// Error occurred
error,
/// Download completed
finished,
}

View File

@@ -1,52 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import '../http_package.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `_my_method_to_hyper_method`
Future<void> setDefaultHeader({required Map<String, String> headers}) =>
RustLib.instance.api.crateApiHttpApiSetDefaultHeader(headers: headers);
Future<RustHttpResponse> fetch({
required MyMethod method,
required String url,
Map<String, String>? headers,
Uint8List? inputData,
String? withIpAddress,
bool? withCustomDns,
}) => RustLib.instance.api.crateApiHttpApiFetch(
method: method,
url: url,
headers: headers,
inputData: inputData,
withIpAddress: withIpAddress,
withCustomDns: withCustomDns,
);
Future<List<String>> dnsLookupTxt({required String host}) =>
RustLib.instance.api.crateApiHttpApiDnsLookupTxt(host: host);
Future<List<String>> dnsLookupIps({required String host}) =>
RustLib.instance.api.crateApiHttpApiDnsLookupIps(host: host);
/// Get the fastest URL from a list of URLs by testing them concurrently.
/// Returns the first URL that responds successfully, canceling other requests.
///
/// # Arguments
/// * `urls` - List of base URLs to test
/// * `path_suffix` - Optional path suffix to append to each URL (e.g., "/api/version")
/// If None, tests the base URL directly
Future<String?> getFasterUrl({
required List<String> urls,
String? pathSuffix,
}) => RustLib.instance.api.crateApiHttpApiGetFasterUrl(
urls: urls,
pathSuffix: pathSuffix,
);
enum MyMethod { options, gets, post, put, delete, head, trace, connect, patch }

View File

@@ -1,75 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
/// 加载 ONNX 翻译模型
///
/// # Arguments
/// * `model_path` - 模型文件夹路径
/// * `model_key` - 模型缓存键(用于标识模型,如 "zh-en"
/// * `quantization_suffix` - 量化后缀(如 "_q4", "_q8",空字符串表示使用默认模型)
/// * `use_xnnpack` - 是否使用 XNNPACK 加速
///
Future<void> loadTranslationModel({
required String modelPath,
required String modelKey,
required String quantizationSuffix,
required bool useXnnpack,
}) => RustLib.instance.api.crateApiOrtApiLoadTranslationModel(
modelPath: modelPath,
modelKey: modelKey,
quantizationSuffix: quantizationSuffix,
useXnnpack: useXnnpack,
);
/// 翻译文本
///
/// # Arguments
/// * `model_key` - 模型缓存键(如 "zh-en"
/// * `text` - 要翻译的文本
///
/// # Returns
/// * `Result<String>` - 翻译后的文本
Future<String> translateText({
required String modelKey,
required String text,
}) => RustLib.instance.api.crateApiOrtApiTranslateText(
modelKey: modelKey,
text: text,
);
/// 批量翻译文本
///
/// # Arguments
/// * `model_key` - 模型缓存键(如 "zh-en"
/// * `texts` - 要翻译的文本列表
///
/// # Returns
/// * `Result<Vec<String>>` - 翻译后的文本列表
Future<List<String>> translateTextBatch({
required String modelKey,
required List<String> texts,
}) => RustLib.instance.api.crateApiOrtApiTranslateTextBatch(
modelKey: modelKey,
texts: texts,
);
/// 卸载模型
///
/// # Arguments
/// * `model_key` - 模型缓存键(如 "zh-en"
///
Future<void> unloadTranslationModel({required String modelKey}) => RustLib
.instance
.api
.crateApiOrtApiUnloadTranslationModel(modelKey: modelKey);
/// 清空所有已加载的模型
///
/// # Returns
Future<void> clearAllModels() =>
RustLib.instance.api.crateApiOrtApiClearAllModels();

View File

@@ -1,50 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `_process_output`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `RsProcess`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`
Stream<RsProcessStreamData> start({
required String executable,
required List<String> arguments,
required String workingDirectory,
}) => RustLib.instance.api.crateApiRsProcessStart(
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
);
Future<void> write({required int rsPid, required String data}) =>
RustLib.instance.api.crateApiRsProcessWrite(rsPid: rsPid, data: data);
class RsProcessStreamData {
final RsProcessStreamDataType dataType;
final String data;
final int rsPid;
const RsProcessStreamData({
required this.dataType,
required this.data,
required this.rsPid,
});
@override
int get hashCode => dataType.hashCode ^ data.hashCode ^ rsPid.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RsProcessStreamData &&
runtimeType == other.runtimeType &&
dataType == other.dataType &&
data == other.data &&
rsPid == other.rsPid;
}
enum RsProcessStreamDataType { output, error, exit }

View File

@@ -1,120 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'unp4k_api.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `dos_datetime_to_millis`, `ensure_files_loaded`, `p4k_get_entry`
/// 打开 P4K 文件(仅打开,不读取文件列表)
Future<void> p4KOpen({required String p4KPath}) =>
RustLib.instance.api.crateApiUnp4KApiP4KOpen(p4KPath: p4KPath);
/// 获取文件数量(会触发文件列表加载)
Future<BigInt> p4KGetFileCount() =>
RustLib.instance.api.crateApiUnp4KApiP4KGetFileCount();
/// 获取所有文件列表
Future<List<P4kFileItem>> p4KGetAllFiles() =>
RustLib.instance.api.crateApiUnp4KApiP4KGetAllFiles();
/// 提取文件到内存
Future<Uint8List> p4KExtractToMemory({required String filePath}) =>
RustLib.instance.api.crateApiUnp4KApiP4KExtractToMemory(filePath: filePath);
/// 提取文件到磁盘
Future<void> p4KExtractToDisk({
required String filePath,
required String outputPath,
}) => RustLib.instance.api.crateApiUnp4KApiP4KExtractToDisk(
filePath: filePath,
outputPath: outputPath,
);
/// 关闭 P4K 读取器
Future<void> p4KClose() => RustLib.instance.api.crateApiUnp4KApiP4KClose();
/// 检查数据是否为 DataForge/DCB 格式
Future<bool> dcbIsDataforge({required List<int> data}) =>
RustLib.instance.api.crateApiUnp4KApiDcbIsDataforge(data: data);
/// 从内存数据打开 DCB 文件
Future<void> dcbOpen({required List<int> data}) =>
RustLib.instance.api.crateApiUnp4KApiDcbOpen(data: data);
/// 获取 DCB 记录数量
Future<BigInt> dcbGetRecordCount() =>
RustLib.instance.api.crateApiUnp4KApiDcbGetRecordCount();
/// 获取所有 DCB 记录路径列表
Future<List<DcbRecordItem>> dcbGetRecordList() =>
RustLib.instance.api.crateApiUnp4KApiDcbGetRecordList();
/// 根据路径获取单条记录的 XML
Future<String> dcbRecordToXml({required String path}) =>
RustLib.instance.api.crateApiUnp4KApiDcbRecordToXml(path: path);
/// 根据索引获取单条记录的 XML
Future<String> dcbRecordToXmlByIndex({required BigInt index}) =>
RustLib.instance.api.crateApiUnp4KApiDcbRecordToXmlByIndex(index: index);
/// 全文搜索 DCB 记录
Future<List<DcbSearchResult>> dcbSearchAll({required String query}) =>
RustLib.instance.api.crateApiUnp4KApiDcbSearchAll(query: query);
/// 导出 DCB 到磁盘
/// merge: true = 合并为单个 XMLfalse = 分离为多个 XML 文件
Future<void> dcbExportToDisk({
required String outputPath,
required String dcbPath,
required bool merge,
}) => RustLib.instance.api.crateApiUnp4KApiDcbExportToDisk(
outputPath: outputPath,
dcbPath: dcbPath,
merge: merge,
);
/// 关闭 DCB 读取器
Future<void> dcbClose() => RustLib.instance.api.crateApiUnp4KApiDcbClose();
/// DCB 记录项信息
@freezed
sealed class DcbRecordItem with _$DcbRecordItem {
const factory DcbRecordItem({required String path, required BigInt index}) =
_DcbRecordItem;
}
@freezed
sealed class DcbSearchMatch with _$DcbSearchMatch {
const factory DcbSearchMatch({
required BigInt lineNumber,
required String lineContent,
}) = _DcbSearchMatch;
}
/// 全文搜索 DCB 记录
/// 返回匹配的记录路径和预览摘要
@freezed
sealed class DcbSearchResult with _$DcbSearchResult {
const factory DcbSearchResult({
required String path,
required BigInt index,
required List<DcbSearchMatch> matches,
}) = _DcbSearchResult;
}
/// P4K 文件项信息
@freezed
sealed class P4kFileItem with _$P4kFileItem {
const factory P4kFileItem({
required String name,
required bool isDirectory,
required BigInt size,
required BigInt compressedSize,
required PlatformInt64 dateModified,
}) = _P4kFileItem;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,141 +0,0 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'webview_api.freezed.dart';
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `WebViewCommand`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`
/// Create a new WebView window and return its ID
String webviewCreate({required WebViewConfiguration config}) =>
RustLib.instance.api.crateApiWebviewApiWebviewCreate(config: config);
/// Navigate to a URL
void webviewNavigate({required String id, required String url}) =>
RustLib.instance.api.crateApiWebviewApiWebviewNavigate(id: id, url: url);
/// Go back in history
void webviewGoBack({required String id}) =>
RustLib.instance.api.crateApiWebviewApiWebviewGoBack(id: id);
/// Go forward in history
void webviewGoForward({required String id}) =>
RustLib.instance.api.crateApiWebviewApiWebviewGoForward(id: id);
/// Reload the current page
void webviewReload({required String id}) =>
RustLib.instance.api.crateApiWebviewApiWebviewReload(id: id);
/// Stop loading
void webviewStop({required String id}) =>
RustLib.instance.api.crateApiWebviewApiWebviewStop(id: id);
/// Execute JavaScript in the WebView
void webviewExecuteScript({required String id, required String script}) =>
RustLib.instance.api.crateApiWebviewApiWebviewExecuteScript(
id: id,
script: script,
);
/// Set window visibility
void webviewSetVisibility({required String id, required bool visible}) =>
RustLib.instance.api.crateApiWebviewApiWebviewSetVisibility(
id: id,
visible: visible,
);
/// Close the WebView window
void webviewClose({required String id}) =>
RustLib.instance.api.crateApiWebviewApiWebviewClose(id: id);
/// Set window size
void webviewSetWindowSize({
required String id,
required int width,
required int height,
}) => RustLib.instance.api.crateApiWebviewApiWebviewSetWindowSize(
id: id,
width: width,
height: height,
);
/// Set window position
void webviewSetWindowPosition({
required String id,
required int x,
required int y,
}) => RustLib.instance.api.crateApiWebviewApiWebviewSetWindowPosition(
id: id,
x: x,
y: y,
);
/// Get the current navigation state
WebViewNavigationState webviewGetState({required String id}) =>
RustLib.instance.api.crateApiWebviewApiWebviewGetState(id: id);
/// Check if the WebView is closed
bool webviewIsClosed({required String id}) =>
RustLib.instance.api.crateApiWebviewApiWebviewIsClosed(id: id);
/// Poll for events from the WebView (non-blocking)
List<WebViewEvent> webviewPollEvents({required String id}) =>
RustLib.instance.api.crateApiWebviewApiWebviewPollEvents(id: id);
/// Get a list of all active WebView IDs
List<String> webviewListAll() =>
RustLib.instance.api.crateApiWebviewApiWebviewListAll();
/// WebView window configuration
@freezed
sealed class WebViewConfiguration with _$WebViewConfiguration {
const WebViewConfiguration._();
const factory WebViewConfiguration({
required String title,
required int width,
required int height,
String? userDataFolder,
required bool enableDevtools,
required bool transparent,
String? userAgent,
}) = _WebViewConfiguration;
static Future<WebViewConfiguration> default_() =>
RustLib.instance.api.crateApiWebviewApiWebViewConfigurationDefault();
}
@freezed
sealed class WebViewEvent with _$WebViewEvent {
const WebViewEvent._();
const factory WebViewEvent.navigationStarted({required String url}) =
WebViewEvent_NavigationStarted;
const factory WebViewEvent.navigationCompleted({required String url}) =
WebViewEvent_NavigationCompleted;
const factory WebViewEvent.titleChanged({required String title}) =
WebViewEvent_TitleChanged;
const factory WebViewEvent.webMessage({required String message}) =
WebViewEvent_WebMessage;
const factory WebViewEvent.windowClosed() = WebViewEvent_WindowClosed;
const factory WebViewEvent.error({required String message}) =
WebViewEvent_Error;
}
/// Navigation state of the WebView
@freezed
sealed class WebViewNavigationState with _$WebViewNavigationState {
const WebViewNavigationState._();
const factory WebViewNavigationState({
required String url,
required String title,
required bool canGoBack,
required bool canGoForward,
required bool isLoading,
}) = _WebViewNavigationState;
static Future<WebViewNavigationState> default_() =>
RustLib.instance.api.crateApiWebviewApiWebViewNavigationStateDefault();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,168 +1,15 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// Web platform stub for win32_api
// Windows API 在 Web 平台上不可用,提供空实现
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_process_path`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `fmt`, `fmt`
Future<void> sendNotify({
String? summary,
String? body,
String? appName,
String? appId,
}) => RustLib.instance.api.crateApiWin32ApiSendNotify(
summary: summary,
body: body,
appName: appName,
appId: appId,
);
/// Get system memory size in GB
Future<BigInt> getSystemMemorySizeGb() =>
RustLib.instance.api.crateApiWin32ApiGetSystemMemorySizeGb();
/// Get number of logical processors
Future<int> getNumberOfLogicalProcessors() =>
RustLib.instance.api.crateApiWin32ApiGetNumberOfLogicalProcessors();
/// Get all system information at once
Future<SystemInfo> getSystemInfo() =>
RustLib.instance.api.crateApiWin32ApiGetSystemInfo();
/// Get GPU info from registry (more accurate VRAM)
Future<String> getGpuInfoFromRegistry() =>
RustLib.instance.api.crateApiWin32ApiGetGpuInfoFromRegistry();
/// Resolve shortcut (.lnk) file to get target path
Future<String> resolveShortcut({required String lnkPath}) =>
RustLib.instance.api.crateApiWin32ApiResolveShortcut(lnkPath: lnkPath);
/// Open file explorer and select file/folder
Future<void> openDirWithExplorer({
required String path,
required bool isFile,
}) => RustLib.instance.api.crateApiWin32ApiOpenDirWithExplorer(
path: path,
isFile: isFile,
);
Future<bool> setForegroundWindow({required String windowName}) => RustLib
.instance
.api
.crateApiWin32ApiSetForegroundWindow(windowName: windowName);
Future<int> getProcessPidByName({required String processName}) => RustLib
.instance
.api
.crateApiWin32ApiGetProcessPidByName(processName: processName);
Future<List<ProcessInfo>> getProcessListByName({required String processName}) =>
RustLib.instance.api.crateApiWin32ApiGetProcessListByName(
processName: processName,
);
/// Kill processes by name
Future<int> killProcessByName({required String processName}) => RustLib
.instance
.api
.crateApiWin32ApiKillProcessByName(processName: processName);
/// Get disk physical sector size for performance
Future<int> getDiskPhysicalSectorSize({required String driveLetter}) => RustLib
.instance
.api
.crateApiWin32ApiGetDiskPhysicalSectorSize(driveLetter: driveLetter);
/// Create a desktop shortcut
Future<void> createDesktopShortcut({
required String targetPath,
required String shortcutName,
}) => RustLib.instance.api.crateApiWin32ApiCreateDesktopShortcut(
targetPath: targetPath,
shortcutName: shortcutName,
);
/// Run a program with admin privileges (UAC)
Future<void> runAsAdmin({required String program, required String args}) =>
RustLib.instance.api.crateApiWin32ApiRunAsAdmin(
program: program,
args: args,
);
/// Start a program (without waiting)
Future<void> startProcess({
required String program,
required List<String> args,
}) => RustLib.instance.api.crateApiWin32ApiStartProcess(
program: program,
args: args,
);
/// Check if NVME patch is applied
Future<bool> checkNvmePatchStatus() =>
RustLib.instance.api.crateApiWin32ApiCheckNvmePatchStatus();
/// Add NVME patch to registry
Future<void> addNvmePatch() =>
RustLib.instance.api.crateApiWin32ApiAddNvmePatch();
/// Remove NVME patch from registry
Future<void> removeNvmePatch() =>
RustLib.instance.api.crateApiWin32ApiRemoveNvmePatch();
class ProcessInfo {
final int pid;
final String name;
final String path;
const ProcessInfo({
required this.pid,
required this.name,
required this.path,
});
@override
int get hashCode => pid.hashCode ^ name.hashCode ^ path.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ProcessInfo &&
runtimeType == other.runtimeType &&
pid == other.pid &&
name == other.name &&
path == other.path;
/// 发送系统通知Web 平台使用控制台输出)
Future<void> sendNotify({String? summary, String? body, String? appName, String? appId}) async {
print('Notification: $summary - $body');
// Web 平台可以使用浏览器 Notification API
// 但需要用户授权,这里简化处理
}
/// System information struct
class SystemInfo {
final String osName;
final String cpuName;
final String gpuInfo;
final String diskInfo;
const SystemInfo({
required this.osName,
required this.cpuName,
required this.gpuInfo,
required this.diskInfo,
});
@override
int get hashCode =>
osName.hashCode ^ cpuName.hashCode ^ gpuInfo.hashCode ^ diskInfo.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SystemInfo &&
runtimeType == other.runtimeType &&
osName == other.osName &&
cpuName == other.cpuName &&
gpuInfo == other.gpuInfo &&
diskInfo == other.diskInfo;
/// 设置窗口为前台Web 平台不支持)
Future<bool> setForegroundWindow({required String windowName}) async {
print('setForegroundWindow not supported on web platform');
return false;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,7 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// HTTP 响应数据类
// 原先由 flutter_rust_bridge 生成,现改为纯 Dart 实现
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'dart:typed_data';
enum MyHttpVersion { http09, http10, http11, http2, http3, httpUnknown }

View File

@@ -1,340 +0,0 @@
// Rust WebView 管理器
// 使用 wry + tao 实现的 WebView 窗口管理
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:starcitizen_doctor/common/rust/api/webview_api.dart'
as rust_webview;
import 'package:starcitizen_doctor/common/utils/log.dart';
typedef OnWebMessageCallback = void Function(String message);
typedef OnNavigationCallback = void Function(String url);
typedef OnNavigationCompletedCallback = void Function(String url);
typedef OnWindowClosedCallback = void Function();
/// Rust WebView 控制器
/// 封装 Rust wry + tao WebView 的业务逻辑
class RustWebViewController {
final String id;
final List<OnWebMessageCallback> _messageCallbacks = [];
final List<OnNavigationCallback> _navigationCallbacks = [];
final List<OnNavigationCompletedCallback> _navigationCompletedCallbacks = [];
final List<OnWindowClosedCallback> _closeCallbacks = [];
Timer? _pollTimer;
bool _isDisposed = false;
/// 本地化脚本(从 assets 加载)
String _localizationScript = "";
/// 当前 URL
String _currentUrl = "";
String get currentUrl => _currentUrl;
RustWebViewController._(this.id);
/// 创建新的 WebView 窗口
static Future<RustWebViewController> create({
String title = "WebView",
int width = 1280,
int height = 720,
String? userDataFolder,
bool enableDevtools = false,
bool transparent = false,
String? userAgent,
}) async {
try {
final config = rust_webview.WebViewConfiguration(
title: title,
width: width,
height: height,
userDataFolder: userDataFolder,
enableDevtools: enableDevtools,
transparent: transparent,
userAgent: userAgent,
);
final id = rust_webview.webviewCreate(config: config);
final controller = RustWebViewController._(id);
// 加载脚本资源
await controller._loadScripts();
// 启动事件轮询
controller._startEventPolling();
return controller;
} catch (e) {
throw Exception("Failed to create WebView: $e");
}
}
/// 加载本地化和拦截器脚本
Future<void> _loadScripts() async {
try {
_localizationScript = await rootBundle.loadString('assets/web_script.js');
} catch (e) {
dPrint("Failed to load scripts: $e");
}
}
/// 启动事件轮询
void _startEventPolling() {
_pollTimer = Timer.periodic(const Duration(milliseconds: 50), (_) {
if (_isDisposed) return;
_pollEvents();
});
}
/// 轮询事件
void _pollEvents() {
try {
final events = rust_webview.webviewPollEvents(id: id);
for (final event in events) {
_handleEvent(event);
}
} catch (e) {
// WebView 可能已关闭
if (!_isDisposed) {
dPrint("Error polling events: $e");
}
}
}
/// 处理事件
void _handleEvent(rust_webview.WebViewEvent event) {
switch (event) {
case rust_webview.WebViewEvent_NavigationStarted(:final url):
dPrint("Navigation started: $url");
_currentUrl = url;
// 导航开始时通知
for (final callback in _navigationCallbacks) {
callback(url);
}
break;
case rust_webview.WebViewEvent_NavigationCompleted(:final url):
dPrint("Navigation completed: $url");
_currentUrl = url;
// 导航完成回调(用于注入脚本)
for (final callback in _navigationCompletedCallbacks) {
callback(url);
}
for (final callback in _navigationCallbacks) {
callback(url);
}
break;
case rust_webview.WebViewEvent_TitleChanged(:final title):
dPrint("Title changed: $title");
break;
case rust_webview.WebViewEvent_WebMessage(:final message):
_handleWebMessage(message);
break;
case rust_webview.WebViewEvent_WindowClosed():
dPrint("Window closed");
for (final callback in _closeCallbacks) {
callback();
}
dispose();
break;
case rust_webview.WebViewEvent_Error(:final message):
dPrint("WebView error: $message");
break;
}
}
/// 处理来自 WebView 的消息
void _handleWebMessage(String message) {
dPrint("Web message: $message");
try {
final data = json.decode(message);
final action = data["action"];
switch (action) {
case "navigation_state":
// 从 JS 获取导航状态更新
final url = data["url"] ?? "";
final isLoading = data["isLoading"] ?? false;
_currentUrl = url;
if (!isLoading) {
for (final callback in _navigationCallbacks) {
callback(url);
}
}
break;
case "close_window":
// 处理来自导航栏的关闭请求
close();
break;
default:
// 转发其他消息给回调
for (final callback in _messageCallbacks) {
callback(message);
}
}
} catch (e) {
// 非 JSON 消息,直接转发
for (final callback in _messageCallbacks) {
callback(message);
}
}
}
/// 导航到 URL
void navigate(String url) {
if (_isDisposed) return;
_currentUrl = url;
rust_webview.webviewNavigate(id: id, url: url);
}
/// 后退
void goBack() {
if (_isDisposed) return;
rust_webview.webviewGoBack(id: id);
}
/// 前进
void goForward() {
if (_isDisposed) return;
rust_webview.webviewGoForward(id: id);
}
/// 刷新
void reload() {
if (_isDisposed) return;
rust_webview.webviewReload(id: id);
}
/// 停止加载
void stop() {
if (_isDisposed) return;
rust_webview.webviewStop(id: id);
}
/// 执行 JavaScript
void executeScript(String script) {
if (_isDisposed) return;
rust_webview.webviewExecuteScript(id: id, script: script);
}
/// 设置窗口可见性
void setVisible(bool visible) {
if (_isDisposed) return;
rust_webview.webviewSetVisibility(id: id, visible: visible);
}
/// 关闭窗口
void close() {
if (_isDisposed) return;
rust_webview.webviewClose(id: id);
dispose();
}
/// 设置窗口大小
void setWindowSize(int width, int height) {
if (_isDisposed) return;
rust_webview.webviewSetWindowSize(id: id, width: width, height: height);
}
/// 设置窗口位置
void setWindowPosition(int x, int y) {
if (_isDisposed) return;
rust_webview.webviewSetWindowPosition(id: id, x: x, y: y);
}
/// 获取当前导航状态
rust_webview.WebViewNavigationState getState() {
return rust_webview.webviewGetState(id: id);
}
/// 检查窗口是否已关闭
bool get isClosed {
if (_isDisposed) return true;
return rust_webview.webviewIsClosed(id: id);
}
/// 添加消息回调
void addOnWebMessageCallback(OnWebMessageCallback callback) {
_messageCallbacks.add(callback);
}
/// 移除消息回调
void removeOnWebMessageCallback(OnWebMessageCallback callback) {
_messageCallbacks.remove(callback);
}
/// 添加导航回调
void addOnNavigationCallback(OnNavigationCallback callback) {
_navigationCallbacks.add(callback);
}
/// 移除导航回调
void removeOnNavigationCallback(OnNavigationCallback callback) {
_navigationCallbacks.remove(callback);
}
/// 添加导航完成回调(用于在页面加载完成后注入脚本)
void addOnNavigationCompletedCallback(
OnNavigationCompletedCallback callback,
) {
_navigationCompletedCallbacks.add(callback);
}
/// 移除导航完成回调
void removeOnNavigationCompletedCallback(
OnNavigationCompletedCallback callback,
) {
_navigationCompletedCallbacks.remove(callback);
}
/// 添加关闭回调
void addOnCloseCallback(OnWindowClosedCallback callback) {
_closeCallbacks.add(callback);
}
/// 移除关闭回调
void removeOnCloseCallback(OnWindowClosedCallback callback) {
_closeCallbacks.remove(callback);
}
/// 注入本地化脚本
void injectLocalizationScript() {
if (_localizationScript.isNotEmpty) {
executeScript(_localizationScript);
}
}
/// 初始化网页本地化
void initWebLocalization() {
executeScript("InitWebLocalization()");
}
/// 更新翻译词典
void updateReplaceWords(List<Map<String, String>> words, bool enableCapture) {
final jsonWords = json.encode(words);
executeScript(
"WebLocalizationUpdateReplaceWords($jsonWords, $enableCapture)",
);
}
/// 执行 RSI 登录脚本
void executeRsiLogin(String channel) {
executeScript('getRSILauncherToken("$channel");');
}
/// 释放资源
void dispose() {
if (_isDisposed) return;
_isDisposed = true;
_pollTimer?.cancel();
_messageCallbacks.clear();
_navigationCallbacks.clear();
_navigationCompletedCallbacks.clear();
_closeCallbacks.clear();
}
}

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/rendering.dart';
@@ -8,118 +7,105 @@ import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:starcitizen_doctor/generated/l10n.dart';
/// String extension for cross-platform path compatibility
extension PathStringExtension on String {
/// Converts path separators to the current platform's format.
/// On Windows: / -> \
/// On Linux/macOS: \ -> /
String get platformPath {
if (Platform.isWindows) {
return replaceAll('/', '\\');
}
return replaceAll('\\', '/');
}
}
Future showToast(BuildContext context, String msg, {BoxConstraints? constraints, String? title}) async {
return showBaseDialog(
context,
title: title ?? S.current.app_common_tip,
content: Text(msg),
actions: [
FilledButton(
child: Padding(
padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
child: Text(S.current.app_common_tip_i_know),
return showBaseDialog(context,
title: title ?? S.current.app_common_tip,
content: Text(msg),
actions: [
FilledButton(
child: Padding(
padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
child: Text(S.current.app_common_tip_i_know),
),
onPressed: () => Navigator.pop(context),
),
onPressed: () => Navigator.pop(context),
),
],
constraints: constraints,
);
],
constraints: constraints);
}
Future<bool> showConfirmDialogs(
BuildContext context,
String title,
Widget content, {
String confirm = "",
String cancel = "",
BoxConstraints? constraints,
}) async {
Future<bool> showConfirmDialogs(BuildContext context, String title, Widget content,
{String confirm = "", String cancel = "", BoxConstraints? constraints}) async {
if (confirm.isEmpty) confirm = S.current.app_common_tip_confirm;
if (cancel.isEmpty) cancel = S.current.app_common_tip_cancel;
final r = await showBaseDialog(
context,
title: title,
content: content,
actions: [
if (confirm.isNotEmpty)
FilledButton(
child: Padding(padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8), child: Text(confirm)),
onPressed: () => Navigator.pop(context, true),
),
if (cancel.isNotEmpty)
Button(
child: Padding(padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8), child: Text(cancel)),
onPressed: () => Navigator.pop(context, false),
),
],
constraints: constraints,
);
final r = await showBaseDialog(context,
title: title,
content: content,
actions: [
if (confirm.isNotEmpty)
FilledButton(
child: Padding(
padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
child: Text(confirm),
),
onPressed: () => Navigator.pop(context, true),
),
if (cancel.isNotEmpty)
Button(
child: Padding(
padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
child: Text(cancel),
),
onPressed: () => Navigator.pop(context, false),
),
],
constraints: constraints);
return r == true;
}
Future<String?> showInputDialogs(
BuildContext context, {
required String title,
required String content,
BoxConstraints? constraints,
String? initialValue,
List<TextInputFormatter>? inputFormatters,
}) async {
Future<String?> showInputDialogs(BuildContext context,
{required String title,
required String content,
BoxConstraints? constraints,
String? initialValue,
List<TextInputFormatter>? inputFormatters}) async {
String? userInput;
constraints ??= BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .38);
constraints ??= BoxConstraints(maxWidth: MediaQuery
.of(context)
.size
.width * .38);
final ok = await showConfirmDialogs(
context,
title,
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (content.isNotEmpty) Text(content, style: TextStyle(color: Colors.white.withValues(alpha: .6))),
const SizedBox(height: 8),
TextFormBox(
initialValue: initialValue,
onChanged: (str) {
userInput = str;
},
inputFormatters: inputFormatters,
),
],
),
constraints: constraints,
);
context,
title,
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (content.isNotEmpty)
Text(
content,
style: TextStyle(color: Colors.white.withValues(alpha: .6)),
),
const SizedBox(height: 8),
TextFormBox(
initialValue: initialValue,
onChanged: (str) {
userInput = str;
},
inputFormatters: inputFormatters,
),
],
),
constraints: constraints);
if (ok == true) return userInput;
return null;
}
Future showBaseDialog(
BuildContext context, {
required String title,
required Widget content,
List<Widget>? actions,
BoxConstraints? constraints,
}) async {
Future showBaseDialog(BuildContext context,
{required String title, required Widget content, List<Widget>? actions, BoxConstraints? constraints}) async {
return await showDialog(
context: context,
builder: (context) => ContentDialog(
title: Text(title),
content: content,
constraints: constraints ?? const BoxConstraints(maxWidth: 512, maxHeight: 756.0),
actions: actions,
),
builder: (context) =>
ContentDialog(
title: Text(title),
content: content,
constraints: constraints ??
const BoxConstraints(
maxWidth: 512,
maxHeight: 756.0,
),
actions: actions,
),
);
}

View File

@@ -0,0 +1,14 @@
// Stub file for web platform - not used
class FileCacheUtils {
static Future<dynamic> getFile(String url) async {
throw UnsupportedError('File operations are not supported on web platform');
}
static Future<bool> clearCache(String url) async {
throw UnsupportedError('File operations are not supported on web platform');
}
static Future<void> clearAllCache() async {
throw UnsupportedError('File operations are not supported on web platform');
}
}

View File

@@ -1,10 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -14,7 +11,6 @@ import 'package:starcitizen_doctor/common/conf/conf.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/generated/l10n.dart';
import 'package:starcitizen_doctor/ui/tools/log_analyze_ui/log_analyze_ui.dart';
import 'package:window_manager/window_manager.dart';
import 'base_utils.dart';
@@ -22,15 +18,6 @@ part 'multi_window_manager.freezed.dart';
part 'multi_window_manager.g.dart';
/// Window type definitions for multi-window support
class WindowTypes {
/// Main application window
static const String main = 'main';
/// Log analyzer window
static const String logAnalyze = 'log_analyze';
}
@freezed
abstract class MultiWindowAppState with _$MultiWindowAppState {
const factory MultiWindowAppState({
@@ -40,49 +27,35 @@ abstract class MultiWindowAppState with _$MultiWindowAppState {
required List<String> gameInstallPaths,
String? languageCode,
String? countryCode,
@Default(10) windowsVersion,
}) = _MultiWindowAppState;
factory MultiWindowAppState.fromJson(Map<String, dynamic> json) => _$MultiWindowAppStateFromJson(json);
}
class MultiWindowManager {
/// Parse window type from arguments string
static String parseWindowType(String arguments) {
if (arguments.isEmpty) {
return WindowTypes.main;
}
try {
final Map<String, dynamic> argument = jsonDecode(arguments);
return argument['window_type'] ?? WindowTypes.main;
} catch (e) {
return WindowTypes.main;
}
static Future<void> launchSubWindow(String type, String title, AppGlobalState appGlobalState) async {
final gameInstallPaths = await SCLoggerHelper.getGameInstallPath(await SCLoggerHelper.getLauncherLogList() ?? [],
checkExists: true, withVersion: AppConf.gameChannels);
final window = await DesktopMultiWindow.createWindow(jsonEncode({
'window_type': type,
'app_state': _appStateToWindowState(
appGlobalState,
gameInstallPaths: gameInstallPaths,
).toJson(),
}));
window.setFrame(const Rect.fromLTWH(0, 0, 900, 1200));
window.setTitle(title);
await window.center();
await window.show();
// sendAppStateBroadcast(appGlobalState);
}
/// Launch a sub-window with specified type and title
static Future<void> launchSubWindow(String type, String title, AppGlobalState appGlobalState) async {
final gameInstallPaths = await SCLoggerHelper.getGameInstallPath(
await SCLoggerHelper.getLauncherLogList() ?? [],
checkExists: true,
withVersion: AppConf.gameChannels,
static void sendAppStateBroadcast(AppGlobalState appGlobalState) {
DesktopMultiWindow.invokeMethod(
0,
'app_state_broadcast',
_appStateToWindowState(appGlobalState).toJson(),
);
final controller = await WindowController.create(
WindowConfiguration(
hiddenAtLaunch: true,
arguments: jsonEncode({
'window_type': type,
'app_state': _appStateToWindowState(appGlobalState, gameInstallPaths: gameInstallPaths).toJson(),
}),
),
);
await Future.delayed(Duration(milliseconds: 500)).then((_) async {
await controller.setFrame(const Rect.fromLTWH(0, 0, 720, 800));
await controller.setTitle(title);
await controller.center();
await controller.show();
});
}
static MultiWindowAppState _appStateToWindowState(AppGlobalState appGlobalState, {List<String>? gameInstallPaths}) {
@@ -93,147 +66,53 @@ class MultiWindowManager {
languageCode: appGlobalState.appLocale?.languageCode,
countryCode: appGlobalState.appLocale?.countryCode,
gameInstallPaths: gameInstallPaths ?? [],
windowsVersion: appGlobalState.windowsVersion,
);
}
/// Run sub-window app with parsed arguments
static Future<void> runSubWindowApp(String arguments, String windowType) async {
final Map<String, dynamic> argument = arguments.isEmpty ? const {} : jsonDecode(arguments);
static void runSubWindowApp(List<String> args) {
final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map<String, dynamic>;
final windowAppState = MultiWindowAppState.fromJson(argument['app_state'] ?? {});
Widget? windowWidget;
switch (windowType) {
case WindowTypes.logAnalyze:
switch (argument["window_type"]) {
case "log_analyze":
windowWidget = ToolsLogAnalyzeDialogUI(appState: windowAppState);
break;
default:
throw Exception('Unknown window type: $windowType');
throw Exception('Unknown window type');
}
await Window.initialize();
if (Platform.isWindows && windowAppState.windowsVersion >= 10) {
await Window.setEffect(effect: WindowEffect.acrylic);
}
final backgroundColor = HexColor(windowAppState.backgroundColor).withValues(alpha: .1);
return runApp(
ProviderScope(
child: FluentApp(
title: "StarCitizenToolBox",
restorationScopeId: "StarCitizenToolBox",
themeMode: ThemeMode.dark,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
FluentLocalizations.delegate,
S.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: windowWidget,
theme: FluentThemeData(
return runApp(ProviderScope(
child: FluentApp(
title: "StarCitizenToolBox",
restorationScopeId: "StarCitizenToolBox",
themeMode: ThemeMode.dark,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
FluentLocalizations.delegate,
S.delegate,
],
supportedLocales: S.delegate.supportedLocales,
home: windowWidget,
theme: FluentThemeData(
brightness: Brightness.dark,
fontFamily: "SourceHanSansCN-Regular",
navigationPaneTheme: NavigationPaneThemeData(backgroundColor: backgroundColor),
navigationPaneTheme: NavigationPaneThemeData(
backgroundColor: HexColor(windowAppState.backgroundColor),
),
menuColor: HexColor(windowAppState.menuColor),
micaBackgroundColor: HexColor(windowAppState.micaColor),
scaffoldBackgroundColor: backgroundColor,
buttonTheme: ButtonThemeData(
defaultButtonStyle: ButtonStyle(
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(color: Colors.white.withValues(alpha: .01)),
),
),
),
),
),
locale: windowAppState.languageCode != null
? Locale(windowAppState.languageCode!, windowAppState.countryCode)
: null,
debugShowCheckedModeBanner: false,
),
defaultButtonStyle: ButtonStyle(
shape: WidgetStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(color: Colors.white.withValues(alpha: .01)))),
))),
locale: windowAppState.languageCode != null
? Locale(windowAppState.languageCode!, windowAppState.countryCode)
: null,
debugShowCheckedModeBanner: false,
),
);
}
}
/// Extension methods for WindowController to add custom functionality
extension WindowControllerExtension on WindowController {
/// Initialize custom window method handlers
Future<void> doCustomInitialize() async {
windowManager.ensureInitialized();
return await setWindowMethodHandler((call) async {
switch (call.method) {
case 'window_center':
return await windowManager.center();
case 'window_close':
return await windowManager.close();
case 'window_show':
return await windowManager.show();
case 'window_hide':
return await windowManager.hide();
case 'window_focus':
return await windowManager.focus();
case 'window_set_frame':
final args = call.arguments as Map;
return await windowManager.setBounds(
Rect.fromLTWH(
args['left'] as double,
args['top'] as double,
args['width'] as double,
args['height'] as double,
),
);
case 'window_set_title':
return await windowManager.setTitle(call.arguments as String);
default:
throw MissingPluginException('Not implemented: ${call.method}');
}
});
}
/// Center the window
Future<void> center() {
return invokeMethod('window_center');
}
/// Close the window
void close() async {
await invokeMethod('window_close');
}
/// Show the window
Future<void> show() {
return invokeMethod('window_show');
}
/// Hide the window
Future<void> hide() {
return invokeMethod('window_hide');
}
/// Focus the window
Future<void> focus() {
return invokeMethod('window_focus');
}
/// Set window frame (position and size)
Future<void> setFrame(Rect frame) {
return invokeMethod('window_set_frame', {
'left': frame.left,
'top': frame.top,
'width': frame.width,
'height': frame.height,
});
}
/// Set window title
Future<void> setTitle(String title) {
return invokeMethod('window_set_title', title);
));
}
}

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$MultiWindowAppState {
String get backgroundColor; String get menuColor; String get micaColor; List<String> get gameInstallPaths; String? get languageCode; String? get countryCode; dynamic get windowsVersion;
String get backgroundColor; String get menuColor; String get micaColor; List<String> get gameInstallPaths; String? get languageCode; String? get countryCode;
/// Create a copy of MultiWindowAppState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $MultiWindowAppStateCopyWith<MultiWindowAppState> get copyWith => _$MultiWindowA
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is MultiWindowAppState&&(identical(other.backgroundColor, backgroundColor) || other.backgroundColor == backgroundColor)&&(identical(other.menuColor, menuColor) || other.menuColor == menuColor)&&(identical(other.micaColor, micaColor) || other.micaColor == micaColor)&&const DeepCollectionEquality().equals(other.gameInstallPaths, gameInstallPaths)&&(identical(other.languageCode, languageCode) || other.languageCode == languageCode)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&const DeepCollectionEquality().equals(other.windowsVersion, windowsVersion));
return identical(this, other) || (other.runtimeType == runtimeType&&other is MultiWindowAppState&&(identical(other.backgroundColor, backgroundColor) || other.backgroundColor == backgroundColor)&&(identical(other.menuColor, menuColor) || other.menuColor == menuColor)&&(identical(other.micaColor, micaColor) || other.micaColor == micaColor)&&const DeepCollectionEquality().equals(other.gameInstallPaths, gameInstallPaths)&&(identical(other.languageCode, languageCode) || other.languageCode == languageCode)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,backgroundColor,menuColor,micaColor,const DeepCollectionEquality().hash(gameInstallPaths),languageCode,countryCode,const DeepCollectionEquality().hash(windowsVersion));
int get hashCode => Object.hash(runtimeType,backgroundColor,menuColor,micaColor,const DeepCollectionEquality().hash(gameInstallPaths),languageCode,countryCode);
@override
String toString() {
return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, gameInstallPaths: $gameInstallPaths, languageCode: $languageCode, countryCode: $countryCode, windowsVersion: $windowsVersion)';
return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, gameInstallPaths: $gameInstallPaths, languageCode: $languageCode, countryCode: $countryCode)';
}
@@ -48,7 +48,7 @@ abstract mixin class $MultiWindowAppStateCopyWith<$Res> {
factory $MultiWindowAppStateCopyWith(MultiWindowAppState value, $Res Function(MultiWindowAppState) _then) = _$MultiWindowAppStateCopyWithImpl;
@useResult
$Res call({
String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion
String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode
});
@@ -65,7 +65,7 @@ class _$MultiWindowAppStateCopyWithImpl<$Res>
/// Create a copy of MultiWindowAppState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? backgroundColor = null,Object? menuColor = null,Object? micaColor = null,Object? gameInstallPaths = null,Object? languageCode = freezed,Object? countryCode = freezed,Object? windowsVersion = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? backgroundColor = null,Object? menuColor = null,Object? micaColor = null,Object? gameInstallPaths = null,Object? languageCode = freezed,Object? countryCode = freezed,}) {
return _then(_self.copyWith(
backgroundColor: null == backgroundColor ? _self.backgroundColor : backgroundColor // ignore: cast_nullable_to_non_nullable
as String,menuColor: null == menuColor ? _self.menuColor : menuColor // ignore: cast_nullable_to_non_nullable
@@ -73,8 +73,7 @@ as String,micaColor: null == micaColor ? _self.micaColor : micaColor // ignore:
as String,gameInstallPaths: null == gameInstallPaths ? _self.gameInstallPaths : gameInstallPaths // ignore: cast_nullable_to_non_nullable
as List<String>,languageCode: freezed == languageCode ? _self.languageCode : languageCode // ignore: cast_nullable_to_non_nullable
as String?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,windowsVersion: freezed == windowsVersion ? _self.windowsVersion : windowsVersion // ignore: cast_nullable_to_non_nullable
as dynamic,
as String?,
));
}
@@ -159,10 +158,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _MultiWindowAppState() when $default != null:
return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode,_that.windowsVersion);case _:
return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode);case _:
return orElse();
}
@@ -180,10 +179,10 @@ return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.game
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode) $default,) {final _that = this;
switch (_that) {
case _MultiWindowAppState():
return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode,_that.windowsVersion);case _:
return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode);case _:
throw StateError('Unexpected subclass');
}
@@ -200,10 +199,10 @@ return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.game
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode)? $default,) {final _that = this;
switch (_that) {
case _MultiWindowAppState() when $default != null:
return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode,_that.windowsVersion);case _:
return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode);case _:
return null;
}
@@ -215,7 +214,7 @@ return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.game
@JsonSerializable()
class _MultiWindowAppState implements MultiWindowAppState {
const _MultiWindowAppState({required this.backgroundColor, required this.menuColor, required this.micaColor, required final List<String> gameInstallPaths, this.languageCode, this.countryCode, this.windowsVersion = 10}): _gameInstallPaths = gameInstallPaths;
const _MultiWindowAppState({required this.backgroundColor, required this.menuColor, required this.micaColor, required final List<String> gameInstallPaths, this.languageCode, this.countryCode}): _gameInstallPaths = gameInstallPaths;
factory _MultiWindowAppState.fromJson(Map<String, dynamic> json) => _$MultiWindowAppStateFromJson(json);
@override final String backgroundColor;
@@ -230,7 +229,6 @@ class _MultiWindowAppState implements MultiWindowAppState {
@override final String? languageCode;
@override final String? countryCode;
@override@JsonKey() final dynamic windowsVersion;
/// Create a copy of MultiWindowAppState
/// with the given fields replaced by the non-null parameter values.
@@ -245,16 +243,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MultiWindowAppState&&(identical(other.backgroundColor, backgroundColor) || other.backgroundColor == backgroundColor)&&(identical(other.menuColor, menuColor) || other.menuColor == menuColor)&&(identical(other.micaColor, micaColor) || other.micaColor == micaColor)&&const DeepCollectionEquality().equals(other._gameInstallPaths, _gameInstallPaths)&&(identical(other.languageCode, languageCode) || other.languageCode == languageCode)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&const DeepCollectionEquality().equals(other.windowsVersion, windowsVersion));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MultiWindowAppState&&(identical(other.backgroundColor, backgroundColor) || other.backgroundColor == backgroundColor)&&(identical(other.menuColor, menuColor) || other.menuColor == menuColor)&&(identical(other.micaColor, micaColor) || other.micaColor == micaColor)&&const DeepCollectionEquality().equals(other._gameInstallPaths, _gameInstallPaths)&&(identical(other.languageCode, languageCode) || other.languageCode == languageCode)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,backgroundColor,menuColor,micaColor,const DeepCollectionEquality().hash(_gameInstallPaths),languageCode,countryCode,const DeepCollectionEquality().hash(windowsVersion));
int get hashCode => Object.hash(runtimeType,backgroundColor,menuColor,micaColor,const DeepCollectionEquality().hash(_gameInstallPaths),languageCode,countryCode);
@override
String toString() {
return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, gameInstallPaths: $gameInstallPaths, languageCode: $languageCode, countryCode: $countryCode, windowsVersion: $windowsVersion)';
return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, gameInstallPaths: $gameInstallPaths, languageCode: $languageCode, countryCode: $countryCode)';
}
@@ -265,7 +263,7 @@ abstract mixin class _$MultiWindowAppStateCopyWith<$Res> implements $MultiWindow
factory _$MultiWindowAppStateCopyWith(_MultiWindowAppState value, $Res Function(_MultiWindowAppState) _then) = __$MultiWindowAppStateCopyWithImpl;
@override @useResult
$Res call({
String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion
String backgroundColor, String menuColor, String micaColor, List<String> gameInstallPaths, String? languageCode, String? countryCode
});
@@ -282,7 +280,7 @@ class __$MultiWindowAppStateCopyWithImpl<$Res>
/// Create a copy of MultiWindowAppState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? backgroundColor = null,Object? menuColor = null,Object? micaColor = null,Object? gameInstallPaths = null,Object? languageCode = freezed,Object? countryCode = freezed,Object? windowsVersion = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? backgroundColor = null,Object? menuColor = null,Object? micaColor = null,Object? gameInstallPaths = null,Object? languageCode = freezed,Object? countryCode = freezed,}) {
return _then(_MultiWindowAppState(
backgroundColor: null == backgroundColor ? _self.backgroundColor : backgroundColor // ignore: cast_nullable_to_non_nullable
as String,menuColor: null == menuColor ? _self.menuColor : menuColor // ignore: cast_nullable_to_non_nullable
@@ -290,8 +288,7 @@ as String,micaColor: null == micaColor ? _self.micaColor : micaColor // ignore:
as String,gameInstallPaths: null == gameInstallPaths ? _self._gameInstallPaths : gameInstallPaths // ignore: cast_nullable_to_non_nullable
as List<String>,languageCode: freezed == languageCode ? _self.languageCode : languageCode // ignore: cast_nullable_to_non_nullable
as String?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,windowsVersion: freezed == windowsVersion ? _self.windowsVersion : windowsVersion // ignore: cast_nullable_to_non_nullable
as dynamic,
as String?,
));
}

View File

@@ -16,7 +16,6 @@ _MultiWindowAppState _$MultiWindowAppStateFromJson(Map<String, dynamic> json) =>
.toList(),
languageCode: json['languageCode'] as String?,
countryCode: json['countryCode'] as String?,
windowsVersion: json['windowsVersion'] ?? 10,
);
Map<String, dynamic> _$MultiWindowAppStateToJson(
@@ -28,5 +27,4 @@ Map<String, dynamic> _$MultiWindowAppStateToJson(
'gameInstallPaths': instance.gameInstallPaths,
'languageCode': instance.languageCode,
'countryCode': instance.countryCode,
'windowsVersion': instance.windowsVersion,
};

View File

@@ -1,121 +0,0 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/ui/auth/auth_page.dart';
/// URL Scheme handler for deep linking
/// Handles: sctoolbox://auth?callbackUrl=https://example.com
class UrlSchemeHandler {
static final UrlSchemeHandler _instance = UrlSchemeHandler._internal();
factory UrlSchemeHandler() => _instance;
UrlSchemeHandler._internal();
final _appLinks = AppLinks();
StreamSubscription<Uri>? _linkSubscription;
BuildContext? _context;
// Debouncing variables
String? _lastHandledUri;
DateTime? _lastHandledTime;
static const _debounceDuration = Duration(seconds: 2);
/// Initialize URL scheme handler
Future<void> initialize(BuildContext context) async {
_context = context;
// Handle initial link when app is launched via URL scheme
try {
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
dPrint('Initial URI: $initialUri');
_handleUri(initialUri);
}
} catch (e) {
dPrint('Failed to get initial URI: $e');
}
// Handle links while app is running
_linkSubscription = _appLinks.uriLinkStream.listen(
(uri) {
dPrint('Received URI: $uri');
_handleUri(uri);
},
onError: (err) {
dPrint('URI link stream error: $err');
},
);
}
/// Handle incoming URI with debouncing
void _handleUri(Uri uri) {
final uriString = uri.toString();
final now = DateTime.now();
// Check if this is a duplicate URI within debounce duration
if (_lastHandledUri == uriString && _lastHandledTime != null) {
final timeSinceLastHandle = now.difference(_lastHandledTime!);
if (timeSinceLastHandle < _debounceDuration) {
dPrint('Debounced duplicate URI: $uriString (${timeSinceLastHandle.inMilliseconds}ms since last)');
return;
}
}
// Update last handled URI and time
_lastHandledUri = uriString;
_lastHandledTime = now;
dPrint('Handling URI: $uri');
// Check if it's an auth request
// Check if it's an auth request
// Expected format: sctoolbox://auth?callbackUrl=https://example.com&state=...&nonce=...
// Note: old format with domain in path (sctoolbox://auth/domain?...) is also supported but domain is ignored
if (uri.scheme == 'sctoolbox' && uri.host == 'auth') {
final callbackUrl = uri.queryParameters['callbackUrl'];
final state = uri.queryParameters['state'];
final nonce = uri.queryParameters['nonce'];
if (callbackUrl == null || callbackUrl.isEmpty) {
dPrint('Invalid auth URI: missing callbackUrl parameter');
return;
}
if (state == null || state.isEmpty) {
dPrint('Invalid auth URI: missing state parameter');
return;
}
dPrint('Auth request - callbackUrl: $callbackUrl, state: $state');
_showAuthDialog(callbackUrl, state, nonce);
}
}
/// Show auth dialog
void _showAuthDialog(String callbackUrl, String state, String? nonce) {
if (_context == null || !_context!.mounted) {
dPrint('Cannot show auth dialog: context not available');
return;
}
showDialog(
context: _context!,
builder: (context) => AuthPage(callbackUrl: callbackUrl, stateParameter: state, nonce: nonce),
);
}
/// Dispose the handler
void dispose() {
_linkSubscription?.cancel();
_linkSubscription = null;
_context = null;
_lastHandledUri = null;
_lastHandledTime = null;
}
/// Update context (useful when switching screens)
void updateContext(BuildContext context) {
_context = context;
}
}

View File

@@ -2,25 +2,46 @@
/// size : 524288
/// compressedSize : 169812
/// isDirectory : false
/// isFile : true
/// isEncrypted : false
/// isUnicodeText : false
/// dateTime : "2019-12-16T15:11:18"
/// version : 45
class AppUnp4kP4kItemData {
AppUnp4kP4kItemData({this.name, this.size, this.compressedSize, this.isDirectory, this.dateModified});
AppUnp4kP4kItemData({
this.name,
this.size,
this.compressedSize,
this.isDirectory,
this.isFile,
this.isEncrypted,
this.isUnicodeText,
this.dateTime,
this.version,
});
AppUnp4kP4kItemData.fromJson(dynamic json) {
name = json['name'];
size = json['size'];
compressedSize = json['compressedSize'];
isDirectory = json['isDirectory'];
dateModified = json['dateModified'];
isFile = json['isFile'];
isEncrypted = json['isEncrypted'];
isUnicodeText = json['isUnicodeText'];
dateTime = json['dateTime'];
version = json['version'];
}
String? name;
num? size;
num? compressedSize;
bool? isDirectory;
/// 文件修改时间(毫秒时间戳)
int? dateModified;
bool? isFile;
bool? isEncrypted;
bool? isUnicodeText;
String? dateTime;
num? version;
List<AppUnp4kP4kItemData> children = [];
Map<String, dynamic> toJson() {
@@ -29,7 +50,11 @@ class AppUnp4kP4kItemData {
map['size'] = size;
map['compressedSize'] = compressedSize;
map['isDirectory'] = isDirectory;
map['dateModified'] = dateModified;
map['isFile'] = isFile;
map['isEncrypted'] = isEncrypted;
map['isUnicodeText'] = isUnicodeText;
map['dateTime'] = dateTime;
map['version'] = version;
return map;
}
}

View File

@@ -1,24 +0,0 @@
/// DCB 记录项数据
class DcbRecordData {
final String path;
final int index;
const DcbRecordData({required this.path, required this.index});
}
/// DCB 搜索匹配数据
class DcbSearchMatchData {
final int lineNumber;
final String lineContent;
const DcbSearchMatchData({required this.lineNumber, required this.lineContent});
}
/// DCB 搜索结果数据
class DcbSearchResultData {
final String path;
final int index;
final List<DcbSearchMatchData> matches;
const DcbSearchResultData({required this.path, required this.index, required this.matches});
}

View File

@@ -17,7 +17,6 @@ class GamePerformanceData {
this.min,
this.value,
this.group,
this.defaultValue,
});
GamePerformanceData.fromJson(dynamic json) {
@@ -29,8 +28,6 @@ class GamePerformanceData {
min = json['min'];
value = json['value'];
group = json['group'];
// Store the initial value as default value
defaultValue = json['value'];
}
String? key;
String? name;
@@ -40,7 +37,6 @@ class GamePerformanceData {
num? min;
num? value;
String? group;
num? defaultValue;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,4 @@ class NoL10n {
static const String langFR = 'Français';
static const String langRU = 'Русский';
static const String langCodeZhCn = 'zh_CN';
static const String aniCatTitle = '【寰宇周刊】';
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
// This is a generated file - do not edit.
//
// Generated from proto/auth/auth.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports

View File

@@ -1,341 +0,0 @@
// This is a generated file - do not edit.
//
// Generated from proto/auth/auth.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:async' as $async;
import 'dart:core' as $core;
import 'package:grpc/service_api.dart' as $grpc;
import 'package:protobuf/protobuf.dart' as $pb;
import 'auth.pb.dart' as $0;
export 'auth.pb.dart';
/// 认证服务
@$pb.GrpcServiceName('auth.AuthService')
class AuthServiceClient extends $grpc.Client {
/// The hostname for this service.
static const $core.String defaultHost = '';
/// OAuth scopes needed for the client.
static const $core.List<$core.String> oauthScopes = [
'',
];
AuthServiceClient(super.channel, {super.options, super.interceptors});
/// 获取服务状态(匿名接口)
$grpc.ResponseFuture<$0.StatusResponse> status(
$0.StatusRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$status, request, options: options);
}
/// 获取当前账号状态(需认证)
$grpc.ResponseFuture<$0.LoginResponse> login(
$0.LoginRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$login, request, options: options);
}
/// 请求注册(匿名接口)
$grpc.ResponseFuture<$0.PreRegisterResponse> preRegister(
$0.PreRegisterRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$preRegister, request, options: options);
}
/// 注册账号(匿名接口)
$grpc.ResponseFuture<$0.RegisterResponse> register(
$0.RegisterRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$register, request, options: options);
}
/// 注销账号(需认证)
$grpc.ResponseFuture<$0.UnregisterResponse> unregister(
$0.UnregisterRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$unregister, request, options: options);
}
/// 验证 JWT token
$grpc.ResponseFuture<$0.ValidateTokenResponse> validateToken(
$0.ValidateTokenRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$validateToken, request, options: options);
}
/// 获取公钥信息
$grpc.ResponseFuture<$0.GetPublicKeyResponse> getPublicKey(
$0.GetPublicKeyRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$getPublicKey, request, options: options);
}
$grpc.ResponseFuture<$0.GetJWTDomainListResponse> getJWTDomainList(
$0.GetJWTDomainListRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$getJWTDomainList, request, options: options);
}
/// 刷新用户资料(需要认证)
$grpc.ResponseFuture<$0.RefreshUserProfileResponse> refreshUserProfile(
$0.RefreshUserProfileRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$refreshUserProfile, request, options: options);
}
/// 生成 OIDC 授权码(供客户端 App 使用)
$grpc.ResponseFuture<$0.GenerateOIDCAuthCodeResponse> generateOIDCAuthCode(
$0.GenerateOIDCAuthCodeRequest request, {
$grpc.CallOptions? options,
}) {
return $createUnaryCall(_$generateOIDCAuthCode, request, options: options);
}
// method descriptors
static final _$status =
$grpc.ClientMethod<$0.StatusRequest, $0.StatusResponse>(
'/auth.AuthService/Status',
($0.StatusRequest value) => value.writeToBuffer(),
$0.StatusResponse.fromBuffer);
static final _$login = $grpc.ClientMethod<$0.LoginRequest, $0.LoginResponse>(
'/auth.AuthService/Login',
($0.LoginRequest value) => value.writeToBuffer(),
$0.LoginResponse.fromBuffer);
static final _$preRegister =
$grpc.ClientMethod<$0.PreRegisterRequest, $0.PreRegisterResponse>(
'/auth.AuthService/PreRegister',
($0.PreRegisterRequest value) => value.writeToBuffer(),
$0.PreRegisterResponse.fromBuffer);
static final _$register =
$grpc.ClientMethod<$0.RegisterRequest, $0.RegisterResponse>(
'/auth.AuthService/Register',
($0.RegisterRequest value) => value.writeToBuffer(),
$0.RegisterResponse.fromBuffer);
static final _$unregister =
$grpc.ClientMethod<$0.UnregisterRequest, $0.UnregisterResponse>(
'/auth.AuthService/Unregister',
($0.UnregisterRequest value) => value.writeToBuffer(),
$0.UnregisterResponse.fromBuffer);
static final _$validateToken =
$grpc.ClientMethod<$0.ValidateTokenRequest, $0.ValidateTokenResponse>(
'/auth.AuthService/ValidateToken',
($0.ValidateTokenRequest value) => value.writeToBuffer(),
$0.ValidateTokenResponse.fromBuffer);
static final _$getPublicKey =
$grpc.ClientMethod<$0.GetPublicKeyRequest, $0.GetPublicKeyResponse>(
'/auth.AuthService/GetPublicKey',
($0.GetPublicKeyRequest value) => value.writeToBuffer(),
$0.GetPublicKeyResponse.fromBuffer);
static final _$getJWTDomainList = $grpc.ClientMethod<
$0.GetJWTDomainListRequest, $0.GetJWTDomainListResponse>(
'/auth.AuthService/GetJWTDomainList',
($0.GetJWTDomainListRequest value) => value.writeToBuffer(),
$0.GetJWTDomainListResponse.fromBuffer);
static final _$refreshUserProfile = $grpc.ClientMethod<
$0.RefreshUserProfileRequest, $0.RefreshUserProfileResponse>(
'/auth.AuthService/RefreshUserProfile',
($0.RefreshUserProfileRequest value) => value.writeToBuffer(),
$0.RefreshUserProfileResponse.fromBuffer);
static final _$generateOIDCAuthCode = $grpc.ClientMethod<
$0.GenerateOIDCAuthCodeRequest, $0.GenerateOIDCAuthCodeResponse>(
'/auth.AuthService/GenerateOIDCAuthCode',
($0.GenerateOIDCAuthCodeRequest value) => value.writeToBuffer(),
$0.GenerateOIDCAuthCodeResponse.fromBuffer);
}
@$pb.GrpcServiceName('auth.AuthService')
abstract class AuthServiceBase extends $grpc.Service {
$core.String get $name => 'auth.AuthService';
AuthServiceBase() {
$addMethod($grpc.ServiceMethod<$0.StatusRequest, $0.StatusResponse>(
'Status',
status_Pre,
false,
false,
($core.List<$core.int> value) => $0.StatusRequest.fromBuffer(value),
($0.StatusResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.LoginRequest, $0.LoginResponse>(
'Login',
login_Pre,
false,
false,
($core.List<$core.int> value) => $0.LoginRequest.fromBuffer(value),
($0.LoginResponse value) => value.writeToBuffer()));
$addMethod(
$grpc.ServiceMethod<$0.PreRegisterRequest, $0.PreRegisterResponse>(
'PreRegister',
preRegister_Pre,
false,
false,
($core.List<$core.int> value) =>
$0.PreRegisterRequest.fromBuffer(value),
($0.PreRegisterResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.RegisterRequest, $0.RegisterResponse>(
'Register',
register_Pre,
false,
false,
($core.List<$core.int> value) => $0.RegisterRequest.fromBuffer(value),
($0.RegisterResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.UnregisterRequest, $0.UnregisterResponse>(
'Unregister',
unregister_Pre,
false,
false,
($core.List<$core.int> value) => $0.UnregisterRequest.fromBuffer(value),
($0.UnregisterResponse value) => value.writeToBuffer()));
$addMethod(
$grpc.ServiceMethod<$0.ValidateTokenRequest, $0.ValidateTokenResponse>(
'ValidateToken',
validateToken_Pre,
false,
false,
($core.List<$core.int> value) =>
$0.ValidateTokenRequest.fromBuffer(value),
($0.ValidateTokenResponse value) => value.writeToBuffer()));
$addMethod(
$grpc.ServiceMethod<$0.GetPublicKeyRequest, $0.GetPublicKeyResponse>(
'GetPublicKey',
getPublicKey_Pre,
false,
false,
($core.List<$core.int> value) =>
$0.GetPublicKeyRequest.fromBuffer(value),
($0.GetPublicKeyResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.GetJWTDomainListRequest,
$0.GetJWTDomainListResponse>(
'GetJWTDomainList',
getJWTDomainList_Pre,
false,
false,
($core.List<$core.int> value) =>
$0.GetJWTDomainListRequest.fromBuffer(value),
($0.GetJWTDomainListResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.RefreshUserProfileRequest,
$0.RefreshUserProfileResponse>(
'RefreshUserProfile',
refreshUserProfile_Pre,
false,
false,
($core.List<$core.int> value) =>
$0.RefreshUserProfileRequest.fromBuffer(value),
($0.RefreshUserProfileResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.GenerateOIDCAuthCodeRequest,
$0.GenerateOIDCAuthCodeResponse>(
'GenerateOIDCAuthCode',
generateOIDCAuthCode_Pre,
false,
false,
($core.List<$core.int> value) =>
$0.GenerateOIDCAuthCodeRequest.fromBuffer(value),
($0.GenerateOIDCAuthCodeResponse value) => value.writeToBuffer()));
}
$async.Future<$0.StatusResponse> status_Pre(
$grpc.ServiceCall $call, $async.Future<$0.StatusRequest> $request) async {
return status($call, await $request);
}
$async.Future<$0.StatusResponse> status(
$grpc.ServiceCall call, $0.StatusRequest request);
$async.Future<$0.LoginResponse> login_Pre(
$grpc.ServiceCall $call, $async.Future<$0.LoginRequest> $request) async {
return login($call, await $request);
}
$async.Future<$0.LoginResponse> login(
$grpc.ServiceCall call, $0.LoginRequest request);
$async.Future<$0.PreRegisterResponse> preRegister_Pre($grpc.ServiceCall $call,
$async.Future<$0.PreRegisterRequest> $request) async {
return preRegister($call, await $request);
}
$async.Future<$0.PreRegisterResponse> preRegister(
$grpc.ServiceCall call, $0.PreRegisterRequest request);
$async.Future<$0.RegisterResponse> register_Pre($grpc.ServiceCall $call,
$async.Future<$0.RegisterRequest> $request) async {
return register($call, await $request);
}
$async.Future<$0.RegisterResponse> register(
$grpc.ServiceCall call, $0.RegisterRequest request);
$async.Future<$0.UnregisterResponse> unregister_Pre($grpc.ServiceCall $call,
$async.Future<$0.UnregisterRequest> $request) async {
return unregister($call, await $request);
}
$async.Future<$0.UnregisterResponse> unregister(
$grpc.ServiceCall call, $0.UnregisterRequest request);
$async.Future<$0.ValidateTokenResponse> validateToken_Pre(
$grpc.ServiceCall $call,
$async.Future<$0.ValidateTokenRequest> $request) async {
return validateToken($call, await $request);
}
$async.Future<$0.ValidateTokenResponse> validateToken(
$grpc.ServiceCall call, $0.ValidateTokenRequest request);
$async.Future<$0.GetPublicKeyResponse> getPublicKey_Pre(
$grpc.ServiceCall $call,
$async.Future<$0.GetPublicKeyRequest> $request) async {
return getPublicKey($call, await $request);
}
$async.Future<$0.GetPublicKeyResponse> getPublicKey(
$grpc.ServiceCall call, $0.GetPublicKeyRequest request);
$async.Future<$0.GetJWTDomainListResponse> getJWTDomainList_Pre(
$grpc.ServiceCall $call,
$async.Future<$0.GetJWTDomainListRequest> $request) async {
return getJWTDomainList($call, await $request);
}
$async.Future<$0.GetJWTDomainListResponse> getJWTDomainList(
$grpc.ServiceCall call, $0.GetJWTDomainListRequest request);
$async.Future<$0.RefreshUserProfileResponse> refreshUserProfile_Pre(
$grpc.ServiceCall $call,
$async.Future<$0.RefreshUserProfileRequest> $request) async {
return refreshUserProfile($call, await $request);
}
$async.Future<$0.RefreshUserProfileResponse> refreshUserProfile(
$grpc.ServiceCall call, $0.RefreshUserProfileRequest request);
$async.Future<$0.GenerateOIDCAuthCodeResponse> generateOIDCAuthCode_Pre(
$grpc.ServiceCall $call,
$async.Future<$0.GenerateOIDCAuthCodeRequest> $request) async {
return generateOIDCAuthCode($call, await $request);
}
$async.Future<$0.GenerateOIDCAuthCodeResponse> generateOIDCAuthCode(
$grpc.ServiceCall call, $0.GenerateOIDCAuthCodeRequest request);
}

View File

@@ -1,351 +0,0 @@
// This is a generated file - do not edit.
//
// Generated from proto/auth/auth.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
// ignore_for_file: unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use generateOIDCAuthCodeRequestDescriptor instead')
const GenerateOIDCAuthCodeRequest$json = {
'1': 'GenerateOIDCAuthCodeRequest',
'2': [
{'1': 'nonce', '3': 1, '4': 1, '5': 9, '10': 'nonce'},
{'1': 'redirect_uri', '3': 2, '4': 1, '5': 9, '10': 'redirectUri'},
],
};
/// Descriptor for `GenerateOIDCAuthCodeRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List generateOIDCAuthCodeRequestDescriptor =
$convert.base64Decode(
'ChtHZW5lcmF0ZU9JRENBdXRoQ29kZVJlcXVlc3QSFAoFbm9uY2UYASABKAlSBW5vbmNlEiEKDH'
'JlZGlyZWN0X3VyaRgCIAEoCVILcmVkaXJlY3RVcmk=');
@$core.Deprecated('Use generateOIDCAuthCodeResponseDescriptor instead')
const GenerateOIDCAuthCodeResponse$json = {
'1': 'GenerateOIDCAuthCodeResponse',
'2': [
{'1': 'code', '3': 1, '4': 1, '5': 9, '10': 'code'},
{'1': 'expires_at', '3': 2, '4': 1, '5': 3, '10': 'expiresAt'},
],
};
/// Descriptor for `GenerateOIDCAuthCodeResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List generateOIDCAuthCodeResponseDescriptor =
$convert.base64Decode(
'ChxHZW5lcmF0ZU9JRENBdXRoQ29kZVJlc3BvbnNlEhIKBGNvZGUYASABKAlSBGNvZGUSHQoKZX'
'hwaXJlc19hdBgCIAEoA1IJZXhwaXJlc0F0');
@$core.Deprecated('Use refreshUserProfileRequestDescriptor instead')
const RefreshUserProfileRequest$json = {
'1': 'RefreshUserProfileRequest',
};
/// Descriptor for `RefreshUserProfileRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List refreshUserProfileRequestDescriptor =
$convert.base64Decode('ChlSZWZyZXNoVXNlclByb2ZpbGVSZXF1ZXN0');
@$core.Deprecated('Use refreshUserProfileResponseDescriptor instead')
const RefreshUserProfileResponse$json = {
'1': 'RefreshUserProfileResponse',
'2': [
{'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'},
{
'1': 'user_info',
'3': 2,
'4': 1,
'5': 11,
'6': '.auth.GameUserInfo',
'10': 'userInfo'
},
],
};
/// Descriptor for `RefreshUserProfileResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List refreshUserProfileResponseDescriptor =
$convert.base64Decode(
'ChpSZWZyZXNoVXNlclByb2ZpbGVSZXNwb25zZRIYCgdzdWNjZXNzGAEgASgIUgdzdWNjZXNzEi'
'8KCXVzZXJfaW5mbxgCIAEoCzISLmF1dGguR2FtZVVzZXJJbmZvUgh1c2VySW5mbw==');
@$core.Deprecated('Use statusRequestDescriptor instead')
const StatusRequest$json = {
'1': 'StatusRequest',
};
/// Descriptor for `StatusRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List statusRequestDescriptor =
$convert.base64Decode('Cg1TdGF0dXNSZXF1ZXN0');
@$core.Deprecated('Use statusResponseDescriptor instead')
const StatusResponse$json = {
'1': 'StatusResponse',
'2': [
{'1': 'online', '3': 1, '4': 1, '5': 8, '10': 'online'},
{'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'},
{'1': 'server_time', '3': 3, '4': 1, '5': 3, '10': 'serverTime'},
],
};
/// Descriptor for `StatusResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List statusResponseDescriptor = $convert.base64Decode(
'Cg5TdGF0dXNSZXNwb25zZRIWCgZvbmxpbmUYASABKAhSBm9ubGluZRIYCgdtZXNzYWdlGAIgAS'
'gJUgdtZXNzYWdlEh8KC3NlcnZlcl90aW1lGAMgASgDUgpzZXJ2ZXJUaW1l');
@$core.Deprecated('Use loginRequestDescriptor instead')
const LoginRequest$json = {
'1': 'LoginRequest',
};
/// Descriptor for `LoginRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List loginRequestDescriptor =
$convert.base64Decode('CgxMb2dpblJlcXVlc3Q=');
@$core.Deprecated('Use gameUserInfoDescriptor instead')
const GameUserInfo$json = {
'1': 'GameUserInfo',
'2': [
{'1': 'game_user_id', '3': 1, '4': 1, '5': 9, '10': 'gameUserId'},
{'1': 'handle_name', '3': 2, '4': 1, '5': 9, '10': 'handleName'},
{'1': 'avatar_url', '3': 3, '4': 1, '5': 9, '10': 'avatarUrl'},
{'1': 'citizen_record', '3': 4, '4': 1, '5': 9, '10': 'citizenRecord'},
{'1': 'enlisted_date', '3': 5, '4': 1, '5': 3, '10': 'enlistedDate'},
],
};
/// Descriptor for `GameUserInfo`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List gameUserInfoDescriptor = $convert.base64Decode(
'CgxHYW1lVXNlckluZm8SIAoMZ2FtZV91c2VyX2lkGAEgASgJUgpnYW1lVXNlcklkEh8KC2hhbm'
'RsZV9uYW1lGAIgASgJUgpoYW5kbGVOYW1lEh0KCmF2YXRhcl91cmwYAyABKAlSCWF2YXRhclVy'
'bBIlCg5jaXRpemVuX3JlY29yZBgEIAEoCVINY2l0aXplblJlY29yZBIjCg1lbmxpc3RlZF9kYX'
'RlGAUgASgDUgxlbmxpc3RlZERhdGU=');
@$core.Deprecated('Use loginResponseDescriptor instead')
const LoginResponse$json = {
'1': 'LoginResponse',
'2': [
{'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'},
{
'1': 'user_info',
'3': 2,
'4': 1,
'5': 11,
'6': '.auth.GameUserInfo',
'10': 'userInfo'
},
{'1': 'last_login_time', '3': 3, '4': 1, '5': 3, '10': 'lastLoginTime'},
],
};
/// Descriptor for `LoginResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List loginResponseDescriptor = $convert.base64Decode(
'Cg1Mb2dpblJlc3BvbnNlEhIKBHV1aWQYASABKAlSBHV1aWQSLwoJdXNlcl9pbmZvGAIgASgLMh'
'IuYXV0aC5HYW1lVXNlckluZm9SCHVzZXJJbmZvEiYKD2xhc3RfbG9naW5fdGltZRgDIAEoA1IN'
'bGFzdExvZ2luVGltZQ==');
@$core.Deprecated('Use preRegisterRequestDescriptor instead')
const PreRegisterRequest$json = {
'1': 'PreRegisterRequest',
'2': [
{'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'},
{'1': 'game_user_id', '3': 2, '4': 1, '5': 9, '10': 'gameUserId'},
],
};
/// Descriptor for `PreRegisterRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List preRegisterRequestDescriptor = $convert.base64Decode(
'ChJQcmVSZWdpc3RlclJlcXVlc3QSEgoEdXVpZBgBIAEoCVIEdXVpZBIgCgxnYW1lX3VzZXJfaW'
'QYAiABKAlSCmdhbWVVc2VySWQ=');
@$core.Deprecated('Use preRegisterResponseDescriptor instead')
const PreRegisterResponse$json = {
'1': 'PreRegisterResponse',
'2': [
{
'1': 'verification_code',
'3': 1,
'4': 1,
'5': 9,
'10': 'verificationCode'
},
{'1': 'expire_time', '3': 2, '4': 1, '5': 3, '10': 'expireTime'},
],
};
/// Descriptor for `PreRegisterResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List preRegisterResponseDescriptor = $convert.base64Decode(
'ChNQcmVSZWdpc3RlclJlc3BvbnNlEisKEXZlcmlmaWNhdGlvbl9jb2RlGAEgASgJUhB2ZXJpZm'
'ljYXRpb25Db2RlEh8KC2V4cGlyZV90aW1lGAIgASgDUgpleHBpcmVUaW1l');
@$core.Deprecated('Use registerRequestDescriptor instead')
const RegisterRequest$json = {
'1': 'RegisterRequest',
'2': [
{'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'},
{'1': 'game_user_id', '3': 2, '4': 1, '5': 9, '10': 'gameUserId'},
],
};
/// Descriptor for `RegisterRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List registerRequestDescriptor = $convert.base64Decode(
'Cg9SZWdpc3RlclJlcXVlc3QSEgoEdXVpZBgBIAEoCVIEdXVpZBIgCgxnYW1lX3VzZXJfaWQYAi'
'ABKAlSCmdhbWVVc2VySWQ=');
@$core.Deprecated('Use registerResponseDescriptor instead')
const RegisterResponse$json = {
'1': 'RegisterResponse',
'2': [
{
'1': 'party_room_secret_key',
'3': 1,
'4': 1,
'5': 9,
'10': 'partyRoomSecretKey'
},
{
'1': 'user_info',
'3': 2,
'4': 1,
'5': 11,
'6': '.auth.GameUserInfo',
'10': 'userInfo'
},
],
};
/// Descriptor for `RegisterResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List registerResponseDescriptor = $convert.base64Decode(
'ChBSZWdpc3RlclJlc3BvbnNlEjEKFXBhcnR5X3Jvb21fc2VjcmV0X2tleRgBIAEoCVIScGFydH'
'lSb29tU2VjcmV0S2V5Ei8KCXVzZXJfaW5mbxgCIAEoCzISLmF1dGguR2FtZVVzZXJJbmZvUgh1'
'c2VySW5mbw==');
@$core.Deprecated('Use unregisterRequestDescriptor instead')
const UnregisterRequest$json = {
'1': 'UnregisterRequest',
};
/// Descriptor for `UnregisterRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List unregisterRequestDescriptor =
$convert.base64Decode('ChFVbnJlZ2lzdGVyUmVxdWVzdA==');
@$core.Deprecated('Use unregisterResponseDescriptor instead')
const UnregisterResponse$json = {
'1': 'UnregisterResponse',
'2': [
{'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'},
],
};
/// Descriptor for `UnregisterResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List unregisterResponseDescriptor =
$convert.base64Decode(
'ChJVbnJlZ2lzdGVyUmVzcG9uc2USGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2Vzcw==');
@$core.Deprecated('Use validateTokenRequestDescriptor instead')
const ValidateTokenRequest$json = {
'1': 'ValidateTokenRequest',
'2': [
{'1': 'token', '3': 1, '4': 1, '5': 9, '10': 'token'},
],
};
/// Descriptor for `ValidateTokenRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List validateTokenRequestDescriptor =
$convert.base64Decode(
'ChRWYWxpZGF0ZVRva2VuUmVxdWVzdBIUCgV0b2tlbhgBIAEoCVIFdG9rZW4=');
@$core.Deprecated('Use validateTokenResponseDescriptor instead')
const ValidateTokenResponse$json = {
'1': 'ValidateTokenResponse',
'2': [
{'1': 'valid', '3': 1, '4': 1, '5': 8, '10': 'valid'},
{'1': 'domain', '3': 2, '4': 1, '5': 9, '10': 'domain'},
{'1': 'issued_at', '3': 3, '4': 1, '5': 3, '10': 'issuedAt'},
{'1': 'expires_at', '3': 4, '4': 1, '5': 3, '10': 'expiresAt'},
{'1': 'error_message', '3': 5, '4': 1, '5': 9, '10': 'errorMessage'},
],
};
/// Descriptor for `ValidateTokenResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List validateTokenResponseDescriptor = $convert.base64Decode(
'ChVWYWxpZGF0ZVRva2VuUmVzcG9uc2USFAoFdmFsaWQYASABKAhSBXZhbGlkEhYKBmRvbWFpbh'
'gCIAEoCVIGZG9tYWluEhsKCWlzc3VlZF9hdBgDIAEoA1IIaXNzdWVkQXQSHQoKZXhwaXJlc19h'
'dBgEIAEoA1IJZXhwaXJlc0F0EiMKDWVycm9yX21lc3NhZ2UYBSABKAlSDGVycm9yTWVzc2FnZQ'
'==');
@$core.Deprecated('Use getPublicKeyRequestDescriptor instead')
const GetPublicKeyRequest$json = {
'1': 'GetPublicKeyRequest',
};
/// Descriptor for `GetPublicKeyRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List getPublicKeyRequestDescriptor =
$convert.base64Decode('ChNHZXRQdWJsaWNLZXlSZXF1ZXN0');
@$core.Deprecated('Use getPublicKeyResponseDescriptor instead')
const GetPublicKeyResponse$json = {
'1': 'GetPublicKeyResponse',
'2': [
{'1': 'public_key_pem', '3': 1, '4': 1, '5': 9, '10': 'publicKeyPem'},
{'1': 'key_id', '3': 2, '4': 1, '5': 9, '10': 'keyId'},
{'1': 'algorithm', '3': 3, '4': 1, '5': 9, '10': 'algorithm'},
],
};
/// Descriptor for `GetPublicKeyResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List getPublicKeyResponseDescriptor = $convert.base64Decode(
'ChRHZXRQdWJsaWNLZXlSZXNwb25zZRIkCg5wdWJsaWNfa2V5X3BlbRgBIAEoCVIMcHVibGljS2'
'V5UGVtEhUKBmtleV9pZBgCIAEoCVIFa2V5SWQSHAoJYWxnb3JpdGhtGAMgASgJUglhbGdvcml0'
'aG0=');
@$core.Deprecated('Use jWTDomainInfoDescriptor instead')
const JWTDomainInfo$json = {
'1': 'JWTDomainInfo',
'2': [
{'1': 'domain', '3': 1, '4': 1, '5': 9, '10': 'domain'},
{'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
],
};
/// Descriptor for `JWTDomainInfo`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List jWTDomainInfoDescriptor = $convert.base64Decode(
'Cg1KV1REb21haW5JbmZvEhYKBmRvbWFpbhgBIAEoCVIGZG9tYWluEhIKBG5hbWUYAiABKAlSBG'
'5hbWU=');
@$core.Deprecated('Use getJWTDomainListRequestDescriptor instead')
const GetJWTDomainListRequest$json = {
'1': 'GetJWTDomainListRequest',
};
/// Descriptor for `GetJWTDomainListRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List getJWTDomainListRequestDescriptor =
$convert.base64Decode('ChdHZXRKV1REb21haW5MaXN0UmVxdWVzdA==');
@$core.Deprecated('Use getJWTDomainListResponseDescriptor instead')
const GetJWTDomainListResponse$json = {
'1': 'GetJWTDomainListResponse',
'2': [
{
'1': 'domains',
'3': 1,
'4': 3,
'5': 11,
'6': '.auth.JWTDomainInfo',
'10': 'domains'
},
],
};
/// Descriptor for `GetJWTDomainListResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List getJWTDomainListResponseDescriptor =
$convert.base64Decode(
'ChhHZXRKV1REb21haW5MaXN0UmVzcG9uc2USLQoHZG9tYWlucxgBIAMoCzITLmF1dGguSldURG'
'9tYWluSW5mb1IHZG9tYWlucw==');

View File

@@ -1,601 +0,0 @@
// This is a generated file - do not edit.
//
// Generated from proto/common/common.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb;
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
/// 获取服务器时间请求
class GetServerTimeRequest extends $pb.GeneratedMessage {
factory GetServerTimeRequest() => create();
GetServerTimeRequest._();
factory GetServerTimeRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory GetServerTimeRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'GetServerTimeRequest',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetServerTimeRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetServerTimeRequest copyWith(void Function(GetServerTimeRequest) updates) =>
super.copyWith((message) => updates(message as GetServerTimeRequest))
as GetServerTimeRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetServerTimeRequest create() => GetServerTimeRequest._();
@$core.override
GetServerTimeRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static GetServerTimeRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<GetServerTimeRequest>(create);
static GetServerTimeRequest? _defaultInstance;
}
/// 获取服务器时间响应
class GetServerTimeResponse extends $pb.GeneratedMessage {
factory GetServerTimeResponse({
$fixnum.Int64? timestamp,
$core.String? timezone,
}) {
final result = create();
if (timestamp != null) result.timestamp = timestamp;
if (timezone != null) result.timezone = timezone;
return result;
}
GetServerTimeResponse._();
factory GetServerTimeResponse.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory GetServerTimeResponse.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'GetServerTimeResponse',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'timestamp')
..aOS(2, _omitFieldNames ? '' : 'timezone')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetServerTimeResponse clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetServerTimeResponse copyWith(
void Function(GetServerTimeResponse) updates) =>
super.copyWith((message) => updates(message as GetServerTimeResponse))
as GetServerTimeResponse;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetServerTimeResponse create() => GetServerTimeResponse._();
@$core.override
GetServerTimeResponse createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static GetServerTimeResponse getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<GetServerTimeResponse>(create);
static GetServerTimeResponse? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get timestamp => $_getI64(0);
@$pb.TagNumber(1)
set timestamp($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasTimestamp() => $_has(0);
@$pb.TagNumber(1)
void clearTimestamp() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get timezone => $_getSZ(1);
@$pb.TagNumber(2)
set timezone($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasTimezone() => $_has(1);
@$pb.TagNumber(2)
void clearTimezone() => $_clearField(2);
}
/// 获取版本请求
class GetVersionRequest extends $pb.GeneratedMessage {
factory GetVersionRequest() => create();
GetVersionRequest._();
factory GetVersionRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory GetVersionRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'GetVersionRequest',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetVersionRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetVersionRequest copyWith(void Function(GetVersionRequest) updates) =>
super.copyWith((message) => updates(message as GetVersionRequest))
as GetVersionRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetVersionRequest create() => GetVersionRequest._();
@$core.override
GetVersionRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static GetVersionRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<GetVersionRequest>(create);
static GetVersionRequest? _defaultInstance;
}
/// 获取版本响应
class GetVersionResponse extends $pb.GeneratedMessage {
factory GetVersionResponse({
$core.int? version,
$core.int? latestVersion,
$core.int? minClientVersion,
}) {
final result = create();
if (version != null) result.version = version;
if (latestVersion != null) result.latestVersion = latestVersion;
if (minClientVersion != null) result.minClientVersion = minClientVersion;
return result;
}
GetVersionResponse._();
factory GetVersionResponse.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory GetVersionResponse.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'GetVersionResponse',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..aI(1, _omitFieldNames ? '' : 'version')
..aI(2, _omitFieldNames ? '' : 'latestVersion')
..aI(3, _omitFieldNames ? '' : 'minClientVersion')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetVersionResponse clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetVersionResponse copyWith(void Function(GetVersionResponse) updates) =>
super.copyWith((message) => updates(message as GetVersionResponse))
as GetVersionResponse;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetVersionResponse create() => GetVersionResponse._();
@$core.override
GetVersionResponse createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static GetVersionResponse getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<GetVersionResponse>(create);
static GetVersionResponse? _defaultInstance;
@$pb.TagNumber(1)
$core.int get version => $_getIZ(0);
@$pb.TagNumber(1)
set version($core.int value) => $_setSignedInt32(0, value);
@$pb.TagNumber(1)
$core.bool hasVersion() => $_has(0);
@$pb.TagNumber(1)
void clearVersion() => $_clearField(1);
@$pb.TagNumber(2)
$core.int get latestVersion => $_getIZ(1);
@$pb.TagNumber(2)
set latestVersion($core.int value) => $_setSignedInt32(1, value);
@$pb.TagNumber(2)
$core.bool hasLatestVersion() => $_has(1);
@$pb.TagNumber(2)
void clearLatestVersion() => $_clearField(2);
@$pb.TagNumber(3)
$core.int get minClientVersion => $_getIZ(2);
@$pb.TagNumber(3)
set minClientVersion($core.int value) => $_setSignedInt32(2, value);
@$pb.TagNumber(3)
$core.bool hasMinClientVersion() => $_has(2);
@$pb.TagNumber(3)
void clearMinClientVersion() => $_clearField(3);
}
/// 信号类型
class SignalType extends $pb.GeneratedMessage {
factory SignalType({
$core.String? id,
$core.String? name,
$core.bool? isSpecial,
}) {
final result = create();
if (id != null) result.id = id;
if (name != null) result.name = name;
if (isSpecial != null) result.isSpecial = isSpecial;
return result;
}
SignalType._();
factory SignalType.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory SignalType.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'SignalType',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'id')
..aOS(2, _omitFieldNames ? '' : 'name')
..aOB(3, _omitFieldNames ? '' : 'isSpecial')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SignalType clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SignalType copyWith(void Function(SignalType) updates) =>
super.copyWith((message) => updates(message as SignalType)) as SignalType;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static SignalType create() => SignalType._();
@$core.override
SignalType createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static SignalType getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<SignalType>(create);
static SignalType? _defaultInstance;
@$pb.TagNumber(1)
$core.String get id => $_getSZ(0);
@$pb.TagNumber(1)
set id($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasId() => $_has(0);
@$pb.TagNumber(1)
void clearId() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get name => $_getSZ(1);
@$pb.TagNumber(2)
set name($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasName() => $_has(1);
@$pb.TagNumber(2)
void clearName() => $_clearField(2);
@$pb.TagNumber(3)
$core.bool get isSpecial => $_getBF(2);
@$pb.TagNumber(3)
set isSpecial($core.bool value) => $_setBool(2, value);
@$pb.TagNumber(3)
$core.bool hasIsSpecial() => $_has(2);
@$pb.TagNumber(3)
void clearIsSpecial() => $_clearField(3);
}
/// 获取信号类型请求
class GetSignalTypesRequest extends $pb.GeneratedMessage {
factory GetSignalTypesRequest() => create();
GetSignalTypesRequest._();
factory GetSignalTypesRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory GetSignalTypesRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'GetSignalTypesRequest',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetSignalTypesRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetSignalTypesRequest copyWith(
void Function(GetSignalTypesRequest) updates) =>
super.copyWith((message) => updates(message as GetSignalTypesRequest))
as GetSignalTypesRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetSignalTypesRequest create() => GetSignalTypesRequest._();
@$core.override
GetSignalTypesRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static GetSignalTypesRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<GetSignalTypesRequest>(create);
static GetSignalTypesRequest? _defaultInstance;
}
/// 获取信号类型响应
class GetSignalTypesResponse extends $pb.GeneratedMessage {
factory GetSignalTypesResponse({
$core.Iterable<SignalType>? signals,
}) {
final result = create();
if (signals != null) result.signals.addAll(signals);
return result;
}
GetSignalTypesResponse._();
factory GetSignalTypesResponse.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory GetSignalTypesResponse.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'GetSignalTypesResponse',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..pPM<SignalType>(1, _omitFieldNames ? '' : 'signals',
subBuilder: SignalType.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetSignalTypesResponse clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetSignalTypesResponse copyWith(
void Function(GetSignalTypesResponse) updates) =>
super.copyWith((message) => updates(message as GetSignalTypesResponse))
as GetSignalTypesResponse;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetSignalTypesResponse create() => GetSignalTypesResponse._();
@$core.override
GetSignalTypesResponse createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static GetSignalTypesResponse getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<GetSignalTypesResponse>(create);
static GetSignalTypesResponse? _defaultInstance;
@$pb.TagNumber(1)
$pb.PbList<SignalType> get signals => $_getList(0);
}
/// 标签
class Tag extends $pb.GeneratedMessage {
factory Tag({
$core.String? id,
$core.String? name,
$core.String? info,
$core.String? color,
$core.Iterable<Tag>? subTags,
}) {
final result = create();
if (id != null) result.id = id;
if (name != null) result.name = name;
if (info != null) result.info = info;
if (color != null) result.color = color;
if (subTags != null) result.subTags.addAll(subTags);
return result;
}
Tag._();
factory Tag.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Tag.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Tag',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'id')
..aOS(2, _omitFieldNames ? '' : 'name')
..aOS(3, _omitFieldNames ? '' : 'info')
..aOS(4, _omitFieldNames ? '' : 'color')
..pPM<Tag>(5, _omitFieldNames ? '' : 'subTags', subBuilder: Tag.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Tag clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Tag copyWith(void Function(Tag) updates) =>
super.copyWith((message) => updates(message as Tag)) as Tag;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Tag create() => Tag._();
@$core.override
Tag createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Tag getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Tag>(create);
static Tag? _defaultInstance;
@$pb.TagNumber(1)
$core.String get id => $_getSZ(0);
@$pb.TagNumber(1)
set id($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasId() => $_has(0);
@$pb.TagNumber(1)
void clearId() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get name => $_getSZ(1);
@$pb.TagNumber(2)
set name($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasName() => $_has(1);
@$pb.TagNumber(2)
void clearName() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get info => $_getSZ(2);
@$pb.TagNumber(3)
set info($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasInfo() => $_has(2);
@$pb.TagNumber(3)
void clearInfo() => $_clearField(3);
@$pb.TagNumber(4)
$core.String get color => $_getSZ(3);
@$pb.TagNumber(4)
set color($core.String value) => $_setString(3, value);
@$pb.TagNumber(4)
$core.bool hasColor() => $_has(3);
@$pb.TagNumber(4)
void clearColor() => $_clearField(4);
@$pb.TagNumber(5)
$pb.PbList<Tag> get subTags => $_getList(4);
}
/// 获取标签请求
class GetTagsRequest extends $pb.GeneratedMessage {
factory GetTagsRequest() => create();
GetTagsRequest._();
factory GetTagsRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory GetTagsRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'GetTagsRequest',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetTagsRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetTagsRequest copyWith(void Function(GetTagsRequest) updates) =>
super.copyWith((message) => updates(message as GetTagsRequest))
as GetTagsRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetTagsRequest create() => GetTagsRequest._();
@$core.override
GetTagsRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static GetTagsRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<GetTagsRequest>(create);
static GetTagsRequest? _defaultInstance;
}
/// 获取标签响应
class GetTagsResponse extends $pb.GeneratedMessage {
factory GetTagsResponse({
$core.Iterable<Tag>? tags,
}) {
final result = create();
if (tags != null) result.tags.addAll(tags);
return result;
}
GetTagsResponse._();
factory GetTagsResponse.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory GetTagsResponse.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'GetTagsResponse',
package: const $pb.PackageName(_omitMessageNames ? '' : 'common'),
createEmptyInstance: create)
..pPM<Tag>(1, _omitFieldNames ? '' : 'tags', subBuilder: Tag.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetTagsResponse clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
GetTagsResponse copyWith(void Function(GetTagsResponse) updates) =>
super.copyWith((message) => updates(message as GetTagsResponse))
as GetTagsResponse;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static GetTagsResponse create() => GetTagsResponse._();
@$core.override
GetTagsResponse createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static GetTagsResponse getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<GetTagsResponse>(create);
static GetTagsResponse? _defaultInstance;
@$pb.TagNumber(1)
$pb.PbList<Tag> get tags => $_getList(0);
}
const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');

Some files were not shown because too many files have changed in this diff Show More