Merge pull request #4490 from horizon-rust/feat/launcher-night-mods

🤖 Agent: Launcher redesign R7b (the instance's Mods tab), approved by Ludovic after the screenshots. main merged in; full pr_ready green, verify and launcher CI green on this head. The only comment is the overlap bot's note on #4478 (different change; not landed).
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-10-30 17:39:06 +01:00
commit 8f376a4712
13 files changed
+540 -25

No files matched your search

+6
View File
@@ -267,6 +267,12 @@ Light); `tokens.css` carries them as `--night-*`. The game's menus don't use it.
A tab names how many it holds ("Worlds 3"); its own buttons (Add server, Open folder) sit at the
strip's end, where a list's header would put them (`HORIZON_LAUNCHER_PAGE=worlds`, `packs`...
opens one for screenshots).
- **The Mods tab** (`launcher/src/instance_mods.rs`): the instance's mods on one nearly opaque
card, its loader in the header ("Fabric mods", "Horizon mods") beside a filter. A row is a
coloured square with the mod's initial, its name and version, where it comes from (Modrinth,
"needed by another mod", or a local file by name), an update as an amber pill when the browser
found one, and a switch (a Java jar turned off is renamed `.disabled`). Update all and Add mods
sit at the strip's end (`HORIZON_LAUNCHER_PAGE=instance-mods`).
- **The sign-in sheet** (`launcher/src/owner_page.rs`, the owner gate): an opaque plate over the
dimmed launcher, never instead of it. It says why (the account that owns Java Edition, no install
needed), the code in mono on a raised box with Copy, and Open link as the main action; the window
+12 -1
View File
@@ -223,5 +223,16 @@
"Multiplayer": "Mehrspieler",
"Packs": "Pakete",
"Screenshots": "Screenshots",
"Console": "Konsole"
"Console": "Konsole",
"Update all ({count})": "Alle aktualisieren ({count})",
"Add mods": "Mods hinzufügen",
"{loader} mods": "{loader}-Mods",
"Horizon mods": "Horizon-Mods",
"Filter installed": "Installierte filtern",
"Reading the mods…": "Mods werden gelesen…",
"No mods yet: Add mods finds some for this instance.": "Noch keine Mods: „Mods hinzufügen“ findet welche für diese Instanz.",
"No mod matches.": "Kein Mod passt.",
"{source} · needed by another mod": "{source} · von einem anderen Mod benötigt",
"Local file · {file}": "Lokale Datei · {file}",
"Update {version}": "Update {version}"
}
+12 -1
View File
@@ -223,5 +223,16 @@
"Multiplayer": "Multijugador",
"Packs": "Paquetes",
"Screenshots": "Capturas",
"Console": "Consola"
"Console": "Consola",
"Update all ({count})": "Actualizar todo ({count})",
"Add mods": "Añadir mods",
"{loader} mods": "Mods de {loader}",
"Horizon mods": "Mods de Horizon",
"Filter installed": "Filtrar instalados",
"Reading the mods…": "Leyendo los mods…",
"No mods yet: Add mods finds some for this instance.": "Aún no hay mods: Añadir mods encuentra algunos para esta instancia.",
"No mod matches.": "Ningún mod coincide.",
"{source} · needed by another mod": "{source} · lo necesita otro mod",
"Local file · {file}": "Archivo local · {file}",
"Update {version}": "Actualizar a {version}"
}
+12 -1
View File
@@ -223,5 +223,16 @@
"Multiplayer": "Multijoueur",
"Packs": "Packs",
"Screenshots": "Captures d'écran",
"Console": "Console"
"Console": "Console",
"Update all ({count})": "Tout mettre à jour ({count})",
"Add mods": "Ajouter des mods",
"{loader} mods": "Mods {loader}",
"Horizon mods": "Mods Horizon",
"Filter installed": "Filtrer les mods installés",
"Reading the mods…": "Lecture des mods…",
"No mods yet: Add mods finds some for this instance.": "Pas encore de mods : Ajouter des mods en trouve pour cette instance.",
"No mod matches.": "Aucun mod ne correspond.",
"{source} · needed by another mod": "{source} · requis par un autre mod",
"Local file · {file}": "Fichier local · {file}",
"Update {version}": "Mettre à jour : {version}"
}
+12 -1
View File
@@ -223,5 +223,16 @@
"Multiplayer": "Gra wieloosobowa",
"Packs": "Paczki",
"Screenshots": "Zrzuty ekranu",
"Console": "Konsola"
"Console": "Konsola",
"Update all ({count})": "Zaktualizuj wszystko ({count})",
"Add mods": "Dodaj mody",
"{loader} mods": "Mody {loader}",
"Horizon mods": "Mody Horizon",
"Filter installed": "Filtruj zainstalowane",
"Reading the mods…": "Odczytywanie modów…",
"No mods yet: Add mods finds some for this instance.": "Brak modów: Dodaj mody znajdzie jakieś dla tej instancji.",
"No mod matches.": "Żaden mod nie pasuje.",
"{source} · needed by another mod": "{source} · wymagany przez inny mod",
"Local file · {file}": "Plik lokalny · {file}",
"Update {version}": "Aktualizuj do {version}"
}
+12 -1
View File
@@ -223,5 +223,16 @@
"Multiplayer": "Сетевая игра",
"Packs": "Наборы",
"Screenshots": "Скриншоты",
"Console": "Консоль"
"Console": "Консоль",
"Update all ({count})": "Обновить все ({count})",
"Add mods": "Добавить моды",
"{loader} mods": "Моды {loader}",
"Horizon mods": "Моды Horizon",
"Filter installed": "Фильтр установленных",
"Reading the mods…": "Чтение модов…",
"No mods yet: Add mods finds some for this instance.": "Модов пока нет: «Добавить моды» найдёт их для этой сборки.",
"No mod matches.": "Нет подходящих модов.",
"{source} · needed by another mod": "{source} · нужен другому моду",
"Local file · {file}": "Локальный файл · {file}",
"Update {version}": "Обновить до {version}"
}
+12 -1
View File
@@ -223,5 +223,16 @@
"Multiplayer": "Мережева гра",
"Packs": "Набори",
"Screenshots": "Знімки екрана",
"Console": "Консоль"
"Console": "Консоль",
"Update all ({count})": "Оновити все ({count})",
"Add mods": "Додати моди",
"{loader} mods": "Моди {loader}",
"Horizon mods": "Моди Horizon",
"Filter installed": "Фільтр встановлених",
"Reading the mods…": "Читання модів…",
"No mods yet: Add mods finds some for this instance.": "Модів поки немає: «Додати моди» знайде їх для цієї збірки.",
"No mod matches.": "Немає відповідних модів.",
"{source} · needed by another mod": "{source} · потрібен іншому моду",
"Local file · {file}": "Локальний файл · {file}",
"Update {version}": "Оновити до {version}"
}
+12 -1
View File
@@ -223,5 +223,16 @@
"Multiplayer": "多人游戏",
"Packs": "资源包",
"Screenshots": "截图",
"Console": "控制台"
"Console": "控制台",
"Update all ({count})": "全部更新({count})",
"Add mods": "添加模组",
"{loader} mods": "{loader} 模组",
"Horizon mods": "Horizon 模组",
"Filter installed": "筛选已安装",
"Reading the mods…": "正在读取模组…",
"No mods yet: Add mods finds some for this instance.": "还没有模组:点击“添加模组”为此实例查找。",
"No mod matches.": "没有匹配的模组。",
"{source} · needed by another mod": "{source} · 其他模组需要",
"Local file · {file}": "本地文件 · {file}",
"Update {version}": "更新到 {version}"
}
+12 -2
View File
@@ -7,6 +7,7 @@ use super::*;
pub(super) enum HomeTab {
#[default]
Overview,
Mods,
Worlds,
Multiplayer,
Packs,
@@ -15,10 +16,11 @@ pub(super) enum HomeTab {
}
impl HomeTab {
/// `HORIZON_LAUNCHER_PAGE=worlds` (or `multiplayer`, `packs`, `screenshots`, `console`) opens
/// Home on that tab, for screenshots.
/// `HORIZON_LAUNCHER_PAGE=worlds` (or `instance-mods`, `multiplayer`, `packs`, `screenshots`,
/// `console`) opens Home on that tab, for screenshots.
pub(super) fn from_env() -> Self {
match std::env::var("HORIZON_LAUNCHER_PAGE").as_deref() {
Ok("instance-mods") => HomeTab::Mods,
Ok("worlds") => HomeTab::Worlds,
Ok("multiplayer") => HomeTab::Multiplayer,
Ok("packs") => HomeTab::Packs,
@@ -37,6 +39,9 @@ impl Launcher {
if self.selected_instance().is_none() {
return tabs;
}
if self.has_mods_tab() {
tabs.push((HomeTab::Mods, self.instance_mods_count(cx)));
}
tabs.push((HomeTab::Worlds, self.worlds_count(cx)));
tabs.push((HomeTab::Multiplayer, Some(self.servers_count())));
tabs.push((HomeTab::Packs, Some(self.packs_count())));
@@ -67,6 +72,7 @@ impl Launcher {
.children(tabs.into_iter().map(|(tab, count)| {
let name = match tab {
HomeTab::Overview => tr("Overview"),
HomeTab::Mods => tr("Mods"),
HomeTab::Worlds => tr("Worlds"),
HomeTab::Multiplayer => tr("Multiplayer"),
HomeTab::Packs => tr("Packs"),
@@ -87,6 +93,10 @@ impl Launcher {
// The open tab's own buttons sit at the strip's end, as a list's header would.
let (actions, body) = match open {
HomeTab::Overview => (None, Some(overview.into_any_element())),
HomeTab::Mods => {
let (actions, body) = split(self.instance_mods_tab(cx));
(Some(actions), Some(body))
}
HomeTab::Worlds => (
None,
self.worlds_section(cx).map(IntoElement::into_any_element),
+405
View File
@@ -0,0 +1,405 @@
//! Home's Mods tab (docs/design-system.md, "Night"; plan R0, screens 5 and 6): every mod of the
//! selected instance, the browser's and those put in its folder by hand, with its version, where
//! it comes from, an update when the browser found one, and a switch. A Java instance turns a mod
//! off by renaming its jar (`.disabled`, as `java_instance` does); a Horizon instance's mods are
//! the registry's, switched as the game's Options > Mods does. Adding mods is the Mods page's.
use super::*;
use crate::plate::alpha;
use crate::shell::{c, Action};
use horizon_launcher::browser::{horizon, installed, java, Kind as PackKind, SourceId};
use servers_page::Field;
use std::path::{Path, PathBuf};
use theme::night;
/// A turned-off jar's suffix, which the loaders don't read.
const OFF: &str = ".disabled";
/// A mod as the tab lists it.
#[derive(Clone, Debug, PartialEq)]
pub(super) struct ModRow {
/// Its file in the instance, as it is when on (`mods/lumen-0.6.2.jar`).
file: String,
name: String,
version: String,
on: bool,
/// What the browser knows of it; None: a file put in by hand.
entry: Option<installed::Entry>,
}
#[derive(Default)]
pub(super) struct InstanceMods {
home: Option<PathBuf>,
fresh: bool,
reading: bool,
list: Vec<ModRow>,
/// The browser was installing at the last draw: its end changes the list.
was_busy: bool,
}
/// The mods of the instance at `home`: a Java one's jars in `mods/` (on or off), a Horizon one's
/// registry mods. Each jar names itself (`fabric.mod.json`, `quilt.mod.json`...), else its file.
fn read(home: &Path, java_instance: bool) -> Vec<ModRow> {
let entries: Vec<installed::Entry> = installed::Installed::load(home)
.entries
.into_iter()
.filter(|e| e.kind == PackKind::Mod)
.collect();
let mut rows: Vec<ModRow> = if java_instance {
let Ok(dir) = std::fs::read_dir(home.join("mods")) else {
return Vec::new();
};
dir.flatten()
.filter_map(|f| {
let name = f.file_name().to_string_lossy().into_owned();
let on = name.ends_with(".jar");
let jar = name.strip_suffix(OFF).unwrap_or(&name);
(jar.ends_with(".jar") && f.path().is_file()).then(|| {
let file = format!("mods/{jar}");
let entry = entries.iter().find(|e| e.file == file).cloned();
let (name, version) = match &entry {
Some(e) => (e.title.clone(), e.number.clone()),
None => horizon_port_pack::jar::read(&f.path())
.map(|j| (j.name, j.version))
.unwrap_or_else(|_| {
(jar.trim_end_matches(".jar").into(), String::new())
}),
};
ModRow {
file,
name,
version,
on,
entry,
}
})
})
.collect()
} else {
entries
.into_iter()
.map(|e| ModRow {
file: e.file.clone(),
name: e.title.clone(),
version: e.number.clone(),
on: horizon::mod_id(&e).is_some_and(|id| java::is_enabled(home, id)),
entry: Some(e),
})
.collect()
};
rows.sort_by_key(|r| r.name.to_lowercase());
rows
}
/// Turns a Java instance's jar on or off by renaming it.
fn switch_jar(home: &Path, file: &str, on: bool) -> Result<(), String> {
let (jar, off) = (home.join(file), home.join(format!("{file}{OFF}")));
let (from, to) = if on { (off, jar) } else { (jar, off) };
std::fs::rename(&from, &to).map_err(|e| format!("{}: {e}", from.display()))
}
impl Launcher {
/// Whether the selected instance has mods to show: Horizon's always, a Java one with a loader.
pub(super) fn has_mods_tab(&self) -> bool {
self.selected_instance()
.is_some_and(|i| i.java.as_ref().is_none_or(|j| j.loader.is_some()))
}
/// Reads the selected instance's mods off the UI thread, once until something changes them.
fn load_instance_mods(&mut self, cx: &mut Context<Self>) {
let Some(i) = self.selected_instance() else {
return;
};
let (home, java_instance) = (i.home.clone(), i.java.is_some());
let m = &mut self.instance_mods;
if m.home.as_ref() != Some(&home) {
*m = InstanceMods {
home: Some(home.clone()),
..InstanceMods::default()
};
}
let busy = self.mods.busy.is_some();
if m.was_busy && !busy {
m.fresh = false;
}
m.was_busy = busy;
if m.fresh || m.reading {
return;
}
m.reading = true;
let task = cx
.background_executor()
.spawn(async move { (home.clone(), read(&home, java_instance)) });
cx.spawn(async move |this, cx| {
let (home, list) = task.await;
let _ = this.update(cx, |this, cx| {
let m = &mut this.instance_mods;
m.reading = false;
if m.home.as_ref() == Some(&home) {
m.list = list;
m.fresh = true;
}
cx.notify();
});
})
.detach();
}
/// How many mods the selected instance has; None while they are read.
pub(super) fn instance_mods_count(&mut self, cx: &mut Context<Self>) -> Option<usize> {
self.mods_load();
self.load_instance_mods(cx);
let m = &self.instance_mods;
m.fresh.then_some(m.list.len())
}
fn switch_mod(&mut self, row: &ModRow) {
let home = self.instance_mods.home.clone().unwrap_or_default();
let java_instance = self.selected_instance().is_some_and(|i| i.java.is_some());
match &row.entry {
Some(e) if !java_instance => self.mods_switch(e),
_ => {
self.mods.message = switch_jar(&home, &row.file, !row.on).err().map(Err);
}
}
self.instance_mods.fresh = false;
}
/// The Mods tab: its buttons (beside the tabs) and the list.
pub(super) fn instance_mods_tab(&mut self, cx: &mut Context<Self>) -> (Div, Div) {
let t = self.theme;
let n = t.night;
let busy = self.mods.busy.is_some();
let updates = self.mods.updates.len();
let actions = div()
.flex()
.items_center()
.gap(px(8.))
.children((updates > 0 && !busy).then(|| {
shell::action("instance-mods-update-all", Action::Raised, &t)
.child(shell::label(trf(
"Update all ({count})",
&[("count", &updates.to_string())],
)))
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
this.mods_update_all();
cx.notify();
}))
}))
.child(
shell::action("instance-mods-add", Action::Raised, &t)
.child(shell::label(tr("Add mods").to_string()))
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
this.go(Page::Mods);
cx.notify();
})),
);
let loader = self
.selected_instance()
.and_then(|i| i.java.as_ref())
.map(|j| j.loader.as_ref().map(|l| l.kind.name()));
let title = match loader {
Some(Some(loader)) => trf("{loader} mods", &[("loader", loader)]),
_ => tr("Horizon mods").to_string(),
};
let filter = self
.servers
.value(Field::InstanceModsFilter)
.trim()
.to_lowercase();
let field = self.field(Field::InstanceModsFilter, "Filter installed", 240., cx);
let m = &self.instance_mods;
let rows: Vec<ModRow> = m
.list
.iter()
.filter(|r| filter.is_empty() || r.name.to_lowercase().contains(&filter))
.cloned()
.collect();
let header = div()
.flex()
.items_center()
.justify_between()
.gap(px(16.))
.px(px(18.))
.py(px(14.))
.child(
shell::text(night::UI, c(n.ink))
.font_weight(FontWeight::SEMIBOLD)
.child(title),
)
.child(field);
let empty = |s: &str| {
shell::text(night::UI, c(n.muted))
.px(px(18.))
.py(px(16.))
.child(tr(s).to_string())
};
let body: Vec<AnyElement> = if !m.fresh {
vec![empty("Reading the mods…").into_any_element()]
} else if m.list.is_empty() {
vec![empty("No mods yet: Add mods finds some for this instance.").into_any_element()]
} else if rows.is_empty() {
vec![empty("No mod matches.").into_any_element()]
} else {
rows.into_iter()
.map(|row| self.mod_row(row, busy, cx).into_any_element())
.collect()
};
let card = plate::cut(
night::PIXEL_CORNER,
2,
// Nearly opaque: a list reads over the sunset.
Some(alpha(n.card, 0.94)),
Some((alpha(n.stroke, n.stroke_alpha), 1.)),
)
.flex()
.flex_col()
.child(header)
.children(body.into_iter().map(|row| {
div()
.border_t_1()
.border_color(alpha(n.stroke, n.rule_alpha))
.child(row)
}));
(
actions,
div()
.flex()
.flex_col()
.gap(px(12.))
.children(self.mods_busy_plate())
.child(card),
)
}
fn mod_row(&self, row: ModRow, busy: bool, cx: &mut Context<Self>) -> Div {
let t = self.theme;
let n = t.night;
// Each mod its own colour, the same every time: a hue from its name.
let hue = row
.name
.bytes()
.fold(7u32, |h, b| h.wrapping_mul(31).wrapping_add(b as u32));
let icon = div()
.flex()
.flex_none()
.items_center()
.justify_center()
.size(px(40.))
.rounded(px(night::RADIUS_CONTROL))
.bg(gpui::hsla((hue % 360) as f32 / 360., 0.42, 0.52, 1.))
.child(
shell::text(night::TITLE, c(n.on_accent))
.font_weight(FontWeight::BOLD)
.child(
row.name
.chars()
.next()
.unwrap_or('?')
.to_uppercase()
.to_string(),
),
);
let source = match &row.entry {
Some(e) if e.dependency => trf(
"{source} · needed by another mod",
&[("source", e.source.label())],
),
Some(e) => e.source.label().to_string(),
None => trf(
"Local file · {file}",
&[("file", row.file.trim_start_matches("mods/"))],
),
};
let update = row.entry.as_ref().and_then(|e| {
let key = (e.source, e.project.clone());
let number = self.mods.updates.get(&key)?.clone();
let (source, id): (SourceId, String) = key;
let chip = div()
.id(SharedString::from(format!("mod-update-{id}")))
.flex()
.flex_none()
.items_center()
.h(px(24.))
.px(px(10.))
.rounded(px(night::RADIUS_CONTROL))
.bg(alpha(n.warn, 0.16))
.cursor_pointer()
.hover(move |s| s.bg(alpha(n.warn, 0.26)))
.child(
shell::text(night::CAPTION, c(n.warn))
.font_weight(FontWeight::SEMIBOLD)
.child(trf("Update {version}", &[("version", &number)])),
);
Some(match busy {
true => chip.opacity(theme::DISABLED),
false => chip.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
this.mods_install(source, id.clone(), None);
cx.notify();
})),
})
});
let switch_row = row.clone();
div()
.flex()
.items_center()
.gap(px(14.))
.px(px(18.))
.py(px(12.))
.child(icon)
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.gap(px(2.))
.child(
div()
.flex()
.items_baseline()
.gap(px(6.))
.child(
shell::text(night::UI, c(n.ink))
.font_weight(FontWeight::SEMIBOLD)
.child(row.name.clone()),
)
.child(shell::text(night::CAPTION, c(n.muted)).child(row.version)),
)
.child(
shell::text(night::CAPTION, c(n.muted))
.truncate()
.child(source),
),
)
.children(update)
.child(
switch(format!("mod-switch-{}", row.file), row.on, &t).on_click(cx.listener(
move |this, _: &ClickEvent, _, cx| {
this.switch_mod(&switch_row);
cx.notify();
},
)),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_java_instance_lists_its_jars_on_and_off() {
let home = std::env::temp_dir().join(format!("instance-mods-{}", std::process::id()));
let mods = home.join("mods");
std::fs::create_dir_all(&mods).unwrap();
std::fs::write(mods.join("zeta-1.0.jar"), b"not a zip").unwrap();
std::fs::write(mods.join("alpha-2.0.jar.disabled"), b"not a zip").unwrap();
std::fs::write(mods.join("notes.txt"), b"").unwrap();
let rows = read(&home, true);
let seen: Vec<_> = rows.iter().map(|r| (r.name.as_str(), r.on)).collect();
assert_eq!(seen, [("alpha-2.0", false), ("zeta-1.0", true)]);
switch_jar(&home, "mods/alpha-2.0.jar", true).unwrap();
assert!(read(&home, true).iter().all(|r| r.on));
let _ = std::fs::remove_dir_all(&home);
}
}
+4
View File
@@ -19,6 +19,7 @@ mod game_tools;
mod home;
mod home_tabs;
mod import_page;
mod instance_mods;
mod instances_page;
mod internet;
mod language_page;
@@ -276,6 +277,8 @@ struct Launcher {
download_seen: Option<(Instant, usize)>,
/// The tab open under Home's hero (`home_tabs.rs`).
home_tab: home_tabs::HomeTab,
/// The selected instance's mods, for Home's Mods tab (`instance_mods.rs`).
instance_mods: instance_mods::InstanceMods,
play_pressed: Option<Instant>,
/// `horizon-launcher benchmark` started this window to measure it.
bench: Option<perf::Bench>,
@@ -480,6 +483,7 @@ impl Launcher {
prepare_started: None,
download_seen: None,
home_tab: home_tabs::HomeTab::from_env(),
instance_mods: Default::default(),
play_pressed: None,
bench: perf::Bench::from_env(),
packs: Vec::new(),
+24 -15
View File
@@ -73,8 +73,10 @@ pub(super) struct Mods {
pub(super) report_list: cull::Cull,
more: bool,
error: Option<String>,
installed: Vec<installed::Entry>,
pub(super) installed: Vec<installed::Entry>,
loaded: bool,
/// Whether the page searched for the instance as it is (its version and loader).
asked: bool,
/// The game folder the browser installs into and lists: the selected instance's.
home: Option<PathBuf>,
/// The selected Java instance's version and loader: its mods and resource packs are the
@@ -85,8 +87,8 @@ pub(super) struct Mods {
world: Option<PathBuf>,
/// The project being installed, and the line and fraction of its download.
pub(super) busy: Option<(String, String, f32)>,
message: Option<Result<String, String>>,
updates: HashMap<(SourceId, String), String>,
pub(super) message: Option<Result<String, String>>,
pub(super) updates: HashMap<(SourceId, String), String>,
confirm_remove: Option<String>,
/// A native mod's Install waiting for the one-time notice (`native_mods::needs_notice`).
native_notice: Option<(SourceId, String, Option<Project>)>,
@@ -166,6 +168,7 @@ impl Mods {
error: None,
installed: Vec::new(),
loaded: false,
asked: false,
home: None,
java: None,
worlds: Vec::new(),
@@ -465,7 +468,7 @@ impl Launcher {
/// Turns a Horizon mod the browser installed on or off, as the game's Options > Mods does;
/// in a Java instance, renames its file.
fn mods_switch(&mut self, e: &installed::Entry) {
pub(super) fn mods_switch(&mut self, e: &installed::Entry) {
let home = self.mods.home();
if self.mods.java.is_some() {
let on = !java_instance::is_on(&home, e);
@@ -580,7 +583,7 @@ impl Launcher {
/// Updates every installed pack a newer version was found for, one after the other on a
/// thread, each checked as an install is. One failing doesn't stop the others.
fn mods_update_all(&mut self) {
pub(super) fn mods_update_all(&mut self) {
if self.mods.busy.is_some() || self.mods.updates.is_empty() {
return;
}
@@ -827,18 +830,18 @@ impl Launcher {
line_edit::mouse(field, cx, |this| &mut this.mods.text)
}
pub(super) fn mods_page(&mut self, cx: &mut Context<Self>) -> Div {
let t = self.theme;
// Packs and mods go into the selected instance: picking another reads its list.
/// Reads what the browser installed in the selected instance (once per instance) and looks
/// for updates: the Mods page and Home's Mods tab (`instance_mods.rs`) show it. Packs and mods
/// go into the selected instance: picking another reads its list.
pub(super) fn mods_load(&mut self) {
let home = self.selected_instance().map(|i| i.home.clone());
let java = self
.selected_instance()
.and_then(|i| i.java.as_ref())
.map(|j| j.target());
// Another version or loader: other results.
if self.mods.loaded && java != self.mods.java {
self.mods.java = java.clone();
self.mods_search(0);
// Another version or loader: other results, on the page's next draw.
if java != self.mods.java {
self.mods.asked = false;
}
self.mods.java = java;
if !self.mods.loaded || (home.is_some() && self.mods.home != home) {
@@ -850,11 +853,17 @@ impl Launcher {
self.mods.read_worlds();
self.mods_check_updates();
self.mods_taken_down();
if !self.mods.loaded {
self.mods_search(0);
}
self.mods.loaded = true;
}
}
pub(super) fn mods_page(&mut self, cx: &mut Context<Self>) -> Div {
let t = self.theme;
self.mods_load();
if !self.mods.asked {
self.mods.asked = true;
self.mods_search(0);
}
let header = page_header(
"Mods",
"Mods and shader packs made for Horizon from its own reviewed registry, and Java \
+5 -1
View File
@@ -38,6 +38,8 @@ pub(super) enum Field {
InstanceGroup,
/// Its search in Mojang's versions (`new_instance.rs`).
InstanceVersion,
/// Home's Mods tab's filter (`instance_mods.rs`).
InstanceModsFilter,
/// Home's Multiplayer section's (`multiplayer_page.rs`).
ServerName,
ServerAddress,
@@ -54,7 +56,7 @@ pub(super) enum Field {
}
impl Field {
const ALL: [Field; 21] = [
const ALL: [Field; 22] = [
Field::Name,
Field::Seed,
Field::Players,
@@ -67,6 +69,7 @@ impl Field {
Field::InstanceName,
Field::InstanceGroup,
Field::InstanceVersion,
Field::InstanceModsFilter,
Field::ServerName,
Field::ServerAddress,
Field::ConsoleSearch,
@@ -999,6 +1002,7 @@ impl Launcher {
Field::InstanceName
| Field::InstanceGroup
| Field::InstanceVersion
| Field::InstanceModsFilter
| Field::ConsoleSearch
| Field::DeleteServer => {}
Field::PreLaunch | Field::Wrapper | Field::LaunchEnv | Field::PostExit => {