Skip to content

Commit

Permalink
Allow custom Java Executable selection!
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
TechnicJelle committed Sep 6, 2024
1 parent 8fe6faa commit 9a57eb0
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 14 deletions.
10 changes: 7 additions & 3 deletions lib/control_panel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ import "package:rxdart/rxdart.dart";
import "package:url_launcher/url_launcher.dart";

import "console.dart";
import "java/java_picker.dart";
import "main.dart";

final portExtractionRegex = RegExp(r"(?:port\s*|:)(\d{4,5})$");

final _processProvider = Provider<RunningProcess?>((ref) {
final Directory? projectDirectory = ref.watch(projectDirectoryProvider);
if (projectDirectory == null) return null;
final process = RunningProcess(projectDirectory);
final String? javaPath = ref.watch(javaPathProvider);
if (javaPath == null) return null;
final process = RunningProcess(projectDirectory, javaPath);
ref.onDispose(() => process.stop());
return process;
});
Expand All @@ -44,6 +47,7 @@ enum RunningProcessState {

class RunningProcess {
final Directory _projectDirectory;
final String _javaPath;

Process? _process;

Expand All @@ -59,7 +63,7 @@ class RunningProcess {

StreamSubscription? _outputStreamSub;

RunningProcess(this._projectDirectory) {
RunningProcess(this._projectDirectory, this._javaPath) {
AppLifecycleListener(
onExitRequested: () async {
if (_stateController.value == RunningProcessState.running) {
Expand Down Expand Up @@ -91,7 +95,7 @@ class RunningProcess {
final String bluemapJarPath = p.join(_projectDirectory.path, blueMapCliJarName);

final process = await Process.start(
"java",
_javaPath,
["-jar", bluemapJarPath, "--render", "--watch", "--webserver"],
workingDirectory: _projectDirectory.path,
mode: ProcessStartMode.normal,
Expand Down
71 changes: 71 additions & 0 deletions lib/java/java_picker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";

import "../prefs.dart";
import "radio_list_tile_custom_java_picker.dart";
import "radio_list_tile_system_java_picker.dart";

final javaPathProvider = Provider<String?>((ref) {
return Prefs.instance.javaPath;
});

enum JavaPickerMode {
system,
pick,
}

class JavaPicker extends ConsumerStatefulWidget {
const JavaPicker({super.key});

@override
ConsumerState<JavaPicker> createState() => _JavaPickerState();
}

class _JavaPickerState extends ConsumerState<JavaPicker> {
JavaPickerMode? javaPickerMode;

@override
void initState() {
super.initState();
if (Prefs.instance.javaPath != null) {
javaPickerMode =
Prefs.instance.javaPath == "java" ? JavaPickerMode.system : JavaPickerMode.pick;
}
}

@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("Select your Java executable:"),
const SizedBox(height: 4),
RadioListTileSystemJavaPicker(
groupValue: javaPickerMode,
onSet: () {
setState(() {
javaPickerMode = JavaPickerMode.system;
});
Prefs.instance.javaPath = "java";
ref.invalidate(javaPathProvider);
},
),
RadioListTileCustomJavaPicker(
groupValue: javaPickerMode,
onChanged: (javaPath) {
setState(() {
javaPickerMode = JavaPickerMode.pick;
});
Prefs.instance.javaPath = javaPath;
ref.invalidate(javaPathProvider);
},
)
],
),
);
}
}
97 changes: 97 additions & 0 deletions lib/java/radio_list_tile_custom_java_picker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import "package:file_picker/file_picker.dart";
import "package:flutter/material.dart";

import "java_picker.dart";
import "util_for_checking_java_path_version.dart";

enum _CustomPickingState {
nothing,
picking,
checking,
failed,
success,
}

class RadioListTileCustomJavaPicker extends StatefulWidget {
final JavaPickerMode? groupValue;
final Function(String javaPath) onChanged;

const RadioListTileCustomJavaPicker({
super.key,
required this.groupValue,
required this.onChanged,
});

@override
State<RadioListTileCustomJavaPicker> createState() =>
_RadioListTileCustomJavaPickerState();
}

class _RadioListTileCustomJavaPickerState extends State<RadioListTileCustomJavaPicker> {
_CustomPickingState customPickingState = _CustomPickingState.nothing;
String? customPickErrorText;
int? customJavaVersion;

@override
Widget build(BuildContext context) {
return RadioListTile<JavaPickerMode>(
title: const Text("Custom"),
subtitle: switch (customPickingState) {
_CustomPickingState.nothing => const Text("Select a Java executable manually"),
_CustomPickingState.picking => const Text("Selecting Java executable..."),
_CustomPickingState.checking => const Text("Checking Java version..."),
_CustomPickingState.failed => Text(
customPickErrorText ?? "Unknown error",
style: const TextStyle(color: Colors.red),
),
_CustomPickingState.success => Text(
"Detected Java version: $customJavaVersion",
),
},
value: JavaPickerMode.pick,
groupValue: widget.groupValue,
onChanged: (JavaPickerMode? value) async {
if (customPickingState != _CustomPickingState.nothing &&
customPickingState != _CustomPickingState.failed &&
customPickingState != _CustomPickingState.success) return;
setState(() => customPickingState = _CustomPickingState.picking);
final FilePickerResult? picked = await FilePicker.platform.pickFiles(
dialogTitle: "Select Java executable",
//cannot use FileType.custom, because it doesn't support files with no extension, which is the case for executables on linux
type: FileType.any,
);
if (picked == null) {
setState(() => customPickingState = _CustomPickingState.nothing);
return; // User canceled the picker
}
setState(() => customPickingState = _CustomPickingState.checking);

final String? javaPath = picked.files.single.path;
if (javaPath == null) {
setState(() {
customPickingState = _CustomPickingState.failed;
customPickErrorText = "Path is null";
});
return;
}

int javaVersion;
try {
javaVersion = await checkJavaVersion(javaPath);
} catch (e) {
setState(() {
customPickingState = _CustomPickingState.failed;
customPickErrorText = e.toString().replaceAll("Exception:", "Error:");
});
return;
}

setState(() {
customPickingState = _CustomPickingState.success;
customJavaVersion = javaVersion;
widget.onChanged(javaPath);
});
},
);
}
}
42 changes: 42 additions & 0 deletions lib/java/radio_list_tile_system_java_picker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";

import "../utils.dart";
import "java_picker.dart";
import "util_for_checking_java_path_version.dart";

final _systemJavaVersionProvider = FutureProvider((ref) async {
return checkJavaVersion("java");
});

class RadioListTileSystemJavaPicker extends ConsumerWidget {
final JavaPickerMode? groupValue;
final Function() onSet;

const RadioListTileSystemJavaPicker({
super.key,
required this.groupValue,
required this.onSet,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<int> javaVersion = ref.watch(_systemJavaVersionProvider);
final bool hasSuitableJavaInstalled = javaVersion.valueOrNull != null;

return RadioListTile<JavaPickerMode>(
title: Text(JavaPickerMode.system.name.capitalize()),
subtitle: switch (javaVersion) {
AsyncData(:final value) => Text("Detected System Java version: $value"),
AsyncError(:final error) => Text(
error.toString(),
style: const TextStyle(color: Colors.red),
),
_ => const Text("Checking System Java version..."),
},
value: JavaPickerMode.system,
groupValue: groupValue,
onChanged: hasSuitableJavaInstalled ? (JavaPickerMode? value) => onSet() : null,
);
}
}
56 changes: 56 additions & 0 deletions lib/java/util_for_checking_java_path_version.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import "dart:io";

const int _minJavaVersion = 16;

/// Checks the Java version at the given path.
/// Throw an exception if the Java version is too old, not installed, or the path is invalid.
Future<int> checkJavaVersion(String javaPath) async {
if (javaPath.isEmpty) {
throw "Provided path is empty.";
}

if (!javaPath.contains("java")) {
throw "Provided path is not a Java executable.";
}

// If the path is not the system Java path, check if the file exists
if (javaPath != "java") {
if (!File(javaPath).existsSync()) {
throw "File at provided path does not exist.";
}
}

try {
ProcessResult jv = await Process.run(javaPath, ["--version"]);
final int exitCode = jv.exitCode;
final String stdout = jv.stdout;
final String stderr = jv.stderr;

if (exitCode != 0) {
throw "Process exited with $exitCode.\n$stderr";
}

RegExp r = RegExp(r"\d+");
final Match? match = r.firstMatch(stdout);
if (match == null) {
throw "Version message did not contain a version number.";
}

int? version = int.tryParse(match.group(0) ?? "");
if (version == null) {
throw "Couldn't parse version message.";
}

if (version < _minJavaVersion) {
throw "System Java version $version is too old. Please install Java $_minJavaVersion or newer.";
}

return version;
} on ProcessException {
if (javaPath == "java") {
throw "Java (probably) not installed on your system. Please install Java $_minJavaVersion or newer.";
} else {
throw "Invalid Java executable.";
}
}
}
30 changes: 20 additions & 10 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart";

import "close_project_button.dart";
import "dual_pane.dart";
import "java/java_picker.dart";
import "path_picker_button.dart";
import "prefs.dart";
import "tech_app.dart";
Expand Down Expand Up @@ -70,19 +71,28 @@ class MyHomePage extends ConsumerWidget {
],
),
body: projectDirectory == null
? const Center(
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Select an empty folder to store your BlueMap files in:"),
SizedBox(height: 8),
PathPickerButton(),
SizedBox(height: 8),
Text("The BlueMap CLI tool will be downloaded into that folder."),
SizedBox(height: 4),
Text("It will generate some default config files for you."),
SizedBox(height: 4),
Text("You will then need to configure your maps in the BlueMap GUI."),
const JavaPicker(),
if (ref.watch(javaPathProvider) != null) ...[
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: const Divider(),
),
const Text("Select an empty folder to store your BlueMap files in:"),
const SizedBox(height: 8),
const PathPickerButton(),
const SizedBox(height: 8),
const Text(
"The BlueMap CLI tool will be downloaded into that folder."),
const SizedBox(height: 4),
const Text("It will generate some default config files for you."),
const SizedBox(height: 4),
const Text(
"You will then need to configure your maps in the BlueMap GUI."),
],
],
),
)
Expand Down
3 changes: 2 additions & 1 deletion lib/path_picker_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:path/path.dart" as p;
import "package:url_launcher/url_launcher_string.dart";

import "java/java_picker.dart";
import "main.dart";
import "prefs.dart";

Expand Down Expand Up @@ -94,7 +95,7 @@ class _PathPickerButtonState extends ConsumerState<PathPickerButton> {
// == Run BlueMap CLI JAR to generate default configs ==
setState(() => _pickingState = _PickingState.running);
ProcessResult run = await Process.run(
"java",
ref.read(javaPathProvider)!,
["-jar", bluemapJar.path],
workingDirectory: projectDirectory.path,
stdoutEncoding: utf8,
Expand Down
Loading

0 comments on commit 9a57eb0

Please sign in to comment.