diff --git a/src/settings/app.rs b/src/settings/app.rs index 3c8015c..bb45ed6 100644 --- a/src/settings/app.rs +++ b/src/settings/app.rs @@ -14,6 +14,7 @@ struct State { flare_settings: FlareSettings, theme: Theme, current_tab: SettingsTab, + selected_extension: Option, } impl State { @@ -25,6 +26,7 @@ impl State { flare_settings: FlareSettings::load(), theme: Theme::default(), current_tab: SettingsTab::default(), + selected_extension: None, }, Task::none(), ) @@ -32,8 +34,10 @@ impl State { } fn update(state: &mut State, message: SettingsMessage) -> Task { - if let SettingsMessage::TabChanged(tab) = message { - state.current_tab = tab; + match &message { + SettingsMessage::TabChanged(tab) => state.current_tab = *tab, + SettingsMessage::ExtensionSelected(idx) => state.selected_extension = Some(*idx), + _ => {} } handle_message(&message, &mut state.preferences, &mut state.flare_settings); Task::none() @@ -48,6 +52,7 @@ fn view(state: &State) -> Element<'_, SettingsMessage> { &state.flare_settings, &state.theme, state.current_tab, + state.selected_extension, )) .width(Length::Fill) .height(Length::Fill) diff --git a/src/settings/extensions.rs b/src/settings/extensions.rs new file mode 100644 index 0000000..86645d9 --- /dev/null +++ b/src/settings/extensions.rs @@ -0,0 +1,235 @@ +use iced::widget::{ + button, checkbox, column, container, pick_list, row, rule, scrollable, text, text_input, +}; +use iced::{Background, Border, Color, Element, Length}; +use serde_json::Value; + +use crate::extensions::{Extension, Preference, PreferenceType}; +use crate::preferences::PreferenceStore; +use crate::theme::Theme; + +use super::SettingsMessage; + +pub fn render_extensions_tab<'a>( + extensions: &'a [Extension], + preferences: &'a PreferenceStore, + theme: &'a Theme, + selected_extension: Option, +) -> Element<'a, SettingsMessage> { + let text_color = theme.colors.text; + let bg_color = theme.colors.background; + + let mut ext_list = column![].spacing(2); + for (idx, ext) in extensions.iter().enumerate() { + let is_selected = selected_extension == Some(idx); + let item_bg = if is_selected { + theme.colors.selection + } else { + Color::TRANSPARENT + }; + + ext_list = ext_list.push( + button(text(&ext.manifest.title).color(text_color)) + .on_press(SettingsMessage::ExtensionSelected(idx)) + .width(Length::Fill) + .style(move |_theme, status| { + let bg = match status { + button::Status::Hovered if !is_selected => theme.colors.text_10, + _ => item_bg, + }; + button::Style { + background: Some(Background::Color(bg)), + text_color, + border: Border::default(), + ..Default::default() + } + }), + ); + } + + let left_panel = container(scrollable(ext_list).height(Length::Fill)) + .width(Length::FillPortion(1)) + .height(Length::Fill) + .style(move |_| container::Style { + background: Some(bg_color.into()), + ..Default::default() + }); + + let right_panel: Element<'a, SettingsMessage> = if let Some(idx) = selected_extension { + if let Some(ext) = extensions.get(idx) { + render_extension_preferences(ext, preferences, extensions, theme) + } else { + container(text("Select an extension").color(theme.colors.text_60)) + .center(Length::Fill) + .into() + } + } else { + container(text("Select an extension").color(theme.colors.text_60)) + .center(Length::Fill) + .into() + }; + + let right_container = container(right_panel) + .width(Length::FillPortion(2)) + .height(Length::Fill) + .style(move |_| container::Style { + background: Some(bg_color.into()), + ..Default::default() + }); + + row![ + left_panel, + rule::vertical(1).style(|iced_theme| rule::Style { + color: theme.colors.border_10, + ..rule::default(iced_theme) + }), + right_container + ] + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +fn render_extension_preferences<'a>( + ext: &'a Extension, + preferences: &'a PreferenceStore, + extensions: &'a [Extension], + theme: &'a Theme, +) -> Element<'a, SettingsMessage> { + let text_color = theme.colors.text; + let bg_color = theme.colors.background; + + let mut content = column![text(&ext.manifest.title).size(20).color(text_color),].spacing(16); + + if let Some(prefs) = &ext.manifest.preferences { + for pref in prefs { + content = content.push(render_preference( + &ext.manifest.name, + pref, + preferences, + extensions, + theme, + )); + } + } + + for cmd in &ext.manifest.commands { + if let Some(prefs) = &cmd.preferences { + if !prefs.is_empty() { + content = content.push( + text(format!("Command: {}", cmd.title)) + .size(14) + .color(text_color), + ); + for pref in prefs { + content = content.push(render_preference( + &ext.manifest.name, + pref, + preferences, + extensions, + theme, + )); + } + } + } + } + + scrollable(content) + .style(move |iced_theme, status| scrollable::Style { + container: container::Style { + background: Some(bg_color.into()), + ..Default::default() + }, + ..scrollable::default(iced_theme, status) + }) + .into() +} + +fn render_preference<'a>( + extension_id: &'a str, + pref: &'a Preference, + store: &'a PreferenceStore, + extensions: &'a [Extension], + theme: &'a Theme, +) -> Element<'a, SettingsMessage> { + let text_color = theme.colors.text; + let current_value = store.get_value(extension_id, &pref.name, extensions); + let ext_id = extension_id.to_string(); + let pref_name = pref.name.clone(); + + let title = pref.title.as_deref().unwrap_or(&pref.name); + + let input: Element<'a, SettingsMessage> = match pref.preference_type { + PreferenceType::Textfield | PreferenceType::Password => { + let value = current_value + .and_then(|v| v.as_str().map(String::from)) + .unwrap_or_default(); + + let is_secure = pref.preference_type == PreferenceType::Password; + let placeholder = pref.placeholder.as_deref().unwrap_or(""); + + let ext_id_clone = ext_id.clone(); + let pref_name_clone = pref_name.clone(); + + text_input(placeholder, &value) + .secure(is_secure) + .on_input(move |v| SettingsMessage::PreferenceChanged { + extension_id: ext_id_clone.clone(), + key: pref_name_clone.clone(), + value: Value::String(v), + }) + .into() + } + PreferenceType::Checkbox => { + let checked = current_value.and_then(|v| v.as_bool()).unwrap_or(false); + let label = pref.label.as_deref().unwrap_or(""); + + checkbox(label, checked) + .on_toggle(move |v| SettingsMessage::PreferenceChanged { + extension_id: ext_id.clone(), + key: pref_name.clone(), + value: Value::Bool(v), + }) + .into() + } + PreferenceType::Dropdown => { + let options: Vec = pref + .data + .as_ref() + .map(|d| d.iter().map(|item| item.title.clone()).collect()) + .unwrap_or_default(); + + let selected = current_value + .and_then(|v| v.as_str().map(String::from)) + .and_then(|val| { + pref.data.as_ref().and_then(|d| { + d.iter() + .find(|item| item.value == val) + .map(|item| item.title.clone()) + }) + }); + + let data = pref.data.clone(); + pick_list(options, selected, move |title| { + let value = data + .as_ref() + .and_then(|d| d.iter().find(|item| item.title == title)) + .map(|item| Value::String(item.value.clone())) + .unwrap_or(Value::Null); + + SettingsMessage::PreferenceChanged { + extension_id: ext_id.clone(), + key: pref_name.clone(), + value, + } + }) + .into() + } + _ => text("Unsupported preference type").into(), + }; + + row![text(title).width(150).color(text_color), input] + .spacing(10) + .align_y(iced::Alignment::Center) + .into() +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 929d674..f6bf140 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -1,4 +1,5 @@ mod app; +mod extensions; mod view; pub use app::run; @@ -15,6 +16,7 @@ pub enum SettingsTab { #[derive(Clone, Debug)] pub enum SettingsMessage { TabChanged(SettingsTab), + ExtensionSelected(usize), PreferenceChanged { extension_id: String, key: String, @@ -32,6 +34,7 @@ fn handle_message( ) { match message { SettingsMessage::TabChanged(_) => {} + SettingsMessage::ExtensionSelected(_) => {} SettingsMessage::PreferenceChanged { extension_id, key, diff --git a/src/settings/view.rs b/src/settings/view.rs index 64300c3..6a2d960 100644 --- a/src/settings/view.rs +++ b/src/settings/view.rs @@ -1,13 +1,11 @@ -use iced::widget::{ - button, checkbox, column, container, pick_list, radio, row, rule, scrollable, text, text_input, -}; +use iced::widget::{button, column, container, radio, row, rule, scrollable, text}; use iced::{Background, Border, Color, Element, Font, Length}; -use serde_json::Value; -use crate::extensions::{Extension, Preference, PreferenceType}; +use crate::extensions::Extension; use crate::preferences::{FlareSettings, PreferenceStore}; use crate::theme::Theme; +use super::extensions::render_extensions_tab; use super::{SettingsMessage, SettingsTab}; const ICON_FONT: Font = Font::with_name("Raycast-Icons"); @@ -87,7 +85,6 @@ fn tab_bar<'a>(current_tab: SettingsTab, theme: &'a Theme) -> Element<'a, Settin ] .align_x(iced::Alignment::Center), ) - .padding([12, 20]) .width(Length::Fill) .style(move |_| container::Style { background: Some(theme.colors.background.into()), @@ -102,10 +99,13 @@ pub fn settings_view<'a>( flare_settings: &'a FlareSettings, theme: &'a Theme, current_tab: SettingsTab, + selected_extension: Option, ) -> Element<'a, SettingsMessage> { let content: Element<'a, SettingsMessage> = match current_tab { SettingsTab::General => render_general_tab(flare_settings, theme), - SettingsTab::Extensions => render_extensions_tab(extensions, preferences, theme), + SettingsTab::Extensions => { + render_extensions_tab(extensions, preferences, theme, selected_extension) + } }; column![tab_bar(current_tab, theme), content] @@ -139,32 +139,6 @@ fn render_general_tab<'a>( .into() } -fn render_extensions_tab<'a>( - extensions: &'a [Extension], - preferences: &'a PreferenceStore, - theme: &'a Theme, -) -> Element<'a, SettingsMessage> { - let text_color = theme.colors.text; - let bg_color = theme.colors.background; - - let content = column![ - text("Extensions").size(20).color(text_color), - render_extension_settings(extensions, preferences, theme), - ] - .spacing(20) - .padding(20); - - scrollable(content) - .style(move |iced_theme, status| scrollable::Style { - container: container::Style { - background: Some(bg_color.into()), - ..Default::default() - }, - ..scrollable::default(iced_theme, status) - }) - .into() -} - fn render_flare_settings<'a>( settings: &'a FlareSettings, theme: &'a Theme, @@ -199,144 +173,3 @@ fn render_flare_settings<'a>( content.into() } - -fn render_extension_settings<'a>( - extensions: &'a [Extension], - preferences: &'a PreferenceStore, - theme: &'a Theme, -) -> Element<'a, SettingsMessage> { - let text_color = theme.colors.text; - let mut content = column![].spacing(10); - - for ext in extensions { - content = content.push( - text(format!("Extension: {}", ext.manifest.title)) - .size(16) - .color(text_color), - ); - - if let Some(prefs) = &ext.manifest.preferences { - for pref in prefs { - content = content.push(render_preference( - &ext.manifest.name, - pref, - preferences, - extensions, - theme, - )); - } - } - - for cmd in &ext.manifest.commands { - if let Some(prefs) = &cmd.preferences { - if !prefs.is_empty() { - content = content.push( - text(format!("Command: {}", cmd.title)) - .size(14) - .color(text_color), - ); - for pref in prefs { - content = content.push(render_preference( - &ext.manifest.name, - pref, - preferences, - extensions, - theme, - )); - } - } - } - } - } - - content.into() -} - -fn render_preference<'a>( - extension_id: &'a str, - pref: &'a Preference, - store: &'a PreferenceStore, - extensions: &'a [Extension], - theme: &'a Theme, -) -> Element<'a, SettingsMessage> { - let text_color = theme.colors.text; - let current_value = store.get_value(extension_id, &pref.name, extensions); - let ext_id = extension_id.to_string(); - let pref_name = pref.name.clone(); - - let title = pref.title.as_deref().unwrap_or(&pref.name); - - let input: Element<'a, SettingsMessage> = match pref.preference_type { - PreferenceType::Textfield | PreferenceType::Password => { - let value = current_value - .and_then(|v| v.as_str().map(String::from)) - .unwrap_or_default(); - - let is_secure = pref.preference_type == PreferenceType::Password; - let placeholder = pref.placeholder.as_deref().unwrap_or(""); - - let ext_id_clone = ext_id.clone(); - let pref_name_clone = pref_name.clone(); - - text_input(placeholder, &value) - .secure(is_secure) - .on_input(move |v| SettingsMessage::PreferenceChanged { - extension_id: ext_id_clone.clone(), - key: pref_name_clone.clone(), - value: Value::String(v), - }) - .into() - } - PreferenceType::Checkbox => { - let checked = current_value.and_then(|v| v.as_bool()).unwrap_or(false); - let label = pref.label.as_deref().unwrap_or(""); - - checkbox(label, checked) - .on_toggle(move |v| SettingsMessage::PreferenceChanged { - extension_id: ext_id.clone(), - key: pref_name.clone(), - value: Value::Bool(v), - }) - .into() - } - PreferenceType::Dropdown => { - let options: Vec = pref - .data - .as_ref() - .map(|d| d.iter().map(|item| item.title.clone()).collect()) - .unwrap_or_default(); - - let selected = current_value - .and_then(|v| v.as_str().map(String::from)) - .and_then(|val| { - pref.data.as_ref().and_then(|d| { - d.iter() - .find(|item| item.value == val) - .map(|item| item.title.clone()) - }) - }); - - let data = pref.data.clone(); - pick_list(options, selected, move |title| { - let value = data - .as_ref() - .and_then(|d| d.iter().find(|item| item.title == title)) - .map(|item| Value::String(item.value.clone())) - .unwrap_or(Value::Null); - - SettingsMessage::PreferenceChanged { - extension_id: ext_id.clone(), - key: pref_name.clone(), - value, - } - }) - .into() - } - _ => text("Unsupported preference type").into(), - }; - - row![text(title).width(150).color(text_color), input] - .spacing(10) - .align_y(iced::Alignment::Center) - .into() -}