use iced::widget::{button, column, container, image, mouse_area, pick_list, row, text, text_input};
use iced::{color, Element, Length};
use crate::app::{Message, View};
use crate::views::mod_details::ModDetailsState;
use crate::views::save_details::SaveDetailsState;
pub fn view<'a>(
active_view: &View,
profiles: &'a [modde_core::profile::ProfileSummary],
active_profile: &'a Option<String>,
experiment_depth: usize,
new_profile_name: &'a str,
selected_game: &'a Option<String>,
mod_details: Option<&'a ModDetailsState>,
save_details: Option<&'a SaveDetailsState>,
) -> Element<'a, Message> {
let nav_button = |label: &'a str, target: View, current: &View| -> Element<'a, Message> {
let is_active = std::mem::discriminant(&target) == std::mem::discriminant(current);
let btn = button(text(label).size(14))
.width(Length::Fill)
.padding([6, 12]);
if is_active {
btn.style(button::primary).into()
} else {
btn.on_press(Message::SwitchView(target))
.style(button::secondary)
.into()
}
};
let nav = column![
nav_button("Mod List", View::ModList, active_view),
nav_button("Saves", View::Saves, active_view),
nav_button("Browse Nexus", View::BrowseNexus, active_view),
nav_button("Collections", View::Collections, active_view),
nav_button(
"Wabbajack",
View::WabbajackInstaller(Default::default()),
active_view,
),
nav_button("Downloads", View::Downloads, active_view),
nav_button("Verify", View::Verify, active_view),
nav_button("Settings", View::Settings, active_view),
]
.spacing(4);
let profile_names: Vec<String> = profiles.iter().map(|p| p.name.clone()).collect();
let profile_selector = column![
text("Profile").size(12),
pick_list(
profile_names,
active_profile.clone(),
Message::SwitchProfile,
)
.width(Length::Fill)
.placeholder("No profiles"),
]
.spacing(4);
let mut profile_actions = row![].spacing(4);
if let Some(name) = active_profile {
let name_del = name.clone();
profile_actions = profile_actions.push(
button(text("Del").size(11))
.on_press(Message::DeleteProfile(name_del))
.style(button::danger)
.padding([3, 8]),
);
let name_fork = name.clone();
profile_actions = profile_actions.push(
button(text("Fork").size(11))
.on_press(Message::ForkProfile {
source: name_fork.clone(),
new_name: format!("{name_fork}-fork"),
})
.style(button::secondary)
.padding([3, 8]),
);
}
let new_profile_section = column![
text("New Profile").size(12),
text_input("Profile name...", new_profile_name)
.on_input(Message::NewProfileNameChanged)
.padding(4)
.size(13)
.width(Length::Fill),
button(text("Create").size(12))
.on_press_maybe(
if new_profile_name.is_empty() || selected_game.is_none() {
None
} else {
Some(Message::CreateProfile {
name: new_profile_name.to_string(),
game_id: selected_game.clone().unwrap(),
})
},
)
.style(button::success)
.padding([4, 12])
.width(Length::Fill),
]
.spacing(4);
let mut sections = column![
nav,
iced::widget::rule::horizontal(1),
profile_selector,
profile_actions,
iced::widget::rule::horizontal(1),
new_profile_section,
]
.spacing(10)
.padding(12)
.width(Length::Fixed(190.0));
if experiment_depth > 0 {
let experiment_section = column![
text(format!("Experiment (depth {experiment_depth})"))
.size(12)
.color(color!(0xFFAA44)),
row![
button(text("Rollback").size(11))
.on_press(Message::RollbackExperiment)
.style(button::danger)
.padding([3, 8]),
button(text("Commit").size(11))
.on_press(Message::CommitExperiment)
.style(button::success)
.padding([3, 8]),
]
.spacing(4),
]
.spacing(4);
sections = sections.push(iced::widget::rule::horizontal(1));
sections = sections.push(experiment_section);
} else if active_profile.is_some() {
sections = sections.push(iced::widget::rule::horizontal(1));
sections = sections.push(
button(text("Try Profile").size(11))
.on_press(Message::TryProfile)
.style(button::secondary)
.padding([3, 8])
.width(Length::Fill),
);
}
if let Some(details) = mod_details {
sections = sections.push(iced::widget::rule::horizontal(1));
sections = sections.push(render_mod_details(details));
} else if let Some(details) = save_details {
sections = sections.push(iced::widget::rule::horizontal(1));
sections = sections.push(render_save_details(details));
}
iced::widget::row![
iced::widget::scrollable(container(sections).style(container::rounded_box))
.height(Length::Fill),
iced::widget::rule::vertical(1),
]
.into()
}
const SUMMARY_MAX: usize = 160;
fn render_mod_details(state: &ModDetailsState) -> Element<'_, Message> {
if state.loading {
return column![
text(&state.name).size(13),
text("Loading…").size(11).color(color!(0x888888)),
]
.spacing(4)
.width(Length::Fill)
.into();
}
if let Some(ref err) = state.error {
return column![
text(&state.name).size(13),
text(err.as_str()).size(11).color(color!(0xFF6666)),
button(text("Open in Nexus").size(11))
.on_press(Message::OpenModPage)
.style(button::text)
.padding([2, 4]),
]
.spacing(4)
.width(Length::Fill)
.into();
}
let thumb_slot: Element<Message> = match &state.thumbnail {
Some(handle) => image(handle.clone())
.width(Length::Fill)
.height(Length::Fixed(96.0))
.content_fit(iced::ContentFit::Contain)
.into(),
None => container(text("…").size(14).color(color!(0x888888)))
.width(Length::Fill)
.height(Length::Fixed(96.0))
.center_x(Length::Fill)
.center_y(Length::Fixed(96.0))
.style(container::bordered_box)
.into(),
};
let thumb_area: Element<Message> = if state.gallery.len() > 1 {
mouse_area(thumb_slot)
.on_press(Message::ModGalleryNext)
.into()
} else {
thumb_slot
};
let gallery_indicator: Element<Message> = if state.gallery.len() > 1 {
text(format!(
"{} / {}",
state.gallery_index + 1,
state.gallery.len()
))
.size(10)
.color(color!(0x888888))
.into()
} else {
iced::widget::Space::new().into()
};
let author_version: Element<Message> = if state.author.is_empty() {
text(&state.version).size(11).color(color!(0xAAAAAA)).into()
} else {
text(format!("by {} · v{}", state.author, state.version))
.size(11)
.color(color!(0xAAAAAA))
.into()
};
let summary_text: Element<Message> = match state.summary.as_deref() {
Some(s) if !s.is_empty() => {
let truncated = if s.chars().count() > SUMMARY_MAX {
let mut t: String = s.chars().take(SUMMARY_MAX).collect();
t.push('…');
t
} else {
s.to_string()
};
text(truncated).size(11).into()
}
_ => iced::widget::Space::new().into(),
};
let disabled = state.action_pending;
let endorsed = state.endorse_status.as_deref() == Some("Endorsed");
let endorse_label = if endorsed { "✓ Endorsed" } else { "Endorse" };
let endorse_style = if endorsed {
button::success
} else if state.endorse_status.is_some() {
button::primary
} else {
button::secondary
};
let mut endorse_btn = button(text(endorse_label).size(11))
.style(endorse_style)
.padding([3, 8])
.width(Length::Fill);
if !disabled && state.endorse_status.is_some() {
endorse_btn = endorse_btn.on_press(Message::ModEndorseToggle);
}
let tracked = state.is_tracked == Some(true);
let track_label = if tracked { "Tracked" } else { "Track" };
let track_style = if tracked {
button::success
} else if state.is_tracked.is_some() {
button::primary
} else {
button::secondary
};
let mut track_btn = button(text(track_label).size(11))
.style(track_style)
.padding([3, 8])
.width(Length::Fill);
if !disabled && state.is_tracked.is_some() {
track_btn = track_btn.on_press(Message::ModTrackToggle);
}
let action_row = row![endorse_btn, track_btn].spacing(4);
let count_line: Element<Message> = if state.endorsement_count > 0 {
text(format!("{} endorsements", state.endorsement_count))
.size(10)
.color(color!(0x888888))
.into()
} else {
iced::widget::Space::new().into()
};
let link_button = button(text("Open in Nexus").size(11))
.on_press(Message::OpenModPage)
.style(button::text)
.padding([2, 4]);
column![
thumb_area,
gallery_indicator,
text(&state.name).size(13),
author_version,
summary_text,
action_row,
count_line,
link_button,
]
.spacing(4)
.width(Length::Fill)
.into()
}
fn render_save_details(state: &SaveDetailsState) -> Element<'_, Message> {
use iced::widget::scrollable;
use modde_core::save::FingerprintCheck;
let mut col = column![].spacing(4).width(Length::Fill);
col = col.push(
text(state.formatted_date())
.size(12)
.color(color!(0xAAAAAA)),
);
col = col.push(text(state.display_title()).size(13));
if let Some(ref cat) = state.category {
col = col.push(
text(format!("[{cat}]"))
.size(11)
.color(color!(0x888888)),
);
}
if let Some(ref name) = state.profile_name {
col = col.push(
text(format!("Profile: {name}"))
.size(11)
.color(color!(0xAAAAAA)),
);
}
col = col.push(
text(format!("{} file(s)", state.file_count))
.size(11),
);
match &state.file_paths {
Some(paths) if !paths.is_empty() => {
let file_list = paths.iter().fold(column![].spacing(1), |col, path| {
let display = std::path::Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(path);
col.push(text(display).size(10).color(color!(0x888888)))
});
col = col.push(
scrollable(file_list)
.height(Length::Fixed(80.0)),
);
}
Some(_) => {} None => {
col = col.push(
text("Loading files...")
.size(10)
.color(color!(0x888888)),
);
}
}
if let Some(ref fp) = state.fingerprint {
let fp_element: Element<Message> = match &state.compatibility {
Some(FingerprintCheck::Compatible) => {
text(format!("Mods: {} [compatible]", fp.short_hash()))
.size(11)
.color(color!(0x44AA44))
.into()
}
Some(FingerprintCheck::Mismatch { removed, added }) => {
column![
text(format!("Mods: {} [mismatch]", fp.short_hash()))
.size(11)
.color(color!(0xFF6644)),
text(format!("-{} removed, +{} added", removed.len(), added.len()))
.size(10)
.color(color!(0xFF6644)),
]
.spacing(1)
.into()
}
Some(FingerprintCheck::NoFingerprint) | None => {
text(format!("Mods: {}", fp.short_hash()))
.size(11)
.color(color!(0x888888))
.into()
}
};
col = col.push(fp_element);
}
col = col.push(
button(text("Restore").size(12))
.on_press(Message::RestoreSaveSnapshot(state.commit_id.clone()))
.style(button::secondary)
.padding([4, 8])
.width(Length::Fill),
);
col = col.push(
text(&state.short_id)
.size(10)
.color(color!(0x666666)),
);
col.into()
}