Redesign launcher cards: icon buttons, proton badge, pill toggles, sub-cards, better header
This commit is contained in:
+300
-260
@@ -778,6 +778,29 @@ fn view(state: &Dashboard) -> Element<'_, Message> {
|
|||||||
|
|
||||||
// ── Card views ──────────────────────────────────────────────────────────────
|
// ── Card views ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Styled inner container for sub-sections inside a card.
|
||||||
|
fn sub_card<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||||
|
container(content)
|
||||||
|
.padding(Padding::from([8, 12]))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.style(|theme: &Theme| {
|
||||||
|
let p = theme.extended_palette();
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(Color {
|
||||||
|
a: 0.35,
|
||||||
|
..p.background.strong.color
|
||||||
|
})),
|
||||||
|
border: Border {
|
||||||
|
color: Color::TRANSPARENT,
|
||||||
|
width: 0.0,
|
||||||
|
radius: 6.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
fn launcher_card<'a>(
|
fn launcher_card<'a>(
|
||||||
l: &'a crate::config::Launcher,
|
l: &'a crate::config::Launcher,
|
||||||
installed: bool,
|
installed: bool,
|
||||||
@@ -786,280 +809,294 @@ fn launcher_card<'a>(
|
|||||||
scan_results: Option<&'a Vec<(String, String)>>,
|
scan_results: Option<&'a Vec<(String, String)>>,
|
||||||
scan_busy: bool,
|
scan_busy: bool,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let status_color = if running {
|
// ── Colours ────────────────────────────────────────────────────────────
|
||||||
Color::from_rgb(0.35, 0.85, 0.45)
|
let accent = Color::from_rgb(0.55, 0.75, 1.0);
|
||||||
} else if installed {
|
let green = Color::from_rgb(0.35, 0.85, 0.45);
|
||||||
Color::from_rgb(0.55, 0.75, 1.0)
|
let muted = Color::from_rgb(0.45, 0.45, 0.45);
|
||||||
} else {
|
let dim = Color::from_rgb(0.38, 0.38, 0.38);
|
||||||
Color::from_rgb(0.45, 0.45, 0.45)
|
let subtle = Color::from_rgb(0.5, 0.5, 0.5);
|
||||||
};
|
|
||||||
let status_dot = if running { "●" } else if installed { "●" } else { "○" };
|
|
||||||
let status_str = if running { "Running" } else if installed { "Installed" } else { "Not installed" };
|
|
||||||
|
|
||||||
|
let status_color = if running { green } else if installed { accent } else { muted };
|
||||||
|
let status_icon = if running { "\u{f287}" } else if installed { "\u{f26a}" } else { "\u{f28a}" }; // circle-fill, check-circle, x-circle
|
||||||
|
let status_str = if running { "Running" } else if installed { "Ready" } else { "Not installed" };
|
||||||
|
|
||||||
|
// ── Action button ─────────────────────────────────────────────────────
|
||||||
let action: Element<Message> = {
|
let action: Element<Message> = {
|
||||||
let n = l.name.clone();
|
let n = l.name.clone();
|
||||||
if !installed {
|
if !installed {
|
||||||
button(text("Setup").size(13))
|
button(
|
||||||
|
row![
|
||||||
|
text("\u{f3e2}").font(iced::Font::with_name("bootstrap-icons")).size(12),
|
||||||
|
text(" Setup").size(13),
|
||||||
|
].align_y(Alignment::Center).spacing(4),
|
||||||
|
)
|
||||||
.on_press(Message::Setup(n))
|
.on_press(Message::Setup(n))
|
||||||
.style(button::secondary)
|
.style(button::secondary)
|
||||||
.into()
|
.into()
|
||||||
} else if running {
|
} else if running {
|
||||||
button(text("Kill").size(13))
|
button(
|
||||||
|
row![
|
||||||
|
text("\u{f5de}").font(iced::Font::with_name("bootstrap-icons")).size(12),
|
||||||
|
text(" Stop").size(13),
|
||||||
|
].align_y(Alignment::Center).spacing(4),
|
||||||
|
)
|
||||||
.on_press(Message::Kill(n))
|
.on_press(Message::Kill(n))
|
||||||
.style(button::danger)
|
.style(button::danger)
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
button(text("Launch").size(13))
|
button(
|
||||||
|
row![
|
||||||
|
text("\u{f4f4}").font(iced::Font::with_name("bootstrap-icons")).size(12),
|
||||||
|
text(" Launch").size(13),
|
||||||
|
].align_y(Alignment::Center).spacing(4),
|
||||||
|
)
|
||||||
.on_press(Message::Launch(n))
|
.on_press(Message::Launch(n))
|
||||||
.style(button::primary)
|
.style(button::primary)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Proton version badge ──────────────────────────────────────────────
|
||||||
let version_badge: Element<Message> = if let Some(v) = &l.proton_version {
|
let version_badge: Element<Message> = if let Some(v) = &l.proton_version {
|
||||||
text(format!(" {v}"))
|
container(
|
||||||
.size(10)
|
text(v).size(10).style(move |_: &Theme| text::Style {
|
||||||
.style(|_: &Theme| text::Style {
|
color: Some(accent),
|
||||||
color: Some(Color::from_rgb(0.45, 0.55, 0.7)),
|
|
||||||
})
|
})
|
||||||
.into()
|
)
|
||||||
|
.padding(Padding::from([2, 6]))
|
||||||
|
.style(move |_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(Color { r: 0.55, g: 0.75, b: 1.0, a: 0.12 })),
|
||||||
|
border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 3.0.into() },
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
} else {
|
} else {
|
||||||
text("").into()
|
text("").into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let name_row = row![
|
// ── Header ────────────────────────────────────────────────────────────
|
||||||
text(&l.display).size(15),
|
let header = row![
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
text(&l.display).size(17),
|
||||||
|
version_badge,
|
||||||
|
].align_y(Alignment::Center).spacing(8),
|
||||||
|
row![
|
||||||
|
text(status_icon).font(iced::Font::with_name("bootstrap-icons")).size(10)
|
||||||
|
.style(move |_: &Theme| text::Style { color: Some(status_color) }),
|
||||||
|
text(format!(" {status_str}")).size(11)
|
||||||
|
.style(move |_: &Theme| text::Style { color: Some(status_color) }),
|
||||||
|
].align_y(Alignment::Center),
|
||||||
|
].spacing(4),
|
||||||
iced::widget::horizontal_space(),
|
iced::widget::horizontal_space(),
|
||||||
action,
|
action,
|
||||||
]
|
]
|
||||||
.align_y(Alignment::Center);
|
.align_y(Alignment::Center);
|
||||||
|
|
||||||
let status_row = row![
|
let mut sections: Vec<Element<Message>> = vec![header.into()];
|
||||||
text(status_dot).size(10).style(move |_: &Theme| text::Style {
|
|
||||||
color: Some(status_color),
|
|
||||||
}),
|
|
||||||
text(format!(" {status_str}")).size(11).style(move |_: &Theme| text::Style {
|
|
||||||
color: Some(status_color),
|
|
||||||
}),
|
|
||||||
version_badge,
|
|
||||||
]
|
|
||||||
.align_y(Alignment::Center);
|
|
||||||
|
|
||||||
let mut rows: Vec<Element<Message>> = vec![
|
// ── Games section ─────────────────────────────────────────────────────
|
||||||
column![name_row, status_row].spacing(3).into(),
|
let has_games = !l.games.is_empty();
|
||||||
];
|
let has_scan = scan_results.map(|r| !r.is_empty()).unwrap_or(false);
|
||||||
|
|
||||||
if l.games.is_empty() && scan_results.map(|r| r.is_empty()).unwrap_or(true) && !scan_busy {
|
if has_games || has_scan || scan_busy {
|
||||||
let lname = l.name.clone();
|
// Section header
|
||||||
let lname2 = l.name.clone();
|
let game_count = l.games.len();
|
||||||
let scan_btn = button(text("Scan for games").size(11))
|
let section_label: String = if !has_games {
|
||||||
.on_press(Message::ScanGames(lname))
|
"Detected".to_string()
|
||||||
.style(button::secondary);
|
} else if game_count == 1 {
|
||||||
let browse_btn = button(text("Browse exe…").size(11))
|
"1 Game".to_string()
|
||||||
.on_press(Message::BrowseGameExe(lname2))
|
} else {
|
||||||
.style(button::secondary);
|
format!("{game_count} Games")
|
||||||
rows.push(
|
};
|
||||||
container(
|
|
||||||
column![
|
|
||||||
text("Games").size(12).style(|_: &Theme| text::Style {
|
|
||||||
color: Some(Color::from_rgb(0.5, 0.5, 0.5)),
|
|
||||||
}),
|
|
||||||
iced::widget::horizontal_rule(1),
|
|
||||||
row![
|
|
||||||
text("No games added yet.").size(12).style(|_: &Theme| text::Style {
|
|
||||||
color: Some(Color::from_rgb(0.45, 0.45, 0.45)),
|
|
||||||
}),
|
|
||||||
iced::widget::horizontal_space(),
|
|
||||||
scan_btn,
|
|
||||||
browse_btn,
|
|
||||||
]
|
|
||||||
.align_y(Alignment::Center)
|
|
||||||
.spacing(6),
|
|
||||||
]
|
|
||||||
.spacing(6),
|
|
||||||
)
|
|
||||||
.padding(Padding { top: 8.0, left: 2.0, right: 0.0, bottom: 0.0 })
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if scan_busy {
|
let lname_scan = l.name.clone();
|
||||||
rows.push(
|
let rescan_btn = button(
|
||||||
container(
|
text("\u{f130}").font(iced::Font::with_name("bootstrap-icons")).size(11)
|
||||||
row![
|
)
|
||||||
text("⟳ Scanning for games…").size(12).style(|_: &Theme| text::Style {
|
.on_press_maybe((!scan_busy).then_some(Message::ScanGames(lname_scan)))
|
||||||
color: Some(Color::from_rgb(0.55, 0.75, 1.0)),
|
.style(button::secondary)
|
||||||
}),
|
.padding([3, 6]);
|
||||||
]
|
|
||||||
)
|
|
||||||
.padding(Padding { top: 6.0, left: 2.0, right: 0.0, bottom: 0.0 })
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(results) = scan_results {
|
let section_header = row![
|
||||||
if !results.is_empty() {
|
text(section_label).size(11)
|
||||||
rows.push(
|
.style(move |_: &Theme| text::Style { color: Some(subtle) }),
|
||||||
|
iced::widget::horizontal_space(),
|
||||||
|
rescan_btn,
|
||||||
|
]
|
||||||
|
.align_y(Alignment::Center);
|
||||||
|
|
||||||
|
let mut game_items: Vec<Element<Message>> = vec![section_header.into()];
|
||||||
|
|
||||||
|
// Scanning indicator
|
||||||
|
if scan_busy {
|
||||||
|
game_items.push(
|
||||||
container(
|
container(
|
||||||
column![
|
row![
|
||||||
text("Detected Games").size(12).style(|_: &Theme| text::Style {
|
text("\u{f130}").font(iced::Font::with_name("bootstrap-icons")).size(12)
|
||||||
color: Some(Color::from_rgb(0.5, 0.5, 0.5)),
|
.style(move |_: &Theme| text::Style { color: Some(accent) }),
|
||||||
}),
|
text(" Scanning for games…").size(12)
|
||||||
iced::widget::horizontal_rule(1),
|
.style(move |_: &Theme| text::Style { color: Some(accent) }),
|
||||||
]
|
].align_y(Alignment::Center),
|
||||||
.spacing(4),
|
|
||||||
)
|
)
|
||||||
.padding(Padding { top: 6.0, left: 2.0, right: 0.0, bottom: 0.0 })
|
.padding(Padding::from([6, 0]))
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (display, exe) in results {
|
|
||||||
let lname = l.name.clone();
|
// Detected (unregistered) games
|
||||||
let d = display.clone();
|
if let Some(results) = scan_results {
|
||||||
let e = exe.clone();
|
for (display, exe) in results {
|
||||||
let add_btn = button(
|
let lname = l.name.clone();
|
||||||
text("\u{f64d}").font(iced::Font::with_name("bootstrap-icons")).size(12)
|
let d = display.clone();
|
||||||
)
|
let e = exe.clone();
|
||||||
.on_press(Message::AddScannedGame(lname, d, e))
|
let add_btn = button(
|
||||||
.style(button::primary)
|
|
||||||
.padding([4, 8]);
|
|
||||||
rows.push(
|
|
||||||
container(
|
|
||||||
row![
|
row![
|
||||||
container(
|
text("\u{f64d}").font(iced::Font::with_name("bootstrap-icons")).size(11),
|
||||||
text("○").size(8).style(|_: &Theme| text::Style {
|
text(" Add").size(11),
|
||||||
color: Some(Color::from_rgb(0.4, 0.4, 0.4)),
|
].align_y(Alignment::Center).spacing(3),
|
||||||
})
|
)
|
||||||
).padding(Padding { top: 0.0, left: 4.0, right: 4.0, bottom: 0.0 }),
|
.on_press(Message::AddScannedGame(lname, d, e))
|
||||||
|
.style(button::primary)
|
||||||
|
.padding([4, 8]);
|
||||||
|
|
||||||
|
game_items.push(sub_card(
|
||||||
|
row![
|
||||||
|
text("◇").size(10).style(move |_: &Theme| text::Style { color: Some(dim) }),
|
||||||
column![
|
column![
|
||||||
text(display).size(13).style(|_: &Theme| text::Style {
|
text(display).size(12),
|
||||||
color: Some(Color::from_rgb(0.8, 0.8, 0.8)),
|
text(exe).size(9).style(move |_: &Theme| text::Style { color: Some(dim) }),
|
||||||
}),
|
|
||||||
text(exe).size(10).style(|_: &Theme| text::Style {
|
|
||||||
color: Some(Color::from_rgb(0.4, 0.4, 0.4)),
|
|
||||||
}),
|
|
||||||
].spacing(1),
|
].spacing(1),
|
||||||
iced::widget::horizontal_space(),
|
iced::widget::horizontal_space(),
|
||||||
add_btn,
|
add_btn,
|
||||||
]
|
]
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.spacing(4),
|
.spacing(8),
|
||||||
)
|
));
|
||||||
.padding(Padding { top: 2.0, left: 2.0, right: 0.0, bottom: 2.0 })
|
}
|
||||||
.into(),
|
if results.is_empty() && !scan_busy {
|
||||||
);
|
game_items.push(
|
||||||
|
container(
|
||||||
|
text("No additional games detected.").size(11)
|
||||||
|
.style(move |_: &Theme| text::Style { color: Some(muted) })
|
||||||
|
)
|
||||||
|
.padding(Padding::from([4, 0]))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if results.is_empty() && !scan_busy {
|
|
||||||
rows.push(
|
|
||||||
container(
|
|
||||||
text("No new games found in this prefix.").size(11).style(|_: &Theme| text::Style {
|
|
||||||
color: Some(Color::from_rgb(0.45, 0.45, 0.45)),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.padding(Padding { top: 6.0, left: 2.0, right: 0.0, bottom: 0.0 })
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Configured games section ────────────────────────────────────────────
|
// Configured games
|
||||||
if !l.games.is_empty() {
|
for g in &l.games {
|
||||||
rows.push(
|
let lname = l.name.clone();
|
||||||
container(
|
let gname = g.name.clone();
|
||||||
column![
|
let game_installed = g.full_exe_path(l).exists();
|
||||||
text("Games").size(12).style(|_: &Theme| text::Style {
|
|
||||||
color: Some(Color::from_rgb(0.5, 0.5, 0.5)),
|
// Play button
|
||||||
}),
|
let play = button(
|
||||||
iced::widget::horizontal_rule(1),
|
text("\u{f4f4}").font(iced::Font::with_name("bootstrap-icons")).size(13)
|
||||||
]
|
|
||||||
.spacing(4),
|
|
||||||
)
|
)
|
||||||
.padding(Padding { top: 8.0, left: 2.0, right: 0.0, bottom: 0.0 })
|
.on_press_maybe(game_installed.then_some(Message::Play(lname.clone(), gname.clone())))
|
||||||
.into(),
|
.style(button::primary)
|
||||||
|
.padding([6, 10]);
|
||||||
|
|
||||||
|
// Status dot
|
||||||
|
let status_dot_color = if game_installed { green } else { muted };
|
||||||
|
let status_dot = text("●").size(7).style(move |_: &Theme| text::Style {
|
||||||
|
color: Some(status_dot_color),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Overlay toggle pills
|
||||||
|
let gm_active = g.gamemode;
|
||||||
|
let hud_active = g.mangohud;
|
||||||
|
let gs_active = g.gamescope.is_some();
|
||||||
|
|
||||||
|
let pill = |label: &'static str, active: bool, msg: Message| -> Element<'a, Message> {
|
||||||
|
let (fg, bg_alpha) = if active {
|
||||||
|
(accent, 0.18)
|
||||||
|
} else {
|
||||||
|
(muted, 0.0)
|
||||||
|
};
|
||||||
|
button(
|
||||||
|
text(label).size(9).style(move |_: &Theme| text::Style { color: Some(fg) })
|
||||||
|
)
|
||||||
|
.on_press(msg)
|
||||||
|
.style(move |theme: &Theme, status| {
|
||||||
|
let mut s = (button::secondary)(theme, status);
|
||||||
|
s.background = Some(Background::Color(Color { r: 0.55, g: 0.75, b: 1.0, a: bg_alpha }));
|
||||||
|
s.border.radius = 10.0.into();
|
||||||
|
s
|
||||||
|
})
|
||||||
|
.padding([2, 8])
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let overlays = row![
|
||||||
|
pill("GM", gm_active, Message::ToggleGameMode(lname.clone(), gname.clone())),
|
||||||
|
pill("HUD", hud_active, Message::ToggleMangoHud(lname.clone(), gname.clone())),
|
||||||
|
pill("GS", gs_active, Message::ToggleGamescope(lname.clone(), gname.clone())),
|
||||||
|
].spacing(4);
|
||||||
|
|
||||||
|
// Remove
|
||||||
|
let remove_game = button(
|
||||||
|
text("\u{f659}").font(iced::Font::with_name("bootstrap-icons")).size(11)
|
||||||
|
)
|
||||||
|
.on_press(Message::RemoveGame(lname, gname))
|
||||||
|
.style(button::danger)
|
||||||
|
.padding([3, 6]);
|
||||||
|
|
||||||
|
game_items.push(sub_card(
|
||||||
|
row![
|
||||||
|
play,
|
||||||
|
container(status_dot).padding(Padding { top: 0.0, left: 2.0, right: 2.0, bottom: 0.0 }),
|
||||||
|
text(&g.display).size(13),
|
||||||
|
iced::widget::horizontal_space(),
|
||||||
|
overlays,
|
||||||
|
container(remove_game).padding(Padding { top: 0.0, left: 8.0, right: 0.0, bottom: 0.0 }),
|
||||||
|
]
|
||||||
|
.align_y(Alignment::Center)
|
||||||
|
.spacing(6),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.push(
|
||||||
|
Column::with_children(game_items).spacing(4).into(),
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
|
// Empty state — no games, no scan results
|
||||||
for g in &l.games {
|
|
||||||
let lname = l.name.clone();
|
let lname = l.name.clone();
|
||||||
let gname = g.name.clone();
|
let lname2 = l.name.clone();
|
||||||
let game_installed = g.full_exe_path(l).exists();
|
let scan_btn = button(
|
||||||
|
|
||||||
let play = button(
|
|
||||||
text("▶").size(12)
|
|
||||||
)
|
|
||||||
.on_press_maybe(game_installed.then_some(Message::Play(lname.clone(), gname.clone())))
|
|
||||||
.style(button::primary)
|
|
||||||
.padding([5, 10]);
|
|
||||||
|
|
||||||
let gm_label = if g.gamemode { "GM ✓" } else { "GM" };
|
|
||||||
let hud_label = if g.mangohud { "HUD ✓" } else { "HUD" };
|
|
||||||
let gs_label = if g.gamescope.is_some() { "GS ✓" } else { "GS" };
|
|
||||||
|
|
||||||
let gm_btn = button(text(gm_label).size(10))
|
|
||||||
.on_press(Message::ToggleGameMode(lname.clone(), gname.clone()))
|
|
||||||
.style(if g.gamemode { button::primary } else { button::secondary })
|
|
||||||
.padding([3, 7]);
|
|
||||||
|
|
||||||
let hud_btn = button(text(hud_label).size(10))
|
|
||||||
.on_press(Message::ToggleMangoHud(lname.clone(), gname.clone()))
|
|
||||||
.style(if g.mangohud { button::primary } else { button::secondary })
|
|
||||||
.padding([3, 7]);
|
|
||||||
|
|
||||||
let gs_btn = button(text(gs_label).size(10))
|
|
||||||
.on_press(Message::ToggleGamescope(lname.clone(), gname.clone()))
|
|
||||||
.style(if g.gamescope.is_some() { button::primary } else { button::secondary })
|
|
||||||
.padding([3, 7]);
|
|
||||||
|
|
||||||
let remove_game = button(
|
|
||||||
text("✕").size(10)
|
|
||||||
)
|
|
||||||
.on_press(Message::RemoveGame(lname, gname))
|
|
||||||
.style(button::danger)
|
|
||||||
.padding([3, 7]);
|
|
||||||
|
|
||||||
let install_indicator = if game_installed {
|
|
||||||
text("●").size(8).style(|_: &Theme| text::Style {
|
|
||||||
color: Some(Color::from_rgb(0.35, 0.85, 0.45)),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
text("●").size(8).style(|_: &Theme| text::Style {
|
|
||||||
color: Some(Color::from_rgb(0.5, 0.5, 0.5)),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let game_row = container(
|
|
||||||
row![
|
row![
|
||||||
play,
|
text("\u{f130}").font(iced::Font::with_name("bootstrap-icons")).size(11),
|
||||||
container(install_indicator).padding(Padding { top: 0.0, left: 2.0, right: 4.0, bottom: 0.0 }),
|
text(" Scan for games").size(11),
|
||||||
text(&g.display).size(13),
|
].align_y(Alignment::Center).spacing(4),
|
||||||
|
)
|
||||||
|
.on_press(Message::ScanGames(lname))
|
||||||
|
.style(button::secondary);
|
||||||
|
let browse_btn = button(
|
||||||
|
row![
|
||||||
|
text("\u{f3e8}").font(iced::Font::with_name("bootstrap-icons")).size(11),
|
||||||
|
text(" Browse…").size(11),
|
||||||
|
].align_y(Alignment::Center).spacing(4),
|
||||||
|
)
|
||||||
|
.on_press(Message::BrowseGameExe(lname2))
|
||||||
|
.style(button::secondary);
|
||||||
|
|
||||||
|
sections.push(sub_card(
|
||||||
|
row![
|
||||||
|
text("No games configured").size(12)
|
||||||
|
.style(move |_: &Theme| text::Style { color: Some(muted) }),
|
||||||
iced::widget::horizontal_space(),
|
iced::widget::horizontal_space(),
|
||||||
row![gm_btn, hud_btn, gs_btn].spacing(3),
|
scan_btn,
|
||||||
container(remove_game).padding(Padding { top: 0.0, left: 6.0, right: 0.0, bottom: 0.0 }),
|
browse_btn,
|
||||||
]
|
]
|
||||||
.align_y(Alignment::Center)
|
.align_y(Alignment::Center)
|
||||||
.spacing(4),
|
.spacing(6),
|
||||||
)
|
));
|
||||||
.padding(Padding { top: 3.0, left: 0.0, right: 0.0, bottom: 3.0 })
|
|
||||||
.width(Length::Fill)
|
|
||||||
.style(|theme: &Theme| {
|
|
||||||
let p = theme.extended_palette();
|
|
||||||
container::Style {
|
|
||||||
background: Some(Background::Color(Color {
|
|
||||||
a: 0.3,
|
|
||||||
..p.background.strong.color
|
|
||||||
})),
|
|
||||||
border: Border {
|
|
||||||
color: Color::TRANSPARENT,
|
|
||||||
width: 0.0,
|
|
||||||
radius: 4.0.into(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
rows.push(game_row.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Add game form ─────────────────────────────────────────────────────
|
||||||
if let Some((name_val, exe_val)) = add_form {
|
if let Some((name_val, exe_val)) = add_form {
|
||||||
let lname = l.name.clone();
|
let lname = l.name.clone();
|
||||||
let lname2 = l.name.clone();
|
let lname2 = l.name.clone();
|
||||||
@@ -1080,68 +1117,71 @@ fn launcher_card<'a>(
|
|||||||
.on_press(Message::BrowseGameExe(lname5))
|
.on_press(Message::BrowseGameExe(lname5))
|
||||||
.style(button::secondary);
|
.style(button::secondary);
|
||||||
let can_confirm = !name_val.trim().is_empty() && !exe_val.trim().is_empty();
|
let can_confirm = !name_val.trim().is_empty() && !exe_val.trim().is_empty();
|
||||||
let confirm_btn = button(text("Add Game").size(11))
|
let confirm_btn = button(
|
||||||
|
row![
|
||||||
|
text("\u{f64d}").font(iced::Font::with_name("bootstrap-icons")).size(11),
|
||||||
|
text(" Add Game").size(11),
|
||||||
|
].align_y(Alignment::Center).spacing(3),
|
||||||
|
)
|
||||||
.on_press_maybe(can_confirm.then_some(Message::AddGameConfirm(lname3)))
|
.on_press_maybe(can_confirm.then_some(Message::AddGameConfirm(lname3)))
|
||||||
.style(button::primary);
|
.style(button::primary);
|
||||||
let cancel_btn = button(text("Cancel").size(11))
|
let cancel_btn = button(text("Cancel").size(11))
|
||||||
.on_press(Message::AddGamePressed(lname4))
|
.on_press(Message::AddGamePressed(lname4))
|
||||||
.style(button::secondary);
|
.style(button::secondary);
|
||||||
rows.push(
|
sections.push(sub_card(
|
||||||
container(
|
column![
|
||||||
column![
|
text("Add Game").size(11).style(move |_: &Theme| text::Style { color: Some(subtle) }),
|
||||||
text("Add Game Manually").size(12).style(|_: &Theme| text::Style {
|
row![name_input, exe_input, browse_btn]
|
||||||
color: Some(Color::from_rgb(0.5, 0.5, 0.5)),
|
.align_y(Alignment::Center)
|
||||||
}),
|
.spacing(6),
|
||||||
row![name_input, exe_input, browse_btn]
|
container(
|
||||||
.align_y(Alignment::Center)
|
row![cancel_btn, confirm_btn].spacing(6),
|
||||||
.spacing(6),
|
)
|
||||||
container(
|
.width(Length::Fill)
|
||||||
row![cancel_btn, confirm_btn].spacing(6),
|
.align_x(Alignment::End),
|
||||||
)
|
]
|
||||||
.width(Length::Fill)
|
.spacing(6),
|
||||||
.align_x(Alignment::End),
|
));
|
||||||
]
|
|
||||||
.spacing(6),
|
|
||||||
)
|
|
||||||
.padding(Padding { top: 8.0, left: 0.0, right: 0.0, bottom: 0.0 })
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Game toolbar ──────────────────────────────────────────────────────────
|
// ── Toolbar ───────────────────────────────────────────────────────────
|
||||||
let lname_add = l.name.clone();
|
let lname_add = l.name.clone();
|
||||||
let lname_scan = l.name.clone();
|
let lname_scan = l.name.clone();
|
||||||
let lname_browse = l.name.clone();
|
let lname_browse = l.name.clone();
|
||||||
|
|
||||||
let add_label = if add_form.is_some() { "Cancel" } else { "+ Add Game" };
|
let add_label = if add_form.is_some() { "Cancel" } else { "+ Add" };
|
||||||
let add_game_btn = button(text(add_label).size(11))
|
let add_game_btn = button(text(add_label).size(10))
|
||||||
.on_press(Message::AddGamePressed(lname_add))
|
.on_press(Message::AddGamePressed(lname_add))
|
||||||
.style(button::secondary);
|
.style(button::secondary)
|
||||||
|
.padding([3, 8]);
|
||||||
|
|
||||||
let scan_btn = button(text("Scan").size(11))
|
let scan_btn = button(
|
||||||
|
text("\u{f130}").font(iced::Font::with_name("bootstrap-icons")).size(10)
|
||||||
|
)
|
||||||
.on_press_maybe((!scan_busy).then_some(Message::ScanGames(lname_scan)))
|
.on_press_maybe((!scan_busy).then_some(Message::ScanGames(lname_scan)))
|
||||||
.style(button::secondary);
|
.style(button::secondary)
|
||||||
|
.padding([3, 6]);
|
||||||
|
|
||||||
let browse_btn = button(text("Browse exe…").size(11))
|
let browse_btn = button(
|
||||||
|
text("\u{f3e8}").font(iced::Font::with_name("bootstrap-icons")).size(10)
|
||||||
|
)
|
||||||
.on_press(Message::BrowseGameExe(lname_browse))
|
.on_press(Message::BrowseGameExe(lname_browse))
|
||||||
.style(button::secondary);
|
.style(button::secondary)
|
||||||
|
.padding([3, 6]);
|
||||||
|
|
||||||
rows.push(
|
sections.push(
|
||||||
container(
|
row![
|
||||||
row![
|
add_game_btn,
|
||||||
add_game_btn,
|
iced::widget::horizontal_space(),
|
||||||
iced::widget::horizontal_space(),
|
scan_btn,
|
||||||
scan_btn,
|
browse_btn,
|
||||||
browse_btn,
|
]
|
||||||
]
|
.align_y(Alignment::Center)
|
||||||
.align_y(Alignment::Center)
|
.spacing(4)
|
||||||
.spacing(6),
|
|
||||||
)
|
|
||||||
.padding(Padding { top: 8.0, right: 0.0, bottom: 0.0, left: 0.0 })
|
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Column::with_children(rows).spacing(5).into()
|
Column::with_children(sections).spacing(10).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn context_menu_card(l: &crate::config::Launcher) -> Element<'_, Message> {
|
fn context_menu_card(l: &crate::config::Launcher) -> Element<'_, Message> {
|
||||||
|
|||||||
Reference in New Issue
Block a user