Merge pull request #8 from StarCitizenToolBox/feat/full_riverpod

Feat: full riverpod
This commit is contained in:
xkeyC 2024-03-10 19:54:22 +08:00 committed by GitHub
commit 00e5f7545b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
92 changed files with 6690 additions and 7108 deletions

View File

@ -26,5 +26,9 @@ linter:
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
dangling_library_doc_comments: false
analyzer:
plugins:
- custom_lint
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

279
lib/app.dart Normal file
View File

@ -0,0 +1,279 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:go_router/go_router.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/ui/home/performance/performance_ui.dart';
import 'package:starcitizen_doctor/ui/splash_ui.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:uuid/uuid.dart';
import 'package:window_manager/window_manager.dart';
import 'api/analytics.dart';
import 'api/api.dart';
import 'common/helper/system_helper.dart';
import 'common/io/rs_http.dart';
import 'common/rust/frb_generated.dart';
import 'data/app_version_data.dart';
import 'ui/home/downloader/home_downloader_ui.dart';
import 'ui/home/game_doctor/game_doctor_ui.dart';
import 'ui/index_ui.dart';
import 'ui/settings/upgrade_dialog.dart';
part 'app.g.dart';
part 'app.freezed.dart';
@riverpod
GoRouter router(RouterRef ref) {
return GoRouter(
routes: [
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())),
GoRoute(
path: 'game_doctor',
pageBuilder: (context, state) =>
myPageBuilder(context, state, const HomeGameDoctorUI()),
),
GoRoute(
path: 'performance',
pageBuilder: (context, state) =>
myPageBuilder(context, state, const HomePerformanceUI()),
),
],
),
],
);
}
@riverpod
class AppGlobalModel extends _$AppGlobalModel {
@override
AppGlobalState build() {
return const AppGlobalState();
}
bool _initialized = false;
Future<void> initApp() async {
if (_initialized) return;
// init Data
final userProfileDir = Platform.environment["USERPROFILE"];
final applicationSupportDir =
(await getApplicationSupportDirectory()).absolute.path;
String? applicationBinaryModuleDir;
final logFile = File(
"$applicationSupportDir\\logs\\${DateTime.now().millisecondsSinceEpoch}.log");
await logFile.create(recursive: true);
setDPrintFile(logFile);
if (ConstConf.isMSE && userProfileDir != null) {
applicationBinaryModuleDir =
"$userProfileDir\\AppData\\Local\\Temp\\SCToolbox\\modules";
} else {
applicationBinaryModuleDir = "$applicationSupportDir\\modules";
}
dPrint("applicationSupportDir == $applicationSupportDir");
dPrint("applicationBinaryModuleDir == $applicationBinaryModuleDir");
state = state.copyWith(
applicationSupportDir: applicationSupportDir,
applicationBinaryModuleDir: applicationBinaryModuleDir,
);
// init Hive
try {
Hive.init("$applicationSupportDir/db");
final box = await Hive.openBox("app_conf");
if (box.get("install_id", defaultValue: "") == "") {
await box.put("install_id", const Uuid().v4());
AnalyticsApi.touch("firstLaunch");
}
final deviceUUID = box.get("install_id", defaultValue: "");
state = state.copyWith(deviceUUID: deviceUUID);
} catch (e) {
exit(1);
}
// init Rust bridge
await RustLib.init();
await RSHttp.init();
dPrint("---- rust bridge inited -----");
// init powershell
await SystemHelper.initPowershellPath();
// get windows info
WindowsDeviceInfo? windowsDeviceInfo;
try {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
windowsDeviceInfo = await deviceInfo.windowsInfo;
} catch (e) {
dPrint("DeviceInfo.windowsInfo error: $e");
}
// init windows
windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setSize(const Size(1280, 810));
await windowManager.setMinimumSize(const Size(1280, 810));
await windowManager.setSkipTaskbar(false);
await windowManager.show();
await Window.initialize();
await Window.hideWindowControls();
if (windowsDeviceInfo?.productName.contains("Windows 11") ?? false) {
await Window.setEffect(
effect: WindowEffect.acrylic,
);
}
});
_initialized = true;
ref.keepAlive();
}
String getUpgradePath() {
return "${state.applicationSupportDir}/._upgrade";
}
// ignore: avoid_build_context_in_providers
Future<bool> checkUpdate(BuildContext context) async {
if (!ConstConf.isMSE) {
final dir = Directory(getUpgradePath());
if (await dir.exists()) {
dir.delete(recursive: true);
}
}
dynamic checkUpdateError;
try {
final networkVersionData = await Api.getAppVersion();
checkActivityThemeColor(networkVersionData);
if (ConstConf.isMSE) {
dPrint(
"lastVersion=${networkVersionData.mSELastVersion} ${networkVersionData.mSELastVersionCode}");
} else {
dPrint(
"lastVersion=${networkVersionData.lastVersion} ${networkVersionData.lastVersionCode}");
}
state = state.copyWith(networkVersionData: networkVersionData);
} catch (e) {
checkUpdateError = e;
dPrint("_checkUpdate Error:$e");
}
await Future.delayed(const Duration(milliseconds: 100));
if (state.networkVersionData == null) {
if (!context.mounted) return false;
await showToast(context,
"网络异常!\n这可能是您的网络环境存在DNS污染请尝试更换DNS。\n或服务器正在维护或遭受攻击,稍后再试。 \n进入离线模式... \n\n请谨慎在离线模式中使用。 \n当前版本构建日期:${ConstConf.appVersionDate}\n QQ群940696487 \n错误信息:$checkUpdateError");
return false;
}
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());
if (r != true) {
if (!context.mounted) return false;
await showToast(context, "获取更新信息失败,请稍后重试。");
return false;
}
return true;
}
return false;
}
Timer? _activityThemeColorTimer;
checkActivityThemeColor(AppVersionData networkVersionData) {
if (_activityThemeColorTimer != null) {
_activityThemeColorTimer?.cancel();
_activityThemeColorTimer = null;
}
final startTime = networkVersionData.activityColors?.startTime;
final endTime = networkVersionData.activityColors?.endTime;
if (startTime == null || endTime == null) return;
final now = DateTime.now().millisecondsSinceEpoch;
dPrint("now == $now start == $startTime end == $endTime");
if (now < startTime) {
_activityThemeColorTimer = Timer(Duration(milliseconds: startTime - now),
() => checkActivityThemeColor(networkVersionData));
dPrint("start Timer ....");
} else if (now >= startTime && now <= endTime) {
dPrint("update Color ....");
// update Color
final colorCfg = networkVersionData.activityColors;
state = state.copyWith(
themeConf: ThemeConf(
backgroundColor:
HexColor(colorCfg?.background ?? "#132431").withOpacity(.75),
menuColor: HexColor(colorCfg?.menu ?? "#132431").withOpacity(.95),
micaColor: HexColor(colorCfg?.mica ?? "#0A3142"),
),
);
// wait for end
_activityThemeColorTimer = Timer(Duration(milliseconds: endTime - now),
() => checkActivityThemeColor(networkVersionData));
} else {
dPrint("reset Color ....");
state = state.copyWith(
themeConf: ThemeConf(
backgroundColor: HexColor("#132431").withOpacity(.75),
menuColor: HexColor("#132431").withOpacity(.95),
micaColor: HexColor("#0A3142"),
),
);
}
}
}
@freezed
class AppGlobalState with _$AppGlobalState {
const factory AppGlobalState({
String? deviceUUID,
String? applicationSupportDir,
String? applicationBinaryModuleDir,
AppVersionData? networkVersionData,
@Default(ThemeConf()) ThemeConf themeConf,
}) = _AppGlobalState;
}
@freezed
class ThemeConf with _$ThemeConf {
const factory ThemeConf({
@Default(Color(0xbf132431)) Color backgroundColor,
@Default(Color(0xf2132431)) Color menuColor,
@Default(Color(0xff0a3142)) Color micaColor,
}) = _ThemeConf;
}

405
lib/app.freezed.dart Normal file
View File

@ -0,0 +1,405 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'app.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$AppGlobalState {
String? get deviceUUID => throw _privateConstructorUsedError;
String? get applicationSupportDir => throw _privateConstructorUsedError;
String? get applicationBinaryModuleDir => throw _privateConstructorUsedError;
AppVersionData? get networkVersionData => throw _privateConstructorUsedError;
ThemeConf get themeConf => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AppGlobalStateCopyWith<AppGlobalState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AppGlobalStateCopyWith<$Res> {
factory $AppGlobalStateCopyWith(
AppGlobalState value, $Res Function(AppGlobalState) then) =
_$AppGlobalStateCopyWithImpl<$Res, AppGlobalState>;
@useResult
$Res call(
{String? deviceUUID,
String? applicationSupportDir,
String? applicationBinaryModuleDir,
AppVersionData? networkVersionData,
ThemeConf themeConf});
$ThemeConfCopyWith<$Res> get themeConf;
}
/// @nodoc
class _$AppGlobalStateCopyWithImpl<$Res, $Val extends AppGlobalState>
implements $AppGlobalStateCopyWith<$Res> {
_$AppGlobalStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? deviceUUID = freezed,
Object? applicationSupportDir = freezed,
Object? applicationBinaryModuleDir = freezed,
Object? networkVersionData = freezed,
Object? themeConf = null,
}) {
return _then(_value.copyWith(
deviceUUID: freezed == deviceUUID
? _value.deviceUUID
: deviceUUID // ignore: cast_nullable_to_non_nullable
as String?,
applicationSupportDir: freezed == applicationSupportDir
? _value.applicationSupportDir
: applicationSupportDir // ignore: cast_nullable_to_non_nullable
as String?,
applicationBinaryModuleDir: freezed == applicationBinaryModuleDir
? _value.applicationBinaryModuleDir
: applicationBinaryModuleDir // ignore: cast_nullable_to_non_nullable
as String?,
networkVersionData: freezed == networkVersionData
? _value.networkVersionData
: networkVersionData // ignore: cast_nullable_to_non_nullable
as AppVersionData?,
themeConf: null == themeConf
? _value.themeConf
: themeConf // ignore: cast_nullable_to_non_nullable
as ThemeConf,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$ThemeConfCopyWith<$Res> get themeConf {
return $ThemeConfCopyWith<$Res>(_value.themeConf, (value) {
return _then(_value.copyWith(themeConf: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$AppGlobalStateImplCopyWith<$Res>
implements $AppGlobalStateCopyWith<$Res> {
factory _$$AppGlobalStateImplCopyWith(_$AppGlobalStateImpl value,
$Res Function(_$AppGlobalStateImpl) then) =
__$$AppGlobalStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String? deviceUUID,
String? applicationSupportDir,
String? applicationBinaryModuleDir,
AppVersionData? networkVersionData,
ThemeConf themeConf});
@override
$ThemeConfCopyWith<$Res> get themeConf;
}
/// @nodoc
class __$$AppGlobalStateImplCopyWithImpl<$Res>
extends _$AppGlobalStateCopyWithImpl<$Res, _$AppGlobalStateImpl>
implements _$$AppGlobalStateImplCopyWith<$Res> {
__$$AppGlobalStateImplCopyWithImpl(
_$AppGlobalStateImpl _value, $Res Function(_$AppGlobalStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? deviceUUID = freezed,
Object? applicationSupportDir = freezed,
Object? applicationBinaryModuleDir = freezed,
Object? networkVersionData = freezed,
Object? themeConf = null,
}) {
return _then(_$AppGlobalStateImpl(
deviceUUID: freezed == deviceUUID
? _value.deviceUUID
: deviceUUID // ignore: cast_nullable_to_non_nullable
as String?,
applicationSupportDir: freezed == applicationSupportDir
? _value.applicationSupportDir
: applicationSupportDir // ignore: cast_nullable_to_non_nullable
as String?,
applicationBinaryModuleDir: freezed == applicationBinaryModuleDir
? _value.applicationBinaryModuleDir
: applicationBinaryModuleDir // ignore: cast_nullable_to_non_nullable
as String?,
networkVersionData: freezed == networkVersionData
? _value.networkVersionData
: networkVersionData // ignore: cast_nullable_to_non_nullable
as AppVersionData?,
themeConf: null == themeConf
? _value.themeConf
: themeConf // ignore: cast_nullable_to_non_nullable
as ThemeConf,
));
}
}
/// @nodoc
class _$AppGlobalStateImpl implements _AppGlobalState {
const _$AppGlobalStateImpl(
{this.deviceUUID,
this.applicationSupportDir,
this.applicationBinaryModuleDir,
this.networkVersionData,
this.themeConf = const ThemeConf()});
@override
final String? deviceUUID;
@override
final String? applicationSupportDir;
@override
final String? applicationBinaryModuleDir;
@override
final AppVersionData? networkVersionData;
@override
@JsonKey()
final ThemeConf themeConf;
@override
String toString() {
return 'AppGlobalState(deviceUUID: $deviceUUID, applicationSupportDir: $applicationSupportDir, applicationBinaryModuleDir: $applicationBinaryModuleDir, networkVersionData: $networkVersionData, themeConf: $themeConf)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AppGlobalStateImpl &&
(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));
}
@override
int get hashCode => Object.hash(
runtimeType,
deviceUUID,
applicationSupportDir,
applicationBinaryModuleDir,
networkVersionData,
themeConf);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AppGlobalStateImplCopyWith<_$AppGlobalStateImpl> get copyWith =>
__$$AppGlobalStateImplCopyWithImpl<_$AppGlobalStateImpl>(
this, _$identity);
}
abstract class _AppGlobalState implements AppGlobalState {
const factory _AppGlobalState(
{final String? deviceUUID,
final String? applicationSupportDir,
final String? applicationBinaryModuleDir,
final AppVersionData? networkVersionData,
final ThemeConf themeConf}) = _$AppGlobalStateImpl;
@override
String? get deviceUUID;
@override
String? get applicationSupportDir;
@override
String? get applicationBinaryModuleDir;
@override
AppVersionData? get networkVersionData;
@override
ThemeConf get themeConf;
@override
@JsonKey(ignore: true)
_$$AppGlobalStateImplCopyWith<_$AppGlobalStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ThemeConf {
Color get backgroundColor => throw _privateConstructorUsedError;
Color get menuColor => throw _privateConstructorUsedError;
Color get micaColor => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ThemeConfCopyWith<ThemeConf> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ThemeConfCopyWith<$Res> {
factory $ThemeConfCopyWith(ThemeConf value, $Res Function(ThemeConf) then) =
_$ThemeConfCopyWithImpl<$Res, ThemeConf>;
@useResult
$Res call({Color backgroundColor, Color menuColor, Color micaColor});
}
/// @nodoc
class _$ThemeConfCopyWithImpl<$Res, $Val extends ThemeConf>
implements $ThemeConfCopyWith<$Res> {
_$ThemeConfCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? backgroundColor = null,
Object? menuColor = null,
Object? micaColor = null,
}) {
return _then(_value.copyWith(
backgroundColor: null == backgroundColor
? _value.backgroundColor
: backgroundColor // ignore: cast_nullable_to_non_nullable
as Color,
menuColor: null == menuColor
? _value.menuColor
: menuColor // ignore: cast_nullable_to_non_nullable
as Color,
micaColor: null == micaColor
? _value.micaColor
: micaColor // ignore: cast_nullable_to_non_nullable
as Color,
) as $Val);
}
}
/// @nodoc
abstract class _$$ThemeConfImplCopyWith<$Res>
implements $ThemeConfCopyWith<$Res> {
factory _$$ThemeConfImplCopyWith(
_$ThemeConfImpl value, $Res Function(_$ThemeConfImpl) then) =
__$$ThemeConfImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({Color backgroundColor, Color menuColor, Color micaColor});
}
/// @nodoc
class __$$ThemeConfImplCopyWithImpl<$Res>
extends _$ThemeConfCopyWithImpl<$Res, _$ThemeConfImpl>
implements _$$ThemeConfImplCopyWith<$Res> {
__$$ThemeConfImplCopyWithImpl(
_$ThemeConfImpl _value, $Res Function(_$ThemeConfImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? backgroundColor = null,
Object? menuColor = null,
Object? micaColor = null,
}) {
return _then(_$ThemeConfImpl(
backgroundColor: null == backgroundColor
? _value.backgroundColor
: backgroundColor // ignore: cast_nullable_to_non_nullable
as Color,
menuColor: null == menuColor
? _value.menuColor
: menuColor // ignore: cast_nullable_to_non_nullable
as Color,
micaColor: null == micaColor
? _value.micaColor
: micaColor // ignore: cast_nullable_to_non_nullable
as Color,
));
}
}
/// @nodoc
class _$ThemeConfImpl implements _ThemeConf {
const _$ThemeConfImpl(
{this.backgroundColor = const Color(0xbf132431),
this.menuColor = const Color(0xf2132431),
this.micaColor = const Color(0xff0a3142)});
@override
@JsonKey()
final Color backgroundColor;
@override
@JsonKey()
final Color menuColor;
@override
@JsonKey()
final Color micaColor;
@override
String toString() {
return 'ThemeConf(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ThemeConfImpl &&
(identical(other.backgroundColor, backgroundColor) ||
other.backgroundColor == backgroundColor) &&
(identical(other.menuColor, menuColor) ||
other.menuColor == menuColor) &&
(identical(other.micaColor, micaColor) ||
other.micaColor == micaColor));
}
@override
int get hashCode =>
Object.hash(runtimeType, backgroundColor, menuColor, micaColor);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ThemeConfImplCopyWith<_$ThemeConfImpl> get copyWith =>
__$$ThemeConfImplCopyWithImpl<_$ThemeConfImpl>(this, _$identity);
}
abstract class _ThemeConf implements ThemeConf {
const factory _ThemeConf(
{final Color backgroundColor,
final Color menuColor,
final Color micaColor}) = _$ThemeConfImpl;
@override
Color get backgroundColor;
@override
Color get menuColor;
@override
Color get micaColor;
@override
@JsonKey(ignore: true)
_$$ThemeConfImplCopyWith<_$ThemeConfImpl> get copyWith =>
throw _privateConstructorUsedError;
}

40
lib/app.g.dart Normal file
View File

@ -0,0 +1,40 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$routerHash() => r'e7b1e3a9fd74b4f00e3d71017615d7fb82bd649d';
/// See also [router].
@ProviderFor(router)
final routerProvider = AutoDisposeProvider<GoRouter>.internal(
router,
name: r'routerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$routerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef RouterRef = AutoDisposeProviderRef<GoRouter>;
String _$appGlobalModelHash() => r'9c114910aed546bfd469c8bbfa50cdd4a5be5028';
/// See also [AppGlobalModel].
@ProviderFor(AppGlobalModel)
final appGlobalModelProvider =
AutoDisposeNotifierProvider<AppGlobalModel, AppGlobalState>.internal(
AppGlobalModel.new,
name: r'appGlobalModelProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appGlobalModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppGlobalModel = AutoDisposeNotifier<AppGlobalState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -1,187 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:starcitizen_doctor/main.dart';
import 'package:starcitizen_doctor/widgets/my_page_route.dart';
import 'package:window_manager/window_manager.dart';
import '../common/utils/log.dart' as log_utils;
import 'dart:ui' as ui;
import 'ui_model.dart';
export '../common/utils/base_utils.dart';
export '../widgets/widgets.dart';
export 'package:fluent_ui/fluent_ui.dart';
class BaseUIContainer extends ConsumerStatefulWidget {
final ConsumerState<BaseUIContainer> Function() uiCreate;
final dynamic Function() modelCreate;
const BaseUIContainer(
{super.key, required this.uiCreate, required this.modelCreate});
@override
// ignore: no_logic_in_create_state
ConsumerState<BaseUIContainer> createState() => uiCreate();
Future push(BuildContext context) {
return Navigator.push(context, makeRoute(context));
}
// Future pushShowModalBottomSheet(BuildContext context) {
// return showModalBottomSheet(
// context: context,
// isScrollControlled: true,
// builder: (BuildContext context) {
// return this;
// },
// );
// }
///
FluentPageRoute makeRoute(BuildContext context) {
return MyPageRoute(
builder: (BuildContext context) {
return this;
},
);
}
// Future pushAndRemoveUntil(BuildContext context) {
// return Navigator.pushAndRemoveUntil(context,
// MaterialPageRoute(builder: (BuildContext context) {
// return this;
// }), (_) => false);
// }
}
abstract class BaseUI<T extends BaseUIModel>
extends ConsumerState<BaseUIContainer> {
BaseUIModel? _needDisposeModel;
late final ChangeNotifierProvider<T> provider = bindUIModel();
// final GlobalKey<ScaffoldState> scaffoldState = GlobalKey();
// RefreshController? refreshController;
@override
Widget build(BuildContext context) {
// get model
final model = ref.watch(provider);
return buildBody(context, model)!;
}
String getUITitle(BuildContext context, T model);
Widget? buildBody(
BuildContext context,
T model,
);
Widget? getBottomNavigationBar(BuildContext context, T model) => null;
Color? getBackgroundColor(BuildContext context, T model) => null;
Widget? getFloatingActionButton(BuildContext context, T model) => null;
bool getDrawerEnableOpenDragGesture(BuildContext context, T model) => true;
Widget? getDrawer(BuildContext context, T model) => null;
Widget makeDefaultPage(BuildContext context, T model,
{Widget? titleRow,
List<Widget>? actions,
Widget? content,
bool automaticallyImplyLeading = true}) {
return NavigationView(
pane: NavigationPane(
size: const NavigationPaneSize(openWidth: 0),
),
appBar: NavigationAppBar(
automaticallyImplyLeading: automaticallyImplyLeading,
title: DragToMoveArea(
child: titleRow ??
Column(children: [Expanded(
child: Row(
children: [
Text(getUITitle(context, model)),
],
),
)],),
),
actions: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [...?actions, const WindowButtons()],
)),
paneBodyBuilder: (
PaneItem? item,
Widget? body,
) {
return SizedBox(
height: MediaQuery.of(context).size.height,
child: content ?? makeLoading(context),
);
},
);
}
@mustCallSuper
@override
void initState() {
dPrint("[base] <$runtimeType> UI Init");
super.initState();
}
@mustCallSuper
@override
void dispose() {
dPrint("[base] <$runtimeType> UI Disposed");
_needDisposeModel?.dispose();
_needDisposeModel = null;
super.dispose();
}
///
dismissKeyBoard() {
FocusManager.instance.primaryFocus?.unfocus();
}
// void updateStatusBarIconColor(BuildContext context) {
// SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
// statusBarBrightness: Theme.of(context).brightness,
// statusBarIconBrightness: getAndroidIconBrightness(context),
// ));
// }
ChangeNotifierProvider<T> bindUIModel() {
final createdModel = widget.modelCreate();
if (createdModel is T) {
_needDisposeModel = createdModel;
return ChangeNotifierProvider<T>((ref) {
return createdModel..context = context;
});
}
return createdModel;
}
// Widget pullToRefreshBody(
// {required BaseUIModel model, required Widget child}) {
// refreshController ??= RefreshController();
// return AppSmartRefresher(
// enablePullUp: false,
// controller: refreshController,
// onRefresh: () async {
// await model.reloadData();
// refreshController?.refreshCompleted();
// },
// child: child,
// );
// }
makeSvgColor(Color color, {BlendMode blendMode = BlendMode.color}) {
return ui.ColorFilter.mode(color, blendMode);
}
dPrint(src) {
log_utils.dPrint("<$runtimeType> $src");
}
}

View File

@ -1,141 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:grpc/grpc.dart';
import 'ui.dart';
import '../common/utils/log.dart' as log_utils;
export '../common/utils/base_utils.dart';
export 'ui.dart';
class BaseUIModel extends ChangeNotifier {
String uiErrorMsg = "";
bool _isDisposed = false;
bool get mounted => !_isDisposed;
BuildContext? context;
BaseUIModel() {
initModel();
}
@mustCallSuper
void initModel() {
dPrint("[base] <$runtimeType> Model Init");
loadData();
}
@mustCallSuper
@override
void dispose() {
_isDisposed = true;
_childUIModels?.forEach((k, value) {
(value as BaseUIModel).dispose();
_childUIModels?[k] = null;
});
dPrint("[base] <$runtimeType> Model Disposed");
super.dispose();
}
Future loadData() async {}
Future reloadData() async {
return loadData();
}
Future onErrorReloadData() async {
return loadData();
}
@override
void notifyListeners() {
if (!mounted) return;
super.notifyListeners();
}
Future<T?> handleError<T>(Future<T> Function() requestFunc,
{bool showFullScreenError = false,
String? errorOverride,
bool noAlert = false}) async {
uiErrorMsg = "";
if (mounted) notifyListeners();
try {
return await requestFunc();
} catch (e) {
dPrint("$runtimeType.handleError Error:$e");
String errorMsg = "Unknown Error";
if (e is GrpcError) {
errorMsg = "远程服务器消息: ${e.message ?? "Unknown Error"}";
} else {
errorMsg = e.toString();
}
if (showFullScreenError) {
uiErrorMsg = errorMsg;
notifyListeners();
return null;
}
if (!noAlert) {
showToast(context!, errorOverride ?? errorMsg,
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .6,
),
title: "出现错误!");
}
}
return null;
}
Map<dynamic, dynamic>? _childUIModels;
Map<dynamic, dynamic>? _childUIProviders;
BaseUIModel? onCreateChildUIModel(modelKey) => null;
dynamic _getChildUIModel(modelKey) {
_childUIModels ??= {};
final cachedModel = _childUIModels![modelKey];
if (cachedModel != null) {
return (cachedModel);
}
final newModel = onCreateChildUIModel(modelKey);
_childUIModels![modelKey] = newModel!;
return newModel;
}
ChangeNotifierProvider<M> getChildUIModelProviders<M extends BaseUIModel>(
modelKey) {
_childUIProviders ??= {};
if (_childUIProviders![modelKey] == null) {
_childUIProviders![modelKey] = ChangeNotifierProvider<M>((ref) {
final c = (_getChildUIModel(modelKey) as M);
return c..context = context;
});
}
return _childUIProviders![modelKey]!;
}
T? getCreatedChildUIModel<T extends BaseUIModel>(String modelKey,
{bool create = false}) {
if (create && _childUIModels?[modelKey] == null) {
_getChildUIModel(modelKey);
}
return _childUIModels?[modelKey] as T?;
}
Future<void> reloadAllChildModels() async {
if (_childUIModels == null) return;
final futureList = <Future>[];
for (var value in _childUIModels!.entries) {
futureList.add(value.value.reloadData());
}
await Future.wait(futureList);
notifyListeners();
}
dismissKeyBoard() {
FocusManager.instance.primaryFocus?.unfocus();
}
dPrint(src) {
log_utils.dPrint("<$runtimeType> $src");
}
}

View File

@ -1,145 +0,0 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/common/rust/frb_generated.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/data/app_version_data.dart';
import 'package:starcitizen_doctor/global_ui_model.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:uuid/uuid.dart';
import 'package:window_manager/window_manager.dart';
class AppConf {
static const String appVersion = "2.10.7 Beta";
static const int appVersionCode = 42;
static const String appVersionDate = "2024-03-01";
static const gameChannels = ["LIVE", "PTU", "EPTU"];
static String deviceUUID = "";
static late final String applicationSupportDir;
static late final String applicationBinaryModuleDir;
static File? appLogFile;
static AppVersionData? networkVersionData;
static bool offlineMode = false;
static late final WindowsDeviceInfo windowsDeviceInfo;
static Color? colorBackground;
static Color? colorMenu;
static Color? colorMica;
static const isMSE =
String.fromEnvironment("MSE", defaultValue: "false") == "true";
static init(List<String> args) async {
dPrint("launch args == $args");
WidgetsFlutterBinding.ensureInitialized();
/// init device info
try {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
windowsDeviceInfo = await deviceInfo.windowsInfo;
} catch (_) {}
/// init Data
final userProfileDir = Platform.environment["USERPROFILE"];
applicationSupportDir =
(await getApplicationSupportDirectory()).absolute.path;
final logFile = File(
"$applicationSupportDir\\logs\\${DateTime.now().millisecondsSinceEpoch}.log");
await logFile.create(recursive: true);
appLogFile = logFile;
if (AppConf.isMSE && userProfileDir != null) {
applicationBinaryModuleDir =
"$userProfileDir\\AppData\\Local\\Temp\\SCToolbox\\modules";
} else {
applicationBinaryModuleDir = "$applicationSupportDir\\modules";
}
dPrint("applicationSupportDir == $applicationSupportDir");
dPrint("applicationBinaryModuleDir == $applicationBinaryModuleDir");
try {
Hive.init("$applicationSupportDir/db");
final box = await Hive.openBox("app_conf");
if (box.get("install_id", defaultValue: "") == "") {
await box.put("install_id", const Uuid().v4());
AnalyticsApi.touch("firstLaunch");
}
deviceUUID = box.get("install_id", defaultValue: "");
} catch (e) {
exit(1);
}
/// check Rust bridge
await RustLib.init();
await RSHttp.init();
dPrint("---- rust bridge inited -----");
await SystemHelper.initPowershellPath();
/// init defaultColor
colorBackground = HexColor("#132431").withOpacity(.75);
colorMenu = HexColor("#132431").withOpacity(.95);
colorMica = HexColor("#0A3142");
/// init windows
await windowManager.ensureInitialized();
windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setSize(const Size(1280, 810));
await windowManager.setMinimumSize(const Size(1280, 810));
await windowManager.center(animate: true);
await windowManager.setSkipTaskbar(false);
await windowManager.setTitleBarStyle(
TitleBarStyle.hidden,
windowButtonVisibility: false,
);
await windowManager.show();
await Window.initialize();
await Window.hideWindowControls();
if (windowsDeviceInfo.productName.contains("Windows 11")) {
await Window.setEffect(
effect: WindowEffect.acrylic,
);
}
});
}
static String getUpgradePath() {
return "${AppConf.applicationSupportDir}/._upgrade";
}
static Future<void> checkUpdate() async {
// clean path
if (!isMSE) {
final dir = Directory(getUpgradePath());
if (await dir.exists()) {
dir.delete(recursive: true);
}
}
try {
networkVersionData = await Api.getAppVersion();
globalUIModel.checkActivityThemeColor();
if (isMSE) {
dPrint(
"lastVersion=${networkVersionData?.mSELastVersion} ${networkVersionData?.mSELastVersionCode}");
} else {
dPrint(
"lastVersion=${networkVersionData?.lastVersion} ${networkVersionData?.lastVersionCode}");
}
} catch (e) {
dPrint("_checkUpdate Error:$e");
}
}
}

View File

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter/services.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
class BinaryModuleConf {
@ -10,8 +9,7 @@ class BinaryModuleConf {
"aria2c": "0",
};
static Future extractModule(List<String> modules) async {
final workingDir = AppConf.applicationBinaryModuleDir;
static Future extractModule(List<String> modules, String workingDir) async {
for (var m in _modules.entries) {
if (!modules.contains(m.key)) continue;
final name = m.key;

View File

@ -0,0 +1,8 @@
class ConstConf {
static const String appVersion = "2.10.7 Beta";
static const int appVersionCode = 42;
static const String appVersionDate = "2024-03-01";
static const gameChannels = ["LIVE", "PTU", "EPTU"];
static const isMSE =
String.fromEnvironment("MSE", defaultValue: "false") == "true";
}

View File

@ -1,48 +0,0 @@
import 'package:fixnum/fixnum.dart';
import 'package:grpc/grpc.dart';
import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pbgrpc.dart';
class PartyRoomGrpcServer {
static const clientVersion = 0;
static final _channel = ClientChannel(
"127.0.0.1",
port: 39399,
options: ChannelOptions(
credentials: const ChannelCredentials.insecure(),
codecRegistry:
CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]),
),
);
static final _indexService = IndexServiceClient(_channel);
static Future<PingData> pingServer() async {
final r = await _indexService.pingServer(PingData(
data: "PING", clientVersion: Int64.parseInt(clientVersion.toString())));
return r;
}
static Future<RoomTypesData> getRoomTypes() async {
final r = await _indexService.getRoomTypes(Empty());
return r;
}
static Future<RoomListData> getRoomList(RoomListPageReqData req) async {
return await _indexService.getRoomList(req);
}
static Future<RoomData> createRoom(RoomData roomData) async {
return await _indexService.createRoom(roomData);
}
static Future<RoomData?> touchUserRoom(String userName, String deviceUUID) {
return _indexService
.touchUser(PreUser(userName: userName, deviceUUID: deviceUUID));
}
static ResponseStream<RoomUpdateMessage> joinRoom(
String roomID, String userName, String deviceUUID) {
return _indexService.joinRoom(
PreUser(roomID: roomID, userName: userName, deviceUUID: deviceUUID));
}
}

View File

@ -1,7 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/rust/api/http_api.dart' as rust_http;
import 'package:starcitizen_doctor/common/rust/api/http_api.dart';
import 'package:starcitizen_doctor/common/rust/http_package.dart';
@ -10,7 +10,7 @@ class RSHttp {
static init() async {
await rust_http.setDefaultHeader(headers: {
"User-Agent":
"SCToolBox/${AppConf.appVersion} (${AppConf.appVersionCode})${AppConf.isMSE ? "" : " DEV"} RSHttp"
"SCToolBox/${ConstConf.appVersion} (${ConstConf.appVersionCode})${ConstConf.isMSE ? "" : " DEV"} RSHttp"
});
}

View File

@ -0,0 +1,17 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
extension AsyncError on Future {
Future<T?> unwrap<T>({BuildContext? context}) async {
try {
return await this;
} catch (e) {
dPrint("unwrap error:$e");
if (context != null) {
showToast(context, "出现错误: $e");
}
return null;
}
}
}

View File

@ -3,9 +3,8 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:synchronized/synchronized.dart';
import '../conf/app_conf.dart';
var _logLock = Lock();
File? _logFile;
void dPrint(src) async {
if (kDebugMode) {
@ -13,7 +12,15 @@ void dPrint(src) async {
}
try {
await _logLock.synchronized(() async {
await AppConf.appLogFile?.writeAsString("$src\n", mode: FileMode.append);
_logFile?.writeAsString("$src\n", mode: FileMode.append);
});
} catch (_) {}
}
void setDPrintFile(File file) {
_logFile = file;
}
File? getDPrintFile() {
return _logFile;
}

View File

@ -0,0 +1,9 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:starcitizen_doctor/app.dart';
extension ProviderExtension on AutoDisposeNotifier {
AppGlobalModel get appGlobalModel =>
ref.read(appGlobalModelProvider.notifier);
AppGlobalState get appGlobalState => ref.read(appGlobalModelProvider);
}

View File

@ -1,159 +0,0 @@
//
// Generated code. Do not modify.
// source: chat.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
import 'chat.pbenum.dart';
export 'chat.pbenum.dart';
class ChatMessage extends $pb.GeneratedMessage {
factory ChatMessage({
$core.String? senderID,
$core.String? receiverID,
ReceiverType? receiverType,
MessageType? messageType,
$core.String? data,
}) {
final $result = create();
if (senderID != null) {
$result.senderID = senderID;
}
if (receiverID != null) {
$result.receiverID = receiverID;
}
if (receiverType != null) {
$result.receiverType = receiverType;
}
if (messageType != null) {
$result.messageType = messageType;
}
if (data != null) {
$result.data = data;
}
return $result;
}
ChatMessage._() : super();
factory ChatMessage.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r);
factory ChatMessage.fromJson($core.String i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ChatMessage',
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'senderID', protoName: 'senderID')
..aOS(2, _omitFieldNames ? '' : 'receiverID', protoName: 'receiverID')
..e<ReceiverType>(
3, _omitFieldNames ? '' : 'receiverType', $pb.PbFieldType.OE,
protoName: 'receiverType',
defaultOrMaker: ReceiverType.RoomMsg,
valueOf: ReceiverType.valueOf,
enumValues: ReceiverType.values)
..e<MessageType>(
4, _omitFieldNames ? '' : 'messageType', $pb.PbFieldType.OE,
protoName: 'messageType',
defaultOrMaker: MessageType.System,
valueOf: MessageType.valueOf,
enumValues: MessageType.values)
..aOS(5, _omitFieldNames ? '' : 'data')
..hasRequiredFields = false;
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
ChatMessage clone() => ChatMessage()..mergeFromMessage(this);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
ChatMessage copyWith(void Function(ChatMessage) updates) =>
super.copyWith((message) => updates(message as ChatMessage))
as ChatMessage;
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ChatMessage create() => ChatMessage._();
ChatMessage createEmptyInstance() => create();
static $pb.PbList<ChatMessage> createRepeated() => $pb.PbList<ChatMessage>();
@$core.pragma('dart2js:noInline')
static ChatMessage getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ChatMessage>(create);
static ChatMessage? _defaultInstance;
@$pb.TagNumber(1)
$core.String get senderID => $_getSZ(0);
@$pb.TagNumber(1)
set senderID($core.String v) {
$_setString(0, v);
}
@$pb.TagNumber(1)
$core.bool hasSenderID() => $_has(0);
@$pb.TagNumber(1)
void clearSenderID() => clearField(1);
@$pb.TagNumber(2)
$core.String get receiverID => $_getSZ(1);
@$pb.TagNumber(2)
set receiverID($core.String v) {
$_setString(1, v);
}
@$pb.TagNumber(2)
$core.bool hasReceiverID() => $_has(1);
@$pb.TagNumber(2)
void clearReceiverID() => clearField(2);
@$pb.TagNumber(3)
ReceiverType get receiverType => $_getN(2);
@$pb.TagNumber(3)
set receiverType(ReceiverType v) {
setField(3, v);
}
@$pb.TagNumber(3)
$core.bool hasReceiverType() => $_has(2);
@$pb.TagNumber(3)
void clearReceiverType() => clearField(3);
@$pb.TagNumber(4)
MessageType get messageType => $_getN(3);
@$pb.TagNumber(4)
set messageType(MessageType v) {
setField(4, v);
}
@$pb.TagNumber(4)
$core.bool hasMessageType() => $_has(3);
@$pb.TagNumber(4)
void clearMessageType() => clearField(4);
@$pb.TagNumber(5)
$core.String get data => $_getSZ(4);
@$pb.TagNumber(5)
set data($core.String v) {
$_setString(4, v);
}
@$pb.TagNumber(5)
$core.bool hasData() => $_has(4);
@$pb.TagNumber(5)
void clearData() => clearField(5);
}
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
const _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');

View File

@ -1,58 +0,0 @@
//
// Generated code. Do not modify.
// source: chat.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class ReceiverType extends $pb.ProtobufEnum {
static const ReceiverType RoomMsg =
ReceiverType._(0, _omitEnumNames ? '' : 'RoomMsg');
static const ReceiverType PrivateMsg =
ReceiverType._(1, _omitEnumNames ? '' : 'PrivateMsg');
static const $core.List<ReceiverType> values = <ReceiverType>[
RoomMsg,
PrivateMsg,
];
static final $core.Map<$core.int, ReceiverType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static ReceiverType? valueOf($core.int value) => _byValue[value];
const ReceiverType._($core.int v, $core.String n) : super(v, n);
}
class MessageType extends $pb.ProtobufEnum {
static const MessageType System =
MessageType._(0, _omitEnumNames ? '' : 'System');
static const MessageType Text =
MessageType._(1, _omitEnumNames ? '' : 'Text');
static const MessageType Image =
MessageType._(2, _omitEnumNames ? '' : 'Image');
static const MessageType Markdown =
MessageType._(3, _omitEnumNames ? '' : 'Markdown');
static const $core.List<MessageType> values = <MessageType>[
System,
Text,
Image,
Markdown,
];
static final $core.Map<$core.int, MessageType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static MessageType? valueOf($core.int value) => _byValue[value];
const MessageType._($core.int v, $core.String n) : super(v, n);
}
const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');

View File

@ -1,88 +0,0 @@
//
// Generated code. Do not modify.
// source: chat.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:async' as $async;
import 'dart:core' as $core;
import 'package:grpc/service_api.dart' as $grpc;
import 'package:protobuf/protobuf.dart' as $pb;
import 'chat.pb.dart' as $1;
import 'index.pb.dart' as $0;
export 'chat.pb.dart';
@$pb.GrpcServiceName('ChatService')
class ChatServiceClient extends $grpc.Client {
static final _$listenMessage = $grpc.ClientMethod<$0.PreUser, $1.ChatMessage>(
'/ChatService/ListenMessage',
($0.PreUser value) => value.writeToBuffer(),
($core.List<$core.int> value) => $1.ChatMessage.fromBuffer(value));
static final _$sendMessage =
$grpc.ClientMethod<$1.ChatMessage, $0.BaseRespData>(
'/ChatService/SendMessage',
($1.ChatMessage value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.BaseRespData.fromBuffer(value));
ChatServiceClient($grpc.ClientChannel channel,
{$grpc.CallOptions? options,
$core.Iterable<$grpc.ClientInterceptor>? interceptors})
: super(channel, options: options, interceptors: interceptors);
$grpc.ResponseStream<$1.ChatMessage> listenMessage($0.PreUser request,
{$grpc.CallOptions? options}) {
return $createStreamingCall(
_$listenMessage, $async.Stream.fromIterable([request]),
options: options);
}
$grpc.ResponseFuture<$0.BaseRespData> sendMessage($1.ChatMessage request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$sendMessage, request, options: options);
}
}
@$pb.GrpcServiceName('ChatService')
abstract class ChatServiceBase extends $grpc.Service {
$core.String get $name => 'ChatService';
ChatServiceBase() {
$addMethod($grpc.ServiceMethod<$0.PreUser, $1.ChatMessage>(
'ListenMessage',
listenMessage_Pre,
false,
true,
($core.List<$core.int> value) => $0.PreUser.fromBuffer(value),
($1.ChatMessage value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$1.ChatMessage, $0.BaseRespData>(
'SendMessage',
sendMessage_Pre,
false,
false,
($core.List<$core.int> value) => $1.ChatMessage.fromBuffer(value),
($0.BaseRespData value) => value.writeToBuffer()));
}
$async.Stream<$1.ChatMessage> listenMessage_Pre(
$grpc.ServiceCall call, $async.Future<$0.PreUser> request) async* {
yield* listenMessage(call, await request);
}
$async.Future<$0.BaseRespData> sendMessage_Pre(
$grpc.ServiceCall call, $async.Future<$1.ChatMessage> request) async {
return sendMessage(call, await request);
}
$async.Stream<$1.ChatMessage> listenMessage(
$grpc.ServiceCall call, $0.PreUser request);
$async.Future<$0.BaseRespData> sendMessage(
$grpc.ServiceCall call, $1.ChatMessage request);
}

View File

@ -1,76 +0,0 @@
//
// Generated code. Do not modify.
// source: chat.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use receiverTypeDescriptor instead')
const ReceiverType$json = {
'1': 'ReceiverType',
'2': [
{'1': 'RoomMsg', '2': 0},
{'1': 'PrivateMsg', '2': 1},
],
};
/// Descriptor for `ReceiverType`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List receiverTypeDescriptor = $convert.base64Decode(
'CgxSZWNlaXZlclR5cGUSCwoHUm9vbU1zZxAAEg4KClByaXZhdGVNc2cQAQ==');
@$core.Deprecated('Use messageTypeDescriptor instead')
const MessageType$json = {
'1': 'MessageType',
'2': [
{'1': 'System', '2': 0},
{'1': 'Text', '2': 1},
{'1': 'Image', '2': 2},
{'1': 'Markdown', '2': 3},
],
};
/// Descriptor for `MessageType`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List messageTypeDescriptor = $convert.base64Decode(
'CgtNZXNzYWdlVHlwZRIKCgZTeXN0ZW0QABIICgRUZXh0EAESCQoFSW1hZ2UQAhIMCghNYXJrZG'
'93bhAD');
@$core.Deprecated('Use chatMessageDescriptor instead')
const ChatMessage$json = {
'1': 'ChatMessage',
'2': [
{'1': 'senderID', '3': 1, '4': 1, '5': 9, '10': 'senderID'},
{'1': 'receiverID', '3': 2, '4': 1, '5': 9, '10': 'receiverID'},
{
'1': 'receiverType',
'3': 3,
'4': 1,
'5': 14,
'6': '.ReceiverType',
'10': 'receiverType'
},
{
'1': 'messageType',
'3': 4,
'4': 1,
'5': 14,
'6': '.MessageType',
'10': 'messageType'
},
{'1': 'data', '3': 5, '4': 1, '5': 9, '10': 'data'},
],
};
/// Descriptor for `ChatMessage`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List chatMessageDescriptor = $convert.base64Decode(
'CgtDaGF0TWVzc2FnZRIaCghzZW5kZXJJRBgBIAEoCVIIc2VuZGVySUQSHgoKcmVjZWl2ZXJJRB'
'gCIAEoCVIKcmVjZWl2ZXJJRBIxCgxyZWNlaXZlclR5cGUYAyABKA4yDS5SZWNlaXZlclR5cGVS'
'DHJlY2VpdmVyVHlwZRIuCgttZXNzYWdlVHlwZRgEIAEoDjIMLk1lc3NhZ2VUeXBlUgttZXNzYW'
'dlVHlwZRISCgRkYXRhGAUgASgJUgRkYXRh');

File diff suppressed because it is too large Load Diff

View File

@ -1,115 +0,0 @@
//
// Generated code. Do not modify.
// source: index.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class RoomStatus extends $pb.ProtobufEnum {
static const RoomStatus All = RoomStatus._(0, _omitEnumNames ? '' : 'All');
static const RoomStatus Open = RoomStatus._(1, _omitEnumNames ? '' : 'Open');
static const RoomStatus Private =
RoomStatus._(2, _omitEnumNames ? '' : 'Private');
static const RoomStatus Full = RoomStatus._(3, _omitEnumNames ? '' : 'Full');
static const RoomStatus Closed =
RoomStatus._(4, _omitEnumNames ? '' : 'Closed');
static const RoomStatus WillOffline =
RoomStatus._(5, _omitEnumNames ? '' : 'WillOffline');
static const RoomStatus Offline =
RoomStatus._(6, _omitEnumNames ? '' : 'Offline');
static const $core.List<RoomStatus> values = <RoomStatus>[
All,
Open,
Private,
Full,
Closed,
WillOffline,
Offline,
];
static final $core.Map<$core.int, RoomStatus> _byValue =
$pb.ProtobufEnum.initByValue(values);
static RoomStatus? valueOf($core.int value) => _byValue[value];
const RoomStatus._($core.int v, $core.String n) : super(v, n);
}
class RoomSortType extends $pb.ProtobufEnum {
static const RoomSortType Default =
RoomSortType._(0, _omitEnumNames ? '' : 'Default');
static const RoomSortType MostPlayerNumber =
RoomSortType._(1, _omitEnumNames ? '' : 'MostPlayerNumber');
static const RoomSortType MinimumPlayerNumber =
RoomSortType._(2, _omitEnumNames ? '' : 'MinimumPlayerNumber');
static const RoomSortType RecentlyCreated =
RoomSortType._(3, _omitEnumNames ? '' : 'RecentlyCreated');
static const RoomSortType OldestCreated =
RoomSortType._(4, _omitEnumNames ? '' : 'OldestCreated');
static const $core.List<RoomSortType> values = <RoomSortType>[
Default,
MostPlayerNumber,
MinimumPlayerNumber,
RecentlyCreated,
OldestCreated,
];
static final $core.Map<$core.int, RoomSortType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static RoomSortType? valueOf($core.int value) => _byValue[value];
const RoomSortType._($core.int v, $core.String n) : super(v, n);
}
class RoomUserStatus extends $pb.ProtobufEnum {
static const RoomUserStatus RoomUserStatusJoin =
RoomUserStatus._(0, _omitEnumNames ? '' : 'RoomUserStatusJoin');
static const RoomUserStatus RoomUserStatusLostOffline =
RoomUserStatus._(1, _omitEnumNames ? '' : 'RoomUserStatusLostOffline');
static const RoomUserStatus RoomUserStatusLeave =
RoomUserStatus._(2, _omitEnumNames ? '' : 'RoomUserStatusLeave');
static const RoomUserStatus RoomUserStatusWaitingConnect =
RoomUserStatus._(3, _omitEnumNames ? '' : 'RoomUserStatusWaitingConnect');
static const $core.List<RoomUserStatus> values = <RoomUserStatus>[
RoomUserStatusJoin,
RoomUserStatusLostOffline,
RoomUserStatusLeave,
RoomUserStatusWaitingConnect,
];
static final $core.Map<$core.int, RoomUserStatus> _byValue =
$pb.ProtobufEnum.initByValue(values);
static RoomUserStatus? valueOf($core.int value) => _byValue[value];
const RoomUserStatus._($core.int v, $core.String n) : super(v, n);
}
class RoomUpdateType extends $pb.ProtobufEnum {
static const RoomUpdateType RoomUpdateData =
RoomUpdateType._(0, _omitEnumNames ? '' : 'RoomUpdateData');
static const RoomUpdateType RoomClose =
RoomUpdateType._(1, _omitEnumNames ? '' : 'RoomClose');
static const $core.List<RoomUpdateType> values = <RoomUpdateType>[
RoomUpdateData,
RoomClose,
];
static final $core.Map<$core.int, RoomUpdateType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static RoomUpdateType? valueOf($core.int value) => _byValue[value];
const RoomUpdateType._($core.int v, $core.String n) : super(v, n);
}
const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');

View File

@ -1,206 +0,0 @@
//
// Generated code. Do not modify.
// source: index.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:async' as $async;
import 'dart:core' as $core;
import 'package:grpc/service_api.dart' as $grpc;
import 'package:protobuf/protobuf.dart' as $pb;
import 'index.pb.dart' as $0;
export 'index.pb.dart';
@$pb.GrpcServiceName('IndexService')
class IndexServiceClient extends $grpc.Client {
static final _$pingServer = $grpc.ClientMethod<$0.PingData, $0.PingData>(
'/IndexService/PingServer',
($0.PingData value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.PingData.fromBuffer(value));
static final _$getRoomTypes = $grpc.ClientMethod<$0.Empty, $0.RoomTypesData>(
'/IndexService/GetRoomTypes',
($0.Empty value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.RoomTypesData.fromBuffer(value));
static final _$createRoom = $grpc.ClientMethod<$0.RoomData, $0.RoomData>(
'/IndexService/CreateRoom',
($0.RoomData value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.RoomData.fromBuffer(value));
static final _$getRoomList =
$grpc.ClientMethod<$0.RoomListPageReqData, $0.RoomListData>(
'/IndexService/GetRoomList',
($0.RoomListPageReqData value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.RoomListData.fromBuffer(value));
static final _$touchUser = $grpc.ClientMethod<$0.PreUser, $0.RoomData>(
'/IndexService/TouchUser',
($0.PreUser value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.RoomData.fromBuffer(value));
static final _$joinRoom =
$grpc.ClientMethod<$0.PreUser, $0.RoomUpdateMessage>(
'/IndexService/JoinRoom',
($0.PreUser value) => value.writeToBuffer(),
($core.List<$core.int> value) =>
$0.RoomUpdateMessage.fromBuffer(value));
static final _$leaveRoom = $grpc.ClientMethod<$0.PreUser, $0.BaseRespData>(
'/IndexService/LeaveRoom',
($0.PreUser value) => value.writeToBuffer(),
($core.List<$core.int> value) => $0.BaseRespData.fromBuffer(value));
IndexServiceClient($grpc.ClientChannel channel,
{$grpc.CallOptions? options,
$core.Iterable<$grpc.ClientInterceptor>? interceptors})
: super(channel, options: options, interceptors: interceptors);
$grpc.ResponseFuture<$0.PingData> pingServer($0.PingData request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$pingServer, request, options: options);
}
$grpc.ResponseFuture<$0.RoomTypesData> getRoomTypes($0.Empty request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$getRoomTypes, request, options: options);
}
$grpc.ResponseFuture<$0.RoomData> createRoom($0.RoomData request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$createRoom, request, options: options);
}
$grpc.ResponseFuture<$0.RoomListData> getRoomList(
$0.RoomListPageReqData request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$getRoomList, request, options: options);
}
$grpc.ResponseFuture<$0.RoomData> touchUser($0.PreUser request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$touchUser, request, options: options);
}
$grpc.ResponseStream<$0.RoomUpdateMessage> joinRoom($0.PreUser request,
{$grpc.CallOptions? options}) {
return $createStreamingCall(
_$joinRoom, $async.Stream.fromIterable([request]),
options: options);
}
$grpc.ResponseFuture<$0.BaseRespData> leaveRoom($0.PreUser request,
{$grpc.CallOptions? options}) {
return $createUnaryCall(_$leaveRoom, request, options: options);
}
}
@$pb.GrpcServiceName('IndexService')
abstract class IndexServiceBase extends $grpc.Service {
$core.String get $name => 'IndexService';
IndexServiceBase() {
$addMethod($grpc.ServiceMethod<$0.PingData, $0.PingData>(
'PingServer',
pingServer_Pre,
false,
false,
($core.List<$core.int> value) => $0.PingData.fromBuffer(value),
($0.PingData value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.Empty, $0.RoomTypesData>(
'GetRoomTypes',
getRoomTypes_Pre,
false,
false,
($core.List<$core.int> value) => $0.Empty.fromBuffer(value),
($0.RoomTypesData value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.RoomData, $0.RoomData>(
'CreateRoom',
createRoom_Pre,
false,
false,
($core.List<$core.int> value) => $0.RoomData.fromBuffer(value),
($0.RoomData value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.RoomListPageReqData, $0.RoomListData>(
'GetRoomList',
getRoomList_Pre,
false,
false,
($core.List<$core.int> value) =>
$0.RoomListPageReqData.fromBuffer(value),
($0.RoomListData value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.PreUser, $0.RoomData>(
'TouchUser',
touchUser_Pre,
false,
false,
($core.List<$core.int> value) => $0.PreUser.fromBuffer(value),
($0.RoomData value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.PreUser, $0.RoomUpdateMessage>(
'JoinRoom',
joinRoom_Pre,
false,
true,
($core.List<$core.int> value) => $0.PreUser.fromBuffer(value),
($0.RoomUpdateMessage value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$0.PreUser, $0.BaseRespData>(
'LeaveRoom',
leaveRoom_Pre,
false,
false,
($core.List<$core.int> value) => $0.PreUser.fromBuffer(value),
($0.BaseRespData value) => value.writeToBuffer()));
}
$async.Future<$0.PingData> pingServer_Pre(
$grpc.ServiceCall call, $async.Future<$0.PingData> request) async {
return pingServer(call, await request);
}
$async.Future<$0.RoomTypesData> getRoomTypes_Pre(
$grpc.ServiceCall call, $async.Future<$0.Empty> request) async {
return getRoomTypes(call, await request);
}
$async.Future<$0.RoomData> createRoom_Pre(
$grpc.ServiceCall call, $async.Future<$0.RoomData> request) async {
return createRoom(call, await request);
}
$async.Future<$0.RoomListData> getRoomList_Pre($grpc.ServiceCall call,
$async.Future<$0.RoomListPageReqData> request) async {
return getRoomList(call, await request);
}
$async.Future<$0.RoomData> touchUser_Pre(
$grpc.ServiceCall call, $async.Future<$0.PreUser> request) async {
return touchUser(call, await request);
}
$async.Stream<$0.RoomUpdateMessage> joinRoom_Pre(
$grpc.ServiceCall call, $async.Future<$0.PreUser> request) async* {
yield* joinRoom(call, await request);
}
$async.Future<$0.BaseRespData> leaveRoom_Pre(
$grpc.ServiceCall call, $async.Future<$0.PreUser> request) async {
return leaveRoom(call, await request);
}
$async.Future<$0.PingData> pingServer(
$grpc.ServiceCall call, $0.PingData request);
$async.Future<$0.RoomTypesData> getRoomTypes(
$grpc.ServiceCall call, $0.Empty request);
$async.Future<$0.RoomData> createRoom(
$grpc.ServiceCall call, $0.RoomData request);
$async.Future<$0.RoomListData> getRoomList(
$grpc.ServiceCall call, $0.RoomListPageReqData request);
$async.Future<$0.RoomData> touchUser(
$grpc.ServiceCall call, $0.PreUser request);
$async.Stream<$0.RoomUpdateMessage> joinRoom(
$grpc.ServiceCall call, $0.PreUser request);
$async.Future<$0.BaseRespData> leaveRoom(
$grpc.ServiceCall call, $0.PreUser request);
}

View File

@ -1,354 +0,0 @@
//
// Generated code. Do not modify.
// source: index.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use roomStatusDescriptor instead')
const RoomStatus$json = {
'1': 'RoomStatus',
'2': [
{'1': 'All', '2': 0},
{'1': 'Open', '2': 1},
{'1': 'Private', '2': 2},
{'1': 'Full', '2': 3},
{'1': 'Closed', '2': 4},
{'1': 'WillOffline', '2': 5},
{'1': 'Offline', '2': 6},
],
};
/// Descriptor for `RoomStatus`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List roomStatusDescriptor = $convert.base64Decode(
'CgpSb29tU3RhdHVzEgcKA0FsbBAAEggKBE9wZW4QARILCgdQcml2YXRlEAISCAoERnVsbBADEg'
'oKBkNsb3NlZBAEEg8KC1dpbGxPZmZsaW5lEAUSCwoHT2ZmbGluZRAG');
@$core.Deprecated('Use roomSortTypeDescriptor instead')
const RoomSortType$json = {
'1': 'RoomSortType',
'2': [
{'1': 'Default', '2': 0},
{'1': 'MostPlayerNumber', '2': 1},
{'1': 'MinimumPlayerNumber', '2': 2},
{'1': 'RecentlyCreated', '2': 3},
{'1': 'OldestCreated', '2': 4},
],
};
/// Descriptor for `RoomSortType`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List roomSortTypeDescriptor = $convert.base64Decode(
'CgxSb29tU29ydFR5cGUSCwoHRGVmYXVsdBAAEhQKEE1vc3RQbGF5ZXJOdW1iZXIQARIXChNNaW'
'5pbXVtUGxheWVyTnVtYmVyEAISEwoPUmVjZW50bHlDcmVhdGVkEAMSEQoNT2xkZXN0Q3JlYXRl'
'ZBAE');
@$core.Deprecated('Use roomUserStatusDescriptor instead')
const RoomUserStatus$json = {
'1': 'RoomUserStatus',
'2': [
{'1': 'RoomUserStatusJoin', '2': 0},
{'1': 'RoomUserStatusLostOffline', '2': 1},
{'1': 'RoomUserStatusLeave', '2': 2},
{'1': 'RoomUserStatusWaitingConnect', '2': 3},
],
};
/// Descriptor for `RoomUserStatus`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List roomUserStatusDescriptor = $convert.base64Decode(
'Cg5Sb29tVXNlclN0YXR1cxIWChJSb29tVXNlclN0YXR1c0pvaW4QABIdChlSb29tVXNlclN0YX'
'R1c0xvc3RPZmZsaW5lEAESFwoTUm9vbVVzZXJTdGF0dXNMZWF2ZRACEiAKHFJvb21Vc2VyU3Rh'
'dHVzV2FpdGluZ0Nvbm5lY3QQAw==');
@$core.Deprecated('Use roomUpdateTypeDescriptor instead')
const RoomUpdateType$json = {
'1': 'RoomUpdateType',
'2': [
{'1': 'RoomUpdateData', '2': 0},
{'1': 'RoomClose', '2': 1},
],
};
/// Descriptor for `RoomUpdateType`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List roomUpdateTypeDescriptor = $convert.base64Decode(
'Cg5Sb29tVXBkYXRlVHlwZRISCg5Sb29tVXBkYXRlRGF0YRAAEg0KCVJvb21DbG9zZRAB');
@$core.Deprecated('Use emptyDescriptor instead')
const Empty$json = {
'1': 'Empty',
};
/// Descriptor for `Empty`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List emptyDescriptor =
$convert.base64Decode('CgVFbXB0eQ==');
@$core.Deprecated('Use baseRespDataDescriptor instead')
const BaseRespData$json = {
'1': 'BaseRespData',
'2': [
{'1': 'code', '3': 1, '4': 1, '5': 5, '10': 'code'},
{'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'},
],
};
/// Descriptor for `BaseRespData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List baseRespDataDescriptor = $convert.base64Decode(
'CgxCYXNlUmVzcERhdGESEgoEY29kZRgBIAEoBVIEY29kZRIYCgdtZXNzYWdlGAIgASgJUgdtZX'
'NzYWdl');
@$core.Deprecated('Use basePageRespDataDescriptor instead')
const BasePageRespData$json = {
'1': 'BasePageRespData',
'2': [
{'1': 'code', '3': 1, '4': 1, '5': 5, '10': 'code'},
{'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'},
{'1': 'hasNext', '3': 3, '4': 1, '5': 8, '10': 'hasNext'},
{'1': 'curPageNum', '3': 4, '4': 1, '5': 4, '10': 'curPageNum'},
{'1': 'pageSize', '3': 5, '4': 1, '5': 3, '10': 'pageSize'},
],
};
/// Descriptor for `BasePageRespData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List basePageRespDataDescriptor = $convert.base64Decode(
'ChBCYXNlUGFnZVJlc3BEYXRhEhIKBGNvZGUYASABKAVSBGNvZGUSGAoHbWVzc2FnZRgCIAEoCV'
'IHbWVzc2FnZRIYCgdoYXNOZXh0GAMgASgIUgdoYXNOZXh0Eh4KCmN1clBhZ2VOdW0YBCABKARS'
'CmN1clBhZ2VOdW0SGgoIcGFnZVNpemUYBSABKANSCHBhZ2VTaXpl');
@$core.Deprecated('Use pingDataDescriptor instead')
const PingData$json = {
'1': 'PingData',
'2': [
{'1': 'data', '3': 1, '4': 1, '5': 9, '10': 'data'},
{'1': 'clientVersion', '3': 2, '4': 1, '5': 18, '10': 'clientVersion'},
{'1': 'serverVersion', '3': 3, '4': 1, '5': 18, '10': 'serverVersion'},
],
};
/// Descriptor for `PingData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List pingDataDescriptor = $convert.base64Decode(
'CghQaW5nRGF0YRISCgRkYXRhGAEgASgJUgRkYXRhEiQKDWNsaWVudFZlcnNpb24YAiABKBJSDW'
'NsaWVudFZlcnNpb24SJAoNc2VydmVyVmVyc2lvbhgDIAEoElINc2VydmVyVmVyc2lvbg==');
@$core.Deprecated('Use roomTypesDataDescriptor instead')
const RoomTypesData$json = {
'1': 'RoomTypesData',
'2': [
{
'1': 'roomTypes',
'3': 1,
'4': 3,
'5': 11,
'6': '.RoomType',
'10': 'roomTypes'
},
],
};
/// Descriptor for `RoomTypesData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomTypesDataDescriptor = $convert.base64Decode(
'Cg1Sb29tVHlwZXNEYXRhEicKCXJvb21UeXBlcxgBIAMoCzIJLlJvb21UeXBlUglyb29tVHlwZX'
'M=');
@$core.Deprecated('Use roomTypeDescriptor instead')
const RoomType$json = {
'1': 'RoomType',
'2': [
{'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
{'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
{'1': 'icon', '3': 3, '4': 1, '5': 9, '10': 'icon'},
{'1': 'desc', '3': 4, '4': 1, '5': 9, '10': 'desc'},
{
'1': 'subTypes',
'3': 5,
'4': 3,
'5': 11,
'6': '.RoomSubtype',
'10': 'subTypes'
},
],
};
/// Descriptor for `RoomType`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomTypeDescriptor = $convert.base64Decode(
'CghSb29tVHlwZRIOCgJpZBgBIAEoCVICaWQSEgoEbmFtZRgCIAEoCVIEbmFtZRISCgRpY29uGA'
'MgASgJUgRpY29uEhIKBGRlc2MYBCABKAlSBGRlc2MSKAoIc3ViVHlwZXMYBSADKAsyDC5Sb29t'
'U3VidHlwZVIIc3ViVHlwZXM=');
@$core.Deprecated('Use roomSubtypeDescriptor instead')
const RoomSubtype$json = {
'1': 'RoomSubtype',
'2': [
{'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
{'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'},
],
};
/// Descriptor for `RoomSubtype`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomSubtypeDescriptor = $convert.base64Decode(
'CgtSb29tU3VidHlwZRIOCgJpZBgBIAEoCVICaWQSEgoEbmFtZRgCIAEoCVIEbmFtZQ==');
@$core.Deprecated('Use roomDataDescriptor instead')
const RoomData$json = {
'1': 'RoomData',
'2': [
{'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
{'1': 'roomTypeID', '3': 2, '4': 1, '5': 9, '10': 'roomTypeID'},
{'1': 'roomSubTypeIds', '3': 3, '4': 3, '5': 9, '10': 'roomSubTypeIds'},
{'1': 'owner', '3': 4, '4': 1, '5': 9, '10': 'owner'},
{'1': 'maxPlayer', '3': 5, '4': 1, '5': 5, '10': 'maxPlayer'},
{'1': 'createTime', '3': 6, '4': 1, '5': 3, '10': 'createTime'},
{'1': 'curPlayer', '3': 7, '4': 1, '5': 5, '10': 'curPlayer'},
{
'1': 'status',
'3': 8,
'4': 1,
'5': 14,
'6': '.RoomStatus',
'10': 'status'
},
{'1': 'deviceUUID', '3': 9, '4': 1, '5': 9, '10': 'deviceUUID'},
{'1': 'announcement', '3': 10, '4': 1, '5': 9, '10': 'announcement'},
{'1': 'avatar', '3': 11, '4': 1, '5': 9, '10': 'avatar'},
{'1': 'updateTime', '3': 12, '4': 1, '5': 3, '10': 'updateTime'},
],
};
/// Descriptor for `RoomData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomDataDescriptor = $convert.base64Decode(
'CghSb29tRGF0YRIOCgJpZBgBIAEoCVICaWQSHgoKcm9vbVR5cGVJRBgCIAEoCVIKcm9vbVR5cG'
'VJRBImCg5yb29tU3ViVHlwZUlkcxgDIAMoCVIOcm9vbVN1YlR5cGVJZHMSFAoFb3duZXIYBCAB'
'KAlSBW93bmVyEhwKCW1heFBsYXllchgFIAEoBVIJbWF4UGxheWVyEh4KCmNyZWF0ZVRpbWUYBi'
'ABKANSCmNyZWF0ZVRpbWUSHAoJY3VyUGxheWVyGAcgASgFUgljdXJQbGF5ZXISIwoGc3RhdHVz'
'GAggASgOMgsuUm9vbVN0YXR1c1IGc3RhdHVzEh4KCmRldmljZVVVSUQYCSABKAlSCmRldmljZV'
'VVSUQSIgoMYW5ub3VuY2VtZW50GAogASgJUgxhbm5vdW5jZW1lbnQSFgoGYXZhdGFyGAsgASgJ'
'UgZhdmF0YXISHgoKdXBkYXRlVGltZRgMIAEoA1IKdXBkYXRlVGltZQ==');
@$core.Deprecated('Use roomListPageReqDataDescriptor instead')
const RoomListPageReqData$json = {
'1': 'RoomListPageReqData',
'2': [
{'1': 'typeID', '3': 1, '4': 1, '5': 9, '10': 'typeID'},
{'1': 'subTypeID', '3': 2, '4': 1, '5': 9, '10': 'subTypeID'},
{
'1': 'status',
'3': 3,
'4': 1,
'5': 14,
'6': '.RoomStatus',
'10': 'status'
},
{'1': 'sort', '3': 4, '4': 1, '5': 14, '6': '.RoomSortType', '10': 'sort'},
{'1': 'pageNum', '3': 5, '4': 1, '5': 4, '10': 'pageNum'},
],
};
/// Descriptor for `RoomListPageReqData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomListPageReqDataDescriptor = $convert.base64Decode(
'ChNSb29tTGlzdFBhZ2VSZXFEYXRhEhYKBnR5cGVJRBgBIAEoCVIGdHlwZUlEEhwKCXN1YlR5cG'
'VJRBgCIAEoCVIJc3ViVHlwZUlEEiMKBnN0YXR1cxgDIAEoDjILLlJvb21TdGF0dXNSBnN0YXR1'
'cxIhCgRzb3J0GAQgASgOMg0uUm9vbVNvcnRUeXBlUgRzb3J0EhgKB3BhZ2VOdW0YBSABKARSB3'
'BhZ2VOdW0=');
@$core.Deprecated('Use roomListDataDescriptor instead')
const RoomListData$json = {
'1': 'RoomListData',
'2': [
{
'1': 'pageData',
'3': 1,
'4': 1,
'5': 11,
'6': '.BasePageRespData',
'10': 'pageData'
},
{'1': 'rooms', '3': 2, '4': 3, '5': 11, '6': '.RoomData', '10': 'rooms'},
],
};
/// Descriptor for `RoomListData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomListDataDescriptor = $convert.base64Decode(
'CgxSb29tTGlzdERhdGESLQoIcGFnZURhdGEYASABKAsyES5CYXNlUGFnZVJlc3BEYXRhUghwYW'
'dlRGF0YRIfCgVyb29tcxgCIAMoCzIJLlJvb21EYXRhUgVyb29tcw==');
@$core.Deprecated('Use preUserDescriptor instead')
const PreUser$json = {
'1': 'PreUser',
'2': [
{'1': 'userName', '3': 1, '4': 1, '5': 9, '10': 'userName'},
{'1': 'deviceUUID', '3': 2, '4': 1, '5': 9, '10': 'deviceUUID'},
{'1': 'roomID', '3': 3, '4': 1, '5': 9, '10': 'roomID'},
],
};
/// Descriptor for `PreUser`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List preUserDescriptor = $convert.base64Decode(
'CgdQcmVVc2VyEhoKCHVzZXJOYW1lGAEgASgJUgh1c2VyTmFtZRIeCgpkZXZpY2VVVUlEGAIgAS'
'gJUgpkZXZpY2VVVUlEEhYKBnJvb21JRBgDIAEoCVIGcm9vbUlE');
@$core.Deprecated('Use roomUserDataDescriptor instead')
const RoomUserData$json = {
'1': 'RoomUserData',
'2': [
{'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
{'1': 'playerName', '3': 2, '4': 1, '5': 9, '10': 'playerName'},
{'1': 'Avatar', '3': 3, '4': 1, '5': 9, '10': 'Avatar'},
{
'1': 'status',
'3': 4,
'4': 1,
'5': 14,
'6': '.RoomUserStatus',
'10': 'status'
},
],
};
/// Descriptor for `RoomUserData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomUserDataDescriptor = $convert.base64Decode(
'CgxSb29tVXNlckRhdGESDgoCaWQYASABKAlSAmlkEh4KCnBsYXllck5hbWUYAiABKAlSCnBsYX'
'llck5hbWUSFgoGQXZhdGFyGAMgASgJUgZBdmF0YXISJwoGc3RhdHVzGAQgASgOMg8uUm9vbVVz'
'ZXJTdGF0dXNSBnN0YXR1cw==');
@$core.Deprecated('Use roomUpdateMessageDescriptor instead')
const RoomUpdateMessage$json = {
'1': 'RoomUpdateMessage',
'2': [
{
'1': 'roomData',
'3': 1,
'4': 1,
'5': 11,
'6': '.RoomData',
'10': 'roomData'
},
{
'1': 'usersData',
'3': 2,
'4': 3,
'5': 11,
'6': '.RoomUserData',
'10': 'usersData'
},
{
'1': 'roomUpdateType',
'3': 3,
'4': 1,
'5': 14,
'6': '.RoomUpdateType',
'10': 'roomUpdateType'
},
],
};
/// Descriptor for `RoomUpdateMessage`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List roomUpdateMessageDescriptor = $convert.base64Decode(
'ChFSb29tVXBkYXRlTWVzc2FnZRIlCghyb29tRGF0YRgBIAEoCzIJLlJvb21EYXRhUghyb29tRG'
'F0YRIrCgl1c2Vyc0RhdGEYAiADKAsyDS5Sb29tVXNlckRhdGFSCXVzZXJzRGF0YRI3Cg5yb29t'
'VXBkYXRlVHlwZRgDIAEoDjIPLlJvb21VcGRhdGVUeXBlUg5yb29tVXBkYXRlVHlwZQ==');

View File

@ -1,101 +0,0 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hexcolor/hexcolor.dart';
import 'base/ui_model.dart';
import 'common/conf/app_conf.dart';
import 'ui/settings/upgrade_dialog_ui.dart';
import 'ui/settings/upgrade_dialog_ui_model.dart';
final globalUIModel = AppGlobalUIModel();
final globalUIModelProvider = ChangeNotifierProvider((ref) => globalUIModel);
class AppGlobalUIModel extends BaseUIModel {
Timer? activityThemeColorTimer;
Future<String?> getRunningGameUser() async {
await Future.delayed(const Duration(milliseconds: 300));
///TODO
return "xkeyC";
}
Future<bool> doCheckUpdate(BuildContext context, {bool init = true}) async {
dynamic checkUpdateError;
if (!init) {
try {
await AppConf.checkUpdate();
} catch (e) {
checkUpdateError = e;
}
}
await Future.delayed(const Duration(milliseconds: 100));
if (AppConf.networkVersionData == null) {
showToast(context,
"网络异常!\n这可能是您的网络环境存在DNS污染请尝试更换DNS。\n或服务器正在维护或遭受攻击,稍后再试。 \n进入离线模式... \n\n请谨慎在离线模式中使用。 \n当前版本构建日期:${AppConf.appVersionDate}\n QQ群940696487 \n错误信息:$checkUpdateError");
return false;
}
final lastVersion = AppConf.isMSE
? AppConf.networkVersionData?.mSELastVersionCode
: AppConf.networkVersionData?.lastVersionCode;
if ((lastVersion ?? 0) > AppConf.appVersionCode) {
// need update
final r = await showDialog(
dismissWithEsc: false,
context: context,
builder: (context) => BaseUIContainer(
uiCreate: () => UpgradeDialogUI(),
modelCreate: () => UpgradeDialogUIModel()));
if (r != true) {
showToast(context, "获取更新信息失败,请稍后重试。");
return false;
}
return true;
}
return false;
}
checkActivityThemeColor() {
if (activityThemeColorTimer != null) {
activityThemeColorTimer?.cancel();
activityThemeColorTimer = null;
}
if (AppConf.networkVersionData == null ||
AppConf.networkVersionData?.activityColors?.enable != true) return;
final startTime = AppConf.networkVersionData!.activityColors?.startTime;
final endTime = AppConf.networkVersionData!.activityColors?.endTime;
if (startTime == null || endTime == null) return;
final now = DateTime.now().millisecondsSinceEpoch;
dPrint("now == $now start == $startTime end == $endTime");
if (now < startTime) {
activityThemeColorTimer = Timer(
Duration(milliseconds: startTime - now), checkActivityThemeColor);
dPrint("start Timer ....");
} else if (now >= startTime && now <= endTime) {
dPrint("update Color ....");
// update Color
final colorCfg = AppConf.networkVersionData!.activityColors;
AppConf.colorBackground =
HexColor(colorCfg?.background ?? "#132431").withOpacity(.75);
AppConf.colorMenu =
HexColor(colorCfg?.menu ?? "#132431").withOpacity(.95);
AppConf.colorMica = HexColor(colorCfg?.mica ?? "#0A3142");
notifyListeners();
// wait for end
activityThemeColorTimer =
Timer(Duration(milliseconds: endTime - now), checkActivityThemeColor);
} else {
dPrint("reset Color ....");
AppConf.colorBackground = HexColor("#132431").withOpacity(.75);
AppConf.colorMenu = HexColor("#132431").withOpacity(.95);
AppConf.colorMica = HexColor("#0A3142");
notifyListeners();
}
notifyListeners();
}
}

View File

@ -1,34 +1,42 @@
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import 'base/ui_model.dart';
import 'common/conf/app_conf.dart';
import 'global_ui_model.dart';
import 'ui/splash_ui.dart';
import 'ui/splash_ui_model.dart';
import 'app.dart';
void main(List<String> args) async {
// webview window
if (runWebViewTitleBarWidget(args,
backgroundColor: const Color.fromRGBO(19, 36, 49, 1),
builder: _defaultWebviewTitleBar)) {
return;
}
await AppConf.init(args);
runApp(ProviderScope(
child: BaseUIContainer(
uiCreate: () => AppUI(),
modelCreate: () => globalUIModelProvider,
),
));
WidgetsFlutterBinding.ensureInitialized();
await _initWindow();
// run app
runApp(const ProviderScope(child: App()));
}
class AppUI extends BaseUI {
_initWindow() async {
await windowManager.ensureInitialized();
await windowManager.setTitleBarStyle(
TitleBarStyle.hidden,
windowButtonVisibility: false,
);
await windowManager.center(animate: true);
}
class App extends HookConsumerWidget {
const App({super.key});
@override
Widget? buildBody(BuildContext context, BaseUIModel model) {
return FluentApp(
title: "StarCitizen Doctor",
restorationScopeId: "Doctor",
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
final appState = ref.watch(appGlobalModelProvider);
return FluentApp.router(
title: "StarCitizenToolBox",
restorationScopeId: "StarCitizenToolBox",
themeMode: ThemeMode.dark,
builder: (context, child) {
return MediaQuery(
@ -41,10 +49,10 @@ class AppUI extends BaseUI {
brightness: Brightness.dark,
fontFamily: "SourceHanSansCN-Regular",
navigationPaneTheme: NavigationPaneThemeData(
backgroundColor: AppConf.colorBackground,
backgroundColor: appState.themeConf.backgroundColor,
),
menuColor: AppConf.colorMenu,
micaBackgroundColor: AppConf.colorMica,
menuColor: appState.themeConf.menuColor,
micaBackgroundColor: appState.themeConf.micaColor,
buttonTheme: ButtonThemeData(
defaultButtonStyle: ButtonStyle(
shape: ButtonState.all(RoundedRectangleBorder(
@ -52,28 +60,9 @@ class AppUI extends BaseUI {
side: BorderSide(color: Colors.white.withOpacity(.01)))),
))),
debugShowCheckedModeBanner: false,
home: BaseUIContainer(
uiCreate: () => SplashUI(), modelCreate: () => SplashUIModel()),
);
}
@override
String getUITitle(BuildContext context, BaseUIModel model) => "";
}
class WindowButtons extends StatelessWidget {
const WindowButtons({super.key});
@override
Widget build(BuildContext context) {
final FluentThemeData theme = FluentTheme.of(context);
return SizedBox(
width: 138,
height: 50,
child: WindowCaption(
brightness: theme.brightness,
backgroundColor: Colors.transparent,
),
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
routeInformationProvider: router.routeInformationProvider,
);
}
}

View File

@ -1,49 +1,81 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/conf/binary_conf.dart';
import 'package:starcitizen_doctor/common/rust/api/process_api.dart'
as rs_process;
import 'dart:io';
import 'dart:math';
import 'package:aria2/aria2.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/binary_conf.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/rust/api/process_api.dart'
as rs_process;
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/utils/provider.dart';
class Aria2cManager {
static bool _isDaemonRunning = false;
part 'aria2c.g.dart';
static final String _aria2cDir =
"${AppConf.applicationBinaryModuleDir}\\aria2c";
part 'aria2c.freezed.dart';
static Aria2c? _aria2c;
static Aria2c getClient() {
if (_aria2c != null) return _aria2c!;
throw "not connect!";
@freezed
class Aria2cModelState with _$Aria2cModelState {
const factory Aria2cModelState({
required String aria2cDir,
Aria2c? aria2c,
Aria2GlobalStat? aria2globalStat,
}) = _Aria2cModelState;
}
static bool get isAvailable => _isDaemonRunning && _aria2c != null;
extension Aria2cModelExt on Aria2cModelState {
bool get isRunning => aria2c != null;
static Future checkLazyLoad() async {
bool get hasDownloadTask => aria2globalStat != null && aria2TotalTaskNum > 0;
int get aria2TotalTaskNum => aria2globalStat == null
? 0
: ((aria2globalStat!.numActive ?? 0) +
(aria2globalStat!.numWaiting ?? 0));
}
@riverpod
class Aria2cModel extends _$Aria2cModel {
bool _disposed = false;
@override
Aria2cModelState build() {
if (appGlobalState.applicationBinaryModuleDir == null) {
throw Exception("applicationBinaryModuleDir is null");
}
ref.onDispose(() {
_disposed = true;
});
ref.keepAlive();
final aria2cDir = "${appGlobalState.applicationBinaryModuleDir}\\aria2c";
// LazyLoad init
() async {
try {
final sessionFile = File("$_aria2cDir\\aria2.session");
final sessionFile = File("$aria2cDir\\aria2.session");
//
if (await sessionFile.exists() &&
(await sessionFile.readAsString()).trim().isNotEmpty) {
await launchDaemon();
dPrint("launch Aria2c daemon");
await launchDaemon(appGlobalState.applicationBinaryModuleDir!);
} else {
dPrint("LazyLoad Aria2c daemon");
}
} catch (e) {
dPrint("Aria2cManager.checkLazyLoad Error:$e");
}
}();
return Aria2cModelState(aria2cDir: aria2cDir);
}
static Future launchDaemon() async {
if (_isDaemonRunning) return;
await BinaryModuleConf.extractModule(["aria2c"]);
Future launchDaemon(String applicationBinaryModuleDir) async {
if (state.aria2c != null) return;
await BinaryModuleConf.extractModule(
["aria2c"], applicationBinaryModuleDir);
/// skip for debug hot reload
if (kDebugMode) {
@ -53,12 +85,12 @@ class Aria2cManager {
}
}
final sessionFile = File("$_aria2cDir\\aria2.session");
final sessionFile = File("${state.aria2cDir}\\aria2.session");
if (!await sessionFile.exists()) {
await sessionFile.create(recursive: true);
}
final exePath = "$_aria2cDir\\aria2c.exe";
final exePath = "${state.aria2cDir}\\aria2c.exe";
final port = await getFreePort();
final pwd = generateRandomPassword(16);
dPrint("pwd === $pwd");
@ -72,7 +104,7 @@ class Aria2cManager {
"-V",
"-c",
"-x 10",
"--dir=$_aria2cDir\\downloads",
"--dir=${state.aria2cDir}\\downloads",
"--disable-ipv6",
"--enable-rpc",
"--pause",
@ -84,7 +116,7 @@ class Aria2cManager {
"--file-allocation=trunc",
"--seed-time=0",
],
workingDirectory: _aria2cDir);
workingDirectory: state.aria2cDir);
String launchError = "";
@ -95,31 +127,29 @@ class Aria2cManager {
_onLaunch(port, pwd, trackerList);
}
} else if (event.startsWith("error:")) {
_isDaemonRunning = false;
_aria2c = null;
state = state.copyWith(aria2c: null);
launchError = event;
} else if (event.startsWith("exit:")) {
_isDaemonRunning = false;
_aria2c = null;
state = state.copyWith(aria2c: null);
launchError = event;
}
});
while (true) {
if (_isDaemonRunning) return;
if (state.aria2c != null) return;
if (launchError.isNotEmpty) throw launchError;
await Future.delayed(const Duration(milliseconds: 100));
}
}
static Future<int> getFreePort() async {
Future<int> getFreePort() async {
final serverSocket = await ServerSocket.bind("127.0.0.1", 0);
final port = serverSocket.port;
await serverSocket.close();
return port;
}
static String generateRandomPassword(int length) {
String generateRandomPassword(int length) {
const String charset =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = Random();
@ -131,7 +161,7 @@ class Aria2cManager {
return buffer.toString();
}
static int textToByte(String text) {
int textToByte(String text) {
if (text.length == 1) {
return 0;
}
@ -147,19 +177,36 @@ class Aria2cManager {
return 0;
}
static Future<void> _onLaunch(
int port, String pwd, String trackerList) async {
_isDaemonRunning = true;
_aria2c = Aria2c("ws://127.0.0.1:$port/jsonrpc", "websocket", pwd);
_aria2c!.getVersion().then((value) {
Future<void> _onLaunch(int port, String pwd, String trackerList) async {
final aria2c = Aria2c("ws://127.0.0.1:$port/jsonrpc", "websocket", pwd);
state = state.copyWith(aria2c: aria2c);
aria2c.getVersion().then((value) {
dPrint("Aria2cManager.connected! version == ${value.version}");
_listenState(aria2c);
});
final box = await Hive.openBox("app_conf");
_aria2c!.changeGlobalOption(Aria2Option()
aria2c.changeGlobalOption(Aria2Option()
..maxOverallUploadLimit =
textToByte(box.get("downloader_up_limit", defaultValue: "0"))
..maxOverallDownloadLimit =
textToByte(box.get("downloader_down_limit", defaultValue: "0"))
..btTracker = trackerList);
}
Future<void> _listenState(Aria2c aria2c) async {
dPrint("Aria2cModel._listenState start");
while (true) {
if (_disposed || state.aria2c == null) {
dPrint("Aria2cModel._listenState end");
return;
}
try {
final aria2globalStat = await aria2c.getGlobalStat();
state = state.copyWith(aria2globalStat: aria2globalStat);
} catch (e) {
dPrint("aria2globalStat update error:$e");
}
await Future.delayed(const Duration(seconds: 1));
}
}
}

View File

@ -0,0 +1,186 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'aria2c.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$Aria2cModelState {
String get aria2cDir => throw _privateConstructorUsedError;
Aria2c? get aria2c => throw _privateConstructorUsedError;
Aria2GlobalStat? get aria2globalStat => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$Aria2cModelStateCopyWith<Aria2cModelState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $Aria2cModelStateCopyWith<$Res> {
factory $Aria2cModelStateCopyWith(
Aria2cModelState value, $Res Function(Aria2cModelState) then) =
_$Aria2cModelStateCopyWithImpl<$Res, Aria2cModelState>;
@useResult
$Res call(
{String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat});
}
/// @nodoc
class _$Aria2cModelStateCopyWithImpl<$Res, $Val extends Aria2cModelState>
implements $Aria2cModelStateCopyWith<$Res> {
_$Aria2cModelStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? aria2cDir = null,
Object? aria2c = freezed,
Object? aria2globalStat = freezed,
}) {
return _then(_value.copyWith(
aria2cDir: null == aria2cDir
? _value.aria2cDir
: aria2cDir // ignore: cast_nullable_to_non_nullable
as String,
aria2c: freezed == aria2c
? _value.aria2c
: aria2c // ignore: cast_nullable_to_non_nullable
as Aria2c?,
aria2globalStat: freezed == aria2globalStat
? _value.aria2globalStat
: aria2globalStat // ignore: cast_nullable_to_non_nullable
as Aria2GlobalStat?,
) as $Val);
}
}
/// @nodoc
abstract class _$$Aria2cModelStateImplCopyWith<$Res>
implements $Aria2cModelStateCopyWith<$Res> {
factory _$$Aria2cModelStateImplCopyWith(_$Aria2cModelStateImpl value,
$Res Function(_$Aria2cModelStateImpl) then) =
__$$Aria2cModelStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat});
}
/// @nodoc
class __$$Aria2cModelStateImplCopyWithImpl<$Res>
extends _$Aria2cModelStateCopyWithImpl<$Res, _$Aria2cModelStateImpl>
implements _$$Aria2cModelStateImplCopyWith<$Res> {
__$$Aria2cModelStateImplCopyWithImpl(_$Aria2cModelStateImpl _value,
$Res Function(_$Aria2cModelStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? aria2cDir = null,
Object? aria2c = freezed,
Object? aria2globalStat = freezed,
}) {
return _then(_$Aria2cModelStateImpl(
aria2cDir: null == aria2cDir
? _value.aria2cDir
: aria2cDir // ignore: cast_nullable_to_non_nullable
as String,
aria2c: freezed == aria2c
? _value.aria2c
: aria2c // ignore: cast_nullable_to_non_nullable
as Aria2c?,
aria2globalStat: freezed == aria2globalStat
? _value.aria2globalStat
: aria2globalStat // ignore: cast_nullable_to_non_nullable
as Aria2GlobalStat?,
));
}
}
/// @nodoc
class _$Aria2cModelStateImpl
with DiagnosticableTreeMixin
implements _Aria2cModelState {
const _$Aria2cModelStateImpl(
{required this.aria2cDir, this.aria2c, this.aria2globalStat});
@override
final String aria2cDir;
@override
final Aria2c? aria2c;
@override
final Aria2GlobalStat? aria2globalStat;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'Aria2cModelState(aria2cDir: $aria2cDir, aria2c: $aria2c, aria2globalStat: $aria2globalStat)';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'Aria2cModelState'))
..add(DiagnosticsProperty('aria2cDir', aria2cDir))
..add(DiagnosticsProperty('aria2c', aria2c))
..add(DiagnosticsProperty('aria2globalStat', aria2globalStat));
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Aria2cModelStateImpl &&
(identical(other.aria2cDir, aria2cDir) ||
other.aria2cDir == aria2cDir) &&
(identical(other.aria2c, aria2c) || other.aria2c == aria2c) &&
(identical(other.aria2globalStat, aria2globalStat) ||
other.aria2globalStat == aria2globalStat));
}
@override
int get hashCode =>
Object.hash(runtimeType, aria2cDir, aria2c, aria2globalStat);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$Aria2cModelStateImplCopyWith<_$Aria2cModelStateImpl> get copyWith =>
__$$Aria2cModelStateImplCopyWithImpl<_$Aria2cModelStateImpl>(
this, _$identity);
}
abstract class _Aria2cModelState implements Aria2cModelState {
const factory _Aria2cModelState(
{required final String aria2cDir,
final Aria2c? aria2c,
final Aria2GlobalStat? aria2globalStat}) = _$Aria2cModelStateImpl;
@override
String get aria2cDir;
@override
Aria2c? get aria2c;
@override
Aria2GlobalStat? get aria2globalStat;
@override
@JsonKey(ignore: true)
_$$Aria2cModelStateImplCopyWith<_$Aria2cModelStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'aria2c.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$aria2cModelHash() => r'6685f6a716016113487de190a61f71196094526e';
/// See also [Aria2cModel].
@ProviderFor(Aria2cModel)
final aria2cModelProvider =
AutoDisposeNotifierProvider<Aria2cModel, Aria2cModelState>.internal(
Aria2cModel.new,
name: r'aria2cModelProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$aria2cModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Aria2cModel = AutoDisposeNotifier<Aria2cModelState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -1,16 +1,20 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/app.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'about_ui_model.dart';
class AboutUI extends BaseUI<AboutUIModel> {
bool isTipTextCn = false;
class AboutUI extends HookConsumerWidget {
const AboutUI({super.key});
@override
Widget? buildBody(BuildContext context, AboutUIModel model) {
Widget build(BuildContext context, WidgetRef ref) {
final isTipTextCn = useState(false);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
@ -20,11 +24,11 @@ class AboutUI extends BaseUI<AboutUIModel> {
Image.asset("assets/app_logo.png", width: 128, height: 128),
const SizedBox(height: 6),
const Text(
"SC汉化盒子 V${AppConf.appVersion} ${AppConf.isMSE ? "" : " +Dev"}",
"SC汉化盒子 V${ConstConf.appVersion} ${ConstConf.isMSE ? "" : " +Dev"}",
style: TextStyle(fontSize: 18)),
const SizedBox(height: 12),
Button(
onPressed: model.checkUpdate,
onPressed: () => _onCheckUpdate(context, ref),
child: const Padding(
padding: EdgeInsets.all(4),
child: Text("检查更新"),
@ -131,15 +135,14 @@ class AboutUI extends BaseUI<AboutUIModel> {
icon: Padding(
padding: const EdgeInsets.all(3),
child: Text(
isTipTextCn ? tipTextCN : tipTextEN,
isTipTextCn.value ? tipTextCN : tipTextEN,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 12, color: Colors.white.withOpacity(.9)),
),
),
onPressed: () {
isTipTextCn = !isTipTextCn;
setState(() {});
isTipTextCn.value = !isTipTextCn.value;
},
),
),
@ -158,6 +161,17 @@ class AboutUI extends BaseUI<AboutUIModel> {
static const tipTextCN =
"这是一个非官方的星际公民工具,不隶属于 Cloud Imperium 公司集团。 本软件中非由其主机或用户创作的所有内容均为其各自所有者的财产。 \nStar Citizen®、Roberts Space Industries® 和 Cloud Imperium® 是 Cloud Imperium Rights LLC 的注册商标。";
@override
String getUITitle(BuildContext context, AboutUIModel model) => "";
_onCheckUpdate(BuildContext context, WidgetRef ref) async {
if (ConstConf.isMSE) {
launchUrlString("ms-windows-store://pdp/?productid=9NF3SWFWNKL1");
return;
} else {
final hasUpdate =
await ref.read(appGlobalModelProvider.notifier).checkUpdate(context);
if (!hasUpdate) {
if (!context.mounted) return;
showToast(context, "已经是最新版本!");
}
}
}
}

View File

@ -1,17 +0,0 @@
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/global_ui_model.dart';
import 'package:url_launcher/url_launcher_string.dart';
class AboutUIModel extends BaseUIModel {
Future<void> checkUpdate() async {
if (AppConf.isMSE) {
launchUrlString("ms-windows-store://pdp/?productid=9NF3SWFWNKL1");
return;
}
final hasUpdate = await globalUIModel.doCheckUpdate(context!);
if (!hasUpdate) {
if (mounted) showToast(context!, "已是最新版本");
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,107 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'home_game_login_dialog_ui_model.dart';
class HomeGameLoginDialogUI extends HookConsumerWidget {
const HomeGameLoginDialogUI({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final loginState = ref.watch(homeGameLoginUIModelProvider);
useEffect(() {
ref.read(homeGameLoginUIModelProvider.notifier).launchWebLogin(context);
return null;
}, const []);
return ContentDialog(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .56,
),
title: (loginState.loginStatus == 2) ? null : const Text("一键启动"),
content: AnimatedSize(
duration: const Duration(milliseconds: 230),
child: Padding(
padding: const EdgeInsets.only(top: 12, bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Row(),
if (loginState.loginStatus == 0) ...[
Center(
child: Column(
children: [
const Text("登录中..."),
const SizedBox(height: 12),
const ProgressRing(),
if (loginState.isDeviceSupportWinHello ?? false)
const SizedBox(height: 24),
Text(
"* 若开启了自动填充,请留意弹出的 Windows Hello 窗口",
style: TextStyle(
fontSize: 13, color: Colors.white.withOpacity(.6)),
)
],
),
),
] else if (loginState.loginStatus == 1) ...[
Text(
"请输入RSI账户 [${loginState.nickname}] 的邮箱,以保存登录状态(输入错误会导致无法进入游戏!)"),
const SizedBox(height: 12),
TextFormBox(
// controller: model.emailCtrl,
),
const SizedBox(height: 6),
Text(
"*该操作同一账号只需执行一次,输入错误请在盒子设置中清理,切换账号请在汉化浏览器中操作。",
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(.6),
),
)
] else if (loginState.loginStatus == 2 ||
loginState.loginStatus == 3) ...[
Center(
child: Column(
children: [
const SizedBox(height: 12),
const Text(
"欢迎回来!",
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 24),
if (loginState.avatarUrl != null)
ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: CacheNetImage(
url: loginState.avatarUrl!,
width: 128,
height: 128,
fit: BoxFit.fill,
),
),
const SizedBox(height: 12),
Text(
loginState.nickname ?? "",
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 32),
Text(loginState.loginStatus == 2
? "正在为您启动游戏..."
: "正在等待优化CPU参数..."),
const SizedBox(height: 12),
const ProgressRing(),
],
),
)
]
],
),
),
),
);
}
}

View File

@ -1,68 +1,78 @@
import 'dart:convert';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
import 'package:local_auth/local_auth.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:cryptography/cryptography.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:hive/hive.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'package:local_auth/local_auth.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/utils/provider.dart';
import 'package:starcitizen_doctor/common/win32/credentials.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
import 'package:starcitizen_doctor/ui/webview/webview.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:uuid/uuid.dart';
class LoginDialogModel extends BaseUIModel {
int loginStatus = 0;
part 'home_game_login_dialog_ui_model.freezed.dart';
String nickname = "";
String? avatarUrl;
String? authToken;
String? webToken;
Map? releaseInfo;
part 'home_game_login_dialog_ui_model.g.dart';
final String installPath;
final HomeUIModel homeUIModel;
// TextEditingController emailCtrl = TextEditingController();
LoginDialogModel(this.installPath, this.homeUIModel);
final LocalAuthentication localAuth = LocalAuthentication();
var isDeviceSupportWinHello = false;
@override
void initModel() {
_launchWebLogin();
super.initModel();
@freezed
class HomeGameLoginState with _$HomeGameLoginState {
const factory HomeGameLoginState({
required int loginStatus,
String? nickname,
String? avatarUrl,
String? authToken,
String? webToken,
Map? releaseInfo,
String? installPath,
bool? isDeviceSupportWinHello,
}) = _LoginStatus;
}
Future<void> _launchWebLogin() async {
isDeviceSupportWinHello = await localAuth.isDeviceSupported();
notifyListeners();
goWebView("登录 RSI 账户", "https://robertsspaceindustries.com/connect",
@riverpod
class HomeGameLoginUIModel extends _$HomeGameLoginUIModel {
@override
HomeGameLoginState build() {
return const HomeGameLoginState(loginStatus: 0);
}
final LocalAuthentication _localAuth = LocalAuthentication();
// ignore: avoid_build_context_in_providers
Future<void> launchWebLogin(BuildContext context) async {
final homeState = ref.read(homeUIModelProvider);
final isDeviceSupportWinHello = await _localAuth.isDeviceSupported();
state = state.copyWith(isDeviceSupportWinHello: isDeviceSupportWinHello);
if (!context.mounted) return;
goWebView(
context, "登录 RSI 账户", "https://robertsspaceindustries.com/connect",
loginMode: true, rsiLoginCallback: (message, ok) async {
// dPrint(
// "======rsiLoginCallback=== $ok ===== data==\n${json.encode(message)}");
if (message == null || !ok) {
Navigator.pop(context!);
Navigator.pop(context);
return;
}
// final emailBox = await Hive.openBox("quick_login_email");
final data = message["data"];
authToken = data["authToken"];
webToken = data["webToken"];
releaseInfo = data["releaseInfo"];
avatarUrl = data["avatar"]
final authToken = data["authToken"];
final webToken = data["webToken"];
final releaseInfo = data["releaseInfo"];
final avatarUrl = data["avatar"]
?.toString()
.replaceAll("url(\"", "")
.replaceAll("\")", "");
Map<String, dynamic> payload = Jwt.parseJwt(authToken!);
nickname = payload["nickname"] ?? "";
final Map<String, dynamic> payload = Jwt.parseJwt(authToken!);
final nickname = payload["nickname"] ?? "";
final inputEmail = data["inputEmail"];
final inputPassword = data["inputPassword"];
@ -71,19 +81,29 @@ class LoginDialogModel extends BaseUIModel {
if (inputEmail != null && inputEmail != "") {
await userBox.put("account_email", inputEmail);
}
state = state.copyWith(
nickname: nickname,
avatarUrl: avatarUrl,
authToken: authToken,
webToken: webToken,
releaseInfo: releaseInfo,
);
if (isDeviceSupportWinHello) {
if (await userBox.get("enable", defaultValue: true)) {
if (inputEmail != null &&
inputEmail != "" &&
inputPassword != null &&
inputPassword != "") {
if (!context.mounted) return;
final ok = await showConfirmDialogs(
context!,
context,
"是否开启自动密码填充?",
const Text(
"盒子将使用 PIN 与 Windows 凭据加密保存您的密码,密码只存储在您的设备中。\n\n当下次登录需要输入密码时您只需授权PIN即可自动填充登录。"));
if (ok == true) {
if (await localAuth.authenticate(localizedReason: "输入PIN以启用加密") ==
if (await _localAuth.authenticate(
localizedReason: "输入PIN以启用加密") ==
true) {
await _savePwd(inputEmail, inputPassword);
}
@ -94,7 +114,8 @@ class LoginDialogModel extends BaseUIModel {
}
}
final buildInfoFile = File("$installPath\\build_manifest.id");
final buildInfoFile =
File("${homeState.scInstalledPath}\\build_manifest.id");
if (await buildInfoFile.exists()) {
final buildInfo =
json.decode(await buildInfoFile.readAsString())["Data"];
@ -105,37 +126,43 @@ class LoginDialogModel extends BaseUIModel {
if (!(releaseInfo!["versionLabel"]!
.toString()
.endsWith(buildInfo["RequestedP4ChangeNum"]!.toString()))) {
if (!context.mounted) return;
final ok = await showConfirmDialogs(
context!,
context,
"游戏版本过期",
Text(
"RSI 服务器报告版本号:${releaseInfo?["versionLabel"]} \n\n本地版本号:${buildInfo["RequestedP4ChangeNum"]} \n\n建议使用 RSI Launcher 更新游戏!"),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .4),
maxWidth: MediaQuery.of(context).size.width * .4),
cancel: "忽略");
if (ok == true) {
Navigator.pop(context!);
if (!context.mounted) return;
Navigator.pop(context);
return;
}
}
}
}
_readyForLaunch();
}, useLocalization: true);
if (!context.mounted) return;
_readyForLaunch(homeState, context);
}, useLocalization: true, homeState: homeState);
}
goWebView(String title, String url,
// ignore: avoid_build_context_in_providers
goWebView(BuildContext context, String title, String url,
{bool useLocalization = false,
bool loginMode = false,
RsiLoginCallback? rsiLoginCallback}) async {
RsiLoginCallback? rsiLoginCallback,
required HomeUIModelState homeState}) async {
if (useLocalization) {
const tipVersion = 2;
final box = await Hive.openBox("app_conf");
final skip = await box.get("skip_web_login_version", defaultValue: 0);
if (skip != tipVersion) {
if (!context.mounted) return;
final ok = await showConfirmDialogs(
context!,
context,
"盒子一键启动",
const Text(
"本功能可以帮您更加便利的启动游戏。\n\n为确保账户安全 ,本功能使用汉化浏览器保留登录状态,且不会保存您的密码信息(除非你启用了自动填充功能)。"
@ -143,7 +170,7 @@ class LoginDialogModel extends BaseUIModel {
style: TextStyle(fontSize: 16),
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .6));
maxWidth: MediaQuery.of(context).size.width * .6));
if (!ok) {
if (loginMode) {
rsiLoginCallback?.call(null, false);
@ -154,84 +181,81 @@ class LoginDialogModel extends BaseUIModel {
}
}
if (!await WebviewWindow.isWebviewAvailable()) {
await showToast(context!, "需要安装 WebView2 Runtime");
if (!context.mounted) return;
await showToast(context, "需要安装 WebView2 Runtime");
if (!context.mounted) return;
await launchUrlString(
"https://developer.microsoft.com/en-us/microsoft-edge/webview2/");
Navigator.pop(context!);
if (!context.mounted) return;
Navigator.pop(context);
return;
}
final webViewModel = WebViewModel(context!,
if (!context.mounted) return;
final webViewModel = WebViewModel(context,
loginMode: loginMode,
loginCallback: rsiLoginCallback,
loginChannel: getChannelID());
loginChannel: getChannelID(homeState.scInstalledPath!));
if (useLocalization) {
try {
await webViewModel
.initLocalization(homeUIModel.appWebLocalizationVersionsData!);
.initLocalization(homeState.webLocalizationVersionsData!);
} catch (_) {}
}
await Future.delayed(const Duration(milliseconds: 500));
await webViewModel.initWebView(
title: title,
applicationSupportDir: appGlobalState.applicationSupportDir!,
appVersionData: appGlobalState.networkVersionData!,
);
await webViewModel.launch(url);
notifyListeners();
await webViewModel.launch(url, appGlobalState.networkVersionData!);
}
// onSaveEmail() async {
// final RegExp emailRegex = RegExp(r'^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$');
// if (!emailRegex.hasMatch(emailCtrl.text.trim())) {
// showToast(context!, "邮箱输入有误!");
// return;
// }
// final emailBox = await Hive.openBox("quick_login_email");
// await emailBox.put(nickname, emailCtrl.text.trim());
// _readyForLaunch();
// notifyListeners();
// }
Future<void> _readyForLaunch() async {
Future<void> _readyForLaunch(
HomeUIModelState homeState,
// ignore: avoid_build_context_in_providers
BuildContext context) async {
final userBox = await Hive.openBox("rsi_account_data");
loginStatus = 2;
notifyListeners();
state = state.copyWith(loginStatus: 2);
final launchData = {
"username": userBox.get("account_email", defaultValue: ""),
"token": webToken,
"auth_token": authToken,
"token": state.webToken,
"auth_token": state.authToken,
"star_network": {
"services_endpoint": releaseInfo?["servicesEndpoint"],
"hostname": releaseInfo?["universeHost"],
"port": releaseInfo?["universePort"],
"services_endpoint": state.releaseInfo?["servicesEndpoint"],
"hostname": state.releaseInfo?["universeHost"],
"port": state.releaseInfo?["universePort"],
},
"TMid": const Uuid().v4(),
};
final executable = releaseInfo?["executable"];
final launchOptions = releaseInfo?["launchOptions"];
final executable = state.releaseInfo?["executable"];
final launchOptions = state.releaseInfo?["launchOptions"];
dPrint("----------launch data ====== -----------\n$launchData");
dPrint(
"----------executable data ====== -----------\n$installPath\\$executable $launchOptions");
final launchFile = File("$installPath\\loginData.json");
"----------executable data ====== -----------\n${homeState.scInstalledPath}\\$executable $launchOptions");
final launchFile = File("${homeState.scInstalledPath}\\loginData.json");
if (await launchFile.exists()) {
await launchFile.delete();
}
await launchFile.create();
await launchFile.writeAsString(json.encode(launchData));
notifyListeners();
await Future.delayed(const Duration(seconds: 1));
await Future.delayed(const Duration(seconds: 3));
final processorAffinity = await SystemHelper.getCpuAffinity();
final homeUIModel = ref.read(homeUIModelProvider.notifier);
if (!context.mounted) return;
homeUIModel.doLaunchGame(
'$installPath\\$executable',
context,
'${homeState.scInstalledPath}\\$executable',
["-no_login_dialog", ...launchOptions.toString().split(" ")],
installPath,
homeState.scInstalledPath!,
processorAffinity);
await Future.delayed(const Duration(seconds: 1));
Navigator.pop(context!);
if (!context.mounted) return;
Navigator.pop(context);
}
String getChannelID() {
String getChannelID(String installPath) {
if (installPath.endsWith("\\LIVE")) {
return "LIVE";
} else if (installPath.endsWith("\\PTU")) {

View File

@ -0,0 +1,303 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'home_game_login_dialog_ui_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$HomeGameLoginState {
int get loginStatus => throw _privateConstructorUsedError;
String? get nickname => throw _privateConstructorUsedError;
String? get avatarUrl => throw _privateConstructorUsedError;
String? get authToken => throw _privateConstructorUsedError;
String? get webToken => throw _privateConstructorUsedError;
Map<dynamic, dynamic>? get releaseInfo => throw _privateConstructorUsedError;
String? get installPath => throw _privateConstructorUsedError;
bool? get isDeviceSupportWinHello => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HomeGameLoginStateCopyWith<HomeGameLoginState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $HomeGameLoginStateCopyWith<$Res> {
factory $HomeGameLoginStateCopyWith(
HomeGameLoginState value, $Res Function(HomeGameLoginState) then) =
_$HomeGameLoginStateCopyWithImpl<$Res, HomeGameLoginState>;
@useResult
$Res call(
{int loginStatus,
String? nickname,
String? avatarUrl,
String? authToken,
String? webToken,
Map<dynamic, dynamic>? releaseInfo,
String? installPath,
bool? isDeviceSupportWinHello});
}
/// @nodoc
class _$HomeGameLoginStateCopyWithImpl<$Res, $Val extends HomeGameLoginState>
implements $HomeGameLoginStateCopyWith<$Res> {
_$HomeGameLoginStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? loginStatus = null,
Object? nickname = freezed,
Object? avatarUrl = freezed,
Object? authToken = freezed,
Object? webToken = freezed,
Object? releaseInfo = freezed,
Object? installPath = freezed,
Object? isDeviceSupportWinHello = freezed,
}) {
return _then(_value.copyWith(
loginStatus: null == loginStatus
? _value.loginStatus
: loginStatus // ignore: cast_nullable_to_non_nullable
as int,
nickname: freezed == nickname
? _value.nickname
: nickname // ignore: cast_nullable_to_non_nullable
as String?,
avatarUrl: freezed == avatarUrl
? _value.avatarUrl
: avatarUrl // ignore: cast_nullable_to_non_nullable
as String?,
authToken: freezed == authToken
? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable
as String?,
webToken: freezed == webToken
? _value.webToken
: webToken // ignore: cast_nullable_to_non_nullable
as String?,
releaseInfo: freezed == releaseInfo
? _value.releaseInfo
: releaseInfo // ignore: cast_nullable_to_non_nullable
as Map<dynamic, dynamic>?,
installPath: freezed == installPath
? _value.installPath
: installPath // ignore: cast_nullable_to_non_nullable
as String?,
isDeviceSupportWinHello: freezed == isDeviceSupportWinHello
? _value.isDeviceSupportWinHello
: isDeviceSupportWinHello // ignore: cast_nullable_to_non_nullable
as bool?,
) as $Val);
}
}
/// @nodoc
abstract class _$$LoginStatusImplCopyWith<$Res>
implements $HomeGameLoginStateCopyWith<$Res> {
factory _$$LoginStatusImplCopyWith(
_$LoginStatusImpl value, $Res Function(_$LoginStatusImpl) then) =
__$$LoginStatusImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int loginStatus,
String? nickname,
String? avatarUrl,
String? authToken,
String? webToken,
Map<dynamic, dynamic>? releaseInfo,
String? installPath,
bool? isDeviceSupportWinHello});
}
/// @nodoc
class __$$LoginStatusImplCopyWithImpl<$Res>
extends _$HomeGameLoginStateCopyWithImpl<$Res, _$LoginStatusImpl>
implements _$$LoginStatusImplCopyWith<$Res> {
__$$LoginStatusImplCopyWithImpl(
_$LoginStatusImpl _value, $Res Function(_$LoginStatusImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? loginStatus = null,
Object? nickname = freezed,
Object? avatarUrl = freezed,
Object? authToken = freezed,
Object? webToken = freezed,
Object? releaseInfo = freezed,
Object? installPath = freezed,
Object? isDeviceSupportWinHello = freezed,
}) {
return _then(_$LoginStatusImpl(
loginStatus: null == loginStatus
? _value.loginStatus
: loginStatus // ignore: cast_nullable_to_non_nullable
as int,
nickname: freezed == nickname
? _value.nickname
: nickname // ignore: cast_nullable_to_non_nullable
as String?,
avatarUrl: freezed == avatarUrl
? _value.avatarUrl
: avatarUrl // ignore: cast_nullable_to_non_nullable
as String?,
authToken: freezed == authToken
? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable
as String?,
webToken: freezed == webToken
? _value.webToken
: webToken // ignore: cast_nullable_to_non_nullable
as String?,
releaseInfo: freezed == releaseInfo
? _value._releaseInfo
: releaseInfo // ignore: cast_nullable_to_non_nullable
as Map<dynamic, dynamic>?,
installPath: freezed == installPath
? _value.installPath
: installPath // ignore: cast_nullable_to_non_nullable
as String?,
isDeviceSupportWinHello: freezed == isDeviceSupportWinHello
? _value.isDeviceSupportWinHello
: isDeviceSupportWinHello // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
}
/// @nodoc
class _$LoginStatusImpl implements _LoginStatus {
const _$LoginStatusImpl(
{required this.loginStatus,
this.nickname,
this.avatarUrl,
this.authToken,
this.webToken,
final Map<dynamic, dynamic>? releaseInfo,
this.installPath,
this.isDeviceSupportWinHello})
: _releaseInfo = releaseInfo;
@override
final int loginStatus;
@override
final String? nickname;
@override
final String? avatarUrl;
@override
final String? authToken;
@override
final String? webToken;
final Map<dynamic, dynamic>? _releaseInfo;
@override
Map<dynamic, dynamic>? get releaseInfo {
final value = _releaseInfo;
if (value == null) return null;
if (_releaseInfo is EqualUnmodifiableMapView) return _releaseInfo;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override
final String? installPath;
@override
final bool? isDeviceSupportWinHello;
@override
String toString() {
return 'HomeGameLoginState(loginStatus: $loginStatus, nickname: $nickname, avatarUrl: $avatarUrl, authToken: $authToken, webToken: $webToken, releaseInfo: $releaseInfo, installPath: $installPath, isDeviceSupportWinHello: $isDeviceSupportWinHello)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LoginStatusImpl &&
(identical(other.loginStatus, loginStatus) ||
other.loginStatus == loginStatus) &&
(identical(other.nickname, nickname) ||
other.nickname == nickname) &&
(identical(other.avatarUrl, avatarUrl) ||
other.avatarUrl == avatarUrl) &&
(identical(other.authToken, authToken) ||
other.authToken == authToken) &&
(identical(other.webToken, webToken) ||
other.webToken == webToken) &&
const DeepCollectionEquality()
.equals(other._releaseInfo, _releaseInfo) &&
(identical(other.installPath, installPath) ||
other.installPath == installPath) &&
(identical(
other.isDeviceSupportWinHello, isDeviceSupportWinHello) ||
other.isDeviceSupportWinHello == isDeviceSupportWinHello));
}
@override
int get hashCode => Object.hash(
runtimeType,
loginStatus,
nickname,
avatarUrl,
authToken,
webToken,
const DeepCollectionEquality().hash(_releaseInfo),
installPath,
isDeviceSupportWinHello);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$LoginStatusImplCopyWith<_$LoginStatusImpl> get copyWith =>
__$$LoginStatusImplCopyWithImpl<_$LoginStatusImpl>(this, _$identity);
}
abstract class _LoginStatus implements HomeGameLoginState {
const factory _LoginStatus(
{required final int loginStatus,
final String? nickname,
final String? avatarUrl,
final String? authToken,
final String? webToken,
final Map<dynamic, dynamic>? releaseInfo,
final String? installPath,
final bool? isDeviceSupportWinHello}) = _$LoginStatusImpl;
@override
int get loginStatus;
@override
String? get nickname;
@override
String? get avatarUrl;
@override
String? get authToken;
@override
String? get webToken;
@override
Map<dynamic, dynamic>? get releaseInfo;
@override
String? get installPath;
@override
bool? get isDeviceSupportWinHello;
@override
@JsonKey(ignore: true)
_$$LoginStatusImplCopyWith<_$LoginStatusImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'home_game_login_dialog_ui_model.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$homeGameLoginUIModelHash() =>
r'c26dfd89985ff9246104135c288b673b7f15acf0';
/// See also [HomeGameLoginUIModel].
@ProviderFor(HomeGameLoginUIModel)
final homeGameLoginUIModelProvider = AutoDisposeNotifierProvider<
HomeGameLoginUIModel, HomeGameLoginState>.internal(
HomeGameLoginUIModel.new,
name: r'homeGameLoginUIModelProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$homeGameLoginUIModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$HomeGameLoginUIModel = AutoDisposeNotifier<HomeGameLoginState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -0,0 +1,55 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Material;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
class HomeMdContentDialogUI extends HookConsumerWidget {
final String title;
final String url;
const HomeMdContentDialogUI(
{super.key, required this.title, required this.url});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Material(
child: ContentDialog(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .6,
),
title: Text(title),
content: LoadingWidget(
onLoadData: _getContent,
childBuilder: (BuildContext context, String data) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(left: 12, right: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: makeMarkdownView(data),
),
),
);
},
),
actions: [
FilledButton(
child: const Padding(
padding: EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
child: Text("关闭"),
),
onPressed: () {
Navigator.pop(context);
})
],
),
);
}
Future<String> _getContent() async {
final r = await RSHttp.getText(url);
return r;
}
}

View File

@ -1,43 +0,0 @@
import 'package:flutter/material.dart' show Material;
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui_model.dart';
class MDContentDialogUI extends BaseUI<MDContentDialogUIModel> {
@override
Widget? buildBody(BuildContext context, MDContentDialogUIModel model) {
return Material(
child: ContentDialog(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .6,
),
title: Text(getUITitle(context, model)),
content: model.data == null
? makeLoading(context)
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(left: 12, right: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: makeMarkdownView(model.data ?? ""),
),
),
),
actions: [
FilledButton(
child: const Padding(
padding: EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
child: Text("关闭"),
),
onPressed: () {
Navigator.pop(context);
})
],
),
);
}
@override
String getUITitle(BuildContext context, MDContentDialogUIModel model) =>
model.title;
}

View File

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

View File

@ -1,17 +1,19 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:file_sizes/file_sizes.dart';
import 'package:intl/intl.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/io/aria2c.dart';
import 'downloader_ui_model.dart';
import 'home_downloader_ui_model.dart';
class DownloaderUI extends BaseUI<DownloaderUIModel> {
final DateFormat formatter = DateFormat('yyyy-MM-dd HH:mm:ss');
class HomeDownloaderUI extends HookConsumerWidget {
const HomeDownloaderUI({super.key});
@override
Widget? buildBody(BuildContext context, DownloaderUIModel model) {
return makeDefaultPage(context, model,
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(homeDownloaderUIModelProvider);
final model = ref.read(homeDownloaderUIModelProvider.notifier);
return makeDefaultPage(context,
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -23,11 +25,11 @@ class DownloaderUI extends BaseUI<DownloaderUIModel> {
const SizedBox(width: 12),
for (final item in <MapEntry<String, IconData>, String>{
const MapEntry("settings", FluentIcons.settings): "限速设置",
if (model.tasks.isNotEmpty)
if (state.tasks.isNotEmpty)
const MapEntry("pause_all", FluentIcons.pause): "全部暂停",
if (model.waitingTasks.isNotEmpty)
if (state.waitingTasks.isNotEmpty)
const MapEntry("resume_all", FluentIcons.download): "恢复全部",
if (model.tasks.isNotEmpty || model.waitingTasks.isNotEmpty)
if (state.tasks.isNotEmpty || state.waitingTasks.isNotEmpty)
const MapEntry("cancel_all", FluentIcons.cancel): "全部取消",
}.entries)
Padding(
@ -43,7 +45,8 @@ class DownloaderUI extends BaseUI<DownloaderUIModel> {
],
),
),
onPressed: () => model.onTapButton(item.key.key)),
onPressed: () =>
model.onTapButton(context, item.key.key)),
),
const SizedBox(width: 12),
],
@ -58,7 +61,7 @@ class DownloaderUI extends BaseUI<DownloaderUIModel> {
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
final (task, type, isFirstType) = model.getTaskAndType(index);
final nt = DownloaderUIModel.getTaskTypeAndName(task);
final nt = HomeDownloaderUIModel.getTaskTypeAndName(task);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -139,7 +142,7 @@ class DownloaderUI extends BaseUI<DownloaderUIModel> {
if (task.status == "active" &&
task.verifiedLength == null)
Text(
"ETA: ${formatter.format(DateTime.now().add(Duration(seconds: model.getETA(task))))}"),
"ETA: ${model.formatter.format(DateTime.now().add(Duration(seconds: model.getETA(task))))}"),
],
),
],
@ -194,7 +197,7 @@ class DownloaderUI extends BaseUI<DownloaderUIModel> {
),
text: const Text('取消下载'),
onPressed: () =>
model.cancelTask(task.gid)),
model.cancelTask(context, task.gid)),
MenuFlyoutItem(
leading: const Icon(
FluentIcons.folder_open,
@ -223,15 +226,13 @@ class DownloaderUI extends BaseUI<DownloaderUIModel> {
width: 8,
height: 8,
decoration: BoxDecoration(
color: Aria2cManager.isAvailable
? Colors.green
: Colors.white,
color: state.isAvailable ? Colors.green : Colors.white,
borderRadius: BorderRadius.circular(1000),
),
),
const SizedBox(width: 12),
Text(
"下载: ${FileSize.getSize(model.globalStat?.downloadSpeed ?? 0)}/s 上传:${FileSize.getSize(model.globalStat?.uploadSpeed ?? 0)}/s",
"下载: ${FileSize.getSize(state.globalStat?.downloadSpeed ?? 0)}/s 上传:${FileSize.getSize(state.globalStat?.uploadSpeed ?? 0)}/s",
style: const TextStyle(fontSize: 12),
)
],
@ -239,9 +240,7 @@ class DownloaderUI extends BaseUI<DownloaderUIModel> {
),
),
],
));
),
useBodyContainer: true);
}
@override
String getUITitle(BuildContext context, DownloaderUIModel model) => "下载管理";
}

View File

@ -1,17 +1,43 @@
// ignore_for_file: avoid_build_context_in_providers
import 'dart:io';
import 'package:aria2/aria2.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:intl/intl.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/io/aria2c.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/utils/provider.dart';
import 'package:starcitizen_doctor/provider/aria2c.dart';
class DownloaderUIModel extends BaseUIModel {
List<Aria2Task> tasks = [];
List<Aria2Task> waitingTasks = [];
List<Aria2Task> stoppedTasks = [];
Aria2GlobalStat? globalStat;
import '../../../widgets/widgets.dart';
part 'home_downloader_ui_model.g.dart';
part 'home_downloader_ui_model.freezed.dart';
@freezed
class HomeDownloaderUIState with _$HomeDownloaderUIState {
const factory HomeDownloaderUIState({
@Default([]) List<Aria2Task> tasks,
@Default([]) List<Aria2Task> waitingTasks,
@Default([]) List<Aria2Task> stoppedTasks,
Aria2GlobalStat? globalStat,
}) = _HomeDownloaderUIState;
}
extension HomeDownloaderUIStateExtension on HomeDownloaderUIState {
bool get isAvailable => globalStat != null;
}
@riverpod
class HomeDownloaderUIModel extends _$HomeDownloaderUIModel {
final DateFormat formatter = DateFormat('yyyy-MM-dd HH:mm:ss');
bool _disposed = false;
final statusMap = {
"active": "下载中...",
@ -29,82 +55,74 @@ class DownloaderUIModel extends BaseUIModel {
};
@override
initModel() {
super.initModel();
HomeDownloaderUIState build() {
state = const HomeDownloaderUIState();
_listenDownloader();
ref.onDispose(() {
_disposed = true;
});
return state;
}
onTapButton(String key) async {
onTapButton(BuildContext context, String key) async {
final aria2cState = ref.read(aria2cModelProvider);
switch (key) {
case "pause_all":
if (!Aria2cManager.isAvailable) return;
await Aria2cManager.getClient().pauseAll();
await Aria2cManager.getClient().saveSession();
if (!aria2cState.isRunning) return;
await aria2cState.aria2c?.pauseAll();
await aria2cState.aria2c?.saveSession();
return;
case "resume_all":
if (!Aria2cManager.isAvailable) return;
await Aria2cManager.getClient().unpauseAll();
await Aria2cManager.getClient().saveSession();
if (!aria2cState.isRunning) return;
await aria2cState.aria2c?.unpauseAll();
await aria2cState.aria2c?.saveSession();
return;
case "cancel_all":
final userOK = await showConfirmDialogs(
context!, "确认取消全部任务?", const Text("如果文件不再需要,你可能需要手动删除下载文件。"));
context, "确认取消全部任务?", const Text("如果文件不再需要,你可能需要手动删除下载文件。"));
if (userOK == true) {
if (!Aria2cManager.isAvailable) return;
if (!aria2cState.isRunning) return;
try {
for (var value in [...tasks, ...waitingTasks]) {
await Aria2cManager.getClient().remove(value.gid!);
for (var value in [...state.tasks, ...state.waitingTasks]) {
await aria2cState.aria2c?.remove(value.gid!);
}
await Aria2cManager.getClient().saveSession();
await aria2cState.aria2c?.saveSession();
} catch (e) {
dPrint("DownloadsUIModel cancel_all Error: $e");
}
}
return;
case "settings":
_showDownloadSpeedSettings();
_showDownloadSpeedSettings(context);
return;
}
}
_listenDownloader() async {
try {
while (true) {
if (!mounted) return;
if (Aria2cManager.isAvailable) {
final aria2c = Aria2cManager.getClient();
tasks.clear();
tasks = await aria2c.tellActive();
waitingTasks = await aria2c.tellWaiting(0, 1000000);
stoppedTasks = await aria2c.tellStopped(0, 1000000);
globalStat = await aria2c.getGlobalStat();
notifyListeners();
}
await Future.delayed(const Duration(seconds: 1));
}
} catch (e) {
dPrint("[DownloadsUIModel]._listenDownloader Error: $e");
}
}
int getTasksLen() {
return tasks.length + waitingTasks.length + stoppedTasks.length;
return state.tasks.length +
state.waitingTasks.length +
state.stoppedTasks.length;
}
(Aria2Task, String, bool) getTaskAndType(int index) {
final tempList = <Aria2Task>[...tasks, ...waitingTasks, ...stoppedTasks];
if (index >= 0 && index < tasks.length) {
final tempList = <Aria2Task>[
...state.tasks,
...state.waitingTasks,
...state.stoppedTasks
];
if (index >= 0 && index < state.tasks.length) {
return (tempList[index], "active", index == 0);
}
if (index >= tasks.length && index < tasks.length + waitingTasks.length) {
return (tempList[index], "waiting", index == tasks.length);
if (index >= state.tasks.length &&
index < state.tasks.length + state.waitingTasks.length) {
return (tempList[index], "waiting", index == state.tasks.length);
}
if (index >= tasks.length + waitingTasks.length &&
if (index >= state.tasks.length + state.waitingTasks.length &&
index < tempList.length) {
return (
tempList[index],
"stopped",
index == tasks.length + waitingTasks.length
index == state.tasks.length + state.waitingTasks.length
);
}
throw Exception("Index out of range or element is null");
@ -126,6 +144,41 @@ class DownloaderUIModel extends BaseUIModel {
}
}
int getETA(Aria2Task task) {
if (task.downloadSpeed == null || task.downloadSpeed == 0) return 0;
final remainingBytes =
(task.totalLength ?? 0) - (task.completedLength ?? 0);
return remainingBytes ~/ (task.downloadSpeed!);
}
Future<void> resumeTask(String? gid) async {
final aria2c = ref.read(aria2cModelProvider).aria2c;
if (gid != null) {
await aria2c?.unpause(gid);
}
}
Future<void> pauseTask(String? gid) async {
final aria2c = ref.read(aria2cModelProvider).aria2c;
if (gid != null) {
await aria2c?.pause(gid);
}
}
Future<void> cancelTask(BuildContext context, String? gid) async {
await Future.delayed(const Duration(milliseconds: 300));
if (gid != null) {
if (!context.mounted) return;
final ok = await showConfirmDialogs(
context, "确认取消下载?", const Text("如果文件不再需要,你可能需要手动删除下载文件。"));
if (ok == true) {
final aria2c = ref.read(aria2cModelProvider).aria2c;
await aria2c?.remove(gid);
await aria2c?.saveSession();
}
}
}
List<Aria2File> getFilesFormTask(Aria2Task task) {
List<Aria2File> l = [];
if (task.files != null) {
@ -137,41 +190,6 @@ class DownloaderUIModel extends BaseUIModel {
return l;
}
int getETA(Aria2Task task) {
if (task.downloadSpeed == null || task.downloadSpeed == 0) return 0;
final remainingBytes =
(task.totalLength ?? 0) - (task.completedLength ?? 0);
return remainingBytes ~/ (task.downloadSpeed!);
}
Future<void> resumeTask(String? gid) async {
final aria2c = Aria2cManager.getClient();
if (gid != null) {
await aria2c.unpause(gid);
}
}
Future<void> pauseTask(String? gid) async {
final aria2c = Aria2cManager.getClient();
if (gid != null) {
await aria2c.pause(gid);
}
}
Future<void> cancelTask(String? gid) async {
await Future.delayed(const Duration(milliseconds: 300));
if (gid != null) {
final ok = await showConfirmDialogs(
context!, "确认取消下载?", const Text("如果文件不再需要,你可能需要手动删除下载文件。"));
if (ok == true) {
final aria2c = Aria2cManager.getClient();
await aria2c.remove(gid);
await Aria2cManager.getClient().saveSession();
}
}
}
openFolder(Aria2Task task) {
final f = getFilesFormTask(task).firstOrNull;
if (f != null) {
@ -179,7 +197,39 @@ class DownloaderUIModel extends BaseUIModel {
}
}
Future<void> _showDownloadSpeedSettings() async {
_listenDownloader() async {
try {
while (true) {
final aria2cState = ref.read(aria2cModelProvider);
if (_disposed) return;
if (aria2cState.isRunning) {
final aria2c = aria2cState.aria2c!;
final tasks = await aria2c.tellActive();
final waitingTasks = await aria2c.tellWaiting(0, 1000000);
final stoppedTasks = await aria2c.tellStopped(0, 1000000);
final globalStat = await aria2c.getGlobalStat();
state = state.copyWith(
tasks: tasks,
waitingTasks: waitingTasks,
stoppedTasks: stoppedTasks,
globalStat: globalStat,
);
} else {
state = state.copyWith(
tasks: [],
waitingTasks: [],
stoppedTasks: [],
globalStat: null,
);
}
await Future.delayed(const Duration(seconds: 1));
}
} catch (e) {
dPrint("[DownloadsUIModel]._listenDownloader Error: $e");
}
}
Future<void> _showDownloadSpeedSettings(BuildContext context) async {
final box = await Hive.openBox("app_conf");
final upCtrl = TextEditingController(
@ -189,8 +239,9 @@ class DownloaderUIModel extends BaseUIModel {
final ifr = FilteringTextInputFormatter.allow(RegExp(r'^\d*[km]?$'));
if (!context.mounted) return;
final ok = await showConfirmDialogs(
context!,
context,
"限速设置",
Column(
mainAxisSize: MainAxisSize.min,
@ -234,17 +285,21 @@ class DownloaderUIModel extends BaseUIModel {
],
));
if (ok == true) {
await handleError(() => Aria2cManager.launchDaemon());
final aria2c = Aria2cManager.getClient();
final upByte = Aria2cManager.textToByte(upCtrl.text.trim());
final downByte = Aria2cManager.textToByte(downCtrl.text.trim());
final r = await handleError(() => aria2c.changeGlobalOption(Aria2Option()
final aria2cState = ref.read(aria2cModelProvider);
final aria2cModel = ref.read(aria2cModelProvider.notifier);
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));
..maxOverallDownloadLimit = downByte)
.unwrap();
if (r != null) {
await box.put('downloader_up_limit', upCtrl.text.trim());
await box.put('downloader_down_limit', downCtrl.text.trim());
notifyListeners();
}
}
}

View File

@ -0,0 +1,232 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'home_downloader_ui_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$HomeDownloaderUIState {
List<Aria2Task> get tasks => throw _privateConstructorUsedError;
List<Aria2Task> get waitingTasks => throw _privateConstructorUsedError;
List<Aria2Task> get stoppedTasks => throw _privateConstructorUsedError;
Aria2GlobalStat? get globalStat => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HomeDownloaderUIStateCopyWith<HomeDownloaderUIState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $HomeDownloaderUIStateCopyWith<$Res> {
factory $HomeDownloaderUIStateCopyWith(HomeDownloaderUIState value,
$Res Function(HomeDownloaderUIState) then) =
_$HomeDownloaderUIStateCopyWithImpl<$Res, HomeDownloaderUIState>;
@useResult
$Res call(
{List<Aria2Task> tasks,
List<Aria2Task> waitingTasks,
List<Aria2Task> stoppedTasks,
Aria2GlobalStat? globalStat});
}
/// @nodoc
class _$HomeDownloaderUIStateCopyWithImpl<$Res,
$Val extends HomeDownloaderUIState>
implements $HomeDownloaderUIStateCopyWith<$Res> {
_$HomeDownloaderUIStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? tasks = null,
Object? waitingTasks = null,
Object? stoppedTasks = null,
Object? globalStat = freezed,
}) {
return _then(_value.copyWith(
tasks: null == tasks
? _value.tasks
: tasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,
waitingTasks: null == waitingTasks
? _value.waitingTasks
: waitingTasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,
stoppedTasks: null == stoppedTasks
? _value.stoppedTasks
: stoppedTasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,
globalStat: freezed == globalStat
? _value.globalStat
: globalStat // ignore: cast_nullable_to_non_nullable
as Aria2GlobalStat?,
) as $Val);
}
}
/// @nodoc
abstract class _$$HomeDownloaderUIStateImplCopyWith<$Res>
implements $HomeDownloaderUIStateCopyWith<$Res> {
factory _$$HomeDownloaderUIStateImplCopyWith(
_$HomeDownloaderUIStateImpl value,
$Res Function(_$HomeDownloaderUIStateImpl) then) =
__$$HomeDownloaderUIStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<Aria2Task> tasks,
List<Aria2Task> waitingTasks,
List<Aria2Task> stoppedTasks,
Aria2GlobalStat? globalStat});
}
/// @nodoc
class __$$HomeDownloaderUIStateImplCopyWithImpl<$Res>
extends _$HomeDownloaderUIStateCopyWithImpl<$Res,
_$HomeDownloaderUIStateImpl>
implements _$$HomeDownloaderUIStateImplCopyWith<$Res> {
__$$HomeDownloaderUIStateImplCopyWithImpl(_$HomeDownloaderUIStateImpl _value,
$Res Function(_$HomeDownloaderUIStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? tasks = null,
Object? waitingTasks = null,
Object? stoppedTasks = null,
Object? globalStat = freezed,
}) {
return _then(_$HomeDownloaderUIStateImpl(
tasks: null == tasks
? _value._tasks
: tasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,
waitingTasks: null == waitingTasks
? _value._waitingTasks
: waitingTasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,
stoppedTasks: null == stoppedTasks
? _value._stoppedTasks
: stoppedTasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,
globalStat: freezed == globalStat
? _value.globalStat
: globalStat // ignore: cast_nullable_to_non_nullable
as Aria2GlobalStat?,
));
}
}
/// @nodoc
class _$HomeDownloaderUIStateImpl implements _HomeDownloaderUIState {
const _$HomeDownloaderUIStateImpl(
{final List<Aria2Task> tasks = const [],
final List<Aria2Task> waitingTasks = const [],
final List<Aria2Task> stoppedTasks = const [],
this.globalStat})
: _tasks = tasks,
_waitingTasks = waitingTasks,
_stoppedTasks = stoppedTasks;
final List<Aria2Task> _tasks;
@override
@JsonKey()
List<Aria2Task> get tasks {
if (_tasks is EqualUnmodifiableListView) return _tasks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_tasks);
}
final List<Aria2Task> _waitingTasks;
@override
@JsonKey()
List<Aria2Task> get waitingTasks {
if (_waitingTasks is EqualUnmodifiableListView) return _waitingTasks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_waitingTasks);
}
final List<Aria2Task> _stoppedTasks;
@override
@JsonKey()
List<Aria2Task> get stoppedTasks {
if (_stoppedTasks is EqualUnmodifiableListView) return _stoppedTasks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_stoppedTasks);
}
@override
final Aria2GlobalStat? globalStat;
@override
String toString() {
return 'HomeDownloaderUIState(tasks: $tasks, waitingTasks: $waitingTasks, stoppedTasks: $stoppedTasks, globalStat: $globalStat)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HomeDownloaderUIStateImpl &&
const DeepCollectionEquality().equals(other._tasks, _tasks) &&
const DeepCollectionEquality()
.equals(other._waitingTasks, _waitingTasks) &&
const DeepCollectionEquality()
.equals(other._stoppedTasks, _stoppedTasks) &&
(identical(other.globalStat, globalStat) ||
other.globalStat == globalStat));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_tasks),
const DeepCollectionEquality().hash(_waitingTasks),
const DeepCollectionEquality().hash(_stoppedTasks),
globalStat);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$HomeDownloaderUIStateImplCopyWith<_$HomeDownloaderUIStateImpl>
get copyWith => __$$HomeDownloaderUIStateImplCopyWithImpl<
_$HomeDownloaderUIStateImpl>(this, _$identity);
}
abstract class _HomeDownloaderUIState implements HomeDownloaderUIState {
const factory _HomeDownloaderUIState(
{final List<Aria2Task> tasks,
final List<Aria2Task> waitingTasks,
final List<Aria2Task> stoppedTasks,
final Aria2GlobalStat? globalStat}) = _$HomeDownloaderUIStateImpl;
@override
List<Aria2Task> get tasks;
@override
List<Aria2Task> get waitingTasks;
@override
List<Aria2Task> get stoppedTasks;
@override
Aria2GlobalStat? get globalStat;
@override
@JsonKey(ignore: true)
_$$HomeDownloaderUIStateImplCopyWith<_$HomeDownloaderUIStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'home_downloader_ui_model.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$homeDownloaderUIModelHash() =>
r'947ebb9abb262aea6121c74481753da0eebb9a79';
/// See also [HomeDownloaderUIModel].
@ProviderFor(HomeDownloaderUIModel)
final homeDownloaderUIModelProvider = AutoDisposeNotifierProvider<
HomeDownloaderUIModel, HomeDownloaderUIState>.internal(
HomeDownloaderUIModel.new,
name: r'homeDownloaderUIModelProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$homeDownloaderUIModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$HomeDownloaderUIModel = AutoDisposeNotifier<HomeDownloaderUIState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -1,13 +1,37 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'game_doctor_ui_model.dart';
class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
class HomeGameDoctorUI extends HookConsumerWidget {
const HomeGameDoctorUI({super.key});
@override
Widget? buildBody(BuildContext context, GameDoctorUIModel model) {
return makeDefaultPage(context, model,
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(homeGameDoctorUIModelProvider);
final homeState = ref.watch(homeUIModelProvider);
final model = ref.read(homeGameDoctorUIModelProvider.notifier);
useEffect(() {
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
dPrint("HomeGameDoctorUI useEffect doCheck timeStamp === $timeStamp");
model.doCheck(context);
});
return null;
}, const []);
return makeDefaultPage(context,
title: "一键诊断 -> ${homeState.scInstalledPath}",
useBodyContainer: true,
content: Stack(
children: [
Column(
@ -33,11 +57,12 @@ class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
],
),
),
onPressed: () => model.onTapButton(item.key)),
onPressed: () =>
_onTapButton(context, item.key, homeState)),
),
],
),
if (model.isChecking)
if (state.isChecking)
Expanded(
child: Center(
child: Column(
@ -45,12 +70,12 @@ class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
children: [
const ProgressRing(),
const SizedBox(height: 12),
Text(model.lastScreenInfo)
Text(state.lastScreenInfo)
],
),
))
else if (model.checkResult == null ||
model.checkResult!.isEmpty) ...[
else if (state.checkResult == null ||
state.checkResult!.isEmpty) ...[
const Expanded(
child: Center(
child: Column(
@ -63,10 +88,10 @@ class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
),
))
] else
...makeResult(context, model),
...makeResult(context, state, model),
],
),
if (model.isFixing)
if (state.isFixing)
Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(150),
@ -77,8 +102,8 @@ class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
children: [
const ProgressRing(),
const SizedBox(height: 12),
Text(model.isFixingString.isNotEmpty
? model.isFixingString
Text(state.isFixingString.isNotEmpty
? state.isFixingString
: "正在处理..."),
],
),
@ -93,29 +118,6 @@ class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
));
}
List<Widget> makeResult(BuildContext context, GameDoctorUIModel model) {
return [
const SizedBox(height: 24),
Text(model.lastScreenInfo, maxLines: 1),
const SizedBox(height: 12),
Text(
"注意:本工具检测结果仅供参考,若您不理解以下操作,请提供截图给有经验的玩家!",
style: TextStyle(color: Colors.red, fontSize: 16),
),
const SizedBox(height: 24),
ListView.builder(
itemCount: model.checkResult!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final item = model.checkResult![index];
return makeResultItem(item, model);
},
),
const SizedBox(height: 64),
];
}
Widget makeRescueBanner(BuildContext context) {
return GestureDetector(
onTap: () async {
@ -146,8 +148,32 @@ class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
);
}
Widget makeResultItem(
MapEntry<String, String> item, GameDoctorUIModel model) {
List<Widget> makeResult(BuildContext context, HomeGameDoctorState state,
HomeGameDoctorUIModel model) {
return [
const SizedBox(height: 24),
Text(state.lastScreenInfo, maxLines: 1),
const SizedBox(height: 12),
Text(
"注意:本工具检测结果仅供参考,若您不理解以下操作,请提供截图给有经验的玩家!",
style: TextStyle(color: Colors.red, fontSize: 16),
),
const SizedBox(height: 24),
ListView.builder(
itemCount: state.checkResult!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final item = state.checkResult![index];
return makeResultItem(context, item, state, model);
},
),
const SizedBox(height: 64),
];
}
Widget makeResultItem(BuildContext context, MapEntry<String, String> item,
HomeGameDoctorState state, HomeGameDoctorUIModel model) {
final errorNames = {
"unSupport_system":
MapEntry("不支持的操作系统,游戏可能无法运行", "请升级您的系统 (${item.value})"),
@ -193,12 +219,10 @@ class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
),
),
trailing: Button(
onPressed: (errorNames[item.key]?.value == null || model.isFixing)
onPressed: (errorNames[item.key]?.value == null || state.isFixing)
? null
: () async {
await model.doFix(item);
model.isFixing = false;
model.notifyListeners();
await model.doFix(context, item);
},
child: const Padding(
padding: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
@ -249,7 +273,22 @@ class GameDoctorUI extends BaseUI<GameDoctorUIModel> {
);
}
@override
String getUITitle(BuildContext context, GameDoctorUIModel model) =>
"一键诊断 > ${model.scInstalledPath}";
_onTapButton(
BuildContext context, String key, HomeUIModelState homeState) async {
switch (key) {
case "rsi_log":
final path = await SCLoggerHelper.getLogFilePath();
if (path == null) return;
SystemHelper.openDir(path);
return;
case "game_log":
if (homeState.scInstalledPath == "not_install" ||
homeState.scInstalledPath == null) {
showToast(context, "请在首页选择游戏安装目录。");
return;
}
SystemHelper.openDir("${homeState.scInstalledPath}\\Game.log");
return;
}
}
}

View File

@ -1,99 +1,187 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:url_launcher/url_launcher_string.dart';
class GameDoctorUIModel extends BaseUIModel {
String scInstalledPath = "";
part 'game_doctor_ui_model.g.dart';
GameDoctorUIModel(this.scInstalledPath);
part 'game_doctor_ui_model.freezed.dart';
String _lastScreenInfo = "";
String get lastScreenInfo => _lastScreenInfo;
List<MapEntry<String, String>>? checkResult;
set lastScreenInfo(String info) {
_lastScreenInfo = info;
notifyListeners();
@freezed
class HomeGameDoctorState with _$HomeGameDoctorState {
const factory HomeGameDoctorState({
@Default(false) bool isChecking,
@Default(false) bool isFixing,
@Default("") String lastScreenInfo,
@Default("") String isFixingString,
List<MapEntry<String, String>>? checkResult,
}) = _HomeGameDoctorState;
}
bool isChecking = false;
bool isFixing = false;
String isFixingString = "";
final cnExp = RegExp(r"[^\x00-\xff]");
@riverpod
class HomeGameDoctorUIModel extends _$HomeGameDoctorUIModel {
@override
void initModel() {
doCheck()?.call();
super.initModel();
HomeGameDoctorState build() {
state = const HomeGameDoctorState();
return state;
}
VoidCallback? doCheck() {
if (isChecking) return null;
return () async {
isChecking = true;
lastScreenInfo = "正在分析...";
await _statCheck();
isChecking = false;
notifyListeners();
};
Future<void> doFix(
// ignore: avoid_build_context_in_providers
BuildContext context,
MapEntry<String, String> item) async {
final checkResult =
List<MapEntry<String, String>>.from(state.checkResult ?? []);
state = state.copyWith(isFixing: true, isFixingString: "");
switch (item.key) {
case "unSupport_system":
showToast(context, "若您的硬件达标,请尝试安装最新的 Windows 系统。");
break;
case "no_live_path":
try {
await Directory(item.value).create(recursive: true);
if (!context.mounted) break;
showToast(context, "创建文件夹成功,请尝试继续下载游戏!");
checkResult.remove(item);
state = state.copyWith(checkResult: checkResult);
} catch (e) {
showToast(context, "创建文件夹失败,请尝试手动创建。\n目录:${item.value} \n错误:$e");
}
break;
case "nvme_PhysicalBytes":
final r = await SystemHelper.addNvmePatch();
if (r == "") {
if (!context.mounted) break;
showToast(context,
"修复成功,请尝试重启后继续安装游戏! 若注册表修改操作导致其他软件出现兼容问题,请使用 工具 中的 NVME 注册表清理。");
checkResult.remove(item);
state = state.copyWith(checkResult: checkResult);
} else {
if (!context.mounted) break;
showToast(context, "修复失败,$r");
}
break;
case "eac_file_miss":
showToast(
context, "未在 LIVE 文件夹找到 EasyAntiCheat 文件 或 文件不完整,请使用 RSI 启动器校验文件");
break;
case "eac_not_install":
final eacJsonPath = "${item.value}\\Settings.json";
final eacJsonData = await File(eacJsonPath).readAsBytes();
final Map eacJson = json.decode(utf8.decode(eacJsonData));
final eacID = eacJson["productid"];
try {
var result = await Process.run(
"${item.value}\\EasyAntiCheat_EOS_Setup.exe", ["install", eacID]);
dPrint("${item.value}\\EasyAntiCheat_EOS_Setup.exe install $eacID");
if (result.stderr == "") {
if (!context.mounted) break;
showToast(context, "修复成功,请尝试启动游戏。(若问题无法解决,请使用工具箱的 《重装 EAC》");
checkResult.remove(item);
state = state.copyWith(checkResult: checkResult);
} else {
if (!context.mounted) break;
showToast(context, "修复失败,${result.stderr}");
}
} catch (e) {
if (!context.mounted) break;
showToast(context, "修复失败,$e");
}
break;
case "cn_user_name":
showToast(context, "即将跳转,教程来自互联网,请谨慎操作...");
await Future.delayed(const Duration(milliseconds: 300));
launchUrlString(
"https://btfy.eu.org/?q=5L+u5pS5d2luZG93c+eUqOaIt+WQjeS7juS4reaWh+WIsOiLseaWhw==");
break;
default:
showToast(context, "该问题暂不支持自动处理,请提供截图寻求帮助");
break;
}
state = state.copyWith(isFixing: false, isFixingString: "");
}
Future _statCheck() async {
checkResult = [];
// ignore: avoid_build_context_in_providers
doCheck(BuildContext context) async {
if (state.isChecking) return;
state = state.copyWith(isChecking: true, lastScreenInfo: "正在分析...");
dPrint("-------- start docker check -----");
if (!context.mounted) return;
await _statCheck(context);
state = state.copyWith(isChecking: false);
}
// ignore: avoid_build_context_in_providers
_statCheck(BuildContext context) async {
final homeState = ref.read(homeUIModelProvider);
final scInstalledPath = homeState.scInstalledPath!;
final checkResult = <MapEntry<String, String>>[];
// TODO for debug
// checkResult?.add(const MapEntry("unSupport_system", "android"));
// checkResult?.add(const MapEntry("nvme_PhysicalBytes", "C"));
// checkResult?.add(const MapEntry("no_live_path", ""));
await _checkPreInstall();
await _checkEAC();
await _checkGameRunningLog();
await _checkPreInstall(context, scInstalledPath, checkResult);
if (!context.mounted) return;
await _checkEAC(context, scInstalledPath, checkResult);
if (!context.mounted) return;
await _checkGameRunningLog(context, scInstalledPath, checkResult);
if (checkResult!.isEmpty) {
checkResult = null;
lastScreenInfo = "分析完毕,没有发现问题";
if (checkResult.isEmpty) {
const lastScreenInfo = "分析完毕,没有发现问题";
state = state.copyWith(checkResult: null, lastScreenInfo: lastScreenInfo);
} else {
lastScreenInfo = "分析完毕,发现 ${checkResult!.length} 个问题";
final lastScreenInfo = "分析完毕,发现 ${checkResult.length} 个问题";
state = state.copyWith(
checkResult: checkResult, lastScreenInfo: lastScreenInfo);
}
if (scInstalledPath == "not_install" && (checkResult?.isEmpty ?? true)) {
showToast(context!, "扫描完毕,没有发现问题,若仍然安装失败,请尝试使用工具箱中的 RSI启动器管理员模式。");
if (scInstalledPath == "not_install" && (checkResult.isEmpty)) {
if (!context.mounted) return;
showToast(context, "扫描完毕,没有发现问题,若仍然安装失败,请尝试使用工具箱中的 RSI启动器管理员模式。");
}
}
Future _checkGameRunningLog() async {
// ignore: avoid_build_context_in_providers
Future _checkGameRunningLog(BuildContext context, String scInstalledPath,
List<MapEntry<String, String>> checkResult) async {
if (scInstalledPath == "not_install") return;
lastScreenInfo = "正在检查Game.log";
const lastScreenInfo = "正在检查Game.log";
state = state.copyWith(lastScreenInfo: lastScreenInfo);
final logs = await SCLoggerHelper.getGameRunningLogs(scInstalledPath);
if (logs == null) return;
final info = SCLoggerHelper.getGameRunningLogInfo(logs);
if (info != null) {
if (info.key != "_") {
checkResult?.add(MapEntry("游戏异常退出:${info.key}", info.value));
checkResult.add(MapEntry("游戏异常退出:${info.key}", info.value));
} else {
checkResult
?.add(MapEntry("游戏异常退出:未知异常", "info:${info.value},请点击右下角加群反馈。"));
.add(MapEntry("游戏异常退出:未知异常", "info:${info.value},请点击右下角加群反馈。"));
}
}
}
Future _checkEAC() async {
// ignore: avoid_build_context_in_providers
Future _checkEAC(BuildContext context, String scInstalledPath,
List<MapEntry<String, String>> checkResult) async {
if (scInstalledPath == "not_install") return;
lastScreenInfo = "正在检查EAC";
const lastScreenInfo = "正在检查EAC";
state = state.copyWith(lastScreenInfo: lastScreenInfo);
final eacPath = "$scInstalledPath\\EasyAntiCheat";
final eacJsonPath = "$eacPath\\Settings.json";
if (!await Directory(eacPath).exists() ||
!await File(eacJsonPath).exists()) {
checkResult?.add(const MapEntry("eac_file_miss", ""));
checkResult.add(const MapEntry("eac_file_miss", ""));
return;
}
final eacJsonData = await File(eacJsonPath).readAsBytes();
@ -101,38 +189,44 @@ class GameDoctorUIModel extends BaseUIModel {
final eacID = eacJson["productid"];
final eacDeploymentId = eacJson["deploymentid"];
if (eacID == null || eacDeploymentId == null) {
checkResult?.add(const MapEntry("eac_file_miss", ""));
checkResult.add(const MapEntry("eac_file_miss", ""));
return;
}
final eacFilePath =
"${Platform.environment["appdata"]}\\EasyAntiCheat\\$eacID\\$eacDeploymentId\\anticheatlauncher.log";
if (!await File(eacFilePath).exists()) {
checkResult?.add(MapEntry("eac_not_install", eacPath));
checkResult.add(MapEntry("eac_not_install", eacPath));
return;
}
}
Future _checkPreInstall() async {
lastScreenInfo = "正在检查:运行环境";
final _cnExp = RegExp(r"[^\x00-\xff]");
// ignore: avoid_build_context_in_providers
Future _checkPreInstall(BuildContext context, String scInstalledPath,
List<MapEntry<String, String>> checkResult) async {
const lastScreenInfo = "正在检查:运行环境";
state = state.copyWith(lastScreenInfo: lastScreenInfo);
if (!(Platform.operatingSystemVersion.contains("Windows 10") ||
Platform.operatingSystemVersion.contains("Windows 11"))) {
checkResult
?.add(MapEntry("unSupport_system", Platform.operatingSystemVersion));
lastScreenInfo = "不支持的操作系统:${Platform.operatingSystemVersion}";
await showToast(context!, lastScreenInfo);
.add(MapEntry("unSupport_system", Platform.operatingSystemVersion));
final lastScreenInfo = "不支持的操作系统:${Platform.operatingSystemVersion}";
state = state.copyWith(lastScreenInfo: lastScreenInfo);
await showToast(context, lastScreenInfo);
}
if (cnExp.hasMatch(await SCLoggerHelper.getLogFilePath() ?? "")) {
checkResult?.add(const MapEntry("cn_user_name", ""));
if (_cnExp.hasMatch(await SCLoggerHelper.getLogFilePath() ?? "")) {
checkResult.add(const MapEntry("cn_user_name", ""));
}
// RAM
final ramSize = await SystemHelper.getSystemMemorySizeGB();
if (ramSize < 16) {
checkResult?.add(MapEntry("low_ram", "$ramSize"));
checkResult.add(MapEntry("low_ram", "$ramSize"));
}
lastScreenInfo = "正在检查:安装信息";
state = state.copyWith(lastScreenInfo: "正在检查:安装信息");
//
try {
final listData = await SCLoggerHelper.getGameInstallPath(
@ -141,13 +235,13 @@ class GameDoctorUIModel extends BaseUIModel {
final checkedPath = [];
for (var installPath in listData) {
if (!checkedPath.contains(installPath)) {
if (cnExp.hasMatch(installPath)) {
checkResult?.add(MapEntry("cn_install_path", installPath));
if (_cnExp.hasMatch(installPath)) {
checkResult.add(MapEntry("cn_install_path", installPath));
}
if (scInstalledPath == "not_install") {
checkedPath.add(installPath);
if (!await Directory(installPath).exists()) {
checkResult?.add(MapEntry("no_live_path", installPath));
checkResult.add(MapEntry("no_live_path", installPath));
}
}
final tp = installPath.split(":")[0];
@ -168,7 +262,7 @@ class GameDoctorUIModel extends BaseUIModel {
final rs = result.stdout.toString().trim();
final physicalBytesPerSectorForPerformance = (int.tryParse(rs) ?? 0);
if (physicalBytesPerSectorForPerformance > 4096) {
checkResult?.add(MapEntry("nvme_PhysicalBytes", element));
checkResult.add(MapEntry("nvme_PhysicalBytes", element));
}
}
}
@ -176,85 +270,4 @@ class GameDoctorUIModel extends BaseUIModel {
dPrint(e);
}
}
Future<void> doFix(MapEntry<String, String> item) async {
isFixing = true;
notifyListeners();
switch (item.key) {
case "unSupport_system":
showToast(context!, "若您的硬件达标,请尝试安装最新的 Windows 系统。");
return;
case "no_live_path":
try {
await Directory(item.value).create(recursive: true);
showToast(context!, "创建文件夹成功,请尝试继续下载游戏!");
checkResult?.remove(item);
notifyListeners();
} catch (e) {
showToast(context!, "创建文件夹失败,请尝试手动创建。\n目录:${item.value} \n错误:$e");
}
return;
case "nvme_PhysicalBytes":
final r = await SystemHelper.addNvmePatch();
if (r == "") {
showToast(context!,
"修复成功,请尝试重启后继续安装游戏! 若注册表修改操作导致其他软件出现兼容问题,请使用 工具 中的 NVME 注册表清理。");
checkResult?.remove(item);
notifyListeners();
} else {
showToast(context!, "修复失败,$r");
}
return;
case "eac_file_miss":
showToast(
context!, "未在 LIVE 文件夹找到 EasyAntiCheat 文件 或 文件不完整,请使用 RSI 启动器校验文件");
return;
case "eac_not_install":
final eacJsonPath = "${item.value}\\Settings.json";
final eacJsonData = await File(eacJsonPath).readAsBytes();
final Map eacJson = json.decode(utf8.decode(eacJsonData));
final eacID = eacJson["productid"];
try {
var result = await Process.run(
"${item.value}\\EasyAntiCheat_EOS_Setup.exe", ["install", eacID]);
dPrint("${item.value}\\EasyAntiCheat_EOS_Setup.exe install $eacID");
if (result.stderr == "") {
showToast(context!, "修复成功,请尝试启动游戏。(若问题无法解决,请使用工具箱的 《重装 EAC》");
checkResult?.remove(item);
notifyListeners();
} else {
showToast(context!, "修复失败,${result.stderr}");
}
} catch (e) {
showToast(context!, "修复失败,$e");
}
return;
case "cn_user_name":
showToast(context!, "即将跳转,教程来自互联网,请谨慎操作...");
await Future.delayed(const Duration(milliseconds: 300));
launchUrlString(
"https://btfy.eu.org/?q=5L+u5pS5d2luZG93c+eUqOaIt+WQjeS7juS4reaWh+WIsOiLseaWhw==");
return;
default:
showToast(context!, "该问题暂不支持自动处理,请提供截图寻求帮助");
return;
}
}
onTapButton(String key) async {
switch (key) {
case "rsi_log":
final path = await SCLoggerHelper.getLogFilePath();
if (path == null) return;
SystemHelper.openDir(path);
return;
case "game_log":
if (scInstalledPath == "not_install") {
showToast(context!, "请在首页选择游戏安装目录。");
return;
}
SystemHelper.openDir("$scInstalledPath\\Game.log");
return;
}
}
}

View File

@ -0,0 +1,242 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'game_doctor_ui_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$HomeGameDoctorState {
bool get isChecking => throw _privateConstructorUsedError;
bool get isFixing => throw _privateConstructorUsedError;
String get lastScreenInfo => throw _privateConstructorUsedError;
String get isFixingString => throw _privateConstructorUsedError;
List<MapEntry<String, String>>? get checkResult =>
throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HomeGameDoctorStateCopyWith<HomeGameDoctorState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $HomeGameDoctorStateCopyWith<$Res> {
factory $HomeGameDoctorStateCopyWith(
HomeGameDoctorState value, $Res Function(HomeGameDoctorState) then) =
_$HomeGameDoctorStateCopyWithImpl<$Res, HomeGameDoctorState>;
@useResult
$Res call(
{bool isChecking,
bool isFixing,
String lastScreenInfo,
String isFixingString,
List<MapEntry<String, String>>? checkResult});
}
/// @nodoc
class _$HomeGameDoctorStateCopyWithImpl<$Res, $Val extends HomeGameDoctorState>
implements $HomeGameDoctorStateCopyWith<$Res> {
_$HomeGameDoctorStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isChecking = null,
Object? isFixing = null,
Object? lastScreenInfo = null,
Object? isFixingString = null,
Object? checkResult = freezed,
}) {
return _then(_value.copyWith(
isChecking: null == isChecking
? _value.isChecking
: isChecking // ignore: cast_nullable_to_non_nullable
as bool,
isFixing: null == isFixing
? _value.isFixing
: isFixing // ignore: cast_nullable_to_non_nullable
as bool,
lastScreenInfo: null == lastScreenInfo
? _value.lastScreenInfo
: lastScreenInfo // ignore: cast_nullable_to_non_nullable
as String,
isFixingString: null == isFixingString
? _value.isFixingString
: isFixingString // ignore: cast_nullable_to_non_nullable
as String,
checkResult: freezed == checkResult
? _value.checkResult
: checkResult // ignore: cast_nullable_to_non_nullable
as List<MapEntry<String, String>>?,
) as $Val);
}
}
/// @nodoc
abstract class _$$HomeGameDoctorStateImplCopyWith<$Res>
implements $HomeGameDoctorStateCopyWith<$Res> {
factory _$$HomeGameDoctorStateImplCopyWith(_$HomeGameDoctorStateImpl value,
$Res Function(_$HomeGameDoctorStateImpl) then) =
__$$HomeGameDoctorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool isChecking,
bool isFixing,
String lastScreenInfo,
String isFixingString,
List<MapEntry<String, String>>? checkResult});
}
/// @nodoc
class __$$HomeGameDoctorStateImplCopyWithImpl<$Res>
extends _$HomeGameDoctorStateCopyWithImpl<$Res, _$HomeGameDoctorStateImpl>
implements _$$HomeGameDoctorStateImplCopyWith<$Res> {
__$$HomeGameDoctorStateImplCopyWithImpl(_$HomeGameDoctorStateImpl _value,
$Res Function(_$HomeGameDoctorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isChecking = null,
Object? isFixing = null,
Object? lastScreenInfo = null,
Object? isFixingString = null,
Object? checkResult = freezed,
}) {
return _then(_$HomeGameDoctorStateImpl(
isChecking: null == isChecking
? _value.isChecking
: isChecking // ignore: cast_nullable_to_non_nullable
as bool,
isFixing: null == isFixing
? _value.isFixing
: isFixing // ignore: cast_nullable_to_non_nullable
as bool,
lastScreenInfo: null == lastScreenInfo
? _value.lastScreenInfo
: lastScreenInfo // ignore: cast_nullable_to_non_nullable
as String,
isFixingString: null == isFixingString
? _value.isFixingString
: isFixingString // ignore: cast_nullable_to_non_nullable
as String,
checkResult: freezed == checkResult
? _value._checkResult
: checkResult // ignore: cast_nullable_to_non_nullable
as List<MapEntry<String, String>>?,
));
}
}
/// @nodoc
class _$HomeGameDoctorStateImpl implements _HomeGameDoctorState {
const _$HomeGameDoctorStateImpl(
{this.isChecking = false,
this.isFixing = false,
this.lastScreenInfo = "",
this.isFixingString = "",
final List<MapEntry<String, String>>? checkResult})
: _checkResult = checkResult;
@override
@JsonKey()
final bool isChecking;
@override
@JsonKey()
final bool isFixing;
@override
@JsonKey()
final String lastScreenInfo;
@override
@JsonKey()
final String isFixingString;
final List<MapEntry<String, String>>? _checkResult;
@override
List<MapEntry<String, String>>? get checkResult {
final value = _checkResult;
if (value == null) return null;
if (_checkResult is EqualUnmodifiableListView) return _checkResult;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
String toString() {
return 'HomeGameDoctorState(isChecking: $isChecking, isFixing: $isFixing, lastScreenInfo: $lastScreenInfo, isFixingString: $isFixingString, checkResult: $checkResult)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HomeGameDoctorStateImpl &&
(identical(other.isChecking, isChecking) ||
other.isChecking == isChecking) &&
(identical(other.isFixing, isFixing) ||
other.isFixing == isFixing) &&
(identical(other.lastScreenInfo, lastScreenInfo) ||
other.lastScreenInfo == lastScreenInfo) &&
(identical(other.isFixingString, isFixingString) ||
other.isFixingString == isFixingString) &&
const DeepCollectionEquality()
.equals(other._checkResult, _checkResult));
}
@override
int get hashCode => Object.hash(
runtimeType,
isChecking,
isFixing,
lastScreenInfo,
isFixingString,
const DeepCollectionEquality().hash(_checkResult));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$HomeGameDoctorStateImplCopyWith<_$HomeGameDoctorStateImpl> get copyWith =>
__$$HomeGameDoctorStateImplCopyWithImpl<_$HomeGameDoctorStateImpl>(
this, _$identity);
}
abstract class _HomeGameDoctorState implements HomeGameDoctorState {
const factory _HomeGameDoctorState(
{final bool isChecking,
final bool isFixing,
final String lastScreenInfo,
final String isFixingString,
final List<MapEntry<String, String>>? checkResult}) =
_$HomeGameDoctorStateImpl;
@override
bool get isChecking;
@override
bool get isFixing;
@override
String get lastScreenInfo;
@override
String get isFixingString;
@override
List<MapEntry<String, String>>? get checkResult;
@override
@JsonKey(ignore: true)
_$$HomeGameDoctorStateImplCopyWith<_$HomeGameDoctorStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'game_doctor_ui_model.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$homeGameDoctorUIModelHash() =>
r'1e32d75095de065cf2cdedf444f74ffc753ce66f';
/// See also [HomeGameDoctorUIModel].
@ProviderFor(HomeGameDoctorUIModel)
final homeGameDoctorUIModelProvider = AutoDisposeNotifierProvider<
HomeGameDoctorUIModel, HomeGameDoctorState>.internal(
HomeGameDoctorUIModel.new,
name: r'homeGameDoctorUIModelProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$homeGameDoctorUIModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$HomeGameDoctorUIModel = AutoDisposeNotifier<HomeGameDoctorState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -1,19 +1,28 @@
import 'package:card_swiper/card_swiper.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/widgets/cache_image.dart';
import 'package:starcitizen_doctor/widgets/countdown_time_text.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'dialogs/home_countdown_dialog_ui.dart';
import 'dialogs/home_md_content_dialog_ui.dart';
import 'home_ui_model.dart';
import 'localization/localization_dialog_ui.dart';
class HomeUI extends HookConsumerWidget {
const HomeUI({super.key});
class HomeUI extends BaseUI<HomeUIModel> {
@override
Widget? buildBody(BuildContext context, HomeUIModel model) {
Widget build(BuildContext context, WidgetRef ref) {
final homeState = ref.watch(homeUIModelProvider);
final model = ref.watch(homeUIModelProvider.notifier);
return Stack(
children: [
Center(
@ -22,29 +31,29 @@ class HomeUI extends BaseUI<HomeUIModel> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (model.appPlacardData != null) ...[
if (homeState.appPlacardData != null) ...[
InfoBar(
title: Text("${model.appPlacardData?.title}"),
content: Text("${model.appPlacardData?.content}"),
title: Text("${homeState.appPlacardData?.title}"),
content: Text("${homeState.appPlacardData?.content}"),
severity: InfoBarSeverity.info,
action: model.appPlacardData?.link == null
action: homeState.appPlacardData?.link == null
? null
: Button(
child: const Text('查看详情'),
onPressed: () => model.showPlacard(),
onPressed: () => _showPlacard(context, homeState),
),
onClose: model.appPlacardData?.alwaysShow == true
onClose: homeState.appPlacardData?.alwaysShow == true
? null
: () => model.closePlacard(),
),
const SizedBox(height: 6),
],
...makeIndex(context, model)
...makeIndex(context, model, homeState)
],
),
),
),
if (model.isFixing)
if (homeState.isFixing)
Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(150),
@ -55,8 +64,8 @@ class HomeUI extends BaseUI<HomeUIModel> {
children: [
const ProgressRing(),
const SizedBox(height: 12),
Text(model.isFixingString.isNotEmpty
? model.isFixingString
Text(homeState.isFixingString.isNotEmpty
? homeState.isFixingString
: "正在处理..."),
],
),
@ -66,7 +75,8 @@ class HomeUI extends BaseUI<HomeUIModel> {
);
}
List<Widget> makeIndex(BuildContext context, HomeUIModel model) {
List<Widget> makeIndex(
BuildContext context, HomeUIModel model, HomeUIModelState homeState) {
const double width = 280;
return [
Stack(
@ -86,7 +96,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
height: 260,
),
),
makeGameStatusCard(context, model, 340)
makeGameStatusCard(context, model, 340, homeState)
],
),
),
@ -95,12 +105,12 @@ class HomeUI extends BaseUI<HomeUIModel> {
Positioned(
top: 0,
left: 24,
child: makeLeftColumn(context, model, width),
child: makeLeftColumn(context, model, width, homeState),
),
Positioned(
right: 24,
top: 0,
child: makeNewsCard(context, model),
child: makeNewsCard(context, model, homeState),
),
],
),
@ -114,52 +124,45 @@ class HomeUI extends BaseUI<HomeUIModel> {
const SizedBox(width: 6),
Expanded(
child: ComboBox<String>(
value: model.scInstalledPath,
value: homeState.scInstalledPath,
items: [
const ComboBoxItem(
value: "not_install",
child: Text("未安装 或 安装失败"),
),
for (final path in model.scInstallPaths)
for (final path in homeState.scInstallPaths)
ComboBoxItem(
value: path,
child: Text(path),
)
],
onChanged: (v) {
model.scInstalledPath = v!;
model.notifyListeners();
},
onChanged: model.onChangeInstallPath,
),
),
const SizedBox(width: 12),
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: model.isRsiLauncherStarting
? const ProgressRing()
: Button(
onPressed: model.appWebLocalizationVersionsData == null
Button(
onPressed: homeState.webLocalizationVersionsData == null
? null
: () => model.launchRSI(),
: () => model.launchRSI(context),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
model.isCurGameRunning
homeState.isCurGameRunning
? FluentIcons.stop_solid
: FluentIcons.play,
color: model.isCurGameRunning
color: homeState.isCurGameRunning
? Colors.red.withOpacity(.8)
: null,
),
)),
),
const SizedBox(width: 12),
Button(
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.folder_open),
),
onPressed: () => model.openDir(model.scInstalledPath)),
onPressed: () => SystemHelper.openDir(homeState.scInstalledPath),
),
const SizedBox(width: 12),
Button(
onPressed: model.reScanPath,
@ -172,12 +175,13 @@ class HomeUI extends BaseUI<HomeUIModel> {
),
),
const SizedBox(height: 8),
Text(model.lastScreenInfo, maxLines: 1),
makeIndexActionLists(context, model),
Text(homeState.lastScreenInfo, maxLines: 1),
makeIndexActionLists(context, model, homeState),
];
}
Widget makeLeftColumn(BuildContext context, HomeUIModel model, double width) {
Widget makeLeftColumn(BuildContext context, HomeUIModel model, double width,
HomeUIModelState homeState) {
return Stack(
children: [
Column(
@ -192,7 +196,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
makeWebViewButton(model,
makeWebViewButton(context, model,
icon: SvgPicture.asset(
"assets/rsi.svg",
colorFilter: makeSvgColor(Colors.white),
@ -206,7 +210,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
width: width,
touchKey: "webLocalization_rsi"),
const SizedBox(height: 12),
makeWebViewButton(model,
makeWebViewButton(context, model,
icon: Row(
children: [
SvgPicture.asset(
@ -224,7 +228,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
width: width,
touchKey: "webLocalization_uex"),
const SizedBox(height: 12),
makeWebViewButton(model,
makeWebViewButton(context, model,
icon: Row(
children: [
Image.asset(
@ -288,10 +292,10 @@ class HomeUI extends BaseUI<HomeUIModel> {
),
),
const SizedBox(height: 16),
makeActivityBanner(context, model, width),
makeActivityBanner(context, model, width, homeState),
],
),
if (model.appWebLocalizationVersionsData == null)
if (homeState.webLocalizationVersionsData == null)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
@ -305,7 +309,8 @@ class HomeUI extends BaseUI<HomeUIModel> {
);
}
Widget makeNewsCard(BuildContext context, HomeUIModel model) {
Widget makeNewsCard(
BuildContext context, HomeUIModel model, HomeUIModelState homeState) {
return ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: Container(
@ -326,16 +331,16 @@ class HomeUI extends BaseUI<HomeUIModel> {
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
child: model.rssVideoItems == null
child: homeState.rssVideoItems == null
? Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(.1)),
child: makeLoading(context),
)
: Swiper(
itemCount: model.rssVideoItems?.length ?? 0,
itemCount: homeState.rssVideoItems?.length ?? 0,
itemBuilder: (context, index) {
final item = model.rssVideoItems![index];
final item = homeState.rssVideoItems![index];
return GestureDetector(
onTap: () {
if (item.link != null) {
@ -352,14 +357,14 @@ class HomeUI extends BaseUI<HomeUIModel> {
),
)),
const SizedBox(height: 1),
if (model.rssTextItems == null)
if (homeState.rssTextItems == null)
makeLoading(context)
else
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final item = model.rssTextItems![index];
final item = homeState.rssTextItems![index];
return Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: BorderRadius.circular(12),
@ -378,7 +383,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
const SizedBox(width: 6),
Expanded(
child: Text(
"${model.handleTitle(item.title)}",
model.handleTitle(item.title),
textAlign: TextAlign.start,
maxLines: 1,
overflow: TextOverflow.ellipsis,
@ -396,7 +401,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
),
));
},
itemCount: model.rssTextItems?.length,
itemCount: homeState.rssTextItems?.length,
),
const SizedBox(height: 12),
],
@ -421,9 +426,10 @@ class HomeUI extends BaseUI<HomeUIModel> {
return const FaIcon(FontAwesomeIcons.rss, size: 14);
}
Widget makeIndexActionLists(BuildContext context, HomeUIModel model) {
Widget makeIndexActionLists(
BuildContext context, HomeUIModel model, HomeUIModelState homeState) {
final items = [
_HomeItemData("auto_check", "一键诊断", "一键诊断星际公民常见问题",
_HomeItemData("game_doctor", "一键诊断", "一键诊断星际公民常见问题",
FluentIcons.auto_deploy_settings),
_HomeItemData(
"localization", "汉化管理", "快捷安装汉化资源", FluentIcons.locale_language),
@ -441,7 +447,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
itemBuilder: (context, index) {
final item = items.elementAt(index);
return HoverButton(
onPressed: () => model.onMenuTap(item.key),
onPressed: () => _onMenuTap(context, item.key, homeState),
builder: (BuildContext context, Set<ButtonStates> states) {
return Container(
width: 300,
@ -483,15 +489,15 @@ class HomeUI extends BaseUI<HomeUIModel> {
],
)),
if (item.key == "localization" &&
model.localizationUpdateInfo != null)
homeState.localizationUpdateInfo != null)
Container(
padding: const EdgeInsets.only(
top: 3, bottom: 3, left: 8, right: 8),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12)),
child:
Text(model.localizationUpdateInfo?.key ?? " "),
child: Text(
homeState.localizationUpdateInfo?.key ?? " "),
),
const SizedBox(width: 12),
const Icon(
@ -508,10 +514,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
);
}
@override
String getUITitle(BuildContext context, HomeUIModel model) => "HOME";
Widget makeWebViewButton(HomeUIModel model,
Widget makeWebViewButton(BuildContext context, HomeUIModel model,
{required Widget icon,
required String name,
required String webTitle,
@ -528,7 +531,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
if (touchKey != null) {
AnalyticsApi.touch(touchKey);
}
model.goWebView(webTitle, webURL, useLocalization: true);
model.goWebView(context, webTitle, webURL, useLocalization: true);
},
child: Container(
width: width,
@ -579,15 +582,22 @@ class HomeUI extends BaseUI<HomeUIModel> {
);
}
Widget makeGameStatusCard(
BuildContext context, HomeUIModel model, double width) {
Widget makeGameStatusCard(BuildContext context, HomeUIModel model,
double width, HomeUIModelState homeState) {
const statusCnName = {
"Platform": "平台",
"Persistent Universe": "持续宇宙",
"Electronic Access": "电子访问",
"Arena Commander": "竞技场指挥官"
};
return Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .2),
borderRadius: BorderRadius.circular(12),
child: GestureDetector(
onTap: () {
model.goWebView(
"RSI 服务器状态", "https://status.robertsspaceindustries.com/",
model.goWebView(context, "RSI 服务器状态",
"https://status.robertsspaceindustries.com/",
useLocalization: true);
},
child: Container(
@ -598,14 +608,14 @@ class HomeUI extends BaseUI<HomeUIModel> {
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(children: [
if (model.scServerStatus == null)
if (homeState.scServerStatus == null)
makeLoading(context, width: 20)
else
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("状态:"),
for (final item in model.scServerStatus ?? [])
for (final item in homeState.scServerStatus ?? [])
Row(
children: [
SizedBox(
@ -622,7 +632,7 @@ class HomeUI extends BaseUI<HomeUIModel> {
),
const SizedBox(width: 5),
Text(
"${model.statusCnName[item["name"]] ?? item["name"]}",
"${statusCnName[item["name"]] ?? item["name"]}",
style: const TextStyle(fontSize: 13),
),
],
@ -641,20 +651,20 @@ class HomeUI extends BaseUI<HomeUIModel> {
);
}
Widget makeActivityBanner(
BuildContext context, HomeUIModel model, double width) {
Widget makeActivityBanner(BuildContext context, HomeUIModel model,
double width, HomeUIModelState homeState) {
return Tilt(
borderRadius: BorderRadius.circular(12),
shadowConfig: const ShadowConfig(disable: true),
child: GestureDetector(
onTap: () => model.onTapFestival(),
onTap: () => _onTapFestival(context),
child: Container(
width: width + 24,
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor),
child: Padding(
padding:
const EdgeInsets.only(left: 12, right: 12, top: 8, bottom: 8),
child: (model.countdownFestivalListData == null)
child: (homeState.countdownFestivalListData == null)
? SizedBox(
width: width,
height: 62,
@ -666,11 +676,12 @@ class HomeUI extends BaseUI<HomeUIModel> {
width: width,
height: 62,
child: Swiper(
itemCount: model.countdownFestivalListData!.length,
itemCount: homeState.countdownFestivalListData!.length,
autoplay: true,
autoplayDelay: 5000,
itemBuilder: (context, index) {
final item = model.countdownFestivalListData![index];
final item =
homeState.countdownFestivalListData![index];
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
@ -714,6 +725,49 @@ class HomeUI extends BaseUI<HomeUIModel> {
),
);
}
_showPlacard(BuildContext context, HomeUIModelState homeState) {
switch (homeState.appPlacardData?.linkType) {
case "external":
launchUrlString(homeState.appPlacardData?.link);
return;
case "doc":
showDialog(
context: context,
builder: (context) {
return HomeMdContentDialogUI(
title: homeState.appPlacardData?.title ?? "公告详情",
url: homeState.appPlacardData?.link,
);
});
return;
}
}
_onTapFestival(BuildContext context) {
showDialog(
context: context, builder: (context) => const HomeCountdownDialogUI());
}
_onMenuTap(
BuildContext context, String key, HomeUIModelState homeState) async {
const String gameInstallReqInfo =
"该功能需要一个有效的安装位置\n\n如果您的游戏未下载完成,请等待下载完毕后使用此功能。\n\n如果您的游戏已下载完毕但未识别,请启动一次游戏后重新打开盒子 或 在设置选项中手动设置安装位置。";
switch (key) {
case "localization":
if (homeState.scInstalledPath == "not_install") {
showToast(context, gameInstallReqInfo);
break;
}
await showDialog(
context: context,
dismissWithEsc: false,
builder: (BuildContext context) => const LocalizationDialogUI());
break;
default:
context.push("/index/$key");
}
}
}
class _HomeItemData {

View File

@ -2,257 +2,127 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dart_rss/dart_rss.dart';
import 'package:dart_rss/domain/rss_item.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/api/rss.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/io/rs_http.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/data/app_placard_data.dart';
import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart';
import 'package:starcitizen_doctor/data/countdown_festival_item_data.dart';
import 'package:starcitizen_doctor/ui/home/countdown/countdown_dialog_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui.dart';
import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/localization/localization_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/login/login_dialog_ui.dart';
import 'package:starcitizen_doctor/ui/home/login/login_dialog_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/performance/performance_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
import 'package:starcitizen_doctor/ui/home/webview/webview_localization_capture_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/dialogs/home_game_login_dialog_ui.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:html/parser.dart' as html;
import 'package:html/dom.dart' as html_dom;
import 'package:windows_ui/windows_ui.dart';
import 'countdown/countdown_dialog_ui.dart';
import 'game_doctor/game_doctor_ui.dart';
import 'game_doctor/game_doctor_ui_model.dart';
import 'localization/localization_ui.dart';
import 'performance/performance_ui.dart';
import 'webview/webview_localization_capture_ui.dart';
import '../webview/webview.dart';
class HomeUIModel extends BaseUIModel {
var scInstalledPath = "not_install";
part 'home_ui_model.freezed.dart';
List<String> scInstallPaths = [];
part 'home_ui_model.g.dart';
String lastScreenInfo = "";
@freezed
class HomeUIModelState with _$HomeUIModelState {
factory HomeUIModelState({
AppPlacardData? appPlacardData,
@Default(false) bool isFixing,
@Default("") String isFixingString,
String? scInstalledPath,
@Default([]) List<String> scInstallPaths,
AppWebLocalizationVersionsData? webLocalizationVersionsData,
@Default("") String lastScreenInfo,
List<RssItem>? rssVideoItems,
List<RssItem>? rssTextItems,
MapEntry<String, bool>? localizationUpdateInfo,
List? scServerStatus,
List<CountdownFestivalItemData>? countdownFestivalListData,
@Default({}) Map<String, bool> isGameRunning,
}) = _HomeUIModelState;
}
bool isFixing = false;
String isFixingString = "";
final Map<String, bool> _isGameRunning = {};
bool get isCurGameRunning => _isGameRunning[scInstalledPath] ?? false;
List<RssItem>? rssVideoItems;
List<RssItem>? rssTextItems;
AppWebLocalizationVersionsData? appWebLocalizationVersionsData;
List<CountdownFestivalItemData>? countdownFestivalListData;
MapEntry<String, bool>? localizationUpdateInfo;
bool _isSendLocalizationUpdateNotification = false;
AppPlacardData? appPlacardData;
List? scServerStatus;
Timer? serverUpdateTimer;
Timer? appUpdateTimer;
final statusCnName = const {
"Platform": "平台",
"Persistent Universe": "持续宇宙",
"Electronic Access": "电子访问",
"Arena Commander": "竞技场指挥官"
};
bool isRsiLauncherStarting = false;
extension HomeUIModelStateEx on HomeUIModelState {
bool get isCurGameRunning => isGameRunning[scInstalledPath] ?? false;
}
@riverpod
class HomeUIModel extends _$HomeUIModel {
@override
Future loadData() async {
if (AppConf.networkVersionData == null) return;
try {
final r = await Api.getAppPlacard();
final box = await Hive.openBox("app_conf");
final version = box.get("close_placard", defaultValue: "");
if (r.enable == true) {
if (r.alwaysShow != true && version == r.version) {
} else {
appPlacardData = r;
}
}
updateSCServerStatus();
notifyListeners();
appWebLocalizationVersionsData = AppWebLocalizationVersionsData.fromJson(
json.decode((await RSHttp.getText(
"${URLConf.webTranslateHomeUrl}/versions.json"))));
countdownFestivalListData = await Api.getFestivalCountdownList();
notifyListeners();
_loadRRS();
} catch (e) {
dPrint(e);
}
// check Localization update
_checkLocalizationUpdate();
notifyListeners();
}
@override
void initModel() {
reScanPath();
serverUpdateTimer = Timer.periodic(
const Duration(minutes: 10),
(timer) {
updateSCServerStatus();
},
);
appUpdateTimer = Timer.periodic(const Duration(minutes: 30), (timer) {
_checkLocalizationUpdate();
});
super.initModel();
}
@override
void dispose() {
serverUpdateTimer?.cancel();
serverUpdateTimer = null;
appUpdateTimer?.cancel();
appUpdateTimer = null;
super.dispose();
}
Future<void> reScanPath() async {
scInstallPaths.clear();
scInstalledPath = "not_install";
lastScreenInfo = "正在扫描 ...";
try {
final listData = await SCLoggerHelper.getLauncherLogList();
if (listData == null) {
lastScreenInfo = "获取log失败";
return;
}
scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData,
withVersion: ["LIVE", "PTU", "EPTU"], checkExists: true);
if (scInstallPaths.isNotEmpty) {
scInstalledPath = scInstallPaths.first;
}
lastScreenInfo = "扫描完毕,共找到 ${scInstallPaths.length} 个有效安装目录";
} catch (e) {
lastScreenInfo = "解析 log 文件失败!";
AnalyticsApi.touch("error_launchLogs");
showToast(context!,
"解析 log 文件失败! \n请关闭游戏退出RSI启动器后重试若仍有问题请使用工具箱中的 RSI Launcher log 修复。");
}
}
updateSCServerStatus() async {
try {
final s = await Api.getScServerStatus();
dPrint("updateSCServerStatus===$s");
scServerStatus = s;
notifyListeners();
} catch (e) {
dPrint(e);
}
}
Future _loadRRS() async {
try {
final v = await RSSApi.getRssVideo();
rssVideoItems = v;
notifyListeners();
final t = await RSSApi.getRssText();
rssTextItems = t;
notifyListeners();
dPrint("RSS update Success !");
} catch (e) {
dPrint("_loadRRS Error:$e");
}
}
openDir(rsiLauncherInstalledPath) async {
await Process.run(SystemHelper.powershellPath,
["explorer.exe", "/select,\"$rsiLauncherInstalledPath\""]);
}
onMenuTap(String key) async {
const String gameInstallReqInfo =
"该功能需要一个有效的安装位置\n\n如果您的游戏未下载完成,请等待下载完毕后使用此功能。\n\n如果您的游戏已下载完毕但未识别,请启动一次游戏后重新打开盒子 或 在设置选项中手动设置安装位置。";
switch (key) {
case "auto_check":
BaseUIContainer(
uiCreate: () => GameDoctorUI(),
modelCreate: () => GameDoctorUIModel(scInstalledPath))
.push(context!);
return;
case "localization":
if (scInstalledPath == "not_install") {
showToast(context!, gameInstallReqInfo);
return;
}
await showDialog(
context: context!,
dismissWithEsc: false,
builder: (BuildContext context) {
return BaseUIContainer(
uiCreate: () => LocalizationUI(),
modelCreate: () => LocalizationUIModel(scInstalledPath));
});
_checkLocalizationUpdate();
return;
case "performance":
if (scInstalledPath == "not_install") {
showToast(context!, gameInstallReqInfo);
return;
}
AnalyticsApi.touch("performance_launch");
BaseUIContainer(
uiCreate: () => PerformanceUI(),
modelCreate: () => PerformanceUIModel(scInstalledPath))
.push(context!);
return;
}
}
showPlacard() {
switch (appPlacardData?.linkType) {
case "external":
launchUrlString(appPlacardData?.link);
return;
case "doc":
showDialog(
context: context!,
builder: (context) {
return BaseUIContainer(
uiCreate: () => MDContentDialogUI(),
modelCreate: () => MDContentDialogUIModel(
appPlacardData?.title ?? "公告详情", appPlacardData?.link));
});
return;
}
HomeUIModelState build() {
state = HomeUIModelState();
_init();
_loadData();
return state;
}
closePlacard() async {
final box = await Hive.openBox("app_conf");
await box.put("close_placard", appPlacardData?.version);
appPlacardData = null;
notifyListeners();
await box.put("close_placard", state.appPlacardData?.version);
state = state.copyWith(appPlacardData: null);
}
goWebView(String title, String url,
Future<void> reScanPath() async {
state = state.copyWith(
scInstalledPath: "not_install", lastScreenInfo: "正在扫描 ...");
try {
final listData = await SCLoggerHelper.getLauncherLogList();
if (listData == null) {
state = state.copyWith(scInstalledPath: "not_install");
return;
}
final scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData,
withVersion: ["LIVE", "PTU", "EPTU"], checkExists: true);
String? scInstalledPath;
if (scInstallPaths.isNotEmpty) {
scInstalledPath = scInstallPaths.first;
}
final lastScreenInfo = "扫描完毕,共找到 ${scInstallPaths.length} 个有效安装目录";
state = state.copyWith(
scInstalledPath: scInstalledPath,
scInstallPaths: scInstallPaths,
lastScreenInfo: lastScreenInfo);
} catch (e) {
state = state.copyWith(
scInstalledPath: "not_install", lastScreenInfo: "解析 log 文件失败!");
AnalyticsApi.touch("error_launchLogs");
// showToast(context!,
// "解析 log 文件失败! \n请关闭游戏退出RSI启动器后重试若仍有问题请使用工具箱中的 RSI Launcher log 修复。");
}
}
String getRssImage(RssItem item) {
final h = html.parse(item.description ?? "");
if (h.body == null) return "";
for (var node in h.body!.nodes) {
if (node is html_dom.Element) {
if (node.localName == "img") {
return node.attributes["src"]?.trim() ?? "";
}
}
}
return "";
}
String handleTitle(String? title) {
if (title == null) return "";
title = title.replaceAll("", "[ ");
title = title.replaceAll("", " ] ");
return title;
}
// ignore: avoid_build_context_in_providers
Future<void> goWebView(BuildContext context, String title, String url,
{bool useLocalization = false,
bool loginMode = false,
RsiLoginCallback? rsiLoginCallback}) async {
@ -262,8 +132,9 @@ class HomeUIModel extends BaseUIModel {
final skip =
await box.get("skip_web_localization_tip_version", defaultValue: 0);
if (skip != tipVersion) {
if (!context.mounted) return;
final ok = await showConfirmDialogs(
context!,
context,
"星际公民网站汉化",
const Text(
"本插功能件仅供大致浏览使用,不对任何有关本功能产生的问题负责!在涉及账号操作前请注意确认网站的原本内容!"
@ -271,7 +142,7 @@ class HomeUIModel extends BaseUIModel {
style: TextStyle(fontSize: 16),
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .6));
maxWidth: MediaQuery.of(context).size.width * .6));
if (!ok) {
if (loginMode) {
rsiLoginCallback?.call(null, false);
@ -282,71 +153,138 @@ class HomeUIModel extends BaseUIModel {
}
}
if (!await WebviewWindow.isWebviewAvailable()) {
showToast(context!, "需要安装 WebView2 Runtime");
if (!context.mounted) return;
showToast(context, "需要安装 WebView2 Runtime");
launchUrlString(
"https://developer.microsoft.com/en-us/microsoft-edge/webview2/");
return;
}
final webViewModel = WebViewModel(context!,
if (!context.mounted) return;
final webViewModel = WebViewModel(context,
loginMode: loginMode, loginCallback: rsiLoginCallback);
if (useLocalization) {
isFixingString = "正在初始化汉化资源...";
isFixing = true;
notifyListeners();
state = state.copyWith(isFixing: true, isFixingString: "正在初始化汉化资源...");
try {
await webViewModel.initLocalization(appWebLocalizationVersionsData!);
await webViewModel.initLocalization(state.webLocalizationVersionsData!);
} catch (e) {
showToast(context!, "初始化网页汉化资源失败!$e");
if (!context.mounted) return;
showToast(context, "初始化网页汉化资源失败!$e");
}
isFixingString = "";
isFixing = false;
state = state.copyWith(isFixingString: "", isFixing: false);
}
await webViewModel.initWebView(
title: title,
applicationSupportDir: appGlobalState.applicationSupportDir!,
appVersionData: appGlobalState.networkVersionData!,
);
if (await File(
"${AppConf.applicationSupportDir}\\webview_data\\enable_webview_localization_capture")
.exists()) {
webViewModel.enableCapture = true;
BaseUIContainer(
uiCreate: () => WebviewLocalizationCaptureUI(),
modelCreate: () =>
WebviewLocalizationCaptureUIModel(webViewModel))
.push(context!)
.then((_) {
webViewModel.enableCapture = false;
await Future.delayed(const Duration(milliseconds: 500));
await webViewModel.launch(url, appGlobalState.networkVersionData!);
}
bool isRSIServerStatusOK(Map map) {
return (map["status"] == "ok" || map["status"] == "operational");
}
Timer? _serverUpdateTimer;
Timer? _appUpdateTimer;
void _init() {
reScanPath();
_serverUpdateTimer = Timer.periodic(
const Duration(minutes: 10),
(timer) {
_updateSCServerStatus();
},
);
_appUpdateTimer = Timer.periodic(const Duration(minutes: 30), (timer) {
_checkLocalizationUpdate();
});
ref.onDispose(() {
_serverUpdateTimer?.cancel();
_serverUpdateTimer = null;
_appUpdateTimer?.cancel();
_appUpdateTimer = null;
});
}
await Future.delayed(const Duration(milliseconds: 500));
await webViewModel.launch(url);
notifyListeners();
void _loadData() async {
if (appGlobalState.networkVersionData == null) return;
try {
final r = await Api.getAppPlacard();
final box = await Hive.openBox("app_conf");
final version = box.get("close_placard", defaultValue: "");
if (r.enable == true) {
if (r.alwaysShow != true && version == r.version) {
} else {
state = state.copyWith(appPlacardData: r);
}
}
launchRSI() async {
if (scInstalledPath == "not_install") {
showToast(context!, "该功能需要一个有效的安装位置");
final appWebLocalizationVersionsData =
AppWebLocalizationVersionsData.fromJson(json.decode(
(await RSHttp.getText(
"${URLConf.webTranslateHomeUrl}/versions.json"))));
final countdownFestivalListData = await Api.getFestivalCountdownList();
state = state.copyWith(
webLocalizationVersionsData: appWebLocalizationVersionsData,
countdownFestivalListData: countdownFestivalListData);
_updateSCServerStatus();
_loadRRS();
} catch (e) {
dPrint(e);
}
// check Localization update
_checkLocalizationUpdate();
}
Future<void> _updateSCServerStatus() async {
try {
final s = await Api.getScServerStatus();
dPrint("updateSCServerStatus===$s");
state = state.copyWith(scServerStatus: s);
} catch (e) {
dPrint(e);
}
}
Future _loadRRS() async {
try {
final rssVideoItems = await RSSApi.getRssVideo();
state = state.copyWith(rssVideoItems: rssVideoItems);
final rssTextItems = await RSSApi.getRssText();
state = state.copyWith(rssTextItems: rssTextItems);
dPrint("RSS update Success !");
} catch (e) {
dPrint("_loadRRS Error:$e");
}
}
void _checkLocalizationUpdate() {}
// ignore: avoid_build_context_in_providers
launchRSI(BuildContext context) async {
if (state.scInstalledPath == "not_install") {
showToast(context, "该功能需要一个有效的安装位置");
return;
}
if (AppConf.isMSE) {
if (isCurGameRunning) {
if (ConstConf.isMSE) {
if (state.isCurGameRunning) {
await Process.run(
SystemHelper.powershellPath, ["ps \"StarCitizen\" | kill"]);
return;
}
AnalyticsApi.touch("gameLaunch");
showDialog(
context: context!,
context: context,
dismissWithEsc: false,
builder: (context) {
return BaseUIContainer(
uiCreate: () => LoginDialog(),
modelCreate: () => LoginDialogModel(scInstalledPath, this));
});
builder: (context) => const HomeGameLoginDialogUI());
} else {
final ok = await showConfirmDialogs(
context!,
context,
"一键启动功能提示",
const Text("为确保账户安全,一键启动功能已在开发版中禁用,我们将在微软商店版本中提供此功能。"
"\n\n微软商店版由微软提供可靠的分发下载与数字签名,可有效防止软件被恶意篡改。\n\n提示:您无需使用盒子启动游戏也可使用汉化。"),
@ -361,14 +299,21 @@ class HomeUIModel extends BaseUIModel {
}
}
bool isRSIServerStatusOK(Map map) {
return (map["status"] == "ok" || map["status"] == "operational");
void onChangeInstallPath(String? value) {
if (value == null) return;
state = state.copyWith(scInstalledPath: value);
}
doLaunchGame(String launchExe, List<String> args, String installPath,
doLaunchGame(
// ignore: avoid_build_context_in_providers
BuildContext context,
String launchExe,
List<String> args,
String installPath,
String? processorAffinity) async {
_isGameRunning[installPath] = true;
notifyListeners();
var runningMap = Map<String, bool>.from(state.isGameRunning);
runningMap[installPath] = true;
state = state.copyWith(isGameRunning: runningMap);
try {
late ProcessResult result;
if (processorAffinity == null) {
@ -391,7 +336,7 @@ class HomeUIModel extends BaseUIModel {
dPrint('stderr: ${result.stderr}');
if (result.exitCode != 0) {
final logs = await SCLoggerHelper.getGameRunningLogs(scInstalledPath);
final logs = await SCLoggerHelper.getGameRunningLogs(installPath);
MapEntry<String, String>? exitInfo;
bool hasUrl = false;
if (logs != null) {
@ -400,7 +345,8 @@ class HomeUIModel extends BaseUIModel {
hasUrl = true;
}
}
showToast(context!,
if (!context.mounted) return;
showToast(context,
"游戏非正常退出\nexitCode=${result.exitCode}\nstdout=${result.stdout ?? ""}\nstderr=${result.stderr ?? ""}\n\n诊断信息:${exitInfo == null ? "未知错误,请通过一键诊断加群反馈。" : exitInfo.key} \n${hasUrl ? "请查看弹出的网页链接获得详细信息。" : exitInfo?.value ?? ""}");
if (hasUrl) {
await Future.delayed(const Duration(seconds: 3));
@ -413,69 +359,8 @@ class HomeUIModel extends BaseUIModel {
await launchFile.delete();
}
} catch (_) {}
_isGameRunning[installPath] = false;
notifyListeners();
}
onTapFestival() {
if (countdownFestivalListData == null) return;
showDialog(
context: context!,
builder: (context) {
return BaseUIContainer(
uiCreate: () => CountdownDialogUI(),
modelCreate: () =>
CountdownDialogUIModel(countdownFestivalListData!));
});
}
getRssImage(RssItem item) {
final h = html.parse(item.description ?? "");
if (h.body == null) return "";
for (var node in h.body!.nodes) {
if (node is html_dom.Element) {
if (node.localName == "img") {
return node.attributes["src"]?.trim() ?? "";
}
}
}
return "";
}
handleTitle(String? title) {
if (title == null) return "";
title = title.replaceAll("", "[ ");
title = title.replaceAll("", " ] ");
return title;
}
Future<void> _checkLocalizationUpdate() async {
final info = await handleError(
() => LocalizationUIModel.checkLocalizationUpdates(scInstallPaths));
dPrint("lUpdateInfo === $info");
localizationUpdateInfo = info;
notifyListeners();
if (info?.value == true && !_isSendLocalizationUpdateNotification) {
final toastNotifier =
ToastNotificationManager.createToastNotifierWithId("SC汉化盒子");
if (toastNotifier != null) {
final toastContent = ToastNotificationManager.getTemplateContent(
ToastTemplateType.toastText02);
if (toastContent != null) {
final xmlNodeList = toastContent.getElementsByTagName('text');
const title = '汉化有新版本!';
final content = '您在 ${info?.key} 安装的汉化有新版本啦!';
xmlNodeList.item(0)?.appendChild(toastContent.createTextNode(title));
xmlNodeList
.item(1)
?.appendChild(toastContent.createTextNode(content));
final toastNotification =
ToastNotification.createToastNotification(toastContent);
toastNotifier.show(toastNotification);
_isSendLocalizationUpdateNotification = true;
}
}
}
runningMap = Map<String, bool>.from(state.isGameRunning);
runningMap[installPath] = false;
state = state.copyWith(isGameRunning: runningMap);
}
}

View File

@ -0,0 +1,464 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'home_ui_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$HomeUIModelState {
AppPlacardData? get appPlacardData => throw _privateConstructorUsedError;
bool get isFixing => throw _privateConstructorUsedError;
String get isFixingString => throw _privateConstructorUsedError;
String? get scInstalledPath => throw _privateConstructorUsedError;
List<String> get scInstallPaths => throw _privateConstructorUsedError;
AppWebLocalizationVersionsData? get webLocalizationVersionsData =>
throw _privateConstructorUsedError;
String get lastScreenInfo => throw _privateConstructorUsedError;
List<RssItem>? get rssVideoItems => throw _privateConstructorUsedError;
List<RssItem>? get rssTextItems => throw _privateConstructorUsedError;
MapEntry<String, bool>? get localizationUpdateInfo =>
throw _privateConstructorUsedError;
List<dynamic>? get scServerStatus => throw _privateConstructorUsedError;
List<CountdownFestivalItemData>? get countdownFestivalListData =>
throw _privateConstructorUsedError;
Map<String, bool> get isGameRunning => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HomeUIModelStateCopyWith<HomeUIModelState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $HomeUIModelStateCopyWith<$Res> {
factory $HomeUIModelStateCopyWith(
HomeUIModelState value, $Res Function(HomeUIModelState) then) =
_$HomeUIModelStateCopyWithImpl<$Res, HomeUIModelState>;
@useResult
$Res call(
{AppPlacardData? appPlacardData,
bool isFixing,
String isFixingString,
String? scInstalledPath,
List<String> scInstallPaths,
AppWebLocalizationVersionsData? webLocalizationVersionsData,
String lastScreenInfo,
List<RssItem>? rssVideoItems,
List<RssItem>? rssTextItems,
MapEntry<String, bool>? localizationUpdateInfo,
List<dynamic>? scServerStatus,
List<CountdownFestivalItemData>? countdownFestivalListData,
Map<String, bool> isGameRunning});
}
/// @nodoc
class _$HomeUIModelStateCopyWithImpl<$Res, $Val extends HomeUIModelState>
implements $HomeUIModelStateCopyWith<$Res> {
_$HomeUIModelStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? appPlacardData = freezed,
Object? isFixing = null,
Object? isFixingString = null,
Object? scInstalledPath = freezed,
Object? scInstallPaths = null,
Object? webLocalizationVersionsData = freezed,
Object? lastScreenInfo = null,
Object? rssVideoItems = freezed,
Object? rssTextItems = freezed,
Object? localizationUpdateInfo = freezed,
Object? scServerStatus = freezed,
Object? countdownFestivalListData = freezed,
Object? isGameRunning = null,
}) {
return _then(_value.copyWith(
appPlacardData: freezed == appPlacardData
? _value.appPlacardData
: appPlacardData // ignore: cast_nullable_to_non_nullable
as AppPlacardData?,
isFixing: null == isFixing
? _value.isFixing
: isFixing // ignore: cast_nullable_to_non_nullable
as bool,
isFixingString: null == isFixingString
? _value.isFixingString
: isFixingString // ignore: cast_nullable_to_non_nullable
as String,
scInstalledPath: freezed == scInstalledPath
? _value.scInstalledPath
: scInstalledPath // ignore: cast_nullable_to_non_nullable
as String?,
scInstallPaths: null == scInstallPaths
? _value.scInstallPaths
: scInstallPaths // ignore: cast_nullable_to_non_nullable
as List<String>,
webLocalizationVersionsData: freezed == webLocalizationVersionsData
? _value.webLocalizationVersionsData
: webLocalizationVersionsData // ignore: cast_nullable_to_non_nullable
as AppWebLocalizationVersionsData?,
lastScreenInfo: null == lastScreenInfo
? _value.lastScreenInfo
: lastScreenInfo // ignore: cast_nullable_to_non_nullable
as String,
rssVideoItems: freezed == rssVideoItems
? _value.rssVideoItems
: rssVideoItems // ignore: cast_nullable_to_non_nullable
as List<RssItem>?,
rssTextItems: freezed == rssTextItems
? _value.rssTextItems
: rssTextItems // ignore: cast_nullable_to_non_nullable
as List<RssItem>?,
localizationUpdateInfo: freezed == localizationUpdateInfo
? _value.localizationUpdateInfo
: localizationUpdateInfo // ignore: cast_nullable_to_non_nullable
as MapEntry<String, bool>?,
scServerStatus: freezed == scServerStatus
? _value.scServerStatus
: scServerStatus // ignore: cast_nullable_to_non_nullable
as List<dynamic>?,
countdownFestivalListData: freezed == countdownFestivalListData
? _value.countdownFestivalListData
: countdownFestivalListData // ignore: cast_nullable_to_non_nullable
as List<CountdownFestivalItemData>?,
isGameRunning: null == isGameRunning
? _value.isGameRunning
: isGameRunning // ignore: cast_nullable_to_non_nullable
as Map<String, bool>,
) as $Val);
}
}
/// @nodoc
abstract class _$$HomeUIModelStateImplCopyWith<$Res>
implements $HomeUIModelStateCopyWith<$Res> {
factory _$$HomeUIModelStateImplCopyWith(_$HomeUIModelStateImpl value,
$Res Function(_$HomeUIModelStateImpl) then) =
__$$HomeUIModelStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{AppPlacardData? appPlacardData,
bool isFixing,
String isFixingString,
String? scInstalledPath,
List<String> scInstallPaths,
AppWebLocalizationVersionsData? webLocalizationVersionsData,
String lastScreenInfo,
List<RssItem>? rssVideoItems,
List<RssItem>? rssTextItems,
MapEntry<String, bool>? localizationUpdateInfo,
List<dynamic>? scServerStatus,
List<CountdownFestivalItemData>? countdownFestivalListData,
Map<String, bool> isGameRunning});
}
/// @nodoc
class __$$HomeUIModelStateImplCopyWithImpl<$Res>
extends _$HomeUIModelStateCopyWithImpl<$Res, _$HomeUIModelStateImpl>
implements _$$HomeUIModelStateImplCopyWith<$Res> {
__$$HomeUIModelStateImplCopyWithImpl(_$HomeUIModelStateImpl _value,
$Res Function(_$HomeUIModelStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? appPlacardData = freezed,
Object? isFixing = null,
Object? isFixingString = null,
Object? scInstalledPath = freezed,
Object? scInstallPaths = null,
Object? webLocalizationVersionsData = freezed,
Object? lastScreenInfo = null,
Object? rssVideoItems = freezed,
Object? rssTextItems = freezed,
Object? localizationUpdateInfo = freezed,
Object? scServerStatus = freezed,
Object? countdownFestivalListData = freezed,
Object? isGameRunning = null,
}) {
return _then(_$HomeUIModelStateImpl(
appPlacardData: freezed == appPlacardData
? _value.appPlacardData
: appPlacardData // ignore: cast_nullable_to_non_nullable
as AppPlacardData?,
isFixing: null == isFixing
? _value.isFixing
: isFixing // ignore: cast_nullable_to_non_nullable
as bool,
isFixingString: null == isFixingString
? _value.isFixingString
: isFixingString // ignore: cast_nullable_to_non_nullable
as String,
scInstalledPath: freezed == scInstalledPath
? _value.scInstalledPath
: scInstalledPath // ignore: cast_nullable_to_non_nullable
as String?,
scInstallPaths: null == scInstallPaths
? _value._scInstallPaths
: scInstallPaths // ignore: cast_nullable_to_non_nullable
as List<String>,
webLocalizationVersionsData: freezed == webLocalizationVersionsData
? _value.webLocalizationVersionsData
: webLocalizationVersionsData // ignore: cast_nullable_to_non_nullable
as AppWebLocalizationVersionsData?,
lastScreenInfo: null == lastScreenInfo
? _value.lastScreenInfo
: lastScreenInfo // ignore: cast_nullable_to_non_nullable
as String,
rssVideoItems: freezed == rssVideoItems
? _value._rssVideoItems
: rssVideoItems // ignore: cast_nullable_to_non_nullable
as List<RssItem>?,
rssTextItems: freezed == rssTextItems
? _value._rssTextItems
: rssTextItems // ignore: cast_nullable_to_non_nullable
as List<RssItem>?,
localizationUpdateInfo: freezed == localizationUpdateInfo
? _value.localizationUpdateInfo
: localizationUpdateInfo // ignore: cast_nullable_to_non_nullable
as MapEntry<String, bool>?,
scServerStatus: freezed == scServerStatus
? _value._scServerStatus
: scServerStatus // ignore: cast_nullable_to_non_nullable
as List<dynamic>?,
countdownFestivalListData: freezed == countdownFestivalListData
? _value._countdownFestivalListData
: countdownFestivalListData // ignore: cast_nullable_to_non_nullable
as List<CountdownFestivalItemData>?,
isGameRunning: null == isGameRunning
? _value._isGameRunning
: isGameRunning // ignore: cast_nullable_to_non_nullable
as Map<String, bool>,
));
}
}
/// @nodoc
class _$HomeUIModelStateImpl implements _HomeUIModelState {
_$HomeUIModelStateImpl(
{this.appPlacardData,
this.isFixing = false,
this.isFixingString = "",
this.scInstalledPath,
final List<String> scInstallPaths = const [],
this.webLocalizationVersionsData,
this.lastScreenInfo = "",
final List<RssItem>? rssVideoItems,
final List<RssItem>? rssTextItems,
this.localizationUpdateInfo,
final List<dynamic>? scServerStatus,
final List<CountdownFestivalItemData>? countdownFestivalListData,
final Map<String, bool> isGameRunning = const {}})
: _scInstallPaths = scInstallPaths,
_rssVideoItems = rssVideoItems,
_rssTextItems = rssTextItems,
_scServerStatus = scServerStatus,
_countdownFestivalListData = countdownFestivalListData,
_isGameRunning = isGameRunning;
@override
final AppPlacardData? appPlacardData;
@override
@JsonKey()
final bool isFixing;
@override
@JsonKey()
final String isFixingString;
@override
final String? scInstalledPath;
final List<String> _scInstallPaths;
@override
@JsonKey()
List<String> get scInstallPaths {
if (_scInstallPaths is EqualUnmodifiableListView) return _scInstallPaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_scInstallPaths);
}
@override
final AppWebLocalizationVersionsData? webLocalizationVersionsData;
@override
@JsonKey()
final String lastScreenInfo;
final List<RssItem>? _rssVideoItems;
@override
List<RssItem>? get rssVideoItems {
final value = _rssVideoItems;
if (value == null) return null;
if (_rssVideoItems is EqualUnmodifiableListView) return _rssVideoItems;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<RssItem>? _rssTextItems;
@override
List<RssItem>? get rssTextItems {
final value = _rssTextItems;
if (value == null) return null;
if (_rssTextItems is EqualUnmodifiableListView) return _rssTextItems;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
final MapEntry<String, bool>? localizationUpdateInfo;
final List<dynamic>? _scServerStatus;
@override
List<dynamic>? get scServerStatus {
final value = _scServerStatus;
if (value == null) return null;
if (_scServerStatus is EqualUnmodifiableListView) return _scServerStatus;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<CountdownFestivalItemData>? _countdownFestivalListData;
@override
List<CountdownFestivalItemData>? get countdownFestivalListData {
final value = _countdownFestivalListData;
if (value == null) return null;
if (_countdownFestivalListData is EqualUnmodifiableListView)
return _countdownFestivalListData;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final Map<String, bool> _isGameRunning;
@override
@JsonKey()
Map<String, bool> get isGameRunning {
if (_isGameRunning is EqualUnmodifiableMapView) return _isGameRunning;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_isGameRunning);
}
@override
String toString() {
return 'HomeUIModelState(appPlacardData: $appPlacardData, isFixing: $isFixing, isFixingString: $isFixingString, scInstalledPath: $scInstalledPath, scInstallPaths: $scInstallPaths, webLocalizationVersionsData: $webLocalizationVersionsData, lastScreenInfo: $lastScreenInfo, rssVideoItems: $rssVideoItems, rssTextItems: $rssTextItems, localizationUpdateInfo: $localizationUpdateInfo, scServerStatus: $scServerStatus, countdownFestivalListData: $countdownFestivalListData, isGameRunning: $isGameRunning)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HomeUIModelStateImpl &&
(identical(other.appPlacardData, appPlacardData) ||
other.appPlacardData == appPlacardData) &&
(identical(other.isFixing, isFixing) ||
other.isFixing == isFixing) &&
(identical(other.isFixingString, isFixingString) ||
other.isFixingString == isFixingString) &&
(identical(other.scInstalledPath, scInstalledPath) ||
other.scInstalledPath == scInstalledPath) &&
const DeepCollectionEquality()
.equals(other._scInstallPaths, _scInstallPaths) &&
(identical(other.webLocalizationVersionsData,
webLocalizationVersionsData) ||
other.webLocalizationVersionsData ==
webLocalizationVersionsData) &&
(identical(other.lastScreenInfo, lastScreenInfo) ||
other.lastScreenInfo == lastScreenInfo) &&
const DeepCollectionEquality()
.equals(other._rssVideoItems, _rssVideoItems) &&
const DeepCollectionEquality()
.equals(other._rssTextItems, _rssTextItems) &&
(identical(other.localizationUpdateInfo, localizationUpdateInfo) ||
other.localizationUpdateInfo == localizationUpdateInfo) &&
const DeepCollectionEquality()
.equals(other._scServerStatus, _scServerStatus) &&
const DeepCollectionEquality().equals(
other._countdownFestivalListData, _countdownFestivalListData) &&
const DeepCollectionEquality()
.equals(other._isGameRunning, _isGameRunning));
}
@override
int get hashCode => Object.hash(
runtimeType,
appPlacardData,
isFixing,
isFixingString,
scInstalledPath,
const DeepCollectionEquality().hash(_scInstallPaths),
webLocalizationVersionsData,
lastScreenInfo,
const DeepCollectionEquality().hash(_rssVideoItems),
const DeepCollectionEquality().hash(_rssTextItems),
localizationUpdateInfo,
const DeepCollectionEquality().hash(_scServerStatus),
const DeepCollectionEquality().hash(_countdownFestivalListData),
const DeepCollectionEquality().hash(_isGameRunning));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$HomeUIModelStateImplCopyWith<_$HomeUIModelStateImpl> get copyWith =>
__$$HomeUIModelStateImplCopyWithImpl<_$HomeUIModelStateImpl>(
this, _$identity);
}
abstract class _HomeUIModelState implements HomeUIModelState {
factory _HomeUIModelState(
{final AppPlacardData? appPlacardData,
final bool isFixing,
final String isFixingString,
final String? scInstalledPath,
final List<String> scInstallPaths,
final AppWebLocalizationVersionsData? webLocalizationVersionsData,
final String lastScreenInfo,
final List<RssItem>? rssVideoItems,
final List<RssItem>? rssTextItems,
final MapEntry<String, bool>? localizationUpdateInfo,
final List<dynamic>? scServerStatus,
final List<CountdownFestivalItemData>? countdownFestivalListData,
final Map<String, bool> isGameRunning}) = _$HomeUIModelStateImpl;
@override
AppPlacardData? get appPlacardData;
@override
bool get isFixing;
@override
String get isFixingString;
@override
String? get scInstalledPath;
@override
List<String> get scInstallPaths;
@override
AppWebLocalizationVersionsData? get webLocalizationVersionsData;
@override
String get lastScreenInfo;
@override
List<RssItem>? get rssVideoItems;
@override
List<RssItem>? get rssTextItems;
@override
MapEntry<String, bool>? get localizationUpdateInfo;
@override
List<dynamic>? get scServerStatus;
@override
List<CountdownFestivalItemData>? get countdownFestivalListData;
@override
Map<String, bool> get isGameRunning;
@override
@JsonKey(ignore: true)
_$$HomeUIModelStateImplCopyWith<_$HomeUIModelStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'home_ui_model.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$homeUIModelHash() => r'a911826a7b852408123bf4b8999ac80c3c582fd4';
/// See also [HomeUIModel].
@ProviderFor(HomeUIModel)
final homeUIModelProvider =
AutoDisposeNotifierProvider<HomeUIModel, HomeUIModelState>.internal(
HomeUIModel.new,
name: r'homeUIModelProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$homeUIModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$HomeUIModel = AutoDisposeNotifier<HomeUIModelState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -0,0 +1,444 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/data/sc_localization_data.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'localization_ui_model.dart';
class LocalizationDialogUI extends HookConsumerWidget {
const LocalizationDialogUI({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(localizationUIModelProvider);
final model = ref.read(localizationUIModelProvider.notifier);
final curInstallInfo = state.apiLocalizationData?[state.patchStatus?.value];
useEffect(() {
addPostFrameCallback(() {
model.checkUserCfg(context);
});
return null;
}, []);
return ContentDialog(
title: makeTitle(context, model, state),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .7,
minHeight: MediaQuery.of(context).size.height * .9),
content: Padding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
child: SingleChildScrollView(
child: Column(
children: [
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: state.patchStatus?.key == true &&
state.patchStatus?.value == "游戏内置"
? Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InfoBar(
title: const Text("警告"),
content: const Text(
"您正在使用游戏内置文本官方文本目前为机器翻译截至3.21.0),建议您在下方安装社区汉化。"),
severity: InfoBarSeverity.info,
style: InfoBarThemeData(decoration: (severity) {
return const BoxDecoration(
color: Color.fromRGBO(155, 7, 7, 1.0));
}, iconColor: (severity) {
return Colors.white;
}),
),
)
: SizedBox(
width: MediaQuery.of(context).size.width,
),
),
makeListContainer(
"汉化状态",
[
if (state.patchStatus == null)
makeLoading(context)
else ...[
const SizedBox(height: 6),
Row(
children: [
Center(
child: Text(
"启用(${LocalizationUIModel.languageSupport[state.selectedLanguage]}"),
),
const Spacer(),
ToggleSwitch(
checked: state.patchStatus?.key == true,
onChanged: model.updateLangCfg,
)
],
),
const SizedBox(height: 12),
Row(
children: [
Text("已安装版本:${state.patchStatus?.value}"),
const Spacer(),
if (state.patchStatus?.value != "游戏内置")
Row(
children: [
Button(
onPressed: model.goFeedback,
child: const Padding(
padding: EdgeInsets.all(4),
child: Row(
children: [
Icon(FluentIcons.feedback),
SizedBox(width: 6),
Text("汉化反馈"),
],
),
)),
const SizedBox(width: 16),
Button(
onPressed: model.doDelIniFile(),
child: const Padding(
padding: EdgeInsets.all(4),
child: Row(
children: [
Icon(FluentIcons.delete),
SizedBox(width: 6),
Text("卸载汉化"),
],
),
)),
],
),
],
),
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: (curInstallInfo != null &&
curInstallInfo.note != null &&
curInstallInfo.note!.isNotEmpty)
? Padding(
padding: const EdgeInsets.only(top: 12),
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(7)),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
"备注:",
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
Text(
"${curInstallInfo.note}",
style: TextStyle(
color:
Colors.white.withOpacity(.8)),
)
],
),
),
),
)
: SizedBox(
width: MediaQuery.of(context).size.width,
),
),
],
],
context),
makeListContainer(
"社区汉化",
[
if (state.apiLocalizationData == null)
makeLoading(context)
else if (state.apiLocalizationData!.isEmpty)
Center(
child: Text(
"该语言/版本 暂无可用汉化,敬请期待!",
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(.8)),
),
)
else
for (final item in state.apiLocalizationData!.entries)
makeRemoteList(context, model, item, state),
],
context),
const SizedBox(height: 12),
IconButton(
icon: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(state.enableCustomize
? FluentIcons.chevron_up
: FluentIcons.chevron_down),
const SizedBox(width: 12),
const Text("高级功能"),
],
),
onPressed: model.toggleCustomize),
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: Column(
children: [
const SizedBox(height: 12),
state.enableCustomize
? makeListContainer(
"自定义文本",
[
if (state.customizeList == null)
makeLoading(context)
else if (state.customizeList!.isEmpty)
Center(
child: Text(
"暂无自定义文本",
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(.8)),
),
)
else ...[
for (final file in state.customizeList!)
Row(
children: [
Text(
model.getCustomizeFileName(file),
),
const Spacer(),
if (state.workingVersion == file)
const Padding(
padding: EdgeInsets.only(right: 12),
child: ProgressRing(),
)
else
Button(
onPressed:
model.doLocalInstall(file),
child: const Padding(
padding: EdgeInsets.only(
left: 8,
right: 8,
top: 4,
bottom: 4),
child: Text("安装"),
))
],
)
],
],
context,
actions: [
Button(
onPressed: () => model.openDir(context),
child: const Padding(
padding: EdgeInsets.all(4),
child: Row(
children: [
Icon(FluentIcons.folder_open),
SizedBox(width: 6),
Text("打开文件夹"),
],
),
)),
])
: SizedBox(
width: MediaQuery.of(context).size.width,
)
],
),
),
],
),
),
),
);
}
Widget makeRemoteList(BuildContext context, LocalizationUIModel model,
MapEntry<String, ScLocalizationData> item, LocalizationUIState state) {
final isWorking = state.workingVersion.isNotEmpty;
final isMineWorking = state.workingVersion == item.key;
final isInstalled = state.patchStatus?.value == item.key;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${item.value.info}",
style: const TextStyle(fontSize: 19),
),
const SizedBox(height: 4),
Text(
"版本号:${item.value.versionName}",
style: TextStyle(color: Colors.white.withOpacity(.6)),
),
const SizedBox(height: 4),
Text(
"通道:${item.value.gameChannel}",
style: TextStyle(color: Colors.white.withOpacity(.6)),
),
const SizedBox(height: 4),
Text(
"更新时间:${item.value.updateAt}",
style: TextStyle(color: Colors.white.withOpacity(.6)),
),
],
),
const Spacer(),
if (isMineWorking)
const Padding(
padding: EdgeInsets.only(right: 12),
child: ProgressRing(),
)
else
Button(
onPressed: ((item.value.enable == true &&
!isWorking &&
!isInstalled)
? model.doRemoteInstall(context, item.value)
: null),
child: Padding(
padding: const EdgeInsets.only(
left: 8, right: 8, top: 4, bottom: 4),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 6),
child: Icon(isInstalled
? FluentIcons.check_mark
: (item.value.enable ?? false)
? FluentIcons.download
: FluentIcons.disable_updates),
),
Text(isInstalled
? "已安装"
: ((item.value.enable ?? false) ? "安装" : "不可用")),
],
),
)),
],
),
const SizedBox(height: 6),
Container(
color: Colors.white.withOpacity(.05),
height: 1,
),
],
),
);
}
Widget makeListContainer(
String title, List<Widget> children, BuildContext context,
{List<Widget> actions = const []}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: AnimatedSize(
duration: const Duration(milliseconds: 130),
child: Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(7)),
child: Padding(
padding:
const EdgeInsets.only(top: 12, bottom: 12, left: 24, right: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
title,
style: const TextStyle(fontSize: 22),
),
const Spacer(),
if (actions.isNotEmpty) ...actions,
],
),
const SizedBox(
height: 6,
),
Container(
color: Colors.white.withOpacity(.1),
height: 1,
),
const SizedBox(height: 12),
...children
],
),
),
),
),
);
}
Widget makeTitle(BuildContext context, LocalizationUIModel model,
LocalizationUIState state) {
return Row(
children: [
IconButton(
icon: const Icon(
FluentIcons.back,
size: 22,
),
onPressed: model.onBack(context)),
const SizedBox(width: 12),
const Text("汉化管理"),
const SizedBox(width: 24),
Text(
"${model.getScInstallPath()}",
style: const TextStyle(fontSize: 13),
),
const Spacer(),
SizedBox(
height: 36,
child: Row(
children: [
const Text(
"语言: ",
style: TextStyle(fontSize: 16),
),
ComboBox<String>(
value: state.selectedLanguage,
items: [
for (final lang
in LocalizationUIModel.languageSupport.entries)
ComboBoxItem(
value: lang.key,
child: Text(lang.value),
)
],
onChanged: state.workingVersion.isNotEmpty
? null
: (v) {
if (v == null) return;
model.selectLang(v);
},
)
],
),
),
const SizedBox(width: 12),
Button(
onPressed: model.doRefresh(),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.refresh),
)),
],
);
}
}

View File

@ -1,419 +0,0 @@
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/data/sc_localization_data.dart';
import 'localization_ui_model.dart';
class LocalizationUI extends BaseUI<LocalizationUIModel> {
@override
Widget? buildBody(BuildContext context, LocalizationUIModel model) {
final curInstallInfo = model.apiLocalizationData?[model.patchStatus?.value];
return ContentDialog(
title: makeTitle(context, model),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .7,
minHeight: MediaQuery.of(context).size.height * .9),
content: Padding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
child: SingleChildScrollView(
child: Column(
children: [
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: model.patchStatus?.key == true &&
model.patchStatus?.value == "游戏内置"
? Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InfoBar(
title: const Text("警告"),
content: const Text(
"您正在使用游戏内置文本官方文本目前为机器翻译截至3.21.0),建议您在下方安装社区汉化。"),
severity: InfoBarSeverity.info,
style: InfoBarThemeData(decoration: (severity) {
return const BoxDecoration(
color: Color.fromRGBO(155, 7, 7, 1.0));
}, iconColor: (severity) {
return Colors.white;
}),
),
)
: SizedBox(
width: MediaQuery.of(context).size.width,
),
),
makeListContainer("汉化状态", [
if (model.patchStatus == null)
makeLoading(context)
else ...[
const SizedBox(height: 6),
Row(
children: [
Center(
child: Text(
"启用(${LocalizationUIModel.languageSupport[model.selectedLanguage]}"),
),
const Spacer(),
ToggleSwitch(
checked: model.patchStatus?.key == true,
onChanged: model.updateLangCfg,
)
],
),
const SizedBox(height: 12),
Row(
children: [
Text("已安装版本:${model.patchStatus?.value}"),
const Spacer(),
if (model.patchStatus?.value != "游戏内置")
Row(
children: [
Button(
onPressed: model.goFeedback,
child: const Padding(
padding: EdgeInsets.all(4),
child: Row(
children: [
Icon(FluentIcons.feedback),
SizedBox(width: 6),
Text("汉化反馈"),
],
),
)),
const SizedBox(width: 16),
Button(
onPressed: model.doDelIniFile(),
child: const Padding(
padding: EdgeInsets.all(4),
child: Row(
children: [
Icon(FluentIcons.delete),
SizedBox(width: 6),
Text("卸载汉化"),
],
),
)),
],
),
],
),
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: (curInstallInfo != null &&
curInstallInfo.note != null &&
curInstallInfo.note!.isNotEmpty)
? Padding(
padding: const EdgeInsets.only(top: 12),
child: Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(7)),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"备注:",
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
Text(
"${curInstallInfo.note}",
style: TextStyle(
color: Colors.white.withOpacity(.8)),
)
],
),
),
),
)
: SizedBox(
width: MediaQuery.of(context).size.width,
),
),
],
]),
makeListContainer("社区汉化", [
if (model.apiLocalizationData == null)
makeLoading(context)
else if (model.apiLocalizationData!.isEmpty)
Center(
child: Text(
"该语言/版本 暂无可用汉化,敬请期待!",
style: TextStyle(
fontSize: 13, color: Colors.white.withOpacity(.8)),
),
)
else
for (final item in model.apiLocalizationData!.entries)
makeRemoteList(context, model, item),
]),
const SizedBox(height: 12),
IconButton(
icon: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(model.enableCustomize
? FluentIcons.chevron_up
: FluentIcons.chevron_down),
const SizedBox(width: 12),
const Text("高级功能"),
],
),
onPressed: () {
model.enableCustomize = !model.enableCustomize;
model.notifyListeners();
}),
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: Column(
children: [
const SizedBox(height: 12),
model.enableCustomize
? makeListContainer("自定义文本", [
if (model.customizeList == null)
makeLoading(context)
else if (model.customizeList!.isEmpty)
Center(
child: Text(
"暂无自定义文本",
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(.8)),
),
)
else ...[
for (final file in model.customizeList!)
Row(
children: [
Text(
model.getCustomizeFileName(file),
),
const Spacer(),
if (model.workingVersion == file)
const Padding(
padding: EdgeInsets.only(right: 12),
child: ProgressRing(),
)
else
Button(
onPressed: model.doLocalInstall(file),
child: const Padding(
padding: EdgeInsets.only(
left: 8,
right: 8,
top: 4,
bottom: 4),
child: Text("安装"),
))
],
)
],
], actions: [
Button(
onPressed: () => model.openDir(),
child: const Padding(
padding: EdgeInsets.all(4),
child: Row(
children: [
Icon(FluentIcons.folder_open),
SizedBox(width: 6),
Text("打开文件夹"),
],
),
)),
])
: SizedBox(
width: MediaQuery.of(context).size.width,
)
],
),
),
],
),
),
),
);
}
Widget makeRemoteList(BuildContext context, LocalizationUIModel model,
MapEntry<String, ScLocalizationData> item) {
final isWorking = model.workingVersion.isNotEmpty;
final isMineWorking = model.workingVersion == item.key;
final isInstalled = model.patchStatus?.value == item.key;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${item.value.info}",
style: const TextStyle(fontSize: 19),
),
const SizedBox(height: 4),
Text(
"版本号:${item.value.versionName}",
style: TextStyle(color: Colors.white.withOpacity(.6)),
),
const SizedBox(height: 4),
Text(
"通道:${item.value.gameChannel}",
style: TextStyle(color: Colors.white.withOpacity(.6)),
),
const SizedBox(height: 4),
Text(
"更新时间:${item.value.updateAt}",
style: TextStyle(color: Colors.white.withOpacity(.6)),
),
],
),
const Spacer(),
if (isMineWorking)
const Padding(
padding: EdgeInsets.only(right: 12),
child: ProgressRing(),
)
else
Button(
onPressed: ((item.value.enable == true &&
!isWorking &&
!isInstalled)
? model.doRemoteInstall(item.value)
: null),
child: Padding(
padding: const EdgeInsets.only(
left: 8, right: 8, top: 4, bottom: 4),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 6),
child: Icon(isInstalled
? FluentIcons.check_mark
: (item.value.enable ?? false)
? FluentIcons.download
: FluentIcons.disable_updates),
),
Text(isInstalled
? "已安装"
: ((item.value.enable ?? false) ? "安装" : "不可用")),
],
),
)),
],
),
const SizedBox(height: 6),
Container(
color: Colors.white.withOpacity(.05),
height: 1,
),
],
),
);
}
Widget makeListContainer(String title, List<Widget> children,
{List<Widget> actions = const []}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: AnimatedSize(
duration: const Duration(milliseconds: 130),
child: Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(7)),
child: Padding(
padding:
const EdgeInsets.only(top: 12, bottom: 12, left: 24, right: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
title,
style: const TextStyle(fontSize: 22),
),
const Spacer(),
if (actions.isNotEmpty) ...actions,
],
),
const SizedBox(
height: 6,
),
Container(
color: Colors.white.withOpacity(.1),
height: 1,
),
const SizedBox(height: 12),
...children
],
),
),
),
),
);
}
Widget makeTitle(BuildContext context, LocalizationUIModel model) {
return Row(
children: [
IconButton(
icon: const Icon(
FluentIcons.back,
size: 22,
),
onPressed: model.onBack()),
const SizedBox(width: 12),
Text(getUITitle(context, model)),
const SizedBox(width: 24),
Text(
model.scInstallPath,
style: const TextStyle(fontSize: 13),
),
const Spacer(),
SizedBox(
height: 36,
child: Row(
children: [
const Text(
"语言: ",
style: TextStyle(fontSize: 16),
),
ComboBox<String>(
value: model.selectedLanguage,
items: [
for (final lang
in LocalizationUIModel.languageSupport.entries)
ComboBoxItem(
value: lang.key,
child: Text(lang.value),
)
],
onChanged: model.workingVersion.isNotEmpty
? null
: (v) {
if (v == null) return;
model.selectLang(v);
},
)
],
),
),
const SizedBox(width: 12),
Button(
onPressed: model.doRefresh(),
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.refresh),
)),
],
);
}
@override
String getUITitle(BuildContext context, LocalizationUIModel model) => "汉化管理";
}

View File

@ -1,233 +1,134 @@
// ignore_for_file: avoid_build_context_in_providers
import 'dart:async';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/utils/provider.dart';
import 'package:starcitizen_doctor/data/sc_localization_data.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:starcitizen_doctor/common/utils/log.dart' as log_utils;
class LocalizationUIModel extends BaseUIModel {
final String scInstallPath;
part 'localization_ui_model.g.dart';
part 'localization_ui_model.freezed.dart';
@freezed
class LocalizationUIState with _$LocalizationUIState {
const factory LocalizationUIState({
String? selectedLanguage,
Map<String, ScLocalizationData>? apiLocalizationData,
@Default("") String workingVersion,
MapEntry<bool, String>? patchStatus,
List<String>? customizeList,
@Default(false) bool enableCustomize,
}) = _LocalizationUIState;
}
@riverpod
class LocalizationUIModel extends _$LocalizationUIModel {
static const languageSupport = {
"chinese_(simplified)": "简体中文",
"chinese_(traditional)": "繁體中文",
};
late String selectedLanguage;
Map<String, ScLocalizationData>? apiLocalizationData;
LocalizationUIModel(this.scInstallPath);
String workingVersion = "";
final downloadDir =
Directory("${AppConf.applicationSupportDir}\\Localizations");
late final _downloadDir =
Directory("${appGlobalState.applicationSupportDir}\\Localizations");
late final customizeDir =
Directory("${downloadDir.absolute.path}\\Customize_ini");
Directory("${_downloadDir.absolute.path}\\Customize_ini");
late final scDataDir = Directory("$scInstallPath\\data");
late final scDataDir =
Directory("${ref.read(homeUIModelProvider).scInstalledPath}\\data");
late final cfgFile = File("${scDataDir.absolute.path}\\system.cfg");
MapEntry<bool, String>? patchStatus;
StreamSubscription? _customizeDirListenSub;
List<String>? customizeList;
StreamSubscription? customizeDirListenSub;
bool enableCustomize = false;
String get _scInstallPath => ref.read(homeUIModelProvider).scInstalledPath!;
@override
void initModel() {
selectedLanguage = languageSupport.entries.first.key;
if (!customizeDir.existsSync()) {
customizeDir.createSync(recursive: true);
LocalizationUIState build() {
state = LocalizationUIState(selectedLanguage: languageSupport.keys.first);
_init();
return state;
}
customizeDirListenSub = customizeDir.watch().listen((event) {
_init() async {
if (!customizeDir.existsSync()) {
await customizeDir.create(recursive: true);
}
_customizeDirListenSub = customizeDir.watch().listen((event) {
_scanCustomizeDir();
});
super.initModel();
ref.onDispose(() {
_customizeDirListenSub?.cancel();
_customizeDirListenSub = null;
});
_loadData();
}
@override
Future loadData() async {
_loadData() async {
await _updateStatus();
_checkUserCfg();
_scanCustomizeDir();
final l =
await handleError(() => Api.getScLocalizationData(selectedLanguage));
final l = await Api.getScLocalizationData(state.selectedLanguage!).unwrap();
if (l != null) {
apiLocalizationData = {};
final apiLocalizationData = <String, ScLocalizationData>{};
for (var element in l) {
final isPTU = !scInstallPath.contains("LIVE");
final isPTU = !_scInstallPath.contains("LIVE");
if (isPTU && element.gameChannel == "PTU") {
apiLocalizationData![element.versionName ?? ""] = element;
apiLocalizationData[element.versionName ?? ""] = element;
} else if (!isPTU && element.gameChannel == "PU") {
apiLocalizationData![element.versionName ?? ""] = element;
apiLocalizationData[element.versionName ?? ""] = element;
}
}
state = state.copyWith(apiLocalizationData: apiLocalizationData);
}
notifyListeners();
}
@override
dispose() {
customizeDirListenSub?.cancel();
super.dispose();
void checkUserCfg(BuildContext context) async {
final userCfgFile = File("$_scInstallPath\\USER.cfg");
if (await userCfgFile.exists()) {
final cfgString = await userCfgFile.readAsString();
if (cfgString.contains("g_language") &&
!cfgString.contains("g_language=${state.selectedLanguage}")) {
if (!context.mounted) return;
final ok = await showConfirmDialogs(
context,
"是否移除不兼容的汉化参数",
const Text(
"USER.cfg 包含不兼容的汉化参数,这可能是以前的汉化文件的残留信息。\n\n这将可能导致汉化无效或乱码,点击确认为您一键移除(不会影响其他配置)。"),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .35));
if (ok == true) {
var finalString = "";
for (var item in cfgString.split("\n")) {
if (!item.trim().startsWith("g_language")) {
finalString = "$finalString$item\n";
}
}
await userCfgFile.delete();
await userCfgFile.create();
await userCfgFile.writeAsString(finalString, flush: true);
_loadData();
}
}
}
}
_scanCustomizeDir() {
final fileList = customizeDir.listSync();
customizeList = [];
for (var value in fileList) {
if (value is File && value.path.endsWith(".ini")) {
customizeList?.add(value.absolute.path);
}
}
notifyListeners();
}
String getCustomizeFileName(String path) {
return path.split("\\").last;
}
_updateStatus() async {
patchStatus = MapEntry(
await getLangCfgEnableLang(lang: selectedLanguage),
await getInstalledIniVersion(
"${scDataDir.absolute.path}\\Localization\\$selectedLanguage\\global.ini"));
notifyListeners();
}
VoidCallback? onBack() {
if (workingVersion.isNotEmpty) return null;
return () {
Navigator.pop(context!);
};
}
void selectLang(String v) {
selectedLanguage = v;
apiLocalizationData = null;
notifyListeners();
reloadData();
}
VoidCallback? doRefresh() {
if (workingVersion.isNotEmpty) return null;
return () {
apiLocalizationData = null;
notifyListeners();
reloadData();
};
}
VoidCallback? doRemoteInstall(ScLocalizationData value) {
return () async {
AnalyticsApi.touch("install_localization");
final downloadUrl =
"${URLConf.gitlabLocalizationUrl}/archive/${value.versionName}.tar.gz";
final savePath =
File("${downloadDir.absolute.path}\\${value.versionName}.sclang");
try {
workingVersion = value.versionName!;
notifyListeners();
if (!await savePath.exists()) {
// download
dPrint("downloading file to $savePath");
final r = await RSHttp.get(downloadUrl);
if (r.statusCode == 200 && r.data != null) {
await savePath.writeAsBytes(r.data!);
} else {
throw "statusCode Error : ${r.statusCode}";
}
} else {
dPrint("use cache $savePath");
}
await Future.delayed(const Duration(milliseconds: 300));
// check file
final globalIni = await compute(_readArchive, savePath.absolute.path);
if (globalIni.isEmpty) {
throw "文件受损,请重新下载";
}
await _installFormString(globalIni, value.versionName ?? "");
} catch (e) {
await showToast(context!, "安装出错!\n\n $e");
if (await savePath.exists()) await savePath.delete();
}
workingVersion = "";
notifyListeners();
};
}
Future<bool> getLangCfgEnableLang({String lang = ""}) async {
if (!await cfgFile.exists()) return false;
final str = (await cfgFile.readAsString()).replaceAll(" ", "");
return str.contains("sys_languages=$lang") &&
str.contains("g_language=$lang") &&
str.contains("g_languageAudio=english");
}
static Future<String> getInstalledIniVersion(String iniPath) async {
final iniFile = File(iniPath);
if (!await iniFile.exists()) return "游戏内置";
final iniStringSplit = (await iniFile.readAsString()).split("\n");
for (var i = iniStringSplit.length - 1; i > 0; i--) {
if (iniStringSplit[i]
.contains("_starcitizen_doctor_localization_version=")) {
final v = iniStringSplit[i]
.trim()
.split("_starcitizen_doctor_localization_version=")[1];
return v;
}
}
return "自定义文件";
}
_installFormString(StringBuffer globalIni, String versionName) async {
final iniFile = File(
"${scDataDir.absolute.path}\\Localization\\$selectedLanguage\\global.ini");
if (versionName.isNotEmpty) {
if (!globalIni.toString().endsWith("\n")) {
globalIni.write("\n");
}
globalIni.write("_starcitizen_doctor_localization_version=$versionName");
}
/// write cfg
if (await cfgFile.exists()) {}
/// write ini
if (await iniFile.exists()) {
await iniFile.delete();
}
await iniFile.create(recursive: true);
await iniFile.writeAsString("\uFEFF${globalIni.toString().trim()}",
flush: true);
await updateLangCfg(true);
await _updateStatus();
}
openDir() async {
showToast(context!,
"即将打开本地化文件夹,请将自定义的 任意名称.ini 文件放入 Customize_ini 文件夹。\n\n添加新文件后未显示请使用右上角刷新按钮。\n\n安装时请确保选择了正确的语言。");
await Process.run(SystemHelper.powershellPath,
["explorer.exe", "/select,\"${customizeDir.absolute.path}\"\\"]);
}
updateLangCfg(bool enable) async {
final status = await getLangCfgEnableLang(lang: selectedLanguage);
Future<void> updateLangCfg(bool enable) async {
final selectedLanguage = state.selectedLanguage!;
final status = await _getLangCfgEnableLang(lang: selectedLanguage);
final exists = await cfgFile.exists();
if (status == enable) {
await _updateStatus();
@ -276,20 +177,112 @@ class LocalizationUIModel extends BaseUIModel {
await cfgFile.create(recursive: true);
await cfgFile.writeAsString(newStr.toString());
await _updateStatus();
notifyListeners();
}
void goFeedback() {
launchUrlString(URLConf.feedbackUrl);
}
VoidCallback? doDelIniFile() {
return () async {
final iniFile = File(
"${scDataDir.absolute.path}\\Localization\\$selectedLanguage\\global.ini");
"${scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini");
if (await iniFile.exists()) await iniFile.delete();
await updateLangCfg(false);
await _updateStatus();
};
}
/// read locale active
void toggleCustomize() {
state = state.copyWith(enableCustomize: !state.enableCustomize);
}
String getCustomizeFileName(String path) {
return path.split("\\").last;
}
VoidCallback? doLocalInstall(String filePath) {
if (state.workingVersion.isNotEmpty) return null;
return () async {
final f = File(filePath);
if (!await f.exists()) return;
state = state.copyWith(workingVersion: filePath);
final str = await f.readAsString();
await _installFormString(
StringBuffer(str), "自定义_${getCustomizeFileName(filePath)}");
state = state.copyWith(workingVersion: "");
};
}
_installFormString(StringBuffer globalIni, String versionName) async {
final iniFile = File(
"${scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini");
if (versionName.isNotEmpty) {
if (!globalIni.toString().endsWith("\n")) {
globalIni.write("\n");
}
globalIni.write("_starcitizen_doctor_localization_version=$versionName");
}
/// write cfg
if (await cfgFile.exists()) {}
/// write ini
if (await iniFile.exists()) {
await iniFile.delete();
}
await iniFile.create(recursive: true);
await iniFile.writeAsString("\uFEFF${globalIni.toString().trim()}",
flush: true);
await updateLangCfg(true);
await _updateStatus();
}
openDir(BuildContext context) async {
showToast(context,
"即将打开本地化文件夹,请将自定义的 任意名称.ini 文件放入 Customize_ini 文件夹。\n\n添加新文件后未显示请使用右上角刷新按钮。\n\n安装时请确保选择了正确的语言。");
await Process.run(SystemHelper.powershellPath,
["explorer.exe", "/select,\"${customizeDir.absolute.path}\"\\"]);
}
VoidCallback? doRemoteInstall(
BuildContext context, ScLocalizationData value) {
return () async {
AnalyticsApi.touch("install_localization");
final downloadUrl =
"${URLConf.gitlabLocalizationUrl}/archive/${value.versionName}.tar.gz";
final savePath =
File("${_downloadDir.absolute.path}\\${value.versionName}.sclang");
try {
state = state.copyWith(workingVersion: value.versionName!);
if (!await savePath.exists()) {
// download
dPrint("downloading file to $savePath");
final r = await RSHttp.get(downloadUrl);
if (r.statusCode == 200 && r.data != null) {
await savePath.writeAsBytes(r.data!);
} else {
throw "statusCode Error : ${r.statusCode}";
}
} else {
dPrint("use cache $savePath");
}
await Future.delayed(const Duration(milliseconds: 300));
// check file
final globalIni = await compute(_readArchive, savePath.absolute.path);
if (globalIni.isEmpty) {
throw "文件受损,请重新下载";
}
await _installFormString(globalIni, value.versionName ?? "");
} catch (e) {
if (!context.mounted) return;
await showToast(context, "安装出错!\n\n $e");
if (await savePath.exists()) await savePath.delete();
}
state = state.copyWith(workingVersion: "");
};
}
static StringBuffer _readArchive(String savePath) {
final inputStream = InputFileStream(savePath);
final archive =
@ -308,87 +301,74 @@ class LocalizationUIModel extends BaseUIModel {
return globalIni;
}
VoidCallback? doLocalInstall(String filePath) {
if (workingVersion.isNotEmpty) return null;
return () async {
final f = File(filePath);
if (!await f.exists()) return;
workingVersion = filePath;
notifyListeners();
final str = await f.readAsString();
await _installFormString(
StringBuffer(str), "自定义_${getCustomizeFileName(filePath)}");
workingVersion = "";
notifyListeners();
String? getScInstallPath() {
return ref.read(homeUIModelProvider).scInstalledPath;
}
void selectLang(String v) {
state = state.copyWith(selectedLanguage: v);
_loadData();
}
VoidCallback? onBack(BuildContext context) {
if (state.workingVersion.isNotEmpty) return null;
return () {
Navigator.pop(context);
};
}
void _checkUserCfg() async {
final userCfgFile = File("$scInstallPath\\USER.cfg");
if (await userCfgFile.exists()) {
final cfgString = await userCfgFile.readAsString();
if (cfgString.contains("g_language") &&
!cfgString.contains("g_language=$selectedLanguage")) {
final ok = await showConfirmDialogs(
context!,
"是否移除不兼容的汉化参数",
const Text(
"USER.cfg 包含不兼容的汉化参数,这可能是以前的汉化文件的残留信息。\n\n这将可能导致汉化无效或乱码,点击确认为您一键移除(不会影响其他配置)。"),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .35));
if (ok == true) {
var finalString = "";
for (var item in cfgString.split("\n")) {
if (!item.trim().startsWith("g_language")) {
finalString = "$finalString$item\n";
}
}
await userCfgFile.delete();
await userCfgFile.create();
await userCfgFile.writeAsString(finalString, flush: true);
reloadData();
}
}
}
VoidCallback? doRefresh() {
if (state.workingVersion.isNotEmpty) return null;
return () {
state = state.copyWith(apiLocalizationData: null);
_loadData();
};
}
static Future<MapEntry<String, bool>?> checkLocalizationUpdates(
List<String> gameInstallPaths) async {
final updateInfo = <String, bool>{};
for (var kv in languageSupport.entries) {
final l = await Api.getScLocalizationData(kv.key);
for (var value in gameInstallPaths) {
final iniPath = "$value\\data\\Localization\\${kv.key}\\global.ini";
if (!await File(iniPath).exists()) {
continue;
}
final installed = await getInstalledIniVersion(iniPath);
if (installed == "游戏内置" || installed == "自定义文件") {
continue;
}
final hasUpdate = l
.where((element) => element.versionName == installed)
.firstOrNull ==
null;
updateInfo[value] = hasUpdate;
void _scanCustomizeDir() {
final fileList = customizeDir.listSync();
final customizeList = <String>[];
for (var value in fileList) {
if (value is File && value.path.endsWith(".ini")) {
customizeList.add(value.absolute.path);
}
}
log_utils.dPrint("checkLocalizationUpdates ==== $updateInfo");
for (var v in updateInfo.entries) {
if (v.value) {
for (var element in AppConf.gameChannels) {
if (v.key.contains("StarCitizen\\$element")) {
return MapEntry(element, true);
} else {
return const MapEntry("", true);
}
}
}
}
return null;
state = state.copyWith(customizeList: customizeList);
}
void goFeedback() {
launchUrlString(URLConf.feedbackUrl);
_updateStatus() async {
final patchStatus = MapEntry(
await _getLangCfgEnableLang(lang: state.selectedLanguage!),
await _getInstalledIniVersion(
"${scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini"));
state = state.copyWith(patchStatus: patchStatus);
}
Future<bool> _getLangCfgEnableLang({String lang = ""}) async {
if (!await cfgFile.exists()) return false;
final str = (await cfgFile.readAsString()).replaceAll(" ", "");
return str.contains("sys_languages=$lang") &&
str.contains("g_language=$lang") &&
str.contains("g_languageAudio=english");
}
static Future<String> _getInstalledIniVersion(String iniPath) async {
final iniFile = File(iniPath);
if (!await iniFile.exists()) return "游戏内置";
final iniStringSplit = (await iniFile.readAsString()).split("\n");
for (var i = iniStringSplit.length - 1; i > 0; i--) {
if (iniStringSplit[i]
.contains("_starcitizen_doctor_localization_version=")) {
final v = iniStringSplit[i]
.trim()
.split("_starcitizen_doctor_localization_version=")[1];
return v;
}
}
return "自定义文件";
}
Future checkLangUpdate() async {
// TODO
}
}

View File

@ -0,0 +1,272 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'localization_ui_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$LocalizationUIState {
String? get selectedLanguage => throw _privateConstructorUsedError;
Map<String, ScLocalizationData>? get apiLocalizationData =>
throw _privateConstructorUsedError;
String get workingVersion => throw _privateConstructorUsedError;
MapEntry<bool, String>? get patchStatus => throw _privateConstructorUsedError;
List<String>? get customizeList => throw _privateConstructorUsedError;
bool get enableCustomize => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LocalizationUIStateCopyWith<LocalizationUIState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LocalizationUIStateCopyWith<$Res> {
factory $LocalizationUIStateCopyWith(
LocalizationUIState value, $Res Function(LocalizationUIState) then) =
_$LocalizationUIStateCopyWithImpl<$Res, LocalizationUIState>;
@useResult
$Res call(
{String? selectedLanguage,
Map<String, ScLocalizationData>? apiLocalizationData,
String workingVersion,
MapEntry<bool, String>? patchStatus,
List<String>? customizeList,
bool enableCustomize});
}
/// @nodoc
class _$LocalizationUIStateCopyWithImpl<$Res, $Val extends LocalizationUIState>
implements $LocalizationUIStateCopyWith<$Res> {
_$LocalizationUIStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? selectedLanguage = freezed,
Object? apiLocalizationData = freezed,
Object? workingVersion = null,
Object? patchStatus = freezed,
Object? customizeList = freezed,
Object? enableCustomize = null,
}) {
return _then(_value.copyWith(
selectedLanguage: freezed == selectedLanguage
? _value.selectedLanguage
: selectedLanguage // ignore: cast_nullable_to_non_nullable
as String?,
apiLocalizationData: freezed == apiLocalizationData
? _value.apiLocalizationData
: apiLocalizationData // ignore: cast_nullable_to_non_nullable
as Map<String, ScLocalizationData>?,
workingVersion: null == workingVersion
? _value.workingVersion
: workingVersion // ignore: cast_nullable_to_non_nullable
as String,
patchStatus: freezed == patchStatus
? _value.patchStatus
: patchStatus // ignore: cast_nullable_to_non_nullable
as MapEntry<bool, String>?,
customizeList: freezed == customizeList
? _value.customizeList
: customizeList // ignore: cast_nullable_to_non_nullable
as List<String>?,
enableCustomize: null == enableCustomize
? _value.enableCustomize
: enableCustomize // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$LocalizationUIStateImplCopyWith<$Res>
implements $LocalizationUIStateCopyWith<$Res> {
factory _$$LocalizationUIStateImplCopyWith(_$LocalizationUIStateImpl value,
$Res Function(_$LocalizationUIStateImpl) then) =
__$$LocalizationUIStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String? selectedLanguage,
Map<String, ScLocalizationData>? apiLocalizationData,
String workingVersion,
MapEntry<bool, String>? patchStatus,
List<String>? customizeList,
bool enableCustomize});
}
/// @nodoc
class __$$LocalizationUIStateImplCopyWithImpl<$Res>
extends _$LocalizationUIStateCopyWithImpl<$Res, _$LocalizationUIStateImpl>
implements _$$LocalizationUIStateImplCopyWith<$Res> {
__$$LocalizationUIStateImplCopyWithImpl(_$LocalizationUIStateImpl _value,
$Res Function(_$LocalizationUIStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? selectedLanguage = freezed,
Object? apiLocalizationData = freezed,
Object? workingVersion = null,
Object? patchStatus = freezed,
Object? customizeList = freezed,
Object? enableCustomize = null,
}) {
return _then(_$LocalizationUIStateImpl(
selectedLanguage: freezed == selectedLanguage
? _value.selectedLanguage
: selectedLanguage // ignore: cast_nullable_to_non_nullable
as String?,
apiLocalizationData: freezed == apiLocalizationData
? _value._apiLocalizationData
: apiLocalizationData // ignore: cast_nullable_to_non_nullable
as Map<String, ScLocalizationData>?,
workingVersion: null == workingVersion
? _value.workingVersion
: workingVersion // ignore: cast_nullable_to_non_nullable
as String,
patchStatus: freezed == patchStatus
? _value.patchStatus
: patchStatus // ignore: cast_nullable_to_non_nullable
as MapEntry<bool, String>?,
customizeList: freezed == customizeList
? _value._customizeList
: customizeList // ignore: cast_nullable_to_non_nullable
as List<String>?,
enableCustomize: null == enableCustomize
? _value.enableCustomize
: enableCustomize // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$LocalizationUIStateImpl implements _LocalizationUIState {
const _$LocalizationUIStateImpl(
{this.selectedLanguage,
final Map<String, ScLocalizationData>? apiLocalizationData,
this.workingVersion = "",
this.patchStatus,
final List<String>? customizeList,
this.enableCustomize = false})
: _apiLocalizationData = apiLocalizationData,
_customizeList = customizeList;
@override
final String? selectedLanguage;
final Map<String, ScLocalizationData>? _apiLocalizationData;
@override
Map<String, ScLocalizationData>? get apiLocalizationData {
final value = _apiLocalizationData;
if (value == null) return null;
if (_apiLocalizationData is EqualUnmodifiableMapView)
return _apiLocalizationData;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override
@JsonKey()
final String workingVersion;
@override
final MapEntry<bool, String>? patchStatus;
final List<String>? _customizeList;
@override
List<String>? get customizeList {
final value = _customizeList;
if (value == null) return null;
if (_customizeList is EqualUnmodifiableListView) return _customizeList;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
@JsonKey()
final bool enableCustomize;
@override
String toString() {
return 'LocalizationUIState(selectedLanguage: $selectedLanguage, apiLocalizationData: $apiLocalizationData, workingVersion: $workingVersion, patchStatus: $patchStatus, customizeList: $customizeList, enableCustomize: $enableCustomize)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LocalizationUIStateImpl &&
(identical(other.selectedLanguage, selectedLanguage) ||
other.selectedLanguage == selectedLanguage) &&
const DeepCollectionEquality()
.equals(other._apiLocalizationData, _apiLocalizationData) &&
(identical(other.workingVersion, workingVersion) ||
other.workingVersion == workingVersion) &&
(identical(other.patchStatus, patchStatus) ||
other.patchStatus == patchStatus) &&
const DeepCollectionEquality()
.equals(other._customizeList, _customizeList) &&
(identical(other.enableCustomize, enableCustomize) ||
other.enableCustomize == enableCustomize));
}
@override
int get hashCode => Object.hash(
runtimeType,
selectedLanguage,
const DeepCollectionEquality().hash(_apiLocalizationData),
workingVersion,
patchStatus,
const DeepCollectionEquality().hash(_customizeList),
enableCustomize);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$LocalizationUIStateImplCopyWith<_$LocalizationUIStateImpl> get copyWith =>
__$$LocalizationUIStateImplCopyWithImpl<_$LocalizationUIStateImpl>(
this, _$identity);
}
abstract class _LocalizationUIState implements LocalizationUIState {
const factory _LocalizationUIState(
{final String? selectedLanguage,
final Map<String, ScLocalizationData>? apiLocalizationData,
final String workingVersion,
final MapEntry<bool, String>? patchStatus,
final List<String>? customizeList,
final bool enableCustomize}) = _$LocalizationUIStateImpl;
@override
String? get selectedLanguage;
@override
Map<String, ScLocalizationData>? get apiLocalizationData;
@override
String get workingVersion;
@override
MapEntry<bool, String>? get patchStatus;
@override
List<String>? get customizeList;
@override
bool get enableCustomize;
@override
@JsonKey(ignore: true)
_$$LocalizationUIStateImplCopyWith<_$LocalizationUIStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'localization_ui_model.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$localizationUIModelHash() =>
r'654fd38b5f38bee5fd2cab69ab003846a311a4ff';
/// See also [LocalizationUIModel].
@ProviderFor(LocalizationUIModel)
final localizationUIModelProvider = AutoDisposeNotifierProvider<
LocalizationUIModel, LocalizationUIState>.internal(
LocalizationUIModel.new,
name: r'localizationUIModelProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$localizationUIModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$LocalizationUIModel = AutoDisposeNotifier<LocalizationUIState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

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

View File

@ -1,15 +1,23 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/data/game_performance_data.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'performance_ui_model.dart';
class PerformanceUI extends BaseUI<PerformanceUIModel> {
class HomePerformanceUI extends HookConsumerWidget {
const HomePerformanceUI({super.key});
@override
Widget? buildBody(BuildContext context, PerformanceUIModel model) {
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(homePerformanceUIModelProvider);
final model = ref.read(homePerformanceUIModelProvider.notifier);
var content = makeLoading(context);
if (model.performanceMap != null) {
if (state.performanceMap != null) {
content = Stack(
children: [
Padding(
@ -20,7 +28,7 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
padding: const EdgeInsets.only(left: 24, right: 24),
child: Column(
children: [
if (model.showGraphicsPerformanceTip)
if (state.showGraphicsPerformanceTip)
InfoBar(
title: const Text("图形优化提示"),
content: const Text(
@ -32,7 +40,7 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
Row(
children: [
Text(
"当前状态:${model.enabled ? "已应用" : "未应用"}",
"当前状态:${state.enabled ? "已应用" : "未应用"}",
style: const TextStyle(fontSize: 18),
),
const SizedBox(width: 32),
@ -72,7 +80,7 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
" 恢复默认 ",
style: TextStyle(fontSize: 16),
),
onPressed: () => model.clean()),
onPressed: () => model.clean(context)),
const SizedBox(width: 24),
Button(
child: const Text(
@ -98,16 +106,16 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
crossAxisCount: 2,
mainAxisSpacing: 1,
crossAxisSpacing: 1,
itemCount: model.performanceMap!.length,
itemCount: state.performanceMap!.length,
itemBuilder: (context, index) {
return makeItemGroup(
model.performanceMap!.entries.elementAt(index));
return makeItemGroup(context,
state.performanceMap!.entries.elementAt(index), model);
},
)),
],
),
),
if (model.workingString.isNotEmpty)
if (state.workingString.isNotEmpty)
Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(150),
@ -118,7 +126,7 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
children: [
const ProgressRing(),
const SizedBox(height: 12),
Text(model.workingString),
Text(state.workingString),
],
),
),
@ -127,10 +135,16 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
);
}
return makeDefaultPage(context, model, content: content);
return makeDefaultPage(context,
title: "性能优化 -> ${model.scPath}",
useBodyContainer: true,
content: content);
}
Widget makeItemGroup(MapEntry<String?, List<GamePerformanceData>> group) {
Widget makeItemGroup(
BuildContext context,
MapEntry<String?, List<GamePerformanceData>> group,
HomePerformanceUIModel model) {
return Padding(
padding: const EdgeInsets.all(12),
child: Container(
@ -152,7 +166,7 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
color: FluentTheme.of(context).cardColor.withOpacity(.2),
height: 1),
const SizedBox(height: 6),
for (final item in group.value) makeItem(item)
for (final item in group.value) makeItem(context, item, model)
],
),
),
@ -160,8 +174,8 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
);
}
Widget makeItem(GamePerformanceData item) {
final model = ref.watch(provider);
Widget makeItem(BuildContext context, GamePerformanceData item,
HomePerformanceUIModel model) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Column(
@ -191,10 +205,10 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
v >= (item.min ?? 0)) {
item.value = v;
}
setState(() {});
model.updateState();
},
onTapOutside: (e) {
setState(() {});
model.updateState();
},
),
),
@ -207,7 +221,7 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
max: item.max?.toDouble() ?? 0,
onChanged: (double value) {
item.value = value.toInt();
setState(() {});
model.updateState();
},
),
)
@ -222,7 +236,7 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
checked: item.value == 1,
onChanged: (bool value) {
item.value = value ? 1 : 0;
setState(() {});
model.updateState();
},
)
],
@ -261,8 +275,4 @@ class PerformanceUI extends BaseUI<PerformanceUIModel> {
),
);
}
@override
String getUITitle(BuildContext context, PerformanceUIModel model) =>
"性能优化 ${model.scPath}";
}

View File

@ -1,70 +1,117 @@
// ignore_for_file: avoid_build_context_in_providers
import 'dart:convert';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/data/game_performance_data.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
class PerformanceUIModel extends BaseUIModel {
String scPath;
part 'performance_ui_model.freezed.dart';
PerformanceUIModel(this.scPath);
part 'performance_ui_model.g.dart';
TextEditingController customizeCtrl = TextEditingController();
@freezed
class HomePerformanceUIState with _$HomePerformanceUIState {
const factory HomePerformanceUIState({
@Default(true) bool showGraphicsPerformanceTip,
@Default(false) bool enabled,
Map<String, List<GamePerformanceData>>? performanceMap,
@Default("") String workingString,
}) = _HomePerformanceUIState;
}
Map<String?, List<GamePerformanceData>>? performanceMap;
@riverpod
class HomePerformanceUIModel extends _$HomePerformanceUIModel {
String get scPath => ref.read(homeUIModelProvider).scInstalledPath!;
List<String> inAppKeys = [];
final customizeCtrl = TextEditingController(text: "");
String workingString = "";
final List<String> _inAppKeys = [];
late final confFile = File("$scPath\\USER.cfg");
bool enabled = false;
bool showGraphicsPerformanceTip = false;
static const _graphicsPerformanceTipVersion = 1;
@override
Future loadData() async {
HomePerformanceUIState build() {
state = const HomePerformanceUIState();
_init();
return state;
}
Future<void> _init() async {
customizeCtrl.clear();
inAppKeys.clear();
_inAppKeys.clear();
final String jsonString =
await rootBundle.loadString('assets/performance.json');
final list = json.decode(jsonString);
if (list is List) {
performanceMap = {};
final performanceMap = <String, List<GamePerformanceData>>{};
for (var element in list) {
final item = GamePerformanceData.fromJson(element);
if (item.key != "customize") {
inAppKeys.add(item.key ?? "");
_inAppKeys.add(item.key ?? "");
}
performanceMap?[item.group] ??= [];
performanceMap?[item.group]?.add(item);
performanceMap[item.group!] ??= [];
performanceMap[item.group]?.add(item);
}
state = state.copyWith(performanceMap: performanceMap);
}
if (await confFile.exists()) {
await _readConf();
} else {
enabled = false;
state = state.copyWith(enabled: false);
}
final box = await Hive.openBox("app_conf");
final v = box.get("close_graphics_performance_tip", defaultValue: -1);
showGraphicsPerformanceTip = v != _graphicsPerformanceTipVersion;
notifyListeners();
state = state.copyWith(
showGraphicsPerformanceTip: v != _graphicsPerformanceTipVersion);
}
_readConf() async {
if (state.performanceMap == null) return;
state = state.copyWith(enabled: true);
final confString = await confFile.readAsString();
for (var value in confString.split("\n")) {
final kv = value.split("=");
for (var m in state.performanceMap!.entries) {
for (var value in m.value) {
if (value.key == kv[0].trim()) {
final v = int.tryParse(kv[1].trim());
if (v != null) value.value = v;
}
}
}
if (kv.length == 2 && !_inAppKeys.contains(kv[0].trim())) {
customizeCtrl.text =
"${customizeCtrl.text}${kv[0].trim()}=${kv[1].trim()}\n";
}
}
}
closeTip() async {
final box = await Hive.openBox("app_conf");
await box.put(
"close_graphics_performance_tip", _graphicsPerformanceTipVersion);
_init();
}
onChangePreProfile(String key) {
switch (key) {
case "low":
performanceMap?.forEach((key, v) {
if (key?.contains("图形") ?? false) {
state.performanceMap?.forEach((key, v) {
if (key.contains("图形")) {
for (var element in v) {
element.value = element.min;
}
@ -72,8 +119,8 @@ class PerformanceUIModel extends BaseUIModel {
});
break;
case "medium":
performanceMap?.forEach((key, v) {
if (key?.contains("图形") ?? false) {
state.performanceMap?.forEach((key, v) {
if (key.contains("图形")) {
for (var element in v) {
element.value = ((element.max ?? 0) ~/ 2);
}
@ -81,8 +128,8 @@ class PerformanceUIModel extends BaseUIModel {
});
break;
case "high":
performanceMap?.forEach((key, v) {
if (key?.contains("图形") ?? false) {
state.performanceMap?.forEach((key, v) {
if (key.contains("图形")) {
for (var element in v) {
element.value = ((element.max ?? 0) / 1.5).ceil();
}
@ -90,8 +137,8 @@ class PerformanceUIModel extends BaseUIModel {
});
break;
case "ultra":
performanceMap?.forEach((key, v) {
if (key?.contains("图形") ?? false) {
state.performanceMap?.forEach((key, v) {
if (key.contains("图形")) {
for (var element in v) {
element.value = element.max;
}
@ -99,16 +146,50 @@ class PerformanceUIModel extends BaseUIModel {
});
break;
}
notifyListeners();
state = state.copyWith();
}
refresh() async {
_init();
}
clean(BuildContext context) async {
state = state.copyWith(workingString: "删除配置文件...");
if (await confFile.exists()) {
await confFile.delete(recursive: true);
}
state = state.copyWith(workingString: "清理着色器");
if (!context.mounted) return;
await cleanShaderCache(context);
state = state.copyWith(workingString: "完成...");
await await Future.delayed(const Duration(milliseconds: 300));
await _init();
state = state.copyWith(workingString: "");
}
cleanShaderCache(BuildContext? context) async {
final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath();
final l =
await Directory(gameShaderCachePath!).list(recursive: false).toList();
for (var value in l) {
if (value is Directory) {
if (!value.absolute.path.contains("Crashes")) {
await value.delete(recursive: true);
}
}
}
await Future.delayed(const Duration(milliseconds: 300));
if (context != null && context.mounted) {
showToast(context, "清理着色器后首次进入游戏可能会出现卡顿,请耐心等待游戏初始化完毕。");
}
}
applyProfile(bool cleanShader) async {
if (performanceMap == null) return;
if (state.performanceMap == null) return;
AnalyticsApi.touch("performance_apply");
workingString = "生成配置文件";
notifyListeners();
state = state.copyWith(workingString: "生成配置文件");
String conf = "";
for (var v in performanceMap!.entries) {
for (var v in state.performanceMap!.entries) {
for (var c in v.value) {
if (c.key != "customize") {
conf = "$conf${c.key}=${c.value}\n";
@ -125,87 +206,23 @@ class PerformanceUIModel extends BaseUIModel {
}
}
}
workingString = "写出配置文件";
notifyListeners();
state = state.copyWith(workingString: "写出配置文件");
if (await confFile.exists()) {
await confFile.delete();
}
await confFile.create();
await confFile.writeAsString(conf);
if (cleanShader) {
workingString = "清理着色器";
notifyListeners();
await _cleanShaderCache();
state = state.copyWith(workingString: "清理着色器");
await cleanShaderCache(null);
}
workingString = "完成...";
notifyListeners();
state = state.copyWith(workingString: "完成...");
await await Future.delayed(const Duration(milliseconds: 300));
await reloadData();
workingString = "";
notifyListeners();
await _init();
state = state.copyWith(workingString: "清理着色器");
}
Future<void> _cleanShaderCache() async {
final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath();
final l =
await Directory(gameShaderCachePath!).list(recursive: false).toList();
for (var value in l) {
if (value is Directory) {
if (!value.absolute.path.contains("Crashes")) {
await value.delete(recursive: true);
}
}
}
await Future.delayed(const Duration(milliseconds: 300));
showToast(context!, "清理着色器后首次进入游戏可能会出现卡顿,请耐心等待游戏初始化完毕。");
}
_readConf() async {
if (performanceMap == null) return;
enabled = true;
final confString = await confFile.readAsString();
for (var value in confString.split("\n")) {
final kv = value.split("=");
for (var m in performanceMap!.entries) {
for (var value in m.value) {
if (value.key == kv[0].trim()) {
final v = int.tryParse(kv[1].trim());
if (v != null) value.value = v;
}
}
}
if (kv.length == 2 && !inAppKeys.contains(kv[0].trim())) {
customizeCtrl.text =
"${customizeCtrl.text}${kv[0].trim()}=${kv[1].trim()}\n";
}
}
notifyListeners();
}
clean() async {
workingString = "删除配置文件...";
notifyListeners();
if (await confFile.exists()) {
await confFile.delete(recursive: true);
}
workingString = "清理着色器";
notifyListeners();
await _cleanShaderCache();
workingString = "完成...";
await await Future.delayed(const Duration(milliseconds: 300));
await reloadData();
workingString = "";
notifyListeners();
}
refresh() async {
await reloadData();
}
closeTip() async {
final box = await Hive.openBox("app_conf");
await box.put(
"close_graphics_performance_tip", _graphicsPerformanceTipVersion);
loadData();
updateState() {
state = state.copyWith();
}
}

View File

@ -0,0 +1,224 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'performance_ui_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$HomePerformanceUIState {
bool get showGraphicsPerformanceTip => throw _privateConstructorUsedError;
bool get enabled => throw _privateConstructorUsedError;
Map<String, List<GamePerformanceData>>? get performanceMap =>
throw _privateConstructorUsedError;
String get workingString => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HomePerformanceUIStateCopyWith<HomePerformanceUIState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $HomePerformanceUIStateCopyWith<$Res> {
factory $HomePerformanceUIStateCopyWith(HomePerformanceUIState value,
$Res Function(HomePerformanceUIState) then) =
_$HomePerformanceUIStateCopyWithImpl<$Res, HomePerformanceUIState>;
@useResult
$Res call(
{bool showGraphicsPerformanceTip,
bool enabled,
Map<String, List<GamePerformanceData>>? performanceMap,
String workingString});
}
/// @nodoc
class _$HomePerformanceUIStateCopyWithImpl<$Res,
$Val extends HomePerformanceUIState>
implements $HomePerformanceUIStateCopyWith<$Res> {
_$HomePerformanceUIStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? showGraphicsPerformanceTip = null,
Object? enabled = null,
Object? performanceMap = freezed,
Object? workingString = null,
}) {
return _then(_value.copyWith(
showGraphicsPerformanceTip: null == showGraphicsPerformanceTip
? _value.showGraphicsPerformanceTip
: showGraphicsPerformanceTip // ignore: cast_nullable_to_non_nullable
as bool,
enabled: null == enabled
? _value.enabled
: enabled // ignore: cast_nullable_to_non_nullable
as bool,
performanceMap: freezed == performanceMap
? _value.performanceMap
: performanceMap // ignore: cast_nullable_to_non_nullable
as Map<String, List<GamePerformanceData>>?,
workingString: null == workingString
? _value.workingString
: workingString // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$HomePerformanceUIStateImplCopyWith<$Res>
implements $HomePerformanceUIStateCopyWith<$Res> {
factory _$$HomePerformanceUIStateImplCopyWith(
_$HomePerformanceUIStateImpl value,
$Res Function(_$HomePerformanceUIStateImpl) then) =
__$$HomePerformanceUIStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool showGraphicsPerformanceTip,
bool enabled,
Map<String, List<GamePerformanceData>>? performanceMap,
String workingString});
}
/// @nodoc
class __$$HomePerformanceUIStateImplCopyWithImpl<$Res>
extends _$HomePerformanceUIStateCopyWithImpl<$Res,
_$HomePerformanceUIStateImpl>
implements _$$HomePerformanceUIStateImplCopyWith<$Res> {
__$$HomePerformanceUIStateImplCopyWithImpl(
_$HomePerformanceUIStateImpl _value,
$Res Function(_$HomePerformanceUIStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? showGraphicsPerformanceTip = null,
Object? enabled = null,
Object? performanceMap = freezed,
Object? workingString = null,
}) {
return _then(_$HomePerformanceUIStateImpl(
showGraphicsPerformanceTip: null == showGraphicsPerformanceTip
? _value.showGraphicsPerformanceTip
: showGraphicsPerformanceTip // ignore: cast_nullable_to_non_nullable
as bool,
enabled: null == enabled
? _value.enabled
: enabled // ignore: cast_nullable_to_non_nullable
as bool,
performanceMap: freezed == performanceMap
? _value._performanceMap
: performanceMap // ignore: cast_nullable_to_non_nullable
as Map<String, List<GamePerformanceData>>?,
workingString: null == workingString
? _value.workingString
: workingString // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$HomePerformanceUIStateImpl implements _HomePerformanceUIState {
const _$HomePerformanceUIStateImpl(
{this.showGraphicsPerformanceTip = true,
this.enabled = false,
final Map<String, List<GamePerformanceData>>? performanceMap,
this.workingString = ""})
: _performanceMap = performanceMap;
@override
@JsonKey()
final bool showGraphicsPerformanceTip;
@override
@JsonKey()
final bool enabled;
final Map<String, List<GamePerformanceData>>? _performanceMap;
@override
Map<String, List<GamePerformanceData>>? get performanceMap {
final value = _performanceMap;
if (value == null) return null;
if (_performanceMap is EqualUnmodifiableMapView) return _performanceMap;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override
@JsonKey()
final String workingString;
@override
String toString() {
return 'HomePerformanceUIState(showGraphicsPerformanceTip: $showGraphicsPerformanceTip, enabled: $enabled, performanceMap: $performanceMap, workingString: $workingString)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HomePerformanceUIStateImpl &&
(identical(other.showGraphicsPerformanceTip,
showGraphicsPerformanceTip) ||
other.showGraphicsPerformanceTip ==
showGraphicsPerformanceTip) &&
(identical(other.enabled, enabled) || other.enabled == enabled) &&
const DeepCollectionEquality()
.equals(other._performanceMap, _performanceMap) &&
(identical(other.workingString, workingString) ||
other.workingString == workingString));
}
@override
int get hashCode => Object.hash(
runtimeType,
showGraphicsPerformanceTip,
enabled,
const DeepCollectionEquality().hash(_performanceMap),
workingString);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$HomePerformanceUIStateImplCopyWith<_$HomePerformanceUIStateImpl>
get copyWith => __$$HomePerformanceUIStateImplCopyWithImpl<
_$HomePerformanceUIStateImpl>(this, _$identity);
}
abstract class _HomePerformanceUIState implements HomePerformanceUIState {
const factory _HomePerformanceUIState(
{final bool showGraphicsPerformanceTip,
final bool enabled,
final Map<String, List<GamePerformanceData>>? performanceMap,
final String workingString}) = _$HomePerformanceUIStateImpl;
@override
bool get showGraphicsPerformanceTip;
@override
bool get enabled;
@override
Map<String, List<GamePerformanceData>>? get performanceMap;
@override
String get workingString;
@override
@JsonKey(ignore: true)
_$$HomePerformanceUIStateImplCopyWith<_$HomePerformanceUIStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'performance_ui_model.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$homePerformanceUIModelHash() =>
r'85e3390e954b35ffeb7cbacf85619b5a61f866bb';
/// See also [HomePerformanceUIModel].
@ProviderFor(HomePerformanceUIModel)
final homePerformanceUIModelProvider = AutoDisposeNotifierProvider<
HomePerformanceUIModel, HomePerformanceUIState>.internal(
HomePerformanceUIModel.new,
name: r'homePerformanceUIModelProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$homePerformanceUIModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$HomePerformanceUIModel = AutoDisposeNotifier<HomePerformanceUIState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -1,43 +0,0 @@
import 'package:markdown_widget/config/all.dart';
import 'package:markdown_widget/widget/blocks/leaf/code_block.dart';
import 'package:markdown_widget/widget/markdown.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'webview_localization_capture_ui_model.dart';
class WebviewLocalizationCaptureUI
extends BaseUI<WebviewLocalizationCaptureUIModel> {
@override
Widget? buildBody(
BuildContext context, WebviewLocalizationCaptureUIModel model) {
return makeDefaultPage(context, model,
content: model.data.isEmpty
? const Center(
child: Text("等待数据"),
)
: Column(
children: [
Expanded(
child: MarkdownWidget(
data: model.renderString,
config: MarkdownConfig(configs: [
const PreConfig(
decoration: BoxDecoration(
color: Color.fromRGBO(0, 0, 0, .4),
borderRadius: BorderRadius.all(Radius.circular(8.0)),
)),
]),
))
],
),
actions: [
IconButton(
icon: const Icon(FluentIcons.refresh), onPressed: model.doClean)
]);
}
@override
String getUITitle(
BuildContext context, WebviewLocalizationCaptureUIModel model) =>
"Webview 翻译捕获工具";
}

View File

@ -1,53 +0,0 @@
import 'dart:convert';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
class WebviewLocalizationCaptureUIModel extends BaseUIModel {
final WebViewModel webViewModel;
WebviewLocalizationCaptureUIModel(this.webViewModel);
Map<String, dynamic> data = {};
String renderString = "";
final jsonEncoder = const JsonEncoder.withIndent(' ');
@override
void initModel() {
webViewModel.addOnWebMessageReceivedCallback(_onMessage);
super.initModel();
}
@override
void dispose() {
webViewModel.removeOnWebMessageReceivedCallback(_onMessage);
super.dispose();
}
void _onMessage(String message) {
final map = json.decode(message);
if (map["action"] == "webview_localization_capture") {
dPrint(
"<WebviewLocalizationCaptureUIModel> webview_localization_capture message == $map");
final key = map["key"];
if (key != null && key.toString().trim() != "") {
if (!(webViewModel.curReplaceWords?.containsKey(key) ?? false)) {
data[key] = map["value"];
}
_updateRenderString();
}
}
}
_updateRenderString() {
renderString = "```json\n${jsonEncoder.convert(data)}\n```";
notifyListeners();
}
doClean() {
data.clear();
_updateRenderString();
}
}

View File

@ -1,23 +1,30 @@
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/main.dart';
import 'package:starcitizen_doctor/ui/about/about_ui.dart';
import 'package:starcitizen_doctor/ui/about/about_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/home_ui.dart';
import 'package:starcitizen_doctor/ui/party_room/party_room_home_ui_model.dart';
import 'package:starcitizen_doctor/ui/settings/settings_ui.dart';
import 'package:fluent_ui/fluent_ui.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/conf/const_conf.dart';
import 'package:starcitizen_doctor/provider/aria2c.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:starcitizen_doctor/ui/party_room/party_room_ui.dart';
import 'package:starcitizen_doctor/ui/settings/settings_ui_model.dart';
import 'package:starcitizen_doctor/ui/tools/tools_ui.dart';
import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:window_manager/window_manager.dart';
import 'home/home_ui_model.dart';
import 'index_ui_model.dart';
import 'party_room/party_room_home_ui.dart';
import 'about/about_ui.dart';
import 'home/home_ui.dart';
import 'settings/settings_ui.dart';
import 'tools/tools_ui.dart';
class IndexUI extends HookConsumerWidget {
const IndexUI({super.key});
class IndexUI extends BaseUI<IndexUIModel> {
@override
Widget? buildBody(BuildContext context, IndexUIModel model) {
Widget build(BuildContext context, WidgetRef ref) {
// pre init child
ref.watch(homeUIModelProvider.select((value) => null));
ref.watch(settingsUIModelProvider.select((value) => null));
final curIndex = useState(0);
return NavigationView(
appBar: NavigationAppBar(
automaticallyImplyLeading: false,
@ -35,7 +42,7 @@ class IndexUI extends BaseUI<IndexUIModel> {
),
const SizedBox(width: 12),
const Text(
"SC汉化盒子 V${AppConf.appVersion} ${AppConf.isMSE ? "" : " Dev"}")
"SC汉化盒子 V${ConstConf.appVersion} ${ConstConf.isMSE ? "" : " Dev"}")
],
),
),
@ -55,89 +62,42 @@ class IndexUI extends BaseUI<IndexUIModel> {
color: Colors.white.withOpacity(.6),
),
),
if (model.aria2TotalTaskNum != 0)
Positioned(
bottom: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.only(
left: 6, right: 6, bottom: 1.5, top: 1.5),
child: Text(
"${model.aria2TotalTaskNum}",
style: const TextStyle(
fontSize: 8,
color: Colors.white,
),
),
))
_makeAria2TaskNumWidget()
],
),
onPressed: model.goDownloader),
onPressed: () => _goDownloader(context),
// onPressed: model.goDownloader
),
const SizedBox(width: 24),
const WindowButtons()
],
)),
pane: NavigationPane(
selected: model.curIndex,
items: getNavigationPaneItems(model),
selected: curIndex.value,
items: getNavigationPaneItems(curIndex),
size: const NavigationPaneSize(openWidth: 64),
),
paneBodyBuilder: (item, child) {
// final name =
// item?.key is ValueKey ? (item!.key as ValueKey).value : null;
return FocusTraversalGroup(
key: ValueKey('body_${model.curIndex}'),
child: getPage(model),
key: ValueKey('body_$curIndex'),
child: getPage(curIndex.value),
);
},
);
}
Widget getPage(IndexUIModel model) {
switch (model.curIndex) {
case 0:
return BaseUIContainer(
uiCreate: () => HomeUI(),
modelCreate: () =>
model.getChildUIModelProviders<HomeUIModel>("home"));
case 1:
return BaseUIContainer(
uiCreate: () => PartyRoomHomeUI(),
modelCreate: () =>
model.getChildUIModelProviders<PartyRoomHomeUIModel>("party"));
case 2:
return BaseUIContainer(
uiCreate: () => ToolsUI(),
modelCreate: () =>
model.getChildUIModelProviders<ToolsUIModel>("tools"));
case 3:
return BaseUIContainer(
uiCreate: () => SettingUI(),
modelCreate: () =>
model.getChildUIModelProviders<SettingUIModel>("settings"));
case 4:
return BaseUIContainer(
uiCreate: () => AboutUI(),
modelCreate: () =>
model.getChildUIModelProviders<AboutUIModel>("about"));
}
return const SizedBox();
}
List<NavigationPaneItem> getNavigationPaneItems(IndexUIModel model) {
final menus = {
Map<IconData, String> get pageMenus => {
FluentIcons.home: "首页",
FluentIcons.game: "大厅",
FluentIcons.toolbox: "工具",
FluentIcons.settings: "设置",
FluentIcons.info: "关于",
};
List<NavigationPaneItem> getNavigationPaneItems(
ValueNotifier<int> curIndexState) {
return [
for (final kv in menus.entries)
for (final kv in pageMenus.entries)
PaneItem(
icon: Padding(
padding: const EdgeInsets.only(top: 6, bottom: 6, left: 4),
@ -154,13 +114,66 @@ class IndexUI extends BaseUI<IndexUIModel> {
),
// title: Text(kv.value),
body: const SizedBox.shrink(),
onTap: () {
model.onIndexMenuTap(kv.value);
},
onTap: () => _onTapIndexMenu(kv.value, curIndexState),
),
];
}
@override
String getUITitle(BuildContext context, IndexUIModel model) => "";
Widget getPage(int value) {
switch (value) {
case 0:
return const HomeUI();
case 1:
return const PartyRoomUI();
case 2:
return const ToolsUI();
case 3:
return const SettingsUI();
case 4:
return const AboutUI();
default:
return Center(
child: Text("UnimplPage $value"),
);
}
}
void _onTapIndexMenu(String value, ValueNotifier<int> curIndexState) {
final pageIndex =
pageMenus.values.toList().indexWhere((element) => element == value);
curIndexState.value = pageIndex;
}
Widget _makeAria2TaskNumWidget() {
return Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
final aria2cState = ref.watch(aria2cModelProvider);
if (!aria2cState.hasDownloadTask) {
return const SizedBox();
}
return Positioned(
bottom: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.only(
left: 6, right: 6, bottom: 1.5, top: 1.5),
child: Text(
"${aria2cState.aria2TotalTaskNum}",
style: const TextStyle(
fontSize: 8,
color: Colors.white,
),
),
));
},
);
}
_goDownloader(BuildContext context) {
context.push('/index/downloader');
}
}

View File

@ -1,124 +0,0 @@
import 'dart:io';
import 'package:aria2/models/aria2GlobalStat.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/io/aria2c.dart';
import 'package:starcitizen_doctor/global_ui_model.dart';
import 'package:starcitizen_doctor/ui/about/about_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/downloader/downloader_ui.dart';
import 'package:starcitizen_doctor/ui/home/downloader/downloader_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:starcitizen_doctor/ui/settings/settings_ui_model.dart';
import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'party_room/party_room_home_ui_model.dart';
class IndexUIModel extends BaseUIModel {
int curIndex = 0;
Aria2GlobalStat? aria2globalStat;
int get aria2TotalTaskNum => aria2globalStat == null
? 0
: ((aria2globalStat!.numActive ?? 0) +
(aria2globalStat!.numWaiting ?? 0));
@override
void initModel() {
_checkRuntime();
_listenAria2c();
Future.delayed(const Duration(milliseconds: 300))
.then((value) => globalUIModel.doCheckUpdate(context!));
super.initModel();
}
@override
BaseUIModel? onCreateChildUIModel(modelKey) {
switch (modelKey) {
case "home":
return HomeUIModel();
case "tools":
return ToolsUIModel();
case "settings":
return SettingUIModel();
case "about":
return AboutUIModel();
case "party":
return PartyRoomHomeUIModel();
}
return null;
}
void onIndexMenuTap(String value) {
final index = {
"首页": 0,
"大厅": 1,
"工具": 2,
"设置": 3,
"关于": 4,
};
curIndex = index[value] ?? 0;
switch (curIndex) {
case 0:
getCreatedChildUIModel("home")?.reloadData();
break;
case 1:
getCreatedChildUIModel("party")?.reloadData();
break;
case 2:
getCreatedChildUIModel("tools")?.reloadData();
break;
case 3:
getCreatedChildUIModel("settings")?.reloadData();
break;
}
notifyListeners();
}
Future<void> _checkRuntime() async {
Future<void> onError() async {
await showToast(context!, "运行环境出错,请检查系统环境变量 PATH");
await launchUrlString(
"https://answers.microsoft.com/zh-hans/windows/forum/all/%E7%B3%BB%E7%BB%9F%E7%8E%AF%E5%A2%83%E5%8F%98/b88369e6-2620-4a77-b07a-d0af50894a07");
await AnalyticsApi.touch("error_powershell");
exit(0);
}
try {
var result =
await Process.run(SystemHelper.powershellPath, ["echo", "ping"]);
if (result.stdout.toString().startsWith("ping")) {
dPrint("powershell check pass");
} else {
onError();
}
} catch (e) {
onError();
}
}
Future<void> goDownloader() async {
await BaseUIContainer(
uiCreate: () => DownloaderUI(),
modelCreate: () => DownloaderUIModel()).push(context!);
}
void _listenAria2c() async {
while (true) {
if (!mounted) return;
try {
if (Aria2cManager.isAvailable) {
final aria2c = Aria2cManager.getClient();
aria2globalStat = await aria2c.getGlobalStat();
notifyListeners();
}
} catch (e) {
dPrint("aria2globalStat update error:$e");
}
await Future.delayed(const Duration(seconds: 5));
}
}
}

View File

@ -1,197 +0,0 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pb.dart';
import 'party_room_create_dialog_ui_model.dart';
class PartyRoomCreateDialogUI extends BaseUI<PartyRoomCreateDialogUIModel> {
@override
Widget? buildBody(BuildContext context, PartyRoomCreateDialogUIModel model) {
return ContentDialog(
title: makeTitle(context, model),
constraints:
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .6),
content: Padding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
child: AnimatedSize(
duration: const Duration(milliseconds: 130),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (model.userName == null) ...[
SizedBox(
height: 200,
child: makeLoading(context),
)
] else ...[
Row(
children: [
const Text("请选择一种玩法"),
const SizedBox(width: 12),
Expanded(
child: SizedBox(
height: 36,
child: ComboBox<RoomType>(
value: model.selectedRoomType,
items: [
for (final t in model.roomTypes.entries)
ComboBoxItem(
value: t.value,
child: Text(t.value.name),
)
],
onChanged: model.onChangeRoomType)),
)
],
),
if (model.selectedRoomType != null &&
(model.selectedRoomType?.subTypes.isNotEmpty ?? false))
...makeSubTypeSelectWidgets(context, model),
const SizedBox(height: 24),
Row(
children: [
const Text("游戏用户名(自动获取)"),
const SizedBox(width: 12),
Expanded(
child: TextFormBox(
initialValue: model.userName,
enabled: false,
),
),
],
),
const SizedBox(height: 24),
Row(
children: [
const Text("最大玩家数2 ~ 32"),
const SizedBox(width: 12),
Expanded(
child: TextFormBox(
controller: model.playerMaxCtrl,
onChanged: (_) => model.notifyListeners(),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: 24),
const Text("公告(可选)"),
const SizedBox(height: 12),
TextFormBox(
controller: model.announcementCtrl,
maxLines: 5,
placeholder: "可编写 任务简报,集合地点,船只要求,活动规则等,公告将会自动发送给进入房间的玩家。",
placeholderStyle:
TextStyle(color: Colors.white.withOpacity(.4)),
),
const SizedBox(height: 32),
for (var v in [
"创建房间后,其他玩家可以在大厅首页看到您的房间和您选择的玩法,当一个玩家选择加入房间时,你们将可以互相看到对方的用户名。当房间人数达到最大玩家数时,将不再接受新的玩家加入。",
"这是《SC汉化盒子》提供的公益服务请勿滥用我们保留拒绝服务的权力。"
]) ...[
Text(
v,
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.6)),
),
const SizedBox(height: 6),
],
]
],
),
),
),
actions: [
if (model.isWorking)
const ProgressRing()
else
FilledButton(
onPressed: model.onSubmit(),
child: const Padding(
padding: EdgeInsets.all(3),
child: Text("创建房间"),
))
],
);
}
Color generateColorFromSeed(String seed) {
int hash = utf8
.encode(seed)
.fold(0, (previousValue, element) => 31 * previousValue + element);
Random random = Random(hash);
return Color.fromARGB(
255, random.nextInt(256), random.nextInt(256), random.nextInt(256));
}
List<Widget> makeSubTypeSelectWidgets(
BuildContext context, PartyRoomCreateDialogUIModel model) {
bool isItemSelected(RoomSubtype subtype) {
return model.selectedSubType.contains(subtype);
}
return [
const SizedBox(height: 24),
const Text("标签(可选)"),
const SizedBox(height: 12),
Row(
children: [
for (var item in model.selectedRoomType!.subTypes)
Container(
decoration: BoxDecoration(
color: isItemSelected(item)
? generateColorFromSeed(item.name).withOpacity(.4)
: FluentTheme.of(context).cardColor,
borderRadius: BorderRadius.circular(1000)),
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 12),
margin: const EdgeInsets.only(right: 12),
child: IconButton(
icon: Row(
children: [
Icon(isItemSelected(item)
? FluentIcons.check_mark
: FluentIcons.add),
const SizedBox(width: 12),
Text(
item.name,
style: TextStyle(
fontSize: 13,
color: isItemSelected(item)
? null
: Colors.white.withOpacity(.4)),
),
],
),
onPressed: () => model.onTapSubType(item)),
)
],
)
];
}
Widget makeTitle(BuildContext context, PartyRoomCreateDialogUIModel model) {
return Row(
children: [
IconButton(
icon: const Icon(
FluentIcons.back,
size: 22,
),
onPressed: model.onBack()),
const SizedBox(width: 12),
Text(getUITitle(context, model)),
],
);
}
@override
String getUITitle(BuildContext context, PartyRoomCreateDialogUIModel model) =>
"创建房间";
}

View File

@ -1,79 +0,0 @@
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/grpc/party_room_server.dart';
import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pb.dart';
import 'package:starcitizen_doctor/global_ui_model.dart';
class PartyRoomCreateDialogUIModel extends BaseUIModel {
Map<String?, RoomType> roomTypes;
RoomType? selectedRoomType;
List<RoomSubtype> selectedSubType = [];
PartyRoomCreateDialogUIModel(this.roomTypes);
String? userName;
bool isWorking = false;
final playerMaxCtrl = TextEditingController(text: "8");
final announcementCtrl = TextEditingController();
@override
initModel() {
super.initModel();
roomTypes.removeWhere((key, value) => key == "");
}
@override
loadData() async {
userName = await globalUIModel.getRunningGameUser();
notifyListeners();
}
onBack() {
if (isWorking) return null;
return () {
Navigator.pop(context!);
};
}
void onChangeRoomType(RoomType? value) {
selectedSubType = [];
selectedRoomType = value;
notifyListeners();
}
onTapSubType(RoomSubtype item) {
if (!selectedSubType.contains(item)) {
selectedSubType.add(item);
} else {
selectedSubType.remove(item);
}
notifyListeners();
}
onSubmit() {
final maxPlayer = int.tryParse(playerMaxCtrl.text) ?? 0;
if (selectedRoomType == null) return null;
if (maxPlayer < 2 || maxPlayer > 32) return null;
return () async {
isWorking = true;
notifyListeners();
final room = await handleError(() => PartyRoomGrpcServer.createRoom(
RoomData(
roomTypeID: selectedRoomType?.id,
roomSubTypeIds: [for (var value in selectedSubType) value.id],
owner: userName,
deviceUUID: AppConf.deviceUUID,
maxPlayer: maxPlayer,
announcement: announcementCtrl.text.trim())));
isWorking = false;
notifyListeners();
if (room != null) {
Navigator.pop(context!, room);
}
};
}
}

View File

@ -1,148 +0,0 @@
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pb.dart';
import 'package:starcitizen_doctor/widgets/cache_image.dart';
import 'party_room_chat_ui_model.dart';
class PartyRoomChatUI extends BaseUI<PartyRoomChatUIModel> {
@override
Widget? buildBody(BuildContext context, PartyRoomChatUIModel model) {
final roomData = model.serverResultRoomData;
if (roomData == null) return makeLoading(context);
final typesMap = model.partyRoomHomeUIModel.roomTypes;
final title =
"${roomData.owner}${typesMap?[roomData.roomTypeID]?.name ?? roomData.roomTypeID}房间";
// final createTime =
// DateTime.fromMillisecondsSinceEpoch(roomData.createTime.toInt());
return Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.black.withOpacity(.25)),
child: makeTitleBar(model, title, roomData),
),
Expanded(
child: Row(
children: [
Container(
width: 220,
padding: const EdgeInsets.only(left: 12, right: 12),
decoration: BoxDecoration(color: Colors.black.withOpacity(.07)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
makeItemRow("玩家数量:",
"${model.playersMap?.length ?? 0} / ${roomData.maxPlayer}"),
const SizedBox(height: 12),
if (model.playersMap == null)
Expanded(child: makeLoading(context))
else
Expanded(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
final item = model.playersMap!.entries
.elementAt(index)
.value;
return Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: CacheNetImage(
url: item.avatar,
width: 28,
height: 28,
),
),
const SizedBox(width: 6),
Expanded(child: Text(item.playerName)),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.only(
top: 3, bottom: 3, left: 12, right: 12),
decoration: BoxDecoration(
color: Colors.green,
borderRadius:
BorderRadius.circular(1000)),
child: Text(
"${model.playerStatusMap[item.status] ?? item.status}",
style: const TextStyle(fontSize: 13),
),
)
],
);
},
itemCount: model.playersMap!.length,
),
)
],
),
),
],
),
)
],
);
}
Widget makeItemRow(String title, String value) {
return Padding(
padding: const EdgeInsets.only(top: 1, bottom: 1),
child: Row(
children: [
Text(
title,
style: TextStyle(color: Colors.white.withOpacity(.6)),
),
const SizedBox(width: 12),
Expanded(
child: Text(
value,
),
),
],
),
);
}
Widget makeTitleBar(
PartyRoomChatUIModel model, String title, RoomData roomData) {
return Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: CacheNetImage(
url: roomData.avatar,
width: 32,
height: 32,
),
),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(fontSize: 18),
),
const SizedBox(width: 12),
Container(
padding:
const EdgeInsets.only(top: 3, bottom: 3, left: 12, right: 12),
decoration: BoxDecoration(
color: Colors.green, borderRadius: BorderRadius.circular(1000)),
child: Text(
"${model.partyRoomHomeUIModel.roomStatus[roomData.status]}")),
const SizedBox(width: 12),
const Spacer(),
IconButton(
icon: const Icon(
FluentIcons.leave,
size: 20,
),
onPressed: () => model.onClose())
],
);
}
@override
String getUITitle(BuildContext context, PartyRoomChatUIModel model) => "Chat";
}

View File

@ -1,93 +0,0 @@
import 'package:grpc/grpc.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/grpc/party_room_server.dart';
import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pb.dart';
import 'package:starcitizen_doctor/global_ui_model.dart';
import 'party_room_home_ui_model.dart';
class PartyRoomChatUIModel extends BaseUIModel {
PartyRoomHomeUIModel partyRoomHomeUIModel;
PartyRoomChatUIModel(this.partyRoomHomeUIModel);
RoomData? selectRoom;
RoomData? serverResultRoomData;
ResponseStream<RoomUpdateMessage>? roomStream;
Map<String, RoomUserData>? playersMap;
setRoom(RoomData? selectRoom) {
if (this.selectRoom == null) {
this.selectRoom = selectRoom;
notifyListeners();
loadRoom();
}
notifyListeners();
}
final playerStatusMap = {
RoomUserStatus.RoomUserStatusJoin: "在线",
RoomUserStatus.RoomUserStatusLostOffline: "离线",
RoomUserStatus.RoomUserStatusLeave: "已离开",
RoomUserStatus.RoomUserStatusWaitingConnect: "正在连接...",
};
onClose() async {
final ok = await showConfirmDialogs(
context!, "确认离开房间?", const Text("离开房间后,您的位置将被释放。"));
if (ok == true) {
partyRoomHomeUIModel.pageCtrl.animateToPage(0,
duration: const Duration(milliseconds: 130),
curve: Curves.easeInOutSine);
disposeRoom();
}
}
loadRoom() async {
if (selectRoom == null) return;
final userName = await globalUIModel.getRunningGameUser();
if (userName == null) return;
roomStream = PartyRoomGrpcServer.joinRoom(
selectRoom!.id, userName, AppConf.deviceUUID);
roomStream!.listen((value) {
dPrint("PartyRoomChatUIModel.roomStream.listen === $value");
if (value.roomUpdateType == RoomUpdateType.RoomClose) {
} else if (value.roomUpdateType == RoomUpdateType.RoomUpdateData) {
if (value.hasRoomData()) {
serverResultRoomData = value.roomData;
}
if (value.usersData.isNotEmpty) {
_updatePlayerList(value.usersData);
}
notifyListeners();
}
})
..onError((err) {
// showToast(context!, "连接到服务器出现错误:$err");
dPrint("PartyRoomChatUIModel.roomStream onError $err");
})
..onDone(() {
// showToast(context!, "房间已关闭");
dPrint("PartyRoomChatUIModel.roomStream onDone");
});
}
disposeRoom() {
selectRoom = null;
roomStream?.cancel();
roomStream = null;
notifyListeners();
}
void _updatePlayerList(List<RoomUserData> usersData) {
playersMap ??= {};
for (var element in usersData) {
playersMap![element.playerName] = element;
}
notifyListeners();
}
}

View File

@ -1,357 +0,0 @@
import 'dart:convert';
import 'dart:math';
import 'dart:ui';
import 'package:extended_image/extended_image.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pb.dart';
import 'package:starcitizen_doctor/widgets/cache_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'party_room_home_ui_model.dart';
class PartyRoomHomeUI extends BaseUI<PartyRoomHomeUIModel> {
@override
void initState() {
Future.delayed(const Duration(milliseconds: 16)).then((_) {
ref.watch(provider).checkUIInit();
});
super.initState();
}
@override
Widget build(BuildContext context) {
// final model = ref.watch(provider);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"联机大厅,敬请期待 ",
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
launchUrlString("https://wj.qq.com/s2/14112124/f4c8/");
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text("诚邀您参与 "),
Text(
"问卷调查。",
style: TextStyle(
color: Colors.blue,
),
)
],
),
),
],
),
);
// return PageView(
// controller: model.pageCtrl,
// physics: const NeverScrollableScrollPhysics(),
// children: [
// super.build(context),
// BaseUIContainer(
// uiCreate: () => PartyRoomChatUI(),
// modelCreate: () =>
// model.getChildUIModelProviders<PartyRoomChatUIModel>("chat"))
// ],
// );
}
@override
Widget? buildBody(BuildContext context, PartyRoomHomeUIModel model) {
if (model.pingServerMessage == null) return makeLoading(context);
if (model.pingServerMessage!.isNotEmpty) {
return Center(
child: Text("${model.pingServerMessage}"),
);
}
if (model.roomTypes == null) return makeLoading(context);
return Column(
children: [
makeHeader(context, model),
if (model.rooms == null)
Expanded(child: makeLoading(context))
else if (model.rooms!.isEmpty)
const Expanded(
child: Center(
child: Text("没有符合条件的房间!"),
))
else
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: AlignedGridView.count(
crossAxisCount: 3,
mainAxisSpacing: 24,
crossAxisSpacing: 24,
itemCount: model.rooms!.length,
itemBuilder: (context, index) {
return makeRoomItemWidget(context, model, index);
},
),
),
),
],
);
}
Widget makeRoomItemWidget(
BuildContext context,
PartyRoomHomeUIModel model,
int index,
) {
final item = model.rooms![index];
final itemType = model.roomTypes?[item.roomTypeID];
final itemSubTypes = {
for (var t in itemType?.subTypes ?? <RoomSubtype>[]) t.id: t
};
final createTime =
DateTime.fromMillisecondsSinceEpoch(item.createTime.toInt());
return Tilt(
borderRadius: BorderRadius.circular(13),
clipBehavior: Clip.hardEdge,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(13),
image: DecorationImage(
image: ExtendedNetworkImageProvider(item.avatar, cache: true),
fit: BoxFit.cover)),
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(.4),
borderRadius: BorderRadius.circular(13),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(13),
clipBehavior: Clip.hardEdge,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Padding(
padding: const EdgeInsets.only(
left: 16, right: 16, top: 12, bottom: 12),
child: GestureDetector(
onTap: () => model.onTapRoom(item),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
"${itemType?.name ?? item.roomTypeID}房间",
style: const TextStyle(fontSize: 20),
),
const SizedBox(width: 16),
Container(
padding: const EdgeInsets.only(
top: 3, bottom: 3, left: 12, right: 12),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(1000)),
child: Text("${model.roomStatus[item.status]}")),
],
),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
makeItemRow("房主:", item.owner),
makeItemRow("玩家数量:",
"${item.curPlayer} / ${item.maxPlayer}"),
makeItemRow("创建时间:", "${createTime.toLocal()}"),
],
),
),
ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: CacheNetImage(
url: item.avatar,
width: 64,
height: 64,
),
),
],
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var value in item.roomSubTypeIds)
makeSubTypeTag(value, model, itemSubTypes),
],
),
)
],
),
),
),
),
),
),
),
);
}
Widget makeSubTypeTag(String id, PartyRoomHomeUIModel model,
Map<String, RoomSubtype> itemSubTypes) {
final name = itemSubTypes[id]?.name ?? id;
final color = generateColorFromSeed(name).withOpacity(.6);
return Container(
padding: const EdgeInsets.only(left: 12, right: 12, top: 5, bottom: 5),
margin: const EdgeInsets.only(right: 6),
decoration:
BoxDecoration(color: color, borderRadius: BorderRadius.circular(12)),
child: Text(
name,
style: const TextStyle(fontSize: 13),
),
);
}
Color generateColorFromSeed(String seed) {
int hash = utf8
.encode(seed)
.fold(0, (previousValue, element) => 31 * previousValue + element);
Random random = Random(hash);
return Color.fromARGB(
255, random.nextInt(256), random.nextInt(256), random.nextInt(256));
}
Widget makeItemRow(String title, String value) {
return Padding(
padding: const EdgeInsets.only(top: 1, bottom: 1),
child: Row(
children: [
Text(
title,
style: TextStyle(color: Colors.white.withOpacity(.6)),
),
const SizedBox(width: 12),
Expanded(
child: Text(
value,
),
),
],
),
);
}
Widget makeHeader(BuildContext context, PartyRoomHomeUIModel model) {
final subTypes = model.getCurRoomSubTypes();
return Container(
padding: const EdgeInsets.only(left: 24, right: 24, top: 16, bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text("房间类型:"),
SizedBox(
height: 36,
child: ComboBox<RoomType>(
value: model.selectedRoomType,
items: [
for (final t in model.roomTypes!.entries)
ComboBoxItem(
value: t.value,
child: Text(t.value.name),
)
],
onChanged: model.onChangeRoomType)),
if (subTypes != null) ...[
const SizedBox(width: 24),
const Text("子类型:"),
SizedBox(
height: 36,
child: ComboBox<RoomSubtype>(
value: model.selectedRoomSubType,
items: [
for (final t in subTypes.entries)
ComboBoxItem(
value: t.value,
child: Text(t.value.name),
)
],
onChanged: model.onChangeRoomSubType)),
],
const SizedBox(width: 24),
const Text("房间状态:"),
SizedBox(
height: 36,
child: ComboBox<RoomStatus>(
value: model.selectedStatus,
items: [
for (final t in model.roomStatus.entries)
ComboBoxItem(
value: t.key,
child: Text(t.value),
)
],
onChanged: model.onChangeRoomStatus)),
const SizedBox(width: 24),
const Text("排序:"),
SizedBox(
height: 36,
child: ComboBox<RoomSortType>(
value: model.selectedSortType,
items: [
for (final t in model.roomSorts.entries)
ComboBoxItem(
value: t.key,
child: Text(t.value),
)
],
onChanged: model.onChangeRoomSort)),
const Spacer(),
Button(
onPressed: model.onRefreshRoom,
child: const Padding(
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.refresh),
),
),
const SizedBox(width: 12),
Button(
onPressed: () => model.onCreateRoom(),
child: const Padding(
padding: EdgeInsets.all(3),
child: Row(
children: [
Icon(FluentIcons.add),
SizedBox(width: 6),
Text("创建房间")
],
),
),
),
],
),
const SizedBox(
height: 6,
),
Text(
model.selectedRoomType?.desc ?? "",
style: TextStyle(fontSize: 13, color: Colors.white.withOpacity(.4)),
),
],
),
);
}
@override
String getUITitle(BuildContext context, PartyRoomHomeUIModel model) =>
"PartyRoom";
}

View File

@ -1,204 +0,0 @@
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/generated/grpc/party_room_server/index.pb.dart';
import 'package:starcitizen_doctor/ui/party_room/dialogs/party_room_create_dialog_ui_model.dart';
import 'dialogs/party_room_create_dialog_ui.dart';
import 'party_room_chat_ui_model.dart';
class PartyRoomHomeUIModel extends BaseUIModel {
String? pingServerMessage;
Map<String?, RoomType>? roomTypes;
RoomType? selectedRoomType;
RoomSubtype? selectedRoomSubType;
final roomStatus = <RoomStatus, String>{
RoomStatus.All: "全部",
RoomStatus.Open: "开启中",
RoomStatus.Full: "已满员",
RoomStatus.Closed: "已封闭",
RoomStatus.WillOffline: "房主离线",
RoomStatus.Offline: "已离线",
};
RoomStatus selectedStatus = RoomStatus.All;
final roomSorts = <RoomSortType, String>{
RoomSortType.Default: "默认",
RoomSortType.MostPlayerNumber: "最多玩家",
RoomSortType.MinimumPlayerNumber: "最少玩家",
RoomSortType.RecentlyCreated: "最近创建",
RoomSortType.OldestCreated: "最久创建",
};
RoomSortType selectedSortType = RoomSortType.Default;
int pageNum = 0;
List<RoomData>? rooms;
final pageCtrl = PageController();
@override
BaseUIModel? onCreateChildUIModel(modelKey) {
switch (modelKey) {
case "chat":
return PartyRoomChatUIModel(this);
}
return null;
}
@override
Future loadData() async {
// if (pingServerMessage != "") {
// pingServerMessage = null;
// notifyListeners();
// await _pingServer();
// }
// await _loadPage();
}
// @override
// reloadData() async {
// pageNum = 0;
// rooms = null;
// notifyListeners();
// _touchUser();
// return super.reloadData();
// }
// _loadPage() async {
// final r = await handleError(() => PartyRoomGrpcServer.getRoomList(
// RoomListPageReqData(
// pageNum: Int64.tryParseInt("$pageNum"),
// typeID: selectedRoomType?.id ?? "",
// subTypeID: selectedRoomSubType?.id ?? "",
// status: selectedStatus)));
// if (r == null) return;
// if (r.pageData.hasNext) {
// pageNum++;
// } else {
// pageNum = -1;
// }
// rooms = r.rooms;
// notifyListeners();
// }
//
// _pingServer() async {
// try {
// final r = await PartyRoomGrpcServer.pingServer();
// dPrint(
// "[PartyRoomHomeUIModel] Connected! serverVersion ==> ${r.serverVersion}");
// pingServerMessage = "";
// notifyListeners();
// } catch (e) {
// pingServerMessage = "服务器连接失败,请稍后重试。\n$e";
// notifyListeners();
// return;
// }
// }
//
// Future<void> _loadTypes() async {
// final r = await handleError(() => PartyRoomGrpcServer.getRoomTypes());
// if (r == null) return;
// selectedRoomType =
// RoomType(id: "", name: "全部", desc: "查看所有类型的房间,寻找一起玩的伙伴。");
// selectedRoomSubType = RoomSubtype(id: "", name: "全部");
// roomTypes = {"": selectedRoomType!};
// for (var element in r.roomTypes) {
// roomTypes![element.id] = element;
// }
// notifyListeners();
// }
Map<String, RoomSubtype>? getCurRoomSubTypes() {
if (selectedRoomType?.subTypes == null) return null;
Map<String, RoomSubtype> types = {};
for (var element in selectedRoomType!.subTypes) {
types[element.id] = element;
}
if (types.isEmpty) return null;
final allSubType = RoomSubtype(id: "", name: "全部");
selectedRoomSubType ??= allSubType;
return {"all": allSubType}..addAll(types);
}
void onChangeRoomType(RoomType? value) {
selectedRoomType = value;
selectedRoomSubType = null;
reloadData();
notifyListeners();
}
void onChangeRoomStatus(RoomStatus? value) {
if (value == null) return;
selectedStatus = value;
reloadData();
notifyListeners();
}
void onChangeRoomSort(RoomSortType? value) {
if (value == null) return;
selectedSortType = value;
reloadData();
notifyListeners();
}
void onChangeRoomSubType(RoomSubtype? value) {
if (value == null) return;
selectedRoomSubType = value;
reloadData();
notifyListeners();
}
onCreateRoom() async {
final room = await showDialog(
context: context!,
dismissWithEsc: false,
builder: (BuildContext context) {
return BaseUIContainer(
uiCreate: () => PartyRoomCreateDialogUI(),
modelCreate: () =>
PartyRoomCreateDialogUIModel(Map.from(roomTypes!)));
},
);
if (room == null) return;
dPrint(room);
reloadData();
}
onRefreshRoom() {
reloadData();
}
// Future<void> _touchUser() async {
// if (getCreatedChildUIModel<PartyRoomChatUIModel>("chat")?.selectRoom ==
// null) {
// final userName = await globalUIModel.getRunningGameUser();
// if (userName == null) return;
// //
// final room = await handleError(() =>
// PartyRoomGrpcServer.touchUserRoom(userName, AppConf.deviceUUID));
// dPrint("touch room == ${room?.toProto3Json()}");
// if (room == null || room.id == "") return;
// onTapRoom(room);
// }
// }
onTapRoom(RoomData item) {
getCreatedChildUIModel<PartyRoomChatUIModel>("chat", create: true)
?.setRoom(item);
notifyListeners();
pageCtrl.animateToPage(1,
duration: const Duration(milliseconds: 100), curve: Curves.easeInExpo);
}
void checkUIInit() {
if (getCreatedChildUIModel<PartyRoomChatUIModel>("chat")?.selectRoom !=
null) {
pageCtrl.jumpToPage(1);
}
}
}

View File

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:url_launcher/url_launcher_string.dart';
class PartyRoomUI extends HookConsumerWidget {
const PartyRoomUI({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"联机大厅,敬请期待 ",
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
launchUrlString("https://wj.qq.com/s2/14112124/f4c8/");
},
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("诚邀您参与 "),
Text(
"问卷调查。",
style: TextStyle(
color: Colors.blue,
),
)
],
),
),
],
),
);
}
}

View File

@ -1,11 +1,16 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/ui/settings/settings_ui_model.dart';
class SettingUI extends BaseUI<SettingUIModel> {
class SettingsUI extends HookConsumerWidget {
const SettingsUI({super.key});
@override
Widget? buildBody(BuildContext context, SettingUIModel model) {
Widget build(BuildContext context, WidgetRef ref) {
final sate = ref.watch(settingsUIModelProvider);
final model = ref.read(settingsUIModelProvider.notifier);
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
@ -13,53 +18,53 @@ class SettingUI extends BaseUI<SettingUIModel> {
child: Column(
children: [
makeSettingsItem(const Icon(FluentIcons.link, size: 20), "创建设置快捷方式",
subTitle: "在桌面创建《SC汉化盒子》快捷方式", onTap: model.addShortCut),
if (AppConf.isMSE) ...[
subTitle: "在桌面创建《SC汉化盒子》快捷方式", onTap: ()=> model.addShortCut(context)),
if (ConstConf.isMSE) ...[
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FluentIcons.reset_device, size: 20), "重置自动密码填充",
subTitle:
"启用:${model.isEnableAutoLogin ? "已启用" : "已禁用"} 设备支持:${model.isDeviceSupportWinHello ? "支持" : "不支持"} 邮箱:${model.autoLoginEmail} 密码:${model.isEnableAutoLoginPwd ? "已加密保存" : "未保存"}",
onTap: model.onResetAutoLogin),
"启用:${sate.isEnableAutoLogin ? "已启用" : "已禁用"} 设备支持:${sate.isDeviceSupportWinHello ? "支持" : "不支持"} 邮箱:${sate.autoLoginEmail} 密码:${sate.isEnableAutoLoginPwd ? "已加密保存" : "未保存"}",
onTap: ()=> model.onResetAutoLogin(context)),
],
const SizedBox(height: 12),
makeSettingsItem(const Icon(FontAwesomeIcons.microchip, size: 20),
"启动游戏时忽略能效核心( 适用于Intel 12th+ 处理器 [实验性功能,请随时反馈]",
subTitle:
"已设置的核心数量:${model.inputGameLaunchECore} (此功能适用于首页的盒子一键启动 或 工具中的RSI启动器管理员模式当为 0 时不启用此功能 ",
onTap: model.setGameLaunchECore),
"已设置的核心数量:${sate.inputGameLaunchECore} (此功能适用于首页的盒子一键启动 或 工具中的RSI启动器管理员模式当为 0 时不启用此功能 ",
onTap:()=> model.setGameLaunchECore(context)),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FluentIcons.folder_open, size: 20),
"设置启动器文件RSI Launcher.exe",
subTitle: model.customLauncherPath != null
? "${model.customLauncherPath}"
subTitle: sate.customLauncherPath != null
? "${sate.customLauncherPath}"
: "手动设置启动器位置,建议仅在无法自动扫描安装位置时使用",
onTap: model.setLauncherPath, onDel: () {
onTap: ()=> model.setLauncherPath(context), onDel: () {
model.delName("custom_launcher_path");
}),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FluentIcons.game, size: 20),
"设置游戏文件 StarCitizen.exe",
subTitle: model.customGamePath != null
? "${model.customGamePath}"
subTitle: sate.customGamePath != null
? "${sate.customGamePath}"
: "手动设置游戏安装位置,建议仅在无法自动扫描安装位置时使用",
onTap: model.setGamePath, onDel: () {
onTap: ()=> model.setGamePath(context), onDel: () {
model.delName("custom_game_path");
}),
const SizedBox(height: 12),
makeSettingsItem(const Icon(FluentIcons.delete, size: 20), "清理汉化文件缓存",
subTitle:
"缓存大小 ${(model.locationCacheSize / 1024 / 1024).toStringAsFixed(2)}MB清理盒子下载的汉化文件缓存不会影响已安装的汉化",
onTap: model.cleanLocationCache),
"缓存大小 ${(sate.locationCacheSize / 1024 / 1024).toStringAsFixed(2)}MB清理盒子下载的汉化文件缓存不会影响已安装的汉化",
onTap: ()=> model.cleanLocationCache(context)),
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FluentIcons.speed_high, size: 20), "工具站访问加速",
onTap: () =>
model.onChangeToolSiteMirror(!model.isEnableToolSiteMirrors),
model.onChangeToolSiteMirror(!sate.isEnableToolSiteMirrors),
subTitle:
"使用镜像服务器加速访问 Dps Uex 等工具网站,若访问异常请关闭该功能。 为保护账户安全任何情况下都不会加速RSI官网。",
onSwitch: model.onChangeToolSiteMirror,
switchStatus: model.isEnableToolSiteMirrors),
switchStatus: sate.isEnableToolSiteMirrors),
const SizedBox(height: 12),
makeSettingsItem(
const Icon(FluentIcons.document_set, size: 20), "查看log",
@ -123,7 +128,4 @@ class SettingUI extends BaseUI<SettingUIModel> {
),
);
}
@override
String getUITitle(BuildContext context, SettingUIModel model) => "SettingUI";
}

View File

@ -1,36 +1,54 @@
// ignore_for_file: avoid_build_context_in_providers
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
import 'package:local_auth/local_auth.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/utils/provider.dart';
import 'package:starcitizen_doctor/common/win32/credentials.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
class SettingUIModel extends BaseUIModel {
var isDeviceSupportWinHello = false;
part 'settings_ui_model.g.dart';
String autoLoginEmail = "-";
bool isEnableAutoLogin = false;
bool isEnableAutoLoginPwd = false;
bool isEnableToolSiteMirrors = false;
String inputGameLaunchECore = "0";
part 'settings_ui_model.freezed.dart';
String? customLauncherPath;
String? customGamePath;
int locationCacheSize = 0;
@freezed
class SettingsUIState with _$SettingsUIState {
const factory SettingsUIState({
@Default(false) isDeviceSupportWinHello,
@Default("-") String autoLoginEmail,
@Default(false) bool isEnableAutoLogin,
@Default(false) bool isEnableAutoLoginPwd,
@Default(false) bool isEnableToolSiteMirrors,
@Default("0") String inputGameLaunchECore,
String? customLauncherPath,
String? customGamePath,
@Default(0) int locationCacheSize,
}) = _SettingsUIState;
}
@riverpod
class SettingsUIModel extends _$SettingsUIModel {
@override
loadData() async {
dPrint("SettingUIModel.loadData");
SettingsUIState build() {
state = const SettingsUIState();
_initState();
return state;
}
void _initState() async {
final LocalAuthentication localAuth = LocalAuthentication();
isDeviceSupportWinHello = await localAuth.isDeviceSupported();
notifyListeners();
final isDeviceSupportWinHello = await localAuth.isDeviceSupported();
state = state.copyWith(isDeviceSupportWinHello: isDeviceSupportWinHello);
_updateGameLaunchECore();
if (AppConf.isMSE) {
if (ConstConf.isMSE) {
_updateAutoLoginAccount();
}
_loadCustomPath();
@ -38,32 +56,39 @@ class SettingUIModel extends BaseUIModel {
_loadToolSiteMirrorState();
}
Future<void> onResetAutoLogin() async {
final ok = await showConfirmDialogs(context!, "确认重置自动填充?",
Future<void> onResetAutoLogin(BuildContext context) async {
final ok = await showConfirmDialogs(context, "确认重置自动填充?",
const Text("这将会删除本地的账号记录,或在下次启动游戏时将自动填充选择 ‘否’ 以禁用自动填充。"));
if (ok) {
final userBox = await Hive.openBox("rsi_account_data");
await userBox.deleteFromDisk();
Win32Credentials.delete("SCToolbox_RSI_Account_secret");
showToast(context!, "已清理自动填充数据");
reloadData();
if (!context.mounted) return;
showToast(context, "已清理自动填充数据");
_initState();
}
}
Future _updateAutoLoginAccount() async {
final userBox = await Hive.openBox("rsi_account_data");
autoLoginEmail = userBox.get("account_email", defaultValue: "-");
isEnableAutoLogin = userBox.get("enable", defaultValue: true);
isEnableAutoLoginPwd =
final autoLoginEmail = userBox.get("account_email", defaultValue: "-");
final isEnableAutoLogin = userBox.get("enable", defaultValue: true);
final isEnableAutoLoginPwd =
userBox.get("account_pwd_encrypted", defaultValue: "") != "";
notifyListeners();
state = state.copyWith(
autoLoginEmail: autoLoginEmail,
isEnableAutoLogin: isEnableAutoLogin,
isEnableAutoLoginPwd: isEnableAutoLoginPwd);
}
Future<void> setGameLaunchECore() async {
Future<void> setGameLaunchECore(BuildContext context) async {
final userBox = await Hive.openBox("app_conf");
final defaultInput =
userBox.get("gameLaunch_eCore_count", defaultValue: "0");
final input = await showInputDialogs(context!,
if (!context.mounted) return;
final input = await showInputDialogs(context,
title: "请输入要忽略的 CPU 核心数",
content:
"Tip您的设备拥有几个能效核心就输入几非大小核设备请保持 0\n\n此功能适用于首页的盒子一键启动 或 工具中的 RSI启动器管理员模式当为 0 时不启用此功能。",
@ -71,17 +96,17 @@ class SettingUIModel extends BaseUIModel {
inputFormatters: [FilteringTextInputFormatter.digitsOnly]);
if (input == null) return;
userBox.put("gameLaunch_eCore_count", input);
reloadData();
_initState();
}
Future _updateGameLaunchECore() async {
final userBox = await Hive.openBox("app_conf");
inputGameLaunchECore =
final inputGameLaunchECore =
userBox.get("gameLaunch_eCore_count", defaultValue: "0");
notifyListeners();
state = state.copyWith(inputGameLaunchECore: inputGameLaunchECore);
}
Future<void> setLauncherPath() async {
Future<void> setLauncherPath(BuildContext context) async {
final r = await FilePicker.platform.pickFiles(
dialogTitle: "请选择RSI启动器位置RSI Launcher.exe",
type: FileType.custom,
@ -90,14 +115,16 @@ class SettingUIModel extends BaseUIModel {
final fileName = r.files.first.path!;
if (fileName.endsWith("\\RSI Launcher.exe")) {
await _saveCustomPath("custom_launcher_path", fileName);
showToast(context!, "设置成功,在对应页面点击刷新即可扫描出新路径");
reloadData();
if (!context.mounted) return;
showToast(context, "设置成功,在对应页面点击刷新即可扫描出新路径");
_initState();
} else {
showToast(context!, "文件有误!");
if (!context.mounted) return;
showToast(context, "文件有误!");
}
}
Future<void> setGamePath() async {
Future<void> setGamePath(BuildContext context) async {
final r = await FilePicker.platform.pickFiles(
dialogTitle: "请选择游戏安装位置StarCitizen.exe",
type: FileType.custom,
@ -111,10 +138,12 @@ class SettingUIModel extends BaseUIModel {
RegExp pathRegex = RegExp(r"\\[^\\]+\\Bin64\\StarCitizen\.exe$");
String extractedPath = fileName.replaceFirst(pathRegex, '');
await _saveCustomPath("custom_game_path", extractedPath);
showToast(context!, "设置成功,在对应页面点击刷新即可扫描出新路径");
reloadData();
if (!context.mounted) return;
showToast(context, "设置成功,在对应页面点击刷新即可扫描出新路径");
_initState();
} else {
showToast(context!, "文件有误!");
if (!context.mounted) return;
showToast(context, "文件有误!");
}
}
@ -125,36 +154,40 @@ class SettingUIModel extends BaseUIModel {
_loadCustomPath() async {
final confBox = await Hive.openBox("app_conf");
customLauncherPath = confBox.get("custom_launcher_path");
customGamePath = confBox.get("custom_game_path");
final customLauncherPath = confBox.get("custom_launcher_path");
final customGamePath = confBox.get("custom_game_path");
state = state.copyWith(
customLauncherPath: customLauncherPath, customGamePath: customGamePath);
}
Future<void> delName(String key) async {
final confBox = await Hive.openBox("app_conf");
await confBox.delete(key);
reloadData();
_initState();
}
_loadLocationCacheSize() async {
final len = await SystemHelper.getDirLen(
"${AppConf.applicationSupportDir}/Localizations");
locationCacheSize = len;
notifyListeners();
"${appGlobalState.applicationSupportDir}/Localizations");
final locationCacheSize = len;
state = state.copyWith(locationCacheSize: locationCacheSize);
}
Future<void> cleanLocationCache() async {
Future<void> cleanLocationCache(BuildContext context) async {
final ok = await showConfirmDialogs(
context!, "确认清理汉化缓存?", const Text("这不会影响已安装的汉化。"));
context, "确认清理汉化缓存?", const Text("这不会影响已安装的汉化。"));
if (ok == true) {
final dir = Directory("${AppConf.applicationSupportDir}/Localizations");
await handleError(() => dir.delete(recursive: true));
reloadData();
final dir =
Directory("${appGlobalState.applicationSupportDir}/Localizations");
if (!context.mounted) return;
await dir.delete(recursive: true).unwrap(context: context);
_initState();
}
}
Future<void> addShortCut() async {
if (AppConf.isMSE) {
showToast(context!, "因微软版功能限制,请在接下来打开的窗口中 手动将《SC汉化盒子》拖动到桌面即可创建快捷方式。");
Future<void> addShortCut(BuildContext context) async {
if (ConstConf.isMSE) {
showToast(context, "因微软版功能限制,请在接下来打开的窗口中 手动将《SC汉化盒子》拖动到桌面即可创建快捷方式。");
await Future.delayed(const Duration(seconds: 1));
Process.run("explorer.exe", ["shell:AppsFolder"]);
return;
@ -174,24 +207,25 @@ class SettingUIModel extends BaseUIModel {
}
""";
await Process.run(SystemHelper.powershellPath, [script]);
showToast(context!, "创建完毕,请返回桌面查看");
if (!context.mounted) return;
showToast(context, "创建完毕,请返回桌面查看");
}
_loadToolSiteMirrorState() async {
final userBox = await Hive.openBox("app_conf");
isEnableToolSiteMirrors =
final isEnableToolSiteMirrors =
userBox.get("isEnableToolSiteMirrors", defaultValue: false);
notifyListeners();
state = state.copyWith(isEnableToolSiteMirrors: isEnableToolSiteMirrors);
}
void onChangeToolSiteMirror(bool? b) async {
final userBox = await Hive.openBox("app_conf");
isEnableToolSiteMirrors = b == true;
final isEnableToolSiteMirrors = b == true;
await userBox.put("isEnableToolSiteMirrors", isEnableToolSiteMirrors);
notifyListeners();
_initState();
}
showLogs() async {
SystemHelper.openDir(AppConf.appLogFile?.absolute.path);
SystemHelper.openDir(getDPrintFile()?.absolute.path.replaceAll("/", "\\"));
}
}

View File

@ -0,0 +1,323 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'settings_ui_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$SettingsUIState {
dynamic get isDeviceSupportWinHello => throw _privateConstructorUsedError;
String get autoLoginEmail => throw _privateConstructorUsedError;
bool get isEnableAutoLogin => throw _privateConstructorUsedError;
bool get isEnableAutoLoginPwd => throw _privateConstructorUsedError;
bool get isEnableToolSiteMirrors => throw _privateConstructorUsedError;
String get inputGameLaunchECore => throw _privateConstructorUsedError;
String? get customLauncherPath => throw _privateConstructorUsedError;
String? get customGamePath => throw _privateConstructorUsedError;
int get locationCacheSize => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SettingsUIStateCopyWith<SettingsUIState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SettingsUIStateCopyWith<$Res> {
factory $SettingsUIStateCopyWith(
SettingsUIState value, $Res Function(SettingsUIState) then) =
_$SettingsUIStateCopyWithImpl<$Res, SettingsUIState>;
@useResult
$Res call(
{dynamic isDeviceSupportWinHello,
String autoLoginEmail,
bool isEnableAutoLogin,
bool isEnableAutoLoginPwd,
bool isEnableToolSiteMirrors,
String inputGameLaunchECore,
String? customLauncherPath,
String? customGamePath,
int locationCacheSize});
}
/// @nodoc
class _$SettingsUIStateCopyWithImpl<$Res, $Val extends SettingsUIState>
implements $SettingsUIStateCopyWith<$Res> {
_$SettingsUIStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isDeviceSupportWinHello = freezed,
Object? autoLoginEmail = null,
Object? isEnableAutoLogin = null,
Object? isEnableAutoLoginPwd = null,
Object? isEnableToolSiteMirrors = null,
Object? inputGameLaunchECore = null,
Object? customLauncherPath = freezed,
Object? customGamePath = freezed,
Object? locationCacheSize = null,
}) {
return _then(_value.copyWith(
isDeviceSupportWinHello: freezed == isDeviceSupportWinHello
? _value.isDeviceSupportWinHello
: isDeviceSupportWinHello // ignore: cast_nullable_to_non_nullable
as dynamic,
autoLoginEmail: null == autoLoginEmail
? _value.autoLoginEmail
: autoLoginEmail // ignore: cast_nullable_to_non_nullable
as String,
isEnableAutoLogin: null == isEnableAutoLogin
? _value.isEnableAutoLogin
: isEnableAutoLogin // ignore: cast_nullable_to_non_nullable
as bool,
isEnableAutoLoginPwd: null == isEnableAutoLoginPwd
? _value.isEnableAutoLoginPwd
: isEnableAutoLoginPwd // ignore: cast_nullable_to_non_nullable
as bool,
isEnableToolSiteMirrors: null == isEnableToolSiteMirrors
? _value.isEnableToolSiteMirrors
: isEnableToolSiteMirrors // ignore: cast_nullable_to_non_nullable
as bool,
inputGameLaunchECore: null == inputGameLaunchECore
? _value.inputGameLaunchECore
: inputGameLaunchECore // ignore: cast_nullable_to_non_nullable
as String,
customLauncherPath: freezed == customLauncherPath
? _value.customLauncherPath
: customLauncherPath // ignore: cast_nullable_to_non_nullable
as String?,
customGamePath: freezed == customGamePath
? _value.customGamePath
: customGamePath // ignore: cast_nullable_to_non_nullable
as String?,
locationCacheSize: null == locationCacheSize
? _value.locationCacheSize
: locationCacheSize // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$SettingsUIStateImplCopyWith<$Res>
implements $SettingsUIStateCopyWith<$Res> {
factory _$$SettingsUIStateImplCopyWith(_$SettingsUIStateImpl value,
$Res Function(_$SettingsUIStateImpl) then) =
__$$SettingsUIStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{dynamic isDeviceSupportWinHello,
String autoLoginEmail,
bool isEnableAutoLogin,
bool isEnableAutoLoginPwd,
bool isEnableToolSiteMirrors,
String inputGameLaunchECore,
String? customLauncherPath,
String? customGamePath,
int locationCacheSize});
}
/// @nodoc
class __$$SettingsUIStateImplCopyWithImpl<$Res>
extends _$SettingsUIStateCopyWithImpl<$Res, _$SettingsUIStateImpl>
implements _$$SettingsUIStateImplCopyWith<$Res> {
__$$SettingsUIStateImplCopyWithImpl(
_$SettingsUIStateImpl _value, $Res Function(_$SettingsUIStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isDeviceSupportWinHello = freezed,
Object? autoLoginEmail = null,
Object? isEnableAutoLogin = null,
Object? isEnableAutoLoginPwd = null,
Object? isEnableToolSiteMirrors = null,
Object? inputGameLaunchECore = null,
Object? customLauncherPath = freezed,
Object? customGamePath = freezed,
Object? locationCacheSize = null,
}) {
return _then(_$SettingsUIStateImpl(
isDeviceSupportWinHello: freezed == isDeviceSupportWinHello
? _value.isDeviceSupportWinHello!
: isDeviceSupportWinHello,
autoLoginEmail: null == autoLoginEmail
? _value.autoLoginEmail
: autoLoginEmail // ignore: cast_nullable_to_non_nullable
as String,
isEnableAutoLogin: null == isEnableAutoLogin
? _value.isEnableAutoLogin
: isEnableAutoLogin // ignore: cast_nullable_to_non_nullable
as bool,
isEnableAutoLoginPwd: null == isEnableAutoLoginPwd
? _value.isEnableAutoLoginPwd
: isEnableAutoLoginPwd // ignore: cast_nullable_to_non_nullable
as bool,
isEnableToolSiteMirrors: null == isEnableToolSiteMirrors
? _value.isEnableToolSiteMirrors
: isEnableToolSiteMirrors // ignore: cast_nullable_to_non_nullable
as bool,
inputGameLaunchECore: null == inputGameLaunchECore
? _value.inputGameLaunchECore
: inputGameLaunchECore // ignore: cast_nullable_to_non_nullable
as String,
customLauncherPath: freezed == customLauncherPath
? _value.customLauncherPath
: customLauncherPath // ignore: cast_nullable_to_non_nullable
as String?,
customGamePath: freezed == customGamePath
? _value.customGamePath
: customGamePath // ignore: cast_nullable_to_non_nullable
as String?,
locationCacheSize: null == locationCacheSize
? _value.locationCacheSize
: locationCacheSize // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
class _$SettingsUIStateImpl implements _SettingsUIState {
const _$SettingsUIStateImpl(
{this.isDeviceSupportWinHello = false,
this.autoLoginEmail = "-",
this.isEnableAutoLogin = false,
this.isEnableAutoLoginPwd = false,
this.isEnableToolSiteMirrors = false,
this.inputGameLaunchECore = "0",
this.customLauncherPath,
this.customGamePath,
this.locationCacheSize = 0});
@override
@JsonKey()
final dynamic isDeviceSupportWinHello;
@override
@JsonKey()
final String autoLoginEmail;
@override
@JsonKey()
final bool isEnableAutoLogin;
@override
@JsonKey()
final bool isEnableAutoLoginPwd;
@override
@JsonKey()
final bool isEnableToolSiteMirrors;
@override
@JsonKey()
final String inputGameLaunchECore;
@override
final String? customLauncherPath;
@override
final String? customGamePath;
@override
@JsonKey()
final int locationCacheSize;
@override
String toString() {
return 'SettingsUIState(isDeviceSupportWinHello: $isDeviceSupportWinHello, autoLoginEmail: $autoLoginEmail, isEnableAutoLogin: $isEnableAutoLogin, isEnableAutoLoginPwd: $isEnableAutoLoginPwd, isEnableToolSiteMirrors: $isEnableToolSiteMirrors, inputGameLaunchECore: $inputGameLaunchECore, customLauncherPath: $customLauncherPath, customGamePath: $customGamePath, locationCacheSize: $locationCacheSize)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SettingsUIStateImpl &&
const DeepCollectionEquality().equals(
other.isDeviceSupportWinHello, isDeviceSupportWinHello) &&
(identical(other.autoLoginEmail, autoLoginEmail) ||
other.autoLoginEmail == autoLoginEmail) &&
(identical(other.isEnableAutoLogin, isEnableAutoLogin) ||
other.isEnableAutoLogin == isEnableAutoLogin) &&
(identical(other.isEnableAutoLoginPwd, isEnableAutoLoginPwd) ||
other.isEnableAutoLoginPwd == isEnableAutoLoginPwd) &&
(identical(
other.isEnableToolSiteMirrors, isEnableToolSiteMirrors) ||
other.isEnableToolSiteMirrors == isEnableToolSiteMirrors) &&
(identical(other.inputGameLaunchECore, inputGameLaunchECore) ||
other.inputGameLaunchECore == inputGameLaunchECore) &&
(identical(other.customLauncherPath, customLauncherPath) ||
other.customLauncherPath == customLauncherPath) &&
(identical(other.customGamePath, customGamePath) ||
other.customGamePath == customGamePath) &&
(identical(other.locationCacheSize, locationCacheSize) ||
other.locationCacheSize == locationCacheSize));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(isDeviceSupportWinHello),
autoLoginEmail,
isEnableAutoLogin,
isEnableAutoLoginPwd,
isEnableToolSiteMirrors,
inputGameLaunchECore,
customLauncherPath,
customGamePath,
locationCacheSize);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$SettingsUIStateImplCopyWith<_$SettingsUIStateImpl> get copyWith =>
__$$SettingsUIStateImplCopyWithImpl<_$SettingsUIStateImpl>(
this, _$identity);
}
abstract class _SettingsUIState implements SettingsUIState {
const factory _SettingsUIState(
{final dynamic isDeviceSupportWinHello,
final String autoLoginEmail,
final bool isEnableAutoLogin,
final bool isEnableAutoLoginPwd,
final bool isEnableToolSiteMirrors,
final String inputGameLaunchECore,
final String? customLauncherPath,
final String? customGamePath,
final int locationCacheSize}) = _$SettingsUIStateImpl;
@override
dynamic get isDeviceSupportWinHello;
@override
String get autoLoginEmail;
@override
bool get isEnableAutoLogin;
@override
bool get isEnableAutoLoginPwd;
@override
bool get isEnableToolSiteMirrors;
@override
String get inputGameLaunchECore;
@override
String? get customLauncherPath;
@override
String? get customGamePath;
@override
int get locationCacheSize;
@override
@JsonKey(ignore: true)
_$$SettingsUIStateImplCopyWith<_$SettingsUIStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'settings_ui_model.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$settingsUIModelHash() => r'34ac24f658a081350be7d2b3bda810d101b888a1';
/// See also [SettingsUIModel].
@ProviderFor(SettingsUIModel)
final settingsUIModelProvider =
AutoDisposeNotifierProvider<SettingsUIModel, SettingsUIState>.internal(
SettingsUIModel.new,
name: r'settingsUIModelProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$settingsUIModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SettingsUIModel = AutoDisposeNotifier<SettingsUIState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -0,0 +1,263 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Material;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:markdown/markdown.dart' as markdown;
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/app.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:html/parser.dart' as html_parser;
import 'package:url_launcher/url_launcher_string.dart';
class UpgradeDialogUI extends HookConsumerWidget {
const UpgradeDialogUI({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appState = ref.watch(appGlobalModelProvider);
final appModel = ref.read(appGlobalModelProvider.notifier);
final description = useState<String?>(null);
final isUsingDiversion = useState(false);
final isUpgrading = useState(false);
final progress = useState(0.0);
final downloadUrl = useState("");
final targetVersion = ConstConf.isMSE
? appState.networkVersionData!.mSELastVersion!
: appState.networkVersionData!.lastVersion!;
final minVersionCode = ConstConf.isMSE
? appState.networkVersionData?.mSEMinVersionCode
: appState.networkVersionData?.minVersionCode;
useEffect(() {
_getUpdateInfo(context, targetVersion, description, downloadUrl);
return null;
}, []);
return Material(
child: ContentDialog(
title: Text("发现新版本 -> $targetVersion"),
constraints:
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .55),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (description.value == null) ...[
const Center(
child: Column(
children: [
ProgressRing(),
SizedBox(height: 16),
Text("正在获取新版本详情...")
],
),
)
] else
...makeMarkdownView(description.value!,
attachmentsUrl: URLConf.giteaAttachmentsUrl),
],
),
),
)),
if (isUsingDiversion.value) ...[
const SizedBox(height: 24),
GestureDetector(
onTap: _launchReleaseUrl,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(.1),
borderRadius: BorderRadius.circular(7)),
child: Text(
"提示:当前正在使用分流服务器进行更新,可能会出现下载速度下降,但有助于我们进行成本控制,若下载异常请点击这里跳转手动安装。",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.7)),
),
),
),
],
if (isUpgrading.value) ...[
const SizedBox(height: 24),
Row(
children: [
Text(progress.value == 100
? "正在安装: "
: "正在下载: ${progress.value.toStringAsFixed(2)}% "),
Expanded(
child: ProgressBar(
value: progress.value == 100 ? null : progress.value,
)),
],
),
],
],
),
actions: isUpgrading.value
? null
: [
if (downloadUrl.value.isNotEmpty)
FilledButton(
onPressed: () => _doUpgrade(
context,
appState,
isUpgrading,
appModel,
downloadUrl,
description,
isUsingDiversion,
progress),
child: const Padding(
padding: EdgeInsets.only(
top: 4, bottom: 4, left: 8, right: 8),
child: Text("立即更新"),
)),
if (ConstConf.appVersionCode >= (minVersionCode ?? 0))
Button(
onPressed: () => _doCancel(context),
child: const Padding(
padding: EdgeInsets.only(
top: 4, bottom: 4, left: 8, right: 8),
child: Text("下次吧"),
)),
],
),
);
}
Future<void> _getUpdateInfo(
BuildContext context,
String targetVersion,
ValueNotifier<String?> description,
ValueNotifier<String> downloadUrl) async {
try {
final r = await Api.getAppReleaseDataByVersionName(targetVersion);
description.value = r["body"];
final assets = List.of(r["assets"] ?? []);
for (var asset in assets) {
if (asset["name"].toString().endsWith("SETUP.exe")) {
downloadUrl.value = asset["browser_download_url"];
}
}
} catch (e) {
dPrint("UpgradeDialogUIModel.loadData Error : $e");
if (!context.mounted) return;
Navigator.pop(context, false);
}
}
void _launchReleaseUrl() {
launchUrlString(URLConf.devReleaseUrl);
}
void _doCancel(BuildContext context) {
Navigator.pop(context, true);
}
String _getDiversionUrl(String description) {
try {
final htmlStr = markdown.markdownToHtml(description);
final html = html_parser.parse(htmlStr);
for (var element in html.querySelectorAll('a')) {
String linkText = element.text;
String linkUrl = element.attributes['href'] ?? '';
if (linkText.trim().endsWith("_SETUP.exe")) {
final diversionDownloadUrl = linkUrl.trim();
dPrint("diversionDownloadUrl === $diversionDownloadUrl");
return diversionDownloadUrl;
}
}
} catch (e) {
dPrint("_checkDiversionUrl Error:$e");
}
return "";
}
Future<void> _doUpgrade(
BuildContext context,
AppGlobalState appState,
ValueNotifier<bool> isUpgrading,
AppGlobalModel appModel,
ValueNotifier<String> downloadUrl,
ValueNotifier<String?> description,
ValueNotifier<bool> isUsingDiversion,
ValueNotifier<double> progress) async {
if (ConstConf.isMSE) {
launchUrlString("ms-windows-store://pdp/?productid=9NF3SWFWNKL1");
await Future.delayed(const Duration(seconds: 3));
if (ConstConf.appVersionCode <
(appState.networkVersionData?.minVersionCode ?? 0)) {
exit(0);
}
if (!context.mounted) return;
_doCancel(context);
return;
}
isUpgrading.value = true;
final fileName = "${appModel.getUpgradePath()}/next_SETUP.exe";
try {
// check diversionDownloadUrl
var url = downloadUrl.value;
final diversionDownloadUrl = _getDiversionUrl(description.value!);
final dio = Dio();
if (diversionDownloadUrl.isNotEmpty) {
try {
final resp = await dio.head(diversionDownloadUrl,
options: Options(
sendTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10)));
if (resp.statusCode == 200) {
isUsingDiversion.value = true;
url = diversionDownloadUrl;
} else {
isUsingDiversion.value = false;
}
dPrint("diversionDownloadUrl head resp == ${resp.headers}");
} catch (e) {
dPrint("diversionDownloadUrl err:$e");
}
}
await dio.download(url, fileName,
onReceiveProgress: (int count, int total) {
progress.value = (count / total) * 100;
});
} catch (_) {
isUpgrading.value = false;
progress.value = 0;
if (!context.mounted) return;
showToast(context, "下载失败,请尝试手动安装!");
return;
}
try {
final r = await (Process.run(
SystemHelper.powershellPath, ["start", fileName, "/SILENT"]));
if (r.stderr.toString().isNotEmpty) {
throw r.stderr;
}
exit(0);
} catch (_) {
isUpgrading.value = false;
progress.value = 0;
if (!context.mounted) return;
showToast(context, "运行失败,请尝试手动安装!");
Process.run(SystemHelper.powershellPath,
["explorer.exe", "/select,\"$fileName\""]);
}
}
}

View File

@ -1,104 +0,0 @@
import 'package:flutter/material.dart' show Material;
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'upgrade_dialog_ui_model.dart';
class UpgradeDialogUI extends BaseUI<UpgradeDialogUIModel> {
@override
Widget? buildBody(BuildContext context, UpgradeDialogUIModel model) {
return Material(
child: ContentDialog(
title: Text("发现新版本 -> ${model.targetVersion}"),
constraints:
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .55),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (model.description == null) ...[
const Center(
child: Column(
children: [
ProgressRing(),
SizedBox(height: 16),
Text("正在获取新版本详情...")
],
),
)
] else
...makeMarkdownView(model.description!,
attachmentsUrl: URLConf.giteaAttachmentsUrl),
],
),
),
)),
if (model.isUsingDiversion) ...[
const SizedBox(height: 24),
GestureDetector(
onTap: model.launchReleaseUrl,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(.1),
borderRadius: BorderRadius.circular(7)),
child: Text(
"提示:当前正在使用分流服务器进行更新,可能会出现下载速度下降,但有助于我们进行成本控制,若下载异常请点击这里跳转手动安装。",
style: TextStyle(
fontSize: 14, color: Colors.white.withOpacity(.7)),
),
),
),
],
if (model.isUpgrading) ...[
const SizedBox(height: 24),
Row(
children: [
Text(model.progress == 100
? "正在安装: "
: "正在下载: ${model.progress?.toStringAsFixed(2) ?? 0}% "),
Expanded(
child: ProgressBar(
value: model.progress == 100 ? null : model.progress,
)),
],
),
],
],
),
actions: model.isUpgrading
? null
: [
if (model.downloadUrl.isNotEmpty)
FilledButton(
onPressed: model.doUpgrade,
child: const Padding(
padding: EdgeInsets.only(
top: 4, bottom: 4, left: 8, right: 8),
child: Text("立即更新"),
)),
if (AppConf.appVersionCode >=
(AppConf.networkVersionData?.minVersionCode ?? 0))
Button(
onPressed: model.doCancel,
child: const Padding(
padding: EdgeInsets.only(
top: 4, bottom: 4, left: 8, right: 8),
child: Text("下次吧"),
)),
],
),
);
}
@override
String getUITitle(BuildContext context, UpgradeDialogUIModel model) => "";
}

View File

@ -1,136 +0,0 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:markdown/markdown.dart';
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:html/parser.dart';
class UpgradeDialogUIModel extends BaseUIModel {
String? description;
String targetVersion = "";
String downloadUrl = "";
String? diversionDownloadUrl;
bool isUsingDiversion = false;
bool isUpgrading = false;
double? progress;
@override
Future loadData() async {
// get download url for gitlab release
try {
targetVersion = AppConf.isMSE
? AppConf.networkVersionData!.mSELastVersion!
: AppConf.networkVersionData!.lastVersion!;
final r = await Api.getAppReleaseDataByVersionName(targetVersion);
description = r["body"];
_checkDiversionUrl();
final assets = List.of(r["assets"] ?? []);
for (var asset in assets) {
if (asset["name"].toString().endsWith("SETUP.exe")) {
downloadUrl = asset["browser_download_url"];
}
}
notifyListeners();
} catch (e) {
dPrint("UpgradeDialogUIModel.loadData Error : $e");
Navigator.pop(context!, false);
}
}
doUpgrade() async {
if (AppConf.isMSE) {
launchUrlString("ms-windows-store://pdp/?productid=9NF3SWFWNKL1");
await Future.delayed(const Duration(seconds: 3));
if (AppConf.appVersionCode <
(AppConf.networkVersionData?.minVersionCode ?? 0)) {
exit(0);
}
Navigator.pop(context!);
}
isUpgrading = true;
notifyListeners();
final fileName = "${AppConf.getUpgradePath()}/next_SETUP.exe";
try {
// check diversionDownloadUrl
var url = downloadUrl;
final dio = Dio();
if (diversionDownloadUrl != null) {
try {
final resp = await dio.head(diversionDownloadUrl!,
options: Options(
sendTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10)));
if (resp.statusCode == 200) {
isUsingDiversion = true;
url = diversionDownloadUrl!;
notifyListeners();
} else {
isUsingDiversion = false;
notifyListeners();
}
dPrint("diversionDownloadUrl head resp == ${resp.headers}");
} catch (e) {
dPrint("diversionDownloadUrl err:$e");
}
}
await dio.download(url, fileName,
onReceiveProgress: (int count, int total) {
progress = (count / total) * 100;
notifyListeners();
});
} catch (_) {
isUpgrading = false;
progress = null;
showToast(context!, "下载失败,请尝试手动安装!");
notifyListeners();
return;
}
try {
final r = await (Process.run(
SystemHelper.powershellPath, ["start", fileName, "/SILENT"]));
if (r.stderr.toString().isNotEmpty) {
throw r.stderr;
}
exit(0);
} catch (_) {
isUpgrading = false;
progress = null;
showToast(context!, "运行失败,请尝试手动安装!");
Process.run(SystemHelper.powershellPath,
["explorer.exe", "/select,\"$fileName\""]);
notifyListeners();
}
}
void doCancel() {
Navigator.pop(context!, true);
}
void _checkDiversionUrl() {
try {
final htmlStr = markdownToHtml(description!);
final html = parse(htmlStr);
html.querySelectorAll('a').forEach((element) {
String linkText = element.text;
String linkUrl = element.attributes['href'] ?? '';
if (linkText.trim().endsWith("_SETUP.exe")) {
diversionDownloadUrl = linkUrl.trim();
dPrint("diversionDownloadUrl === $diversionDownloadUrl");
}
});
} catch (e) {
dPrint("_checkDiversionUrl Error:$e");
}
}
void launchReleaseUrl() {
launchUrlString(URLConf.devReleaseUrl);
}
}

View File

@ -1,12 +1,30 @@
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:fluent_ui/fluent_ui.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/api/analytics.dart';
import 'package:starcitizen_doctor/app.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/provider/aria2c.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'splash_ui_model.dart';
class SplashUI extends HookConsumerWidget {
const SplashUI({super.key});
class SplashUI extends BaseUI<SplashUIModel> {
@override
Widget? buildBody(BuildContext context, SplashUIModel model) {
return makeDefaultPage(context, model,
Widget build(BuildContext context, WidgetRef ref) {
final stepState = useState(0);
final step = stepState.value;
useEffect(() {
final appModel = ref.read(appGlobalModelProvider.notifier);
_initApp(context, appModel, stepState, ref);
return null;
}, const []);
return makeDefaultPage(context,
content: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
@ -15,9 +33,9 @@ class SplashUI extends BaseUI<SplashUIModel> {
const SizedBox(height: 32),
const ProgressRing(),
const SizedBox(height: 32),
if (model.step == 0) const Text("正在检测可用性,这可能需要一点时间..."),
if (model.step == 1) const Text("正在检查更新..."),
if (model.step == 2) const Text("即将完成..."),
if (step == 0) const Text("正在检测可用性,这可能需要一点时间..."),
if (step == 1) const Text("正在检查更新..."),
if (step == 2) const Text("即将完成..."),
],
),
),
@ -34,12 +52,27 @@ class SplashUI extends BaseUI<SplashUIModel> {
),
const SizedBox(width: 12),
const Text(
"SC汉化盒子 V${AppConf.appVersion} ${AppConf.isMSE ? "" : " Dev"}")
"SC汉化盒子 V${ConstConf.appVersion} ${ConstConf.isMSE ? "" : " Dev"}")
],
),
));
}
@override
String getUITitle(BuildContext context, SplashUIModel model) => "";
void _initApp(BuildContext context, AppGlobalModel appModel,
ValueNotifier<int> stepState, WidgetRef ref) async {
await appModel.initApp();
AnalyticsApi.touch("launch");
try {
await URLConf.checkHost();
} catch (e) {
dPrint("checkHost Error:$e");
}
stepState.value = 1;
if (!context.mounted) return;
await appModel.checkUpdate(context);
stepState.value = 2;
ref.read(aria2cModelProvider);
if (!context.mounted) return;
context.go("/index");
}
}

View File

@ -1,39 +0,0 @@
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/io/aria2c.dart';
import 'package:starcitizen_doctor/ui/index_ui.dart';
import 'package:starcitizen_doctor/ui/index_ui_model.dart';
import '../common/conf/app_conf.dart';
class SplashUIModel extends BaseUIModel {
int step = 0;
@override
void initModel() {
_initApp();
super.initModel();
}
Future<void> _initApp() async {
AnalyticsApi.touch("launch");
try {
await URLConf.checkHost();
} catch (e) {
dPrint("checkHost Error:$e");
}
step = 1;
notifyListeners();
await AppConf.checkUpdate();
step = 2;
notifyListeners();
await Aria2cManager.checkLazyLoad();
Navigator.pushAndRemoveUntil(
context!,
BaseUIContainer(
uiCreate: () => IndexUI(),
modelCreate: () => IndexUIModel()).makeRoute(context!),
(route) => false);
}
}

View File

@ -1,11 +1,25 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'tools_ui_model.dart';
class ToolsUI extends HookConsumerWidget {
const ToolsUI({super.key});
class ToolsUI extends BaseUI<ToolsUIModel> {
@override
Widget? buildBody(BuildContext context, ToolsUIModel model) {
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(toolsUIModelProvider);
final model = ref.read(toolsUIModelProvider.notifier);
useEffect(() {
addPostFrameCallback(() {
model.loadToolsCard(context, skipPathScan: false);
});
return null;
}, []);
return Stack(
children: [
Column(
@ -18,15 +32,18 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
Expanded(
child: Column(
children: [
makeGameLauncherPathSelect(context, model),
makeGameLauncherPathSelect(context, model, state),
const SizedBox(height: 12),
makeGamePathSelect(context, model),
makeGamePathSelect(context, model, state),
],
),
),
const SizedBox(width: 12),
Button(
onPressed: model.working ? null : model.loadData,
onPressed: state.working
? null
: () =>
model.loadToolsCard(context, skipPathScan: false),
child: const Padding(
padding: EdgeInsets.only(
top: 30, bottom: 30, left: 12, right: 12),
@ -37,7 +54,7 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
),
),
const SizedBox(height: 12),
if (model.items.isEmpty)
if (state.items.isEmpty)
const Expanded(
child: Center(
child: Column(
@ -58,12 +75,12 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
itemCount: (model.isItemLoading)
? model.items.length + 1
: model.items.length,
itemCount: (state.isItemLoading)
? state.items.length + 1
: state.items.length,
shrinkWrap: true,
itemBuilder: (context, index) {
if (index == model.items.length) {
if (index == state.items.length) {
return Container(
width: 300,
height: 200,
@ -73,7 +90,7 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
),
child: makeLoading(context));
}
final item = model.items[index];
final item = state.items[index];
return Container(
width: 300,
height: 200,
@ -119,7 +136,7 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
children: [
const Spacer(),
Button(
onPressed: model.working
onPressed: state.working
? null
: item.onTap == null
? null
@ -148,7 +165,7 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
)
],
),
if (model.working)
if (state.working)
Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(150),
@ -168,7 +185,8 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
);
}
Widget makeGamePathSelect(BuildContext context, ToolsUIModel model) {
Widget makeGamePathSelect(
BuildContext context, ToolsUIModel model, ToolsUIState state) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -178,18 +196,17 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
child: SizedBox(
height: 36,
child: ComboBox<String>(
value: model.scInstalledPath,
value: state.scInstalledPath,
items: [
for (final path in model.scInstallPaths)
for (final path in state.scInstallPaths)
ComboBoxItem(
value: path,
child: Text(path),
)
],
onChanged: (v) {
model.loadData(skipPathScan: true);
model.scInstalledPath = v!;
model.notifyListeners();
model.loadToolsCard(context, skipPathScan: true);
model.onChangeGamePath(v!);
},
),
),
@ -200,12 +217,13 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.folder_open),
),
onPressed: () => model.openDir(model.scInstalledPath))
onPressed: () => model.openDir(state.scInstalledPath))
],
);
}
Widget makeGameLauncherPathSelect(BuildContext context, ToolsUIModel model) {
Widget makeGameLauncherPathSelect(
BuildContext context, ToolsUIModel model, ToolsUIState state) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -215,18 +233,17 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
child: SizedBox(
height: 36,
child: ComboBox<String>(
value: model.rsiLauncherInstalledPath,
value: state.rsiLauncherInstalledPath,
items: [
for (final path in model.rsiLauncherInstallPaths)
for (final path in state.rsiLauncherInstallPaths)
ComboBoxItem(
value: path,
child: Text(path),
)
],
onChanged: (v) {
model.loadData(skipPathScan: true);
model.rsiLauncherInstalledPath = v!;
model.notifyListeners();
model.loadToolsCard(context, skipPathScan: true);
model.onChangeLauncherPath(v!);
},
),
),
@ -237,11 +254,8 @@ class ToolsUI extends BaseUI<ToolsUIModel> {
padding: EdgeInsets.all(6),
child: Icon(FluentIcons.folder_open),
),
onPressed: () => model.openDir(model.rsiLauncherInstalledPath))
onPressed: () => model.openDir(state.rsiLauncherInstalledPath))
],
);
}
@override
String getUITitle(BuildContext context, ToolsUIModel model) => "ToolsUI";
}

View File

@ -1,100 +1,122 @@
// ignore_for_file: avoid_build_context_in_providers
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
import 'package:starcitizen_doctor/common/io/aria2c.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/ui/home/downloader/downloader_ui.dart';
import 'package:starcitizen_doctor/ui/home/downloader/downloader_ui_model.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/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';
part 'tools_ui_model.g.dart';
class ToolsUIModel extends BaseUIModel {
bool _working = false;
part 'tools_ui_model.freezed.dart';
String scInstalledPath = "";
String rsiLauncherInstalledPath = "";
class ToolsItemData {
String key;
List<String> scInstallPaths = [];
List<String> rsiLauncherInstallPaths = [];
ToolsItemData(this.key, this.name, this.infoString, this.icon, {this.onTap});
set working(bool b) {
_working = b;
notifyListeners();
String name;
String infoString;
Widget icon;
AsyncCallback? onTap;
}
bool get working => _working;
var items = <_ToolsItemData>[];
bool isItemLoading = false;
@freezed
class ToolsUIState with _$ToolsUIState {
const factory ToolsUIState({
@Default(false) bool working,
@Default("") String scInstalledPath,
@Default("") String rsiLauncherInstalledPath,
@Default([]) List<String> scInstallPaths,
@Default([]) List<String> rsiLauncherInstallPaths,
@Default([]) List<ToolsItemData> items,
@Default(false) bool isItemLoading,
}) = _ToolsUIState;
}
@riverpod
class ToolsUIModel extends _$ToolsUIModel {
@override
Future loadData({bool skipPathScan = false}) async {
if (isItemLoading) return;
items.clear();
notifyListeners();
ToolsUIState build() {
state = const ToolsUIState();
return state;
}
loadToolsCard(BuildContext context, {bool skipPathScan = false}) async {
if (state.isItemLoading) return;
var items = <ToolsItemData>[];
state = state.copyWith(items: items, isItemLoading: true);
if (!skipPathScan) {
await reScanPath();
await reScanPath(context);
}
try {
items = [
_ToolsItemData(
"systeminfo",
ToolsItemData(
"systemnfo",
"查看系统信息",
"查看系统关键信息,用于快速问诊 \n\n耗时操作,请耐心等待。",
const Icon(FluentIcons.system, size: 28),
onTap: _showSystemInfo,
onTap: () => _showSystemInfo(context),
),
_ToolsItemData(
ToolsItemData(
"p4k_downloader",
"P4K 分流下载 / 修复",
"使用星际公民中文百科提供的分流下载服务,可用于下载或修复 p4k。 \n资源有限,请勿滥用。",
const Icon(FontAwesomeIcons.download, size: 28),
onTap: _downloadP4k,
onTap: () => _downloadP4k(context),
),
_ToolsItemData(
ToolsItemData(
"reinstall_eac",
"重装 EasyAntiCheat 反作弊",
"若您遇到 EAC 错误,且自动修复无效,请尝试使用此功能重装 EAC。",
const Icon(FluentIcons.game, size: 28),
onTap: _reinstallEAC,
onTap: () => _reinstallEAC(context),
),
_ToolsItemData(
ToolsItemData(
"rsilauncher_admin_mode",
"RSI Launcher 管理员模式",
"以管理员身份运行RSI启动器可能会解决一些问题。\n\n若设置了能效核心屏蔽参数,也会在此应用。",
const Icon(FluentIcons.admin, size: 28),
onTap: _adminRSILauncher,
onTap: () => _adminRSILauncher(context),
),
];
isItemLoading = true;
items.add(await _addShaderCard());
notifyListeners();
items.add(await _addPhotographyCard());
notifyListeners();
items.addAll(await _addLogCard());
notifyListeners();
items.addAll(await _addNvmePatchCard());
notifyListeners();
// close loading
isItemLoading = false;
notifyListeners();
state = state.copyWith(items: items);
if (!context.mounted) return;
items.add(await _addShaderCard(context));
state = state.copyWith(items: items);
if (!context.mounted) return;
items.add(await _addPhotographyCard(context));
state = state.copyWith(items: items);
if (!context.mounted) return;
items.addAll(await _addLogCard(context));
state = state.copyWith(items: items);
if (!context.mounted) return;
items.addAll(await _addNvmePatchCard(context));
state = state.copyWith(items: items, isItemLoading: false);
} catch (e) {
showToast(context!, "初始化失败,请截图报告给开发者。$e");
if (!context.mounted) return;
showToast(context, "初始化失败,请截图报告给开发者。$e");
}
notifyListeners();
}
Future<List<_ToolsItemData>> _addLogCard() async {
Future<List<ToolsItemData>> _addLogCard(BuildContext context) async {
double logPathLen = 0;
try {
logPathLen =
@ -103,83 +125,85 @@ class ToolsUIModel extends BaseUIModel {
1024;
} catch (_) {}
return [
_ToolsItemData(
ToolsItemData(
"rsilauncher_log_fix",
"RSI Launcher Log 修复",
"在某些情况下 RSI启动器 的 log 文件会损坏,导致无法完成问题扫描,使用此工具清理损坏的 log 文件。\n\n当前日志文件大小:${(logPathLen.toStringAsFixed(4))} MB",
const Icon(FontAwesomeIcons.bookBible, size: 28),
onTap: _rsiLogFix,
onTap: () => _rsiLogFix(context),
),
];
}
Future<List<_ToolsItemData>> _addNvmePatchCard() async {
Future<List<ToolsItemData>> _addNvmePatchCard(BuildContext context) async {
final nvmePatchStatus = await SystemHelper.checkNvmePatchStatus();
return [
if (nvmePatchStatus)
_ToolsItemData(
ToolsItemData(
"remove_nvme_settings",
"移除 nvme 注册表补丁",
"若您使用 nvme 补丁出现问题,请运行此工具。(可能导致游戏 安装/更新 不可用。)\n\n当前补丁状态:${(nvmePatchStatus) ? "已安装" : "未安装"}",
const Icon(FluentIcons.hard_drive, size: 28),
onTap: nvmePatchStatus
? () async {
working = true;
state = state.copyWith(working: true);
await SystemHelper.doRemoveNvmePath();
working = false;
showToast(context!, "已移除,重启生效!");
loadData(skipPathScan: true);
state = state.copyWith(working: false);
if (!context.mounted) return;
showToast(context, "已移除,重启电脑生效!");
loadToolsCard(context, skipPathScan: true);
}
: null,
),
if (!nvmePatchStatus)
_ToolsItemData(
ToolsItemData(
"add_nvme_settings",
"写入 nvme 注册表补丁",
"手动写入NVM补丁该功能仅在您知道自己在作什么的情况下使用",
const Icon(FontAwesomeIcons.cashRegister, size: 28),
onTap: () async {
working = true;
state = state.copyWith(working: true);
final r = await SystemHelper.addNvmePatch();
if (r == "") {
showToast(context!,
"修复成功,请尝试重启后继续安装游戏! 若注册表修改操作导致其他软件出现兼容问题,请使用 工具 中的 NVME 注册表清理。");
notifyListeners();
if (!context.mounted) return;
showToast(context,
"修复成功,请尝试重启电脑后继续安装游戏! 若注册表修改操作导致其他软件出现兼容问题,请使用 工具 中的 NVME 注册表清理。");
} else {
showToast(context!, "修复失败,$r");
if (!context.mounted) return;
showToast(context, "修复失败,$r");
}
working = false;
loadData(skipPathScan: true);
state = state.copyWith(working: false);
loadToolsCard(context, skipPathScan: true);
},
)
];
}
Future<_ToolsItemData> _addShaderCard() async {
Future<ToolsItemData> _addShaderCard(BuildContext context) async {
final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath();
return _ToolsItemData(
return ToolsItemData(
"clean_shaders",
"清理着色器缓存",
"若游戏画面出现异常或版本更新后可使用本工具清理过期的着色器当大于500M时建议清理 \n\n缓存大小:${((await SystemHelper.getDirLen(gameShaderCachePath ?? "", skipPath: [
"$gameShaderCachePath\\Crashes"
])) / 1024 / 1024).toStringAsFixed(4)} MB",
const Icon(FontAwesomeIcons.shapes, size: 28),
onTap: _cleanShaderCache,
onTap: () => _cleanShaderCache(context),
);
}
Future<_ToolsItemData> _addPhotographyCard() async {
Future<ToolsItemData> _addPhotographyCard(BuildContext context) async {
//
final isEnable = await _checkPhotographyStatus();
final isEnable = await _checkPhotographyStatus(context);
return _ToolsItemData(
return ToolsItemData(
"photography_mode",
isEnable ? "关闭摄影模式" : "开启摄影模式",
isEnable
? "还原镜头摇晃效果。\n\n@拉邦那 Lapernum 提供参数信息。"
: "一键关闭游戏内镜头晃动以便于摄影操作。\n\n @拉邦那 Lapernum 提供参数信息。",
const Icon(FontAwesomeIcons.camera, size: 28),
onTap: () => _onChangePhotographyMode(isEnable),
onTap: () => _onChangePhotographyMode(context, isEnable),
);
}
@ -188,11 +212,19 @@ class ToolsUIModel extends BaseUIModel {
/// -----------------------------------------------------------------------------------------
/// -----------------------------------------------------------------------------------------
Future<void> reScanPath() async {
scInstallPaths.clear();
rsiLauncherInstallPaths.clear();
scInstalledPath = "";
rsiLauncherInstalledPath = "";
Future<void> reScanPath(BuildContext context) async {
var scInstallPaths = <String>[];
var rsiLauncherInstallPaths = <String>[];
var scInstalledPath = "";
var rsiLauncherInstalledPath = "";
state = state.copyWith(
scInstalledPath: scInstalledPath,
rsiLauncherInstalledPath: rsiLauncherInstalledPath,
scInstallPaths: scInstallPaths,
rsiLauncherInstallPaths: rsiLauncherInstallPaths,
);
try {
rsiLauncherInstalledPath = await SystemHelper.getRSILauncherPath();
rsiLauncherInstallPaths.add(rsiLauncherInstalledPath);
@ -205,29 +237,37 @@ class ToolsUIModel extends BaseUIModel {
if (scInstallPaths.isNotEmpty) {
scInstalledPath = scInstallPaths.first;
}
state = state.copyWith(
scInstalledPath: scInstalledPath,
rsiLauncherInstalledPath: rsiLauncherInstalledPath,
scInstallPaths: scInstallPaths,
rsiLauncherInstallPaths: rsiLauncherInstallPaths,
);
} catch (e) {
dPrint(e);
showToast(context!, "解析 log 文件失败!\n请尝试使用 RSI Launcher log 修复 工具!");
if (!context.mounted) return;
showToast(context, "解析 log 文件失败!\n请尝试使用 RSI Launcher log 修复 工具!");
}
notifyListeners();
if (rsiLauncherInstalledPath == "") {
showToast(context!, "未找到 RSI 启动器,请尝试重新安装,或在设置中手动添加。");
if (!context.mounted) return;
showToast(context, "未找到 RSI 启动器,请尝试重新安装,或在设置中手动添加。");
}
if (scInstalledPath == "") {
showToast(context!, "未找到星际公民游戏安装位置,请至少完成一次游戏启动操作 或在设置中手动添加。");
if (!context.mounted) return;
showToast(context, "未找到星际公民游戏安装位置,请至少完成一次游戏启动操作 或在设置中手动添加。");
}
}
/// EAC
Future<void> _reinstallEAC() async {
if (scInstalledPath.isEmpty) {
showToast(context!, "该功能需要一个有效的游戏安装目录");
Future<void> _reinstallEAC(BuildContext context) async {
if (state.scInstalledPath.isEmpty) {
showToast(context, "该功能需要一个有效的游戏安装目录");
return;
}
working = true;
state = state.copyWith(working: true);
try {
final eacPath = "$scInstalledPath\\EasyAntiCheat";
final eacPath = "${state.scInstalledPath}\\EasyAntiCheat";
final eacJsonPath = "$eacPath\\Settings.json";
if (await File(eacJsonPath).exists()) {
Map<String, String> envVars = Platform.environment;
@ -246,18 +286,20 @@ class ToolsUIModel extends BaseUIModel {
if (await dir.exists()) {
await dir.delete(recursive: true);
}
final eacLauncher = File("$scInstalledPath\\StarCitizen_Launcher.exe");
final eacLauncher =
File("${state.scInstalledPath}\\StarCitizen_Launcher.exe");
if (await eacLauncher.exists()) {
await eacLauncher.delete(recursive: true);
}
showToast(context!,
if (!context.mounted) return;
showToast(context,
"已为您移除 EAC 文件,接下来将为您打开 RSI 启动器,请您前往 SETTINGS -> VERIFY 重装 EAC。");
_adminRSILauncher();
_adminRSILauncher(context);
} catch (e) {
showToast(context!, "出现错误:$e");
showToast(context, "出现错误:$e");
}
working = false;
loadData(skipPathScan: true);
state = state.copyWith(working: false);
loadToolsCard(context, skipPathScan: true);
}
Future<String> getSystemInfo() async {
@ -269,31 +311,34 @@ class ToolsUIModel extends BaseUIModel {
}
/// RSI
Future _adminRSILauncher() async {
if (rsiLauncherInstalledPath == "") {
showToast(context!, "未找到 RSI 启动器目录,请您尝试手动操作。");
Future _adminRSILauncher(BuildContext context) async {
if (state.rsiLauncherInstalledPath == "") {
showToast(context, "未找到 RSI 启动器目录,请您尝试手动操作。");
}
handleError(
() => SystemHelper.checkAndLaunchRSILauncher(rsiLauncherInstalledPath));
SystemHelper.checkAndLaunchRSILauncher(state.rsiLauncherInstalledPath);
}
Future<void> _rsiLogFix() async {
working = true;
Future<void> _rsiLogFix(BuildContext context) async {
state = state.copyWith(working: true);
final path = await SCLoggerHelper.getLogFilePath();
if (!await File(path!).exists()) {
if (!context.mounted) return;
showToast(
context!, "日志文件不存在,请尝试进行一次游戏启动或游戏安装,并退出启动器,若无法解决问题,请尝试将启动器更新至最新版本!");
context, "日志文件不存在,请尝试进行一次游戏启动或游戏安装,并退出启动器,若无法解决问题,请尝试将启动器更新至最新版本!");
return;
}
try {
SystemHelper.killRSILauncher();
await File(path).delete(recursive: true);
showToast(context!, "清理完毕,请完成一次安装 / 游戏启动 操作。");
SystemHelper.checkAndLaunchRSILauncher(rsiLauncherInstalledPath);
if (!context.mounted) return;
showToast(context, "清理完毕,请完成一次安装 / 游戏启动 操作。");
SystemHelper.checkAndLaunchRSILauncher(state.rsiLauncherInstalledPath);
} catch (_) {
showToast(context!, "清理失败,请手动移除,文件位置:$path");
if (!context.mounted) return;
showToast(context, "清理失败,请手动移除,文件位置:$path");
}
working = false;
state = state.copyWith(working: false);
}
openDir(path) async {
@ -301,11 +346,12 @@ class ToolsUIModel extends BaseUIModel {
SystemHelper.powershellPath, ["explorer.exe", "/select,\"$path\""]);
}
Future _showSystemInfo() async {
working = true;
Future _showSystemInfo(BuildContext context) async {
state = state.copyWith(working: true);
final systemInfo = await getSystemInfo();
if (!context.mounted) return;
showDialog<String>(
context: context!,
context: context,
builder: (context) => ContentDialog(
title: const Text('系统信息'),
content: Text(systemInfo),
@ -323,11 +369,11 @@ class ToolsUIModel extends BaseUIModel {
],
),
);
working = false;
state = state.copyWith(working: false);
}
Future<void> _cleanShaderCache() async {
working = true;
Future<void> _cleanShaderCache(BuildContext context) async {
state = state.copyWith(working: true);
final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath();
final l =
await Directory(gameShaderCachePath!).list(recursive: false).toList();
@ -338,42 +384,46 @@ class ToolsUIModel extends BaseUIModel {
}
}
}
loadData(skipPathScan: true);
working = false;
if (!context.mounted) return;
loadToolsCard(context, skipPathScan: true);
state = state.copyWith(working: false);
}
Future<void> _downloadP4k() async {
String savePath = scInstalledPath;
Future<void> _downloadP4k(BuildContext context) async {
String savePath = state.scInstalledPath;
String fileName = "Data.p4k";
if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) {
showToast(context!, "RSI启动器正在运行请先关闭启动器再使用此功能",
if (!context.mounted) return;
showToast(context, "RSI启动器正在运行请先关闭启动器再使用此功能",
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .35));
maxWidth: MediaQuery.of(context).size.width * .35));
return;
}
if (!context.mounted) return;
await showToast(
context!,
context,
"P4k 是星际公民的核心游戏文件,高达 100GB+盒子提供的离线下载是为了帮助一些p4k文件下载超级慢的用户 或用于修复官方启动器无法修复的 p4k 文件。"
"\n\n接下来会弹窗询问您保存位置(可以选择星际公民文件夹也可以选择别处),下载完成后请确保 P4K 文件夹位于 LIVE 文件夹内,之后使用星际公民启动器校验更新即可。");
try {
working = true;
notifyListeners();
await Aria2cManager.launchDaemon();
final aria2c = Aria2cManager.getClient();
state = state.copyWith(working: true);
final aria2cManager = ref.read(aria2cModelProvider.notifier);
await aria2cManager
.launchDaemon(appGlobalState.applicationBinaryModuleDir!);
final aria2c = ref.read(aria2cModelProvider).aria2c!;
// check download task list
for (var value in [
...await aria2c.tellActive(),
...await aria2c.tellWaiting(0, 100000)
]) {
final t = DownloaderUIModel.getTaskTypeAndName(value);
final t = HomeDownloaderUIModel.getTaskTypeAndName(value);
if (t.key == "torrent" && t.value.contains("Data.p4k")) {
showToast(context!, "已经有一个p4k下载任务正在进行中请前往下载管理器查看");
working = false;
if (!context.mounted) return;
showToast(context, "已经有一个p4k下载任务正在进行中请前往下载管理器查看");
state = state.copyWith(working: false);
return;
}
}
@ -386,8 +436,9 @@ class ToolsUIModel extends BaseUIModel {
}
}
if (torrentUrl == "") {
working = false;
showToast(context!, "功能维护中,请稍后重试!");
state = state.copyWith(working: false);
if (!context.mounted) return;
showToast(context, "功能维护中,请稍后重试!");
return;
}
@ -396,44 +447,46 @@ class ToolsUIModel extends BaseUIModel {
fileName: fileName,
lockParentWindow: true);
if (userSelect == null) {
working = false;
state = state.copyWith(working: false);
return;
}
savePath = userSelect;
dPrint(savePath);
notifyListeners();
if (savePath.endsWith("\\$fileName")) {
savePath = savePath.substring(0, savePath.length - fileName.length - 1);
}
final btData = await handleError(() => RSHttp.get(torrentUrl));
if (!context.mounted) return;
final btData = await RSHttp.get(torrentUrl).unwrap(context: context);
if (btData == null || btData.data == null) {
working = false;
state = state.copyWith(working: false);
return;
}
final b64Str = base64Encode(btData.data!);
final gid =
await aria2c.addTorrent(b64Str, extraParams: {"dir": savePath});
working = false;
state = state.copyWith(working: false);
dPrint("Aria2cManager.aria2c.addUri resp === $gid");
await aria2c.saveSession();
AnalyticsApi.touch("p4k_download");
BaseUIContainer(
uiCreate: () => DownloaderUI(),
modelCreate: () => DownloaderUIModel()).push(context!);
if (!context.mounted) return;
context.push("/index/downloader");
} catch (e) {
working = false;
showToast(context!, "初始化失败!: $e");
state = state.copyWith(working: false);
if (!context.mounted) return;
showToast(context, "初始化失败!: $e");
}
await Future.delayed(const Duration(seconds: 3));
launchUrlString(
"https://citizenwiki.cn/SC%E6%B1%89%E5%8C%96%E7%9B%92%E5%AD%90#%E5%88%86%E6%B5%81%E4%B8%8B%E8%BD%BD%E6%95%99%E7%A8%8B");
}
Future<bool> _checkPhotographyStatus({bool? setMode}) async {
Future<bool> _checkPhotographyStatus(BuildContext context,
{bool? setMode}) async {
final scInstalledPath = state.scInstalledPath;
const keys = ["AudioShakeStrength", "CameraSpringMovement", "ShakeScale"];
final attributesFile = File(
"$scInstalledPath\\USER\\Client\\0\\Profiles\\default\\attributes.xml");
@ -459,7 +512,8 @@ class ToolsUIModel extends BaseUIModel {
return isEnable;
} else {
if (!await attributesFile.exists()) {
showToast(context!, "配置文件不存在,请尝试运行一次游戏");
if (!context.mounted) return false;
showToast(context, "配置文件不存在,请尝试运行一次游戏");
return false;
}
final xmlFile = XmlDocument.parse(await attributesFile.readAsString());
@ -482,19 +536,17 @@ class ToolsUIModel extends BaseUIModel {
return true;
}
_onChangePhotographyMode(bool isEnable) async {
await handleError(() => _checkPhotographyStatus(setMode: !isEnable));
reloadData();
}
_onChangePhotographyMode(BuildContext context, bool isEnable) async {
_checkPhotographyStatus(context, setMode: !isEnable)
.unwrap(context: context);
loadToolsCard(context, skipPathScan: true);
}
class _ToolsItemData {
String key;
_ToolsItemData(this.key, this.name, this.infoString, this.icon, {this.onTap});
String name;
String infoString;
Widget icon;
AsyncCallback? onTap;
void onChangeGamePath(String v) {
state = state.copyWith(scInstalledPath: v);
}
void onChangeLauncherPath(String s) {
state = state.copyWith(rsiLauncherInstalledPath: s);
}
}

View File

@ -0,0 +1,300 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'tools_ui_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$ToolsUIState {
bool get working => throw _privateConstructorUsedError;
String get scInstalledPath => throw _privateConstructorUsedError;
String get rsiLauncherInstalledPath => throw _privateConstructorUsedError;
List<String> get scInstallPaths => throw _privateConstructorUsedError;
List<String> get rsiLauncherInstallPaths =>
throw _privateConstructorUsedError;
List<ToolsItemData> get items => throw _privateConstructorUsedError;
bool get isItemLoading => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ToolsUIStateCopyWith<ToolsUIState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ToolsUIStateCopyWith<$Res> {
factory $ToolsUIStateCopyWith(
ToolsUIState value, $Res Function(ToolsUIState) then) =
_$ToolsUIStateCopyWithImpl<$Res, ToolsUIState>;
@useResult
$Res call(
{bool working,
String scInstalledPath,
String rsiLauncherInstalledPath,
List<String> scInstallPaths,
List<String> rsiLauncherInstallPaths,
List<ToolsItemData> items,
bool isItemLoading});
}
/// @nodoc
class _$ToolsUIStateCopyWithImpl<$Res, $Val extends ToolsUIState>
implements $ToolsUIStateCopyWith<$Res> {
_$ToolsUIStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? working = null,
Object? scInstalledPath = null,
Object? rsiLauncherInstalledPath = null,
Object? scInstallPaths = null,
Object? rsiLauncherInstallPaths = null,
Object? items = null,
Object? isItemLoading = null,
}) {
return _then(_value.copyWith(
working: null == working
? _value.working
: working // ignore: cast_nullable_to_non_nullable
as bool,
scInstalledPath: null == scInstalledPath
? _value.scInstalledPath
: scInstalledPath // ignore: cast_nullable_to_non_nullable
as String,
rsiLauncherInstalledPath: null == rsiLauncherInstalledPath
? _value.rsiLauncherInstalledPath
: rsiLauncherInstalledPath // ignore: cast_nullable_to_non_nullable
as String,
scInstallPaths: null == scInstallPaths
? _value.scInstallPaths
: scInstallPaths // ignore: cast_nullable_to_non_nullable
as List<String>,
rsiLauncherInstallPaths: null == rsiLauncherInstallPaths
? _value.rsiLauncherInstallPaths
: rsiLauncherInstallPaths // ignore: cast_nullable_to_non_nullable
as List<String>,
items: null == items
? _value.items
: items // ignore: cast_nullable_to_non_nullable
as List<ToolsItemData>,
isItemLoading: null == isItemLoading
? _value.isItemLoading
: isItemLoading // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$ToolsUIStateImplCopyWith<$Res>
implements $ToolsUIStateCopyWith<$Res> {
factory _$$ToolsUIStateImplCopyWith(
_$ToolsUIStateImpl value, $Res Function(_$ToolsUIStateImpl) then) =
__$$ToolsUIStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{bool working,
String scInstalledPath,
String rsiLauncherInstalledPath,
List<String> scInstallPaths,
List<String> rsiLauncherInstallPaths,
List<ToolsItemData> items,
bool isItemLoading});
}
/// @nodoc
class __$$ToolsUIStateImplCopyWithImpl<$Res>
extends _$ToolsUIStateCopyWithImpl<$Res, _$ToolsUIStateImpl>
implements _$$ToolsUIStateImplCopyWith<$Res> {
__$$ToolsUIStateImplCopyWithImpl(
_$ToolsUIStateImpl _value, $Res Function(_$ToolsUIStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? working = null,
Object? scInstalledPath = null,
Object? rsiLauncherInstalledPath = null,
Object? scInstallPaths = null,
Object? rsiLauncherInstallPaths = null,
Object? items = null,
Object? isItemLoading = null,
}) {
return _then(_$ToolsUIStateImpl(
working: null == working
? _value.working
: working // ignore: cast_nullable_to_non_nullable
as bool,
scInstalledPath: null == scInstalledPath
? _value.scInstalledPath
: scInstalledPath // ignore: cast_nullable_to_non_nullable
as String,
rsiLauncherInstalledPath: null == rsiLauncherInstalledPath
? _value.rsiLauncherInstalledPath
: rsiLauncherInstalledPath // ignore: cast_nullable_to_non_nullable
as String,
scInstallPaths: null == scInstallPaths
? _value._scInstallPaths
: scInstallPaths // ignore: cast_nullable_to_non_nullable
as List<String>,
rsiLauncherInstallPaths: null == rsiLauncherInstallPaths
? _value._rsiLauncherInstallPaths
: rsiLauncherInstallPaths // ignore: cast_nullable_to_non_nullable
as List<String>,
items: null == items
? _value._items
: items // ignore: cast_nullable_to_non_nullable
as List<ToolsItemData>,
isItemLoading: null == isItemLoading
? _value.isItemLoading
: isItemLoading // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$ToolsUIStateImpl implements _ToolsUIState {
const _$ToolsUIStateImpl(
{this.working = false,
this.scInstalledPath = "",
this.rsiLauncherInstalledPath = "",
final List<String> scInstallPaths = const [],
final List<String> rsiLauncherInstallPaths = const [],
final List<ToolsItemData> items = const [],
this.isItemLoading = false})
: _scInstallPaths = scInstallPaths,
_rsiLauncherInstallPaths = rsiLauncherInstallPaths,
_items = items;
@override
@JsonKey()
final bool working;
@override
@JsonKey()
final String scInstalledPath;
@override
@JsonKey()
final String rsiLauncherInstalledPath;
final List<String> _scInstallPaths;
@override
@JsonKey()
List<String> get scInstallPaths {
if (_scInstallPaths is EqualUnmodifiableListView) return _scInstallPaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_scInstallPaths);
}
final List<String> _rsiLauncherInstallPaths;
@override
@JsonKey()
List<String> get rsiLauncherInstallPaths {
if (_rsiLauncherInstallPaths is EqualUnmodifiableListView)
return _rsiLauncherInstallPaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_rsiLauncherInstallPaths);
}
final List<ToolsItemData> _items;
@override
@JsonKey()
List<ToolsItemData> get items {
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_items);
}
@override
@JsonKey()
final bool isItemLoading;
@override
String toString() {
return 'ToolsUIState(working: $working, scInstalledPath: $scInstalledPath, rsiLauncherInstalledPath: $rsiLauncherInstalledPath, scInstallPaths: $scInstallPaths, rsiLauncherInstallPaths: $rsiLauncherInstallPaths, items: $items, isItemLoading: $isItemLoading)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ToolsUIStateImpl &&
(identical(other.working, working) || other.working == working) &&
(identical(other.scInstalledPath, scInstalledPath) ||
other.scInstalledPath == scInstalledPath) &&
(identical(
other.rsiLauncherInstalledPath, rsiLauncherInstalledPath) ||
other.rsiLauncherInstalledPath == rsiLauncherInstalledPath) &&
const DeepCollectionEquality()
.equals(other._scInstallPaths, _scInstallPaths) &&
const DeepCollectionEquality().equals(
other._rsiLauncherInstallPaths, _rsiLauncherInstallPaths) &&
const DeepCollectionEquality().equals(other._items, _items) &&
(identical(other.isItemLoading, isItemLoading) ||
other.isItemLoading == isItemLoading));
}
@override
int get hashCode => Object.hash(
runtimeType,
working,
scInstalledPath,
rsiLauncherInstalledPath,
const DeepCollectionEquality().hash(_scInstallPaths),
const DeepCollectionEquality().hash(_rsiLauncherInstallPaths),
const DeepCollectionEquality().hash(_items),
isItemLoading);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ToolsUIStateImplCopyWith<_$ToolsUIStateImpl> get copyWith =>
__$$ToolsUIStateImplCopyWithImpl<_$ToolsUIStateImpl>(this, _$identity);
}
abstract class _ToolsUIState implements ToolsUIState {
const factory _ToolsUIState(
{final bool working,
final String scInstalledPath,
final String rsiLauncherInstalledPath,
final List<String> scInstallPaths,
final List<String> rsiLauncherInstallPaths,
final List<ToolsItemData> items,
final bool isItemLoading}) = _$ToolsUIStateImpl;
@override
bool get working;
@override
String get scInstalledPath;
@override
String get rsiLauncherInstalledPath;
@override
List<String> get scInstallPaths;
@override
List<String> get rsiLauncherInstallPaths;
@override
List<ToolsItemData> get items;
@override
bool get isItemLoading;
@override
@JsonKey(ignore: true)
_$$ToolsUIStateImplCopyWith<_$ToolsUIStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tools_ui_model.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$toolsUIModelHash() => r'4fb78bfe350d792cfdadd3314f4763097ea1b279';
/// See also [ToolsUIModel].
@ProviderFor(ToolsUIModel)
final toolsUIModelProvider =
AutoDisposeNotifierProvider<ToolsUIModel, ToolsUIState>.internal(
ToolsUIModel.new,
name: r'toolsUIModelProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$toolsUIModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ToolsUIModel = AutoDisposeNotifier<ToolsUIState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@ -6,17 +6,17 @@ import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:hive/hive.dart';
import 'package:local_auth/local_auth.dart';
import 'package:starcitizen_doctor/common/conf/app_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/win32/credentials.dart';
import 'package:starcitizen_doctor/data/app_version_data.dart';
import 'package:starcitizen_doctor/data/app_web_localization_versions_data.dart';
import '../../../base/ui.dart';
typedef RsiLoginCallback = void Function(Map? data, bool success);
class WebViewModel {
@ -52,7 +52,10 @@ class WebViewModel {
final RsiLoginCallback? loginCallback;
initWebView({String title = ""}) async {
initWebView(
{String title = "",
required String applicationSupportDir,
required AppVersionData appVersionData}) async {
try {
final userBox = await Hive.openBox("app_conf");
isEnableToolSiteMirrors =
@ -61,8 +64,7 @@ class WebViewModel {
configuration: CreateConfiguration(
windowWidth: loginMode ? 960 : 1920,
windowHeight: loginMode ? 720 : 1080,
userDataFolderWindows:
"${AppConf.applicationSupportDir}/webview_data",
userDataFolderWindows: "$applicationSupportDir/webview_data",
title: title));
// webview.openDevToolsWindow();
webview.isNavigating.addListener(() async {
@ -140,8 +142,8 @@ class WebViewModel {
webview.evaluateJavaScript(
"getRSILauncherToken(\"$loginChannel\");");
}
} else if (url
.startsWith(await _handleMirrorsUrl("https://www.erkul.games"))) {
} else if (url.startsWith(await _handleMirrorsUrl(
"https://www.erkul.games", appVersionData))) {
dPrint("load script");
await Future.delayed(const Duration(milliseconds: 100));
await webview.evaluateJavaScript(localizationScript);
@ -149,8 +151,8 @@ class WebViewModel {
final replaceWords = _getLocalizationResource("DPS");
await webview.evaluateJavaScript(
"WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)");
} else if (url
.startsWith(await _handleMirrorsUrl("https://uexcorp.space"))) {
} else if (url.startsWith(await _handleMirrorsUrl(
"https://uexcorp.space", appVersionData))) {
dPrint("load script");
await Future.delayed(const Duration(milliseconds: 100));
await webview.evaluateJavaScript(localizationScript);
@ -186,10 +188,11 @@ class WebViewModel {
}
}
Future<String> _handleMirrorsUrl(String url) async {
Future<String> _handleMirrorsUrl(
String url, AppVersionData appVersionData) async {
var finalUrl = url;
if (isEnableToolSiteMirrors) {
for (var kv in AppConf.networkVersionData!.webMirrors!.entries) {
for (var kv in appVersionData.webMirrors!.entries) {
if (url.startsWith(kv.key)) {
finalUrl = url.replaceFirst(kv.key, kv.value);
}
@ -198,8 +201,8 @@ class WebViewModel {
return finalUrl;
}
launch(String url) async {
webview.launch(await _handleMirrorsUrl(url));
launch(String url, AppVersionData appVersionData) async {
webview.launch(await _handleMirrorsUrl(url, appVersionData));
}
initLocalization(AppWebLocalizationVersionsData v) async {

View File

@ -1,25 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
class MyPageRoute extends FluentPageRoute {
late final WidgetBuilder _builder;
MyPageRoute({required super.builder}) : _builder = builder;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
assert(debugCheckHasFluentTheme(context));
final result = _builder(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: EntrancePageTransition(
animation: CurvedAnimation(
parent: animation,
curve: FluentTheme.of(context).animationCurve,
),
child: result,
),
);
}
}

View File

@ -1,11 +1,18 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:window_manager/window_manager.dart';
import 'package:markdown_widget/config/all.dart';
import 'package:markdown_widget/widget/all.dart';
import 'package:extended_image/extended_image.dart';
import 'dart:ui' as ui;
import 'package:markdown_widget/config/all.dart';
import 'package:markdown_widget/widget/all.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../base/ui.dart';
export 'src/cache_image.dart';
export 'src/countdown_time_text.dart';
export '../common/utils/async.dart';
export '../common/utils/base_utils.dart';
Widget makeLoading(
BuildContext context, {
@ -16,46 +23,66 @@ Widget makeLoading(
child: SizedBox(
width: width,
height: width,
// child: Lottie.asset("images/lottie/loading.zip", width: width),
child: const ProgressRing(),
),
);
}
Widget makeSafeAre(BuildContext context, {bool withKeyboard = true}) {
return SafeArea(
child: Column(
Widget makeDefaultPage(BuildContext context,
{Widget? titleRow,
List<Widget>? actions,
Widget? content,
bool automaticallyImplyLeading = true,
String title = "",
bool useBodyContainer = false}) {
return NavigationView(
appBar: NavigationAppBar(
automaticallyImplyLeading: automaticallyImplyLeading,
title: DragToMoveArea(
child: titleRow ??
Column(
children: [
const SizedBox(height: 4),
if (withKeyboard)
SizedBox(
height: MediaQuery.of(context).viewInsets.bottom,
),
Expanded(
child: Row(
children: [
Text(title),
],
));
),
)
],
),
),
actions: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [...?actions, const WindowButtons()],
)),
content: useBodyContainer
? Container(
decoration: BoxDecoration(
color: FluentTheme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(9),
),
child: content,
)
: content,
);
}
makeSvgColor(Color color) {
return ui.ColorFilter.mode(color, ui.BlendMode.srcIn);
}
class WindowButtons extends StatelessWidget {
const WindowButtons({super.key});
bool isPadUI(BuildContext context) {
final size = MediaQuery.of(context).size;
return size.width >= size.height;
@override
Widget build(BuildContext context) {
final FluentThemeData theme = FluentTheme.of(context);
return SizedBox(
width: 138,
height: 50,
child: WindowCaption(
brightness: theme.brightness,
backgroundColor: Colors.transparent,
),
);
}
fastPadding(
{required double? all,
required Widget child,
double left = 0.0,
double top = 0.0,
double right = 0.0,
double bottom = 0.0}) {
return Padding(
padding: all != null
? EdgeInsets.all(all)
: EdgeInsets.only(left: left, top: top, right: right, bottom: bottom),
child: child);
}
List<Widget> makeMarkdownView(String description, {String? attachmentsUrl}) {
@ -103,10 +130,79 @@ List<Widget> makeMarkdownView(String description, {String? attachmentsUrl}) {
]));
}
class NoScrollBehavior extends ScrollBehavior {
ColorFilter makeSvgColor(Color color) {
return ui.ColorFilter.mode(color, ui.BlendMode.srcIn);
}
CustomTransitionPage<T> myPageBuilder<T>(
BuildContext context, GoRouterState state, Widget child) {
return CustomTransitionPage(
child: child,
transitionDuration: const Duration(milliseconds: 150),
reverseTransitionDuration: const Duration(milliseconds: 150),
transitionsBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: const Offset(0.0, 0.0),
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
});
}
class LoadingWidget<T> extends HookConsumerWidget {
final T? data;
final Future<T?> Function()? onLoadData;
final Widget Function(BuildContext context, T data) childBuilder;
const LoadingWidget(
{super.key, this.data, required this.childBuilder, this.onLoadData});
@override
Widget buildOverscrollIndicator(
BuildContext context, Widget child, ScrollableDetails details) {
return child;
Widget build(BuildContext context, WidgetRef ref) {
final dataState = useState<T?>(null);
final errorMsg = useState("");
useEffect(() {
if (data == null && onLoadData != null) {
_loadData(dataState, errorMsg);
return null;
}
return null;
}, const []);
if (errorMsg.value.isNotEmpty) {
return Button(
onPressed: () {
_loadData(dataState, errorMsg);
},
child: Center(
child: Text(errorMsg.value),
),
);
}
if (dataState.value == null && data == null) return makeLoading(context);
return childBuilder(context, (data ?? dataState.value) as T);
}
void _loadData(
ValueNotifier<T?> dataState, ValueNotifier<String> errorMsg) async {
errorMsg.value = "";
try {
final r = await onLoadData!();
dataState.value = r;
} catch (e) {
errorMsg.value = e.toString();
}
}
}
addPostFrameCallback(Function() callback) {
WidgetsBinding.instance.addPostFrameCallback((_) {
callback();
});
}

View File

@ -30,7 +30,12 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.3.6
flutter_riverpod: ^2.4.10
riverpod_annotation: ^2.3.4
flutter_hooks: ^0.20.5
hooks_riverpod: ^2.4.10
json_annotation: ^4.8.1
go_router: ^13.2.0
window_manager: ^0.3.2
fluent_ui: 4.8.5
flutter_staggered_grid_view: ^0.7.0
@ -73,8 +78,8 @@ dependencies:
rust_builder:
path: rust_builder
aria2:
#git: https://github.com/xkeyC/dart_aria2_rpc.git
path: ../../xkeyC/dart_aria2_rpc
git: https://github.com/xkeyC/dart_aria2_rpc.git
# path: ../../xkeyC/dart_aria2_rpc
intl: ^0.18.0
synchronized: ^3.1.0+1
dependency_overrides:
@ -91,8 +96,12 @@ dev_dependencies:
# rules and activating additional ones.
flutter_lints: ^3.0.0
msix: ^3.16.4
build_runner: ^2.4.6
build_runner: ^2.4.8
freezed: ^2.4.5
json_serializable: ^6.7.1
riverpod_generator: ^2.3.11
custom_lint: ^0.6.2
riverpod_lint: ^2.3.9
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -94,7 +94,7 @@ fn _reade_resp_header(r_header: &HeaderMap) -> HashMap<String, String> {
for ele in r_header {
resp_headers.insert(
ele.0.as_str().to_string(),
ele.1.to_str().unwrap().to_string(),
ele.1.to_str().unwrap_or("").to_string(),
);
}
resp_headers