feat: desktop_multi_window Support

This commit is contained in:
xkeyC 2025-03-16 17:14:45 +08:00
parent b18024a8ce
commit f5f3e4753c
17 changed files with 588 additions and 163 deletions

View File

@ -58,45 +58,35 @@ GoRouter router(Ref ref) {
routes: [
GoRoute(
path: '/',
pageBuilder: (context, state) =>
myPageBuilder(context, state, const SplashUI()),
pageBuilder: (context, state) => myPageBuilder(context, state, const SplashUI()),
),
GoRoute(
path: '/index',
pageBuilder: (context, state) =>
myPageBuilder(context, state, const IndexUI()),
pageBuilder: (context, state) => myPageBuilder(context, state, const IndexUI()),
routes: [
GoRoute(
path: "downloader",
pageBuilder: (context, state) =>
myPageBuilder(context, state, const HomeDownloaderUI())),
pageBuilder: (context, state) => myPageBuilder(context, state, const HomeDownloaderUI())),
GoRoute(
path: 'game_doctor',
pageBuilder: (context, state) =>
myPageBuilder(context, state, const HomeGameDoctorUI()),
pageBuilder: (context, state) => myPageBuilder(context, state, const HomeGameDoctorUI()),
),
GoRoute(
path: 'performance',
pageBuilder: (context, state) =>
myPageBuilder(context, state, const HomePerformanceUI()),
pageBuilder: (context, state) => myPageBuilder(context, state, const HomePerformanceUI()),
),
GoRoute(
path: 'advanced_localization',
pageBuilder: (context, state) =>
myPageBuilder(context, state, const AdvancedLocalizationUI()))
pageBuilder: (context, state) => myPageBuilder(context, state, const AdvancedLocalizationUI()))
],
),
GoRoute(path: '/tools', builder: (_, __) => const SizedBox(), routes: [
GoRoute(
path: 'unp4kc',
pageBuilder: (context, state) =>
myPageBuilder(context, state, const UnP4kcUI()),
pageBuilder: (context, state) => myPageBuilder(context, state, const UnP4kcUI()),
),
]),
GoRoute(
path: '/guide',
pageBuilder: (context, state) =>
myPageBuilder(context, state, const GuideUI()))
GoRoute(path: '/guide', pageBuilder: (context, state) => myPageBuilder(context, state, const GuideUI()))
],
);
}
@ -218,11 +208,9 @@ class AppGlobalModel extends _$AppGlobalModel {
AppConf.setNetworkChannels(networkVersionData.gameChannels);
checkActivityThemeColor(networkVersionData);
if (ConstConf.isMSE) {
dPrint(
"lastVersion=${networkVersionData.mSELastVersion} ${networkVersionData.mSELastVersionCode}");
dPrint("lastVersion=${networkVersionData.mSELastVersion} ${networkVersionData.mSELastVersionCode}");
} else {
dPrint(
"lastVersion=${networkVersionData.lastVersion} ${networkVersionData.lastVersionCode}");
dPrint("lastVersion=${networkVersionData.lastVersion} ${networkVersionData.lastVersionCode}");
}
state = state.copyWith(networkVersionData: networkVersionData);
} catch (e) {
@ -234,23 +222,18 @@ class AppGlobalModel extends _$AppGlobalModel {
if (state.networkVersionData == null) {
if (!context.mounted) return false;
await showToast(
context,
S.current.app_common_network_error(
ConstConf.appVersionDate, checkUpdateError.toString()));
context, S.current.app_common_network_error(ConstConf.appVersionDate, checkUpdateError.toString()));
return false;
}
if (!Platform.isWindows) return false;
final lastVersion = ConstConf.isMSE
? state.networkVersionData?.mSELastVersionCode
: state.networkVersionData?.lastVersionCode;
final lastVersion =
ConstConf.isMSE ? state.networkVersionData?.mSELastVersionCode : state.networkVersionData?.lastVersionCode;
if ((lastVersion ?? 0) > ConstConf.appVersionCode) {
// need update
if (!context.mounted) return false;
final r = await showDialog(
dismissWithEsc: false,
context: context,
builder: (context) => const UpgradeDialogUI());
final r =
await showDialog(dismissWithEsc: false, context: context, builder: (context) => const UpgradeDialogUI());
if (r != true) {
if (!context.mounted) return false;
@ -277,8 +260,8 @@ class AppGlobalModel extends _$AppGlobalModel {
dPrint("now == $now start == $startTime end == $endTime");
if (now < startTime) {
_activityThemeColorTimer = Timer(Duration(milliseconds: startTime - now),
() => checkActivityThemeColor(networkVersionData));
_activityThemeColorTimer =
Timer(Duration(milliseconds: startTime - now), () => checkActivityThemeColor(networkVersionData));
dPrint("start Timer ....");
} else if (now >= startTime && now <= endTime) {
dPrint("update Color ....");
@ -286,17 +269,15 @@ class AppGlobalModel extends _$AppGlobalModel {
final colorCfg = networkVersionData.activityColors;
state = state.copyWith(
themeConf: ThemeConf(
backgroundColor: HexColor(colorCfg?.background ?? "#132431")
.withValues(alpha: .75),
menuColor:
HexColor(colorCfg?.menu ?? "#132431").withValues(alpha: .95),
backgroundColor: HexColor(colorCfg?.background ?? "#132431").withValues(alpha: .75),
menuColor: HexColor(colorCfg?.menu ?? "#132431").withValues(alpha: .95),
micaColor: HexColor(colorCfg?.mica ?? "#0A3142"),
),
);
// wait for end
_activityThemeColorTimer = Timer(Duration(milliseconds: endTime - now),
() => checkActivityThemeColor(networkVersionData));
_activityThemeColorTimer =
Timer(Duration(milliseconds: endTime - now), () => checkActivityThemeColor(networkVersionData));
} else {
dPrint("reset Color ....");
state = state.copyWith(
@ -317,9 +298,8 @@ class AppGlobalModel extends _$AppGlobalModel {
await appConfBox.put("app_locale", null);
return;
}
final localeCode = value.countryCode != null
? "${value.languageCode}_${value.countryCode ?? ""}"
: value.languageCode;
final localeCode =
value.countryCode != null ? "${value.languageCode}_${value.countryCode ?? ""}" : value.languageCode;
dPrint("changeLocale == $value localeCode=== $localeCode");
await appConfBox.put("app_locale", localeCode);
state = state.copyWith(appLocale: value);
@ -329,8 +309,7 @@ class AppGlobalModel extends _$AppGlobalModel {
Future<String> _initAppDir() async {
if (Platform.isWindows) {
final userProfileDir = Platform.environment["USERPROFILE"];
final applicationSupportDir =
(await getApplicationSupportDirectory()).absolute.path;
final applicationSupportDir = (await getApplicationSupportDirectory()).absolute.path;
String? applicationBinaryModuleDir;
try {
await initDPrintFile(applicationSupportDir);
@ -338,8 +317,7 @@ class AppGlobalModel extends _$AppGlobalModel {
dPrint("initDPrintFile Error: $e");
}
if (ConstConf.isMSE && userProfileDir != null) {
applicationBinaryModuleDir =
"$userProfileDir\\AppData\\Local\\Temp\\SCToolbox\\modules";
applicationBinaryModuleDir = "$userProfileDir\\AppData\\Local\\Temp\\SCToolbox\\modules";
} else {
applicationBinaryModuleDir = "$applicationSupportDir\\modules";
}
@ -351,8 +329,7 @@ class AppGlobalModel extends _$AppGlobalModel {
);
return applicationSupportDir;
} else {
final applicationSupportDir =
(await getApplicationSupportDirectory()).absolute.path;
final applicationSupportDir = (await getApplicationSupportDirectory()).absolute.path;
final applicationBinaryModuleDir = "$applicationSupportDir/modules";
dPrint("applicationSupportDir == $applicationSupportDir");
dPrint("applicationBinaryModuleDir == $applicationBinaryModuleDir");

View File

@ -7,16 +7,14 @@ import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:starcitizen_doctor/generated/l10n.dart';
Future showToast(BuildContext context, String msg,
{BoxConstraints? constraints, String? title}) async {
Future showToast(BuildContext context, String msg, {BoxConstraints? constraints, String? title}) async {
return showBaseDialog(context,
title: title ?? S.current.app_common_tip,
content: Text(msg),
actions: [
FilledButton(
child: Padding(
padding:
const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
child: Text(S.current.app_common_tip_i_know),
),
onPressed: () => Navigator.pop(context),
@ -25,11 +23,8 @@ Future showToast(BuildContext context, String msg,
constraints: constraints);
}
Future<bool> showConfirmDialogs(
BuildContext context, String title, Widget content,
{String confirm = "",
String cancel = "",
BoxConstraints? constraints}) async {
Future<bool> showConfirmDialogs(BuildContext context, String title, Widget content,
{String confirm = "", String cancel = "", BoxConstraints? constraints}) async {
if (confirm.isEmpty) confirm = S.current.app_common_tip_confirm;
if (cancel.isEmpty) cancel = S.current.app_common_tip_cancel;
@ -40,8 +35,7 @@ Future<bool> showConfirmDialogs(
if (confirm.isNotEmpty)
FilledButton(
child: Padding(
padding:
const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
child: Text(confirm),
),
onPressed: () => Navigator.pop(context, true),
@ -49,8 +43,7 @@ Future<bool> showConfirmDialogs(
if (cancel.isNotEmpty)
Button(
child: Padding(
padding:
const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
child: Text(cancel),
),
onPressed: () => Navigator.pop(context, false),
@ -62,13 +55,15 @@ Future<bool> showConfirmDialogs(
Future<String?> showInputDialogs(BuildContext context,
{required String title,
required String content,
BoxConstraints? constraints,
String? initialValue,
List<TextInputFormatter>? inputFormatters}) async {
required String content,
BoxConstraints? constraints,
String? initialValue,
List<TextInputFormatter>? inputFormatters}) async {
String? userInput;
constraints ??=
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .38);
constraints ??= BoxConstraints(maxWidth: MediaQuery
.of(context)
.size
.width * .38);
final ok = await showConfirmDialogs(
context,
title,
@ -97,22 +92,20 @@ Future<String?> showInputDialogs(BuildContext context,
}
Future showBaseDialog(BuildContext context,
{required String title,
required Widget content,
List<Widget>? actions,
BoxConstraints? constraints}) async {
{required String title, required Widget content, List<Widget>? actions, BoxConstraints? constraints}) async {
return await showDialog(
context: context,
builder: (context) => ContentDialog(
title: Text(title),
content: content,
constraints: constraints ??
const BoxConstraints(
maxWidth: 512,
maxHeight: 756.0,
),
actions: actions,
),
builder: (context) =>
ContentDialog(
title: Text(title),
content: content,
constraints: constraints ??
const BoxConstraints(
maxWidth: 512,
maxHeight: 756.0,
),
actions: actions,
),
);
}
@ -120,10 +113,8 @@ bool stringIsNotEmpty(String? s) {
return s != null && (s.isNotEmpty);
}
Future<Uint8List?> widgetToPngImage(GlobalKey repaintBoundaryKey,
{double pixelRatio = 3.0}) async {
RenderRepaintBoundary? boundary = repaintBoundaryKey.currentContext
?.findRenderObject() as RenderRepaintBoundary?;
Future<Uint8List?> widgetToPngImage(GlobalKey repaintBoundaryKey, {double pixelRatio = 3.0}) async {
RenderRepaintBoundary? boundary = repaintBoundaryKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
if (boundary == null) return null;
ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
@ -133,11 +124,25 @@ Future<Uint8List?> widgetToPngImage(GlobalKey repaintBoundaryKey,
return pngBytes;
}
double roundDoubleTo(double value, double precision) =>
(value * precision).round() / precision;
double roundDoubleTo(double value, double precision) => (value * precision).round() / precision;
int getMinNumber(List<int> list) {
if (list.isEmpty) return 0;
list.sort((a, b) => a.compareTo(b));
return list.first;
}
String colorToHexCode(Color color, {ignoreTransparency = false}) {
final colorValue = color.toARGB32();
final colorAlpha = ((0xff000000 & colorValue) >> 24);
final r = ((0x00ff0000 & colorValue) >> 16).toRadixString(16).padLeft(2, '0');
final g = ((0x0000ff00 & colorValue) >> 8).toRadixString(16).padLeft(2, '0');
final b = ((0x000000ff & colorValue) >> 0).toRadixString(16).padLeft(2, '0');
final a = colorAlpha.toRadixString(16).padLeft(2, '0');
if (ignoreTransparency || colorAlpha == 255) {
return '#$r$g$b';
} else {
return '#$a$r$g$b';
}
}

View File

@ -0,0 +1,108 @@
import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:starcitizen_doctor/app.dart';
import 'package:starcitizen_doctor/ui/tools/log_analyze_ui/log_analyze_ui.dart';
import 'base_utils.dart';
part 'multi_window_manager.freezed.dart';
part 'multi_window_manager.g.dart';
@freezed
class MultiWindowAppState with _$MultiWindowAppState {
const factory MultiWindowAppState({
required String backgroundColor,
required String menuColor,
required String micaColor,
String? languageCode,
String? countryCode,
}) = _MultiWindowAppState;
factory MultiWindowAppState.fromJson(Map<String, dynamic> json) => _$MultiWindowAppStateFromJson(json);
}
class MultiWindowManager {
static Future<void> launchSubWindow(String type, AppGlobalState appGlobalState) async {
final window = await DesktopMultiWindow.createWindow(jsonEncode({
'window_type': type,
'app_state': _appStateToWindowState(appGlobalState).toJson(),
}));
window.setTitle("Log 分析器");
await window.center();
await window.show();
// sendAppStateBroadcast(appGlobalState);
}
static sendAppStateBroadcast(AppGlobalState appGlobalState) {
DesktopMultiWindow.invokeMethod(
0,
'app_state_broadcast',
_appStateToWindowState(appGlobalState).toJson(),
);
}
static MultiWindowAppState _appStateToWindowState(AppGlobalState appGlobalState) {
return MultiWindowAppState(
backgroundColor: colorToHexCode(appGlobalState.themeConf.backgroundColor),
menuColor: colorToHexCode(appGlobalState.themeConf.menuColor),
micaColor: colorToHexCode(appGlobalState.themeConf.micaColor),
languageCode: appGlobalState.appLocale?.languageCode,
countryCode: appGlobalState.appLocale?.countryCode,
);
}
static void runSubWindowApp(List<String> args) {
final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map<String, dynamic>;
Widget? windowWidget;
switch (argument["window_type"]) {
case "log_analyze":
windowWidget = const ToolsLogAnalyzeDialogUI();
break;
default:
throw Exception('Unknown window type');
}
final windowAppState = MultiWindowAppState.fromJson(argument['app_state'] ?? {});
return runApp(ProviderScope(
child: FluentApp(
title: "StarCitizenToolBox",
restorationScopeId: "StarCitizenToolBox",
themeMode: ThemeMode.dark,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
FluentLocalizations.delegate,
],
supportedLocales: const [Locale('en', 'US')],
home: windowWidget,
theme: FluentThemeData(
brightness: Brightness.dark,
fontFamily: "SourceHanSansCN-Regular",
navigationPaneTheme: NavigationPaneThemeData(
backgroundColor: HexColor(windowAppState.backgroundColor),
),
menuColor: HexColor(windowAppState.menuColor),
micaBackgroundColor: HexColor(windowAppState.micaColor),
buttonTheme: ButtonThemeData(
defaultButtonStyle: ButtonStyle(
shape: WidgetStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(color: Colors.white.withValues(alpha: .01)))),
))),
locale: windowAppState.languageCode != null
? Locale(windowAppState.languageCode!, windowAppState.countryCode)
: null,
debugShowCheckedModeBanner: false,
),
));
}
}

View File

@ -0,0 +1,255 @@
// 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 'multi_window_manager.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');
MultiWindowAppState _$MultiWindowAppStateFromJson(Map<String, dynamic> json) {
return _MultiWindowAppState.fromJson(json);
}
/// @nodoc
mixin _$MultiWindowAppState {
String get backgroundColor => throw _privateConstructorUsedError;
String get menuColor => throw _privateConstructorUsedError;
String get micaColor => throw _privateConstructorUsedError;
String? get languageCode => throw _privateConstructorUsedError;
String? get countryCode => throw _privateConstructorUsedError;
/// Serializes this MultiWindowAppState to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of MultiWindowAppState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MultiWindowAppStateCopyWith<MultiWindowAppState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MultiWindowAppStateCopyWith<$Res> {
factory $MultiWindowAppStateCopyWith(
MultiWindowAppState value, $Res Function(MultiWindowAppState) then) =
_$MultiWindowAppStateCopyWithImpl<$Res, MultiWindowAppState>;
@useResult
$Res call(
{String backgroundColor,
String menuColor,
String micaColor,
String? languageCode,
String? countryCode});
}
/// @nodoc
class _$MultiWindowAppStateCopyWithImpl<$Res, $Val extends MultiWindowAppState>
implements $MultiWindowAppStateCopyWith<$Res> {
_$MultiWindowAppStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MultiWindowAppState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? backgroundColor = null,
Object? menuColor = null,
Object? micaColor = null,
Object? languageCode = freezed,
Object? countryCode = freezed,
}) {
return _then(_value.copyWith(
backgroundColor: null == backgroundColor
? _value.backgroundColor
: backgroundColor // ignore: cast_nullable_to_non_nullable
as String,
menuColor: null == menuColor
? _value.menuColor
: menuColor // ignore: cast_nullable_to_non_nullable
as String,
micaColor: null == micaColor
? _value.micaColor
: micaColor // ignore: cast_nullable_to_non_nullable
as String,
languageCode: freezed == languageCode
? _value.languageCode
: languageCode // ignore: cast_nullable_to_non_nullable
as String?,
countryCode: freezed == countryCode
? _value.countryCode
: countryCode // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
/// @nodoc
abstract class _$$MultiWindowAppStateImplCopyWith<$Res>
implements $MultiWindowAppStateCopyWith<$Res> {
factory _$$MultiWindowAppStateImplCopyWith(_$MultiWindowAppStateImpl value,
$Res Function(_$MultiWindowAppStateImpl) then) =
__$$MultiWindowAppStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String backgroundColor,
String menuColor,
String micaColor,
String? languageCode,
String? countryCode});
}
/// @nodoc
class __$$MultiWindowAppStateImplCopyWithImpl<$Res>
extends _$MultiWindowAppStateCopyWithImpl<$Res, _$MultiWindowAppStateImpl>
implements _$$MultiWindowAppStateImplCopyWith<$Res> {
__$$MultiWindowAppStateImplCopyWithImpl(_$MultiWindowAppStateImpl _value,
$Res Function(_$MultiWindowAppStateImpl) _then)
: super(_value, _then);
/// Create a copy of MultiWindowAppState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? backgroundColor = null,
Object? menuColor = null,
Object? micaColor = null,
Object? languageCode = freezed,
Object? countryCode = freezed,
}) {
return _then(_$MultiWindowAppStateImpl(
backgroundColor: null == backgroundColor
? _value.backgroundColor
: backgroundColor // ignore: cast_nullable_to_non_nullable
as String,
menuColor: null == menuColor
? _value.menuColor
: menuColor // ignore: cast_nullable_to_non_nullable
as String,
micaColor: null == micaColor
? _value.micaColor
: micaColor // ignore: cast_nullable_to_non_nullable
as String,
languageCode: freezed == languageCode
? _value.languageCode
: languageCode // ignore: cast_nullable_to_non_nullable
as String?,
countryCode: freezed == countryCode
? _value.countryCode
: countryCode // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$MultiWindowAppStateImpl implements _MultiWindowAppState {
const _$MultiWindowAppStateImpl(
{required this.backgroundColor,
required this.menuColor,
required this.micaColor,
this.languageCode,
this.countryCode});
factory _$MultiWindowAppStateImpl.fromJson(Map<String, dynamic> json) =>
_$$MultiWindowAppStateImplFromJson(json);
@override
final String backgroundColor;
@override
final String menuColor;
@override
final String micaColor;
@override
final String? languageCode;
@override
final String? countryCode;
@override
String toString() {
return 'MultiWindowAppState(backgroundColor: $backgroundColor, menuColor: $menuColor, micaColor: $micaColor, languageCode: $languageCode, countryCode: $countryCode)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MultiWindowAppStateImpl &&
(identical(other.backgroundColor, backgroundColor) ||
other.backgroundColor == backgroundColor) &&
(identical(other.menuColor, menuColor) ||
other.menuColor == menuColor) &&
(identical(other.micaColor, micaColor) ||
other.micaColor == micaColor) &&
(identical(other.languageCode, languageCode) ||
other.languageCode == languageCode) &&
(identical(other.countryCode, countryCode) ||
other.countryCode == countryCode));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, backgroundColor, menuColor,
micaColor, languageCode, countryCode);
/// Create a copy of MultiWindowAppState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MultiWindowAppStateImplCopyWith<_$MultiWindowAppStateImpl> get copyWith =>
__$$MultiWindowAppStateImplCopyWithImpl<_$MultiWindowAppStateImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$MultiWindowAppStateImplToJson(
this,
);
}
}
abstract class _MultiWindowAppState implements MultiWindowAppState {
const factory _MultiWindowAppState(
{required final String backgroundColor,
required final String menuColor,
required final String micaColor,
final String? languageCode,
final String? countryCode}) = _$MultiWindowAppStateImpl;
factory _MultiWindowAppState.fromJson(Map<String, dynamic> json) =
_$MultiWindowAppStateImpl.fromJson;
@override
String get backgroundColor;
@override
String get menuColor;
@override
String get micaColor;
@override
String? get languageCode;
@override
String? get countryCode;
/// Create a copy of MultiWindowAppState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MultiWindowAppStateImplCopyWith<_$MultiWindowAppStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'multi_window_manager.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$MultiWindowAppStateImpl _$$MultiWindowAppStateImplFromJson(
Map<String, dynamic> json) =>
_$MultiWindowAppStateImpl(
backgroundColor: json['backgroundColor'] as String,
menuColor: json['menuColor'] as String,
micaColor: json['micaColor'] as String,
languageCode: json['languageCode'] as String?,
countryCode: json['countryCode'] as String?,
);
Map<String, dynamic> _$$MultiWindowAppStateImplToJson(
_$MultiWindowAppStateImpl instance) =>
<String, dynamic>{
'backgroundColor': instance.backgroundColor,
'menuColor': instance.menuColor,
'micaColor': instance.micaColor,
'languageCode': instance.languageCode,
'countryCode': instance.countryCode,
};

View File

@ -7,6 +7,7 @@ import 'package:window_manager/window_manager.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'app.dart';
import 'common/utils/multi_window_manager.dart';
void main(List<String> args) async {
// webview window
@ -14,6 +15,10 @@ void main(List<String> args) async {
backgroundColor: const Color.fromRGBO(19, 36, 49, 1), builder: _defaultWebviewTitleBar)) {
return;
}
if (args.firstOrNull == 'multi_window') {
MultiWindowManager.runSubWindowApp(args);
return;
}
WidgetsFlutterBinding.ensureInitialized();
await _initWindow();
// run app

View File

@ -0,0 +1,12 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'log_analyze_provider.g.dart';
@riverpod
class ToolsLogAnalyze extends _$ToolsLogAnalyze {
@override
void build() async {
return;
}
}

View File

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

View File

@ -0,0 +1,18 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ToolsLogAnalyzeDialogUI extends HookConsumerWidget {
const ToolsLogAnalyzeDialogUI({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ScaffoldPage(
header: const PageHeader(
title: Text("Log 分析器"),
),
content: Column(
children: [],
),
);
}
}

View File

@ -16,6 +16,7 @@ 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/log.dart';
import 'package:starcitizen_doctor/common/utils/multi_window_manager.dart';
import 'package:starcitizen_doctor/common/utils/provider.dart';
import 'package:starcitizen_doctor/provider/aria2c.dart';
import 'package:starcitizen_doctor/ui/home/downloader/home_downloader_ui_model.dart';
@ -90,6 +91,13 @@ class ToolsUIModel extends _$ToolsUIModel {
const Icon(FluentIcons.virtual_network, size: 24),
onTap: () => _doHostsBooster(context),
),
ToolsItemData(
"log_analyze",
"log 分析器",
"分析您的游玩记录 (登录、死亡、击杀 等信息)",
Icon(FluentIcons.analytics_logo),
onTap: () => _showLogAnalyze(context),
),
ToolsItemData(
"rsilauncher_enhance_mod",
S.current.tools_rsi_launcher_enhance_title,
@ -167,9 +175,8 @@ class ToolsUIModel extends _$ToolsUIModel {
ToolsItemData(
"remove_nvme_settings",
S.current.tools_action_remove_nvme_registry_patch,
S.current.tools_action_info_nvme_patch_issue(nvmePatchStatus
? S.current.localization_info_installed
: S.current.tools_action_info_not_installed),
S.current.tools_action_info_nvme_patch_issue(
nvmePatchStatus ? S.current.localization_info_installed : S.current.tools_action_info_not_installed),
const Icon(FluentIcons.hard_drive, size: 24),
onTap: nvmePatchStatus
? () async {
@ -177,8 +184,7 @@ class ToolsUIModel extends _$ToolsUIModel {
await SystemHelper.doRemoveNvmePath();
state = state.copyWith(working: false);
if (!context.mounted) return;
showToast(context,
S.current.tools_action_info_removed_restart_effective);
showToast(context, S.current.tools_action_info_removed_restart_effective);
loadToolsCard(context, skipPathScan: true);
}
: null,
@ -194,8 +200,7 @@ class ToolsUIModel extends _$ToolsUIModel {
final r = await SystemHelper.addNvmePatch();
if (r == "") {
if (!context.mounted) return;
showToast(
context, S.current.tools_action_info_fix_success_restart);
showToast(context, S.current.tools_action_info_fix_success_restart);
} else {
if (!context.mounted) return;
showToast(context, S.current.doctor_action_result_fix_fail(r));
@ -209,11 +214,11 @@ class ToolsUIModel extends _$ToolsUIModel {
Future<ToolsItemData> _addShaderCard(BuildContext context) async {
final gameShaderCachePath = await SCLoggerHelper.getShaderCachePath();
final shaderSize = ((await SystemHelper.getDirLen(gameShaderCachePath ?? "",
skipPath: ["$gameShaderCachePath\\Crashes"])) /
1024 /
1024)
.toStringAsFixed(4);
final shaderSize =
((await SystemHelper.getDirLen(gameShaderCachePath ?? "", skipPath: ["$gameShaderCachePath\\Crashes"])) /
1024 /
1024)
.toStringAsFixed(4);
return ToolsItemData(
"clean_shaders",
S.current.tools_action_clear_shader_cache,
@ -229,12 +234,8 @@ class ToolsUIModel extends _$ToolsUIModel {
return ToolsItemData(
"photography_mode",
isEnable
? S.current.tools_action_close_photography_mode
: S.current.tools_action_open_photography_mode,
isEnable
? S.current.tools_action_info_restore_lens_shake
: S.current.tools_action_info_one_key_close_lens_shake,
isEnable ? S.current.tools_action_close_photography_mode : S.current.tools_action_open_photography_mode,
isEnable ? S.current.tools_action_info_restore_lens_shake : S.current.tools_action_info_one_key_close_lens_shake,
const Icon(FontAwesomeIcons.camera, size: 24),
onTap: () => _onChangePhotographyMode(context, isEnable),
);
@ -245,8 +246,7 @@ class ToolsUIModel extends _$ToolsUIModel {
/// -----------------------------------------------------------------------------------------
/// -----------------------------------------------------------------------------------------
Future<void> reScanPath(BuildContext context,
{bool checkActive = false, bool skipToast = false}) async {
Future<void> reScanPath(BuildContext context, {bool checkActive = false, bool skipToast = false}) async {
var scInstallPaths = <String>[];
var rsiLauncherInstallPaths = <String>[];
var scInstalledPath = "";
@ -298,8 +298,7 @@ class ToolsUIModel extends _$ToolsUIModel {
/// EAC
Future<void> _reinstallEAC(BuildContext context) async {
if (state.scInstalledPath.isEmpty) {
showToast(
context, S.current.tools_action_info_valid_game_directory_needed);
showToast(context, S.current.tools_action_info_valid_game_directory_needed);
return;
}
state = state.copyWith(working: true);
@ -312,8 +311,7 @@ class ToolsUIModel extends _$ToolsUIModel {
final Map eacJson = json.decode(eacJsonData);
final eacID = eacJson["productid"];
if (eacID != null) {
final eacCacheDir =
Directory("${envVars["appdata"]}\\EasyAntiCheat\\$eacID");
final eacCacheDir = Directory("${envVars["appdata"]}\\EasyAntiCheat\\$eacID");
if (await eacCacheDir.exists()) {
await eacCacheDir.delete(recursive: true);
}
@ -323,8 +321,7 @@ class ToolsUIModel extends _$ToolsUIModel {
if (await dir.exists()) {
await dir.delete(recursive: true);
}
final eacLauncher =
File("${state.scInstalledPath}\\StarCitizen_Launcher.exe");
final eacLauncher = File("${state.scInstalledPath}\\StarCitizen_Launcher.exe");
if (await eacLauncher.exists()) {
await eacLauncher.delete(recursive: true);
}
@ -350,8 +347,7 @@ class ToolsUIModel extends _$ToolsUIModel {
/// RSI
Future _adminRSILauncher(BuildContext context) async {
if (state.rsiLauncherInstalledPath == "") {
showToast(context,
S.current.tools_action_info_rsi_launcher_directory_not_found);
showToast(context, S.current.tools_action_info_rsi_launcher_directory_not_found);
}
SystemHelper.checkAndLaunchRSILauncher(state.rsiLauncherInstalledPath);
}
@ -375,8 +371,7 @@ class ToolsUIModel extends _$ToolsUIModel {
actions: [
FilledButton(
child: Padding(
padding:
const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
padding: const EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8),
child: Text(S.current.action_close),
),
onPressed: () => Navigator.pop(context),
@ -390,8 +385,7 @@ class ToolsUIModel extends _$ToolsUIModel {
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();
final l = await Directory(gameShaderCachePath!).list(recursive: false).toList();
for (var value in l) {
if (value is Directory) {
if (!value.absolute.path.contains("Crashes")) {
@ -410,10 +404,8 @@ class ToolsUIModel extends _$ToolsUIModel {
if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) {
if (!context.mounted) return;
showToast(
context, S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .35));
showToast(context, S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35));
return;
}
@ -427,20 +419,15 @@ class ToolsUIModel extends _$ToolsUIModel {
try {
state = state.copyWith(working: true);
final aria2cManager = ref.read(aria2cModelProvider.notifier);
await aria2cManager
.launchDaemon(appGlobalState.applicationBinaryModuleDir!);
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)
]) {
for (var value in [...await aria2c.tellActive(), ...await aria2c.tellWaiting(0, 100000)]) {
final t = HomeDownloaderUIModel.getTaskTypeAndName(value);
if (t.key == "torrent" && t.value.contains("Data.p4k")) {
if (!context.mounted) return;
showToast(
context, S.current.tools_action_info_p4k_download_in_progress);
showToast(context, S.current.tools_action_info_p4k_download_in_progress);
state = state.copyWith(working: false);
return;
}
@ -449,15 +436,12 @@ class ToolsUIModel extends _$ToolsUIModel {
if (torrentUrl == "") {
state = state.copyWith(working: false);
if (!context.mounted) return;
showToast(
context, S.current.tools_action_info_function_under_maintenance);
showToast(context, S.current.tools_action_info_function_under_maintenance);
return;
}
final userSelect = await FilePicker.platform.saveFile(
initialDirectory: savePath,
fileName: fileName,
lockParentWindow: true);
final userSelect =
await FilePicker.platform.saveFile(initialDirectory: savePath, fileName: fileName, lockParentWindow: true);
if (userSelect == null) {
state = state.copyWith(working: false);
return;
@ -478,8 +462,7 @@ class ToolsUIModel extends _$ToolsUIModel {
}
final b64Str = base64Encode(btData.data!);
final gid =
await aria2c.addTorrent(b64Str, extraParams: {"dir": savePath});
final gid = await aria2c.addTorrent(b64Str, extraParams: {"dir": savePath});
state = state.copyWith(working: false);
dPrint("Aria2cManager.aria2c.addUri resp === $gid");
await aria2c.saveSession();
@ -497,24 +480,19 @@ class ToolsUIModel extends _$ToolsUIModel {
launchUrlString("https://support.citizenwiki.cn/d/8");
}
Future<bool> _checkPhotographyStatus(BuildContext context,
{bool? setMode}) async {
Future<bool> _checkPhotographyStatus(BuildContext context, {bool? setMode}) async {
final scInstalledPath = state.scInstalledPath;
final keys = ["AudioShakeStrength", "CameraSpringMovement", "ShakeScale"];
final attributesFile = File(
"$scInstalledPath\\USER\\Client\\0\\Profiles\\default\\attributes.xml");
final attributesFile = File("$scInstalledPath\\USER\\Client\\0\\Profiles\\default\\attributes.xml");
if (setMode == null) {
bool isEnable = false;
if (scInstalledPath.isNotEmpty) {
if (await attributesFile.exists()) {
final xmlFile =
XmlDocument.parse(await attributesFile.readAsString());
final xmlFile = XmlDocument.parse(await attributesFile.readAsString());
isEnable = true;
for (var k in keys) {
if (!isEnable) break;
final e = xmlFile.rootElement.children
.where((element) => element.getAttribute("name") == k)
.firstOrNull;
final e = xmlFile.rootElement.children.where((element) => element.getAttribute("name") == k).firstOrNull;
if (e != null && e.getAttribute("value") == "0") {
} else {
isEnable = false;
@ -531,8 +509,7 @@ class ToolsUIModel extends _$ToolsUIModel {
}
final xmlFile = XmlDocument.parse(await attributesFile.readAsString());
// clear all
xmlFile.rootElement.children.removeWhere(
(element) => keys.contains(element.getAttribute("name")));
xmlFile.rootElement.children.removeWhere((element) => keys.contains(element.getAttribute("name")));
if (setMode) {
for (var element in keys) {
XmlElement newNode = XmlElement(XmlName('Attr'), [
@ -550,8 +527,7 @@ class ToolsUIModel extends _$ToolsUIModel {
}
_onChangePhotographyMode(BuildContext context, bool isEnable) async {
_checkPhotographyStatus(context, setMode: !isEnable)
.unwrap(context: context);
_checkPhotographyStatus(context, setMode: !isEnable).unwrap(context: context);
loadToolsCard(context, skipPathScan: true);
}
@ -564,23 +540,18 @@ class ToolsUIModel extends _$ToolsUIModel {
}
_doHostsBooster(BuildContext context) async {
showDialog(
context: context,
builder: (BuildContext context) => const HostsBoosterDialogUI());
showDialog(context: context, builder: (BuildContext context) => const HostsBoosterDialogUI());
}
_unp4kc(BuildContext context) async {
context.push("/tools/unp4kc");
}
static rsiEnhance(BuildContext context,
{bool showNotGameInstallMsg = false}) async {
static rsiEnhance(BuildContext context, {bool showNotGameInstallMsg = false}) async {
if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) {
if (!context.mounted) return;
showToast(
context, S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .35));
showToast(context, S.current.tools_action_info_rsi_launcher_running_warning,
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .35));
return;
}
if (!context.mounted) return;
@ -590,4 +561,13 @@ class ToolsUIModel extends _$ToolsUIModel {
showNotGameInstallMsg: showNotGameInstallMsg,
));
}
_showLogAnalyze(BuildContext context) async {
if (state.scInstalledPath.isEmpty) {
showToast(context, S.current.tools_action_info_valid_game_directory_needed);
return;
}
if (!context.mounted) return;
await MultiWindowManager.launchSubWindow("log_analyze", appGlobalState);
}
}

View File

@ -6,7 +6,7 @@ part of 'tools_ui_model.dart';
// RiverpodGenerator
// **************************************************************************
String _$toolsUIModelHash() => r'ba4d824a61e83b496dbff4d7b5f2e29bd2ed4904';
String _$toolsUIModelHash() => r'af6e6de30c191a7c9a4e2a1c96a688fba6ee086c';
/// See also [ToolsUIModel].
@ProviderFor(ToolsUIModel)

View File

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <desktop_multi_window/desktop_multi_window_plugin.h>
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
@ -13,6 +14,9 @@
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_multi_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopMultiWindowPlugin");
desktop_multi_window_plugin_register_with_registrar(desktop_multi_window_registrar);
g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin");
desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar);

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_multi_window
desktop_webview_window
flutter_acrylic
screen_retriever_linux

View File

@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
import desktop_multi_window
import desktop_webview_window
import device_info_plus
import file_picker
@ -15,6 +16,7 @@ import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin"))
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))

View File

@ -66,6 +66,7 @@ dependencies:
re_highlight: ^0.0.3
shelf: ^1.4.1
qr_flutter: ^4.1.0
desktop_multi_window: ^0.2.1
dependency_overrides:
http: ^1.1.2

View File

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <desktop_multi_window/desktop_multi_window_plugin.h>
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
@ -13,6 +14,8 @@
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DesktopMultiWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopMultiWindowPlugin"));
DesktopWebviewWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin"));
FlutterAcrylicPluginRegisterWithRegistrar(

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_multi_window
desktop_webview_window
flutter_acrylic
screen_retriever_windows