Compare commits
14 Commits
dependabot
...
lite_web_v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b7be216a8 | ||
|
|
9e324c01fb | ||
|
|
79cc157e04 | ||
|
|
6d2bb89c64 | ||
|
|
66ab76e784 | ||
|
|
acea7bc68c | ||
|
|
4a8b18fed0 | ||
|
|
16cc835f23 | ||
|
|
6984da58b8 | ||
|
|
71c3b61bdd | ||
|
|
c31b31516f | ||
|
|
476c40f4cd | ||
|
|
193d2c7496 | ||
|
|
334ed424e9 |
77
.github/workflows/linux_nightly.yml
vendored
@@ -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
|
||||
4
.github/workflows/windows_nightly.yml
vendored
@@ -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
@@ -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/
|
||||
25
.metadata
@@ -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
|
||||
|
||||
|
||||
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"dart.flutterSdkPath": ".fvm/versions/stable",
|
||||
"cmake.ignoreCMakeListsMissing": true
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
该工具为 星际公民玩家 提供 一键诊断,官网及工具网站汉化,游戏汉化,游戏性能优化 等功能,致力于带来更愉快的游戏体验。
|
||||
|
||||
[](https://github.com/StarCitizenToolBox/app/actions/workflows/windows_nightly.yml) [](https://github.com/StarCitizenToolBox/app/actions/workflows/windows_nightly.yml) [](http://translate.42kit.com/engage/sctoolbox/)
|
||||
[](https://github.com/StarCitizenToolBox/app/actions/workflows/windows_nightly.yml) [](http://translate.42kit.com/engage/sctoolbox/)
|
||||
|
||||
[](https://apps.microsoft.com/detail/9NF3SWFWNKL1?launch=true)
|
||||
|
||||
|
||||
@@ -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
|
||||

|
||||
@@ -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.
|
||||
BIN
assets/backgrounds/SC_01_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
assets/backgrounds/SC_02_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
assets/backgrounds/SC_03_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 519 KiB |
BIN
assets/backgrounds/SC_04_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
assets/backgrounds/SC_05_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
assets/backgrounds/SC_06_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
assets/backgrounds/SC_07_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
assets/backgrounds/SC_08_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
assets/backgrounds/SC_09_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
assets/backgrounds/SC_10_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
assets/backgrounds/SC_11_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
assets/backgrounds/SC_12_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
assets/backgrounds/SC_13_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 965 KiB |
BIN
assets/backgrounds/SC_14_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
assets/backgrounds/SC_15_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
assets/backgrounds/SC_16_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
assets/backgrounds/SC_17_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/backgrounds/SC_18_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
assets/backgrounds/SC_19_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
assets/backgrounds/SC_20_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
assets/backgrounds/SC_21_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 361 KiB |
BIN
assets/backgrounds/SC_22_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
assets/backgrounds/SC_23_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
assets/backgrounds/SC_24_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 339 KiB |
BIN
assets/backgrounds/SC_25_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
assets/backgrounds/SC_26_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
assets/backgrounds/SC_27_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 339 KiB |
BIN
assets/backgrounds/SC_28_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
assets/backgrounds/SC_29_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
assets/backgrounds/SC_30_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 377 KiB |
BIN
assets/backgrounds/SC_31_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
assets/backgrounds/SC_32_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 611 KiB |
BIN
assets/backgrounds/SC_33_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
assets/backgrounds/SC_34_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
assets/backgrounds/SC_35_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 370 KiB |
BIN
assets/backgrounds/SC_36_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
assets/backgrounds/SC_37_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 414 KiB |
BIN
assets/backgrounds/SC_38_Wallpaper_3840x2160.webp
Normal file
|
After Width: | Height: | Size: 456 KiB |
BIN
assets/binary/aria2c.zip
Normal file
BIN
assets/binary/unp4kc.zip
Normal 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');
|
||||
})();
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
@@ -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");
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
165
lib/app.dart
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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\"\"",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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();
|
||||
@@ -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 }
|
||||
@@ -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 = 合并为单个 XML,false = 分离为多个 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
14
lib/common/utils/file_cache_utils_stub.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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>{};
|
||||
|
||||
@@ -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 = '【寰宇周刊】';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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==');
|
||||
@@ -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');
|
||||