1use std::path::PathBuf;
2
3use crate::views::selectable_text::text;
4use iced::widget::{button, column, pick_list, row, scrollable, text_input};
5use iced::{Alignment, Element, Length, color};
6
7use crate::action_button::{ButtonAction, DescribedButtonExt};
8use crate::app::{Message, NexusAuthStatus, SettingsState};
9use crate::semantics;
10
11pub fn view(state: SettingsState) -> Element<'static, Message> {
13 let title = text("Settings").size(20);
14
15 let nexus_status_widget: Element<'static, Message> = match &state.nexus_status {
17 Some(NexusAuthStatus::Checking) => {
18 text("Checking...").size(12).color(color!(0xAAAA44)).into()
19 }
20 Some(NexusAuthStatus::Valid {
21 username,
22 is_premium,
23 }) => {
24 let tier = if *is_premium { "Premium" } else { "Standard" };
25 text(format!("Logged in as {username} ({tier})"))
26 .size(12)
27 .color(color!(0x88CC88))
28 .into()
29 }
30 Some(NexusAuthStatus::Invalid(err)) => text(format!("Invalid: {err}"))
31 .size(12)
32 .color(color!(0xFF4444))
33 .into(),
34 None => text("Not validated").size(12).into(),
35 };
36
37 let game_path_str = state
38 .game_install_paths
39 .first()
40 .map(|install| install.path.display().to_string())
41 .unwrap_or_default();
42 let download_dir_str = state
43 .download_dir
44 .as_ref()
45 .map(|p| p.display().to_string())
46 .unwrap_or_default();
47 let nexus_source = state
48 .nexus_api_key_source
49 .as_ref()
50 .map(|source| format!("Using {}", source.label()))
51 .unwrap_or_else(|| "No key configured".to_string());
52 let show_hide_label = if state.nexus_api_key_visible {
53 "Hide"
54 } else {
55 "Show"
56 };
57
58 let api_key_section = column![
59 text("Nexus Mods API Key").size(14),
60 text("Required for downloading mods and browsing collections.").size(11),
61 row![
62 text_input("Enter your API key...", &state.nexus_api_key_draft)
63 .id(semantics::widget_id("settings.nexus_api_key"))
64 .secure(!state.nexus_api_key_visible)
65 .on_input(Message::SetNexusApiKeyDraft)
66 .padding(8)
67 .width(Length::Fill),
68 semantics::test_id(
69 "settings.nexus_api_key.toggle_visibility",
70 button(text(show_hide_label).size(13))
71 .style(button::secondary)
72 .padding([6, 12])
73 .on_action(ButtonAction::ToggleNexusApiKeyVisibility),
74 ),
75 ]
76 .spacing(8)
77 .align_y(Alignment::Center),
78 row![
79 semantics::test_id(
80 "settings.nexus_api_key.replace",
81 button(text("Replace").size(13))
82 .style(button::secondary)
83 .padding([6, 12])
84 .on_action(ButtonAction::ReplaceNexusApiKey),
85 ),
86 semantics::test_id(
87 "settings.nexus_api_key.remove_modde_config",
88 button(text("Remove modde config").size(13))
89 .style(button::secondary)
90 .padding([6, 12])
91 .on_action(ButtonAction::RemoveNexusConfigKey),
92 ),
93 semantics::test_id(
94 "settings.nexus_api_key.validate",
95 button(text("Validate").size(13))
96 .style(button::primary)
97 .padding([6, 12])
98 .on_action(ButtonAction::ValidateNexusKey),
99 ),
100 ]
101 .spacing(8)
102 .align_y(Alignment::Center),
103 text(nexus_source).size(12),
104 text(if state.nexus_config_key_exists {
105 "modde config key exists"
106 } else {
107 "No modde config key"
108 })
109 .size(11),
110 nexus_status_widget,
111 ]
112 .spacing(4);
113
114 let game_path_section = column![
116 text("Game Install Path").size(14),
117 text("Root directory of the game installation.").size(11),
118 row![
119 text_input("/path/to/game", &game_path_str,)
120 .id(semantics::widget_id("settings.game_path"))
121 .on_input(|s| Message::SetGamePath {
122 game_id: "default".to_string(),
123 path: PathBuf::from(s)
124 })
125 .padding(8)
126 .width(Length::Fill),
127 semantics::test_id(
128 "settings.game_path.browse",
129 button(text("Browse").size(13))
130 .style(button::secondary)
131 .padding([6, 12])
132 .on_action(ButtonAction::BrowseGamePath),
133 ),
134 ]
135 .spacing(8)
136 .align_y(Alignment::Center),
137 ]
138 .spacing(4);
139
140 let download_dir_section = column![
142 text("Download Directory").size(14),
143 text("Where downloaded mod archives are stored.").size(11),
144 row![
145 text_input("/path/to/downloads", &download_dir_str,)
146 .id(semantics::widget_id("settings.download_dir"))
147 .on_input(|s| Message::SetDownloadDir(PathBuf::from(s)))
148 .padding(8)
149 .width(Length::Fill),
150 semantics::test_id(
151 "settings.download_dir.browse",
152 button(text("Browse").size(13))
153 .style(button::secondary)
154 .padding([6, 12])
155 .on_action(ButtonAction::BrowseDownloadDir),
156 ),
157 ]
158 .spacing(8)
159 .align_y(Alignment::Center),
160 ]
161 .spacing(4);
162
163 let stock_game_section = column![
165 text("Stock Game Snapshot").size(14),
166 text("Create a snapshot of your clean game install for virtual deployment.").size(11),
167 row![
168 semantics::test_id(
169 "settings.stock_snapshot.create",
170 button(text("Create Snapshot").size(13))
171 .style(button::primary)
172 .padding([6, 14])
173 .on_action(ButtonAction::CreateStockSnapshot),
174 ),
175 semantics::test_id(
176 "settings.stock_snapshot.verify",
177 button(text("Verify Snapshot").size(13))
178 .style(button::secondary)
179 .padding([6, 14])
180 .on_action(ButtonAction::VerifyStockSnapshot),
181 ),
182 text(if state.has_stock_snapshot {
183 "Snapshot exists"
184 } else {
185 "No snapshot created"
186 })
187 .size(12),
188 ]
189 .spacing(12)
190 .align_y(Alignment::Center),
191 ]
192 .spacing(4);
193
194 let theme_options = vec![
196 "Dark".to_string(),
197 "Light".to_string(),
198 "Dracula".to_string(),
199 "Nord".to_string(),
200 "Gruvbox Dark".to_string(),
201 "Catppuccin Mocha".to_string(),
202 ];
203 let theme_section = column![
204 text("Theme").size(14),
205 pick_list(
206 theme_options,
207 Some(state.theme_name.clone()),
208 Message::SetTheme,
209 )
210 .width(Length::Fixed(200.0)),
211 ]
212 .spacing(4);
213
214 let content = scrollable(
215 column![
216 api_key_section,
217 iced::widget::rule::horizontal(1),
218 game_path_section,
219 iced::widget::rule::horizontal(1),
220 download_dir_section,
221 iced::widget::rule::horizontal(1),
222 stock_game_section,
223 iced::widget::rule::horizontal(1),
224 theme_section,
225 ]
226 .spacing(16)
227 .padding(16),
228 )
229 .height(Length::Fill);
230
231 column![title, iced::widget::rule::horizontal(1), content,]
232 .spacing(8)
233 .padding(16)
234 .width(Length::Fill)
235 .height(Length::Fill)
236 .into()
237}