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:
+85
-46
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user