* feat: added adding new instances of a cluster with 'start from scratch' option enabling user to create a vanilla/modded cluster; fixed couple pre-existing bugs that wouldn't let other loaders boot up * feat: fully implemented cluster creation modal design * chore: refactoring * feat: expanded the tags section on customization step; found that fabric doesnt work with 1.8.9 * feat: added gallery covers handling for new cluster instances * feat: added 'redirecting' from fabric to orinthe on versions pre-1.14 * feat: added java-version-override * feat: added loader types migrations (right now they dont migrate mods because there are no bundles) * chore: wider loader dropdown for multiloader cluster (if it will be used in the future) * chore: fixed more issues with isolated and shared dir, with migrations and instances naming * chore: refactor; taken the loader version dropdown from version picker view back to loader selector * chore: deleted Quilt from UI --------- Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
1014 lines
32 KiB
Rust
1014 lines
32 KiB
Rust
use std::sync::Arc;
|
|
|
|
use freya::prelude::*;
|
|
use oneclient_common::Patch;
|
|
use oneclient_common::domain::GameLoader;
|
|
use oneclient_core::settings::{GameSettingsProfile, PackageUpdateMode, ProfileUpdate, Resolution};
|
|
use oneclient_java::JavaRuntime;
|
|
|
|
use crate::components::{
|
|
Button, Dropdown, Icon, IconType, ScrollArea, TextInput, memory_field, toggle,
|
|
toggle_controlled, validate_number,
|
|
};
|
|
use crate::hooks::{
|
|
ClusterAction, java_runtimes, loader_versions, mutation_is_running, query_error,
|
|
settled_or_loading, try_game_profile, use_cluster_mutation, use_clusters, use_dispatch,
|
|
use_game_profile, use_game_snapshot, use_java_runtimes, use_loader_versions,
|
|
use_release_migration_checking, use_settings_snapshot,
|
|
};
|
|
use crate::layout::cluster_content;
|
|
use crate::theme::colors;
|
|
use crate::ui::centered_note;
|
|
use crate::view::app::clusters::{DeleteInstanceModal, EditInstanceModal, InstanceFacts};
|
|
use crate::view::app::settings::{section_header, settings_row, settings_row_disabled};
|
|
use oneclient_core::clusters::rank_migration_sources;
|
|
|
|
use super::cluster_not_found;
|
|
use crate::hooks::use_cluster;
|
|
|
|
#[derive(PartialEq)]
|
|
pub struct ClusterSettings {
|
|
pub cluster_id: i64,
|
|
}
|
|
|
|
impl Component for ClusterSettings {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let global = use_settings_snapshot().settings.global_game_settings;
|
|
|
|
let cluster = use_cluster(cluster_id);
|
|
let profile_name = cluster
|
|
.as_ref()
|
|
.and_then(|c| c.setting_profile_name.clone());
|
|
let profile_query = use_game_profile(profile_name);
|
|
let mc_version = cluster
|
|
.as_ref()
|
|
.map(|c| c.mc_version.clone())
|
|
.unwrap_or_default();
|
|
let loader = cluster
|
|
.as_ref()
|
|
.map(|c| c.mc_loader)
|
|
.unwrap_or(GameLoader::Fabric);
|
|
let versions_query = use_loader_versions(mc_version, loader);
|
|
let runtimes_query = use_java_runtimes();
|
|
|
|
let Some(cluster) = cluster else {
|
|
return cluster_not_found();
|
|
};
|
|
|
|
let Some(profile) = try_game_profile(&profile_query) else {
|
|
let note = match query_error(&profile_query) {
|
|
Some(err) => format!("Could not load these settings: {err}"),
|
|
None => "Loading settings...".to_string(),
|
|
};
|
|
return cluster_content().child(centered_note(¬e)).into_element();
|
|
};
|
|
|
|
let versions = loader_versions(&versions_query);
|
|
let runtimes = java_runtimes(&runtimes_query);
|
|
|
|
let instance_section: Vec<Element> = if cluster.user_created {
|
|
vec![
|
|
section_header("INSTANCE").into_element(),
|
|
InstanceRow {
|
|
facts: InstanceFacts {
|
|
cluster_id,
|
|
name: cluster.name.clone(),
|
|
description: cluster.description.clone(),
|
|
tags: cluster.tags.clone(),
|
|
cover: cluster.cover_file(),
|
|
mc_version: cluster.mc_version.clone(),
|
|
mc_loader: cluster.mc_loader,
|
|
kind: cluster.kind,
|
|
},
|
|
}
|
|
.into_element(),
|
|
]
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
let mod_loader = !cluster.lacks_mod_loader();
|
|
let loader_section: Vec<Element> = if mod_loader {
|
|
vec![
|
|
section_header("LOADER").into_element(),
|
|
LoaderRow {
|
|
cluster_id,
|
|
loader,
|
|
selected: cluster.mc_loader_version.clone(),
|
|
versions,
|
|
locked: cluster.user_created,
|
|
}
|
|
.into_element(),
|
|
]
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
let migrate_row: Vec<Element> = if mod_loader {
|
|
vec![MigrateFromRow { cluster_id }.into_element()]
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
cluster_content()
|
|
.child(
|
|
ScrollArea::new()
|
|
.width(Size::fill())
|
|
.height(Size::fill())
|
|
.scrollbar_gutter(true)
|
|
.spacing(4.)
|
|
.children(instance_section)
|
|
.child(section_header("GAME"))
|
|
.child(
|
|
ToggleRow {
|
|
cluster_id,
|
|
value: profile.force_fullscreen,
|
|
global: global.force_fullscreen.unwrap_or(false),
|
|
}
|
|
.into_element(),
|
|
)
|
|
.child(
|
|
ResolutionRow {
|
|
cluster_id,
|
|
value: profile.resolution,
|
|
global: global.resolution.unwrap_or_default(),
|
|
}
|
|
.into_element(),
|
|
)
|
|
.child(
|
|
MemoryRow {
|
|
cluster_id,
|
|
value: profile.mem_max,
|
|
global: global
|
|
.mem_max
|
|
.unwrap_or_else(oneclient_common::default_mem_max),
|
|
}
|
|
.into_element(),
|
|
)
|
|
.append_children(loader_section)
|
|
.child(section_header("DIRECTORY"))
|
|
.child(
|
|
DedicatedDirRow {
|
|
cluster_id,
|
|
dedicated: cluster.uses_dedicated_dir(),
|
|
locked: cluster.is_isolated(),
|
|
}
|
|
.into_element(),
|
|
)
|
|
.child(section_header("SHORTCUT"))
|
|
.child(ShortcutRow { cluster_id }.into_element())
|
|
.child(section_header("JAVA"))
|
|
.child(
|
|
JavaRow {
|
|
cluster_id,
|
|
value: profile.java_path.clone(),
|
|
global: global.java_path.clone(),
|
|
runtimes,
|
|
}
|
|
.into_element(),
|
|
)
|
|
.child(text_row(cluster_id, TextField::JvmArgs, &profile, &global))
|
|
.child(section_header("PROCESS"))
|
|
.child(text_row(cluster_id, TextField::Pre, &profile, &global))
|
|
.child(text_row(cluster_id, TextField::Wrapper, &profile, &global))
|
|
.child(text_row(cluster_id, TextField::Post, &profile, &global))
|
|
.child(section_header("CONTENT"))
|
|
.child(
|
|
BrowserUpdateModeRow {
|
|
cluster_id,
|
|
value: profile.browser_update_mode,
|
|
global: global.browser_update_mode.unwrap_or_default(),
|
|
}
|
|
.into_element(),
|
|
)
|
|
.append_children(migrate_row)
|
|
.child(section_header("REPAIR"))
|
|
.child(VerifyFilesRow { cluster_id }.into_element()),
|
|
)
|
|
.into_element()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum Field {
|
|
ForceFullscreen,
|
|
Resolution,
|
|
MemMax,
|
|
JavaPath,
|
|
BrowserUpdateMode,
|
|
}
|
|
|
|
fn clear_update(field: Field) -> ProfileUpdate {
|
|
let mut u = ProfileUpdate::default();
|
|
match field {
|
|
Field::ForceFullscreen => u.force_fullscreen = Patch::Clear,
|
|
Field::Resolution => u.resolution = Patch::Clear,
|
|
Field::MemMax => u.mem_max = Patch::Clear,
|
|
Field::JavaPath => u.java_path = Patch::Clear,
|
|
Field::BrowserUpdateMode => u.browser_update_mode = Patch::Clear,
|
|
}
|
|
u
|
|
}
|
|
|
|
fn runtime_label(runtime: &JavaRuntime) -> String {
|
|
format!("{} {} ({})", runtime.vendor, runtime.major, runtime.version)
|
|
}
|
|
|
|
fn reset_button(overridden: bool, on_reset: EventHandler<()>) -> impl IntoElement {
|
|
let color = if overridden {
|
|
colors::fg_secondary()
|
|
} else {
|
|
colors::fg_secondary().with_a(60)
|
|
};
|
|
|
|
Button::new()
|
|
.small()
|
|
.ghost()
|
|
.icon()
|
|
.tooltip(if overridden {
|
|
"Reset to the global setting"
|
|
} else {
|
|
"Already using the global setting"
|
|
})
|
|
.corner_radius(CornerRadius::new_all(7.))
|
|
.maybe(overridden, |el| el.on_press(move |_| on_reset.call(())))
|
|
.enabled(overridden)
|
|
.child(Icon::new(IconType::RefreshCcw02).size(15.).color(color))
|
|
.into_element()
|
|
}
|
|
|
|
fn override_cell(
|
|
control: impl IntoElement,
|
|
overridden: bool,
|
|
on_reset: EventHandler<()>,
|
|
) -> impl IntoElement {
|
|
rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(10.)
|
|
.child(control)
|
|
.child(reset_button(overridden, on_reset))
|
|
.into_element()
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct ToggleRow {
|
|
cluster_id: i64,
|
|
value: Option<bool>,
|
|
global: bool,
|
|
}
|
|
|
|
impl Component for ToggleRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let overridden = self.value.is_some();
|
|
let global = self.global;
|
|
let dispatch = use_dispatch();
|
|
|
|
let initial = self.value.unwrap_or(self.global);
|
|
let mut state = use_state(move || initial);
|
|
let mut last = use_state(move || initial);
|
|
{
|
|
let dispatch = dispatch.clone();
|
|
use_side_effect(move || {
|
|
let v = *state.read();
|
|
if v == *last.peek() {
|
|
return;
|
|
}
|
|
last.set(v);
|
|
dispatch.update_cluster_profile(
|
|
cluster_id,
|
|
ProfileUpdate {
|
|
force_fullscreen: Patch::Set(v),
|
|
..Default::default()
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
let on_reset: EventHandler<()> = (move |()| {
|
|
last.set(global);
|
|
state.set(global);
|
|
dispatch.update_cluster_profile(cluster_id, clear_update(Field::ForceFullscreen));
|
|
})
|
|
.into();
|
|
|
|
settings_row(
|
|
IconType::Maximize01,
|
|
"Force Fullscreen",
|
|
"Force Minecraft to start in fullscreen mode.",
|
|
override_cell(toggle(state), overridden, on_reset),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct InstanceRow {
|
|
facts: InstanceFacts,
|
|
}
|
|
|
|
impl Component for InstanceRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let mut editing = use_state(|| false);
|
|
let mut deleting = use_state(|| false);
|
|
let cluster_id = self.facts.cluster_id;
|
|
let name = self.facts.name.clone();
|
|
let facts = self.facts.clone();
|
|
|
|
let buttons = rect()
|
|
.horizontal()
|
|
.spacing(8.)
|
|
.child(
|
|
Button::new()
|
|
.small()
|
|
.secondary()
|
|
.on_press(move |_| editing.set(true))
|
|
.text("Edit"),
|
|
)
|
|
.child(
|
|
Button::new()
|
|
.small()
|
|
.danger()
|
|
.on_press(move |_| deleting.set(true))
|
|
.text("Delete"),
|
|
);
|
|
|
|
rect()
|
|
.vertical()
|
|
.width(Size::fill())
|
|
.child(settings_row(
|
|
IconType::Pencil01,
|
|
"Instance Details",
|
|
"Change this instance's name, description, tags and cover image, or remove it from your list.",
|
|
buttons,
|
|
))
|
|
.maybe_child(editing.read().then(|| {
|
|
EditInstanceModal::new(facts.clone(), move |()| editing.set(false))
|
|
.into_element()
|
|
}))
|
|
.maybe_child(deleting.read().then(|| {
|
|
DeleteInstanceModal::new(cluster_id, name.clone(), move |()| deleting.set(false))
|
|
.into_element()
|
|
}))
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct ShortcutRow {
|
|
cluster_id: i64,
|
|
}
|
|
|
|
impl Component for ShortcutRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let dispatch = use_dispatch();
|
|
|
|
let button = Button::new()
|
|
.small()
|
|
.secondary()
|
|
.on_press(move |_| dispatch.create_cluster_shortcut(cluster_id))
|
|
.text("Create Shortcut");
|
|
|
|
settings_row(
|
|
IconType::Rocket02,
|
|
"Desktop Shortcut",
|
|
"Save a shortcut that starts this version straight from your desktop, \
|
|
without opening the launcher first.",
|
|
button,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct VerifyFilesRow {
|
|
cluster_id: i64,
|
|
}
|
|
|
|
impl Component for VerifyFilesRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let mutation = use_cluster_mutation();
|
|
let running = mutation_is_running(&mutation);
|
|
|
|
let on_press = move |_| {
|
|
mutation.mutate(ClusterAction::VerifyFiles { cluster_id });
|
|
};
|
|
|
|
// The pass reads every installed file so say it takes a while otherwise the button reads as broken
|
|
let button = Button::new()
|
|
.small()
|
|
.secondary()
|
|
.enabled(!running)
|
|
.maybe(!running, |el| el.on_press(on_press))
|
|
.text(if running {
|
|
"Verifying..."
|
|
} else {
|
|
"Verify Files"
|
|
});
|
|
|
|
settings_row(
|
|
IconType::ClipboardCheck,
|
|
"Verify Files",
|
|
"Check every installed file against its official checksum and \
|
|
re-download anything corrupt or missing. Takes a few minutes.",
|
|
button,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct DedicatedDirRow {
|
|
cluster_id: i64,
|
|
dedicated: bool,
|
|
locked: bool,
|
|
}
|
|
|
|
impl Component for DedicatedDirRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let dedicated = self.dedicated;
|
|
let mutation = use_cluster_mutation();
|
|
|
|
if self.locked {
|
|
return settings_row_disabled(
|
|
IconType::Folder,
|
|
"Dedicated Directory",
|
|
"Vanilla and modded instances always run in their own folder, so their worlds, settings and packs stay separate.",
|
|
toggle_controlled(true, (|()| {}).into()),
|
|
)
|
|
.into_element();
|
|
}
|
|
|
|
let on_toggle: EventHandler<()> = (move |()| {
|
|
mutation.mutate(ClusterAction::SetDedicatedDir {
|
|
cluster_id,
|
|
dedicated: !dedicated,
|
|
});
|
|
})
|
|
.into();
|
|
|
|
settings_row(
|
|
IconType::Folder,
|
|
"Dedicated Directory",
|
|
"Run this cluster in its own .minecraft instead of the shared one.",
|
|
toggle_controlled(dedicated, on_toggle),
|
|
)
|
|
.into_element()
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct MemoryRow {
|
|
cluster_id: i64,
|
|
value: Option<u32>,
|
|
global: u32,
|
|
}
|
|
|
|
impl Component for MemoryRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let overridden = self.value.is_some();
|
|
let dispatch = use_dispatch();
|
|
|
|
let global = self.global;
|
|
let initial = self.value.map(|v| v.to_string()).unwrap_or_default();
|
|
let mut memory = use_state({
|
|
let v = initial.clone();
|
|
move || v
|
|
});
|
|
let mut last = use_state(move || initial);
|
|
{
|
|
let dispatch = dispatch.clone();
|
|
use_side_effect(move || {
|
|
let raw = memory.read().clone();
|
|
if raw == *last.peek() {
|
|
return;
|
|
}
|
|
last.set(raw.clone());
|
|
let mem_max = match raw.trim() {
|
|
"" => Patch::Clear,
|
|
m => m.parse::<u32>().map(Patch::Set).unwrap_or(Patch::Unchanged),
|
|
};
|
|
dispatch.update_cluster_profile(
|
|
cluster_id,
|
|
ProfileUpdate {
|
|
mem_max,
|
|
..Default::default()
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
let on_reset: EventHandler<()> = (move |()| {
|
|
last.set(String::new());
|
|
memory.set(String::new());
|
|
dispatch.update_cluster_profile(cluster_id, clear_update(Field::MemMax));
|
|
})
|
|
.into();
|
|
|
|
let control = memory_field(memory, "Global", global);
|
|
|
|
settings_row(
|
|
IconType::Database01,
|
|
"Memory",
|
|
"The amount of memory in megabytes allocated for the game. Presets leave 2 GB for the system.",
|
|
override_cell(control, overridden, on_reset),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct ResolutionRow {
|
|
cluster_id: i64,
|
|
value: Option<Resolution>,
|
|
global: Resolution,
|
|
}
|
|
|
|
impl Component for ResolutionRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let overridden = self.value.is_some();
|
|
let dispatch = use_dispatch();
|
|
let global = self.global;
|
|
let resolved = self.value.unwrap_or(self.global);
|
|
|
|
let mut width = use_state({
|
|
let v = resolved.width.to_string();
|
|
move || v
|
|
});
|
|
let mut height = use_state({
|
|
let v = resolved.height.to_string();
|
|
move || v
|
|
});
|
|
let mut last = use_state(move || (resolved.width.to_string(), resolved.height.to_string()));
|
|
{
|
|
let dispatch = dispatch.clone();
|
|
use_side_effect(move || {
|
|
let w = width.read().clone();
|
|
let h = height.read().clone();
|
|
if (w.clone(), h.clone()) == *last.peek() {
|
|
return;
|
|
}
|
|
last.set((w.clone(), h.clone()));
|
|
let resolution = match (w.trim(), h.trim()) {
|
|
("", "") => Patch::Clear,
|
|
(w, h) => match (w.parse::<u32>(), h.parse::<u32>()) {
|
|
(Ok(w), Ok(h)) => Patch::Set(Resolution::new(w, h)),
|
|
_ => Patch::Unchanged,
|
|
},
|
|
};
|
|
dispatch.update_cluster_profile(
|
|
cluster_id,
|
|
ProfileUpdate {
|
|
resolution,
|
|
..Default::default()
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
let on_reset: EventHandler<()> = (move |()| {
|
|
let gw = global.width.to_string();
|
|
let gh = global.height.to_string();
|
|
last.set((gw.clone(), gh.clone()));
|
|
width.set(gw);
|
|
height.set(gh);
|
|
dispatch.update_cluster_profile(cluster_id, clear_update(Field::Resolution));
|
|
})
|
|
.into();
|
|
|
|
let control = rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(8.)
|
|
.child(
|
|
TextInput::new(width)
|
|
.placeholder("854")
|
|
.on_validate(validate_number)
|
|
.text_align(TextAlign::Center)
|
|
.width(Size::px(70.)),
|
|
)
|
|
.child(
|
|
Icon::new(IconType::X)
|
|
.size(14.)
|
|
.color(colors::fg_secondary()),
|
|
)
|
|
.child(
|
|
TextInput::new(height)
|
|
.placeholder("480")
|
|
.on_validate(validate_number)
|
|
.text_align(TextAlign::Center)
|
|
.width(Size::px(70.)),
|
|
);
|
|
|
|
settings_row(
|
|
IconType::LayoutTop,
|
|
"Resolution",
|
|
"The game window resolution in pixels.",
|
|
override_cell(control, overridden, on_reset),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum TextField {
|
|
JvmArgs,
|
|
Pre,
|
|
Wrapper,
|
|
Post,
|
|
}
|
|
|
|
impl TextField {
|
|
fn meta(self) -> (IconType, &'static str, &'static str, &'static str) {
|
|
match self {
|
|
Self::JvmArgs => (
|
|
IconType::Terminal,
|
|
"JVM Arguments",
|
|
"Extra arguments passed to Java. Separate them with spaces; quote values containing spaces.",
|
|
"-XX:+UseG1GC",
|
|
),
|
|
Self::Pre => (
|
|
IconType::FilePlus02,
|
|
"Pre-Launch Command",
|
|
"Command to run before launching the game.",
|
|
"echo 'Game started'",
|
|
),
|
|
Self::Wrapper => (
|
|
IconType::ParagraphWrap,
|
|
"Wrapper Command",
|
|
"Command to run when launching the game.",
|
|
"gamescope",
|
|
),
|
|
Self::Post => (
|
|
IconType::FileX02,
|
|
"Post-Exit Command",
|
|
"Command to run after exiting the game.",
|
|
"echo 'Game exited'",
|
|
),
|
|
}
|
|
}
|
|
|
|
fn value(self, profile: &GameSettingsProfile) -> Option<String> {
|
|
match self {
|
|
Self::JvmArgs => profile.launch_args.clone(),
|
|
Self::Pre => profile.hook_pre.clone(),
|
|
Self::Wrapper => profile.hook_wrapper.clone(),
|
|
Self::Post => profile.hook_post.clone(),
|
|
}
|
|
}
|
|
|
|
fn patch(self, value: Patch<String>) -> ProfileUpdate {
|
|
let mut u = ProfileUpdate::default();
|
|
match self {
|
|
Self::JvmArgs => u.launch_args = value,
|
|
Self::Pre => u.hook_pre = value,
|
|
Self::Wrapper => u.hook_wrapper = value,
|
|
Self::Post => u.hook_post = value,
|
|
}
|
|
u
|
|
}
|
|
}
|
|
|
|
fn text_row(
|
|
cluster_id: i64,
|
|
field: TextField,
|
|
profile: &GameSettingsProfile,
|
|
global: &GameSettingsProfile,
|
|
) -> impl IntoElement {
|
|
TextRow {
|
|
cluster_id,
|
|
field,
|
|
value: field.value(profile),
|
|
global: field.value(global).unwrap_or_default(),
|
|
}
|
|
.into_element()
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct TextRow {
|
|
cluster_id: i64,
|
|
field: TextField,
|
|
value: Option<String>,
|
|
global: String,
|
|
}
|
|
|
|
impl Component for TextRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let field = self.field;
|
|
let overridden = self.value.is_some();
|
|
let (icon, title, description, placeholder) = field.meta();
|
|
let dispatch = use_dispatch();
|
|
|
|
let global = self.global.clone();
|
|
let initial = self.value.clone().unwrap_or_else(|| self.global.clone());
|
|
let mut text = use_state({
|
|
let v = initial.clone();
|
|
move || v
|
|
});
|
|
let mut last = use_state(move || initial);
|
|
{
|
|
let dispatch = dispatch.clone();
|
|
use_side_effect(move || {
|
|
let raw = text.read().clone();
|
|
if raw == *last.peek() {
|
|
return;
|
|
}
|
|
last.set(raw.clone());
|
|
let patch = match raw.trim() {
|
|
"" => Patch::Clear,
|
|
v => Patch::Set(v.to_string()),
|
|
};
|
|
dispatch.update_cluster_profile(cluster_id, field.patch(patch));
|
|
});
|
|
}
|
|
|
|
let on_reset: EventHandler<()> = (move |()| {
|
|
last.set(global.clone());
|
|
text.set(global.clone());
|
|
dispatch.update_cluster_profile(cluster_id, field.patch(Patch::Clear));
|
|
})
|
|
.into();
|
|
|
|
let control = TextInput::new(text)
|
|
.placeholder(placeholder)
|
|
.expandable(true)
|
|
.width(Size::px(220.));
|
|
|
|
settings_row(
|
|
icon,
|
|
title,
|
|
description,
|
|
override_cell(control, overridden, on_reset),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct JavaRow {
|
|
cluster_id: i64,
|
|
value: Option<String>,
|
|
global: Option<String>,
|
|
runtimes: Vec<JavaRuntime>,
|
|
}
|
|
|
|
impl Component for JavaRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let overridden = self.value.is_some();
|
|
let dispatch = use_dispatch();
|
|
let runtimes = self.runtimes.clone();
|
|
|
|
let resolved = self.value.clone().or_else(|| self.global.clone());
|
|
|
|
let control: Element = if runtimes.is_empty() {
|
|
label()
|
|
.text("No runtimes installed")
|
|
.font_size(12.)
|
|
.color(colors::fg_secondary())
|
|
.into_element()
|
|
} else {
|
|
let mut options: Vec<String> = vec!["Automatic".into()];
|
|
options.extend(runtimes.iter().map(runtime_label));
|
|
|
|
let selected = resolved
|
|
.as_ref()
|
|
.and_then(|path| runtimes.iter().find(|r| &r.absolute_path == path))
|
|
.map(runtime_label)
|
|
.unwrap_or_else(|| "Automatic".into());
|
|
|
|
let paths: Vec<String> = runtimes.iter().map(|r| r.absolute_path.clone()).collect();
|
|
|
|
let dispatch = dispatch.clone();
|
|
Dropdown::new(selected, options)
|
|
.width(Size::px(260.))
|
|
.height(Size::px(34.))
|
|
.on_select(move |idx: usize| {
|
|
if idx == 0 {
|
|
dispatch.update_cluster_profile(cluster_id, clear_update(Field::JavaPath));
|
|
} else if let Some(path) = paths.get(idx - 1) {
|
|
dispatch.update_cluster_profile(
|
|
cluster_id,
|
|
ProfileUpdate {
|
|
java_path: Patch::Set(path.clone()),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
})
|
|
.into_element()
|
|
};
|
|
|
|
let on_reset: EventHandler<()> = (move |()| {
|
|
dispatch.update_cluster_profile(cluster_id, clear_update(Field::JavaPath));
|
|
})
|
|
.into();
|
|
|
|
settings_row(
|
|
IconType::CodeSnippet02,
|
|
"Java Runtime",
|
|
"The Java runtime used to launch this cluster.",
|
|
override_cell(control, overridden, on_reset),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct BrowserUpdateModeRow {
|
|
cluster_id: i64,
|
|
value: Option<PackageUpdateMode>,
|
|
global: PackageUpdateMode,
|
|
}
|
|
|
|
impl Component for BrowserUpdateModeRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let overridden = self.value.is_some();
|
|
let resolved = self.value.unwrap_or(self.global);
|
|
let dispatch = use_dispatch();
|
|
|
|
let options: Vec<String> = PackageUpdateMode::ALL
|
|
.iter()
|
|
.map(|mode| mode.label().to_string())
|
|
.collect();
|
|
|
|
let control = {
|
|
let dispatch = dispatch.clone();
|
|
Dropdown::new(resolved.label(), options)
|
|
.width(Size::px(220.))
|
|
.height(Size::px(34.))
|
|
.on_select(move |idx: usize| {
|
|
if let Some(mode) = PackageUpdateMode::ALL.get(idx).copied() {
|
|
dispatch.update_cluster_profile(
|
|
cluster_id,
|
|
ProfileUpdate {
|
|
browser_update_mode: Patch::Set(mode),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
})
|
|
};
|
|
|
|
let on_reset: EventHandler<()> = (move |()| {
|
|
dispatch.update_cluster_profile(cluster_id, clear_update(Field::BrowserUpdateMode));
|
|
})
|
|
.into();
|
|
|
|
settings_row(
|
|
IconType::RefreshCw01,
|
|
"Browser Package Updates",
|
|
"What to do when content installed from the browser has a newer version. Bundle content is not affected.",
|
|
override_cell(control, overridden, on_reset),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct MigrateFromRow {
|
|
cluster_id: i64,
|
|
}
|
|
|
|
impl Component for MigrateFromRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let dispatch = use_dispatch();
|
|
let checking = use_release_migration_checking(cluster_id);
|
|
let running = use_game_snapshot().is_running(cluster_id);
|
|
let clusters = settled_or_loading(&use_clusters()).unwrap_or_default();
|
|
let mut picked = use_state(|| None::<i64>);
|
|
|
|
let sources = clusters
|
|
.iter()
|
|
.find(|cluster| cluster.id == cluster_id)
|
|
.map(|target| rank_migration_sources(target, &clusters))
|
|
.unwrap_or_default();
|
|
|
|
let selected = picked
|
|
.read()
|
|
.and_then(|id| sources.iter().find(|source| source.id == id))
|
|
.or_else(|| sources.first())
|
|
.cloned();
|
|
let selected_id = selected.as_ref().map(|source| source.id);
|
|
|
|
let mut button = Button::new()
|
|
.secondary()
|
|
.enabled(selected_id.is_some() && !checking && !running)
|
|
.on_press(move |_| {
|
|
if let Some(source_id) = selected_id {
|
|
dispatch.open_manual_migration(cluster_id, source_id);
|
|
}
|
|
});
|
|
button = if checking {
|
|
button
|
|
.child(Icon::new(IconType::Loading02).size(14.))
|
|
.text("Checking…")
|
|
} else {
|
|
button
|
|
.child(Icon::new(IconType::Copy01).size(14.))
|
|
.text("Migrate...")
|
|
};
|
|
if running {
|
|
button = button.tooltip("Close the game before migrating");
|
|
}
|
|
|
|
let picker: Element = if sources.is_empty() {
|
|
label()
|
|
.text("No other clusters with this loader")
|
|
.font_size(12.)
|
|
.color(colors::fg_secondary())
|
|
.into_element()
|
|
} else {
|
|
let ids: Vec<i64> = sources.iter().map(|source| source.id).collect();
|
|
let names: Vec<String> = sources.iter().map(|source| source.name.clone()).collect();
|
|
Dropdown::new(
|
|
selected.map(|source| source.name).unwrap_or_default(),
|
|
names,
|
|
)
|
|
.width(Size::px(180.))
|
|
.height(Size::px(34.))
|
|
.on_select(move |index: usize| {
|
|
if let Some(id) = ids.get(index) {
|
|
picked.set(Some(*id));
|
|
}
|
|
})
|
|
.into_element()
|
|
};
|
|
|
|
settings_row(
|
|
IconType::Copy01,
|
|
"Migrate",
|
|
"Bring mods, resource packs and shaders you installed in another cluster. Compatible versions are downloaded; the other cluster is left as it is.",
|
|
rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(8.)
|
|
.child(picker)
|
|
.child(button),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
struct LoaderRow {
|
|
cluster_id: i64,
|
|
loader: GameLoader,
|
|
selected: Option<String>,
|
|
versions: Arc<[String]>,
|
|
locked: bool,
|
|
}
|
|
|
|
impl Component for LoaderRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let cluster_id = self.cluster_id;
|
|
let dispatch = use_dispatch();
|
|
let versions = self.versions.clone();
|
|
let selected = self
|
|
.selected
|
|
.clone()
|
|
.unwrap_or_else(|| versions.first().cloned().unwrap_or_else(|| "Latest".into()));
|
|
|
|
if self.locked {
|
|
return settings_row_disabled(
|
|
IconType::Rocket02,
|
|
"Loader Version",
|
|
"Set when the instance was created.",
|
|
label()
|
|
.text(format!("{} {selected}", self.loader))
|
|
.font_size(12.)
|
|
.color(colors::fg_secondary()),
|
|
)
|
|
.into_element();
|
|
}
|
|
|
|
let control: Element = if versions.is_empty() {
|
|
label()
|
|
.text("No versions available")
|
|
.font_size(12.)
|
|
.color(colors::fg_secondary())
|
|
.into_element()
|
|
} else {
|
|
let options = versions.to_vec();
|
|
Dropdown::new(selected, options.clone())
|
|
.width(Size::px(220.))
|
|
.height(Size::px(34.))
|
|
.on_select(move |idx: usize| {
|
|
if let Some(version) = options.get(idx) {
|
|
dispatch.set_cluster_loader_version(cluster_id, version.clone());
|
|
}
|
|
})
|
|
.into_element()
|
|
};
|
|
|
|
settings_row(
|
|
IconType::Rocket02,
|
|
"Loader Version",
|
|
format!(
|
|
"The {} loader version used to launch this cluster.",
|
|
self.loader
|
|
),
|
|
control,
|
|
)
|
|
.into_element()
|
|
}
|
|
}
|