feat: desktop_multi_window: ^0.3.0

This commit is contained in:
xkeyC
2025-11-15 22:06:56 +08:00
parent d82cfb41aa
commit 3f660c7d5e
16 changed files with 389 additions and 178 deletions

View File

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

View File

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

View File

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