refactor(setup): complete UX overhaul of the install wizard
- Step indicator (1→2→3) on both screens showing current position
- Fixed window size (520×440, non-resizable) for consistent presentation
- view_install redesigned with card-based layout matching view_picking
- Status messages colour-coded: blue=info, green=success, red=error
- Official installer shown as a badge ("✓ Official installer") — URL hidden
- Download/install progress in a styled card with byte counter
- Finished state has distinct success (green border) / failure (red border) banners
- Installation log collapsed by default behind "Show details ▼" toggle
- Removed "via umu-run" and other internal tool references from user-facing text
- Removed raw "Paste a URL…" initial status — context is clear from the card
- Window title simplified to "umutray — <Launcher Name>"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+311
-127
@@ -30,6 +30,7 @@ pub enum Message {
|
|||||||
// Finished stage
|
// Finished stage
|
||||||
LaunchNow,
|
LaunchNow,
|
||||||
Close,
|
Close,
|
||||||
|
ToggleLog,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -65,6 +66,7 @@ struct State {
|
|||||||
status: String,
|
status: String,
|
||||||
download: Arc<Mutex<DownloadProgress>>,
|
download: Arc<Mutex<DownloadProgress>>,
|
||||||
log: Arc<Mutex<Vec<String>>>,
|
log: Arc<Mutex<Vec<String>>>,
|
||||||
|
show_log: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for State {
|
impl Drop for State {
|
||||||
@@ -94,14 +96,11 @@ impl State {
|
|||||||
status: String::new(),
|
status: String::new(),
|
||||||
download: Arc::new(Mutex::new(DownloadProgress::default())),
|
download: Arc::new(Mutex::new(DownloadProgress::default())),
|
||||||
log: Arc::new(Mutex::new(Vec::new())),
|
log: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
show_log: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_install(config: Config, launcher: Launcher) -> Self {
|
fn new_install(config: Config, launcher: Launcher) -> Self {
|
||||||
let status = format!(
|
|
||||||
"Paste an installer URL or a local .exe path for {}.",
|
|
||||||
launcher.display
|
|
||||||
);
|
|
||||||
let template_options = config::presets()
|
let template_options = config::presets()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| l.display.clone())
|
.map(|l| l.display.clone())
|
||||||
@@ -116,9 +115,10 @@ impl State {
|
|||||||
installer: None,
|
installer: None,
|
||||||
downloaded_temp: None,
|
downloaded_temp: None,
|
||||||
stage: Stage::Idle,
|
stage: Stage::Idle,
|
||||||
status,
|
status: String::new(),
|
||||||
download: Arc::new(Mutex::new(DownloadProgress::default())),
|
download: Arc::new(Mutex::new(DownloadProgress::default())),
|
||||||
log: Arc::new(Mutex::new(Vec::new())),
|
log: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
show_log: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,15 +270,21 @@ fn update(state: &mut State, message: Message) -> Task<Message> {
|
|||||||
state.status = format!("Download failed: {e}");
|
state.status = format!("Download failed: {e}");
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::ToggleLog => {
|
||||||
|
state.show_log = !state.show_log;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::InstallPressed => {
|
Message::InstallPressed => {
|
||||||
let Some(installer) = state.installer.clone() else {
|
let Some(installer) = state.installer.clone() else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
state.stage = Stage::Installing;
|
state.stage = Stage::Installing;
|
||||||
state.status = "Running installer via umu-run (this may take several minutes)…".into();
|
let display_name = state.launcher.as_ref().map(|l| l.display.clone()).unwrap_or_default();
|
||||||
|
state.status = format!("Installing {}. This may take a few minutes…", display_name);
|
||||||
if let Ok(mut v) = state.log.lock() {
|
if let Ok(mut v) = state.log.lock() {
|
||||||
v.clear();
|
v.clear();
|
||||||
}
|
}
|
||||||
|
state.show_log = false;
|
||||||
let config = state.config.clone();
|
let config = state.config.clone();
|
||||||
let launcher = state
|
let launcher = state
|
||||||
.launcher
|
.launcher
|
||||||
@@ -375,6 +381,7 @@ fn view_picking(state: &State) -> Element<'_, Message> {
|
|||||||
// ── Header ───────────────────────────────────────────────────────────────
|
// ── Header ───────────────────────────────────────────────────────────────
|
||||||
let header = container(
|
let header = container(
|
||||||
column![
|
column![
|
||||||
|
step_indicator(1),
|
||||||
text("Add a Launcher").size(26),
|
text("Add a Launcher").size(26),
|
||||||
text("Choose a launcher and set its install location.").size(13)
|
text("Choose a launcher and set its install location.").size(13)
|
||||||
.style(|_: &Theme| text::Style {
|
.style(|_: &Theme| text::Style {
|
||||||
@@ -472,17 +479,91 @@ fn view_picking(state: &State) -> Element<'_, Message> {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn step_indicator<'a>(step: u8) -> Element<'a, Message> {
|
||||||
|
let active = Color::from_rgb(0.55, 0.75, 1.0);
|
||||||
|
let done = Color::from_rgb(0.4, 0.85, 0.4);
|
||||||
|
let muted = Color::from_rgb(0.4, 0.4, 0.4);
|
||||||
|
|
||||||
|
let label = |n: u8, label: &'static str| -> Element<'a, Message> {
|
||||||
|
let (num_color, text_color) = if n < step {
|
||||||
|
(done, done)
|
||||||
|
} else if n == step {
|
||||||
|
(active, active)
|
||||||
|
} else {
|
||||||
|
(muted, muted)
|
||||||
|
};
|
||||||
|
row![
|
||||||
|
text(format!("{n}")).size(12).style(move |_: &Theme| text::Style { color: Some(num_color) }),
|
||||||
|
text(format!(" {label}")).size(12).style(move |_: &Theme| text::Style { color: Some(text_color) }),
|
||||||
|
]
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sep = |c: Color| -> Element<'a, Message> {
|
||||||
|
text(" — ").size(12).style(move |_: &Theme| text::Style { color: Some(c) }).into()
|
||||||
|
};
|
||||||
|
|
||||||
|
container(
|
||||||
|
row![
|
||||||
|
label(1, "Choose"),
|
||||||
|
sep(if step > 1 { done } else { muted }),
|
||||||
|
label(2, "Install"),
|
||||||
|
sep(if step > 2 { done } else { muted }),
|
||||||
|
label(3, "Done"),
|
||||||
|
]
|
||||||
|
.align_y(Alignment::Center),
|
||||||
|
)
|
||||||
|
.padding(Padding { top: 0.0, right: 0.0, bottom: 12.0, left: 0.0 })
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_card(msg: String, kind: StatusKind) -> Element<'static, Message> {
|
||||||
|
let color = match kind {
|
||||||
|
StatusKind::Info => Color::from_rgb(0.55, 0.75, 1.0),
|
||||||
|
StatusKind::Success => Color::from_rgb(0.35, 0.85, 0.45),
|
||||||
|
StatusKind::Error => Color::from_rgb(1.0, 0.45, 0.35),
|
||||||
|
StatusKind::Neutral => Color::from_rgb(0.6, 0.6, 0.6),
|
||||||
|
};
|
||||||
|
container(
|
||||||
|
text(msg).size(13).style(move |_: &Theme| text::Style { color: Some(color) }),
|
||||||
|
)
|
||||||
|
.padding(Padding::from([10, 14]))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.style(|theme: &Theme| {
|
||||||
|
let p = theme.extended_palette();
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(p.background.weak.color)),
|
||||||
|
border: Border { color: p.background.strong.color, width: 1.0, radius: 6.0.into() },
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StatusKind { Info, Success, Error, Neutral }
|
||||||
|
|
||||||
fn view_install(state: &State) -> Element<'_, Message> {
|
fn view_install(state: &State) -> Element<'_, Message> {
|
||||||
let launcher = state
|
let launcher = state
|
||||||
.launcher
|
.launcher
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("launcher must be set in install stage");
|
.expect("launcher must be set in install stage");
|
||||||
|
|
||||||
let can_go_back = state.selected_template.is_some()
|
let finished = matches!(state.stage, Stage::Finished);
|
||||||
&& matches!(state.stage, Stage::Idle | Stage::Ready | Stage::Finished);
|
let install_success = finished && launcher.full_exe_path().exists();
|
||||||
|
let is_official = launcher.installer_url.as_deref()
|
||||||
|
.map(|u| state.source == u)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let back_btn: Element<Message> = if state.selected_template.is_some() {
|
// ── Step indicator ────────────────────────────────────────────────────────
|
||||||
button(text("← Back").size(13))
|
let step = if finished { 3 } else { 2 };
|
||||||
|
let steps = step_indicator(step);
|
||||||
|
|
||||||
|
// ── Back button ───────────────────────────────────────────────────────────
|
||||||
|
let can_go_back = state.selected_template.is_some()
|
||||||
|
&& matches!(state.stage, Stage::Idle | Stage::Ready);
|
||||||
|
let back_el: Element<Message> = if state.selected_template.is_some() && !finished {
|
||||||
|
button(text("← Back").size(12))
|
||||||
.on_press_maybe(can_go_back.then_some(Message::Back))
|
.on_press_maybe(can_go_back.then_some(Message::Back))
|
||||||
.style(button::secondary)
|
.style(button::secondary)
|
||||||
.into()
|
.into()
|
||||||
@@ -490,84 +571,81 @@ fn view_install(state: &State) -> Element<'_, Message> {
|
|||||||
text("").into()
|
text("").into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let header = row![
|
// ── Header ────────────────────────────────────────────────────────────────
|
||||||
text(format!("Setup: {}", launcher.display)).size(24),
|
let header_row = row![
|
||||||
|
column![
|
||||||
|
steps,
|
||||||
|
text(&launcher.display).size(22),
|
||||||
|
]
|
||||||
|
.spacing(0),
|
||||||
iced::widget::horizontal_space(),
|
iced::widget::horizontal_space(),
|
||||||
back_btn,
|
back_el,
|
||||||
]
|
]
|
||||||
.align_y(iced::Alignment::Center);
|
.align_y(Alignment::Start);
|
||||||
|
|
||||||
let is_official = launcher.installer_url.as_deref()
|
// ── Source / installer card (only shown in Idle) ───────────────────────────
|
||||||
.map(|u| state.source == u)
|
let source_card: Element<Message> = if matches!(state.stage, Stage::Idle) {
|
||||||
.unwrap_or(false);
|
let inner: Element<Message> = if is_official {
|
||||||
|
column![
|
||||||
let source_row: Element<Message> = if is_official && !matches!(state.stage, Stage::Idle) {
|
text("Installer").size(12).style(|_: &Theme| text::Style {
|
||||||
// Already past the confirmation step — don't show the input again
|
color: Some(Color::from_rgb(0.55, 0.75, 1.0)),
|
||||||
text("").into()
|
}),
|
||||||
} else if is_official {
|
row![
|
||||||
// Official URL pre-filled: show read-only info + small "use custom URL" toggle
|
text("✓ Official installer").size(13).style(|_: &Theme| text::Style {
|
||||||
column![
|
color: Some(Color::from_rgb(0.35, 0.85, 0.45)),
|
||||||
text("✓ Using official installer").size(12).style(|_: &Theme| text::Style {
|
}),
|
||||||
color: Some(Color::from_rgb(0.4, 0.85, 0.4)),
|
iced::widget::horizontal_space(),
|
||||||
}),
|
]
|
||||||
text_input("Or paste a custom URL / local .exe path to override:", &state.source)
|
.align_y(Alignment::Center),
|
||||||
.on_input(Message::SourceChanged)
|
text("Using the official download. To use a different installer, paste a URL or path below.")
|
||||||
.padding(6)
|
.size(11)
|
||||||
.size(12),
|
.style(|_: &Theme| text::Style { color: Some(Color::from_rgb(0.45, 0.45, 0.45)) }),
|
||||||
]
|
text_input("Custom URL or local .exe path (optional)", &state.source)
|
||||||
.spacing(4)
|
.on_input(Message::SourceChanged)
|
||||||
.into()
|
.padding(7)
|
||||||
|
.size(12),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
column![
|
||||||
|
text("Installer").size(12).style(|_: &Theme| text::Style {
|
||||||
|
color: Some(Color::from_rgb(0.55, 0.75, 1.0)),
|
||||||
|
}),
|
||||||
|
text_input("Paste a URL or local .exe path", &state.source)
|
||||||
|
.on_input(Message::SourceChanged)
|
||||||
|
.padding(8),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
section_card(inner)
|
||||||
} else {
|
} else {
|
||||||
column![
|
text("").into()
|
||||||
text("Installer URL or local .exe path:").size(12),
|
|
||||||
text_input("https://… or /path/to/installer.exe", &state.source)
|
|
||||||
.on_input(Message::SourceChanged)
|
|
||||||
.padding(8),
|
|
||||||
]
|
|
||||||
.spacing(4)
|
|
||||||
.into()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let finished = matches!(state.stage, Stage::Finished);
|
// ── Status card ───────────────────────────────────────────────────────────
|
||||||
let install_success = finished && launcher.full_exe_path().exists();
|
let status_el: Element<Message> = if !state.status.is_empty() {
|
||||||
|
let kind = match state.stage {
|
||||||
// Single context-aware action button (changes 3 & 4)
|
Stage::Finished if install_success => StatusKind::Success,
|
||||||
let action_btn: Option<Element<Message>> = match state.stage {
|
Stage::Finished => StatusKind::Error,
|
||||||
Stage::Idle => {
|
Stage::Busy | Stage::Installing => StatusKind::Info,
|
||||||
let enabled = !state.source.trim().is_empty();
|
Stage::Ready => StatusKind::Success,
|
||||||
Some(
|
_ => StatusKind::Neutral,
|
||||||
button(text("Download →"))
|
};
|
||||||
.on_press_maybe(enabled.then_some(Message::PreparePressed))
|
status_card(state.status.clone(), kind)
|
||||||
.into(),
|
} else if matches!(state.stage, Stage::Idle) && is_official {
|
||||||
)
|
status_card(
|
||||||
}
|
format!("Ready to download the official {} installer.", launcher.display),
|
||||||
Stage::Busy => Some(
|
StatusKind::Neutral,
|
||||||
button(text("Downloading…"))
|
)
|
||||||
.on_press_maybe(None::<Message>)
|
} else {
|
||||||
.into(),
|
text("").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());
|
// ── Progress bar ──────────────────────────────────────────────────────────
|
||||||
|
let progress_el: Element<Message> = if matches!(state.stage, Stage::Busy) {
|
||||||
let progress_row: Element<Message> = if matches!(state.stage, Stage::Busy) {
|
let p = state.download.lock().map(|p| (p.bytes, p.total)).unwrap_or((0, None));
|
||||||
let p = state
|
|
||||||
.download
|
|
||||||
.lock()
|
|
||||||
.map(|p| (p.bytes, p.total))
|
|
||||||
.unwrap_or((0, None));
|
|
||||||
let (bytes, total) = p;
|
let (bytes, total) = p;
|
||||||
let fraction = match total {
|
let fraction = match total {
|
||||||
Some(t) if t > 0 => (bytes as f32) / (t as f32),
|
Some(t) if t > 0 => (bytes as f32) / (t as f32),
|
||||||
@@ -577,79 +655,180 @@ fn view_install(state: &State) -> Element<'_, Message> {
|
|||||||
Some(t) => format!("{} / {}", fmt_bytes(bytes), fmt_bytes(t)),
|
Some(t) => format!("{} / {}", fmt_bytes(bytes), fmt_bytes(t)),
|
||||||
None => format!("{} downloaded", fmt_bytes(bytes)),
|
None => format!("{} downloaded", fmt_bytes(bytes)),
|
||||||
};
|
};
|
||||||
column![progress_bar(0.0..=1.0, fraction), text(label).size(12)]
|
section_card(
|
||||||
.spacing(4)
|
column![progress_bar(0.0..=1.0, fraction), text(label).size(12)]
|
||||||
.into()
|
.spacing(6),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
text("").into()
|
text("").into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let log_pane: Element<Message> = if matches!(state.stage, Stage::Installing | Stage::Finished) {
|
// ── Finished banner ───────────────────────────────────────────────────────
|
||||||
let lines: Vec<String> = state.log.lock().map(|v| v.clone()).unwrap_or_default();
|
let finished_banner: Element<Message> = if finished {
|
||||||
let tail: Vec<Element<Message>> = lines
|
let (icon, msg, kind) = if install_success {
|
||||||
.iter()
|
("✓", format!("{} is ready to use.", launcher.display), StatusKind::Success)
|
||||||
.rev()
|
} else {
|
||||||
.take(80)
|
("✗", "Installation did not complete successfully. See details below.".to_string(), StatusKind::Error)
|
||||||
.rev()
|
};
|
||||||
.map(|l| text(l.clone()).size(11).into())
|
container(
|
||||||
.collect();
|
column![
|
||||||
scrollable(Column::with_children(tail).spacing(2))
|
text(format!("{icon} {msg}")).size(15).style({
|
||||||
.height(Length::Fixed(220.0))
|
let color = if install_success {
|
||||||
.into()
|
Color::from_rgb(0.35, 0.85, 0.45)
|
||||||
|
} else {
|
||||||
|
Color::from_rgb(1.0, 0.45, 0.35)
|
||||||
|
};
|
||||||
|
move |_: &Theme| text::Style { color: Some(color) }
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.padding(Padding::from([12, 14]))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.style(move |theme: &Theme| {
|
||||||
|
let p = theme.extended_palette();
|
||||||
|
let border_color = if install_success {
|
||||||
|
Color::from_rgb(0.2, 0.5, 0.25)
|
||||||
|
} else {
|
||||||
|
Color::from_rgb(0.5, 0.2, 0.2)
|
||||||
|
};
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(p.background.weak.color)),
|
||||||
|
border: Border { color: border_color, width: 1.0, radius: 6.0.into() },
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
} else {
|
} else {
|
||||||
text("").into()
|
text("").into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let finished_row: Element<Message> = if finished {
|
// ── Log toggle + pane ─────────────────────────────────────────────────────
|
||||||
let close_btn = button(text("Close").size(13))
|
let has_log = matches!(state.stage, Stage::Installing | Stage::Finished);
|
||||||
.on_press(Message::Close)
|
let log_el: Element<Message> = if has_log {
|
||||||
|
let toggle_label = if state.show_log { "Hide details ▲" } else { "Show details ▼" };
|
||||||
|
let toggle_btn = button(text(toggle_label).size(12))
|
||||||
|
.on_press(Message::ToggleLog)
|
||||||
.style(button::secondary);
|
.style(button::secondary);
|
||||||
let launch_btn = button(text("Open launcher").size(13))
|
|
||||||
.on_press_maybe(install_success.then_some(Message::LaunchNow))
|
|
||||||
.style(button::primary);
|
|
||||||
row![close_btn, launch_btn].spacing(10).into()
|
|
||||||
} else {
|
|
||||||
text("").into()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Success banner (change 7)
|
if state.show_log {
|
||||||
let success_banner: Element<Message> = if finished && install_success {
|
let lines: Vec<String> = state.log.lock().map(|v| v.clone()).unwrap_or_default();
|
||||||
text(format!("✓ {} installed successfully!", launcher.display))
|
let rows: Vec<Element<Message>> = lines
|
||||||
.size(14)
|
.iter().rev().take(80).rev()
|
||||||
.color(Color::from_rgb(0.4, 0.9, 0.4))
|
.map(|l| text(l.clone()).size(11).style(|_: &Theme| text::Style {
|
||||||
|
color: Some(Color::from_rgb(0.55, 0.55, 0.55)),
|
||||||
|
}).into())
|
||||||
|
.collect();
|
||||||
|
column![
|
||||||
|
toggle_btn,
|
||||||
|
container(
|
||||||
|
scrollable(Column::with_children(rows).spacing(1))
|
||||||
|
.height(Length::Fixed(180.0)),
|
||||||
|
)
|
||||||
|
.padding(Padding::from([6, 10]))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.style(|theme: &Theme| {
|
||||||
|
let p = theme.extended_palette();
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(p.background.weak.color)),
|
||||||
|
border: Border { color: p.background.strong.color, width: 1.0, radius: 4.0.into() },
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
.into()
|
.into()
|
||||||
|
} else {
|
||||||
|
toggle_btn.into()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
text("").into()
|
text("").into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut body = column![
|
// ── Action button ─────────────────────────────────────────────────────────
|
||||||
header,
|
let action_el: Element<Message> = match state.stage {
|
||||||
source_row,
|
Stage::Idle => {
|
||||||
|
let ready = is_official || !state.source.trim().is_empty();
|
||||||
|
container(
|
||||||
|
button(text("Download →").size(14))
|
||||||
|
.on_press_maybe(ready.then_some(Message::PreparePressed))
|
||||||
|
.style(button::primary),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(Alignment::End)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
Stage::Busy => container(
|
||||||
|
button(text("Downloading…").size(14))
|
||||||
|
.on_press_maybe(None::<Message>)
|
||||||
|
.style(button::primary),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(Alignment::End)
|
||||||
|
.into(),
|
||||||
|
Stage::Ready => container(
|
||||||
|
button(text("Install →").size(14))
|
||||||
|
.on_press(Message::InstallPressed)
|
||||||
|
.style(button::primary),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(Alignment::End)
|
||||||
|
.into(),
|
||||||
|
Stage::Installing => container(
|
||||||
|
button(text("Installing…").size(14))
|
||||||
|
.on_press_maybe(None::<Message>)
|
||||||
|
.style(button::primary),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(Alignment::End)
|
||||||
|
.into(),
|
||||||
|
Stage::Finished => {
|
||||||
|
let close_btn = button(text("Close").size(13))
|
||||||
|
.on_press(Message::Close)
|
||||||
|
.style(button::secondary);
|
||||||
|
let launch_btn = button(text("Open Launcher").size(13))
|
||||||
|
.on_press_maybe(install_success.then_some(Message::LaunchNow))
|
||||||
|
.style(button::primary);
|
||||||
|
container(
|
||||||
|
row![close_btn, launch_btn].spacing(10),
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.align_x(Alignment::End)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
Stage::Picking => text("").into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Assembly ──────────────────────────────────────────────────────────────
|
||||||
|
let body = column![
|
||||||
|
header_row,
|
||||||
|
source_card,
|
||||||
|
status_el,
|
||||||
|
progress_el,
|
||||||
|
finished_banner,
|
||||||
|
log_el,
|
||||||
|
action_el,
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
.padding(20);
|
.padding(Padding { top: 24.0, right: 24.0, bottom: 24.0, left: 24.0 });
|
||||||
|
|
||||||
if let Some(btn) = action_btn {
|
container(scrollable(body))
|
||||||
body = body.push(btn);
|
.width(Length::Fill)
|
||||||
}
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
let body = body
|
|
||||||
.push(progress_row)
|
|
||||||
.push(status)
|
|
||||||
.push(success_banner)
|
|
||||||
.push(finished_row)
|
|
||||||
.push(log_pane);
|
|
||||||
|
|
||||||
container(body).into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(config: &Config, launcher: &Launcher) -> Result<()> {
|
pub fn run(config: &Config, launcher: &Launcher) -> Result<()> {
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
let launcher = launcher.clone();
|
let launcher = launcher.clone();
|
||||||
let title = format!("umutray setup — {}", launcher.display);
|
let title = format!("umutray — {}", launcher.display);
|
||||||
iced::application(move |_: &State| title.clone(), update, view)
|
iced::application(move |_: &State| title.clone(), update, view)
|
||||||
.subscription(subscription)
|
.subscription(subscription)
|
||||||
.theme(|_| Theme::Dark)
|
.theme(|_| Theme::Dark)
|
||||||
|
.window(iced::window::Settings {
|
||||||
|
size: iced::Size::new(520.0, 440.0),
|
||||||
|
resizable: false,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
.run_with(move || {
|
.run_with(move || {
|
||||||
(
|
(
|
||||||
State::new_install(config.clone(), launcher.clone()),
|
State::new_install(config.clone(), launcher.clone()),
|
||||||
@@ -664,6 +843,11 @@ pub fn run_new(config: &Config) -> Result<()> {
|
|||||||
iced::application(|_: &State| "umutray — Add Launcher".to_string(), update, view)
|
iced::application(|_: &State| "umutray — Add Launcher".to_string(), update, view)
|
||||||
.subscription(subscription)
|
.subscription(subscription)
|
||||||
.theme(|_| Theme::Dark)
|
.theme(|_| Theme::Dark)
|
||||||
|
.window(iced::window::Settings {
|
||||||
|
size: iced::Size::new(520.0, 440.0),
|
||||||
|
resizable: false,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
.run_with(move || (State::new_picking(config.clone()), Task::none()))
|
.run_with(move || (State::new_picking(config.clone()), Task::none()))
|
||||||
.map_err(|e| anyhow::anyhow!("iced: {e}"))
|
.map_err(|e| anyhow::anyhow!("iced: {e}"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user