Skip to main content

modde_ui/views/
settings.rs

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
11/// Render the settings view.
12pub fn view(state: SettingsState) -> Element<'static, Message> {
13    let title = text("Settings").size(20);
14
15    // Nexus API key with validation status
16    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    // Game install path
115    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    // Download directory
141    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    // Stock game snapshot with verify button
164    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    // Theme selection
195    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}