refactor(setup): clean up setup wizard UX

- Hide raw filesystem paths from all status messages; use launcher
  display names instead (e.g. "Downloading Battle.net installer…")
- Simplify installer source label: show "✓ Official installer detected"
  badge when URL is auto-filled
- Replace separate "Download / Prepare" + "Run installer" buttons with
  a single context-aware action button whose label tracks the stage:
  Download → / Downloading… / Install → / Installing…
- Remove auto-download on launcher confirm; show confirmation prompt
  "Ready to download the official X installer. Press Download → to begin."
- Soften the exe-not-found error message; remove raw path and config
  editing advice — direct users to the log instead
- Rename "Launch now" → "Open launcher" for clarity
- Show "✓ X installed successfully!" success banner above Close/Open
  buttons when install completes and the exe is present

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-18 23:06:32 -07:00
parent 156bb460a0
commit d4f0515a82
+85 -46
View File
@@ -3,7 +3,7 @@ use anyhow::Result;
use iced::widget::{ use iced::widget::{
button, column, container, pick_list, progress_bar, row, scrollable, text, text_input, Column, button, column, container, pick_list, progress_bar, row, scrollable, text, text_input, Column,
}; };
use iced::{Element, Length, Subscription, Task, Theme}; use iced::{Color, Element, Length, Subscription, Task, Theme};
use std::ffi::OsString; use std::ffi::OsString;
use std::io::{BufRead, BufReader, Read, Write}; use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -99,8 +99,8 @@ impl State {
fn new_install(config: Config, launcher: Launcher) -> Self { fn new_install(config: Config, launcher: Launcher) -> Self {
let status = format!( let status = format!(
"Paste an installer URL or a local .exe path. It will install into {}.", "Paste an installer URL or a local .exe path for {}.",
launcher.prefix_dir.display() launcher.display
); );
let template_options = config::presets() let template_options = config::presets()
.iter() .iter()
@@ -171,30 +171,21 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
preset.prefix_dir = PathBuf::from(&prefix); preset.prefix_dir = PathBuf::from(&prefix);
// If we know the official installer URL, pre-fill source and // If we know the official installer URL, pre-fill source and
// kick off the download automatically so the user doesn't have // let the user confirm before downloading.
// to find or paste anything.
if let Some(url) = preset.installer_url.clone() { if let Some(url) = preset.installer_url.clone() {
state.source = url.clone(); state.source = url;
state.stage = Stage::Busy; state.stage = Stage::Idle;
state.status = format!( state.status = format!(
"Found official installer — downloading to {}", "Ready to download the official {} installer. Press Download to begin.",
preset.prefix_dir.display() preset.display
); );
if let Ok(mut p) = state.download.lock() {
*p = DownloadProgress::default();
}
let name = preset.name.clone();
let progress = state.download.clone();
state.launcher = Some(preset); state.launcher = Some(preset);
return Task::perform( return Task::none();
async_blocking(move || download_blocking(&url, &name, progress)),
Message::PrepareDone,
);
} }
state.status = format!( state.status = format!(
"Paste an installer URL or a local .exe path. It will install into {}.", "Paste an installer URL or a local .exe path for {}.",
preset.prefix_dir.display() preset.display
); );
state.launcher = Some(preset); state.launcher = Some(preset);
state.stage = Stage::Idle; state.stage = Stage::Idle;
@@ -228,7 +219,12 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
if path.is_file() { if path.is_file() {
state.installer = Some(path.clone()); state.installer = Some(path.clone());
state.stage = Stage::Ready; state.stage = Stage::Ready;
state.status = format!("Ready: {}", path.display()); let display = state
.launcher
.as_ref()
.map(|l| l.display.as_str())
.unwrap_or("installer");
state.status = format!("Local installer ready for {}. Press Install → to continue.", display);
return Task::none(); return Task::none();
} }
if !src.starts_with("http://") && !src.starts_with("https://") { if !src.starts_with("http://") && !src.starts_with("https://") {
@@ -236,7 +232,12 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
return Task::none(); return Task::none();
} }
state.stage = Stage::Busy; state.stage = Stage::Busy;
state.status = format!("Downloading {src}"); let display_name = state
.launcher
.as_ref()
.map(|l| l.display.clone())
.unwrap_or_else(|| "installer".to_string());
state.status = format!("Downloading {} installer…", display_name);
if let Ok(mut p) = state.download.lock() { if let Ok(mut p) = state.download.lock() {
*p = DownloadProgress::default(); *p = DownloadProgress::default();
} }
@@ -256,7 +257,12 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
state.downloaded_temp = Some(path.clone()); state.downloaded_temp = Some(path.clone());
state.installer = Some(path.clone()); state.installer = Some(path.clone());
state.stage = Stage::Ready; state.stage = Stage::Ready;
state.status = format!("Downloaded to {}", path.display()); let display_name = state
.launcher
.as_ref()
.map(|l| l.display.as_str())
.unwrap_or("installer");
state.status = format!("Download complete. Press Install → to run the {} installer.", display_name);
Task::none() Task::none()
} }
Message::PrepareDone(Err(e)) => { Message::PrepareDone(Err(e)) => {
@@ -309,11 +315,9 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
"✓ Installer finished (exit {code}). {} is ready.", "✓ Installer finished (exit {code}). {} is ready.",
launcher.display, launcher.display,
), ),
Ok(code) => format!( Ok(_) => "Installation finished but the launcher wasn't found where expected. \
"umu-run exited {code} but the expected exe is not at {}.\n\ Check the log below for details."
Check the installer's destination path, or edit exe_path in config.", .to_string(),
exe.display(),
),
Err(e) => format!("Install failed: {e}"), Err(e) => format!("Install failed: {e}"),
}; };
Task::none() Task::none()
@@ -430,7 +434,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
let source_label = if launcher.installer_url.is_some() let source_label = if launcher.installer_url.is_some()
&& state.source == launcher.installer_url.as_deref().unwrap_or("") && state.source == launcher.installer_url.as_deref().unwrap_or("")
{ {
"Official installer URL (auto-filled — or paste your own):" "Official installer detected — or paste a custom URL:"
} else { } else {
"Installer URL or local .exe path:" "Installer URL or local .exe path:"
}; };
@@ -439,18 +443,37 @@ fn view_install(state: &State) -> Element<'_, Message> {
.on_input(Message::SourceChanged) .on_input(Message::SourceChanged)
.padding(8); .padding(8);
let prepare_enabled = matches!(state.stage, Stage::Idle | Stage::Ready | Stage::Finished);
let install_enabled = matches!(state.stage, Stage::Ready);
let finished = matches!(state.stage, Stage::Finished); let finished = matches!(state.stage, Stage::Finished);
let install_success = finished let install_success = finished && launcher.full_exe_path().exists();
&& launcher
.full_exe_path()
.exists();
let prepare_btn = button(text("Download / Prepare")) // Single context-aware action button (changes 3 & 4)
.on_press_maybe(prepare_enabled.then_some(Message::PreparePressed)); let action_btn: Option<Element<Message>> = match state.stage {
let install_btn = button(text("Run installer")) Stage::Idle => {
.on_press_maybe(install_enabled.then_some(Message::InstallPressed)); let enabled = !state.source.trim().is_empty();
Some(
button(text("Download →"))
.on_press_maybe(enabled.then_some(Message::PreparePressed))
.into(),
)
}
Stage::Busy => Some(
button(text("Downloading…"))
.on_press_maybe(None::<Message>)
.into(),
),
Stage::Ready => Some(
button(text("Install →"))
.on_press(Message::InstallPressed)
.into(),
),
Stage::Installing => Some(
button(text("Installing…"))
.on_press_maybe(None::<Message>)
.into(),
),
Stage::Finished => None,
Stage::Picking => None,
};
let status = text(state.status.clone()); let status = text(state.status.clone());
@@ -496,7 +519,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
let close_btn = button(text("Close").size(13)) let close_btn = button(text("Close").size(13))
.on_press(Message::Close) .on_press(Message::Close)
.style(button::secondary); .style(button::secondary);
let launch_btn = button(text("Launch now").size(13)) let launch_btn = button(text("Open launcher").size(13))
.on_press_maybe(install_success.then_some(Message::LaunchNow)) .on_press_maybe(install_success.then_some(Message::LaunchNow))
.style(button::primary); .style(button::primary);
row![close_btn, launch_btn].spacing(10).into() row![close_btn, launch_btn].spacing(10).into()
@@ -504,21 +527,37 @@ fn view_install(state: &State) -> Element<'_, Message> {
text("").into() text("").into()
}; };
let body = column![ // Success banner (change 7)
let success_banner: Element<Message> = if finished && install_success {
text(format!("{} installed successfully!", launcher.display))
.size(14)
.color(Color::from_rgb(0.4, 0.9, 0.4))
.into()
} else {
text("").into()
};
let mut body = column![
header, header,
prefix, prefix,
expected, expected,
text(source_label).size(12), text(source_label).size(12),
input, input,
row![prepare_btn, install_btn].spacing(12),
progress_row,
status,
finished_row,
log_pane,
] ]
.spacing(12) .spacing(12)
.padding(20); .padding(20);
if let Some(btn) = action_btn {
body = body.push(btn);
}
let body = body
.push(progress_row)
.push(status)
.push(success_banner)
.push(finished_row)
.push(log_pane);
container(body).into() container(body).into()
} }