diff --git a/lib/app.dart b/lib/app.dart index e10b723..5121260 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -49,6 +49,7 @@ abstract class AppGlobalState with _$AppGlobalState { @Default(ThemeConf()) ThemeConf themeConf, Locale? appLocale, Box? appConfBox, + @Default(10) windowsVersion, }) = _AppGlobalState; } @@ -56,17 +57,15 @@ abstract class AppGlobalState with _$AppGlobalState { GoRouter router(Ref ref) { return GoRouter( routes: [ - GoRoute( - path: '/', - pageBuilder: (context, state) => myPageBuilder(context, state, const SplashUI()), - ), + GoRoute(path: '/', pageBuilder: (context, state) => myPageBuilder(context, state, const SplashUI())), GoRoute( path: '/index', pageBuilder: (context, state) => myPageBuilder(context, state, const IndexUI()), routes: [ GoRoute( - path: "downloader", - pageBuilder: (context, state) => myPageBuilder(context, state, const HomeDownloaderUI())), + path: "downloader", + pageBuilder: (context, state) => myPageBuilder(context, state, const HomeDownloaderUI()), + ), GoRoute( path: 'game_doctor', pageBuilder: (context, state) => myPageBuilder(context, state, const HomeGameDoctorUI()), @@ -76,17 +75,19 @@ GoRouter router(Ref ref) { pageBuilder: (context, state) => myPageBuilder(context, state, const HomePerformanceUI()), ), GoRoute( - path: 'advanced_localization', - pageBuilder: (context, state) => myPageBuilder(context, state, const AdvancedLocalizationUI())) + path: 'advanced_localization', + pageBuilder: (context, state) => myPageBuilder(context, state, const AdvancedLocalizationUI()), + ), ], ), - GoRoute(path: '/tools', builder: (_, _) => const SizedBox(), routes: [ - GoRoute( - path: 'unp4kc', - pageBuilder: (context, state) => myPageBuilder(context, state, const UnP4kcUI()), - ), - ]), - GoRoute(path: '/guide', pageBuilder: (context, state) => myPageBuilder(context, state, const GuideUI())) + GoRoute( + path: '/tools', + builder: (_, _) => const SizedBox(), + routes: [ + GoRoute(path: 'unp4kc', pageBuilder: (context, state) => myPageBuilder(context, state, const UnP4kcUI())), + ], + ), + GoRoute(path: '/guide', pageBuilder: (context, state) => myPageBuilder(context, state, const GuideUI())), ], ); } @@ -94,13 +95,13 @@ GoRouter router(Ref ref) { @riverpod class AppGlobalModel extends _$AppGlobalModel { static Map get appLocaleSupport => { - const Locale("auto"): S.current.settings_app_language_auto, - const Locale("zh", "CN"): NoL10n.langZHS, - const Locale("zh", "TW"): NoL10n.langZHT, - const Locale("en"): NoL10n.langEn, - const Locale("ja"): NoL10n.langJa, - const Locale("ru"): NoL10n.langRU, - }; + const Locale("auto"): S.current.settings_app_language_auto, + const Locale("zh", "CN"): NoL10n.langZHS, + const Locale("zh", "TW"): NoL10n.langZHT, + const Locale("en"): NoL10n.langEn, + const Locale("ja"): NoL10n.langJa, + const Locale("ru"): NoL10n.langRU, + }; @override AppGlobalState build() { @@ -174,9 +175,9 @@ class AppGlobalModel extends _$AppGlobalModel { await Window.initialize(); await Window.hideWindowControls(); if (windowsDeviceInfo?.productName.contains("Windows 11") ?? false) { - await Window.setEffect( - effect: WindowEffect.acrylic, - ); + await Window.setEffect(effect: WindowEffect.acrylic); + state = state.copyWith(windowsVersion: 11); + dPrint("---- Windows 11 Acrylic Effect init -----"); } } }); @@ -226,18 +227,24 @@ class AppGlobalModel extends _$AppGlobalModel { if (state.networkVersionData == null) { if (!context.mounted) return false; await showToast( - context, S.current.app_common_network_error(ConstConf.appVersionDate, checkUpdateError.toString())); + context, + S.current.app_common_network_error(ConstConf.appVersionDate, checkUpdateError.toString()), + ); return false; } if (!Platform.isWindows) return false; - final lastVersion = - ConstConf.isMSE ? state.networkVersionData?.mSELastVersionCode : state.networkVersionData?.lastVersionCode; + final lastVersion = ConstConf.isMSE + ? state.networkVersionData?.mSELastVersionCode + : state.networkVersionData?.lastVersionCode; if ((lastVersion ?? 0) > ConstConf.appVersionCode) { // need update if (!context.mounted) return false; - final r = - await showDialog(dismissWithEsc: false, context: context, builder: (context) => const UpgradeDialogUI()); + final r = await showDialog( + dismissWithEsc: false, + context: context, + builder: (context) => const UpgradeDialogUI(), + ); if (r != true) { if (!context.mounted) return false; @@ -264,8 +271,10 @@ class AppGlobalModel extends _$AppGlobalModel { dPrint("now == $now start == $startTime end == $endTime"); if (now < startTime) { - _activityThemeColorTimer = - Timer(Duration(milliseconds: startTime - now), () => checkActivityThemeColor(networkVersionData)); + _activityThemeColorTimer = Timer( + Duration(milliseconds: startTime - now), + () => checkActivityThemeColor(networkVersionData), + ); dPrint("start Timer ...."); } else if (now >= startTime && now <= endTime) { dPrint("update Color ...."); @@ -280,8 +289,10 @@ class AppGlobalModel extends _$AppGlobalModel { ); // wait for end - _activityThemeColorTimer = - Timer(Duration(milliseconds: endTime - now), () => checkActivityThemeColor(networkVersionData)); + _activityThemeColorTimer = Timer( + Duration(milliseconds: endTime - now), + () => checkActivityThemeColor(networkVersionData), + ); } else { dPrint("reset Color ...."); state = state.copyWith( @@ -302,8 +313,9 @@ class AppGlobalModel extends _$AppGlobalModel { await appConfBox.put("app_locale", null); return; } - final localeCode = - value.countryCode != null ? "${value.languageCode}_${value.countryCode ?? ""}" : value.languageCode; + final localeCode = value.countryCode != null + ? "${value.languageCode}_${value.countryCode ?? ""}" + : value.languageCode; dPrint("changeLocale == $value localeCode=== $localeCode"); await appConfBox.put("app_locale", localeCode); state = state.copyWith(appLocale: value); diff --git a/lib/app.freezed.dart b/lib/app.freezed.dart index 65c7d9a..d938456 100644 --- a/lib/app.freezed.dart +++ b/lib/app.freezed.dart @@ -14,7 +14,7 @@ T _$identity(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; + String? get deviceUUID; String? get applicationSupportDir; String? get applicationBinaryModuleDir; AppVersionData? get networkVersionData; ThemeConf get themeConf; Locale? get appLocale; Box? get appConfBox; dynamic get windowsVersion; /// 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 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)); + 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)); } @override -int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox); +int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox,const DeepCollectionEquality().hash(windowsVersion)); @override String toString() { - return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox)'; + return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox, windowsVersion: $windowsVersion)'; } @@ -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 + String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, dynamic windowsVersion }); @@ -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,}) { +@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,}) { 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,7 +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?, +as Box?,windowsVersion: freezed == windowsVersion ? _self.windowsVersion : windowsVersion // ignore: cast_nullable_to_non_nullable +as dynamic, )); } /// Create a copy of AppGlobalState @@ -165,10 +166,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(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; switch (_that) { case _AppGlobalState() when $default != null: -return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox);case _: +return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.windowsVersion);case _: return orElse(); } @@ -186,10 +187,10 @@ return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBi /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, dynamic windowsVersion) $default,) {final _that = this; switch (_that) { case _AppGlobalState(): -return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox);case _: +return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.windowsVersion);case _: throw StateError('Unexpected subclass'); } @@ -206,10 +207,10 @@ return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBi /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, dynamic windowsVersion)? $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);case _: +return $default(_that.deviceUUID,_that.applicationSupportDir,_that.applicationBinaryModuleDir,_that.networkVersionData,_that.themeConf,_that.appLocale,_that.appConfBox,_that.windowsVersion);case _: return null; } @@ -221,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}); + const _AppGlobalState({this.deviceUUID, this.applicationSupportDir, this.applicationBinaryModuleDir, this.networkVersionData, this.themeConf = const ThemeConf(), this.appLocale, this.appConfBox, this.windowsVersion = 10}); @override final String? deviceUUID; @@ -231,6 +232,7 @@ class _AppGlobalState implements AppGlobalState { @override@JsonKey() final ThemeConf themeConf; @override final Locale? appLocale; @override final Box? appConfBox; +@override@JsonKey() final dynamic windowsVersion; /// Create a copy of AppGlobalState /// with the given fields replaced by the non-null parameter values. @@ -242,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)); + 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)); } @override -int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox); +int get hashCode => Object.hash(runtimeType,deviceUUID,applicationSupportDir,applicationBinaryModuleDir,networkVersionData,themeConf,appLocale,appConfBox,const DeepCollectionEquality().hash(windowsVersion)); @override String toString() { - return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox)'; + return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf, appLocale: $appLocale, appConfBox: $appConfBox, windowsVersion: $windowsVersion)'; } @@ -262,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 + String? deviceUUID, String? applicationSupportDir, String? applicationBinaryModuleDir, AppVersionData? networkVersionData, ThemeConf themeConf, Locale? appLocale, Box? appConfBox, dynamic windowsVersion }); @@ -279,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,}) { +@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,}) { 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 @@ -288,7 +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?, +as Box?,windowsVersion: freezed == windowsVersion ? _self.windowsVersion : windowsVersion // ignore: cast_nullable_to_non_nullable +as dynamic, )); } diff --git a/lib/app.g.dart b/lib/app.g.dart index ad8194c..74b51d6 100644 --- a/lib/app.g.dart +++ b/lib/app.g.dart @@ -82,7 +82,7 @@ final class AppGlobalModelProvider } } -String _$appGlobalModelHash() => r'53dd9ed5e197333b509d282eb01073f15572820c'; +String _$appGlobalModelHash() => r'51f72c5d8538e2a4f11d256802b1a1f2e04d03be'; abstract class _$AppGlobalModel extends $Notifier { AppGlobalState build(); diff --git a/lib/common/conf/conf.dart b/lib/common/conf/conf.dart index dfd55cf..6e00eb3 100644 --- a/lib/common/conf/conf.dart +++ b/lib/common/conf/conf.dart @@ -1,7 +1,7 @@ class ConstConf { - static const String appVersion = "2.15.0"; - static const int appVersionCode = 70; - static const String appVersionDate = "2025-11-8"; + static const String appVersion = "2.15.1 Beta"; + static const int appVersionCode = 71; + static const String appVersionDate = "2025-11-15"; static const _gameChannels = [ "LIVE", "4.0_PREVIEW", diff --git a/lib/common/rust/api/ort_api.dart b/lib/common/rust/api/ort_api.dart new file mode 100644 index 0000000..49973bf --- /dev/null +++ b/lib/common/rust/api/ort_api.dart @@ -0,0 +1,72 @@ +// 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",空字符串表示使用默认模型) +/// +Future loadTranslationModel({ + required String modelPath, + required String modelKey, + required String quantizationSuffix, +}) => RustLib.instance.api.crateApiOrtApiLoadTranslationModel( + modelPath: modelPath, + modelKey: modelKey, + quantizationSuffix: quantizationSuffix, +); + +/// 翻译文本 +/// +/// # Arguments +/// * `model_key` - 模型缓存键(如 "zh-en") +/// * `text` - 要翻译的文本 +/// +/// # Returns +/// * `Result` - 翻译后的文本 +Future translateText({ + required String modelKey, + required String text, +}) => RustLib.instance.api.crateApiOrtApiTranslateText( + modelKey: modelKey, + text: text, +); + +/// 批量翻译文本 +/// +/// # Arguments +/// * `model_key` - 模型缓存键(如 "zh-en") +/// * `texts` - 要翻译的文本列表 +/// +/// # Returns +/// * `Result>` - 翻译后的文本列表 +Future> translateTextBatch({ + required String modelKey, + required List texts, +}) => RustLib.instance.api.crateApiOrtApiTranslateTextBatch( + modelKey: modelKey, + texts: texts, +); + +/// 卸载模型 +/// +/// # Arguments +/// * `model_key` - 模型缓存键(如 "zh-en") +/// +Future unloadTranslationModel({required String modelKey}) => RustLib + .instance + .api + .crateApiOrtApiUnloadTranslationModel(modelKey: modelKey); + +/// 清空所有已加载的模型 +/// +/// # Returns +Future clearAllModels() => + RustLib.instance.api.crateApiOrtApiClearAllModels(); diff --git a/lib/common/rust/frb_generated.dart b/lib/common/rust/frb_generated.dart index f7d0ba7..5cbea7e 100644 --- a/lib/common/rust/frb_generated.dart +++ b/lib/common/rust/frb_generated.dart @@ -5,6 +5,7 @@ import 'api/asar_api.dart'; import 'api/http_api.dart'; +import 'api/ort_api.dart'; import 'api/rs_process.dart'; import 'api/win32_api.dart'; import 'dart:async'; @@ -68,7 +69,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => 1832496273; + int get rustContentHash => -706588047; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -79,6 +80,8 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { + Future crateApiOrtApiClearAllModels(); + Future> crateApiHttpApiDnsLookupIps({required String host}); Future> crateApiHttpApiDnsLookupTxt({required String host}); @@ -96,6 +99,12 @@ abstract class RustLibApi extends BaseApi { required String asarPath, }); + Future crateApiOrtApiLoadTranslationModel({ + required String modelPath, + required String modelKey, + required String quantizationSuffix, + }); + Future crateApiAsarApiRsiLauncherAsarDataWriteMainJs({ required RsiLauncherAsarData that, required List content, @@ -122,6 +131,18 @@ abstract class RustLibApi extends BaseApi { required String workingDirectory, }); + Future crateApiOrtApiTranslateText({ + required String modelKey, + required String text, + }); + + Future> crateApiOrtApiTranslateTextBatch({ + required String modelKey, + required List texts, + }); + + Future crateApiOrtApiUnloadTranslationModel({required String modelKey}); + Future crateApiRsProcessWrite({ required int rsPid, required String data, @@ -136,6 +157,27 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required super.portManager, }); + @override + Future crateApiOrtApiClearAllModels() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire.wire__crate__api__ort_api__clear_all_models(port_); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiOrtApiClearAllModelsConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiOrtApiClearAllModelsConstMeta => + const TaskConstMeta(debugName: "clear_all_models", argNames: []); + @override Future> crateApiHttpApiDnsLookupIps({required String host}) { return handler.executeNormal( @@ -268,6 +310,42 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["asarPath"], ); + @override + Future crateApiOrtApiLoadTranslationModel({ + required String modelPath, + required String modelKey, + required String quantizationSuffix, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(modelPath); + var arg1 = cst_encode_String(modelKey); + var arg2 = cst_encode_String(quantizationSuffix); + return wire.wire__crate__api__ort_api__load_translation_model( + port_, + arg0, + arg1, + arg2, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiOrtApiLoadTranslationModelConstMeta, + argValues: [modelPath, modelKey, quantizationSuffix], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiOrtApiLoadTranslationModelConstMeta => + const TaskConstMeta( + debugName: "load_translation_model", + argNames: ["modelPath", "modelKey", "quantizationSuffix"], + ); + @override Future crateApiAsarApiRsiLauncherAsarDataWriteMainJs({ required RsiLauncherAsarData that, @@ -443,6 +521,102 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["executable", "arguments", "workingDirectory", "streamSink"], ); + @override + Future crateApiOrtApiTranslateText({ + required String modelKey, + required String text, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(modelKey); + var arg1 = cst_encode_String(text); + return wire.wire__crate__api__ort_api__translate_text( + port_, + arg0, + arg1, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_String, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiOrtApiTranslateTextConstMeta, + argValues: [modelKey, text], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiOrtApiTranslateTextConstMeta => + const TaskConstMeta( + debugName: "translate_text", + argNames: ["modelKey", "text"], + ); + + @override + Future> crateApiOrtApiTranslateTextBatch({ + required String modelKey, + required List texts, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(modelKey); + var arg1 = cst_encode_list_String(texts); + return wire.wire__crate__api__ort_api__translate_text_batch( + port_, + arg0, + arg1, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_list_String, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiOrtApiTranslateTextBatchConstMeta, + argValues: [modelKey, texts], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiOrtApiTranslateTextBatchConstMeta => + const TaskConstMeta( + debugName: "translate_text_batch", + argNames: ["modelKey", "texts"], + ); + + @override + Future crateApiOrtApiUnloadTranslationModel({ + required String modelKey, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(modelKey); + return wire.wire__crate__api__ort_api__unload_translation_model( + port_, + arg0, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiOrtApiUnloadTranslationModelConstMeta, + argValues: [modelKey], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiOrtApiUnloadTranslationModelConstMeta => + const TaskConstMeta( + debugName: "unload_translation_model", + argNames: ["modelKey"], + ); + @override Future crateApiRsProcessWrite({ required int rsPid, diff --git a/lib/common/rust/frb_generated.io.dart b/lib/common/rust/frb_generated.io.dart index e9a0a38..c03e956 100644 --- a/lib/common/rust/frb_generated.io.dart +++ b/lib/common/rust/frb_generated.io.dart @@ -5,6 +5,7 @@ import 'api/asar_api.dart'; import 'api/http_api.dart'; +import 'api/ort_api.dart'; import 'api/rs_process.dart'; import 'api/win32_api.dart'; import 'dart:async'; @@ -583,7 +584,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { // AUTO GENERATED FILE, DO NOT EDIT. // // Generated by `package:ffigen`. -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import /// generated by flutter_rust_bridge class RustLibWire implements BaseWire { @@ -614,6 +615,18 @@ class RustLibWire implements BaseWire { late final _store_dart_post_cobject = _store_dart_post_cobjectPtr .asFunction(); + void wire__crate__api__ort_api__clear_all_models(int port_) { + return _wire__crate__api__ort_api__clear_all_models(port_); + } + + late final _wire__crate__api__ort_api__clear_all_modelsPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__ort_api__clear_all_models', + ); + late final _wire__crate__api__ort_api__clear_all_models = + _wire__crate__api__ort_api__clear_all_modelsPtr + .asFunction(); + void wire__crate__api__http_api__dns_lookup_ips( int port_, ffi.Pointer host, @@ -733,6 +746,44 @@ class RustLibWire implements BaseWire { void Function(int, ffi.Pointer) >(); + void wire__crate__api__ort_api__load_translation_model( + int port_, + ffi.Pointer model_path, + ffi.Pointer model_key, + ffi.Pointer quantization_suffix, + ) { + return _wire__crate__api__ort_api__load_translation_model( + port_, + model_path, + model_key, + quantization_suffix, + ); + } + + late final _wire__crate__api__ort_api__load_translation_modelPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__ort_api__load_translation_model', + ); + late final _wire__crate__api__ort_api__load_translation_model = + _wire__crate__api__ort_api__load_translation_modelPtr + .asFunction< + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + >(); + void wire__crate__api__asar_api__rsi_launcher_asar_data_write_main_js( int port_, ffi.Pointer that, @@ -898,6 +949,95 @@ class RustLibWire implements BaseWire { ) >(); + void wire__crate__api__ort_api__translate_text( + int port_, + ffi.Pointer model_key, + ffi.Pointer text, + ) { + return _wire__crate__api__ort_api__translate_text(port_, model_key, text); + } + + late final _wire__crate__api__ort_api__translate_textPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ) + > + >('frbgen_starcitizen_doctor_wire__crate__api__ort_api__translate_text'); + late final _wire__crate__api__ort_api__translate_text = + _wire__crate__api__ort_api__translate_textPtr + .asFunction< + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ) + >(); + + void wire__crate__api__ort_api__translate_text_batch( + int port_, + ffi.Pointer model_key, + ffi.Pointer texts, + ) { + return _wire__crate__api__ort_api__translate_text_batch( + port_, + model_key, + texts, + ); + } + + late final _wire__crate__api__ort_api__translate_text_batchPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__ort_api__translate_text_batch', + ); + late final _wire__crate__api__ort_api__translate_text_batch = + _wire__crate__api__ort_api__translate_text_batchPtr + .asFunction< + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ) + >(); + + void wire__crate__api__ort_api__unload_translation_model( + int port_, + ffi.Pointer model_key, + ) { + return _wire__crate__api__ort_api__unload_translation_model( + port_, + model_key, + ); + } + + late final _wire__crate__api__ort_api__unload_translation_modelPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__ort_api__unload_translation_model', + ); + late final _wire__crate__api__ort_api__unload_translation_model = + _wire__crate__api__ort_api__unload_translation_modelPtr + .asFunction< + void Function(int, ffi.Pointer) + >(); + void wire__crate__api__rs_process__write( int port_, int rs_pid, diff --git a/lib/common/utils/multi_window_manager.dart b/lib/common/utils/multi_window_manager.dart index 44f4e3f..9a70968 100644 --- a/lib/common/utils/multi_window_manager.dart +++ b/lib/common/utils/multi_window_manager.dart @@ -2,6 +2,8 @@ import 'dart:convert'; 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'; @@ -11,6 +13,7 @@ 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'; @@ -18,6 +21,15 @@ 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({ @@ -27,35 +39,49 @@ abstract class MultiWindowAppState with _$MultiWindowAppState { required List gameInstallPaths, String? languageCode, String? countryCode, + @Default(10) windowsVersion, }) = _MultiWindowAppState; factory MultiWindowAppState.fromJson(Map json) => _$MultiWindowAppStateFromJson(json); } class MultiWindowManager { - static Future 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); + /// Parse window type from arguments string + static String parseWindowType(String arguments) { + if (arguments.isEmpty) { + return WindowTypes.main; + } + try { + final Map argument = jsonDecode(arguments); + return argument['window_type'] ?? WindowTypes.main; + } catch (e) { + return WindowTypes.main; + } } - static void sendAppStateBroadcast(AppGlobalState appGlobalState) { - DesktopMultiWindow.invokeMethod( - 0, - 'app_state_broadcast', - _appStateToWindowState(appGlobalState).toJson(), + /// Launch a sub-window with specified type and title + static Future launchSubWindow(String type, String title, AppGlobalState appGlobalState) async { + final gameInstallPaths = await SCLoggerHelper.getGameInstallPath( + await SCLoggerHelper.getLauncherLogList() ?? [], + checkExists: true, + withVersion: AppConf.gameChannels, ); + + 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, 800, 1200)); + await controller.setTitle(title); + await controller.center(); + await controller.show(); + }); } static MultiWindowAppState _appStateToWindowState(AppGlobalState appGlobalState, {List? gameInstallPaths}) { @@ -66,53 +92,147 @@ class MultiWindowManager { languageCode: appGlobalState.appLocale?.languageCode, countryCode: appGlobalState.appLocale?.countryCode, gameInstallPaths: gameInstallPaths ?? [], + windowsVersion: appGlobalState.windowsVersion, ); } - static void runSubWindowApp(List args) { - final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map; + /// Run sub-window app with parsed arguments + static Future runSubWindowApp(String arguments, String windowType) async { + final Map argument = arguments.isEmpty ? const {} : jsonDecode(arguments); final windowAppState = MultiWindowAppState.fromJson(argument['app_state'] ?? {}); Widget? windowWidget; - switch (argument["window_type"]) { - case "log_analyze": + + switch (windowType) { + case WindowTypes.logAnalyze: windowWidget = ToolsLogAnalyzeDialogUI(appState: windowAppState); break; default: - throw Exception('Unknown window type'); + throw Exception('Unknown window type: $windowType'); } - 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( + + await Window.initialize(); + + if (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( brightness: Brightness.dark, fontFamily: "SourceHanSansCN-Regular", - navigationPaneTheme: NavigationPaneThemeData( - backgroundColor: HexColor(windowAppState.backgroundColor), - ), + navigationPaneTheme: NavigationPaneThemeData(backgroundColor: 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 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 center() { + return invokeMethod('window_center'); + } + + /// Close the window + void close() async { + await invokeMethod('window_close'); + } + + /// Show the window + Future show() { + return invokeMethod('window_show'); + } + + /// Hide the window + Future hide() { + return invokeMethod('window_hide'); + } + + /// Focus the window + Future focus() { + return invokeMethod('window_focus'); + } + + /// Set window frame (position and size) + Future setFrame(Rect frame) { + return invokeMethod('window_set_frame', { + 'left': frame.left, + 'top': frame.top, + 'width': frame.width, + 'height': frame.height, + }); + } + + /// Set window title + Future setTitle(String title) { + return invokeMethod('window_set_title', title); } } diff --git a/lib/common/utils/multi_window_manager.freezed.dart b/lib/common/utils/multi_window_manager.freezed.dart index 3c0fdfc..ca40755 100644 --- a/lib/common/utils/multi_window_manager.freezed.dart +++ b/lib/common/utils/multi_window_manager.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$MultiWindowAppState { - String get backgroundColor; String get menuColor; String get micaColor; List get gameInstallPaths; String? get languageCode; String? get countryCode; + String get backgroundColor; String get menuColor; String get micaColor; List get gameInstallPaths; String? get languageCode; String? get countryCode; dynamic get windowsVersion; /// 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 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)); + 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)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,backgroundColor,menuColor,micaColor,const DeepCollectionEquality().hash(gameInstallPaths),languageCode,countryCode); +int get hashCode => Object.hash(runtimeType,backgroundColor,menuColor,micaColor,const DeepCollectionEquality().hash(gameInstallPaths),languageCode,countryCode,const DeepCollectionEquality().hash(windowsVersion)); @override String toString() { - return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, gameInstallPaths: $gameInstallPaths, languageCode: $languageCode, countryCode: $countryCode)'; + return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, gameInstallPaths: $gameInstallPaths, languageCode: $languageCode, countryCode: $countryCode, windowsVersion: $windowsVersion)'; } @@ -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 gameInstallPaths, String? languageCode, String? countryCode + String backgroundColor, String menuColor, String micaColor, List gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion }); @@ -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,}) { +@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,}) { 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,7 +73,8 @@ 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,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?, +as String?,windowsVersion: freezed == windowsVersion ? _self.windowsVersion : windowsVersion // ignore: cast_nullable_to_non_nullable +as dynamic, )); } @@ -158,10 +159,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String backgroundColor, String menuColor, String micaColor, List gameInstallPaths, String? languageCode, String? countryCode)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String backgroundColor, String menuColor, String micaColor, List gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion)? $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);case _: +return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode,_that.windowsVersion);case _: return orElse(); } @@ -179,10 +180,10 @@ return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.game /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String backgroundColor, String menuColor, String micaColor, List gameInstallPaths, String? languageCode, String? countryCode) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String backgroundColor, String menuColor, String micaColor, List gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion) $default,) {final _that = this; switch (_that) { case _MultiWindowAppState(): -return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode);case _: +return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode,_that.windowsVersion);case _: throw StateError('Unexpected subclass'); } @@ -199,10 +200,10 @@ return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.game /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String backgroundColor, String menuColor, String micaColor, List gameInstallPaths, String? languageCode, String? countryCode)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String backgroundColor, String menuColor, String micaColor, List gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion)? $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);case _: +return $default(_that.backgroundColor,_that.menuColor,_that.micaColor,_that.gameInstallPaths,_that.languageCode,_that.countryCode,_that.windowsVersion);case _: return null; } @@ -214,7 +215,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 gameInstallPaths, this.languageCode, this.countryCode}): _gameInstallPaths = gameInstallPaths; + const _MultiWindowAppState({required this.backgroundColor, required this.menuColor, required this.micaColor, required final List gameInstallPaths, this.languageCode, this.countryCode, this.windowsVersion = 10}): _gameInstallPaths = gameInstallPaths; factory _MultiWindowAppState.fromJson(Map json) => _$MultiWindowAppStateFromJson(json); @override final String backgroundColor; @@ -229,6 +230,7 @@ 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. @@ -243,16 +245,16 @@ Map 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)); + 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)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,backgroundColor,menuColor,micaColor,const DeepCollectionEquality().hash(_gameInstallPaths),languageCode,countryCode); +int get hashCode => Object.hash(runtimeType,backgroundColor,menuColor,micaColor,const DeepCollectionEquality().hash(_gameInstallPaths),languageCode,countryCode,const DeepCollectionEquality().hash(windowsVersion)); @override String toString() { - return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, gameInstallPaths: $gameInstallPaths, languageCode: $languageCode, countryCode: $countryCode)'; + return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, gameInstallPaths: $gameInstallPaths, languageCode: $languageCode, countryCode: $countryCode, windowsVersion: $windowsVersion)'; } @@ -263,7 +265,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 gameInstallPaths, String? languageCode, String? countryCode + String backgroundColor, String menuColor, String micaColor, List gameInstallPaths, String? languageCode, String? countryCode, dynamic windowsVersion }); @@ -280,7 +282,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,}) { +@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,}) { 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 @@ -288,7 +290,8 @@ 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,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?, +as String?,windowsVersion: freezed == windowsVersion ? _self.windowsVersion : windowsVersion // ignore: cast_nullable_to_non_nullable +as dynamic, )); } diff --git a/lib/common/utils/multi_window_manager.g.dart b/lib/common/utils/multi_window_manager.g.dart index 829da24..a0be5c9 100644 --- a/lib/common/utils/multi_window_manager.g.dart +++ b/lib/common/utils/multi_window_manager.g.dart @@ -16,6 +16,7 @@ _MultiWindowAppState _$MultiWindowAppStateFromJson(Map json) => .toList(), languageCode: json['languageCode'] as String?, countryCode: json['countryCode'] as String?, + windowsVersion: json['windowsVersion'] ?? 10, ); Map _$MultiWindowAppStateToJson( @@ -27,4 +28,5 @@ Map _$MultiWindowAppStateToJson( 'gameInstallPaths': instance.gameInstallPaths, 'languageCode': instance.languageCode, 'countryCode': instance.countryCode, + 'windowsVersion': instance.windowsVersion, }; diff --git a/lib/main.dart b/lib/main.dart index 5299e5f..332a3a9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,28 +11,42 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'app.dart'; import 'common/utils/multi_window_manager.dart'; -void main(List args) async { +Future main(List args) async { // webview window - if (runWebViewTitleBarWidget(args, - backgroundColor: const Color.fromRGBO(19, 36, 49, 1), builder: _defaultWebviewTitleBar)) { - return; - } - if (args.firstOrNull == 'multi_window') { - MultiWindowManager.runSubWindowApp(args); + if (runWebViewTitleBarWidget( + args, + backgroundColor: const Color.fromRGBO(19, 36, 49, 1), + builder: _defaultWebviewTitleBar, + )) { return; } + WidgetsFlutterBinding.ensureInitialized(); - await _initWindow(); - // run app - runApp(const ProviderScope(child: App())); + await windowManager.ensureInitialized(); + + // Get the current window controller + final windowController = await WindowController.fromCurrentEngine(); + + // Parse window arguments to determine which window to show + final windowType = MultiWindowManager.parseWindowType(windowController.arguments); + + // Initialize window-specific handlers for sub-windows + if (windowType != WindowTypes.main) { + await windowController.doCustomInitialize(); + } + + // Run different apps based on the window type + switch (windowType) { + case WindowTypes.main: + await _initWindow(); + runApp(const ProviderScope(child: App())); + default: + MultiWindowManager.runSubWindowApp(windowController.arguments, windowType); + } } Future _initWindow() async { - await windowManager.ensureInitialized(); - await windowManager.setTitleBarStyle( - TitleBarStyle.hidden, - windowButtonVisibility: false, - ); + await windowManager.setTitleBarStyle(TitleBarStyle.hidden, windowButtonVisibility: false); await windowManager.setSize(const Size(1280, 810)); await windowManager.setMinimumSize(const Size(1280, 810)); await windowManager.center(animate: true); @@ -73,18 +87,22 @@ class App extends HookConsumerWidget with WindowListener { ); }, theme: FluentThemeData( - brightness: Brightness.dark, - fontFamily: "SourceHanSansCN-Regular", - navigationPaneTheme: NavigationPaneThemeData( - backgroundColor: appState.themeConf.backgroundColor, + brightness: Brightness.dark, + fontFamily: "SourceHanSansCN-Regular", + navigationPaneTheme: NavigationPaneThemeData(backgroundColor: appState.themeConf.backgroundColor), + menuColor: appState.themeConf.menuColor, + micaBackgroundColor: appState.themeConf.micaColor, + buttonTheme: ButtonThemeData( + defaultButtonStyle: ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + side: BorderSide(color: Colors.white.withValues(alpha: .01)), + ), + ), ), - menuColor: appState.themeConf.menuColor, - micaBackgroundColor: appState.themeConf.micaColor, - buttonTheme: ButtonThemeData( - defaultButtonStyle: ButtonStyle( - shape: WidgetStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), side: BorderSide(color: Colors.white.withValues(alpha: .01)))), - ))), + ), + ), locale: appState.appLocale, debugShowCheckedModeBanner: false, routeInformationParser: router.routeInformationParser, @@ -97,11 +115,19 @@ class App extends HookConsumerWidget with WindowListener { Future onWindowClose() async { debugPrint("onWindowClose"); if (await windowManager.isPreventClose()) { - final windows = await DesktopMultiWindow.getAllSubWindowIds(); - for (final id in windows) { - await WindowController.fromWindowId(id).close(); + final mainWindow = await WindowController.fromCurrentEngine(); + final windows = await WindowController.getAll(); + for (final controller in windows) { + if (controller.windowId != mainWindow.windowId) { + try { + controller.close(); + } catch (e) { + debugPrint("Error closing window ${controller.windowId}: $e"); + } + } } - await windowManager.destroy(); + await windowManager.setPreventClose(false); + await windowManager.close(); exit(0); } super.onWindowClose(); @@ -112,42 +138,28 @@ Widget _defaultWebviewTitleBar(BuildContext context) { final state = TitleBarWebViewState.of(context); final controller = TitleBarWebViewController.of(context); return FluentTheme( - data: FluentThemeData.dark(), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (Platform.isMacOS) const SizedBox(width: 96), - IconButton( - onPressed: !state.canGoBack ? null : controller.back, - icon: const Icon(FluentIcons.chevron_left), - ), - const SizedBox(width: 12), - IconButton( - onPressed: !state.canGoForward ? null : controller.forward, - icon: const Icon(FluentIcons.chevron_right), - ), - const SizedBox(width: 12), - if (state.isLoading) - IconButton( - onPressed: controller.stop, - icon: const Icon(FluentIcons.chrome_close), - ) - else - IconButton( - onPressed: controller.reload, - icon: const Icon(FluentIcons.refresh), - ), - const SizedBox(width: 12), - (state.isLoading) - ? const SizedBox( - width: 24, - height: 24, - child: ProgressRing(), - ) - : const SizedBox(width: 24), - const SizedBox(width: 12), - SelectableText(state.url ?? ""), - const Spacer() - ], - )); + data: FluentThemeData.dark(), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (Platform.isMacOS) const SizedBox(width: 96), + IconButton(onPressed: !state.canGoBack ? null : controller.back, icon: const Icon(FluentIcons.chevron_left)), + const SizedBox(width: 12), + IconButton( + onPressed: !state.canGoForward ? null : controller.forward, + icon: const Icon(FluentIcons.chevron_right), + ), + const SizedBox(width: 12), + if (state.isLoading) + IconButton(onPressed: controller.stop, icon: const Icon(FluentIcons.chrome_close)) + else + IconButton(onPressed: controller.reload, icon: const Icon(FluentIcons.refresh)), + const SizedBox(width: 12), + (state.isLoading) ? const SizedBox(width: 24, height: 24, child: ProgressRing()) : const SizedBox(width: 24), + const SizedBox(width: 12), + SelectableText(state.url ?? ""), + const Spacer(), + ], + ), + ); } diff --git a/lib/provider/aria2c.dart b/lib/provider/aria2c.dart index 3e4f75a..47b5ecc 100644 --- a/lib/provider/aria2c.dart +++ b/lib/provider/aria2c.dart @@ -8,11 +8,11 @@ import 'package:flutter/foundation.dart'; import 'package:hive_ce/hive.dart'; import 'package:starcitizen_doctor/api/api.dart'; import 'package:starcitizen_doctor/common/helper/system_helper.dart'; -import 'package:starcitizen_doctor/common/rust/api/rs_process.dart' - as rs_process; +import 'package:starcitizen_doctor/common/rust/api/rs_process.dart' as rs_process; import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/common/utils/provider.dart'; +import 'package:starcitizen_doctor/ui/home/downloader/home_downloader_ui_model.dart'; part 'aria2c.g.dart'; @@ -20,11 +20,8 @@ part 'aria2c.freezed.dart'; @freezed abstract class Aria2cModelState with _$Aria2cModelState { - const factory Aria2cModelState({ - required String aria2cDir, - Aria2c? aria2c, - Aria2GlobalStat? aria2globalStat, - }) = _Aria2cModelState; + const factory Aria2cModelState({required String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat}) = + _Aria2cModelState; } extension Aria2cModelExt on Aria2cModelState { @@ -32,10 +29,8 @@ extension Aria2cModelExt on Aria2cModelState { bool get hasDownloadTask => aria2globalStat != null && aria2TotalTaskNum > 0; - int get aria2TotalTaskNum => aria2globalStat == null - ? 0 - : ((aria2globalStat!.numActive ?? 0) + - (aria2globalStat!.numWaiting ?? 0)); + int get aria2TotalTaskNum => + aria2globalStat == null ? 0 : ((aria2globalStat!.numActive ?? 0) + (aria2globalStat!.numWaiting ?? 0)); } @riverpod @@ -57,16 +52,16 @@ class Aria2cModel extends _$Aria2cModel { try { final sessionFile = File("$aria2cDir\\aria2.session"); // 有下载任务则第一时间初始化 - if (await sessionFile.exists() && - (await sessionFile.readAsString()).trim().isNotEmpty) { - dPrint("launch Aria2c daemon"); - await launchDaemon(appGlobalState.applicationBinaryModuleDir!); - } else { - dPrint("LazyLoad Aria2c daemon"); - } - } catch (e) { - dPrint("Aria2cManager.checkLazyLoad Error:$e"); - } + if (await sessionFile.exists () + && (await sessionFile.readAsString()).trim().isNotEmpty) { + dPrint("launch Aria2c daemon"); + await launchDaemon(appGlobalState.applicationBinaryModuleDir!); + } else { + dPrint("LazyLoad Aria2c daemon"); + } + } catch (e) { + dPrint("Aria2cManager.checkLazyLoad Error:$e"); + } }(); return Aria2cModelState(aria2cDir: aria2cDir); @@ -74,8 +69,7 @@ class Aria2cModel extends _$Aria2cModel { Future launchDaemon(String applicationBinaryModuleDir) async { if (state.aria2c != null) return; - await BinaryModuleConf.extractModule( - ["aria2c"], applicationBinaryModuleDir); + await BinaryModuleConf.extractModule(["aria2c"], applicationBinaryModuleDir); /// skip for debug hot reload if (kDebugMode) { @@ -99,30 +93,30 @@ class Aria2cModel extends _$Aria2cModel { dPrint("Aria2cManager .----- aria2c start $port------"); final stream = rs_process.start( - executable: exePath, - arguments: [ - "-V", - "-c", - "-x 16", - "--dir=${state.aria2cDir}\\downloads", - "--disable-ipv6", - "--enable-rpc", - "--pause", - "--rpc-listen-port=$port", - "--rpc-secret=$pwd", - "--input-file=${sessionFile.absolute.path.trim()}", - "--save-session=${sessionFile.absolute.path.trim()}", - "--save-session-interval=60", - "--file-allocation=trunc", - "--seed-time=0", - ], - workingDirectory: state.aria2cDir); + executable: exePath, + arguments: [ + "-V", + "-c", + "-x 16", + "--dir=${state.aria2cDir}\\downloads", + "--disable-ipv6", + "--enable-rpc", + "--pause", + "--rpc-listen-port=$port", + "--rpc-secret=$pwd", + "--input-file=${sessionFile.absolute.path.trim()}", + "--save-session=${sessionFile.absolute.path.trim()}", + "--save-session-interval=60", + "--file-allocation=trunc", + "--seed-time=0", + ], + workingDirectory: state.aria2cDir, + ); String launchError = ""; stream.listen((event) { - dPrint( - "Aria2cManager.rs_process event === [${event.rsPid}] ${event.dataType} >> ${event.data}"); + dPrint("Aria2cManager.rs_process event === [${event.rsPid}] ${event.dataType} >> ${event.data}"); switch (event.dataType) { case rs_process.RsProcessStreamDataType.output: if (event.data.contains("IPv4 RPC: listening on TCP port")) { @@ -155,8 +149,7 @@ class Aria2cModel extends _$Aria2cModel { } String generateRandomPassword(int length) { - const String charset = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const String charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random = Random(); StringBuffer buffer = StringBuffer(); for (int i = 0; i < length; i++) { @@ -190,12 +183,12 @@ class Aria2cModel extends _$Aria2cModel { _listenState(aria2c); }); final box = await Hive.openBox("app_conf"); - aria2c.changeGlobalOption(Aria2Option() - ..maxOverallUploadLimit = - textToByte(box.get("downloader_up_limit", defaultValue: "0")) - ..maxOverallDownloadLimit = - textToByte(box.get("downloader_down_limit", defaultValue: "0")) - ..btTracker = trackerList); + aria2c.changeGlobalOption( + Aria2Option() + ..maxOverallUploadLimit = textToByte(box.get("downloader_up_limit", defaultValue: "0")) + ..maxOverallDownloadLimit = textToByte(box.get("downloader_down_limit", defaultValue: "0")) + ..btTracker = trackerList, + ); } Future _listenState(Aria2c aria2c) async { @@ -214,4 +207,16 @@ class Aria2cModel extends _$Aria2cModel { await Future.delayed(const Duration(seconds: 1)); } } + + Future isNameInTask(String name) async { + final aria2c = state.aria2c; + if (aria2c == null) return false; + for (var value in [...await aria2c.tellActive(), ...await aria2c.tellWaiting(0, 100000)]) { + final t = HomeDownloaderUIModel.getTaskTypeAndName(value); + if (t.key == "torrent" && t.value.contains(name)) { + return true; + } + } + return false; + } } diff --git a/lib/provider/aria2c.g.dart b/lib/provider/aria2c.g.dart index c3877b2..8e2b783 100644 --- a/lib/provider/aria2c.g.dart +++ b/lib/provider/aria2c.g.dart @@ -41,7 +41,7 @@ final class Aria2cModelProvider } } -String _$aria2cModelHash() => r'3d51aeefd92e5291dca1f01db961f9c5496ec24f'; +String _$aria2cModelHash() => r'17956c60a79c68ae13b8b8e700ebbafb70e93194'; abstract class _$Aria2cModel extends $Notifier { Aria2cModelState build(); diff --git a/lib/ui/home/downloader/home_downloader_ui_model.dart b/lib/ui/home/downloader/home_downloader_ui_model.dart index d98f5a8..0d3dbcc 100644 --- a/lib/ui/home/downloader/home_downloader_ui_model.dart +++ b/lib/ui/home/downloader/home_downloader_ui_model.dart @@ -79,9 +79,10 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { return; case "cancel_all": final userOK = await showConfirmDialogs( - context, - S.current.downloader_action_confirm_cancel_all_tasks, - Text(S.current.downloader_info_manual_file_deletion_note)); + context, + S.current.downloader_action_confirm_cancel_all_tasks, + Text(S.current.downloader_info_manual_file_deletion_note), + ); if (userOK == true) { if (!aria2cState.isRunning) return; try { @@ -101,31 +102,19 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { } int getTasksLen() { - return state.tasks.length + - state.waitingTasks.length + - state.stoppedTasks.length; + return state.tasks.length + state.waitingTasks.length + state.stoppedTasks.length; } (Aria2Task, String, bool) getTaskAndType(int index) { - final tempList = [ - ...state.tasks, - ...state.waitingTasks, - ...state.stoppedTasks - ]; + final tempList = [...state.tasks, ...state.waitingTasks, ...state.stoppedTasks]; if (index >= 0 && index < state.tasks.length) { return (tempList[index], "active", index == 0); } - if (index >= state.tasks.length && - index < state.tasks.length + state.waitingTasks.length) { + if (index >= state.tasks.length && index < state.tasks.length + state.waitingTasks.length) { return (tempList[index], "waiting", index == state.tasks.length); } - if (index >= state.tasks.length + state.waitingTasks.length && - index < tempList.length) { - return ( - tempList[index], - "stopped", - index == state.tasks.length + state.waitingTasks.length - ); + if (index >= state.tasks.length + state.waitingTasks.length && index < tempList.length) { + return (tempList[index], "stopped", index == state.tasks.length + state.waitingTasks.length); } throw Exception("Index out of range or element is null"); } @@ -148,8 +137,7 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { int getETA(Aria2Task task) { if (task.downloadSpeed == null || task.downloadSpeed == 0) return 0; - final remainingBytes = - (task.totalLength ?? 0) - (task.completedLength ?? 0); + final remainingBytes = (task.totalLength ?? 0) - (task.completedLength ?? 0); return remainingBytes ~/ (task.downloadSpeed!); } @@ -172,9 +160,10 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { if (gid != null) { if (!context.mounted) return; final ok = await showConfirmDialogs( - context, - S.current.downloader_action_confirm_cancel_download, - Text(S.current.downloader_info_manual_file_deletion_note)); + context, + S.current.downloader_action_confirm_cancel_download, + Text(S.current.downloader_info_manual_file_deletion_note), + ); if (ok == true) { final aria2c = ref.read(aria2cModelProvider).aria2c; await aria2c?.remove(gid); @@ -204,8 +193,8 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { Future _listenDownloader() async { try { while (true) { - final aria2cState = ref.read(aria2cModelProvider); if (_disposed) return; + final aria2cState = ref.read(aria2cModelProvider); if (aria2cState.isRunning) { final aria2c = aria2cState.aria2c!; final tasks = await aria2c.tellActive(); @@ -219,12 +208,7 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { globalStat: globalStat, ); } else { - state = state.copyWith( - tasks: [], - waitingTasks: [], - stoppedTasks: [], - globalStat: null, - ); + state = state.copyWith(tasks: [], waitingTasks: [], stoppedTasks: [], globalStat: null); } await Future.delayed(const Duration(seconds: 1)); } @@ -236,72 +220,64 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { Future _showDownloadSpeedSettings(BuildContext context) async { final box = await Hive.openBox("app_conf"); - final upCtrl = TextEditingController( - text: box.get("downloader_up_limit", defaultValue: "")); - final downCtrl = TextEditingController( - text: box.get("downloader_down_limit", defaultValue: "")); + final upCtrl = TextEditingController(text: box.get("downloader_up_limit", defaultValue: "")); + final downCtrl = TextEditingController(text: box.get("downloader_down_limit", defaultValue: "")); final ifr = FilteringTextInputFormatter.allow(RegExp(r'^\d*[km]?$')); if (!context.mounted) return; final ok = await showConfirmDialogs( - context, - S.current.downloader_speed_limit_settings, - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.current.downloader_info_p2p_network_note, - style: TextStyle( - fontSize: 14, - color: Colors.white.withValues(alpha: .6), - ), - ), - const SizedBox(height: 24), - Text(S.current.downloader_info_download_unit_input_prompt), - const SizedBox(height: 12), - Text(S.current.downloader_input_upload_speed_limit), - const SizedBox(height: 6), - TextFormBox( - placeholder: "1、100k、10m、0", - controller: upCtrl, - placeholderStyle: - TextStyle(color: Colors.white.withValues(alpha: .6)), - inputFormatters: [ifr], - ), - const SizedBox(height: 12), - Text(S.current.downloader_input_download_speed_limit), - const SizedBox(height: 6), - TextFormBox( - placeholder: "1、100k、10m、0", - controller: downCtrl, - placeholderStyle: - TextStyle(color: Colors.white.withValues(alpha: .6)), - inputFormatters: [ifr], - ), - const SizedBox(height: 24), - Text( - S.current.downloader_input_info_p2p_upload_note, - style: TextStyle( - fontSize: 13, - color: Colors.white.withValues(alpha: .6), - ), - ) - ], - )); + context, + S.current.downloader_speed_limit_settings, + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.current.downloader_info_p2p_network_note, + style: TextStyle(fontSize: 14, color: Colors.white.withValues(alpha: .6)), + ), + const SizedBox(height: 24), + Text(S.current.downloader_info_download_unit_input_prompt), + const SizedBox(height: 12), + Text(S.current.downloader_input_upload_speed_limit), + const SizedBox(height: 6), + TextFormBox( + placeholder: "1、100k、10m、0", + controller: upCtrl, + placeholderStyle: TextStyle(color: Colors.white.withValues(alpha: .6)), + inputFormatters: [ifr], + ), + const SizedBox(height: 12), + Text(S.current.downloader_input_download_speed_limit), + const SizedBox(height: 6), + TextFormBox( + placeholder: "1、100k、10m、0", + controller: downCtrl, + placeholderStyle: TextStyle(color: Colors.white.withValues(alpha: .6)), + inputFormatters: [ifr], + ), + const SizedBox(height: 24), + Text( + S.current.downloader_input_info_p2p_upload_note, + style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .6)), + ), + ], + ), + ); if (ok == true) { final aria2cState = ref.read(aria2cModelProvider); final aria2cModel = ref.read(aria2cModelProvider.notifier); - await aria2cModel - .launchDaemon(appGlobalState.applicationBinaryModuleDir!); + await aria2cModel.launchDaemon(appGlobalState.applicationBinaryModuleDir!); final aria2c = aria2cState.aria2c!; final upByte = aria2cModel.textToByte(upCtrl.text.trim()); final downByte = aria2cModel.textToByte(downCtrl.text.trim()); final r = await aria2c - .changeGlobalOption(Aria2Option() - ..maxOverallUploadLimit = upByte - ..maxOverallDownloadLimit = downByte) + .changeGlobalOption( + Aria2Option() + ..maxOverallUploadLimit = upByte + ..maxOverallDownloadLimit = downByte, + ) .unwrap(); if (r != null) { await box.put('downloader_up_limit', upCtrl.text.trim()); diff --git a/lib/ui/home/downloader/home_downloader_ui_model.g.dart b/lib/ui/home/downloader/home_downloader_ui_model.g.dart index 47f226a..67e4a2a 100644 --- a/lib/ui/home/downloader/home_downloader_ui_model.g.dart +++ b/lib/ui/home/downloader/home_downloader_ui_model.g.dart @@ -42,7 +42,7 @@ final class HomeDownloaderUIModelProvider } String _$homeDownloaderUIModelHash() => - r'5b410cd38315d94279b18f147903eca4b09bd445'; + r'cb5d0973d56bbf40673afc2a734b49f5d034ab98'; abstract class _$HomeDownloaderUIModel extends $Notifier { diff --git a/lib/ui/home/home_ui_model.g.dart b/lib/ui/home/home_ui_model.g.dart index 00590f0..41f41b5 100644 --- a/lib/ui/home/home_ui_model.g.dart +++ b/lib/ui/home/home_ui_model.g.dart @@ -41,7 +41,7 @@ final class HomeUIModelProvider } } -String _$homeUIModelHash() => r'9dc8191f358c2d8e21ed931b3755e08ce394558e'; +String _$homeUIModelHash() => r'7dfe73383f7be2e520a42d176e199a8db208f008'; abstract class _$HomeUIModel extends $Notifier { HomeUIModelState build(); diff --git a/lib/ui/home/input_method/input_method_dialog_ui.dart b/lib/ui/home/input_method/input_method_dialog_ui.dart index 7a46900..630b47e 100644 --- a/lib/ui/home/input_method/input_method_dialog_ui.dart +++ b/lib/ui/home/input_method/input_method_dialog_ui.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/ui/home/input_method/input_method_dialog_ui_model.dart'; import 'package:starcitizen_doctor/ui/home/input_method/server.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; @@ -60,7 +61,9 @@ class InputMethodDialogUI extends HookConsumerWidget { ), SizedBox(height: 12), TextFormBox( - placeholder: S.current.input_method_input_placeholder, + placeholder: state.isEnableAutoTranslate + ? "${S.current.input_method_input_placeholder}\n\n本地翻译模型对中英混合处理能力较差,如有需要,建议分开发送。" + : S.current.input_method_input_placeholder, controller: srcTextCtrl, maxLines: 5, placeholderStyle: TextStyle(color: Colors.white.withValues(alpha: .6)), @@ -68,7 +71,9 @@ class InputMethodDialogUI extends HookConsumerWidget { onChanged: (str) async { final text = model.onTextChange("src", str); destTextCtrl.text = text ?? ""; - if (text != null) {} + if (text != null) { + model.checkAutoTranslate(); + } }, ), SizedBox(height: 16), @@ -91,17 +96,23 @@ class InputMethodDialogUI extends HookConsumerWidget { placeholderStyle: TextStyle(color: Colors.white.withValues(alpha: .6)), style: TextStyle(fontSize: 16, color: Colors.white), enabled: true, - onChanged: (str) { - // final text = model.onTextChange("dest", str); - // if (text != null) { - // srcTextCtrl.text = text; - // } - }, + onChanged: (str) {}, ), SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + Row( + children: [ + Text(S.current.input_method_auto_translate), + SizedBox(width: 6), + ToggleSwitch( + checked: state.isEnableAutoTranslate, + onChanged: (b) => _onSwitchAutoTranslate(context, model, b), + ), + ], + ), + SizedBox(width: 24), Row( children: [ Text(S.current.input_method_remote_input_service), @@ -194,4 +205,52 @@ class InputMethodDialogUI extends HookConsumerWidget { await serverModel.stopServer().unwrap(context: context); } } + + void _onSwitchAutoTranslate(BuildContext context, InputMethodDialogUIModel model, bool b) async { + if (b) { + // 检查下载任务 + if (await model.isTranslateModelDownloading()) { + if (!context.mounted) return; + showToast(context, "模型正在下载中,请稍后..."); + return; + } + // 打开,检查本地模型 + if (!await model.checkLocalTranslateModelAvailable()) { + if (!context.mounted) return; + // 询问用户是否下载模型 + final userOK = await showConfirmDialogs( + context, + "是否下载 AI 模型以使用翻译功能?", + Text( + "大约需要 200MB 的本地空间。" + "\n\n我们使用本地模型进行翻译,您的翻译数据不会发送给任何第三方。" + "\n\n模型未对游戏术语优化,请自行判断使用。", + ), + ); + if (userOK) { + try { + final guid = await model.doDownloadTranslateModel(); + if (guid.isNotEmpty) { + if (!context.mounted) return; + context.go("/index/downloader"); + await Future.delayed(Duration(seconds: 1)).then((_) { + if (!context.mounted) return; + showToast(context, "下载已开始,请在模型下载完成后重新启用翻译功能。"); + }); + return; + } + } catch (e) { + dPrint("下载模型失败:$e"); + if (context.mounted) { + showToast(context, "下载模型失败:$e"); + } + return; + } + } + return; + } + } + if (!context.mounted) return; + model.toggleAutoTranslate(b, context: context).unwrap(context: context); + } } diff --git a/lib/ui/home/input_method/input_method_dialog_ui_model.dart b/lib/ui/home/input_method/input_method_dialog_ui_model.dart index b9865f0..f5320db 100644 --- a/lib/ui/home/input_method/input_method_dialog_ui_model.dart +++ b/lib/ui/home/input_method/input_method_dialog_ui_model.dart @@ -1,12 +1,22 @@ +// ignore_for_file: avoid_build_context_in_providers, use_build_context_synchronously import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hive_ce/hive.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:starcitizen_doctor/api/api.dart'; +import 'package:starcitizen_doctor/common/io/rs_http.dart'; +import 'package:starcitizen_doctor/common/utils/async.dart'; +import 'package:starcitizen_doctor/common/utils/base_utils.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/common/utils/provider.dart'; +import 'package:starcitizen_doctor/provider/aria2c.dart'; import 'package:starcitizen_doctor/ui/home/localization/localization_ui_model.dart'; +import 'package:starcitizen_doctor/common/rust/api/ort_api.dart' as ort; part 'input_method_dialog_ui_model.g.dart'; @@ -44,7 +54,8 @@ class InputMethodDialogUIModel extends _$InputMethodDialogUIModel { final worldMaps = keyMaps?.map((key, value) => MapEntry(value.trim(), key)); final appBox = await Hive.openBox("app_conf"); final enableAutoCopy = appBox.get("enableAutoCopy", defaultValue: false); - final isEnableAutoTranslate = appBox.get("isEnableAutoTranslate", defaultValue: false); + final isEnableAutoTranslate = appBox.get("isEnableAutoTranslate_v2", defaultValue: false); + _checkAutoTranslateOnInit(); state = state.copyWith( keyMaps: keyMaps, worldMaps: worldMaps, @@ -134,14 +145,216 @@ class InputMethodDialogUIModel extends _$InputMethodDialogUIModel { _srcTextCtrl?.text = text; _destTextCtrl?.text = onTextChange("src", text) ?? ""; if (_destTextCtrl?.text.isEmpty ?? true) return; + checkAutoTranslate(webMessage: true); if (autoCopy && !state.isAutoTranslateWorking) { Clipboard.setData(ClipboardData(text: _destTextCtrl?.text ?? "")); } } - Future toggleAutoTranslate(bool b) async { + // ignore: duplicate_ignore + // ignore: avoid_build_context_in_providers + Future toggleAutoTranslate(bool b, {BuildContext? context}) async { state = state.copyWith(isEnableAutoTranslate: b); final appConf = await Hive.openBox("app_conf"); - await appConf.put("isEnableAutoTranslate", b); + await appConf.put("isEnableAutoTranslate_v2", b); + if (b) { + mountOnnxTranslationProvider(_localTranslateModelDir, _localTranslateModelName, context: context); + } + } + + Timer? _translateTimer; + + Future checkAutoTranslate({bool webMessage = false}) async { + final sourceText = _srcTextCtrl?.text ?? ""; + final content = _destTextCtrl?.text ?? ""; + if (sourceText.trim().isEmpty) return; + if (state.isEnableAutoTranslate) { + if (_translateTimer != null) _translateTimer?.cancel(); + state = state.copyWith(isAutoTranslateWorking: true); + _translateTimer = Timer(Duration(milliseconds: webMessage ? 150 : 400), () async { + try { + final inputText = sourceText.replaceAll("\n", " "); + final r = await doTranslateText(inputText); + if (r != null) { + String resultText = r; + // resultText 首字母大写 + if (content.isNotEmpty) { + final firstChar = resultText.characters.first; + resultText = resultText.replaceFirst(firstChar, firstChar.toUpperCase()); + } + _destTextCtrl?.text = "$content \n[en] $resultText"; + if (state.enableAutoCopy || webMessage) { + Clipboard.setData(ClipboardData(text: _destTextCtrl?.text ?? "")); + } + } + } catch (e) { + dPrint("[InputMethodDialogUIModel] AutoTranslate error: $e"); + } + state = state.copyWith(isAutoTranslateWorking: false); + }); + } + } + + String get _localTranslateModelName => "opus-mt-zh-en_onnx"; + + String get _localTranslateModelDir => "${appGlobalState.applicationSupportDir}/onnx_models"; + + OnnxTranslationProvider get _localTranslateModelProvider => + onnxTranslationProvider(_localTranslateModelDir, _localTranslateModelName); + + void _checkAutoTranslateOnInit() { + // 检查模型文件是否存在,不存在则关闭自动翻译 + if (state.isEnableAutoTranslate) { + checkLocalTranslateModelAvailable().then((available) { + if (!available) { + toggleAutoTranslate(false); + } + }); + } + } + + Future checkLocalTranslateModelAvailable() async { + final fileCheckList = const [ + "config.json", + "tokenizer.json", + "vocab.json", + "onnx/decoder_model_q4f16.onnx", + "onnx/encoder_model_q4f16.onnx", + ]; + var allExist = true; + for (var fileName in fileCheckList) { + final filePath = "$_localTranslateModelDir/$_localTranslateModelName/$fileName"; + if (!await File(filePath).exists()) { + allExist = false; + break; + } + } + return allExist; + } + + Future doDownloadTranslateModel() async { + state = state.copyWith(isAutoTranslateWorking: true); + try { + final aria2cManager = ref.read(aria2cModelProvider.notifier); + await aria2cManager.launchDaemon(appGlobalState.applicationBinaryModuleDir!); + final aria2c = ref.read(aria2cModelProvider).aria2c!; + + if (await aria2cManager.isNameInTask(_localTranslateModelName)) { + throw Exception("Model is already downloading"); + } + + final l = await Api.getAppTorrentDataList(); + final modelTorrent = l.firstWhere( + (element) => element.name == _localTranslateModelName, + orElse: () => throw Exception("Model torrent not found"), + ); + final torrentUrl = modelTorrent.url; + if (torrentUrl?.isEmpty ?? true) { + throw Exception("Get model torrent url failed"); + } + // get torrent Data + final data = await RSHttp.get(torrentUrl!); + final b64Str = base64Encode(data.data!); + final gid = await aria2c.addTorrent(b64Str, extraParams: {"dir": _localTranslateModelDir}); + return gid; + } catch (e) { + dPrint("[InputMethodDialogUIModel] doDownloadTranslateModel error: $e"); + rethrow; + } finally { + state = state.copyWith(isAutoTranslateWorking: false); + } + } + + Future mountOnnxTranslationProvider( + String localTranslateModelDir, + String localTranslateModelName, { + BuildContext? context, + }) async { + if (!ref.exists(_localTranslateModelProvider)) { + ref.listen(_localTranslateModelProvider, ((_, _) {})); + final err = await ref.read(_localTranslateModelProvider.notifier).initModel(); + _handleTranslateModel(context, err); + } else { + // 重新加载 + final err = await ref.read(_localTranslateModelProvider.notifier).initModel(); + _handleTranslateModel(context, err); + } + } + + Future _handleTranslateModel(BuildContext? context, String? err) async { + if (err != null) { + dPrint("[InputMethodDialogUIModel] mountOnnxTranslationProvider failed to init model"); + if (context != null) { + if (!context.mounted) return; + final userOK = await showConfirmDialogs(context, "翻译模型加载失败", Text("是否删除本地文件,稍后您可以尝试重新下载。错误信息:\n$err")); + if (userOK) { + // 删除文件,并禁用开关 + final dir = Directory("$_localTranslateModelDir/$_localTranslateModelName"); + if (await dir.exists()) { + await dir.delete(recursive: true); + dPrint("[InputMethodDialogUIModel] Deleted local translate model files."); + toggleAutoTranslate(false); + } + } + } else { + // 禁用开关 + toggleAutoTranslate(false); + } + } + } + + Future doTranslateText(String text) async { + if (!ref.exists(_localTranslateModelProvider)) { + await mountOnnxTranslationProvider(_localTranslateModelDir, _localTranslateModelName); + } + final onnxTranslationState = ref.read(_localTranslateModelProvider); + if (!onnxTranslationState) { + return null; + } + try { + final result = await ort.translateText(modelKey: _localTranslateModelName, text: text); + return result; + } catch (e) { + dPrint("[InputMethodDialogUIModel] doTranslateText error: $e"); + return null; + } + } + + Future isTranslateModelDownloading() async { + final aria2cManager = ref.read(aria2cModelProvider.notifier); + return await aria2cManager.isNameInTask(_localTranslateModelName); + } +} + +@riverpod +class OnnxTranslation extends _$OnnxTranslation { + @override + bool build(String modelDir, String modelName) { + dPrint("[OnnxTranslation] Build provider for model: $modelName"); + ref.onDispose(disposeModel); + return false; + } + + Future initModel() async { + dPrint("[OnnxTranslation] Load model: $modelName from $modelDir"); + String? errorMessage; + try { + await ort.loadTranslationModel( + modelPath: "$modelDir/$modelName", + modelKey: modelName, + quantizationSuffix: "_q4f16", + ); + state = true; + } catch (e) { + dPrint("[OnnxTranslation] Load model error: $e"); + errorMessage = e.toString(); + state = false; + } + return errorMessage; + } + + Future disposeModel() async { + await ort.unloadTranslationModel(modelKey: modelName).unwrap(); + dPrint("[OnnxTranslation] Unload model: $modelName"); } } diff --git a/lib/ui/home/input_method/input_method_dialog_ui_model.g.dart b/lib/ui/home/input_method/input_method_dialog_ui_model.g.dart index 9c335da..6270440 100644 --- a/lib/ui/home/input_method/input_method_dialog_ui_model.g.dart +++ b/lib/ui/home/input_method/input_method_dialog_ui_model.g.dart @@ -43,7 +43,7 @@ final class InputMethodDialogUIModelProvider } String _$inputMethodDialogUIModelHash() => - r'c07ef2474866bdb3944892460879121e0f90591f'; + r'f216c1a5b6d68b3924af7b351314c618dcac80b5'; abstract class _$InputMethodDialogUIModel extends $Notifier { @@ -65,3 +65,102 @@ abstract class _$InputMethodDialogUIModel element.handleValue(ref, created); } } + +@ProviderFor(OnnxTranslation) +const onnxTranslationProvider = OnnxTranslationFamily._(); + +final class OnnxTranslationProvider + extends $NotifierProvider { + const OnnxTranslationProvider._({ + required OnnxTranslationFamily super.from, + required (String, String) super.argument, + }) : super( + retry: null, + name: r'onnxTranslationProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$onnxTranslationHash(); + + @override + String toString() { + return r'onnxTranslationProvider' + '' + '$argument'; + } + + @$internal + @override + OnnxTranslation create() => OnnxTranslation(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(bool value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is OnnxTranslationProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$onnxTranslationHash() => r'4f3dc0e361dca2d6b00f557496bdf006cc6c235c'; + +final class OnnxTranslationFamily extends $Family + with + $ClassFamilyOverride< + OnnxTranslation, + bool, + bool, + bool, + (String, String) + > { + const OnnxTranslationFamily._() + : super( + retry: null, + name: r'onnxTranslationProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + OnnxTranslationProvider call(String modelDir, String modelName) => + OnnxTranslationProvider._(argument: (modelDir, modelName), from: this); + + @override + String toString() => r'onnxTranslationProvider'; +} + +abstract class _$OnnxTranslation extends $Notifier { + late final _$args = ref.$arg as (String, String); + String get modelDir => _$args.$1; + String get modelName => _$args.$2; + + bool build(String modelDir, String modelName); + @$mustCallSuper + @override + void runBuild() { + final created = build(_$args.$1, _$args.$2); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + bool, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/ui/home/localization/advanced_localization_ui_model.g.dart b/lib/ui/home/localization/advanced_localization_ui_model.g.dart index 38fb741..4b5d99d 100644 --- a/lib/ui/home/localization/advanced_localization_ui_model.g.dart +++ b/lib/ui/home/localization/advanced_localization_ui_model.g.dart @@ -47,7 +47,7 @@ final class AdvancedLocalizationUIModelProvider } String _$advancedLocalizationUIModelHash() => - r'2f890c854bc56e506c441acabc2014438a163617'; + r'c7cca8935ac7df2281e83297b11b6b82d94f7a59'; abstract class _$AdvancedLocalizationUIModel extends $Notifier { diff --git a/lib/ui/home/localization/localization_ui_model.dart b/lib/ui/home/localization/localization_ui_model.dart index 680dd18..2954421 100644 --- a/lib/ui/home/localization/localization_ui_model.dart +++ b/lib/ui/home/localization/localization_ui_model.dart @@ -66,6 +66,10 @@ class LocalizationUIModel extends _$LocalizationUIModel { @override LocalizationUIState build() { state = LocalizationUIState(selectedLanguage: languageSupport.keys.first); + ref.onDispose(() { + _customizeDirListenSub?.cancel(); + _customizeDirListenSub = null; + }); _init(); return state; } @@ -74,10 +78,6 @@ class LocalizationUIModel extends _$LocalizationUIModel { if (_scInstallPath == "not_install") { return; } - ref.onDispose(() { - _customizeDirListenSub?.cancel(); - _customizeDirListenSub = null; - }); final appConfBox = await Hive.openBox("app_conf"); final lang = await appConfBox.get("localization_selectedLanguage", defaultValue: languageSupport.keys.first); state = state.copyWith(selectedLanguage: lang); diff --git a/lib/ui/home/localization/localization_ui_model.g.dart b/lib/ui/home/localization/localization_ui_model.g.dart index 88a4385..eee7afd 100644 --- a/lib/ui/home/localization/localization_ui_model.g.dart +++ b/lib/ui/home/localization/localization_ui_model.g.dart @@ -42,7 +42,7 @@ final class LocalizationUIModelProvider } String _$localizationUIModelHash() => - r'd3797a7ff3d31dd1d4b05aed4a9969f4be6853c5'; + r'3d3f0ed7fa3631eca4e10d456c437f6fca8eedff'; abstract class _$LocalizationUIModel extends $Notifier { LocalizationUIState build(); diff --git a/lib/ui/home/performance/performance_ui_model.g.dart b/lib/ui/home/performance/performance_ui_model.g.dart index 2733524..70dc3d2 100644 --- a/lib/ui/home/performance/performance_ui_model.g.dart +++ b/lib/ui/home/performance/performance_ui_model.g.dart @@ -42,7 +42,7 @@ final class HomePerformanceUIModelProvider } String _$homePerformanceUIModelHash() => - r'c3c55c0470ef8c8be4915a1878deba332653ecde'; + r'4c5c33fe7d85dc8f6bf0d019c1b870d285d594ff'; abstract class _$HomePerformanceUIModel extends $Notifier { diff --git a/lib/ui/tools/tools_ui_model.dart b/lib/ui/tools/tools_ui_model.dart index e9eb890..b049272 100644 --- a/lib/ui/tools/tools_ui_model.dart +++ b/lib/ui/tools/tools_ui_model.dart @@ -19,7 +19,6 @@ import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/common/utils/multi_window_manager.dart'; import 'package:starcitizen_doctor/common/utils/provider.dart'; import 'package:starcitizen_doctor/provider/aria2c.dart'; -import 'package:starcitizen_doctor/ui/home/downloader/home_downloader_ui_model.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:xml/xml.dart'; @@ -176,7 +175,8 @@ class ToolsUIModel extends _$ToolsUIModel { "remove_nvme_settings", S.current.tools_action_remove_nvme_registry_patch, S.current.tools_action_info_nvme_patch_issue( - nvmePatchStatus ? S.current.localization_info_installed : S.current.tools_action_info_not_installed), + nvmePatchStatus ? S.current.localization_info_installed : S.current.tools_action_info_not_installed, + ), const Icon(FluentIcons.hard_drive, size: 24), onTap: nvmePatchStatus ? () async { @@ -208,7 +208,7 @@ class ToolsUIModel extends _$ToolsUIModel { state = state.copyWith(working: false); loadToolsCard(context, skipPathScan: true); }, - ) + ), ]; } @@ -266,8 +266,11 @@ class ToolsUIModel extends _$ToolsUIModel { if (listData == null) { return; } - scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData, - checkExists: checkActive, withVersion: AppConf.gameChannels); + scInstallPaths = await SCLoggerHelper.getGameInstallPath( + listData, + checkExists: checkActive, + withVersion: AppConf.gameChannels, + ); if (scInstallPaths.isNotEmpty) { scInstalledPath = scInstallPaths.first; } @@ -337,11 +340,12 @@ class ToolsUIModel extends _$ToolsUIModel { Future getSystemInfo() async { return S.current.tools_action_info_system_info_content( - await SystemHelper.getSystemName(), - await SystemHelper.getCpuName(), - await SystemHelper.getSystemMemorySizeGB(), - await SystemHelper.getGpuInfo(), - await SystemHelper.getDiskInfo()); + await SystemHelper.getSystemName(), + await SystemHelper.getCpuName(), + await SystemHelper.getSystemMemorySizeGB(), + await SystemHelper.getGpuInfo(), + await SystemHelper.getDiskInfo(), + ); } /// 管理员模式运行 RSI 启动器 @@ -365,9 +369,7 @@ class ToolsUIModel extends _$ToolsUIModel { builder: (context) => ContentDialog( title: Text(S.current.tools_action_info_system_info_title), content: Text(systemInfo), - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * .65, - ), + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .65), actions: [ FilledButton( child: Padding( @@ -404,8 +406,11 @@ class ToolsUIModel extends _$ToolsUIModel { if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) { if (!context.mounted) return; - showToast(context, S.current.tools_action_info_rsi_launcher_running_warning, - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35)); + showToast( + context, + S.current.tools_action_info_rsi_launcher_running_warning, + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35), + ); return; } @@ -423,14 +428,11 @@ class ToolsUIModel extends _$ToolsUIModel { final aria2c = ref.read(aria2cModelProvider).aria2c!; // check download task list - for (var value in [...await aria2c.tellActive(), ...await aria2c.tellWaiting(0, 100000)]) { - final t = HomeDownloaderUIModel.getTaskTypeAndName(value); - if (t.key == "torrent" && t.value.contains("Data.p4k")) { - if (!context.mounted) return; - showToast(context, S.current.tools_action_info_p4k_download_in_progress); - state = state.copyWith(working: false); - return; - } + if (await aria2cManager.isNameInTask("Data.p4k")) { + if (!context.mounted) return; + showToast(context, S.current.tools_action_info_p4k_download_in_progress); + state = state.copyWith(working: false); + return; } if (torrentUrl == "") { @@ -440,8 +442,11 @@ class ToolsUIModel extends _$ToolsUIModel { return; } - final userSelect = - await FilePicker.platform.saveFile(initialDirectory: savePath, fileName: fileName, lockParentWindow: true); + final userSelect = await FilePicker.platform.saveFile( + initialDirectory: savePath, + fileName: fileName, + lockParentWindow: true, + ); if (userSelect == null) { state = state.copyWith(working: false); return; @@ -550,16 +555,18 @@ class ToolsUIModel extends _$ToolsUIModel { static Future rsiEnhance(BuildContext context, {bool showNotGameInstallMsg = false}) async { if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) { if (!context.mounted) return; - showToast(context, S.current.tools_action_info_rsi_launcher_running_warning, - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35)); + showToast( + context, + S.current.tools_action_info_rsi_launcher_running_warning, + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35), + ); return; } if (!context.mounted) return; showDialog( - context: context, - builder: (BuildContext context) => RsiLauncherEnhanceDialogUI( - showNotGameInstallMsg: showNotGameInstallMsg, - )); + context: context, + builder: (BuildContext context) => RsiLauncherEnhanceDialogUI(showNotGameInstallMsg: showNotGameInstallMsg), + ); } Future _showLogAnalyze(BuildContext context) async { @@ -568,6 +575,10 @@ class ToolsUIModel extends _$ToolsUIModel { return; } if (!context.mounted) return; - await MultiWindowManager.launchSubWindow("log_analyze", S.current.log_analyzer_window_title, appGlobalState); + await MultiWindowManager.launchSubWindow( + WindowTypes.logAnalyze, + S.current.log_analyzer_window_title, + appGlobalState, + ); } } diff --git a/lib/ui/tools/tools_ui_model.g.dart b/lib/ui/tools/tools_ui_model.g.dart index 8ae0dff..23d0d07 100644 --- a/lib/ui/tools/tools_ui_model.g.dart +++ b/lib/ui/tools/tools_ui_model.g.dart @@ -41,7 +41,7 @@ final class ToolsUIModelProvider } } -String _$toolsUIModelHash() => r'81a73aeccf978f7e620681eaf1a3d4182ff48f9e'; +String _$toolsUIModelHash() => r'78732ff16e87cc9f92174bda43d0fafadba51146'; abstract class _$ToolsUIModel extends $Notifier { ToolsUIState build(); diff --git a/linux/my_application.cc b/linux/my_application.cc index c4b824f..0b13577 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -7,6 +7,8 @@ #include "flutter/generated_plugin_registrant.h" +#include "desktop_multi_window/desktop_multi_window_plugin.h" + struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; @@ -59,6 +61,10 @@ static void my_application_activate(GApplication* application) { fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + desktop_multi_window_plugin_set_window_created_callback([](FlPluginRegistry* registry){ + fl_register_plugins(registry); + }); + gtk_widget_grab_focus(GTK_WIDGET(view)); } diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift index 3cc05eb..d396133 100644 --- a/macos/Runner/MainFlutterWindow.swift +++ b/macos/Runner/MainFlutterWindow.swift @@ -1,5 +1,6 @@ import Cocoa import FlutterMacOS +import desktop_multi_window class MainFlutterWindow: NSWindow { override func awakeFromNib() { @@ -9,6 +10,11 @@ class MainFlutterWindow: NSWindow { self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) + + FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in + // Register the plugin which you want access from other isolate. + RegisterGeneratedPlugins(registry: controller) + } super.awakeFromNib() } diff --git a/pubspec.lock b/pubspec.lock index 9d7b740..b652b5f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,34 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a url: "https://pub.dev" source: hosted - version: "85.0.0" + version: "88.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" url: "https://pub.dev" source: hosted - version: "7.6.0" + version: "8.1.1" analyzer_buffer: dependency: transitive description: name: analyzer_buffer - sha256: f7833bee67c03c37241c67f8741b17cc501b69d9758df7a5a4a13ed6c947be43 + sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 url: "https://pub.dev" source: hosted - version: "0.1.10" + version: "0.1.11" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce + sha256: dd574a0ab77de88b7d9c12bc4b626109a5ca9078216a79041a5c24c3a1bd103c url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "0.13.7" archive: dependency: "direct main" description: @@ -278,42 +278,42 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "78085fbe842de7c5bef92de811ca81536968dbcbbcdac5c316711add2d15e796" + sha256: "751ee9440920f808266c3ec2553420dea56d3c7837dd2d62af76b11be3fcece5" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.8.1" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: cc5532d5733d4eccfccaaec6070a1926e9f21e613d93ad0927fad020b95c9e52 + sha256: "1128db6f58e71d43842f3b9be7465c83f0c47f4dd8918f878dd6ad3b72a32072" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.8.1" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: cc4684d22ca05bf0a4a51127e19a8aea576b42079ed2bc9e956f11aaebe35dd1 + sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.8.1" custom_lint_visitor: dependency: transitive description: name: custom_lint_visitor - sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2" + sha256: "446d68322747ec1c36797090de776aa72228818d3d80685a91ff524d163fee6d" url: "https://pub.dev" source: hosted - version: "1.0.0+7.7.0" + version: "1.0.0+8.1.1" dart_style: dependency: transitive description: name: dart_style - sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" dbus: dependency: transitive description: @@ -326,10 +326,10 @@ packages: dependency: "direct main" description: name: desktop_multi_window - sha256: "3ea2d696e50c3df696aabfddbd98c220ab4dde38f12c2ab12d1103bfe00ae79b" + sha256: "60ba38725b8887b60e44d15afdcf0c3813568b5da2ccaf1e7f6fd09a380a6e24" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.3.0" desktop_webview_window: dependency: "direct main" description: @@ -422,10 +422,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f + sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967 url: "https://pub.dev" source: hosted - version: "10.3.3" + version: "10.3.6" file_sizes: dependency: "direct main" description: @@ -618,10 +618,10 @@ packages: dependency: "direct main" description: name: hexcolor - sha256: c07f4bbb9095df87eeca87e7c69e8c3d60f70c66102d7b8d61c4af0453add3f6 + sha256: "0f237eed7db96ebacd8fda00d17f5ae262aaa84c213d53457c06b1dcbdfa81f2" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" highlight: dependency: transitive description: @@ -666,10 +666,10 @@ packages: dependency: "direct overridden" description: name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" http_client_helper: dependency: transitive description: @@ -874,10 +874,10 @@ packages: dependency: "direct main" description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -890,10 +890,10 @@ packages: dependency: transitive description: name: mockito - sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99" + sha256: "4feb43bc4eb6c03e832f5fcd637d1abb44b98f9cfa245c58e27382f58859f8f6" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.1" msix: dependency: "direct dev" description: @@ -1261,10 +1261,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" + sha256: "9098ab86015c4f1d8af6486b547b11100e73b193e1899015033cb3e14ad20243" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.2" source_helper: dependency: transitive description: @@ -1297,14 +1297,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" stack_trace: dependency: transitive description: @@ -1373,26 +1365,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.12" timing: dependency: transitive description: @@ -1485,10 +1477,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.2" vector_graphics: dependency: transitive description: @@ -1588,10 +1580,11 @@ packages: window_manager: dependency: "direct main" description: - name: window_manager - sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" - url: "https://pub.dev" - source: hosted + path: "packages/window_manager" + ref: "6fae92d21b4c80ce1b8f71c1190d7970cf722bd4" + resolved-ref: "6fae92d21b4c80ce1b8f71c1190d7970cf722bd4" + url: "https://github.com/boyan01/window_manager.git" + source: git version: "0.5.1" xdg_directories: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 5df54c1..e7fc839 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,11 +15,15 @@ dependencies: sdk: flutter flutter_riverpod: ^3.0.3 riverpod_annotation: ^3.0.3 - flutter_hooks: ^0.21.3+1 + flutter_hooks: ^0.21.3 hooks_riverpod: ^3.0.3 json_annotation: ^4.9.0 go_router: ^17.0.0 - window_manager: ^0.5.1 + window_manager: + git: + url: https://github.com/boyan01/window_manager.git + path: packages/window_manager + ref: 6fae92d21b4c80ce1b8f71c1190d7970cf722bd4 fluent_ui: 4.11.3 flutter_staggered_grid_view: ^0.7.0 flutter_acrylic: ^1.1.4 @@ -33,20 +37,20 @@ dependencies: markdown_widget: ^2.3.2+8 extended_image: ^10.0.1 device_info_plus: ^12.2.0 - file_picker: ^10.3.3 + file_picker: ^10.3.6 file_sizes: ^1.0.6 desktop_webview_window: ^0.2.3 flutter_svg: ^2.2.2 archive: ^4.0.7 jwt_decode: ^0.3.1 - uuid: ^4.5.1 + uuid: ^4.5.2 flutter_tilt: ^3.3.2 card_swiper: ^3.0.1 ffi: ^2.1.4 flutter_rust_bridge: ^2.11.1 freezed_annotation: ^3.1.0 - meta: ^1.16.0 - hexcolor: ^3.0.1 + meta: ^1.17.0 + hexcolor: ^3.0.2 html: ^0.15.6 fixnum: ^1.1.1 rust_builder: @@ -54,7 +58,6 @@ dependencies: aria2: git: https://github.com/xkeyC/dart_aria2_rpc.git # path: ../../xkeyC/dart_aria2_rpc - # path: ../../xkeyC/dart_aria2_rpc intl: any synchronized: ^3.4.0 super_sliver_list: ^0.4.1 @@ -63,13 +66,13 @@ dependencies: re_highlight: ^0.0.3 shelf: ^1.4.2 qr_flutter: ^4.1.0 - desktop_multi_window: ^0.2.1 + desktop_multi_window: ^0.3.0 watcher: ^1.1.4 path: ^1.9.1 crypto: ^3.0.7 xml: ^6.6.1 dependency_overrides: - http: ^1.5.0 + http: ^1.6.0 intl: ^0.20.2 dev_dependencies: @@ -81,7 +84,7 @@ dev_dependencies: freezed: ^3.2.3 json_serializable: ^6.11.1 riverpod_generator: ^3.0.3 - custom_lint: ^0.8.0 + custom_lint: ^0.8.1 riverpod_lint: ^3.0.3 ffigen: ^20.0.0 sct_dev_tools: diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0da8580..5bc9c55 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,6 +17,20 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -316,12 +330,24 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + [[package]] name = "bitflags" version = "2.10.0" @@ -389,6 +415,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.43" @@ -496,6 +531,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + [[package]] name = "compression-codecs" version = "0.4.31" @@ -630,6 +680,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -655,14 +715,38 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + [[package]] name = "darling" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", ] [[package]] @@ -679,13 +763,24 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", "quote", "syn", ] @@ -699,6 +794,15 @@ dependencies = [ "cc", ] +[[package]] +name = "dary_heap" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" +dependencies = [ + "serde", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -729,6 +833,16 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.4" @@ -739,6 +853,37 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -865,6 +1010,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" + [[package]] name = "event-listener" version = "5.4.1" @@ -902,6 +1053,18 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -1230,7 +1393,7 @@ dependencies = [ "parking_lot", "rand", "resolv-conf", - "smallvec", + "smallvec 1.15.1", "thiserror 2.0.17", "tokio", "tracing", @@ -1293,7 +1456,7 @@ dependencies = [ "itoa", "pin-project-lite", "pin-utils", - "smallvec", + "smallvec 1.15.1", "tokio", "want", ] @@ -1337,7 +1500,7 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1418,7 +1581,7 @@ dependencies = [ "icu_normalizer_data", "icu_properties", "icu_provider", - "smallvec", + "smallvec 1.15.1", "zerovec", ] @@ -1480,7 +1643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", - "smallvec", + "smallvec 1.15.1", "utf8_iter", ] @@ -1575,6 +1738,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1603,6 +1775,17 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1654,6 +1837,32 @@ dependencies = [ "time", ] +[[package]] +name = "macro_rules_attribute" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1725,11 +1934,33 @@ dependencies = [ "parking_lot", "portable-atomic", "rustc_version", - "smallvec", + "smallvec 1.15.1", "tagptr", "uuid", ] +[[package]] +name = "monostate" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "native-tls" version = "0.2.14" @@ -1747,6 +1978,36 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "ndarray" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7c9125e8f6f10c9da3aad044cc918cf8784fa34de857b1aa68038eb05a50a9" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + [[package]] name = "nix" version = "0.30.1" @@ -1784,12 +2045,30 @@ dependencies = [ "zbus", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1873,6 +2152,28 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "openssl" version = "0.10.74" @@ -1927,6 +2228,31 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "ort" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa7e49bd669d32d7bc2a15ec540a527e7764aec722a45467814005725bcd721" +dependencies = [ + "ndarray 0.16.1", + "ort-sys", + "smallvec 2.0.0-alpha.10", + "tracing", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2aba9f5c7c479925205799216e7e5d07cc1d4fa76ea8058c60a9a30f6a4e890" +dependencies = [ + "flate2", + "pkg-config", + "sha2", + "tar", + "ureq", +] + [[package]] name = "oslog" version = "0.2.0" @@ -1969,10 +2295,25 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec", + "smallvec 1.15.1", "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2037,6 +2378,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -2203,6 +2553,43 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-cond" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" +dependencies = [ + "either", + "itertools 0.14.0", + "rayon", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2268,7 +2655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "async-compression", - "base64", + "base64 0.22.1", "bytes", "cookie", "cookie_store", @@ -2340,10 +2727,14 @@ dependencies = [ "flutter_rust_bridge", "futures", "hickory-resolver", + "ndarray 0.17.1", "notify-rust", "once_cell", + "ort", "reqwest", "scopeguard", + "serde_json", + "tokenizers", "tokio", "url", "walkdir", @@ -2581,7 +2972,7 @@ version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -2600,7 +2991,7 @@ version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn", @@ -2659,6 +3050,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smallvec" +version = "2.0.0-alpha.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d44cfb396c3caf6fbfd0ab422af02631b69ddd96d2eff0b0f0724f9024051b" + [[package]] name = "socket2" version = "0.5.10" @@ -2679,6 +3076,29 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom", + "serde", + "unicode-segmentation", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2761,6 +3181,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tauri-winrt-notification" version = "0.7.2" @@ -2900,6 +3331,39 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokenizers" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6475a27088c98ea96d00b39a9ddfb63780d1ad4cceb6f48374349a96ab2b7842" +dependencies = [ + "ahash", + "aho-corasick", + "compact_str", + "dary_heap", + "derive_builder", + "esaxx-rs", + "getrandom 0.3.4", + "itertools 0.14.0", + "log", + "macro_rules_attribute", + "monostate", + "onig", + "paste", + "rand", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror 2.0.17", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + [[package]] name = "tokio" version = "1.48.0" @@ -3117,18 +3581,69 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +[[package]] +name = "unicode-normalization-alignments" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" +dependencies = [ + "smallvec 1.15.1", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +dependencies = [ + "base64 0.22.1", + "der", + "log", + "native-tls", + "percent-encoding", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf-8", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.7" @@ -3141,6 +3656,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3309,7 +3830,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d12a78aa0bab22d2f26ed1a96df7ab58e8a93506a3e20adb47c51a93b4e1357" dependencies = [ "const_format", - "itertools", + "itertools 0.11.0", "nom", "pori", "regex", @@ -3337,6 +3858,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.3" @@ -3865,6 +4395,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 5b2cf8d..204095f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -23,6 +23,10 @@ scopeguard = "1.2" notify-rust = "4" asar = "0.3.0" walkdir = "2.5.0" +ort = { version = "2.0.0-rc.10", features = ["xnnpack", "download-binaries", "ndarray"] } +tokenizers = { version = "0.22", default-features = false, features = ["onig"] } +ndarray = "0.17" +serde_json = "1.0" [target.'cfg(windows)'.dependencies] windows = { version = "0.62.2", features = ["Win32_UI_WindowsAndMessaging"] } diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs index cb0e8f9..1c73f59 100644 --- a/rust/src/api/mod.rs +++ b/rust/src/api/mod.rs @@ -5,3 +5,4 @@ pub mod http_api; pub mod rs_process; pub mod win32_api; pub mod asar_api; +pub mod ort_api; diff --git a/rust/src/api/ort_api.rs b/rust/src/api/ort_api.rs new file mode 100644 index 0000000..8a661e5 --- /dev/null +++ b/rust/src/api/ort_api.rs @@ -0,0 +1,107 @@ +use anyhow::Result; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Mutex; + +use crate::ort_models::opus_mt::OpusMtModel; + +/// 全局模型缓存 +static MODEL_CACHE: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); + +/// 加载 ONNX 翻译模型 +/// +/// # Arguments +/// * `model_path` - 模型文件夹路径 +/// * `model_key` - 模型缓存键(用于标识模型,如 "zh-en") +/// * `quantization_suffix` - 量化后缀(如 "_q4", "_q8",空字符串表示使用默认模型) +/// +pub fn load_translation_model( + model_path: String, + model_key: String, + quantization_suffix: String, +) -> Result<()> { + let model = OpusMtModel::new(&model_path, &quantization_suffix)?; + + let mut cache = MODEL_CACHE + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock model cache: {}", e))?; + + cache.insert(model_key, model); + + Ok(()) +} + +/// 翻译文本 +/// +/// # Arguments +/// * `model_key` - 模型缓存键(如 "zh-en") +/// * `text` - 要翻译的文本 +/// +/// # Returns +/// * `Result` - 翻译后的文本 +pub fn translate_text(model_key: String, text: String) -> Result { + let cache = MODEL_CACHE + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock model cache: {}", e))?; + + let model = cache.get(&model_key).ok_or_else(|| { + anyhow::anyhow!( + "Model not found: {}. Please load the model first.", + model_key + ) + })?; + + model.translate(&text) +} + +/// 批量翻译文本 +/// +/// # Arguments +/// * `model_key` - 模型缓存键(如 "zh-en") +/// * `texts` - 要翻译的文本列表 +/// +/// # Returns +/// * `Result>` - 翻译后的文本列表 +pub fn translate_text_batch(model_key: String, texts: Vec) -> Result> { + let cache = MODEL_CACHE + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock model cache: {}", e))?; + + let model = cache.get(&model_key).ok_or_else(|| { + anyhow::anyhow!( + "Model not found: {}. Please load the model first.", + model_key + ) + })?; + + model.translate_batch(&texts) +} + +/// 卸载模型 +/// +/// # Arguments +/// * `model_key` - 模型缓存键(如 "zh-en") +/// +pub fn unload_translation_model(model_key: String) -> Result<()> { + let mut cache = MODEL_CACHE + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock model cache: {}", e))?; + + cache.remove(&model_key); + + Ok(()) +} + +/// 清空所有已加载的模型 +/// +/// # Returns +pub fn clear_all_models() -> Result<()> { + let mut cache = MODEL_CACHE + .lock() + .map_err(|e| anyhow::anyhow!("Failed to lock model cache: {}", e))?; + + cache.clear(); + + Ok(()) +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 69a3d28..6e2f10b 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueNom, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1832496273; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -706588047; // Section: executor @@ -45,6 +45,27 @@ flutter_rust_bridge::frb_generated_default_handler!(); // Section: wire_funcs +fn wire__crate__api__ort_api__clear_all_models_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "clear_all_models", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = crate::api::ort_api::clear_all_models()?; + Ok(output_ok) + })(), + ) + } + }, + ) +} fn wire__crate__api__http_api__dns_lookup_ips_impl( port_: flutter_rust_bridge::for_generated::MessagePort, host: impl CstDecode, @@ -161,6 +182,37 @@ fn wire__crate__api__asar_api__get_rsi_launcher_asar_data_impl( }, ) } +fn wire__crate__api__ort_api__load_translation_model_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + model_path: impl CstDecode, + model_key: impl CstDecode, + quantization_suffix: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "load_translation_model", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_model_path = model_path.cst_decode(); + let api_model_key = model_key.cst_decode(); + let api_quantization_suffix = quantization_suffix.cst_decode(); + move |context| { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = crate::api::ort_api::load_translation_model( + api_model_path, + api_model_key, + api_quantization_suffix, + )?; + Ok(output_ok) + })(), + ) + } + }, + ) +} fn wire__crate__api__asar_api__rsi_launcher_asar_data_write_main_js_impl( port_: flutter_rust_bridge::for_generated::MessagePort, that: impl CstDecode, @@ -315,6 +367,82 @@ fn wire__crate__api__rs_process__start_impl( }, ) } +fn wire__crate__api__ort_api__translate_text_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + model_key: impl CstDecode, + text: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "translate_text", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_model_key = model_key.cst_decode(); + let api_text = text.cst_decode(); + move |context| { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = + crate::api::ort_api::translate_text(api_model_key, api_text)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} +fn wire__crate__api__ort_api__translate_text_batch_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + model_key: impl CstDecode, + texts: impl CstDecode>, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "translate_text_batch", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_model_key = model_key.cst_decode(); + let api_texts = texts.cst_decode(); + move |context| { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = + crate::api::ort_api::translate_text_batch(api_model_key, api_texts)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} +fn wire__crate__api__ort_api__unload_translation_model_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + model_key: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "unload_translation_model", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_model_key = model_key.cst_decode(); + move |context| { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = + crate::api::ort_api::unload_translation_model(api_model_key)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} fn wire__crate__api__rs_process__write_impl( port_: flutter_rust_bridge::for_generated::MessagePort, rs_pid: impl CstDecode, @@ -1361,6 +1489,13 @@ mod io { } } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__ort_api__clear_all_models( + port_: i64, + ) { + wire__crate__api__ort_api__clear_all_models_impl(port_) + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__http_api__dns_lookup_ips( port_: i64, @@ -1406,6 +1541,21 @@ mod io { wire__crate__api__asar_api__get_rsi_launcher_asar_data_impl(port_, asar_path) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__ort_api__load_translation_model( + port_: i64, + model_path: *mut wire_cst_list_prim_u_8_strict, + model_key: *mut wire_cst_list_prim_u_8_strict, + quantization_suffix: *mut wire_cst_list_prim_u_8_strict, + ) { + wire__crate__api__ort_api__load_translation_model_impl( + port_, + model_path, + model_key, + quantization_suffix, + ) + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__asar_api__rsi_launcher_asar_data_write_main_js( port_: i64, @@ -1459,6 +1609,32 @@ mod io { ) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__ort_api__translate_text( + port_: i64, + model_key: *mut wire_cst_list_prim_u_8_strict, + text: *mut wire_cst_list_prim_u_8_strict, + ) { + wire__crate__api__ort_api__translate_text_impl(port_, model_key, text) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__ort_api__translate_text_batch( + port_: i64, + model_key: *mut wire_cst_list_prim_u_8_strict, + texts: *mut wire_cst_list_String, + ) { + wire__crate__api__ort_api__translate_text_batch_impl(port_, model_key, texts) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__ort_api__unload_translation_model( + port_: i64, + model_key: *mut wire_cst_list_prim_u_8_strict, + ) { + wire__crate__api__ort_api__unload_translation_model_impl(port_, model_key) + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__rs_process__write( port_: i64, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 2c46138..118a7c7 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,3 +1,4 @@ pub mod api; mod frb_generated; pub mod http_package; +pub mod ort_models; diff --git a/rust/src/ort_models/mod.rs b/rust/src/ort_models/mod.rs new file mode 100644 index 0000000..5fe1506 --- /dev/null +++ b/rust/src/ort_models/mod.rs @@ -0,0 +1 @@ +pub mod opus_mt; \ No newline at end of file diff --git a/rust/src/ort_models/opus_mt.rs b/rust/src/ort_models/opus_mt.rs new file mode 100644 index 0000000..ef274d9 --- /dev/null +++ b/rust/src/ort_models/opus_mt.rs @@ -0,0 +1,403 @@ +use anyhow::{anyhow, Context, Result}; +use ndarray::{Array2, ArrayD}; +use ort::{ + execution_providers::XNNPACKExecutionProvider, session::builder::GraphOptimizationLevel, + session::Session, value::Value, +}; +use std::path::Path; +use std::sync::Mutex; +use tokenizers::Tokenizer; + +/// Opus-MT 翻译模型的推理结构 +pub struct OpusMtModel { + encoder_session: Mutex, + decoder_session: Mutex, + tokenizer: Tokenizer, + config: ModelConfig, +} + +/// 模型配置 +#[derive(Debug, Clone)] +pub struct ModelConfig { + pub max_length: usize, + pub num_beams: usize, + pub decoder_start_token_id: i64, + pub eos_token_id: i64, + pub pad_token_id: i64, +} + +impl Default for ModelConfig { + fn default() -> Self { + Self { + max_length: 512, + num_beams: 1, + decoder_start_token_id: 0, + eos_token_id: 0, + pad_token_id: 0, + } + } +} + +impl OpusMtModel { + /// 从模型路径创建新的 OpusMT 模型实例 + /// + /// # Arguments + /// * `model_path` - 模型文件夹路径(应包含 onnx 子文件夹) + /// * `quantization_suffix` - 量化后缀,如 "_q4", "_q8",为空字符串则使用默认模型 + /// + /// # Returns + /// * `Result` - 成功返回模型实例,失败返回错误 + pub fn new>(model_path: P, quantization_suffix: &str) -> Result { + let model_path = model_path.as_ref(); + + // onnx-community 标准:模型在 onnx 子文件夹中 + let onnx_dir = model_path.join("onnx"); + + // 加载 tokenizer(在根目录) + let tokenizer_path = model_path.join("tokenizer.json"); + + // 动态加载并修复 tokenizer + let tokenizer = + Self::load_tokenizer(&tokenizer_path).context("Failed to load tokenizer")?; + + // 构建模型文件名 + let encoder_filename = if quantization_suffix.is_empty() { + "encoder_model.onnx".to_string() + } else { + format!("encoder_model{}.onnx", quantization_suffix) + }; + + let decoder_filename = if quantization_suffix.is_empty() { + "decoder_model.onnx".to_string() + } else { + format!("decoder_model{}.onnx", quantization_suffix) + }; + + // 加载 encoder 模型(在 onnx 子目录) + let encoder_path = onnx_dir.join(&encoder_filename); + if !encoder_path.exists() { + return Err(anyhow!( + "Encoder model not found: {}", + encoder_path.display() + )); + } + + let encoder_session = Session::builder() + .context("Failed to create encoder session builder")? + .with_optimization_level(GraphOptimizationLevel::Level3) + .context("Failed to set optimization level")? + .with_intra_threads(4) + .context("Failed to set intra threads")? + .with_execution_providers([XNNPACKExecutionProvider::default().build()]) + .context("Failed to register XNNPACK execution provider")? + .commit_from_file(&encoder_path) + .context(format!( + "Failed to load encoder model: {}", + encoder_filename + ))?; + + // 加载 decoder 模型(在 onnx 子目录) + let decoder_path = onnx_dir.join(&decoder_filename); + if !decoder_path.exists() { + return Err(anyhow!( + "Decoder model not found: {}", + decoder_path.display() + )); + } + + let decoder_session = Session::builder() + .context("Failed to create decoder session builder")? + .with_optimization_level(GraphOptimizationLevel::Level3) + .context("Failed to set optimization level")? + .with_intra_threads(4) + .context("Failed to set intra threads")? + .with_execution_providers([XNNPACKExecutionProvider::default().build()]) + .context("Failed to register XNNPACK execution provider")? + .commit_from_file(&decoder_path) + .context(format!( + "Failed to load decoder model: {}", + decoder_filename + ))?; + + // 加载配置(如果存在,在根目录) + let config = Self::load_config(model_path)?; + + Ok(Self { + encoder_session: Mutex::new(encoder_session), + decoder_session: Mutex::new(decoder_session), + tokenizer, + config, + }) + } + + /// 动态加载 tokenizer,自动修复常见问题 + fn load_tokenizer(tokenizer_path: &Path) -> Result { + use std::fs; + + // 读取原始文件 + let content = + fs::read_to_string(tokenizer_path).context("Failed to read tokenizer.json")?; + + // 解析为 JSON + let mut json: serde_json::Value = + serde_json::from_str(&content).context("Failed to parse tokenizer.json")?; + + let mut needs_fix = false; + + // 修复 normalizer 中的问题 + if let Some(obj) = json.as_object_mut() { + if let Some(normalizer) = obj.get("normalizer") { + let mut should_remove_normalizer = false; + + if normalizer.is_null() { + // normalizer 是 null,需要移除 + should_remove_normalizer = true; + } else if let Some(norm_obj) = normalizer.as_object() { + // 检查是否是有问题的 Precompiled 类型 + if let Some(type_val) = norm_obj.get("type") { + if type_val.as_str() == Some("Precompiled") { + // 检查 precompiled_charsmap 字段 + if let Some(precompiled) = norm_obj.get("precompiled_charsmap") { + if precompiled.is_null() { + // precompiled_charsmap 是 null,移除整个 normalizer + should_remove_normalizer = true; + } + } else { + // 缺少 precompiled_charsmap 字段,移除整个 normalizer + should_remove_normalizer = true; + } + } + } + } + + if should_remove_normalizer { + obj.remove("normalizer"); + needs_fix = true; + } + } + } + + // 从修复后的 JSON 字符串加载 tokenizer + let json_str = if needs_fix { + serde_json::to_string(&json).context("Failed to serialize fixed tokenizer")? + } else { + content + }; + + // 从字节数组加载 tokenizer + Tokenizer::from_bytes(json_str.as_bytes()) + .map_err(|e| anyhow!("Failed to load tokenizer: {}", e)) + } + + /// 从配置文件加载模型配置 + fn load_config(model_path: &Path) -> Result { + let config_path = model_path.join("config.json"); + + if config_path.exists() { + let config_str = + std::fs::read_to_string(config_path).context("Failed to read config.json")?; + let config_json: serde_json::Value = + serde_json::from_str(&config_str).context("Failed to parse config.json")?; + + Ok(ModelConfig { + max_length: config_json["max_length"].as_u64().unwrap_or(512) as usize, + num_beams: config_json["num_beams"].as_u64().unwrap_or(1) as usize, + decoder_start_token_id: config_json["decoder_start_token_id"].as_i64().unwrap_or(0), + eos_token_id: config_json["eos_token_id"].as_i64().unwrap_or(0), + pad_token_id: config_json["pad_token_id"].as_i64().unwrap_or(0), + }) + } else { + Ok(ModelConfig::default()) + } + } + + /// 翻译文本 + /// + /// # Arguments + /// * `text` - 要翻译的文本 + /// + /// # Returns + /// * `Result` - 翻译后的文本 + pub fn translate(&self, text: &str) -> Result { + // 1. Tokenize 输入文本 + let encoding = self + .tokenizer + .encode(text, true) + .map_err(|e| anyhow!("Failed to encode text: {}", e))?; + + let input_ids = encoding.get_ids(); + let attention_mask = encoding.get_attention_mask(); + + // 2. 准备 encoder 输入 + let batch_size = 1; + let seq_len = input_ids.len(); + + let input_ids_array: Array2 = Array2::from_shape_vec( + (batch_size, seq_len), + input_ids.iter().map(|&id| id as i64).collect(), + ) + .context("Failed to create input_ids array")?; + + let attention_mask_array: Array2 = Array2::from_shape_vec( + (batch_size, seq_len), + attention_mask.iter().map(|&mask| mask as i64).collect(), + ) + .context("Failed to create attention_mask array")?; + + // 3. 运行 encoder + let input_ids_value = Value::from_array(( + input_ids_array.shape().to_vec(), + input_ids_array.into_raw_vec_and_offset().0, + )) + .context("Failed to create input_ids value")?; + let attention_mask_value = Value::from_array(( + attention_mask_array.shape().to_vec(), + attention_mask_array.clone().into_raw_vec_and_offset().0, + )) + .context("Failed to create attention_mask value")?; + + let encoder_inputs = ort::inputs![ + "input_ids" => input_ids_value, + "attention_mask" => attention_mask_value, + ]; + + let mut encoder_session = self + .encoder_session + .lock() + .map_err(|e| anyhow!("Failed to lock encoder session: {}", e))?; + let encoder_outputs = encoder_session + .run(encoder_inputs) + .context("Failed to run encoder")?; + + let encoder_hidden_states = encoder_outputs["last_hidden_state"] + .try_extract_tensor::() + .context("Failed to extract encoder hidden states")?; + + // 将 tensor 转换为 ArrayD + let (shape, data) = encoder_hidden_states; + let shape_vec: Vec = shape.iter().map(|&x| x as usize).collect(); + let encoder_array = ArrayD::from_shape_vec(shape_vec, data.to_vec()) + .context("Failed to create encoder array")?; + + // 4. 贪婪解码生成输出 + let output_ids = self.greedy_decode(encoder_array, &attention_mask_array)?; + + // 5. Decode 输出 token IDs + let output_tokens: Vec = output_ids.iter().map(|&id| id as u32).collect(); + let decoded = self + .tokenizer + .decode(&output_tokens, true) + .map_err(|e| anyhow!("Failed to decode output: {}", e))?; + + Ok(decoded) + } + + /// 贪婪解码 + fn greedy_decode( + &self, + encoder_hidden_states: ArrayD, + encoder_attention_mask: &Array2, + ) -> Result> { + let batch_size = 1; + let mut generated_ids = vec![self.config.decoder_start_token_id]; + + for _ in 0..self.config.max_length { + // 准备 decoder 输入 + let decoder_input_len = generated_ids.len(); + let decoder_input_ids: Array2 = + Array2::from_shape_vec((batch_size, decoder_input_len), generated_ids.clone()) + .context("Failed to create decoder input_ids")?; + + // 创建 ORT Value + let decoder_input_value = Value::from_array(( + decoder_input_ids.shape().to_vec(), + decoder_input_ids.into_raw_vec_and_offset().0, + )) + .context("Failed to create decoder input value")?; + let encoder_hidden_value = Value::from_array(( + encoder_hidden_states.shape().to_vec(), + encoder_hidden_states.clone().into_raw_vec_and_offset().0, + )) + .context("Failed to create encoder hidden value")?; + let encoder_mask_value = Value::from_array(( + encoder_attention_mask.shape().to_vec(), + encoder_attention_mask.clone().into_raw_vec_and_offset().0, + )) + .context("Failed to create encoder mask value")?; + + // 运行 decoder + let decoder_inputs = ort::inputs![ + "input_ids" => decoder_input_value, + "encoder_hidden_states" => encoder_hidden_value, + "encoder_attention_mask" => encoder_mask_value, + ]; + + let mut decoder_session = self + .decoder_session + .lock() + .map_err(|e| anyhow!("Failed to lock decoder session: {}", e))?; + let decoder_outputs = decoder_session + .run(decoder_inputs) + .context("Failed to run decoder")?; + + // 获取 logits + let logits_tensor = decoder_outputs["logits"] + .try_extract_tensor::() + .context("Failed to extract logits")?; + + let (logits_shape, logits_data) = logits_tensor; + let vocab_size = logits_shape[2] as usize; + + // 获取最后一个 token 的 logits + let last_token_idx = decoder_input_len - 1; + let last_logits_start = last_token_idx * vocab_size; + let last_logits_end = last_logits_start + vocab_size; + + let last_logits_slice = &logits_data[last_logits_start..last_logits_end]; + + // 找到最大概率的 token + let next_token_id = last_logits_slice + .iter() + .enumerate() + .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) + .map(|(idx, _)| idx as i64) + .context("Failed to find max token")?; + + // 检查是否到达结束 token + if next_token_id == self.config.eos_token_id { + break; + } + + generated_ids.push(next_token_id); + } + + Ok(generated_ids) + } + + /// 批量翻译文本 + /// + /// # Arguments + /// * `texts` - 要翻译的文本列表 + /// + /// # Returns + /// * `Result>` - 翻译后的文本列表 + pub fn translate_batch(&self, texts: &[String]) -> Result> { + texts.iter().map(|text| self.translate(text)).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_translation() { + let model = OpusMtModel::new( + "C:\\Users\\xkeyc\\Downloads\\onnx_models\\opus-mt-zh-en", + "_q4f16", + ) + .unwrap(); + let result = model.translate("你好世界").unwrap(); + println!("Translation: {}", result); + } +} diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 955ee30..9c69cdb 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -3,6 +3,7 @@ #include #include "flutter/generated_plugin_registrant.h" +#include "desktop_multi_window/desktop_multi_window_plugin.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} @@ -25,6 +26,12 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); + DesktopMultiWindowSetWindowCreatedCallback([](void *controller) { + auto *flutter_view_controller = + reinterpret_cast(controller); + auto *registry = flutter_view_controller->engine(); + RegisterPlugins(registry); + }); SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() {