commit e9243eb18f1a6ab0b8a70931c99d7e72e36f7381
parent a151e8921f824d5e4990af558692bab675656295
Author: Sylvia Ivory <git@sivory.net>
Date: Wed, 18 Jun 2025 20:21:28 -0700
Validate settings inputs
Diffstat:
3 files changed, 131 insertions(+), 32 deletions(-)
diff --git a/src/config.rs b/src/config.rs
@@ -1,5 +1,3 @@
-use std::fmt;
-
#[derive(Debug, Clone, PartialEq, strum::Display, strum::VariantArray)]
pub enum BackgroundMode {
Unsplash,
diff --git a/src/main.rs b/src/main.rs
@@ -1,21 +1,20 @@
use std::{cell::RefCell, rc::Rc};
-use chrono::{DateTime, Local};
+use chrono::{
+ DateTime, Local,
+ format::{Item, StrftimeItems},
+};
use iced::{
Color, Element, Font, Length, Size, Subscription, Task,
font::Weight,
time,
- widget::{
- button, center, column, combo_box, container, horizontal_space, row, stack, text,
- text_input,
- },
+ widget::{center, column, container, horizontal_space, row, stack, text},
window,
};
use background::{BackgroundKind, background};
-use config::{BackgroundMode, Config};
+use config::Config;
use icon::{icon, icon_button};
-use strum::VariantArray;
mod background;
mod config;
@@ -26,6 +25,8 @@ struct Fjordgard {
config: Rc<RefCell<Config>>,
time: DateTime<Local>,
background: BackgroundKind,
+ format_string: String,
+ format_parsed: Vec<Item<'static>>,
settings_window: Option<settings::Settings>,
main_window: window::Id,
@@ -54,12 +55,20 @@ enum Message {
impl Fjordgard {
fn new() -> (Self, Task<Message>) {
let (id, open) = window::open(window::Settings::default());
+ let config = Config::default();
+
+ let format_string = config.time_format.clone();
+ let format_parsed = StrftimeItems::new_lenient(&format_string)
+ .parse_to_owned()
+ .unwrap();
(
Self {
- config: Rc::new(RefCell::new(Config::default())),
+ config: Rc::new(RefCell::new(config)),
time: Local::now(),
background: BackgroundKind::Color(Color::from_rgb8(255, 255, 255)),
+ format_string,
+ format_parsed,
settings_window: None,
main_window: id,
@@ -67,6 +76,7 @@ impl Fjordgard {
open.map(|_| Message::MainWindowOpened),
)
}
+
fn title(&self, window_id: window::Id) -> String {
if window_id == self.main_window {
String::from("Fjordgard")
@@ -79,6 +89,16 @@ impl Fjordgard {
match msg {
Message::Tick(time) => {
self.time = time;
+
+ let config_format = &self.config.borrow().time_format;
+
+ if &self.format_string != config_format {
+ self.format_string = config_format.clone();
+ self.format_parsed = StrftimeItems::new_lenient(&config_format)
+ .parse_to_owned()
+ .unwrap();
+ }
+
Task::none()
}
Message::OpenSettings => {
@@ -128,18 +148,11 @@ impl Fjordgard {
}
fn view_main(&self) -> Element<Message> {
- let config = self.config.borrow();
- let dt = self.time.format(&config.time_format);
- let mut time_text = String::new();
-
- if let Err(_) = dt.write_to(&mut time_text) {
- time_text = String::from("Invalid time format")
- }
-
let mut bold = Font::DEFAULT;
bold.weight = Weight::Bold;
- let time_widget = text(time_text)
+ let time_text = self.time.format_with_items(self.format_parsed.iter());
+ let time_widget = text(time_text.to_string())
.size(100)
.font(bold)
.width(Length::Fill)
diff --git a/src/settings.rs b/src/settings.rs
@@ -1,7 +1,7 @@
use std::{cell::RefCell, rc::Rc};
use iced::{
- Element, Length, Task,
+ Background, Border, Color, Element, Length, Task, Theme,
widget::{
button, column, combo_box, container, row, scrollable, text, text_input, vertical_space,
},
@@ -106,8 +106,8 @@ impl Settings {
pub fn update(&mut self, msg: Message) -> Task<Message> {
match msg {
- Message::Location(location) => {
- self.location = location;
+ Message::TimeFormat(format) => {
+ self.time_format = format;
Task::none()
}
Message::BackgroundMode(mode) => {
@@ -115,11 +115,64 @@ impl Settings {
self.background_mode = mode;
Task::none()
}
+ Message::Background(background) => {
+ self.background = background;
+ Task::none()
+ }
+ Message::Location(location) => {
+ self.location = location;
+ Task::none()
+ }
+ Message::Name(name) => {
+ self.name = name;
+ Task::none()
+ }
+ Message::Latitude(latitude) => {
+ self.latitude = latitude;
+ Task::none()
+ }
+ Message::Longitude(longitude) => {
+ self.longitude = longitude;
+ Task::none()
+ }
_ => Task::none(),
}
}
pub fn view(&self) -> Element<Message> {
+ let (latitude, longitude, name) = match self.location {
+ WeatherLocation::Disabled => (None, None, None),
+ WeatherLocation::LocationName => (None, None, Some(Message::Name)),
+ WeatherLocation::Coordinates => {
+ (Some(Message::Latitude), Some(Message::Longitude), None)
+ }
+ };
+
+ let mut save_message = Some(Message::Save);
+
+ let color_style = if self.background_mode == BackgroundMode::Solid
+ && Color::parse(&self.background).is_none()
+ {
+ save_message = None;
+ text_input_error
+ } else {
+ text_input::default
+ };
+
+ let latitude_style = if self.latitude.parse::<f64>().is_ok() || latitude.is_none() {
+ text_input::default
+ } else {
+ save_message = None;
+ text_input_error
+ };
+
+ let longitude_style = if self.longitude.parse::<f64>().is_ok() || longitude.is_none() {
+ text_input::default
+ } else {
+ save_message = None;
+ text_input_error
+ };
+
let mut background_mode_row =
row![text(self.background_mode.edit_text()).width(Length::FillPortion(1))];
@@ -139,18 +192,11 @@ impl Settings {
background_mode_row = background_mode_row.push(
text_input(self.background_mode.default_background(), &self.background)
.on_input(Message::Background)
- .width(Length::FillPortion(2)),
+ .width(Length::FillPortion(2))
+ .style(color_style),
);
}
- let (latitude, longitude, name) = match self.location {
- WeatherLocation::Disabled => (None, None, None),
- WeatherLocation::LocationName => (None, None, Some(Message::Name)),
- WeatherLocation::Coordinates => {
- (Some(Message::Latitude), Some(Message::Longitude), None)
- }
- };
-
let mut results = column![];
for res in self.location_results.iter() {
@@ -193,12 +239,14 @@ impl Settings {
text_input("", &self.latitude)
.width(Length::FillPortion(2))
.on_input_maybe(latitude)
+ .style(latitude_style)
],
row![
text("Longitude").width(Length::FillPortion(1)),
text_input("", &self.longitude)
.width(Length::FillPortion(2))
.on_input_maybe(longitude)
+ .style(longitude_style)
],
row![
text("Location").width(Length::FillPortion(1)),
@@ -211,7 +259,7 @@ impl Settings {
64.0 * (self.location_results.len().clamp(0, 1) as f32)
))
.width(Length::Fill),
- button("Save").on_press(Message::Save)
+ button("Save").on_press_maybe(save_message)
]
.spacing(10),
)
@@ -220,3 +268,43 @@ impl Settings {
.into()
}
}
+
+fn text_input_error(theme: &Theme, status: text_input::Status) -> text_input::Style {
+ let palette = theme.extended_palette();
+
+ let active = text_input::Style {
+ background: Background::Color(palette.danger.weak.color),
+ border: Border {
+ radius: 2.0.into(),
+ width: 1.0,
+ color: palette.danger.strong.color,
+ },
+ icon: palette.danger.weak.text,
+ placeholder: palette.danger.strong.color,
+ value: palette.danger.weak.text,
+ selection: palette.danger.strong.color,
+ };
+
+ match status {
+ text_input::Status::Active => active,
+ text_input::Status::Hovered => text_input::Style {
+ border: Border {
+ color: palette.danger.base.text,
+ ..active.border
+ },
+ ..active
+ },
+ text_input::Status::Focused => text_input::Style {
+ border: Border {
+ color: palette.background.strong.color,
+ ..active.border
+ },
+ ..active
+ },
+ text_input::Status::Disabled => text_input::Style {
+ background: Background::Color(palette.danger.weak.color),
+ value: active.placeholder,
+ ..active
+ },
+ }
+}