diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be59910..032dd46 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,8 @@ | PathList | 游戏文件夹列表 | list(string) | | Path_$name | 版本路径 | string | | Game_$name | 版本列表 | list(string) | -| java | Java路径 | string | +| javaSelectedPath | 所选Java路径 | string | +| javaRuntimes | Java运行时列表(以JSON格式存储) | string | 离线账号配置 offline_account_$name list(string) | 序号 | 值 | diff --git a/lib/function/java/java_service.dart b/lib/function/java/java_service.dart new file mode 100644 index 0000000..0b60293 --- /dev/null +++ b/lib/function/java/java_service.dart @@ -0,0 +1,147 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:fml/function/java/java_utils.dart'; +import 'package:fml/function/java/models/java_info.dart'; +import 'package:fml/function/java/models/java_runtime.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class JavaService { + JavaService._(); + + static List _javaRuntimes = []; + static List get javaRuntimes => _javaRuntimes; + + static String _javaSelectedPath = ''; + static String get javaSelectedPath => _javaSelectedPath; + + static JavaInfo? _systemDefaultJavaInfo; + static JavaInfo? get systemDefaultJavaInfo => _systemDefaultJavaInfo; + + static late final Future initFuture; + + static Future init() async { + final futures = await Future.wait([ + JavaUtils.getSystemDefaultJavaInfo(), + SharedPreferences.getInstance(), + ]); + + _systemDefaultJavaInfo = futures[0] as JavaInfo?; + SharedPreferences prefs = futures[1] as SharedPreferences; + + final cachedJson = prefs.getString('javaRuntimes') ?? ''; + final systemJavaInfo = _systemDefaultJavaInfo; + final List validPaths = []; + + _javaRuntimes = []; + + late final List cachedRuntimes; + + if (cachedJson.isEmpty) { + // 初次打开/缓存为空,直接执行搜索并写入 + _javaRuntimes = await JavaUtils.searchPotentialJavaExecutables(); + updateJavaRuntimes(_javaRuntimes, prefs); + } else { + cachedRuntimes = await readJavaRuntimesFromPrefs(cachedJson); + + // 遍历缓存的列表 + for (final javaRuntime in cachedRuntimes) { + // 仅检测对应文件是否存在 + if (await File(javaRuntime.executable).exists()) { + _javaRuntimes.add(javaRuntime); + validPaths.add(javaRuntime.executable); + } + } + } + + // 缓存内java列表出现了变化,再次写入SharedPreferences + if (cachedJson.isNotEmpty && validPaths.length != cachedRuntimes.length) { + updateJavaRuntimes(_javaRuntimes, prefs); + } + + // 缓存内路径全部失效,搜索Java + if (_javaRuntimes.isEmpty) { + _javaRuntimes = await JavaUtils.searchPotentialJavaExecutables(); + + updateJavaRuntimes(_javaRuntimes, prefs); + } + + /// 处理_currentJavaPath + _javaSelectedPath = prefs.getString('javaSelectedPath') ?? ''; + + // 若不存在/为空,设置为系统java,若系统java也不存在,设置为扫描到的第一个JavaRuntime + if (_javaSelectedPath.isEmpty) { + if (systemJavaInfo != null) { + _javaSelectedPath = systemJavaInfo.path; + } else if (javaRuntimes.isNotEmpty) { + _javaSelectedPath = _javaRuntimes.first.executable; + } + + prefs.setString('javaSelectedPath', _javaSelectedPath); + } else { + // 缓存的java已无效,重复上方逻辑 + final info = await JavaUtils.probeJavaExecutable(_javaSelectedPath); + + if (info == null) { + if (systemJavaInfo != null) { + _javaSelectedPath = systemJavaInfo.path; + } else if (javaRuntimes.isNotEmpty) { + _javaSelectedPath = _javaRuntimes.first.executable; + } + + prefs.setString('javaSelectedPath', _javaSelectedPath); + } + } + } + + /// + /// 从JSON字符串反序列化出存储的JavaRuntimes列表 + /// + static Future> readJavaRuntimesFromPrefs( + String input, + ) async { + final List decoded = jsonDecode(input); + + return decoded + .map((e) => JavaRuntime.fromJson(e as Map)) + .toList(); + } + + /// + /// 更新[_javaRuntimes]并写入 + /// + static Future updateJavaRuntimes( + List newList, [ + SharedPreferences? prefsIn, + ]) async { + _javaRuntimes = newList; + + // 如果外部传了prefs就用外部的,否则内部获取实例 + final prefs = prefsIn ?? await SharedPreferences.getInstance(); + + final String jsonString = jsonEncode(newList); + + await prefs.setString('javaRuntimes', jsonString); + } + + /// + /// 更新[_javaSelectedPath]并写入 + /// + static Future updateJavaSelectedPath(String newPath) async { + // 检查路径 + if (newPath == _javaSelectedPath) return; + + final newFile = File(newPath); + + if (!newFile.existsSync()) { + throw ArgumentError('路径不存在: $newPath'); + } + + // 更新路径并写入 + _javaSelectedPath = newPath; + + final prefs = await SharedPreferences.getInstance(); + + await prefs.setString('javaSelectedPath', _javaSelectedPath); + } +} diff --git a/lib/function/java/java_manager.dart b/lib/function/java/java_utils.dart similarity index 58% rename from lib/function/java/java_manager.dart rename to lib/function/java/java_utils.dart index 7ad0ce5..ccc2126 100644 --- a/lib/function/java/java_manager.dart +++ b/lib/function/java/java_utils.dart @@ -3,15 +3,15 @@ import 'dart:io'; import 'package:fml/function/log.dart'; import 'package:fml/function/java/models/java_info.dart'; import 'package:fml/function/java/models/java_runtime.dart'; -import 'package:path/path.dart' as path; +import 'package:win32_registry/win32_registry.dart'; -class JavaManager { - JavaManager._(); +class JavaUtils { + JavaUtils._(); /// /// Java 可执行文件名称 /// - static String kJavaExecutableName = Platform.isWindows ? 'java.exe' : 'java'; + static String _javaExecutableName = Platform.isWindows ? 'java.exe' : 'java'; static final RegExp _vendorVersionRegExp = RegExp( r'(?:(OpenJDK|java|IBM|AdoptOpenJDK|Microsoft).*?)?version\s+"([^"]+)"', @@ -24,12 +24,11 @@ class JavaManager { /// 寻找 Java 可执行文件 /// static Future> searchPotentialJavaExecutables({ - int searchDepth = 4, + int searchDepth = 0, }) async { final Set found = {}; - final List result = []; - // PATH + // 搜索PATH final pathSeparator = Platform.isWindows ? ';' : ':'; final pathEntries = Platform.environment['PATH']?.split(pathSeparator) ?? []; @@ -37,14 +36,14 @@ class JavaManager { for (final entry in pathEntries) { if (entry.trim().isEmpty) continue; - final javaPath = _join(entry, kJavaExecutableName); + final javaPath = _join(entry, _javaExecutableName); if (await File(javaPath).exists()) { found.add(await File(javaPath).resolveSymbolicLinks()); } } - // 常用系统目录 + // 搜索常用系统目录 final List candidates = []; if (Platform.isWindows) { @@ -75,11 +74,78 @@ class JavaManager { } } - // 用户jdks - final home = Platform.environment['HOME']; + // 搜索用户jdks + final home = + Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; if (home != null) candidates.add(Directory('$home/.jdks')); + // 在Windows下通过注册表搜索 + if (Platform.isWindows) { + final rootKeys = [RegistryHive.currentUser, RegistryHive.localMachine]; + + // 大部分的注册表路径 + const paths = [ + r'SOFTWARE\JavaSoft\Java Runtime Environment', + r'SOFTWARE\JavaSoft\Java Development Kit', + r'SOFTWARE\JavaSoft\JDK', + r'SOFTWARE\Wow6432Node\JavaSoft\Java Runtime Environment', + r'SOFTWARE\Wow6432Node\JavaSoft\Java Development Kit', + r'SOFTWARE\WOW6432Node\JavaSoft\JDK', + r'SOFTWARE\AdoptOpenJDK\JDK', + r'SOFTWARE\Microsoft\JDK', + r'SOFTWARE\Azul Systems\Zulu', + r'SOFTWARE\Amazon\Corretto', + r'SOFTWARE\RedHat\OpenJDK\JDK', + r'SOFTWARE\BellSoft\Liberica', + ]; + + for (final root in rootKeys) { + for (final path in paths) { + RegistryKey? key; + + try { + key = Registry.openPath(root, path: path); + + // 枚举所有版本子键 + for (final versionKeyName in key.subkeyNames) { + if (versionKeyName == 'null') continue; + + final versionKey = Registry.openPath( + root, + path: '$path\\$versionKeyName', + desiredAccessRights: AccessRights.readOnly, + ); + + // 读取路径 + final javaHome = versionKey.getStringValue('JavaHome'); + final installationPath = versionKey.getStringValue( + 'InstallationPath', + ); + + try { + String? javaPath = javaHome ?? installationPath; + + if (javaPath != null && javaPath.isNotEmpty) { + candidates.add(Directory(javaPath)); + } + } finally { + // 确保键被关闭 + versionKey.close(); + } + } + + key.close(); + } catch (e) { + // 键不存在或无权限,跳过 + } finally { + // 确保键被关闭 + key?.close(); + } + } + } + } + for (final dir in candidates) { if (!await dir.exists()) continue; @@ -89,9 +155,13 @@ class JavaManager { // 快速检查预期布局 final javaHome = entry.path; final probes = _possibleExecutablePaths(javaHome); + for (final p in probes) { - final f = File(p); - if (await f.exists()) found.add(await f.resolveSymbolicLinks()); + final file = File(p); + + if (await file.exists()) { + found.add(await file.resolveSymbolicLinks()); + } } } } @@ -100,13 +170,16 @@ class JavaManager { } } + final List result = []; + // 同时检查每个候选目录下常见的顶级 JDK 名称 + // 并将其转换为JavaRuntime for (final exe in found) { try { - final info = await _probeJavaExecutable(exe); + final info = await probeJavaExecutable(exe); if (info != null) { - final isJdk = await _looksLikeJdk(exe); + final isJdk = await looksLikeJdk(exe); result.add(JavaRuntime(info: info, executable: exe, isJdk: isJdk)); } } catch (e) { @@ -114,38 +187,6 @@ class JavaManager { } } - final roots = []; - - if (Platform.isWindows) { - // Windows枚举 - for (int i = 0; i < 26; i++) { - // CharCode 68(排除A,B,C) - final drive = '${String.fromCharCode(68 + i)}:\\'; - final dir = Directory(drive); - - try { - if (dir.existsSync()) { - roots.add(dir); - } - } catch (_) { - // 忽略无法访问的驱动器 - } - } - - //final stopwatch = Stopwatch()..start(); - for (final rootDir in roots) { - final javaRuntimes = await _searchJavaInDirRecursive( - dir: rootDir, - searchDepth: searchDepth, - ); - - result.addAll(javaRuntimes); - } - //stopwatch.stop(); - - //print('timeee: ${stopwatch.elapsedMilliseconds} ms, $result'); - } - // 去重返回(按 executable 路径去重) final Map uniqueByExecutable = {}; @@ -157,66 +198,6 @@ class JavaManager { return uniqueByExecutable.values.toList(); } - /// - /// 递归搜索Java运行时 - /// - /// [dir] 要搜索的根目录 - /// [searchDepth] 最大允许的递归深度 - /// [currentDepth] 当前递归深度(内部调用使用) - /// - static Future> _searchJavaInDirRecursive({ - required Directory dir, - required int searchDepth, - int currentDepth = 0, - }) async { - List result = []; - - if (currentDepth > searchDepth) return result; - - try { - // 检查当前目录下的文件 - final dirs = dir.list(followLinks: false); - - await for (final entity in dirs) { - final name = path.basename(entity.path); - - // 排除 . 开头目录 - if (name.startsWith('.')) continue; - - if (entity is File) { - final lowerFileName = name.toLowerCase(); - - if (lowerFileName == kJavaExecutableName) { - // 创建JavaRuntime逻辑 - final exePath = entity.path; - final info = await _probeJavaExecutable(exePath); - - if (info != null) { - final isJdk = await _looksLikeJdk(exePath); - - result.add( - JavaRuntime(info: info, executable: exePath, isJdk: isJdk), - ); - } - } - } else if (entity is Directory) { - // 递归搜索子目录(深度+1) - result.addAll( - await _searchJavaInDirRecursive( - dir: entity, - currentDepth: currentDepth + 1, - searchDepth: searchDepth, - ), - ); - } - } - } catch (e) { - // 忽略权限错误或无法访问的目录 - } - - return result; - } - /// /// 可能的可执行文件路径 /// @@ -224,20 +205,23 @@ class JavaManager { final List probes = []; if (Platform.isMacOS) { - probes.add('$javaHome/jre.bundle/Contents/Home/bin/$kJavaExecutableName'); + probes.add('$javaHome/jre.bundle/Contents/Home/bin/$_javaExecutableName'); + probes.add('$javaHome/Contents/Home/bin/$_javaExecutableName'); + } - probes.add('$javaHome/Contents/Home/bin/$kJavaExecutableName'); + if (Platform.isWindows) { + probes.add('$javaHome/$_javaExecutableName'); + probes.add('$javaHome/bin/$_javaExecutableName'); + probes.add('$javaHome/jre/bin/$_javaExecutableName'); } - probes.add('$javaHome/bin/$kJavaExecutableName'); - probes.add('$javaHome/jre/bin/$kJavaExecutableName'); return probes; } /// /// 检查可执行文件是否看为 JDK(存在 javac) /// - static Future _looksLikeJdk(String exe) async { + static Future looksLikeJdk(String exe) async { try { final bin = File(exe).parent; final javac = File( @@ -253,7 +237,7 @@ class JavaManager { /// /// Java 可执行文件信息 /// - static Future _probeJavaExecutable(String exe) async { + static Future probeJavaExecutable(String exe) async { // 首先尝试“java -version” try { final proc = await Process.start(exe, ['-version']); @@ -353,6 +337,72 @@ class JavaManager { return null; } + /// + /// 获取系统默认 Java 信息 + /// + static Future getSystemDefaultJavaInfo() async { + try { + final javaVersionProcess = await Process.run('java', ['-version']); + + if (javaVersionProcess.exitCode != 0) { + LogUtil.log( + '获取系统默认 Java 信息失败,退出码:${javaVersionProcess.exitCode}', + level: 'WARN', + ); + } + + final versionOutput = (javaVersionProcess.stderr as String).isNotEmpty + ? javaVersionProcess.stderr as String + : javaVersionProcess.stdout as String; + + final parsedVersion = JavaUtils.parseVersionOutput(versionOutput); + + if (parsedVersion == null) { + LogUtil.log('无法解析系统默认 Java 版本信息', level: 'WARN'); + return null; + } + + String executablePath = ''; + + try { + if (Platform.isWindows) { + final where = await Process.run('where', ['java']); + + if (where.exitCode == 0) { + executablePath = (where.stdout as String) + .toString() + .split('\n') + .first + .trim(); + } + } else { + final which = await Process.run('which', ['java']); + + if (which.exitCode == 0) { + executablePath = (which.stdout as String) + .toString() + .split('\n') + .first + .trim(); + } + } + } catch (e) { + LogUtil.log('获取系统默认 Java 路径时出错:$e', level: 'WARN'); + } + + return JavaInfo( + version: parsedVersion['version'] ?? 'unknown', + vendor: parsedVersion['vendor'], + path: executablePath, + os: Platform.operatingSystem, + arch: Platform.version, + ); + } catch (e) { + LogUtil.log('执行 "java -version" 时出错:$e', level: 'WARN'); + return null; + } + } + /// /// 路径拼接 /// diff --git a/lib/function/java/models/java_info.dart b/lib/function/java/models/java_info.dart index aef1e72..2f32216 100644 --- a/lib/function/java/models/java_info.dart +++ b/lib/function/java/models/java_info.dart @@ -13,6 +13,22 @@ class JavaInfo { required this.arch, }); + Map toJson() => { + 'version': version, + 'vendor': vendor, + 'path': path, + 'os': os, + 'arch': arch, + }; + + factory JavaInfo.fromJson(Map json) => JavaInfo( + version: json['version'] as String, + vendor: json['vendor'] as String?, + path: json['path'] as String, + os: json['os'] as String, + arch: json['arch'] as String, + ); + @override String toString() => '$version (${vendor ?? 'Unknown'}) @ $path'; } diff --git a/lib/function/java/models/java_runtime.dart b/lib/function/java/models/java_runtime.dart index 89306f3..ad7f3ef 100644 --- a/lib/function/java/models/java_runtime.dart +++ b/lib/function/java/models/java_runtime.dart @@ -11,6 +11,18 @@ class JavaRuntime { required this.isJdk, }); + Map toJson() => { + 'info': info.toJson(), + 'executable': executable, + 'isJdk': isJdk, + }; + + factory JavaRuntime.fromJson(Map json) => JavaRuntime( + info: JavaInfo.fromJson(json['info'] as Map), + executable: json['executable'] as String, + isJdk: json['isJdk'] as bool, + ); + @override String toString() => '${isJdk ? 'JDK' : 'JRE'} ${info.version} @ $executable'; } diff --git a/lib/function/launcher/fabric.dart b/lib/function/launcher/fabric.dart index b4cac36..3a54d7b 100644 --- a/lib/function/launcher/fabric.dart +++ b/lib/function/launcher/fabric.dart @@ -1,17 +1,23 @@ import 'dart:async'; import 'dart:io'; +import 'package:fml/function/java/java_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; import 'package:path/path.dart' as p; import 'package:fml/function/log.dart'; -import 'package:fml/function/launcher/login/microsoft_login.dart' as microsoft_login; -import 'package:fml/function/launcher/login/external_login.dart' as external_login; +import 'package:fml/function/launcher/login/microsoft_login.dart' + as microsoft_login; +import 'package:fml/function/launcher/login/external_login.dart' + as external_login; typedef ProgressCallback = void Function(String message); typedef ErrorCallback = void Function(String error); // library获取 -Future> loadLibraryArtifactPaths(String versionJsonPath, String gamePath) async { +Future> loadLibraryArtifactPaths( + String versionJsonPath, + String gamePath, +) async { final file = File(versionJsonPath); if (!await file.exists()) return []; late final dynamic root; @@ -77,32 +83,68 @@ Future getAssetIndex(String versionJsonPath) async { // fabric相关 Future> getFabricInfo(String path) async { final file = File(path); - if (!await file.exists()) return {'game': null, 'fabric': null, 'mixin': null, 'libraries': []}; + if (!await file.exists()) { + return { + 'game': null, + 'fabric': null, + 'mixin': null, + 'libraries': [], + }; + } dynamic root; try { root = jsonDecode(await file.readAsString()); } catch (_) { - return {'game': null, 'fabric': null, 'mixin': null, 'libraries': []}; + return { + 'game': null, + 'fabric': null, + 'mixin': null, + 'libraries': [], + }; + } + if (root is! Map) { + return { + 'game': null, + 'fabric': null, + 'mixin': null, + 'libraries': [], + }; } - if (root is! Map) return {'game': null, 'fabric': null, 'mixin': null, 'libraries': []}; final patches = root['patches']; - if (patches is! List) return {'game': null, 'fabric': null, 'mixin': null, 'libraries': []}; + if (patches is! List) { + return { + 'game': null, + 'fabric': null, + 'mixin': null, + 'libraries': [], + }; + } String? gameVer; String? fabricVer; String? mixin; final List fabricLibraries = []; String? readVersion(Map m) { String? s; - if (m['version'] is String && (m['version'] as String).isNotEmpty) s = m['version'] as String; - s ??= (m['versionId'] is String && (m['versionId'] as String).isNotEmpty) ? m['versionId'] as String : null; - s ??= (m['ver'] is String && (m['ver'] as String).isNotEmpty) ? m['ver'] as String : null; + if (m['version'] is String && (m['version'] as String).isNotEmpty) { + s = m['version'] as String; + } + s ??= (m['versionId'] is String && (m['versionId'] as String).isNotEmpty) + ? m['versionId'] as String + : null; + s ??= (m['ver'] is String && (m['ver'] as String).isNotEmpty) + ? m['ver'] as String + : null; final meta = m['metadata']; - if (s == null && meta is Map && meta['version'] is String && (meta['version'] as String).isNotEmpty) { + if (s == null && + meta is Map && + meta['version'] is String && + (meta['version'] as String).isNotEmpty) { s = meta['version'] as String; } return s; } + for (final patch in patches) { if (patch is! Map) continue; final id = patch['id']; @@ -123,7 +165,12 @@ Future> getFabricInfo(String path) async { final groupIdParts = parts[0].split('.'); final artifactId = parts[1]; final version = parts[2]; - final jarPath = p.joinAll([...groupIdParts, artifactId, version, '$artifactId-$version.jar']); + final jarPath = p.joinAll([ + ...groupIdParts, + artifactId, + version, + '$artifactId-$version.jar', + ]); fabricLibraries.add(jarPath); } if (name.contains('net.fabricmc:sponge-mixin')) { @@ -138,23 +185,31 @@ Future> getFabricInfo(String path) async { 'game': gameVer, 'fabric': fabricVer, 'mixin': mixin, - 'libraries': fabricLibraries + 'libraries': fabricLibraries, }; } // 登录模式 String _getLoginMode(String loginMode) { switch (loginMode) { - case '0': return 'offline'; - case '1': return 'online'; - case '2': return 'external'; - default: return 'unknown'; + case '0': + return 'offline'; + case '1': + return 'online'; + case '2': + return 'external'; + default: + return 'unknown'; } } // 从fabric.json文件中获取Fabric信息 -Future> getFabricInfoFromFabricJson(String gamePath, String game) async { - final fabricJsonPath = '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game${Platform.pathSeparator}fabric.json'; +Future> getFabricInfoFromFabricJson( + String gamePath, + String game, +) async { + final fabricJsonPath = + '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game${Platform.pathSeparator}fabric.json'; final file = File(fabricJsonPath); if (!await file.exists()) { LogUtil.log('fabric.json 不存在: $fabricJsonPath', level: 'ERROR'); @@ -171,12 +226,12 @@ Future> getFabricInfoFromFabricJson(String gamePath, String final json = jsonDecode(content); if (json is! Map) { return { - 'loader': null, - 'intermediary': null, - 'mixin': null, - 'libraries': [], - 'asm': {} - }; + 'loader': null, + 'intermediary': null, + 'mixin': null, + 'libraries': [], + 'asm': {}, + }; } // 获取loader版本 final String? loaderVersion = json['loader']?['version']; @@ -198,7 +253,8 @@ Future> getFabricInfoFromFabricJson(String gamePath, String final groupId = parts[0].replaceAll('.', Platform.pathSeparator); final artifactId = parts[1]; final version = parts[2]; - final jarPath = '$groupId${Platform.pathSeparator}$artifactId${Platform.pathSeparator}$version${Platform.pathSeparator}$artifactId-$version.jar'; + final jarPath = + '$groupId${Platform.pathSeparator}$artifactId${Platform.pathSeparator}$version${Platform.pathSeparator}$artifactId-$version.jar'; libraries.add(jarPath); if (name.startsWith('org.ow2.asm:')) { asmVersions[artifactId] = version; @@ -241,50 +297,65 @@ Future fabricLauncher({ final prefs = await SharedPreferences.getInstance(); // 游戏参数 onProgress?.call('正在获取游戏参数'); - final java = prefs.getString('java') ?? 'java'; + final java = JavaService.javaSelectedPath; final selectedPath = prefs.getString('SelectedPath') ?? ''; final gamePath = prefs.getString('Path_$selectedPath') ?? ''; final game = prefs.getString('SelectedGame') ?? ''; - final nativesPath = '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game${Platform.pathSeparator}natives'; + final nativesPath = + '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game${Platform.pathSeparator}natives'; final version = prefs.getString('version') ?? ''; final cfg = prefs.getStringList('Config_${selectedPath}_$game') ?? []; - final jsonPath = '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game${Platform.pathSeparator}$game.json'; + final jsonPath = + '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game${Platform.pathSeparator}$game.json'; final libraries = await loadLibraryArtifactPaths(jsonPath, gamePath); final separator = Platform.isWindows ? ';' : ':'; - final gameJar = '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game${Platform.pathSeparator}$game.jar'; + final gameJar = + '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game${Platform.pathSeparator}$game.jar'; final assetIndex = await getAssetIndex(jsonPath) ?? ''; // 从fabric.json获取Fabric信息 onProgress?.call('正在获取Fabric参数'); final fabricInfo = await getFabricInfoFromFabricJson(gamePath, game); // 基础路径 onProgress?.call('正在构建路径'); - final fabricLoader = '$gamePath${Platform.pathSeparator}libraries${Platform.pathSeparator}net${Platform.pathSeparator}fabricmc${Platform.pathSeparator}fabric-loader${Platform.pathSeparator}${fabricInfo['loader'] ?? ''}${Platform.pathSeparator}fabric-loader-${fabricInfo['loader'] ?? ''}.jar'; - final intermediary = '$gamePath${Platform.pathSeparator}libraries${Platform.pathSeparator}net${Platform.pathSeparator}fabricmc${Platform.pathSeparator}intermediary${Platform.pathSeparator}${fabricInfo['intermediary'] ?? ''}${Platform.pathSeparator}intermediary-${fabricInfo['intermediary'] ?? ''}.jar'; + final fabricLoader = + '$gamePath${Platform.pathSeparator}libraries${Platform.pathSeparator}net${Platform.pathSeparator}fabricmc${Platform.pathSeparator}fabric-loader${Platform.pathSeparator}${fabricInfo['loader'] ?? ''}${Platform.pathSeparator}fabric-loader-${fabricInfo['loader'] ?? ''}.jar'; + final intermediary = + '$gamePath${Platform.pathSeparator}libraries${Platform.pathSeparator}net${Platform.pathSeparator}fabricmc${Platform.pathSeparator}intermediary${Platform.pathSeparator}${fabricInfo['intermediary'] ?? ''}${Platform.pathSeparator}intermediary-${fabricInfo['intermediary'] ?? ''}.jar'; // 构建Fabric依赖库路径 onProgress?.call('正在构建Fabric依赖库路径'); final List fabricLibraryPaths = []; for (final lib in (fabricInfo['libraries'] as List? ?? [])) { - fabricLibraryPaths.add('$gamePath${Platform.pathSeparator}libraries${Platform.pathSeparator}$lib'); + fabricLibraryPaths.add( + '$gamePath${Platform.pathSeparator}libraries${Platform.pathSeparator}$lib', + ); } // 过滤原版库中与Fabric提供的ASM组件冲突的版本 onProgress?.call('正在准备ASM组件'); - final Map fabricAsmVersions = fabricInfo['asm'] as Map? ?? {}; + final Map fabricAsmVersions = + fabricInfo['asm'] as Map? ?? {}; final List filteredLibraries = []; for (final lib in libraries) { bool shouldExclude = false; // 检查是否是ASM组件 - if (lib.contains('${Platform.pathSeparator}org${Platform.pathSeparator}ow2${Platform.pathSeparator}asm${Platform.pathSeparator}')) { + if (lib.contains( + '${Platform.pathSeparator}org${Platform.pathSeparator}ow2${Platform.pathSeparator}asm${Platform.pathSeparator}', + )) { for (final asmComponent in fabricAsmVersions.keys) { // 如果路径中包含ASM组件名称 - if (lib.contains('${Platform.pathSeparator}$asmComponent${Platform.pathSeparator}')) { + if (lib.contains( + '${Platform.pathSeparator}$asmComponent${Platform.pathSeparator}', + )) { // 检查版本是否不同于Fabric提供的版本 final libParts = p.split(lib); for (int i = 0; i < libParts.length - 1; i++) { if (libParts[i] == asmComponent && i > 0) { - final version = libParts[i-1]; + final version = libParts[i - 1]; if (version != fabricAsmVersions[asmComponent]) { shouldExclude = true; - LogUtil.log('排除冲突的ASM组件: $lib (版本 $version 与Fabric提供的版本 ${fabricAsmVersions[asmComponent]} 冲突)', level: 'INFO'); + LogUtil.log( + '排除冲突的ASM组件: $lib (版本 $version 与Fabric提供的版本 ${fabricAsmVersions[asmComponent]} 冲突)', + level: 'INFO', + ); break; } } @@ -306,16 +377,29 @@ Future fabricLauncher({ } // 检查关键文件是否存在 onProgress?.call('正在检查文件完整性'); - LogUtil.log('Fabric Loader 文件存在: ${File(fabricLoader).existsSync()}', level: 'INFO'); - LogUtil.log('Intermediary 文件存在: ${File(intermediary).existsSync()}', level: 'INFO'); + LogUtil.log( + 'Fabric Loader 文件存在: ${File(fabricLoader).existsSync()}', + level: 'INFO', + ); + LogUtil.log( + 'Intermediary 文件存在: ${File(intermediary).existsSync()}', + level: 'INFO', + ); for (final lib in fabricLibraryPaths) { if (lib.contains('sponge-mixin')) { - LogUtil.log('Sponge Mixin 文件存在: ${File(lib).existsSync()}', level: 'INFO'); + LogUtil.log( + 'Sponge Mixin 文件存在: ${File(lib).existsSync()}', + level: 'INFO', + ); } } final accountName = prefs.getString('SelectedAccountName') ?? ''; final accountType = prefs.getString('SelectedAccountType') ?? ''; - final accountInfo = prefs.getStringList('${_getLoginMode(accountType)}_account_$accountName') ?? []; + final accountInfo = + prefs.getStringList( + '${_getLoginMode(accountType)}_account_$accountName', + ) ?? + []; // 账号信息 String uuid = ''; String token = ''; @@ -334,14 +418,17 @@ Future fabricLauncher({ if (accountInfo[0] == '2') { if (await external_login.checkAuthlibInjector(gamePath)) { onProgress?.call('AuthlibInjector已存在'); - } - else { + } else { onProgress?.call('正在下载AuthlibInjector'); await external_login.downloadAuthlibInjector(gamePath); } uuid = accountInfo[1]; onProgress?.call('正在检查令牌'); - if (await external_login.checkToken(accountInfo[2], accountInfo[5], accountInfo[6])) { + if (await external_login.checkToken( + accountInfo[2], + accountInfo[5], + accountInfo[6], + )) { token = accountInfo[5]; } else { token = await external_login.refreshToken( @@ -349,7 +436,7 @@ Future fabricLauncher({ accountInfo[5], accountInfo[6], accountName, - uuid + uuid, ); } } @@ -370,32 +457,50 @@ Future fabricLauncher({ '-Djna.tmpdir=$nativesPath', '-Dfabric.gameDir=$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game', '-Dfabric.modsDir=$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game${Platform.pathSeparator}mods', - if (accountInfo[0] == '2') '-javaagent:$gamePath${Platform.pathSeparator}authlib-injector.jar=${accountInfo[2]}', - '-cp', cp, + if (accountInfo[0] == '2') + '-javaagent:$gamePath${Platform.pathSeparator}authlib-injector.jar=${accountInfo[2]}', + '-cp', + cp, 'net.fabricmc.loader.impl.launch.knot.KnotClient', - '--username', accountName, - '--version', game, - '--gameDir', '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game', - '--assetsDir', '$gamePath${Platform.pathSeparator}assets', - '--assetIndex', assetIndex, - '--uuid', uuid, - if (accountInfo[0] == '0') '--accessToken', accountInfo[0], - if (accountInfo[0] == '0') '--clientId', '"\${clientid}"', - if (accountInfo[0] == '1' || accountInfo[0] == '2') '--accessToken', token, - if (accountInfo[0] == '1' || accountInfo[0] == '2') '--userType', 'mojang', - if (accountInfo[0] == '2') '--clientId', token, - '--versionType', '"FML $version"', - '--xuid', '"\${auth_xuid}"', - '--width', cfg[2], - '--height', cfg[3], - if (cfg[1] == '1') '--fullscreen' + '--username', + accountName, + '--version', + game, + '--gameDir', + '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game', + '--assetsDir', + '$gamePath${Platform.pathSeparator}assets', + '--assetIndex', + assetIndex, + '--uuid', + uuid, + if (accountInfo[0] == '0') '--accessToken', + accountInfo[0], + if (accountInfo[0] == '0') '--clientId', + '"\${clientid}"', + if (accountInfo[0] == '1' || accountInfo[0] == '2') '--accessToken', + token, + if (accountInfo[0] == '1' || accountInfo[0] == '2') '--userType', + 'mojang', + if (accountInfo[0] == '2') '--clientId', + token, + '--versionType', + '"FML $version"', + '--xuid', + '"\${auth_xuid}"', + '--width', + cfg[2], + '--height', + cfg[3], + if (cfg[1] == '1') '--fullscreen', ]; LogUtil.log('使用的Java: $java', level: 'INFO'); onProgress?.call('正在启动游戏'); final out = await Process.start( - java, - args, - workingDirectory: '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game', + java, + args, + workingDirectory: + '$gamePath${Platform.pathSeparator}versions${Platform.pathSeparator}$game', ); out.stdout.listen((_) {}); out.stderr.listen((_) {}); diff --git a/lib/function/launcher/neoforge.dart b/lib/function/launcher/neoforge.dart index 748e236..b016ff8 100644 --- a/lib/function/launcher/neoforge.dart +++ b/lib/function/launcher/neoforge.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:fml/function/java/java_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; import 'package:path/path.dart' as p; @@ -182,7 +183,7 @@ Future neoforgeLauncher({ onProgress?.call('正在准备启动'); final prefs = await SharedPreferences.getInstance(); // 游戏参数 - final java = prefs.getString('java') ?? 'java'; + final java = JavaService.javaSelectedPath; final selectedPath = prefs.getString('SelectedPath') ?? ''; final gamePath = prefs.getString('Path_$selectedPath') ?? ''; final game = prefs.getString('SelectedGame') ?? ''; diff --git a/lib/function/launcher/vanilla.dart b/lib/function/launcher/vanilla.dart index 4b89c26..b0d0e9f 100644 --- a/lib/function/launcher/vanilla.dart +++ b/lib/function/launcher/vanilla.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:fml/constants.dart'; +import 'package:fml/function/java/java_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; import 'dart:async'; @@ -96,7 +97,7 @@ Future vanillaLauncher({ onProgress?.call('正在准备启动'); final prefs = await SharedPreferences.getInstance(); // 游戏参数 - final java = prefs.getString('java') ?? 'java'; + final java = JavaService.javaSelectedPath; final selectedPath = prefs.getString('SelectedPath') ?? ''; final gamePath = prefs.getString('Path_$selectedPath') ?? ''; final game = prefs.getString('SelectedGame') ?? ''; diff --git a/lib/main.dart b/lib/main.dart index 9d1fe25..b094ab4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:fml/function/java/java_service.dart'; import 'package:fml/function/slide_page_route.dart'; import 'package:fml/function/dio_client.dart'; import 'package:lazy_load_indexed_stack/lazy_load_indexed_stack.dart'; @@ -39,6 +40,8 @@ void main() async { await initLogs(); + JavaService.initFuture = JavaService.init(); + runApp(const FMLBaseApp()); } @@ -237,7 +240,6 @@ class MainStartPage extends StatefulWidget { class MainStartPageState extends State { int _selectedIndex = 0; - bool? _javaInstalled; // 使页面仅被初始化一次 final List _mainPages = const [ @@ -250,46 +252,165 @@ class MainStartPageState extends State { @override void initState() { super.initState(); - _checkJavaInstalled(); + _checkUpdate(); } - // 检查是否安装Java - Future _checkJavaInstalled() async { - try { - final result = await Process.run('java', ['-version']); - setState(() { - _javaInstalled = result.exitCode == 0; - }); - if (_javaInstalled == false) { - _showJavaNotFoundDialog(); - } - } catch (e) { - setState(() { - _javaInstalled = false; - }); - _showJavaNotFoundDialog(); - } - } + @override + Widget build(BuildContext context) { + // 主界面内容 + Widget mainContent = FutureBuilder( + future: JavaService.initFuture, + + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + JavaService.javaRuntimes.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('未检测到 Java'), + content: const Text( + '未检测到 Java 环境或者 Java 环境未正确配置,请先安装 Java 后再打开启动器', + ), + actions: [ + TextButton( + onPressed: () => _launchURL(AppUrls.javaDownload), + child: const Text('打开Java下载页面'), + ), + ], + ), + ); + }); + } - // 显示Java未找到的对话框 - Future _showJavaNotFoundDialog() async { - WidgetsBinding.instance.addPostFrameCallback((_) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - title: const Text('未检测到 Java'), - content: const Text('未检测到 Java 环境或者 Java 环境未正确配置,请先安装 Java 后再打开启动器'), - actions: [ - TextButton( - onPressed: () => _launchURL(AppUrls.javaDownload), - child: const Text('打开Java下载页面'), - ), - ], - ), + return Scaffold( + appBar: AppBar(title: const Text(kAppName)), + body: Row( + children: [ + NavigationRail( + selectedIndex: _selectedIndex, + onDestinationSelected: (int index) { + setState(() => _selectedIndex = index); + }, + labelType: NavigationRailLabelType.all, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.play_arrow), + label: Text('启动'), + ), + NavigationRailDestination( + icon: Icon(Icons.hub), + label: Text('联机'), + ), + NavigationRailDestination( + icon: Icon(Icons.download), + label: Text('下载'), + ), + NavigationRailDestination( + icon: Icon(Icons.settings), + label: Text('设置'), + ), + ], + ), + // 显示当前页面 + Expanded( + child: LazyLoadIndexedStack( + index: _selectedIndex, + children: _mainPages, + ), + ), + ], + ), + ); + }, + ); + + // macOS 菜单栏 + if (Platform.isMacOS) { + return PlatformMenuBar( + menus: [ + // FML 菜单 + PlatformMenu( + label: kAppName, + menus: [ + PlatformMenuItem( + label: '关于', + onSelected: () => _showAboutDialog(context), + ), + PlatformMenuItem(label: '检查更新', onSelected: () => _checkUpdate()), + PlatformMenuItem( + label: '设置', + shortcut: const SingleActivator( + LogicalKeyboardKey.comma, + meta: true, + ), + onSelected: () => setState(() => _selectedIndex = 3), + ), + PlatformMenuItem( + label: '退出', + shortcut: const SingleActivator( + LogicalKeyboardKey.keyQ, + meta: true, + ), + onSelected: () => SystemNavigator.pop(), + ), + ], + ), + // 导航菜单 + PlatformMenu( + label: '导航', + menus: [ + PlatformMenuItem( + label: '启动', + shortcut: const SingleActivator( + LogicalKeyboardKey.digit1, + meta: true, + ), + onSelected: () => setState(() => _selectedIndex = 0), + ), + PlatformMenuItem( + label: '联机', + shortcut: const SingleActivator( + LogicalKeyboardKey.digit2, + meta: true, + ), + onSelected: () => setState(() => _selectedIndex = 1), + ), + PlatformMenuItem( + label: '下载', + shortcut: const SingleActivator( + LogicalKeyboardKey.digit3, + meta: true, + ), + onSelected: () => setState(() => _selectedIndex = 2), + ), + PlatformMenuItem( + label: '设置', + shortcut: const SingleActivator( + LogicalKeyboardKey.digit4, + meta: true, + ), + onSelected: () => setState(() => _selectedIndex = 3), + ), + ], + ), + // 帮助菜单 + PlatformMenu( + label: '帮助', + menus: [ + PlatformMenuItem( + label: '访问 GitHub', + onSelected: () => _launchURL(AppUrls.githubProject), + ), + ], + ), + ], + child: mainContent, ); - }); + } + return mainContent; } // 打开URL @@ -319,9 +440,12 @@ class MainStartPageState extends State { final response = await DioClient().dio.get( AppUrls.latestVersionApi, options: Options( - headers: {'User-Agent': '$kAppNameAbb/${Platform.operatingSystem}/$gAppVersion+$gAppBuildNumber ${kDebugMode ? 'debug' : ''}'}, + headers: { + 'User-Agent': + '$kAppNameAbb/${Platform.operatingSystem}/$gAppVersion+$gAppBuildNumber ${kDebugMode ? 'debug' : ''}', + }, ), - ); + ); if (response.statusCode == 200) { String rawVersionData = response.data.toString(); String cleanedVersionString = rawVersionData @@ -392,134 +516,6 @@ class MainStartPageState extends State { ); } - @override - Widget build(BuildContext context) { - // 主界面内容 - Widget mainContent = Scaffold( - appBar: AppBar(title: const Text(kAppName)), - body: Row( - children: [ - NavigationRail( - selectedIndex: _selectedIndex, - onDestinationSelected: (int index) { - setState(() => _selectedIndex = index); - }, - labelType: NavigationRailLabelType.all, - destinations: const [ - NavigationRailDestination( - icon: Icon(Icons.play_arrow), - label: Text('启动'), - ), - NavigationRailDestination( - icon: Icon(Icons.hub), - label: Text('联机'), - ), - NavigationRailDestination( - icon: Icon(Icons.download), - label: Text('下载'), - ), - NavigationRailDestination( - icon: Icon(Icons.settings), - label: Text('设置'), - ), - ], - ), - // 显示当前页面 - Expanded( - child: LazyLoadIndexedStack( - index: _selectedIndex, - children: _mainPages, - ), - ), - ], - ), - ); - // macOS 菜单栏 - if (Platform.isMacOS) { - return PlatformMenuBar( - menus: [ - // FML 菜单 - PlatformMenu( - label: kAppName, - menus: [ - PlatformMenuItem( - label: '关于', - onSelected: () => _showAboutDialog(context), - ), - PlatformMenuItem(label: '检查更新', onSelected: () => _checkUpdate()), - PlatformMenuItem( - label: '设置', - shortcut: const SingleActivator( - LogicalKeyboardKey.comma, - meta: true, - ), - onSelected: () => setState(() => _selectedIndex = 3), - ), - PlatformMenuItem( - label: '退出', - shortcut: const SingleActivator( - LogicalKeyboardKey.keyQ, - meta: true, - ), - onSelected: () => SystemNavigator.pop(), - ), - ], - ), - // 导航菜单 - PlatformMenu( - label: '导航', - menus: [ - PlatformMenuItem( - label: '启动', - shortcut: const SingleActivator( - LogicalKeyboardKey.digit1, - meta: true, - ), - onSelected: () => setState(() => _selectedIndex = 0), - ), - PlatformMenuItem( - label: '联机', - shortcut: const SingleActivator( - LogicalKeyboardKey.digit2, - meta: true, - ), - onSelected: () => setState(() => _selectedIndex = 1), - ), - PlatformMenuItem( - label: '下载', - shortcut: const SingleActivator( - LogicalKeyboardKey.digit3, - meta: true, - ), - onSelected: () => setState(() => _selectedIndex = 2), - ), - PlatformMenuItem( - label: '设置', - shortcut: const SingleActivator( - LogicalKeyboardKey.digit4, - meta: true, - ), - onSelected: () => setState(() => _selectedIndex = 3), - ), - ], - ), - // 帮助菜单 - PlatformMenu( - label: '帮助', - menus: [ - PlatformMenuItem( - label: '访问 GitHub', - onSelected: () => _launchURL(AppUrls.githubProject), - ), - ], - ), - ], - child: mainContent, - ); - } - return mainContent; - } - // 显示关于对话框 Future _showAboutDialog(BuildContext context) async { const channel = MethodChannel(kNativeMethodChannel); diff --git a/lib/pages/setting/java.dart b/lib/pages/setting/java.dart index 989d621..b1b9ff1 100644 --- a/lib/pages/setting/java.dart +++ b/lib/pages/setting/java.dart @@ -1,11 +1,14 @@ import 'dart:io'; +import 'dart:isolate'; + +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:fml/constants.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:fml/function/log.dart'; -import 'package:fml/function/java/java_manager.dart'; +import 'package:fml/function/java/java_service.dart'; +import 'package:fml/function/java/java_utils.dart'; import 'package:fml/function/java/models/java_info.dart'; import 'package:fml/function/java/models/java_runtime.dart'; +import 'package:open_filex/open_filex.dart'; class JavaPage extends StatefulWidget { const JavaPage({super.key}); @@ -15,32 +18,12 @@ class JavaPage extends StatefulWidget { } class JavaPageState extends State { - late Future> _javaRuntimesFuture; - late Future _systemDefaultJavaInfo; - - String? _currentJavaPath; - // 每个设置间的间距 static const _itemsPadding = Padding( padding: EdgeInsets.symmetric(vertical: kDefaultPadding / 2), ); - @override - void initState() { - super.initState(); - _getCurrentJavaPathFromPrefs(); - _refresh(); - } - - /// - /// 刷新 Java 列表与系统默认 Java - /// - Future _refresh() async { - setState(() { - _systemDefaultJavaInfo = _getSystemDefaultJavaInfo(); - _javaRuntimesFuture = JavaManager.searchPotentialJavaExecutables(); - }); - } + bool _isRefreshing = false; @override Widget build(BuildContext context) { @@ -52,224 +35,387 @@ class JavaPageState extends State { children: [ // 大标题 - Padding( - padding: const EdgeInsets.only( - left: kDefaultPadding / 2, - top: kDefaultPadding, - bottom: kDefaultPadding, - ), - child: Text( - '设备上的Java列表', - style: Theme.of(context).textTheme.headlineMedium, - ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only( + left: kDefaultPadding / 2, + top: kDefaultPadding, + bottom: kDefaultPadding, + ), + + child: Text( + '设备上的Java列表', + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + + Spacer(), + + IconButton( + icon: const Icon(Icons.refresh), + tooltip: '刷新', + onPressed: _isRefreshing + ? null + : () async { + setState(() => _isRefreshing = true); + + try { + // 在子线程获取数据 + final searchResults = await Isolate.run( + () => JavaUtils.searchPotentialJavaExecutables(), + ); + + // 将javaRuntimes转换为Map + final Map runtimeMap = { + for (var runtime in JavaService.javaRuntimes) + runtime.executable: runtime, + }; + + // 通过Map查重 + int addedCount = 0; + for (var result in searchResults) { + if (!runtimeMap.containsKey(result.executable)) { + runtimeMap[result.executable] = result; + // 发现新Java时计数 + addedCount++; + } + } + + final totalList = runtimeMap.values.toList(); + + // 更新并写入 + await JavaService.updateJavaRuntimes(totalList); + + if (!mounted) return; + + String message = addedCount > 0 + ? '刷新完成,搜索到了$addedCount个Java(共有${totalList.length}个)' + : '刷新完成,未发现新的Java (共有${totalList.length}个)'; + + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + } finally { + if (mounted) setState(() => _isRefreshing = false); + } + }, + ), + + IconButton( + icon: const Icon(Icons.add), + tooltip: '手动添加一个Java', + onPressed: () async => await _pickAndAddJavaRuntime(), + ), + ], ), _itemsPadding, - // 确保FutureBuilder占满剩余空间 + // 确保ListView占满剩余空间 Expanded( - child: FutureBuilder>( - future: Future.wait([ - _systemDefaultJavaInfo, - _javaRuntimesFuture, - ]), - - builder: (context, snapshot) { - // 加载中显示CircularProgressIndicator - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - - // 加载失败显示错误信息 - // TODO: 包装一个表示错误的组件 - if (snapshot.hasError) { - return Center(child: Text('检测失败:${snapshot.error}')); - } - - // Index0: _systemDefaultJavaInfo 的结果(JavaInfo?) - // Index1: _javaRuntimesFuture 的结果(List) - final results = snapshot.data ?? []; - - // 提取系统默认 Java 信息 - final JavaInfo? systemJavaInfo = results.isNotEmpty - ? results[0] as JavaInfo? - : null; - - // 检测系统默认Java是否存在 - final systemJavaExists = systemJavaInfo != null; - - // 提取扫描到的Java运行时列表 - List javaRuntimes = []; - if (results.length > 1) { - javaRuntimes = (results[1] as List).cast(); - } - - // 如果系统默认存在且路径不为空,移除扫描列表中与系统默认路径相同的项 - if (systemJavaInfo != null && systemJavaInfo.path.isNotEmpty) { - javaRuntimes.removeWhere( - (runtime) => runtime.executable == systemJavaInfo.path, - ); - } - - final totalItems = systemJavaExists - ? javaRuntimes.length + 1 - : javaRuntimes.length; - - if (totalItems == 0) { - return const Center(child: Text('未检测到 Java')); - } - - return ListView.builder( - itemCount: totalItems, - - itemBuilder: (context, index) { - if (systemJavaExists && index == 0) { + child: _isRefreshing + ? Center(child: CircularProgressIndicator()) + : ListView.builder( + itemCount: JavaService.javaRuntimes.length, + itemBuilder: (context, index) { + // 构建Java的卡片 + final javaRuntime = JavaService.javaRuntimes[index]; + final isCurrentJava = - _currentJavaPath == 'default' || - _currentJavaPath == null; + JavaService.javaSelectedPath == + javaRuntime.executable; + + final isSystemDefault = + javaRuntime.executable == + JavaService.systemDefaultJavaInfo?.path; - // 构建系统默认Card return _buildJavaCard( - javaInfo: systemJavaInfo, + javaInfo: javaRuntime.info, - typeChipLabel: '系统默认', + typeChipLabel: javaRuntime.isJdk ? 'JDK' : 'JRE', - vendor: systemJavaInfo.vendor, + vendor: javaRuntime.info.vendor, isCurrent: isCurrentJava, - onTap: _setSystemJava, + isSystemDefault: isSystemDefault, + + onTap: () async => { + // 异步更新数据 + await JavaService.updateJavaSelectedPath( + javaRuntime.executable, + ), + + // 通知UI刷新 + if (mounted) {setState(() {})}, + }, + + onLongPress: () => + _buildOnLongPressDialog(context, javaRuntime), ); - } + }, + ), + ), + ], + ), + ); + } + + Future _buildOnLongPressDialog( + BuildContext context, + JavaRuntime javaRuntime, + ) async { + await showDialog( + context: context, + + builder: (context) { + return SimpleDialog( + title: Text('选择要进行的操作'), - // 构建非系统默认的Java的卡片 - final realIndex = systemJavaExists ? index - 1 : index; - final javaRuntime = javaRuntimes[realIndex]; + children: [ + SimpleDialogOption( + child: Text('在文件资源管理器中显示父文件夹'), + onPressed: () { + // 打开父文件夹 + final parentDirPath = File(javaRuntime.executable).parent.path; - final isCurrentJava = - _currentJavaPath == javaRuntime.executable; + OpenFilex.open(parentDirPath); - return _buildJavaCard( - javaInfo: javaRuntime.info, + Navigator.of(context).pop(); + }, + ), + + Divider(), + + SimpleDialogOption( + child: Text('在列表中删除'), + + onPressed: () { + final javaName = + '${javaRuntime.info.vendor} ${javaRuntime.info.version}'; + + showCustomDialog( + context: context, + title: '提示', + barrierDismissible: false, + + content: Text('你确认要在列表中删除 $javaName 吗?'), + + actions: [ + TextButton( + onPressed: () async { + if (!mounted) return; + + // 弹出当前Dialog + Navigator.of(context).maybePop(); + + if (JavaService.javaRuntimes.length <= 1) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('该Java为最后一个Java,无法移除!')), + ); + + return; + } + + // 从列表中移除 + JavaService.javaRuntimes.remove(javaRuntime); - typeChipLabel: javaRuntime.isJdk ? 'JDK' : 'JRE', + // 刷新UI 并执行异步操作 + setState(() {}); - vendor: javaRuntime.info.vendor, + await JavaService.updateJavaRuntimes( + JavaService.javaRuntimes, + ); - isCurrent: isCurrentJava, + if (!mounted) return; - onTap: () => - _setCurrentJavaPathToPrefs(javaRuntime.executable), - ); - }, + // 若移除的为当前Java,将第一个设置为javaRuntimes的第一个 + if (JavaService.javaRuntimes.isNotEmpty) { + await JavaService.updateJavaSelectedPath( + JavaService.javaRuntimes.first.executable, + ); + } + + // 显示SnackBar并弹出父Dialog + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('已从列表中移除 $javaName')), + ); + + Navigator.of(context).maybePop(); + }, + + child: Text('是'), + ), + + TextButton( + onPressed: () => Navigator.of(context).maybePop(), + child: Text('否'), + ), + ], ); }, ), - ), - ], - ), + ], + ); + }, ); } /// - /// 从SharedPreferences读取选择的Java + /// 显示FilePicker并根据选择的文件添加JavaRuntimes /// - Future _getCurrentJavaPathFromPrefs() async { - final prefs = await SharedPreferences.getInstance(); + Future _pickAndAddJavaRuntime() async { + // 打开FilePicker + final result = await FilePicker.platform.pickFiles( + dialogTitle: '选择Java路径', + type: FileType.custom, + allowedExtensions: Platform.isWindows ? ['exe'] : [], + ); - setState(() { - _currentJavaPath = prefs.getString('java'); - }); - } + if (!mounted) return; - /// - /// 写入当前 Java - /// - Future _setCurrentJavaPathToPrefs(String path) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('java', path); + // 未选择文件 + if (result == null) { + showCustomDialog( + context: context, + title: '提示', - setState(() { - _currentJavaPath = path; - }); - } + content: Text('未选择任何文件'), - /// - /// 设置为系统 Java - /// - Future _setSystemJava() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.remove('java'); + actions: [ + if (mounted) + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('关闭'), + ), + ], + ); - setState(() { - _currentJavaPath = 'default'; - }); - } + return; + } + + // 读取选择的文件的信息 + final file = result.files.single; + final fileName = file.name.toLowerCase(); + final validNames = Platform.isWindows + ? ['java.exe', 'javaw.exe'] + : ['java', 'javaw']; + + final exe = file.path!; + final info = await JavaUtils.probeJavaExecutable(exe); + + // 选择的文件文件名合法 + if (file.path != null && validNames.contains(fileName)) { + if (!mounted) return; + + showCustomDialog( + context: context, + title: '正在添加Java', + content: SizedBox( + height: 80, + child: Center(child: CircularProgressIndicator()), + ), + + barrierDismissible: false, + ); - // - // 获取系统默认 Java 信息 - // - Future _getSystemDefaultJavaInfo() async { - try { - final javaVersionProcess = await Process.run('java', ['-version']); - - if (javaVersionProcess.exitCode != 0) { - LogUtil.log( - '获取系统默认 Java 信息失败,退出码:${javaVersionProcess.exitCode}', - level: 'WARN', + if (info != null) { + // 查重逻辑 + final alreadyExists = JavaService.javaRuntimes.any( + (runtime) => runtime.executable == exe, ); - } - final versionOutput = (javaVersionProcess.stderr as String).isNotEmpty - ? javaVersionProcess.stderr as String - : javaVersionProcess.stdout as String; + if (alreadyExists) { + Navigator.of(context).pop(); - final parsedVersion = JavaManager.parseVersionOutput(versionOutput); + showCustomDialog( + context: context, + title: '提示', - if (parsedVersion == null) { - LogUtil.log('无法解析系统默认 Java 版本信息', level: 'WARN'); - return null; - } + content: Text('该Java已存在'), - String executablePath = ''; - - try { - if (Platform.isWindows) { - final where = await Process.run('where', ['java']); - - if (where.exitCode == 0) { - executablePath = (where.stdout as String) - .toString() - .split('\n') - .first - .trim(); - } - } else { - final which = await Process.run('which', ['java']); - - if (which.exitCode == 0) { - executablePath = (which.stdout as String) - .toString() - .split('\n') - .first - .trim(); - } + actions: [ + if (mounted) + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('关闭'), + ), + ], + ); + + return; } - } catch (e) { - LogUtil.log('获取系统默认 Java 路径时出错:$e', level: 'WARN'); - } - return JavaInfo( - version: parsedVersion['version'] ?? 'unknown', - vendor: parsedVersion['vendor'], - path: executablePath, - os: Platform.operatingSystem, - arch: Platform.version, - ); - } catch (e) { - LogUtil.log('执行 "java -version" 时出错:$e', level: 'WARN'); - return null; + // 添加并写入SharedPreference + final isJdk = await JavaUtils.looksLikeJdk(exe); + + JavaService.javaRuntimes.add( + JavaRuntime(info: info, executable: exe, isJdk: isJdk), + ); + + JavaService.updateJavaRuntimes(JavaService.javaRuntimes); + + Navigator.of(context).pop(); + + // 调用setState触发页面更新 + setState(() {}); + + if (!mounted) return; + + showCustomDialog( + context: context, + title: '提示', + content: Text('添加 ${info.vendor} ${info.version} 成功!'), + + actions: [ + if (mounted) + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('关闭'), + ), + ], + ); + + return; + } } + + // 最后分支,选择的文件文件名不合法等情况 + if (!mounted) return; + + Navigator.of(context).maybePop(); + + showCustomDialog( + context: context, + title: '提示', + content: Text('请选择正确的Java可执行文件'), + + actions: [ + TextButton( + onPressed: () => {if (mounted) Navigator.of(context).pop()}, + child: Text('关闭'), + ), + ], + ); + } + + /// + /// 显示一个自定义的AlertDialog + /// + void showCustomDialog({ + required BuildContext context, + required String title, + Widget? content, + bool barrierDismissible = true, + List? actions, + }) { + showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (_) => + AlertDialog(title: Text(title), content: content, actions: actions), + ); } Widget _buildJavaCard({ @@ -277,7 +423,9 @@ class JavaPageState extends State { required String typeChipLabel, String? vendor, required bool isCurrent, + required bool isSystemDefault, required VoidCallback onTap, + required VoidCallback onLongPress, }) { return Card( // 裁剪掉ListTile超出圆角的部分 @@ -309,6 +457,13 @@ class JavaPageState extends State { SizedBox(width: kDefaultPadding / 2), ], + + if (isSystemDefault) ...[ + Chip(label: Text('系统默认')), + + SizedBox(width: kDefaultPadding / 2), + ], + Chip(label: Text(typeChipLabel)), SizedBox(width: kDefaultPadding / 2), @@ -317,6 +472,7 @@ class JavaPageState extends State { ], ), onTap: onTap, + onLongPress: onLongPress, ), ); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 8e8962b..3f49cfb 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.lock b/pubspec.lock index 43a0ae1..71bd820 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -157,18 +157,18 @@ packages: dependency: "direct main" description: name: desktop_drop - sha256: e70b46b2d61f1af7a81a40d1f79b43c28a879e30a4ef31e87e9c27bea4d784e8 + sha256: aa1e797255bfbc76f9eb5aa4f61e5b68dbf69962ab1be6495816d2f251bc0d1f url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.0" + version: "0.7.1" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c" + sha256: b4fed1b2835da9d670d7bed7db79ae2a94b0f5ad6312268158a9b5479abbacdd url: "https://pub.flutter-io.cn" source: hosted - version: "12.3.0" + version: "12.4.0" device_info_plus_platform_interface: dependency: transitive description: @@ -245,10 +245,10 @@ packages: dependency: transitive description: name: file_selector_android - sha256: bf7ab65776d7e176280c853679e7742668586ba1663f7f1561e897fadad6c3ba + sha256: "89243030ea4b3463fb402b44d5eeacc4ccb1c46a88870cb2a5080d693200c1ed" url: "https://pub.flutter-io.cn" source: hosted - version: "0.5.2+5" + version: "0.5.2+6" file_selector_ios: dependency: transitive description: @@ -420,10 +420,10 @@ packages: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.2" + version: "1.0.3" html: dependency: "direct main" description: @@ -464,6 +464,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.20.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" js: dependency: transitive description: @@ -540,10 +556,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -584,14 +600,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "9.3.0" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.7.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" url: "https://pub.flutter-io.cn" source: hosted - version: "9.0.0" + version: "9.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -620,10 +652,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.23" + version: "2.3.1" path_provider_foundation: dependency: transitive description: @@ -704,6 +736,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.0" screen_retriever: dependency: transitive description: @@ -748,10 +788,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa" url: "https://pub.flutter-io.cn" source: hosted - version: "12.0.1" + version: "12.0.2" share_plus_platform_interface: dependency: transitive description: @@ -857,10 +897,10 @@ packages: dependency: "direct main" description: name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" url: "https://pub.flutter-io.cn" source: hosted - version: "3.4.0" + version: "3.4.0+1" system_info2: dependency: "direct main" description: @@ -881,10 +921,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.10" + version: "0.7.9" timezone: dependency: transitive description: @@ -993,10 +1033,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.flutter-io.cn" source: hosted - version: "15.0.2" + version: "15.2.0" web: dependency: transitive description: @@ -1014,7 +1054,7 @@ packages: source: hosted version: "5.15.0" win32_registry: - dependency: transitive + dependency: "direct main" description: name: win32_registry sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" @@ -1054,5 +1094,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.3 <4.0.0" + dart: ">=3.11.0 <4.0.0" flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 2089967..195f68d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: window_manager: ^0.5.1 intl: ^0.20.2 lazy_load_indexed_stack: ^1.2.1 + win32_registry: ^2.1.0 + open_filex: ^4.7.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d1a96c5..b79beb0 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flutter_local_notifications_windows + jni ) set(PLUGIN_BUNDLED_LIBRARIES)